@ -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 < WalletNode , Set < BlockchainTransactionHash > > getHistory ( Wallet wallet ) throws ServerException {
Map < WalletNode , Set < BlockchainTransactionHash > > 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 < WalletNode , Set < BlockchainTransactionHash > > nodeTransactionMap ) throws ServerException {
getHistory ( wallet , wallet . getNode ( keyPurpose ) . getChildren ( ) , nodeTransactionMap ) ;
getMempool ( wallet , wallet . getNode ( keyPurpose ) . getChildren ( ) , nodeTransactionMap ) ;
}
public void getHistory ( Wallet wallet , Collection < WalletNode > nodes ) throws ServerException {
getReferences ( wallet , "blockchain.scripthash.get_history" , nodes ) ;
public void getHistory ( Wallet wallet , Collection < WalletNode > nodes , Map < WalletNode , Set < BlockchainTransactionHash > > nodeTransactionMap ) throws ServerException {
getReferences ( wallet , "blockchain.scripthash.get_history" , nodes , nodeTransactionMap ) ;
}
public void getMempool ( Wallet wallet , Collection < WalletNode > nodes ) throws ServerException {
getReferences ( wallet , "blockchain.scripthash.get_mempool" , nodes ) ;
public void getMempool ( Wallet wallet , Collection < WalletNode > nodes , Map < WalletNode , Set < BlockchainTransactionHash > > nodeTransactionMap ) throws ServerException {
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 {
JsonRpcClient client = new JsonRpcClient ( getTransport ( ) ) ;
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 ( ) ;
if ( optionalNode . isPresent ( ) ) {
WalletNode node = optionalNode . get ( ) ;
Set < BlockchainTransactionHash > references = Arrays . stream ( txes ) . map ( ScriptHashTx : : getBlockchainTransactionHash ) . collect ( Collectors . toSet ( ) ) ;
for ( BlockchainTransactionHash reference : references ) {
if ( ! node . getHistory ( ) . add ( reference ) ) {
Optional < BlockchainTransactionHash > 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 < BlockchainTransactionHash > references = Arrays . stream ( txes ) . map ( ScriptHashTx : : getBlockchainTransactionHash ) . collect ( Collectors . toCollection ( TreeSet : : new ) ) ;
Set < BlockchainTransactionHash > existingReferences = nodeTransactionMap . get ( node ) ;
if ( existingReferences = = null & & ! references . isEmpty ( ) ) {
nodeTransactionMap . put ( node , references ) ;
} else {
for ( BlockchainTransactionHash reference : references ) {
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 {
getReferencedTransactions ( wallet , KeyPurpose . RECEIVE ) ;
getReferencedTransactions ( wallet , KeyPurpose . CHANGE ) ;
}
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 ( ) ) ;
public void getReferencedTransactions ( Wallet wallet , Map < WalletNode , Set < BlockchainTransactionHash > > nodeTransactionMap ) throws ServerException {
Set < BlockchainTransactionHash > references = new TreeSet < > ( ) ;
for ( Set < BlockchainTransactionHash > nodeReferences : nodeTransactionMap . values ( ) ) {
references . addAll ( nodeReferences ) ;
}
Map < Sha256Hash , Transaction > transactionMap = getTransactions ( references ) ;
wallet . getTransactions ( ) . putAll ( transactionMap ) ;
Map < Sha256Hash , BlockchainTransaction > 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 < Sha256Hash , Transaction > getTransactions ( Set < BlockchainTransactionHash > references ) throws ServerException {
public Map < Sha256Hash , Blockchain Transaction> getTransactions ( Set < BlockchainTransactionHash > references ) throws ServerException {
try {
Set < BlockchainTransactionHash > checkReferences = new TreeSet < > ( references ) ;
JsonRpcClient client = new JsonRpcClient ( getTransport ( ) ) ;
BatchRequestBuilder < String , String > batchRequest = client . createBatchRequest ( ) . keysType ( String . class ) . returnType ( String . class ) ;
for ( BlockchainTransactionHash reference : references ) {
@ -145,12 +154,25 @@ public class ElectrumServer {
}
Map < String , String > result = batchRequest . execute ( ) ;
Map < Sha256Hash , Transaction > transactionMap = new HashMap < > ( ) ;
Map < Sha256Hash , Blockchain Transaction> 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 < 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 ;
@ -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 ) {
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 < WalletNode , Set < BlockchainTransactionHash > > nodeTransactionMap = electrumServer . getHistory ( wallet ) ;
electrumServer . getReferencedTransactions ( wallet , nodeTransactionMap ) ;
electrumServer . calculateNodeHistory ( wallet , nodeTransactionMap ) ;
return true ;
}
} ;