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}