001package org.consensusj.bitcoin.jsonrpc;
002
003import com.fasterxml.jackson.databind.JavaType;
004import org.bitcoinj.base.AddressParser;
005import org.bitcoinj.base.BitcoinNetwork;
006import org.bitcoinj.base.Network;
007import org.bitcoinj.base.ScriptType;
008import org.bitcoinj.params.BitcoinNetworkParams;
009import org.consensusj.bitcoin.json.pojo.AddressInfo;
010import org.consensusj.bitcoin.json.pojo.LoadWalletResult;
011import org.consensusj.bitcoin.json.pojo.Outpoint;
012import org.consensusj.bitcoin.json.pojo.SignedRawTransaction;
013import org.consensusj.bitcoin.json.pojo.UnspentOutput;
014import org.bitcoinj.core.Block;
015import org.bouncycastle.util.encoders.Hex;
016import org.consensusj.bitcoin.jsonrpc.bitcoind.BitcoinConfFile;
017import org.consensusj.jsonrpc.JsonRpcStatusException;
018import org.bitcoinj.base.Address;
019import org.bitcoinj.base.Coin;
020import org.bitcoinj.crypto.ECKey;
021import org.bitcoinj.base.Sha256Hash;
022import org.bitcoinj.core.Transaction;
023import org.bitcoinj.core.TransactionOutPoint;
024import org.bitcoinj.core.TransactionOutput;
025import org.consensusj.jsonrpc.JsonRpcTransport;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import javax.net.ssl.SSLContext;
030import java.io.IOException;
031import java.math.BigInteger;
032import java.net.URI;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.stream.Collectors;
037
038/**
039 * Extended Bitcoin JSON-RPC Client with added convenience methods.
040 *
041 * This class adds extra methods that aren't 1:1 mappings to standard
042 * Bitcoin API RPC methods, but are useful for many common use cases -- specifically
043 * the ones we ran into while building integration tests.
044 */
045public class BitcoinExtendedClient extends BitcoinClient {
046    private static final Logger log = LoggerFactory.getLogger(BitcoinExtendedClient.class);
047
048    public static final Address DEFAULT_REGTEST_MINING_ADDRESS = AddressParser.getDefault().parseAddress("mwQA8f4pH23BfHyy4zf8mgAyeNu5uoy6GU");
049    private static final BigInteger NotSoPrivatePrivateInt = new BigInteger(1, Hex.decode("180cb41c7c600be951b5d3d0a7334acc7506173875834f7a6c4c786a28fcbb19"));
050    private static final String RegTestMiningAddressLabel = "RegTestMiningAddress";
051    public static final String REGTEST_WALLET_NAME = "consensusj-regtest-wallet";
052    private boolean regTestWalletInitialized = false;
053    private /* lazy */ Address regTestMiningAddress;
054
055    public final Coin stdTxFee = Coin.valueOf(10000);
056    public final Coin stdRelayTxFee = Coin.valueOf(1000);
057    public final Integer defaultMaxConf = 9999999;
058    public final long stdTxFeeSatoshis = stdTxFee.getValue();
059
060    public Coin getStdTxFee() {
061        return stdTxFee;
062    }
063
064    public Coin getStdRelayTxFee() {
065        return stdRelayTxFee;
066    }
067
068    public Integer getDefaultMaxConf() {
069        return defaultMaxConf;
070    }
071
072
073    public BitcoinExtendedClient(SSLContext sslContext, Network network, URI server, String rpcuser, String rpcpassword) {
074        super(sslContext, network, server, rpcuser, rpcpassword);
075    }
076
077    public BitcoinExtendedClient(Network network, URI server, String rpcuser, String rpcpassword) {
078        this(JsonRpcTransport.getDefaultSSLContext(), network, server, rpcuser, rpcpassword);
079    }
080
081    public BitcoinExtendedClient(URI server, String rpcuser, String rpcpassword) {
082        this(JsonRpcTransport.getDefaultSSLContext(), null, server, rpcuser, rpcpassword);
083    }
084
085    public BitcoinExtendedClient(RpcConfig config) {
086        this(config.network(), config.getURI(), config.getUsername(), config.getPassword());
087    }
088
089    /**
090     * Constructor that uses bitcoin.conf to get connection information (Incubating)
091     */
092    public BitcoinExtendedClient() {
093        this(BitcoinConfFile.readDefaultConfig().getRPCConfig());
094    }
095
096    /**
097     * Incubating: clone the client with a new base URI for a named wallet.
098     * @param walletName wallet name
099     * @param rpcUser username must be provided because it is not accessible in the parent class
100     * @param rpcPassword password must be provided because it is not accessible in the parent class
101     * @return A new client with a baseURI configured for the specified wallet name.
102     */
103    public BitcoinExtendedClient withWallet(String walletName, String rpcUser, String rpcPassword) {
104        return new BitcoinExtendedClient(this.getNetwork(), this.getServerURI().resolve("/wallet/" + walletName), rpcUser, rpcPassword);
105    }
106
107    public synchronized Address getRegTestMiningAddress() {
108        if (getNetwork() != BitcoinNetwork.REGTEST) {
109            throw new UnsupportedOperationException("Operation only supported in RegTest context");
110        }
111        if (regTestMiningAddress == null) {
112            if (!regTestWalletInitialized) {
113                initRegTestWallet();
114            }
115            // If in the future, we want to manage the keys for mined coins on the client side,
116            // we could initialize regTestMiningKey from a bitcoinj-generated ECKey or HD Keychain.
117            try {
118                ECKey notSoPrivatePrivateKey = ECKey.fromPrivate(NotSoPrivatePrivateInt, false);
119                Address address = notSoPrivatePrivateKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.REGTEST);
120                AddressInfo addressInfo = getAddressInfo(address);
121                if (addressInfo.getIsmine() && !addressInfo.getIswatchonly() && addressInfo.getSolvable()) {
122                    log.warn("Address with label {} is present in server-side wallet", RegTestMiningAddressLabel);
123                    regTestMiningAddress = address;
124                } else {
125                    // Import known private key with label and rescan the blockchain for any transactions from
126                    // previous test runs, rescan shouldn't take too long on a typical RegTest chain
127                    log.warn("Adding private key for {} and rescanning chain", RegTestMiningAddressLabel);
128                    importPrivKey(notSoPrivatePrivateKey, RegTestMiningAddressLabel, true);
129                    regTestMiningAddress = address;
130                }
131                log.warn("Retrieved regTestMiningAddress = {}", regTestMiningAddress);
132            } catch (IOException e) {
133                log.error("Exception while checking/importing regTestMiningAddress", e);
134                throw new RuntimeException(e);
135            }
136        }
137        return regTestMiningAddress;
138    }
139
140    /**
141     * Initialize a server-side wallet for RegTest mining and test transaction funding. Creates a non-descriptor wallet
142     * with name {@link #REGTEST_WALLET_NAME} if it doesn't already exist.
143     */
144    public synchronized void initRegTestWallet() {
145        if (!regTestWalletInitialized) {
146            // Create a named wallet for RegTest (previously we used the default wallet with an empty-string name)
147            int bitcoinCoreVersion = getServerVersion();
148            try {
149                List<String> walletList = listWallets();
150                if (!walletList.contains(REGTEST_WALLET_NAME)) {
151                    createRegTestWallet(bitcoinCoreVersion, REGTEST_WALLET_NAME);
152                }
153            } catch (IOException ioe) {
154                throw new RuntimeException(ioe);
155            }
156            regTestWalletInitialized = true;
157        }
158    }
159
160    /**
161     * Create a server-side wallet suitable for RegTest mining/funding, defaulting all other parameters.
162     * @param bitcoinCoreVersion Used to select correct JSON-RPC parameters to used based on server version
163     * @param name name of wallet to create
164     */
165    private void createRegTestWallet(int bitcoinCoreVersion, String name) throws JsonRpcStatusException, IOException {
166        LoadWalletResult result = (bitcoinCoreVersion >= BITCOIN_CORE_VERSION_DESC_DEFAULT)
167                    // Create a (non-descriptor) wallet
168                    ? createWallet(name, false, false, null, null, false, null, null)
169                    : createWallet(name, false, false, null, null);
170        if (result.getWarning().isEmpty()) {
171            log.info("Created REGTEST wallet: \"{}\"", result.getName());
172        } else {
173            log.warn("Warning creating REGTEST wallet \"{}\": {}", result.getName(), result.getWarning());
174        }
175    }
176
177    /**
178     * Generate blocks and funds (RegTest only)
179     *
180     * Use this to generate blocks and receive the block reward in {@code this.regTestMiningAddress}
181     * which can the be used to fund transactions in RegTest mode.
182     *
183     * @param numBlocks Number of blocks to mine
184     * @return list of block hashes
185     * @throws JsonRpcStatusException something broke
186     * @throws IOException something broke
187     */
188    public List<Sha256Hash> generateBlocks(int numBlocks) throws JsonRpcStatusException, IOException {
189        return this.generateToAddress(numBlocks, getRegTestMiningAddress());
190    }
191
192    /**
193     * Calculate the subsidy portion block reward for a given height on the current network
194     * @param height the height at which to calculate the mining subsidy
195     * @return mining subsidy
196     */
197    public Coin getBlockSubsidy(int height) {
198        BitcoinNetworkParams params = (BitcoinNetworkParams) BitcoinNetworkParams.of(getNetwork());
199        return params.getBlockInflation(height);
200    }
201
202    /**
203     * Returns information about a block at index provided.
204     *
205     * Uses two RPCs: {@code getblockhash} and then {@code getblock}.
206     *
207     * @param index The block index
208     * @return The information about the block
209     * @throws JsonRpcStatusException JSON RPC status exception
210     * @throws IOException network error
211     */
212    public Block getBlock(int index) throws JsonRpcStatusException, IOException {
213        Sha256Hash blockHash = getBlockHash(index);
214        return getBlock(blockHash);
215    }
216
217    /**
218     * Clears the memory pool and returns a list of the removed transactions.
219     *
220     * Note: this is a customized command, which is currently not part of Bitcoin Core.
221     * See https://github.com/OmniLayer/OmniJ/pull/72[Pull Request #72] on GitHub
222     *
223     * @return A list of transaction hashes of the removed transactions
224     * @throws JsonRpcStatusException JSON RPC status exception
225     * @throws IOException network error
226     */
227    public List<Sha256Hash> clearMemPool() throws JsonRpcStatusException, IOException {
228        JavaType resultType = collectionTypeForClasses(List.class, Sha256Hash.class);
229        return send("clearmempool", resultType);
230    }
231
232    /**
233     * Creates a raw transaction, spending from a single address, whereby no new change address is created, and
234     * remaining amounts are returned to {@code fromAddress}.
235     * <p>
236     * Note: the transaction inputs are not signed, and the transaction is not stored in the wallet or transmitted to
237     * the network.
238     *
239     * @param fromAddress The source to spend from
240     * @param outputs The destinations and amounts to transfer
241     * @return The hex-encoded raw transaction
242     */
243    public String createRawTransaction(Address fromAddress, Map<Address, Coin> outputs) throws JsonRpcStatusException, IOException {
244        // Copy the Map (which may be immutable) and add prepare change output if needed.
245        Map<Address, Coin> outputsWithChange = new HashMap<>(outputs);
246        // Get unspent outputs via RPC
247        List<UnspentOutput> unspentOutputs = listUnspent(0, defaultMaxConf, fromAddress);
248
249        // Gather inputs as OutPoints
250        List<Outpoint> inputs = unspentOutputs.stream()
251                .map(this::unspentToOutpoint)
252                .collect(Collectors.toList());
253
254        // Calculate change
255        final long amountIn = unspentOutputs
256                .stream()
257                .map(UnspentOutput::getAmount)
258                .mapToLong(Coin::getValue)
259                .sum();
260        final long amountOut = outputs.values()
261                .stream()
262                .mapToLong(Coin::getValue)
263                .sum();
264
265        // Change is the difference less the standard transaction fee
266        final long amountChange = amountIn - amountOut - stdTxFeeSatoshis;
267        if (amountChange < 0) {
268            // TODO: Throw Exception
269            System.out.println("Insufficient funds"); // + ": ${amountIn} < ${amountOut + stdTxFee}"
270        }
271        if (amountChange > 0) {
272            // Add a change output that returns change to sending address
273            outputsWithChange.put(fromAddress, Coin.valueOf(amountChange));
274        }
275
276        // Call the server to create the transaction
277        return createRawTransaction(inputs, outputsWithChange);
278    }
279
280    /**
281     * Creates a raw transaction, sending {@code amount} from a single address to a destination, whereby no new change
282     * address is created, and remaining amounts are returned to {@code fromAddress}.
283     * <p>
284     * Note: the transaction inputs are not signed, and the transaction is not stored in the wallet or transmitted to
285     * the network.
286     *
287     * @param fromAddress The source to spent from
288     * @param toAddress The destination
289     * @param amount The amount
290     * @return The hex-encoded raw transaction
291     */
292    public String createRawTransaction(Address fromAddress, Address toAddress, Coin amount) throws JsonRpcStatusException, IOException {
293        return createRawTransaction(fromAddress, Map.of(toAddress, amount));
294    }
295
296    /**
297     * Returns the Bitcoin balance of an address (in the server-side wallet.)
298     *
299     * @param address The address
300     * @return The balance
301     */
302    public Coin getBitcoinBalance(Address address) throws JsonRpcStatusException, IOException {
303        // NOTE: because null is currently removed from the argument lists passed via RPC, using it here for default
304        // values would result in the RPC call "listunspent" with arguments [["address"]], which is invalid, similar
305        // to a call with arguments [null, null, ["address"]], as expected arguments are either [], [int], [int, int]
306        // or [int, int, array]
307        return getBitcoinBalance(address, 1, defaultMaxConf);
308    }
309
310    /**
311     * Returns the Bitcoin balance of an address (in the server-side wallet) where spendable outputs have at least
312     * {@code minConf} confirmations.
313     *
314     * @param address The address
315     * @param minConf Minimum amount of confirmations
316     * @return The balance
317     */
318    public Coin getBitcoinBalance(Address address, Integer minConf) throws JsonRpcStatusException, IOException {
319        return getBitcoinBalance(address, minConf, defaultMaxConf);
320    }
321
322    /**
323     * Returns the Bitcoin balance of an address (in the server-side wallet) where spendable outputs have at least
324     * {@code minConf} and not more than {@code maxConf} confirmations.
325     *
326     * @param address The address (must be in wallet)
327     * @param minConf Minimum amount of confirmations
328     * @param maxConf Maximum amount of confirmations
329     * @return The balance
330     */
331    public Coin getBitcoinBalance(Address address, Integer minConf, Integer maxConf) throws JsonRpcStatusException, IOException {
332        return listUnspent(minConf, maxConf, address)
333                .stream()
334                .map(UnspentOutput::getAmount)
335                .reduce(Coin.ZERO, Coin::add);
336    }
337
338    /**
339     * Sends BTC from an address to a destination, whereby no new change address is created, and any leftover is
340     * returned to the sending address.
341     *
342     * @param fromAddress The source to spent from
343     * @param toAddress   The destination address
344     * @param amount      The amount to transfer
345     * @return The transaction hash
346     */
347    public Sha256Hash sendBitcoin(Address fromAddress, Address toAddress, Coin amount) throws JsonRpcStatusException, IOException {
348        Map<Address, Coin> outputs = Map.of(toAddress, amount);
349        return sendBitcoin(fromAddress, outputs);
350    }
351
352    /**
353     * Sends BTC from an address to the destinations, whereby no new change address is created, and any leftover is
354     * returned to the sending address.
355     *
356     * @param fromAddress The source to spent from
357     * @param outputs     The destinations and amounts to transfer
358     * @return The transaction hash
359     */
360    public Sha256Hash sendBitcoin(Address fromAddress, Map<Address, Coin> outputs) throws JsonRpcStatusException, IOException {
361        String unsignedTxHex = createRawTransaction(fromAddress, outputs);
362        SignedRawTransaction signingResult = signRawTransactionWithWallet(unsignedTxHex);
363
364        boolean complete = signingResult.isComplete();
365        assert complete;
366
367        String signedTxHex = signingResult.getHex();
368        Sha256Hash txid = sendRawTransaction(signedTxHex);
369
370        return txid;
371    }
372
373    /**
374     * Create a signed transaction locally (i.e. with a client-side key.) Finds UTXOs this
375     * key can spend (assuming they are ScriptType.P2PKH UTXOs)
376     *
377     * @param fromKey Signing key
378     * @param outputs Outputs to sign
379     * @return A bitcoinj Transaction objects that is properly signed
380     * @throws JsonRpcStatusException A JSON-RPC error was returned
381     * @throws IOException            An I/O error occured
382     */
383    public Transaction createSignedTransaction(ECKey fromKey, List<TransactionOutput> outputs) throws JsonRpcStatusException, IOException {
384        Address fromAddress = fromKey.toAddress(ScriptType.P2PKH, getNetwork());
385
386        Transaction tx = new Transaction();   // Create a new transaction
387        outputs.forEach(tx::addOutput);                     // Add all requested outputs to it
388
389        // Fetch all UTXOs for the sending Address
390        List<TransactionOutput> unspentOutputs = listUnspentJ(fromAddress);
391
392        // Calculate change (units are satoshis)
393        // First sum all available UTXOs
394        final Coin amountIn = unspentOutputs.stream()
395                .map(TransactionOutput::getValue)
396                .reduce(Coin.ZERO, Coin::add);
397        // Then sum the requested outputs for this transaction
398        final Coin amountOut = outputs.stream()
399                .map(TransactionOutput::getValue)
400                .reduce(Coin.ZERO, Coin::add);
401        // Change is the difference less the standard transaction fee
402        final long amountChange = amountIn.value - amountOut.value - stdTxFeeSatoshis;
403        if (amountChange < 0) {
404            // TODO: Throw Exception
405            System.out.println("Insufficient funds"); // + ": ${amountIn} < ${amountOut + stdTxFeeSatoshis}"
406        }
407        if (amountChange > 0) {
408            // Add a change output that returns change to sending address
409            tx.addOutput(Coin.valueOf(amountChange), fromAddress);
410        }
411
412        // Add *all* UTXOs for fromAddress as inputs (this perhaps unnecessarily consolidates coins, with
413        // a higher tx fee and some loss of privacy) and sign them
414        unspentOutputs.forEach(unspent -> tx.addSignedInput(unspent, fromKey));
415        return tx;
416    }
417
418    /**
419     * Create a signed transaction locally (i.e. with a client-side key.) Finds UTXOs this
420     * key can spend (assuming they are org.bitcoinj.base.ScriptType.P2PKH UTXOs)
421     *
422     * @param fromKey   Signing key
423     * @param toAddress Destination address
424     * @param amount    Amount to send
425     * @return A bitcoinj Transaction objects that is properly signed
426     * @throws JsonRpcStatusException A JSON-RPC error was returned
427     * @throws IOException            An I/O error occured
428     */
429    public Transaction createSignedTransaction(ECKey fromKey, Address toAddress, Coin amount) throws JsonRpcStatusException, IOException {
430        List<TransactionOutput> outputs = List.of(
431                new TransactionOutput(null, amount, toAddress));
432        return createSignedTransaction(fromKey, outputs);
433    }
434
435    /**
436     * Build a list of bitcoinj {@link TransactionOutput}s using {@link BitcoinClient#listUnspent}
437     * and {@link BitcoinClient#getRawTransaction} RPCs.
438     *
439     * @param fromAddress Address to get UTXOs for
440     * @return All unspent TransactionOutputs for fromAddress
441     */
442    public List<TransactionOutput> listUnspentJ(Address fromAddress) throws JsonRpcStatusException, IOException {
443        List<UnspentOutput> unspentOutputs = listUnspent(0, defaultMaxConf, fromAddress); // RPC UnspentOutput objects
444        return unspentOutputs.stream()
445                .map(this::unspentToTransactionOutput)
446                .collect(Collectors.toList());
447    }
448
449    /**
450     * Build a list of bitcoinj {@link TransactionOutPoint}s using {@link BitcoinClient#listUnspent}.
451     *
452     * @param fromAddress Address to get UTXOs for
453     * @return All unspent TransactionOutPoints for fromAddress
454     */
455    public List<TransactionOutPoint> listUnspentOutPoints(Address fromAddress) throws JsonRpcStatusException, IOException {
456        List<UnspentOutput> unspentOutputsRPC = listUnspent(0, defaultMaxConf, fromAddress); // RPC UnspentOutput objects
457        return unspentOutputsRPC.stream()
458                .map(this::unspentToTransactionOutpoint)
459                .collect(Collectors.toList());
460    }
461    
462    /**
463     * Convert an {@link UnspentOutput} JSONRPC-POJO to a *bitcoinj* {@link TransactionOutput} (that's out-PUT).
464     * Calls {@link BitcoinClient#getRawTransaction(Sha256Hash)}
465     * for every item. Throws {@link RuntimeException} if any of those RPC calls fails.
466     *
467     * @param unspentOutput The input POJO
468     * @return The *bitcoinj* object  (that's out-PUT)
469     */
470    private TransactionOutput unspentToTransactionOutput(UnspentOutput unspentOutput) {
471        try {
472            return getRawTransaction(unspentOutput.getTxid())
473                    .getOutput(unspentOutput.getVout());
474        } catch (IOException e) {
475            throw new RuntimeException(e);
476        }
477    }
478
479    /**
480     * Convert an {@link UnspentOutput} JSONRPC-POJO to a *bitcoinj* {@link TransactionOutPoint} (that's out-POINT).
481     *
482     * @param unspentOutput The input POJO
483     * @return The *bitcoinj* object  (that's out-POINT)
484     */
485    private TransactionOutPoint unspentToTransactionOutpoint(UnspentOutput unspentOutput) {
486        return new TransactionOutPoint(unspentOutput.getVout(), unspentOutput.getTxid());
487    }
488
489    /**
490     * Convert an {@link UnspentOutput} JSONRPC-POJO to a JSONRPC-POJO {@link Outpoint} .
491     *
492     * @param unspentOutput The input UnspentOutput POJO
493     * @return the Outpoint POJO
494     */
495    private Outpoint unspentToOutpoint(UnspentOutput unspentOutput) {
496        return new Outpoint(unspentOutput.getTxid(), unspentOutput.getVout());
497    }
498}