From 1be9ac1072e2e7920998b8ac17a594d994fea97a Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 14 May 2026 12:40:02 +0200 Subject: [PATCH] hide wallet rescan hyperlink when nothing further can be scanned --- drongo | 2 +- .../sparrow/control/CoinTreeTable.java | 42 ++++++++++++++++++- .../sparrow/net/ElectrumServer.java | 29 +++++++++++++ .../sparrow/net/SilentPaymentsScanCache.java | 4 ++ .../sparrow/net/cormorant/Cormorant.java | 4 ++ .../cormorant/bitcoind/BitcoindClient.java | 9 +++- .../sparrow/wallet/WalletForm.java | 16 +------ 7 files changed, 87 insertions(+), 19 deletions(-) diff --git a/drongo b/drongo index 29cd4b79..e4b63fbd 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 29cd4b7909a2d891107af55b1d4d6c2690cfa2ab +Subproject commit e4b63fbd19e9df6daf749bae02c5e02523c64ce4 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index 6688c43f..2eb337f1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -16,7 +16,10 @@ import com.sparrowwallet.sparrow.event.WalletDataChangedEvent; import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; +import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ServerType; +import com.sparrowwallet.sparrow.net.cormorant.Cormorant; +import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient; import com.sparrowwallet.sparrow.wallet.Entry; import io.reactivex.Observable; import io.reactivex.subjects.PublishSubject; @@ -107,7 +110,7 @@ public class CoinTreeTable extends TreeTableView { setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage())); } else if(event.isLoading()) { if(event.getStatusMessage() != null) { - setPlaceholder(new Label(event.getStatusMessage() + "...")); + setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "..."))); } else { setPlaceholder(new Label("Loading transactions...")); } @@ -123,7 +126,7 @@ public class CoinTreeTable extends TreeTableView { StackPane stackPane = new StackPane(); stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions")); - if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting()) { + if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) { Hyperlink hyperlink = new Hyperlink(); hyperlink.setTranslateY(30); hyperlink.setOnAction(event -> { @@ -150,12 +153,47 @@ public class CoinTreeTable extends TreeTableView { } stackPane.getChildren().add(hyperlink); + } else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) { + Date prunedDate = getPrunedDate(); + if(prunedDate != null) { + DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN); + Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate)); + prunedLabel.setTranslateY(30); + stackPane.getChildren().add(prunedLabel); + } } stackPane.setAlignment(Pos.CENTER); return stackPane; } + private boolean isFullyScanned(Wallet wallet) { + if(wallet.getPolicyType() == PolicyType.SINGLE_SP) { + return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress()); + } + + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + Date prunedDate = getPrunedDate(); + return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate); + } + + return false; + } + + private static Date getPrunedDate() { + Cormorant cormorant = ElectrumServer.getCormorant(); + if(cormorant == null) { + return null; + } + + BitcoindClient bitcoindClient = cormorant.getBitcoindClient(); + if(bitcoindClient == null || !bitcoindClient.isPruned()) { + return null; + } + + return bitcoindClient.getCachedPrunedDate(); + } + protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) { WalletTable.Sort columnSort = getSavedColumnSort(); if(columnSort == null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index c1187a39..729caa7f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -89,6 +89,8 @@ public class ElectrumServer { private static final Map spScanCaches = new ConcurrentHashMap<>(); + private static final int TAPROOT_ACTIVATION_HEIGHT = 709632; + private final static Map subscribedRecent = new ConcurrentHashMap<>(); private final static Map broadcastRecent = new ConcurrentHashMap<>(); @@ -1377,6 +1379,33 @@ public class ElectrumServer { return spScanCaches.containsKey(scanAddress.getAddress()); } + public static boolean isSilentPaymentsFullyCovered(SilentPaymentScanAddress scanAddress) { + if(scanAddress == null) { + return false; + } + + SilentPaymentsScanCache cache = spScanCaches.get(scanAddress.getAddress()); + if(cache == null) { + return false; + } + + cache.lock(); + try { + Integer serverStart = cache.getServerStart(); + if(!cache.isCompleted() || serverStart == null) { + return false; + } + int earliestPossibleStart = Network.get() == Network.MAINNET ? TAPROOT_ACTIVATION_HEIGHT : 0; + return serverStart <= earliestPossibleStart; + } finally { + cache.unlock(); + } + } + + public static Cormorant getCormorant() { + return cormorant; + } + private static void cancelSilentPaymentScans() { for(SilentPaymentsScanCache cache : spScanCaches.values()) { cache.lock(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsScanCache.java b/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsScanCache.java index 05035cf7..48b215ed 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsScanCache.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SilentPaymentsScanCache.java @@ -43,6 +43,10 @@ class SilentPaymentsScanCache { return state == State.CANCELLED; } + boolean isCompleted() { + return state == State.COMPLETED; + } + void cancel() { assert lock.isHeldByCurrentThread(); if(state == State.SCANNING) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java index a135397b..19c22708 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java @@ -115,4 +115,8 @@ public class Cormorant { public static EventBus getEventBus() { return EVENT_BUS; } + + public BitcoindClient getBitcoindClient() { + return bitcoindClient; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java index 70c32d71..1f3942ae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java @@ -77,6 +77,7 @@ public class BitcoindClient { private final boolean useWallets; private boolean pruned; private Integer pruneHeight; + private volatile Date cachedPrunedDate; private boolean legacyWalletExists; private final Lock syncingLock = new ReentrantLock(); @@ -285,12 +286,18 @@ public class BitcoindClient { if(blockchainInfo.pruned()) { String pruneBlockHash = getBitcoindService().getBlockHash(blockchainInfo.pruneheight()); VerboseBlockHeader pruneBlockHeader = getBitcoindService().getBlockHeader(pruneBlockHash); - return Optional.of(new Date(pruneBlockHeader.time() * 1000)); + Date prunedDate = new Date(pruneBlockHeader.time() * 1000); + cachedPrunedDate = prunedDate; + return Optional.of(prunedDate); } return Optional.empty(); } + public Date getCachedPrunedDate() { + return cachedPrunedDate; + } + private ScanDate getScanDate(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose, Date earliestBirthDate) { Integer range = (keyPurpose == null ? null : Math.max(getWalletRange(normalizedDescriptor, wallet, keyPurpose), getDefaultRange(wallet, keyPurpose))); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index f0a08cef..305a193e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -228,7 +228,7 @@ public class WalletForm { boolean shouldHold = !spSubscriptionHeld; - ElectrumServer.SilentPaymentScanService scanService = new ElectrumServer.SilentPaymentScanService(wallet, shouldHold, computeNeededStart(wallet)); + ElectrumServer.SilentPaymentScanService scanService = new ElectrumServer.SilentPaymentScanService(wallet, shouldHold, wallet.getNeededScanStart()); scanService.setOnSucceeded(workerStateEvent -> { spScanInProgress = false; spSubscriptionHeld = true; @@ -296,20 +296,6 @@ public class WalletForm { } } - private static int computeNeededStart(Wallet wallet) { - Integer stored = wallet.getStoredBlockHeight(); - if(stored != null && stored > 0) { - return Math.max(0, stored - BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM); - } - if(wallet.getBirthHeight() != null) { - return Math.max(0, wallet.getBirthHeight() - BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM); - } - if(wallet.getBirthDate() != null) { - return (int)(wallet.getBirthDate().getTime() / 1000L); - } - return 0; - } - private void updateWallets(Integer blockHeight, Wallet previousWallet) { List nestedHistoryChangedNodes = new ArrayList<>(); for(Wallet childWallet : new ArrayList<>(wallet.getChildWallets())) {