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}