001package org.consensusj.bitcoin.services; 002 003import org.bitcoinj.base.Address; 004import org.bitcoinj.base.Coin; 005import org.bitcoinj.base.Sha256Hash; 006import org.bitcoinj.core.InsufficientMoneyException; 007import org.bitcoinj.core.Transaction; 008import org.bitcoinj.core.TransactionOutput; 009import org.bitcoinj.wallet.Wallet; 010import org.consensusj.bitcoin.json.rpc.BitcoinJsonRpc; 011import org.consensusj.bitcoinj.service.SignTransactionService; 012import org.consensusj.bitcoinj.signing.FeeCalculator; 013import org.consensusj.bitcoinj.signing.HDKeychainSigner; 014import org.consensusj.bitcoinj.signing.RawTransactionSigningRequest; 015import org.consensusj.bitcoinj.signing.SigningRequest; 016import org.consensusj.bitcoinj.signing.SigningUtils; 017import org.consensusj.bitcoinj.signing.TestnetFeeCalculator; 018import org.consensusj.bitcoinj.signing.TransactionInputData; 019import org.consensusj.bitcoinj.signing.TransactionOutputAddress; 020import org.consensusj.bitcoinj.signing.TransactionOutputData; 021import org.consensusj.bitcoinj.signing.Utxo; 022 023import java.io.IOException; 024import java.util.Collection; 025import java.util.List; 026import java.util.Optional; 027import java.util.concurrent.CompletableFuture; 028 029/** 030 * Transaction completion and signing service that uses a bitcoinj {@link Wallet}. This 031 * service can find available UTXOs, build and sign transactions. 032 */ 033public class WalletSigningService implements SignTransactionService { 034 private final Wallet wallet; 035 private final HDKeychainSigner signer; 036 private final FeeCalculator feeCalculator = new TestnetFeeCalculator(); 037 038 public WalletSigningService(Wallet wallet) { 039 this.wallet = wallet; 040 signer = new HDKeychainSigner(wallet.getActiveKeyChain()); 041 } 042 043 /** 044 * 045 * @param rawRequest "raw" signing request with UTXO hash, index information only 046 * @return A (future) signed transaction 047 */ 048 public CompletableFuture<Transaction> signTransaction(RawTransactionSigningRequest rawRequest) { 049 SigningRequest completeRequest; 050 try { 051 // Foreach incomplete input, find the UTXO in the wallet and make a complete input 052 List<TransactionInputData> inputs = rawRequest.inputs().stream() 053 .map(input -> TransactionInputData.of( 054 findUtxo(input.toUtxo()) 055 .orElseThrow(() -> new RuntimeException("UTXO not found in wallet")) 056 ) 057 ) 058 .toList(); 059 // Make a (full) signing request that can be signed with a keychain alone 060 completeRequest = SigningRequest.of(inputs, rawRequest.outputs()); 061 } catch (RuntimeException e) { 062 return CompletableFuture.failedFuture(e); 063 } 064 065 return signTransaction(completeRequest); 066 } 067 068 069 /** 070 * @param request "complete" signing request with UTXO hash, index, amount, scriptPubKey 071 * @return A (future) signed transaction 072 */ 073 @Override 074 public CompletableFuture<Transaction> signTransaction(SigningRequest request) { 075 return signer.signTransaction(request); 076 } 077 078 /** 079 * Create and sign a transaction to send coins to the specified address. Implements the transaction-building 080 * and signing portion of `sendtoaddress` RPC. 081 * @param toAddress destination address 082 * @param amount amount to send 083 * @return a future signed transaction 084 */ 085 @Override 086 public CompletableFuture<Transaction> signSendToAddress(Address toAddress, Coin amount) throws IOException, InsufficientMoneyException { 087 List<TransactionInputData> utxos = getInputs(); 088 TransactionOutputData outputData = new TransactionOutputAddress(amount, toAddress); 089 SigningRequest bitcoinSendReq = createBitcoinSigningRequest(utxos, List.of(outputData), wallet.currentChangeAddress()); 090 return signer.signTransaction(bitcoinSendReq); 091 } 092 093 @Override 094 public SigningRequest createBitcoinSigningRequest(List<TransactionInputData> inputUtxos, List<TransactionOutputData> outputs, Address changeAddress) throws InsufficientMoneyException { 095 SigningRequest request = SigningRequest.of(inputUtxos, outputs); 096 // TODO: see Wallet.calculateFee 097 return SigningUtils.addChange(request, changeAddress, feeCalculator); 098 } 099 100 List<TransactionInputData> getInputs() { 101 List<TransactionOutput> spendCandidates = findUnspentOutputs(1, BitcoinJsonRpc.DEFAULT_MAX_CONF, List.of()); 102 return spendCandidates.stream() 103 .map(TransactionInputData::fromTxOut) 104 .toList(); 105 } 106 107 /** 108 */ 109 public Optional<Utxo.Complete> findUtxo(Utxo utxo) { 110 List<TransactionOutput> candidates = wallet.calculateAllSpendCandidates(); 111 return candidates.stream() 112 .filter(out -> out.getParentTransactionHash().equals(utxo.txId()) && 113 out.getIndex() == utxo.index()) 114 .findFirst() 115 .map(out -> new Utxo.Complete(out.getParentTransactionHash(), 116 out.getIndex(), 117 out.getValue(), 118 out.getScriptPubKey())); 119 } 120 121 /** 122 * @param txId txid 123 * @param vout output index 124 * @return list of matching transaction outputs (bitcoinj objects) 125 */ 126 public Optional<TransactionOutput> findUnspentOutput(Sha256Hash txId, int vout) { 127 return wallet.calculateAllSpendCandidates().stream() 128 .filter(out -> out.getParentTransactionDepthInBlocks() >= 1 && 129 out.getParentTransactionHash().equals(txId) && 130 out.getIndex() == vout) 131 .findFirst(); 132 } 133 134 /** 135 * @param minConf minimum confirmations 136 * @param maxConf maximum confirmations 137 * @param addresses List of wallet addresses these outputs should belong to (empty list is allowed matches all addresses) 138 * @return list of matching transaction outputs (bitcoinj objects) 139 */ 140 List<TransactionOutput> findUnspentOutputs(int minConf, int maxConf, Collection<Address> addresses) { 141 return wallet.calculateAllSpendCandidates().stream() 142 .filter(out -> out.getParentTransactionDepthInBlocks() >= minConf && 143 out.getParentTransactionDepthInBlocks() <= maxConf && 144 matchesAddresses(out, addresses)) 145 .toList(); 146 } 147 148 // empty collection is wildcard 149 private boolean matchesAddresses(TransactionOutput out, Collection<Address> addresses) { 150 return addresses.size() == 0 || addresses.stream().anyMatch(a -> matchesAddress(out, a)); 151 } 152 153 private boolean matchesAddress(TransactionOutput out, Address address) { 154 return out.getScriptPubKey().getToAddress(wallet.network()).equals(address); 155 } 156}