diff --git a/drongo b/drongo index 601c11bd..81378b28 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 601c11bd50405cc781bc35f8c680ba4c5e48ae91 +Subproject commit 81378b28b25d02dca8cdfc21a6b4fae0421d82b1 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 87b97701..8ea162f4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -515,6 +515,7 @@ public class AppController implements Initializable { tab.setContent(walletLoader.load()); WalletController controller = walletLoader.getController(); WalletForm walletForm = new WalletForm(storage, wallet); + EventManager.get().register(walletForm); controller.setWalletForm(walletForm); if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) { diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java new file mode 100644 index 00000000..f2fd8fd0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java @@ -0,0 +1,19 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletNode; + +import java.util.List; + +public class WalletHistoryChangedEvent extends WalletChangedEvent { + private final List historyChangedNodes; + + public WalletHistoryChangedEvent(Wallet wallet, List historyChangedNodes) { + super(wallet); + this.historyChangedNodes = historyChangedNodes; + } + + public List getHistoryChangedNodes() { + return historyChangedNodes; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java index 073ec494..f1e10f5e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java @@ -127,33 +127,47 @@ public class ElectrumServer { } public Map> getHistory(Wallet wallet) throws ServerException { - Map> nodeTransactionMap = new HashMap<>(); - getHistory(wallet, KeyPurpose.RECEIVE, nodeTransactionMap); - getHistory(wallet, KeyPurpose.CHANGE, nodeTransactionMap); + Map> receiveTransactionMap = new TreeMap<>(); + getHistory(wallet, KeyPurpose.RECEIVE, receiveTransactionMap); - return nodeTransactionMap; + Map> changeTransactionMap = new TreeMap<>(); + getHistory(wallet, KeyPurpose.CHANGE, changeTransactionMap); + + receiveTransactionMap.putAll(changeTransactionMap); + return receiveTransactionMap; } public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map> nodeTransactionMap) throws ServerException { - getHistory(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap); - //Not necessary, mempool transactions included in history - //getMempool(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap); + WalletNode purposeNode = wallet.getNode(keyPurpose); + getHistory(wallet, purposeNode.getChildren(), nodeTransactionMap, 0); + + int historySize = purposeNode.getChildren().size(); + int gapLimitSize = nodeTransactionMap.size() + Wallet.DEFAULT_LOOKAHEAD; + while(historySize < gapLimitSize) { + purposeNode.fillToIndex(gapLimitSize - 1); + getHistory(wallet, purposeNode.getChildren(), nodeTransactionMap, historySize); + historySize = purposeNode.getChildren().size(); + gapLimitSize = nodeTransactionMap.size() + Wallet.DEFAULT_LOOKAHEAD; + } } - public void getHistory(Wallet wallet, Collection nodes, Map> nodeTransactionMap) throws ServerException { - getReferences(wallet, "blockchain.scripthash.get_history", nodes, nodeTransactionMap); + public void getHistory(Wallet wallet, Collection nodes, Map> nodeTransactionMap, int startIndex) throws ServerException { + getReferences(wallet, "blockchain.scripthash.get_history", nodes, nodeTransactionMap, startIndex); } - public void getMempool(Wallet wallet, Collection nodes, Map> nodeTransactionMap) throws ServerException { - getReferences(wallet, "blockchain.scripthash.get_mempool", nodes, nodeTransactionMap); + public void getMempool(Wallet wallet, Collection nodes, Map> nodeTransactionMap, int startIndex) throws ServerException { + getReferences(wallet, "blockchain.scripthash.get_mempool", nodes, nodeTransactionMap, startIndex); } - public void getReferences(Wallet wallet, String method, Collection nodes, Map> nodeTransactionMap) throws ServerException { + public void getReferences(Wallet wallet, String method, Collection nodes, Map> nodeTransactionMap, int startIndex) throws ServerException { try { JsonRpcClient client = new JsonRpcClient(getTransport()); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class); + for(WalletNode node : nodes) { - batchRequest.add(node.getDerivationPath(), method, getScriptHash(wallet, node)); + if(node.getIndex() >= startIndex) { + batchRequest.add(node.getDerivationPath(), method, getScriptHash(wallet, node)); + } } Map result; @@ -392,16 +406,17 @@ public class ElectrumServer { public void calculateNodeHistory(Wallet wallet, Map> nodeTransactionMap, WalletNode node) { Set transactionOutputs = new TreeSet<>(); + //First check all provided txes that pay to this node Script nodeScript = wallet.getOutputScript(node); Set history = nodeTransactionMap.get(node); for(BlockTransactionHash reference : history) { BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash()); - if (blockTransaction == null || blockTransaction.equals(UNFETCHABLE_BLOCK_TRANSACTION)) { + if(blockTransaction == null || blockTransaction.equals(UNFETCHABLE_BLOCK_TRANSACTION)) { throw new IllegalStateException("Could not retrieve transaction for hash " + reference.getHashAsString()); } Transaction transaction = blockTransaction.getTransaction(); - for (int outputIndex = 0; outputIndex < transaction.getOutputs().size(); outputIndex++) { + for(int outputIndex = 0; outputIndex < transaction.getOutputs().size(); outputIndex++) { TransactionOutput output = transaction.getOutputs().get(outputIndex); if (output.getScript().equals(nodeScript)) { BlockTransactionHashIndex receivingTXO = new BlockTransactionHashIndex(reference.getHash(), reference.getHeight(), blockTransaction.getDate(), reference.getFee(), output.getIndex(), output.getValue()); @@ -410,9 +425,10 @@ public class ElectrumServer { } } + //Then check all provided txes that pay from this node for(BlockTransactionHash reference : history) { BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash()); - if (blockTransaction == null || blockTransaction.equals(UNFETCHABLE_BLOCK_TRANSACTION)) { + if(blockTransaction == null || blockTransaction.equals(UNFETCHABLE_BLOCK_TRANSACTION)) { throw new IllegalStateException("Could not retrieve transaction for hash " + reference.getHashAsString()); } Transaction transaction = blockTransaction.getTransaction(); @@ -443,7 +459,7 @@ public class ElectrumServer { BlockTransactionHashIndex spendingTXI = new BlockTransactionHashIndex(reference.getHash(), reference.getHeight(), blockTransaction.getDate(), reference.getFee(), inputIndex, spentOutput.getValue()); BlockTransactionHashIndex spentTXO = new BlockTransactionHashIndex(spentTxHash.getHash(), spentTxHash.getHeight(), previousTransaction.getDate(), spentTxHash.getFee(), spentOutput.getIndex(), spentOutput.getValue(), spendingTXI); - Optional optionalReference = transactionOutputs.stream().filter(receivedTXO -> receivedTXO.equals(spentTXO)).findFirst(); + Optional optionalReference = transactionOutputs.stream().filter(receivedTXO -> receivedTXO.getHash().equals(spentTXO.getHash()) && receivedTXO.getIndex() == spentTXO.getIndex()).findFirst(); if(optionalReference.isEmpty()) { throw new IllegalStateException("Found spent transaction output " + spentTXO + " but no record of receiving it"); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 2350566f..6518f55f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -32,6 +32,8 @@ import tornadofx.control.Fieldset; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.ResourceBundle; import java.util.stream.Collectors; @@ -72,6 +74,8 @@ public class SettingsController extends WalletFormController implements Initiali private final SimpleIntegerProperty totalKeystores = new SimpleIntegerProperty(0); + private boolean initialising = true; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -90,9 +94,10 @@ public class SettingsController extends WalletFormController implements Initiali scriptType.getSelectionModel().select(policyType.getDefaultScriptType()); } - if(oldValue != null) { + if(!initialising) { clearKeystoreTabs(); } + initialising = false; multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI)); if(policyType.equals(PolicyType.MULTI)) { @@ -133,7 +138,9 @@ public class SettingsController extends WalletFormController implements Initiali keystore.setWalletModel(WalletModel.SPARROW); walletForm.getWallet().getKeystores().add(keystore); } - walletForm.getWallet().setKeystores(walletForm.getWallet().getKeystores().subList(0, numCosigners.intValue())); + List newKeystoreList = new ArrayList<>(walletForm.getWallet().getKeystores().subList(0, numCosigners.intValue())); + walletForm.getWallet().getKeystores().clear(); + walletForm.getWallet().getKeystores().addAll(newKeystoreList); for(int i = 0; i < walletForm.getWallet().getKeystores().size(); i++) { Keystore keystore = walletForm.getWallet().getKeystores().get(i); @@ -155,7 +162,8 @@ public class SettingsController extends WalletFormController implements Initiali keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs()); totalKeystores.unbind(); totalKeystores.setValue(0); - walletForm.revertAndRefresh(); + walletForm.revert(); + initialising = true; setFieldsFromWallet(walletForm.getWallet()); }); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java new file mode 100644 index 00000000..79471a01 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java @@ -0,0 +1,34 @@ +package com.sparrowwallet.sparrow.wallet; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.io.Storage; + +import java.io.IOException; + +public class SettingsWalletForm extends WalletForm { + private Wallet walletCopy; + + public SettingsWalletForm(Storage storage, Wallet currentWallet) { + super(storage, currentWallet); + this.walletCopy = currentWallet.copy(); + } + + @Override + public Wallet getWallet() { + return walletCopy; + } + + @Override + public void revert() { + this.walletCopy = super.getWallet().copy(); + } + + @Override + public void saveAndRefresh() throws IOException { + //TODO: Detect trivial changes and don't clear history + walletCopy.clearHistory(); + wallet = walletCopy.copy(); + save(); + refreshHistory(wallet.getStoredBlockHeight()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index aa90298b..fd97b053 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -60,7 +60,13 @@ public class WalletController extends WalletFormController implements Initializa Node walletFunction = functionLoader.load(); walletFunction.setUserData(function); WalletFormController controller = functionLoader.getController(); - controller.setWalletForm(getWalletForm()); + + WalletForm walletForm = getWalletForm(); + if(function.equals(Function.SETTINGS)) { + walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet()); + } + + controller.setWalletForm(walletForm); walletFunction.setViewOrder(1); walletPane.getChildren().add(walletFunction); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 9400e695..b10d5c1f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -4,32 +4,30 @@ import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.NewBlockEvent; import com.sparrowwallet.sparrow.event.WalletChangedEvent; +import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent; import com.sparrowwallet.sparrow.io.ElectrumServer; import com.sparrowwallet.sparrow.io.Storage; +import javafx.application.Platform; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; public class WalletForm { private final Storage storage; - private Wallet oldWallet; - private Wallet wallet; + protected Wallet wallet; private final List accountEntries = new ArrayList<>(); public WalletForm(Storage storage, Wallet currentWallet) { this.storage = storage; - this.oldWallet = currentWallet.copy(); this.wallet = currentWallet; - refreshHistory(); - - EventManager.get().register(this); + refreshHistory(wallet.getStoredBlockHeight()); } public Wallet getWallet() { @@ -44,41 +42,60 @@ public class WalletForm { return storage.getWalletFile(); } - public void revertAndRefresh() { - this.wallet = oldWallet.copy(); - refreshHistory(); + public void revert() { + throw new UnsupportedOperationException("Only SettingsWalletForm supports revert"); } public void save() throws IOException { storage.storeWallet(wallet); - oldWallet = wallet.copy(); } public void saveAndRefresh() throws IOException { - //TODO: Detect trivial changes and don't clear history wallet.clearHistory(); save(); - refreshHistory(); + refreshHistory(wallet.getStoredBlockHeight()); } - public void refreshHistory() { - if(wallet.isValid()) { + public void refreshHistory(Integer blockHeight) { + Wallet previousWallet = wallet.copy(); + if(wallet.isValid() && AppController.isOnline()) { ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet); historyService.setOnSucceeded(workerStateEvent -> { - //TODO: Show connected - try { - storage.storeWallet(wallet); - } catch (IOException e) { - e.printStackTrace(); - } + wallet.setStoredBlockHeight(blockHeight); + notifyIfHistoryChanged(previousWallet); }); historyService.setOnFailed(workerStateEvent -> { - //TODO: Show not connected, log exception + workerStateEvent.getSource().getException().printStackTrace(); }); historyService.start(); } } + private void notifyIfHistoryChanged(Wallet previousWallet) { + List historyChangedNodes = new ArrayList<>(); + historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren())); + historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren())); + + if(!historyChangedNodes.isEmpty()) { + Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, historyChangedNodes))); + } + } + + private List getHistoryChangedNodes(Set previousNodes, Set currentNodes) { + List changedNodes = new ArrayList<>(); + for(WalletNode currentNode : currentNodes) { + Optional optPreviousNode = previousNodes.stream().filter(node -> node.equals(currentNode)).findFirst(); + if(optPreviousNode.isPresent()) { + WalletNode previousNode = optPreviousNode.get(); + if(!currentNode.getTransactionOutputs().equals(previousNode.getTransactionOutputs())) { + changedNodes.add(currentNode); + } + } + } + + return changedNodes; + } + public NodeEntry getNodeEntry(KeyPurpose keyPurpose) { NodeEntry purposeEntry; Optional optionalPurposeEntry = accountEntries.stream().filter(entry -> entry.getNode().getKeyPurpose().equals(keyPurpose)).findFirst(); @@ -123,7 +140,11 @@ public class WalletForm { @Subscribe public void newBlock(NewBlockEvent event) { - refreshHistory(); - wallet.setStoredBlockHeight(event.getHeight()); + refreshHistory(event.getHeight()); + } + + @Subscribe + public void connected(ConnectionEvent event) { + refreshHistory(event.getBlockHeight()); } }