Skip to content
Bitcoin Roots

v29.4-roots.4 · Commit 23: [PATCH 23/37] feat(wallet): add transient private-key sweep RPC

src/wallet/rpc/spend.cpp

Before: src/wallet/rpc/spend.cpp · After: src/wallet/rpc/spend.cpp

File metadata
index 53692f85114844ad70a03f435c8ef28e7139b2b3..ea29d8fd2b11d5ac79e7f4ffdf6ef4a39a88d6c4 100644

Download original patch · Release notes

Diff rows 1–88 of 88. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.

src/wallet/rpc/spend.cpp CHANGED
@@ -31,6 +31,88 @@ using common::TransactionErrorString;
31
31
  using node::TransactionError;
32
32
 
33
33
  namespace wallet {
34
+ //! Sweep coins controlled by transient keys. The keys are deliberately kept out
35
+ //! of the wallet and are only used by the local signing provider below.
36
+ RPCHelpMan sweepprivkeys()
37
+ {
38
+ return RPCHelpMan{"sweepprivkeys",
39
+ "Sweep confirmed UTXOs for supplied private keys to an address already controlled by this wallet.\n"
40
+ "Supplied keys are never imported or saved. Set broadcast to true only after reviewing a preview.\n",
41
+ {
42
+ {"privkeys", RPCArg::Type::ARR, RPCArg::Optional::NO, "Base58-encoded private keys", {{"privatekey", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "private key"}}},
43
+ {"destination", RPCArg::Type::STR, RPCArg::Optional::NO, "An address controlled by this wallet"},
44
+ {"broadcast", RPCArg::Type::BOOL, RPCArg::Default{false}, "Broadcast the signed transaction instead of returning a preview"},
45
+ },
46
+ RPCResult{RPCResult::Type::OBJ, "", "", {
47
+ {RPCResult::Type::STR_HEX, "hex", "Signed transaction"},
48
+ {RPCResult::Type::STR_HEX, "txid", "Transaction id"},
49
+ {RPCResult::Type::STR_AMOUNT, "amount", "Total value of swept inputs"},
50
+ {RPCResult::Type::STR_AMOUNT, "fee", "Transaction fee"},
51
+ {RPCResult::Type::NUM, "inputs", "Number of swept inputs"},
52
+ {RPCResult::Type::BOOL, "broadcast", "Whether the transaction was broadcast"},
53
+ }},
54
+ RPCExamples{HelpExampleCli("sweepprivkeys", "'[\"cV...\"]' bcrt1... false")},
55
+ [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
56
+ const std::shared_ptr<CWallet> pwallet = GetWalletForJSONRPCRequest(request);
57
+ if (!pwallet) return UniValue::VNULL;
58
+
59
+ FlatSigningProvider provider;
60
+ std::set<CKeyID> seen_keys;
61
+ std::set<CScript> scan_objects;
62
+ for (const UniValue& value : request.params[0].get_array().getValues()) {
63
+ CKey key{DecodeSecret(value.get_str())};
64
+ if (!key.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key or wrong network");
65
+ const CPubKey pubkey{key.GetPubKey()};
66
+ if (!seen_keys.insert(pubkey.GetID()).second) throw JSONRPCError(RPC_INVALID_PARAMETER, "Duplicate private key");
67
+ provider.keys.emplace(pubkey.GetID(), key);
68
+ provider.pubkeys.emplace(pubkey.GetID(), pubkey);
69
+ scan_objects.insert(GetScriptForDestination(PKHash(pubkey)));
70
+ if (pubkey.IsCompressed()) {
71
+ const CScript witness{GetScriptForDestination(WitnessV0KeyHash(pubkey))};
72
+ scan_objects.insert(witness);
73
+ provider.scripts.emplace(CScriptID(witness), witness);
74
+ scan_objects.insert(GetScriptForDestination(ScriptHash(witness)));
75
+ }
76
+ }
77
+ if (seen_keys.empty()) throw JSONRPCError(RPC_INVALID_PARAMETER, "At least one private key is required");
78
+
79
+ const CTxDestination destination{DecodeDestination(request.params[1].get_str())};
80
+ if (!IsValidDestination(destination)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid destination");
81
+ const CScript destination_script{GetScriptForDestination(destination)};
82
+ {
83
+ LOCK(pwallet->cs_wallet);
84
+ if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) || !(pwallet->IsMine(destination_script) & ISMINE_SPENDABLE)) {
85
+ throw JSONRPCError(RPC_WALLET_ERROR, "Destination is not controlled by this wallet");
86
+ }
87
+ }
88
+
89
+ std::map<COutPoint, Coin> coins;
90
+ switch (pwallet->chain().findScriptPubKeys(scan_objects, coins)) {
91
+ case interfaces::ScanResult::SUCCESS: break;
92
+ case interfaces::ScanResult::BUSY: throw JSONRPCError(RPC_MISC_ERROR, "UTXO scan already in progress");
93
+ case interfaces::ScanResult::ABORTED: throw JSONRPCError(RPC_MISC_ERROR, "UTXO scan aborted");
94
+ case interfaces::ScanResult::UNAVAILABLE: throw JSONRPCError(RPC_MISC_ERROR, "UTXO scan unavailable");
95
+ }
96
+ const bool broadcast{!request.params[2].isNull() && request.params[2].get_bool()};
97
+ auto swept{CreateSweepTransaction(*pwallet, provider, coins, destination_script, broadcast ? SweepBroadcastFn{[&](const CTransactionRef& tx, std::string& error) {
98
+ return pwallet->chain().broadcastTransaction(tx, pwallet->m_default_max_tx_fee, /*relay=*/true, error);
99
+ }} : SweepBroadcastFn{})};
100
+ if (!swept) {
101
+ const std::string error{util::ErrorString(swept).original};
102
+ const int code{error == "No spendable UTXOs found" || error == "Fee exceeds swept value" ? RPC_WALLET_INSUFFICIENT_FUNDS :
103
+ error == "Unable to sign all swept inputs" ? RPC_VERIFY_ERROR : RPC_WALLET_ERROR};
104
+ throw JSONRPCError(code, error);
105
+ }
106
+ UniValue result{UniValue::VOBJ};
107
+ result.pushKV("hex", EncodeHexTx(*swept->tx));
108
+ result.pushKV("txid", swept->tx->GetHash().GetHex());
109
+ result.pushKV("amount", ValueFromAmount(swept->amount));
110
+ result.pushKV("fee", ValueFromAmount(swept->fee));
111
+ result.pushKV("inputs", coins.size());
112
+ result.pushKV("broadcast", broadcast);
113
+ return result;
114
+ }};
115
+ }
34
116
  std::vector<CRecipient> CreateRecipients(const std::vector<std::pair<CTxDestination, CAmount>>& outputs, const std::set<int>& subtract_fee_outputs)
35
117
  {
36
118
  std::vector<CRecipient> recipients;