hide wallet rescan hyperlink when nothing further can be scanned
What changed, and why it matters
This commit is a user-interface polish change for the Sparrow Wallet desktop app. It hides the 'Rescan Wallet' hyperlink when the wallet has already been scanned as far back as possible, and shows a helpful label instead. There is no indication it fixes a security vulnerability.
No security action required. Treat as a normal feature/UX improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch adjusts the empty-state placeholder in CoinTreeTable so that the rescan hyperlink is not offered when a wallet is fully scanned. It adds helper methods to determine if a wallet is fully scanned: for Silent Payments, by checking whether the scan cache has completed and reached the Taproot activation height; for Bitcoin Core pruned nodes, by comparing the wallet birth date to the pruned start date. It also caches the pruned date in BitcoindClient and exposes it, and replaces a local computeNeededStart() method with wallet.getNeededScanStart(). The changes are UI/UX and state-cleanup focused, not security fixes.
Changed components
CoinTreeTable UI placeholderElectrumServer silent-payment scan helpersSilentPaymentsScanCache completion stateCormorant/BitcoindClient pruned date cachingWalletForm scan service start computationInspect captured patch +87 / −19
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java
index 6688c43..2eb337f 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<Entry> {
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<Entry> {
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<Entry> {
}
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 c1187a3..729caa7 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<String, SilentPaymentsScanCache> spScanCaches = new ConcurrentHashMap<>();
+ private static final int TAPROOT_ACTIVATION_HEIGHT = 709632;
+
private final static Map<String, Integer> subscribedRecent = new ConcurrentHashMap<>();
private final static Map<String, String> 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 05035cf..48b215e 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 a135397..19c2270 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 70c32d7..1f3942a 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 f0a08ce..305a193 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<WalletNode> nestedHistoryChangedNodes = new ArrayList<>();
for(Wallet childWallet : new ArrayList<>(wallet.getChildWallets())) {
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.