package com.msgilligan.bitcoinj.rpc; import com.msgilligan.bitcoinj.json.pojo.Outpoint; import com.msgilligan.bitcoinj.json.pojo.SignedRawTransaction; import com.msgilligan.bitcoinj.json.pojo.UnspentOutput; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.params.RegTestParams; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * = Extended Bitcoin JSON-RPC Client with added convenience methods * * This class adds extra methods that aren't 1:1 mappings to standard * Bitcoin API RPC methods, but are useful for many common use cases -- specifically * the ones we ran into while building integration tests. */ public class BitcoinExtendedClient extends BitcoinClient { public final Coin stdTxFee = Coin.valueOf(10000); public final Coin stdRelayTxFee = Coin.valueOf(1000); public final Integer defaultMaxConf = 9999999; public final long stdTxFeeSatoshis = stdTxFee.getValue(); @Deprecated public BitcoinExtendedClient(URI server, String rpcuser, String rpcpassword) { super(RegTestParams.get(), server, rpcuser, rpcpassword); } public BitcoinExtendedClient(NetworkParameters netParams, URI server, String rpcuser, String rpcpassword) { super(netParams, server, rpcuser, rpcpassword); } public BitcoinExtendedClient(RPCConfig config) { super(RegTestParams.get(), config.getURI(), config.getUsername(), config.getPassword()); } /** * Creates a raw transaction, spending from a single address, whereby no new change address is created, and * remaining amounts are returned to {@code fromAddress}. * * Note: the transaction inputs are not signed, and the transaction is not stored in the wallet or transmitted to * the network. * * @param fromAddress The source to spend from * @param outputs The destinations and amounts to transfer * @return The hex-encoded raw transaction */ public String createRawTransaction(Address fromAddress, Map<Address, Coin> outputs) throws JsonRPCStatusException, IOException { // Get unspent outputs via RPC List<UnspentOutput> unspentOutputs = listUnspent(0, defaultMaxConf, Collections.singletonList(fromAddress)); // Gather inputs List<Outpoint> inputs = new ArrayList<>(); for (UnspentOutput input : unspentOutputs) { inputs.add(new Outpoint(input.getTxid(), input.getVout())); } // Calculate change long amountIn = 0; long amountOut = 0; for (UnspentOutput it : unspentOutputs) { amountIn += it.getAmount().value; } for (Coin it : outputs.values()) { amountOut += it.value; } Coin amountChange = Coin.valueOf(amountIn - amountOut - stdTxFee.value); if (amountIn < (amountOut + stdTxFee.value)) { System.out.println("Insufficient funds"); // + ": ${amountIn} < ${amountOut + stdTxFee}" } // Copy the Map (which may be immutable) and add change output if needed. Map<Address,Coin> outputsWithChange = new HashMap<>(outputs); if (amountChange.value > 0) { outputsWithChange.put(fromAddress, amountChange); } return createRawTransaction(inputs, outputsWithChange); } /** * Creates a raw transaction, sending {@code amount} from a single address to a destination, whereby no new change * address is created, and remaining amounts are returned to {@code fromAddress}. * * Note: the transaction inputs are not signed, and the transaction is not stored in the wallet or transmitted to * the network. * * @param fromAddress The source to spent from * @param toAddress The destination * @param amount The amount * @return The hex-encoded raw transaction */ public String createRawTransaction(Address fromAddress, Address toAddress, Coin amount) throws JsonRPCStatusException, IOException { Map<Address, Coin> outputs = Collections.singletonMap(toAddress, amount); return createRawTransaction(fromAddress, outputs); } /** * Returns the Bitcoin balance of an address. * * @param address The address * @return The balance */ public Coin getBitcoinBalance(Address address) throws JsonRPCStatusException, IOException { // NOTE: because null is currently removed from the argument lists passed via RPC, using it here for default // values would result in the RPC call "listunspent" with arguments [["address"]], which is invalid, similar // to a call with arguments [null, null, ["address"]], as expected arguments are either [], [int], [int, int] // or [int, int, array] return getBitcoinBalance(address, 1, defaultMaxConf); } /** * Returns the Bitcoin balance of an address where spendable outputs have at least {@code minConf} confirmations. * * @param address The address * @param minConf Minimum amount of confirmations * @return The balance */ public Coin getBitcoinBalance(Address address, Integer minConf) throws JsonRPCStatusException, IOException { return getBitcoinBalance(address, minConf, defaultMaxConf); } /** * Returns the Bitcoin balance of an address where spendable outputs have at least {@code minConf} and not more * than {@code maxConf} confirmations. * * @param address The address (must be in wallet) * @param minConf Minimum amount of confirmations * @param maxConf Maximum amount of confirmations * @return The balance */ public Coin getBitcoinBalance(Address address, Integer minConf, Integer maxConf) throws JsonRPCStatusException, IOException { long btcBalance = 0; List<UnspentOutput> unspentOutputs = listUnspent(minConf, maxConf, Collections.singletonList(address)); for (UnspentOutput unspentOutput : unspentOutputs) { btcBalance += unspentOutput.getAmount().value; } return Coin.valueOf(btcBalance); } /** * Sends BTC from an address to a destination, whereby no new change address is created, and any leftover is * returned to the sending address. * * @param fromAddress The source to spent from * @param toAddress The destination address * @param amount The amount to transfer * @return The transaction hash */ public Sha256Hash sendBitcoin(Address fromAddress, Address toAddress, Coin amount) throws JsonRPCStatusException, IOException { Map<Address, Coin> outputs = Collections.singletonMap(toAddress, amount); return sendBitcoin(fromAddress, outputs); } /** * Sends BTC from an address to the destinations, whereby no new change address is created, and any leftover is * returned to the sending address. * * @param fromAddress The source to spent from * @param outputs The destinations and amounts to transfer * @return The transaction hash */ public Sha256Hash sendBitcoin(Address fromAddress, Map<Address, Coin> outputs) throws JsonRPCStatusException, IOException { String unsignedTxHex = createRawTransaction(fromAddress, outputs); SignedRawTransaction signingResult = signRawTransaction(unsignedTxHex); Boolean complete = signingResult.isComplete(); assert complete; String signedTxHex = signingResult.getHex(); Sha256Hash txid = sendRawTransaction(signedTxHex); return txid; } public Transaction createSignedTransaction(ECKey fromKey, List<TransactionOutput> outputs) throws JsonRPCStatusException, IOException { Address fromAddress = fromKey.toAddress(getNetParams()); Transaction tx = new Transaction(getNetParams()); List<TransactionOutput> unspentOutputs = listUnspentJ(fromAddress); // Add outputs to the transaction for (TransactionOutput it : outputs) { tx.addOutput(it); } // Calculate change (units are satoshis) // long amountIn = (long) unspentOutputs.sum { TransactionOutput it -> it.value.longValue() } long amountIn = 0; for (TransactionOutput it : unspentOutputs) { amountIn += it.getValue().value; } // long amountOut = (long) outputs.sum { TransactionOutput it -> it.value.longValue() } long amountOut = 0; for (TransactionOutput it : outputs) { amountOut += it.getValue().value; } long amountChange = amountIn - amountOut - stdTxFeeSatoshis; if (amountChange < 0) { // TODO: Throw Exception System.out.println("Insufficient funds"); // + ": ${amountIn} < ${amountOut + stdTxFeeSatoshis}" } if (amountChange > 0) { // Add a change output tx.addOutput(Coin.valueOf(amountChange), fromAddress); } // Add all UTXOs for fromAddress as inputs for (TransactionOutput it : unspentOutputs) { tx.addSignedInput(it, fromKey); } return tx; } public Transaction createSignedTransaction(ECKey fromKey, Address toAddress, Coin amount) throws JsonRPCStatusException, IOException { List<TransactionOutput> outputs = Collections.singletonList( new TransactionOutput(getNetParams(), null, amount, toAddress)); return createSignedTransaction(fromKey, outputs); } /** * Build a list of bitcoinj <code>TransactionOutput</code>s using <code>listUnspent</code> * and <code>getRawTransaction</code> RPCs * * @param fromAddress Address to get UTXOs for * @return All unspent TransactionOutputs for fromAddress */ public List<TransactionOutput> listUnspentJ(Address fromAddress) throws JsonRPCStatusException, IOException { List<Address> addresses = Collections.singletonList(fromAddress); List<UnspentOutput> unspentOutputsRPC = listUnspent(0, defaultMaxConf, addresses); // RPC UnspentOutput objects List<TransactionOutput> unspentOutputsJ = new ArrayList<TransactionOutput>(); for (UnspentOutput it : unspentOutputsRPC) { unspentOutputsJ.add(getRawTransaction(it.getTxid()).getOutput(it.getVout())); } return unspentOutputsJ; } public List<TransactionOutPoint> listUnspentOutPoints(Address fromAddress) throws JsonRPCStatusException, IOException { List<Address> addresses = Collections.singletonList(fromAddress); List<UnspentOutput> unspentOutputsRPC = listUnspent(0, defaultMaxConf, addresses); // RPC UnspentOutput objects List<TransactionOutPoint> unspentOutPoints = new ArrayList<TransactionOutPoint>(); for (UnspentOutput it : unspentOutputsRPC) { unspentOutPoints.add(new TransactionOutPoint(getNetParams(), it.getVout(), it.getTxid())); } return unspentOutPoints; } }