diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 81bfea33e..4a8393bc5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1010,6 +1010,7 @@ public void refreshWallet(ActionEvent event) { Wallet pastWallet = wallet.copy(); walletTabData.getStorage().backupTempWallet(); wallet.clearHistory(); + AppServices.clearTransactionHistoryCache(wallet); EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile())); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index a1394a2e9..a1a030f7a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -496,6 +496,10 @@ public static void addPayjoinURI(BitcoinURI bitcoinURI) { payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI); } + public static void clearTransactionHistoryCache(Wallet wallet) { + ElectrumServer.clearRetrievedScriptHashes(wallet); + } + public static Optional showWarningDialog(String title, String content, ButtonType... buttons) { return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 343ed3e18..e3bc5e25d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -44,6 +44,10 @@ public class ElectrumServer { private static final Map> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>()); + private static String previousServerAddress; + + private static Map retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>()); + private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc(); private static String bwtElectrumServer; @@ -84,6 +88,12 @@ private static synchronized Transport getTransport() throws ServerException { throw new ServerConfigException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString()); } + //If changing server, don't rely on previous transaction history + if(!electrumServer.equals(previousServerAddress)) { + retrievedScriptHashes.clear(); + } + previousServerAddress = electrumServer; + HostAndPort server = protocol.getServerHostAndPort(electrumServer); if(Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) { @@ -150,6 +160,11 @@ public static synchronized void closeActiveConnection() throws ServerException { } } + public static void clearRetrievedScriptHashes(Wallet wallet) { + wallet.getNode(KeyPurpose.RECEIVE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash)); + wallet.getNode(KeyPurpose.CHANGE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash)); + } + public Map> getHistory(Wallet wallet) throws ServerException { Map> receiveTransactionMap = new TreeMap<>(); getHistory(wallet, KeyPurpose.RECEIVE, receiveTransactionMap); @@ -177,6 +192,15 @@ public Map> getHistory(Wallet wallet, Coll Set newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet()); getReferencedTransactions(wallet, nodeTransactionMap); + //Subscribe and retrieve transaction history from child nodes if necessary to maintain gap limit + Set keyPurposes = nodes.stream().map(WalletNode::getKeyPurpose).collect(Collectors.toUnmodifiableSet()); + for(KeyPurpose keyPurpose : keyPurposes) { + WalletNode purposeNode = wallet.getNode(keyPurpose); + getHistoryToGapLimit(wallet, nodeTransactionMap, purposeNode); + } + + log.debug("Fetched nodes history for: " + nodeTransactionMap.keySet()); + if(!newReferences.isEmpty()) { //Look for additional nodes to fetch history for by considering the inputs and outputs of new transactions found log.debug(wallet.getName() + " found new transactions: " + newReferences); @@ -216,13 +240,22 @@ public Map> getHistory(Wallet wallet, Coll public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map> nodeTransactionMap) throws ServerException { WalletNode purposeNode = wallet.getNode(keyPurpose); - //Subscribe to all existing address WalletNodes and add them to nodeTransactionMap as keys to empty sets if they have history + //Subscribe to all existing address WalletNodes and add them to nodeTransactionMap as keys to empty sets if they have history that needs to be fetched subscribeWalletNodes(wallet, purposeNode.getChildren(), nodeTransactionMap, 0); //All WalletNode keys in nodeTransactionMap need to have their history fetched (nodes without history will not be keys in the map yet) getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, 0); //Fetch all referenced transaction to wallet transactions map. We do this now even though it is done again later to get it done before too many script hashes are subscribed getReferencedTransactions(wallet, nodeTransactionMap); + //Increase child nodes if necessary to maintain gap limit, and ensure they are subscribed and history is fetched + getHistoryToGapLimit(wallet, nodeTransactionMap, purposeNode); + + log.debug("Fetched history for: " + nodeTransactionMap.keySet()); + //Set the remaining WalletNode keys in nodeTransactionMap to empty sets to indicate no history (if no script hash history has already been retrieved in a previous call) + purposeNode.getChildren().stream().filter(node -> !nodeTransactionMap.containsKey(node) && retrievedScriptHashes.get(getScriptHash(wallet, node)) == null).forEach(node -> nodeTransactionMap.put(node, Collections.emptySet())); + } + + private void getHistoryToGapLimit(Wallet wallet, Map> nodeTransactionMap, WalletNode purposeNode) throws ServerException { //Because node children are added sequentially in WalletNode.fillToIndex, we can simply look at the number of children to determine the highest filled index int historySize = purposeNode.getChildren().size(); //The gap limit size takes the highest used index in the retrieved history and adds the gap limit (plus one to be comparable to the number of children since index is zero based) @@ -235,9 +268,6 @@ public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map !nodeTransactionMap.containsKey(node)).forEach(node -> nodeTransactionMap.put(node, Collections.emptySet())); } private int getGapLimitSize(Wallet wallet, Map> nodeTransactionMap) { @@ -319,9 +349,12 @@ public void subscribeWalletNodes(Wallet wallet, Collection nodes, Ma if(node != null && node.getIndex() >= startIndex) { String scriptHash = getScriptHash(wallet, node); - if(getSubscribedScriptHashStatus(scriptHash) != null) { - //Already subscribed, but still need to fetch history from a used node - nodeTransactionMap.put(node, new TreeSet<>()); + String subscribedStatus = getSubscribedScriptHashStatus(scriptHash); + if(subscribedStatus != null) { + //Already subscribed, but still need to fetch history from a used node if not previously fetched + if(!subscribedStatus.equals(retrievedScriptHashes.get(scriptHash))) { + nodeTransactionMap.put(node, new TreeSet<>()); + } } else if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) { //Unique script hash we are not yet subscribed to pathScriptHashes.put(node.getDerivationPath(), scriptHash); @@ -329,6 +362,8 @@ public void subscribeWalletNodes(Wallet wallet, Collection nodes, Ma } } + log.debug("Subscribe to: " + pathScriptHashes.keySet()); + if(pathScriptHashes.isEmpty()) { return; } @@ -343,8 +378,8 @@ public void subscribeWalletNodes(Wallet wallet, Collection nodes, Ma WalletNode node = optionalNode.get(); String scriptHash = getScriptHash(wallet, node); - //Check if there is history for this script hash - if(status != null) { + //Check if there is history for this script hash, and if the history has changed since last fetched + if(status != null && !status.equals(retrievedScriptHashes.get(scriptHash))) { //Set the value for this node to be an empty set to mark it as requiring a get_history RPC call for this wallet nodeTransactionMap.put(node, new TreeSet<>()); } @@ -1037,6 +1072,13 @@ protected Boolean call() throws ServerException { Map> nodeTransactionMap = (nodes == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, nodes)); electrumServer.getReferencedTransactions(wallet, nodeTransactionMap); electrumServer.calculateNodeHistory(wallet, nodeTransactionMap); + + //Add all of the script hashes we have now fetched the history for so we don't need to fetch again until the script hash status changes + for(WalletNode node : nodeTransactionMap.keySet()) { + String scriptHash = getScriptHash(wallet, node); + retrievedScriptHashes.put(scriptHash, getSubscribedScriptHashStatus(scriptHash)); + } + return true; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 053d169b0..ce291f2c1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -318,6 +318,7 @@ public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) { public void walletTabsClosed(WalletTabsClosedEvent event) { for(WalletTabData tabData : event.getClosedWalletTabData()) { if(tabData.getWalletForm() == this) { + AppServices.clearTransactionHistoryCache(wallet); EventManager.get().unregister(this); } }