diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -170,6 +170,7 @@ {"addpeeraddress", 1, "port"}, {"addpeeraddress", 2, "tried"}, {"stop", 0, "wait"}, + {"createwallettransaction", 1, "amount"}, // Avalanche {"addavalanchenode", 0, "nodeid"}, {"buildavalancheproof", 0, "sequence"}, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -289,7 +289,8 @@ } UniValue SendMoney(CWallet *const pwallet, const CCoinControl &coin_control, - std::vector &recipients, mapValue_t map_value) { + std::vector &recipients, mapValue_t map_value, + bool broadcast = true) { EnsureWalletIsUnlocked(pwallet); // Shuffle recipient list @@ -307,7 +308,8 @@ if (!fCreated) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original); } - pwallet->CommitTransaction(tx, std::move(map_value), {} /* orderForm */); + pwallet->CommitTransaction(tx, std::move(map_value), {} /* orderForm */, + broadcast); return tx->GetId().GetHex(); } @@ -4882,6 +4884,56 @@ RPCHelpMan signmessage(); +static RPCHelpMan createwallettransaction() { + return RPCHelpMan{ + "createwallettransaction", + "Create a transaction sending an amount to a given address.\n" + + HELP_REQUIRING_PASSPHRASE, + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, + "The bitcoin address to send to."}, + {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, + "The amount in " + Currency::get().ticker + " to send. eg 0.1"}, + }, + RPCResult{RPCResult::Type::STR_HEX, "txid", "The transaction id."}, + RPCExamples{ + HelpExampleCli("createwallettransaction", + "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100000") + + HelpExampleRpc("createwallettransaction", + "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\", 100000")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + std::shared_ptr const wallet = + GetWalletForJSONRPCRequest(request); + if (!wallet) { + return NullUniValue; + } + CWallet *const pwallet = wallet.get(); + + // Make sure the results are valid at least up to the most recent + // block the user could have gotten from another RPC command prior + // to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + EnsureWalletIsUnlocked(pwallet); + + UniValue address_amounts(UniValue::VOBJ); + const std::string address = request.params[0].get_str(); + address_amounts.pushKV(address, request.params[1]); + UniValue subtractFeeFromAmount(UniValue::VARR); + + std::vector recipients; + ParseRecipients(address_amounts, subtractFeeFromAmount, recipients, + wallet->GetChainParams()); + + CCoinControl coin_control; + return SendMoney(pwallet, coin_control, recipients, {}, false); + }, + }; +} + Span GetWalletRPCCommands() { // clang-format off static const CRPCCommand commands[] = { @@ -4929,6 +4981,8 @@ { "wallet", upgradewallet, }, { "wallet", walletcreatefundedpsbt, }, { "wallet", walletprocesspsbt, }, + // For testing purpose + { "hidden", createwallettransaction, }, }; // clang-format on diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -669,18 +669,20 @@ bool sign = true, bool bip32derivs = true) const; /** - * Submit the transaction to the node's mempool and then relay to peers. - * Should be called after CreateTransaction unless you want to abort - * broadcasting the transaction. + * Add the transaction to the wallet and maybe attempt to broadcast it. + * Should be called after CreateTransaction. The broadcast flag can be set + * to false if you want to abort broadcasting the transaction. * * @param[in] tx The transaction to be broadcast. * @param[in] mapValue key-values to be set on the transaction. * @param[in] orderForm BIP 70 / BIP 21 order form details to be set on the * transaction. + * @param[in] broadcast Whether to broadcast this transaction. */ void CommitTransaction( CTransactionRef tx, mapValue_t mapValue, - std::vector> orderForm); + std::vector> orderForm, + bool broadcast = true); /** * Pass this transaction to node for mempool insertion and relay to peers diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2120,7 +2120,8 @@ void CWallet::CommitTransaction( CTransactionRef tx, mapValue_t mapValue, - std::vector> orderForm) { + std::vector> orderForm, + bool broadcast) { LOCK(cs_wallet); WalletLogPrintfToBeContinued("CommitTransaction:\n%s", tx->ToString()); @@ -2148,8 +2149,9 @@ // fInMempool flag is cached properly CWalletTx &wtx = mapWallet.at(tx->GetId()); - if (!fBroadcastTransactions) { - // Don't submit tx to the mempool + if (!broadcast || !fBroadcastTransactions) { + // Don't submit tx to the mempool if the flag is unset for this single + // transaction, or if the wallet doesn't broadcast transactions at all. return; } diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -10,8 +10,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error -FAR_IN_THE_FUTURE = 2000000000 - def create_transactions(node, address, amt, fees): # Create and sign raw transactions from node to address for amt. @@ -48,19 +46,8 @@ def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True - self.extra_args = [ - # Limit mempool descendants as a hack to have wallet txs rejected - # from the mempool. This will no longer work after wellington, so - # move the activation in the future for this test. - [ - "-limitdescendantcount=3", - f"-wellingtonactivationtime={FAR_IN_THE_FUTURE}", - ], - [], - ] # whitelist peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") + self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -298,11 +285,10 @@ after = self.nodes[1].getbalances()["mine"]["untrusted_pending"] assert_equal(before + Decimal("100000"), after) - # Create 3 more wallet txs, where the last is not accepted to the - # mempool because it is the third descendant of the tx above - for _ in range(3): - # Set amount high enough such that all coins are spent by each tx - txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 99000000) + # Create a wallet txs which is not added to the mempool + txid = self.nodes[0].createwallettransaction( + self.nodes[0].getnewaddress(), 99000000 + ) self.log.info("Check that wallet txs not in the mempool are untrusted") assert txid not in self.nodes[0].getrawmempool() @@ -328,14 +314,9 @@ block_reorg = self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)[0] assert_equal(self.nodes[0].getbalance(minconf=0), total_amount) - self.log.info("Put txs back into mempool of node 1 (not node 0)") + self.log.info("Put txs back into the mempool of nodes") self.nodes[0].invalidateblock(block_reorg) self.nodes[1].invalidateblock(block_reorg) - # wallet txs not in the mempool are untrusted - assert_equal(self.nodes[0].getbalance(minconf=0), 0) - self.generatetoaddress(self.nodes[0], 1, ADDRESS_WATCHONLY, sync_fun=self.no_op) - # wallet txs not in the mempool are untrusted - assert_equal(self.nodes[0].getbalance(minconf=0), 0) # Now confirm tx_orig self.restart_node(1, ["-persistmempool=0"])