diff --git a/drongo b/drongo index f88e3d44..a25d020e 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit f88e3d442357e514421b993155062f7ea8177d06 +Subproject commit a25d020e54543cd48be1501f4390cff4c8daeee1 diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java index ab05c008..f5453569 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java @@ -5,11 +5,8 @@ import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder; import com.google.common.net.HostAndPort; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Utils; -import com.sparrowwallet.drongo.protocol.Sha256Hash; -import com.sparrowwallet.drongo.protocol.Transaction; -import com.sparrowwallet.drongo.wallet.BlockchainTransactionHash; -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.drongo.protocol.*; +import com.sparrowwallet.drongo.wallet.*; import javafx.concurrent.Service; import javafx.concurrent.Task; import org.jetbrains.annotations.NotNull; @@ -64,25 +61,28 @@ public class ElectrumServer { return serverVersion.get(1); } - public void getHistory(Wallet wallet) throws ServerException { - getHistory(wallet, KeyPurpose.RECEIVE); - getHistory(wallet, KeyPurpose.CHANGE); + public Map> getHistory(Wallet wallet) throws ServerException { + Map> nodeTransactionMap = new HashMap<>(); + getHistory(wallet, KeyPurpose.RECEIVE, nodeTransactionMap); + getHistory(wallet, KeyPurpose.CHANGE, nodeTransactionMap); + + return nodeTransactionMap; } - public void getHistory(Wallet wallet, KeyPurpose keyPurpose) throws ServerException { - getHistory(wallet, wallet.getNode(keyPurpose).getChildren()); - getMempool(wallet, wallet.getNode(keyPurpose).getChildren()); + public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map> nodeTransactionMap) throws ServerException { + getHistory(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap); + getMempool(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap); } - public void getHistory(Wallet wallet, Collection nodes) throws ServerException { - getReferences(wallet, "blockchain.scripthash.get_history", nodes); + public void getHistory(Wallet wallet, Collection nodes, Map> nodeTransactionMap) throws ServerException { + getReferences(wallet, "blockchain.scripthash.get_history", nodes, nodeTransactionMap); } - public void getMempool(Wallet wallet, Collection nodes) throws ServerException { - getReferences(wallet, "blockchain.scripthash.get_mempool", nodes); + public void getMempool(Wallet wallet, Collection nodes, Map> nodeTransactionMap) throws ServerException { + getReferences(wallet, "blockchain.scripthash.get_mempool", nodes, nodeTransactionMap); } - public void getReferences(Wallet wallet, String method, Collection nodes) throws ServerException { + public void getReferences(Wallet wallet, String method, Collection nodes, Map> nodeTransactionMap) throws ServerException { try { JsonRpcClient client = new JsonRpcClient(getTransport()); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class); @@ -97,16 +97,22 @@ public class ElectrumServer { Optional optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst(); if(optionalNode.isPresent()) { WalletNode node = optionalNode.get(); - Set references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).collect(Collectors.toSet()); - for(BlockchainTransactionHash reference : references) { - if(!node.getHistory().add(reference)) { - Optional optionalReference = node.getHistory().stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst(); - if(optionalReference.isPresent()) { - BlockchainTransactionHash existingReference = optionalReference.get(); - if(existingReference.getHeight() < reference.getHeight()) { - node.getHistory().remove(existingReference); - node.getHistory().add(reference); + Set references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).collect(Collectors.toCollection(TreeSet::new)); + Set existingReferences = nodeTransactionMap.get(node); + + if(existingReferences == null && !references.isEmpty()) { + nodeTransactionMap.put(node, references); + } else { + for(BlockchainTransactionHash reference : references) { + if(!existingReferences.add(reference)) { + Optional optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst(); + if(optionalReference.isPresent()) { + BlockchainTransactionHash existingReference = optionalReference.get(); + if(existingReference.getHeight() < reference.getHeight()) { + existingReferences.remove(existingReference); + existingReferences.add(reference); + } } } } @@ -120,24 +126,27 @@ public class ElectrumServer { } } - public void getReferencedTransactions(Wallet wallet) throws ServerException { - getReferencedTransactions(wallet, KeyPurpose.RECEIVE); - getReferencedTransactions(wallet, KeyPurpose.CHANGE); - } - - public void getReferencedTransactions(Wallet wallet, KeyPurpose keyPurpose) throws ServerException { - WalletNode purposeNode = wallet.getNode(keyPurpose); - Set references = new HashSet<>(); - for(WalletNode addressNode : purposeNode.getChildren()) { - references.addAll(addressNode.getHistory()); + public void getReferencedTransactions(Wallet wallet, Map> nodeTransactionMap) throws ServerException { + Set references = new TreeSet<>(); + for(Set nodeReferences : nodeTransactionMap.values()) { + references.addAll(nodeReferences); } - Map transactionMap = getTransactions(references); - wallet.getTransactions().putAll(transactionMap); + Map transactionMap = getTransactions(references); + for(Sha256Hash hash : transactionMap.keySet()) { + if(wallet.getTransactions().get(hash) == null) { + wallet.getTransactions().put(hash, transactionMap.get(hash)); + } else if(wallet.getTransactions().get(hash).getHeight() <= 0) { + transactionMap.get(hash).setLabel(wallet.getTransactions().get(hash).getLabel()); + wallet.getTransactions().put(hash, transactionMap.get(hash)); + } + } } - public Map getTransactions(Set references) throws ServerException { + public Map getTransactions(Set references) throws ServerException { try { + Set checkReferences = new TreeSet<>(references); + JsonRpcClient client = new JsonRpcClient(getTransport()); BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class); for(BlockchainTransactionHash reference : references) { @@ -145,12 +154,25 @@ public class ElectrumServer { } Map result = batchRequest.execute(); - Map transactionMap = new HashMap<>(); + Map transactionMap = new HashMap<>(); for(String txid : result.keySet()) { Sha256Hash hash = Sha256Hash.wrap(txid); byte[] rawtx = Utils.hexToBytes(result.get(txid)); Transaction transaction = new Transaction(rawtx); - transactionMap.put(hash, transaction); + + Optional optionalReference = references.stream().filter(reference -> reference.getHash().equals(hash)).findFirst(); + if(optionalReference.isEmpty()) { + throw new IllegalStateException("Returned transaction " + hash.toString() + " that was not requested"); + } + BlockchainTransactionHash reference = optionalReference.get(); + BlockchainTransaction blockchainTransaction = new BlockchainTransaction(reference.getHash(), reference.getHeight(), reference.getFee(), transaction); + + transactionMap.put(hash, blockchainTransaction); + checkReferences.remove(reference); + } + + if(!checkReferences.isEmpty()) { + throw new IllegalStateException("Could not retrieve transactions " + checkReferences); } return transactionMap; @@ -161,6 +183,74 @@ public class ElectrumServer { } } + public void calculateNodeHistory(Wallet wallet, Map> nodeTransactionMap) { + for(WalletNode node : nodeTransactionMap.keySet()) { + calculateNodeHistory(wallet, nodeTransactionMap, node); + } + } + + public void calculateNodeHistory(Wallet wallet, Map> nodeTransactionMap, WalletNode node) { + Script nodeScript = wallet.getOutputScript(node); + Set history = nodeTransactionMap.get(node); + for(BlockchainTransactionHash reference : history) { + BlockchainTransaction blockchainTransaction = wallet.getTransactions().get(reference.getHash()); + if(blockchainTransaction == null) { + throw new IllegalStateException("Could not retrieve transaction for hash " + reference.getHashAsString()); + } + + Transaction transaction = blockchainTransaction.getTransaction(); + for(int inputIndex = 0; inputIndex < transaction.getInputs().size(); inputIndex++) { + TransactionInput input = transaction.getInputs().get(inputIndex); + Sha256Hash previousHash = input.getOutpoint().getHash(); + BlockchainTransaction previousTransaction = wallet.getTransactions().get(previousHash); + if(previousTransaction == null) { + //No referenced transaction found, cannot check if spends from wallet + //This is fine so long as all referenced transactions have been returned, in which case this refers to a transaction that does not affect this wallet + continue; + } + + Optional optionalTxHash = history.stream().filter(txHash -> txHash.getHash().equals(previousHash)).findFirst(); + if(optionalTxHash.isEmpty()) { + //No previous transaction history found, cannot check if spends from wallet + //This is fine so long as all referenced transactions have been returned, in which case this refers to a transaction that does not affect this wallet node + continue; + } + + BlockchainTransactionHash spentTxHash = optionalTxHash.get(); + TransactionOutput spentOutput = previousTransaction.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex()); + if(spentOutput.getScript().equals(nodeScript)) { + BlockchainTransactionHashIndex spendingTXI = new BlockchainTransactionHashIndex(reference.getHash(), reference.getHeight(), reference.getFee(), inputIndex, spentOutput.getValue()); + BlockchainTransactionHashIndex spentTXO = new BlockchainTransactionHashIndex(spentTxHash.getHash(), spentTxHash.getHeight(), spentTxHash.getFee(), spentOutput.getIndex(), spentOutput.getValue(), spendingTXI); + + Optional optionalReference = node.getTransactionOutputs().stream().filter(receivedTXO -> receivedTXO.equals(spentTXO)).findFirst(); + if(optionalReference.isEmpty()) { + throw new IllegalStateException("Found spent transaction output " + spentTXO + " but no record of receiving it"); + } + + BlockchainTransactionHashIndex receivedTXO = optionalReference.get(); + receivedTXO.setSpentBy(spendingTXI); + } + } + + for(int outputIndex = 0; outputIndex < transaction.getOutputs().size(); outputIndex++) { + TransactionOutput output = transaction.getOutputs().get(outputIndex); + if(output.getScript().equals(nodeScript)) { + BlockchainTransactionHashIndex receivingTXO = new BlockchainTransactionHashIndex(reference.getHash(), reference.getHeight(), reference.getFee(), output.getIndex(), output.getValue()); + Optional optionalExistingTXO = node.getTransactionOutputs().stream().filter(txo -> txo.getHash().equals(receivingTXO.getHash()) && txo.getIndex() == receivingTXO.getIndex() && txo.getHeight() != receivingTXO.getHeight()).findFirst(); + if(optionalExistingTXO.isEmpty()) { + node.getTransactionOutputs().add(receivingTXO); + } else { + BlockchainTransactionHashIndex existingTXO = optionalExistingTXO.get(); + if(existingTXO.getHeight() < receivingTXO.getHeight()) { + node.getTransactionOutputs().remove(existingTXO); + node.getTransactionOutputs().add(receivingTXO); + } + } + } + } + } + } + private String getScriptHash(Wallet wallet, WalletNode node) { byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram()); byte[] reversed = Utils.reverseBytes(hash); @@ -174,16 +264,12 @@ public class ElectrumServer { public BlockchainTransactionHash getBlockchainTransactionHash() { Sha256Hash hash = Sha256Hash.wrap(tx_hash); - return new BlockchainTransactionHash(hash, height, fee); + return new BlockchainTransaction(hash, height, fee, null); } @Override public String toString() { - return "ScriptHashTx{" + - "height=" + height + - ", tx_hash='" + tx_hash + '\'' + - ", fee=" + fee + - '}'; + return "ScriptHashTx{height=" + height + ", tx_hash='" + tx_hash + '\'' + ", fee=" + fee + '}'; } } @@ -300,8 +386,9 @@ public class ElectrumServer { return new Task<>() { protected Boolean call() throws ServerException { ElectrumServer electrumServer = new ElectrumServer(); - electrumServer.getHistory(wallet); - electrumServer.getReferencedTransactions(wallet); + Map> nodeTransactionMap = electrumServer.getHistory(wallet); + electrumServer.getReferencedTransactions(wallet, nodeTransactionMap); + electrumServer.calculateNodeHistory(wallet, nodeTransactionMap); return true; } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 880b5f25..63158846 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -331,21 +331,22 @@ public class Storage { Iterator iter = children.iterator(); while(iter.hasNext()) { JsonObject childObject = (JsonObject)iter.next(); - if(childObject.get("children") != null && childObject.getAsJsonArray("children").size() == 0) { - childObject.remove("children"); - } + removeEmptyCollection(childObject, "children"); + removeEmptyCollection(childObject, "transactionOutputs"); - if(childObject.get("history") != null && childObject.getAsJsonArray("history").size() == 0) { - childObject.remove("history"); - } - - if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("history") == null) { + if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("transactionOutputs") == null) { iter.remove(); } } return jsonObject; } + + private void removeEmptyCollection(JsonObject jsonObject, String memberName) { + if(jsonObject.get(memberName) != null && jsonObject.getAsJsonArray(memberName).size() == 0) { + jsonObject.remove(memberName); + } + } } private static class NodeDeserializer implements JsonDeserializer { @@ -356,8 +357,8 @@ public class Storage { if(childNode.getChildren() == null) { childNode.setChildren(new TreeSet<>()); } - if(childNode.getHistory() == null) { - childNode.setHistory(new TreeSet<>()); + if(childNode.getTransactionOutputs() == null) { + childNode.setTransactionOutputs(new TreeSet<>()); } }