mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
refactor transaction history storage
This commit is contained in:
parent
0836273c8f
commit
a6e340eef5
3 changed files with 147 additions and 59 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit f88e3d442357e514421b993155062f7ea8177d06
|
Subproject commit a25d020e54543cd48be1501f4390cff4c8daeee1
|
|
@ -5,11 +5,8 @@ import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockchainTransactionHash;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
@ -64,25 +61,28 @@ public class ElectrumServer {
|
||||||
return serverVersion.get(1);
|
return serverVersion.get(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getHistory(Wallet wallet) throws ServerException {
|
public Map<WalletNode, Set<BlockchainTransactionHash>> getHistory(Wallet wallet) throws ServerException {
|
||||||
getHistory(wallet, KeyPurpose.RECEIVE);
|
Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap = new HashMap<>();
|
||||||
getHistory(wallet, KeyPurpose.CHANGE);
|
getHistory(wallet, KeyPurpose.RECEIVE, nodeTransactionMap);
|
||||||
|
getHistory(wallet, KeyPurpose.CHANGE, nodeTransactionMap);
|
||||||
|
|
||||||
|
return nodeTransactionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getHistory(Wallet wallet, KeyPurpose keyPurpose) throws ServerException {
|
public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
getHistory(wallet, wallet.getNode(keyPurpose).getChildren());
|
getHistory(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap);
|
||||||
getMempool(wallet, wallet.getNode(keyPurpose).getChildren());
|
getMempool(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getHistory(Wallet wallet, Collection<WalletNode> nodes) throws ServerException {
|
public void getHistory(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
getReferences(wallet, "blockchain.scripthash.get_history", nodes);
|
getReferences(wallet, "blockchain.scripthash.get_history", nodes, nodeTransactionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getMempool(Wallet wallet, Collection<WalletNode> nodes) throws ServerException {
|
public void getMempool(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
getReferences(wallet, "blockchain.scripthash.get_mempool", nodes);
|
getReferences(wallet, "blockchain.scripthash.get_mempool", nodes, nodeTransactionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getReferences(Wallet wallet, String method, Collection<WalletNode> nodes) throws ServerException {
|
public void getReferences(Wallet wallet, String method, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
try {
|
try {
|
||||||
JsonRpcClient client = new JsonRpcClient(getTransport());
|
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||||
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
|
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
|
||||||
|
@ -97,16 +97,22 @@ public class ElectrumServer {
|
||||||
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
|
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
|
||||||
if(optionalNode.isPresent()) {
|
if(optionalNode.isPresent()) {
|
||||||
WalletNode node = optionalNode.get();
|
WalletNode node = optionalNode.get();
|
||||||
Set<BlockchainTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
for(BlockchainTransactionHash reference : references) {
|
Set<BlockchainTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).collect(Collectors.toCollection(TreeSet::new));
|
||||||
if(!node.getHistory().add(reference)) {
|
Set<BlockchainTransactionHash> existingReferences = nodeTransactionMap.get(node);
|
||||||
Optional<BlockchainTransactionHash> optionalReference = node.getHistory().stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst();
|
|
||||||
if(optionalReference.isPresent()) {
|
if(existingReferences == null && !references.isEmpty()) {
|
||||||
BlockchainTransactionHash existingReference = optionalReference.get();
|
nodeTransactionMap.put(node, references);
|
||||||
if(existingReference.getHeight() < reference.getHeight()) {
|
} else {
|
||||||
node.getHistory().remove(existingReference);
|
for(BlockchainTransactionHash reference : references) {
|
||||||
node.getHistory().add(reference);
|
if(!existingReferences.add(reference)) {
|
||||||
|
Optional<BlockchainTransactionHash> 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 {
|
public void getReferencedTransactions(Wallet wallet, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
getReferencedTransactions(wallet, KeyPurpose.RECEIVE);
|
Set<BlockchainTransactionHash> references = new TreeSet<>();
|
||||||
getReferencedTransactions(wallet, KeyPurpose.CHANGE);
|
for(Set<BlockchainTransactionHash> nodeReferences : nodeTransactionMap.values()) {
|
||||||
}
|
references.addAll(nodeReferences);
|
||||||
|
|
||||||
public void getReferencedTransactions(Wallet wallet, KeyPurpose keyPurpose) throws ServerException {
|
|
||||||
WalletNode purposeNode = wallet.getNode(keyPurpose);
|
|
||||||
Set<BlockchainTransactionHash> references = new HashSet<>();
|
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
|
||||||
references.addAll(addressNode.getHistory());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Sha256Hash, Transaction> transactionMap = getTransactions(references);
|
Map<Sha256Hash, BlockchainTransaction> transactionMap = getTransactions(references);
|
||||||
wallet.getTransactions().putAll(transactionMap);
|
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<Sha256Hash, Transaction> getTransactions(Set<BlockchainTransactionHash> references) throws ServerException {
|
public Map<Sha256Hash, BlockchainTransaction> getTransactions(Set<BlockchainTransactionHash> references) throws ServerException {
|
||||||
try {
|
try {
|
||||||
|
Set<BlockchainTransactionHash> checkReferences = new TreeSet<>(references);
|
||||||
|
|
||||||
JsonRpcClient client = new JsonRpcClient(getTransport());
|
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||||
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
||||||
for(BlockchainTransactionHash reference : references) {
|
for(BlockchainTransactionHash reference : references) {
|
||||||
|
@ -145,12 +154,25 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
Map<String, String> result = batchRequest.execute();
|
Map<String, String> result = batchRequest.execute();
|
||||||
|
|
||||||
Map<Sha256Hash, Transaction> transactionMap = new HashMap<>();
|
Map<Sha256Hash, BlockchainTransaction> transactionMap = new HashMap<>();
|
||||||
for(String txid : result.keySet()) {
|
for(String txid : result.keySet()) {
|
||||||
Sha256Hash hash = Sha256Hash.wrap(txid);
|
Sha256Hash hash = Sha256Hash.wrap(txid);
|
||||||
byte[] rawtx = Utils.hexToBytes(result.get(txid));
|
byte[] rawtx = Utils.hexToBytes(result.get(txid));
|
||||||
Transaction transaction = new Transaction(rawtx);
|
Transaction transaction = new Transaction(rawtx);
|
||||||
transactionMap.put(hash, transaction);
|
|
||||||
|
Optional<BlockchainTransactionHash> 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;
|
return transactionMap;
|
||||||
|
@ -161,6 +183,74 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void calculateNodeHistory(Wallet wallet, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap) {
|
||||||
|
for(WalletNode node : nodeTransactionMap.keySet()) {
|
||||||
|
calculateNodeHistory(wallet, nodeTransactionMap, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculateNodeHistory(Wallet wallet, Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap, WalletNode node) {
|
||||||
|
Script nodeScript = wallet.getOutputScript(node);
|
||||||
|
Set<BlockchainTransactionHash> 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<BlockchainTransactionHash> 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<BlockchainTransactionHashIndex> 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<BlockchainTransactionHashIndex> 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) {
|
private String getScriptHash(Wallet wallet, WalletNode node) {
|
||||||
byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram());
|
byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram());
|
||||||
byte[] reversed = Utils.reverseBytes(hash);
|
byte[] reversed = Utils.reverseBytes(hash);
|
||||||
|
@ -174,16 +264,12 @@ public class ElectrumServer {
|
||||||
|
|
||||||
public BlockchainTransactionHash getBlockchainTransactionHash() {
|
public BlockchainTransactionHash getBlockchainTransactionHash() {
|
||||||
Sha256Hash hash = Sha256Hash.wrap(tx_hash);
|
Sha256Hash hash = Sha256Hash.wrap(tx_hash);
|
||||||
return new BlockchainTransactionHash(hash, height, fee);
|
return new BlockchainTransaction(hash, height, fee, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "ScriptHashTx{" +
|
return "ScriptHashTx{height=" + height + ", tx_hash='" + tx_hash + '\'' + ", fee=" + fee + '}';
|
||||||
"height=" + height +
|
|
||||||
", tx_hash='" + tx_hash + '\'' +
|
|
||||||
", fee=" + fee +
|
|
||||||
'}';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,8 +386,9 @@ public class ElectrumServer {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected Boolean call() throws ServerException {
|
protected Boolean call() throws ServerException {
|
||||||
ElectrumServer electrumServer = new ElectrumServer();
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
electrumServer.getHistory(wallet);
|
Map<WalletNode, Set<BlockchainTransactionHash>> nodeTransactionMap = electrumServer.getHistory(wallet);
|
||||||
electrumServer.getReferencedTransactions(wallet);
|
electrumServer.getReferencedTransactions(wallet, nodeTransactionMap);
|
||||||
|
electrumServer.calculateNodeHistory(wallet, nodeTransactionMap);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -331,21 +331,22 @@ public class Storage {
|
||||||
Iterator<JsonElement> iter = children.iterator();
|
Iterator<JsonElement> iter = children.iterator();
|
||||||
while(iter.hasNext()) {
|
while(iter.hasNext()) {
|
||||||
JsonObject childObject = (JsonObject)iter.next();
|
JsonObject childObject = (JsonObject)iter.next();
|
||||||
if(childObject.get("children") != null && childObject.getAsJsonArray("children").size() == 0) {
|
removeEmptyCollection(childObject, "children");
|
||||||
childObject.remove("children");
|
removeEmptyCollection(childObject, "transactionOutputs");
|
||||||
}
|
|
||||||
|
|
||||||
if(childObject.get("history") != null && childObject.getAsJsonArray("history").size() == 0) {
|
if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("transactionOutputs") == null) {
|
||||||
childObject.remove("history");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("history") == null) {
|
|
||||||
iter.remove();
|
iter.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonObject;
|
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<WalletNode> {
|
private static class NodeDeserializer implements JsonDeserializer<WalletNode> {
|
||||||
|
@ -356,8 +357,8 @@ public class Storage {
|
||||||
if(childNode.getChildren() == null) {
|
if(childNode.getChildren() == null) {
|
||||||
childNode.setChildren(new TreeSet<>());
|
childNode.setChildren(new TreeSet<>());
|
||||||
}
|
}
|
||||||
if(childNode.getHistory() == null) {
|
if(childNode.getTransactionOutputs() == null) {
|
||||||
childNode.setHistory(new TreeSet<>());
|
childNode.setTransactionOutputs(new TreeSet<>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue