/tmp/bitcoin/src/wallet/rpc/wallet.cpp
Line | Count | Source |
1 | | // Copyright (c) 2010 Satoshi Nakamoto |
2 | | // Copyright (c) 2009-present The Bitcoin Core developers |
3 | | // Distributed under the MIT software license, see the accompanying |
4 | | // file COPYING or http://www.opensource.org/licenses/mit-license.php. |
5 | | |
6 | | #include <bitcoin-build-config.h> // IWYU pragma: keep |
7 | | |
8 | | #include <wallet/rpc/wallet.h> |
9 | | |
10 | | #include <coins.h> |
11 | | #include <core_io.h> |
12 | | #include <key.h> |
13 | | #include <key_io.h> |
14 | | #include <rpc/server.h> |
15 | | #include <rpc/util.h> |
16 | | #include <univalue.h> |
17 | | #include <util/bip32.h> |
18 | | #include <util/translation.h> |
19 | | #include <wallet/context.h> |
20 | | #include <wallet/export.h> |
21 | | #include <wallet/receive.h> |
22 | | #include <wallet/rpc/util.h> |
23 | | #include <wallet/wallet.h> |
24 | | #include <wallet/walletutil.h> |
25 | | |
26 | | #include <algorithm> |
27 | | #include <optional> |
28 | | #include <string_view> |
29 | | |
30 | | |
31 | | namespace wallet { |
32 | | |
33 | | using HDPubKeyMap = CWallet::HDPubKeyMap; |
34 | | using HDKeyFilter = CWallet::HDKeyFilter; |
35 | | |
36 | | static const std::map<uint64_t, std::string> WALLET_FLAG_CAVEATS{ |
37 | | {WALLET_FLAG_AVOID_REUSE, |
38 | | "You need to rescan the blockchain in order to correctly mark used " |
39 | | "destinations in the past. Until this is done, some destinations may " |
40 | | "be considered unused, even if the opposite is the case."}, |
41 | | }; |
42 | | |
43 | | static RPCMethod getwalletinfo() |
44 | 1.28k | { |
45 | 1.28k | return RPCMethod{"getwalletinfo", |
46 | 1.28k | "Returns an object containing various wallet state info.\n", |
47 | 1.28k | {}, |
48 | 1.28k | RPCResult{ |
49 | 1.28k | RPCResult::Type::OBJ, "", "", |
50 | 1.28k | { |
51 | 1.28k | { |
52 | 1.28k | {RPCResult::Type::STR, "walletname", "the wallet name"}, |
53 | 1.28k | {RPCResult::Type::NUM, "walletversion", "(DEPRECATED) only related to unsupported legacy wallet, returns the latest version 169900 for backwards compatibility"}, |
54 | 1.28k | {RPCResult::Type::STR, "format", "the database format (only sqlite)"}, |
55 | 1.28k | {RPCResult::Type::NUM, "txcount", "the total number of transactions in the wallet"}, |
56 | 1.28k | {RPCResult::Type::NUM, "keypoolsize", "how many new keys are pre-generated (only counts external keys)"}, |
57 | 1.28k | {RPCResult::Type::NUM, "keypoolsize_hd_internal", /*optional=*/true, "how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)"}, |
58 | 1.28k | {RPCResult::Type::NUM_TIME, "unlocked_until", /*optional=*/true, "the " + UNIX_EPOCH_TIME + " until which the wallet is unlocked for transfers, or 0 if the wallet is locked (only present for passphrase-encrypted wallets)"}, |
59 | 1.28k | {RPCResult::Type::BOOL, "private_keys_enabled", "false if privatekeys are disabled for this wallet (enforced watch-only wallet)"}, |
60 | 1.28k | {RPCResult::Type::BOOL, "avoid_reuse", "whether this wallet tracks clean/dirty coins in terms of reuse"}, |
61 | 1.28k | {RPCResult::Type::OBJ, "scanning", "current scanning details, or false if no scan is in progress", |
62 | 1.28k | { |
63 | 1.28k | {RPCResult::Type::NUM, "duration", "elapsed seconds since scan start"}, |
64 | 1.28k | {RPCResult::Type::NUM, "progress", "scanning progress percentage [0.0, 1.0]"}, |
65 | 1.28k | }, {.skip_type_check=true}, }, |
66 | 1.28k | {RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for output script management"}, |
67 | 1.28k | {RPCResult::Type::BOOL, "external_signer", "whether this wallet is configured to use an external signer such as a hardware wallet"}, |
68 | 1.28k | {RPCResult::Type::BOOL, "blank", "Whether this wallet intentionally does not contain any keys, scripts, or descriptors"}, |
69 | 1.28k | {RPCResult::Type::NUM_TIME, "birthtime", /*optional=*/true, "The start time for blocks scanning. It could be modified by (re)importing any descriptor with an earlier timestamp."}, |
70 | 1.28k | {RPCResult::Type::ARR, "flags", "The flags currently set on the wallet", |
71 | 1.28k | { |
72 | 1.28k | {RPCResult::Type::STR, "flag", "The name of the flag"}, |
73 | 1.28k | }}, |
74 | 1.28k | RESULT_LAST_PROCESSED_BLOCK, |
75 | 1.28k | }}, |
76 | 1.28k | }, |
77 | 1.28k | RPCExamples{ |
78 | 1.28k | HelpExampleCli("getwalletinfo", "") |
79 | 1.28k | + HelpExampleRpc("getwalletinfo", "") |
80 | 1.28k | }, |
81 | 1.28k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
82 | 1.28k | { |
83 | 452 | const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request); |
84 | 452 | if (!pwallet) return UniValue::VNULL; |
85 | | |
86 | | // Make sure the results are valid at least up to the most recent block |
87 | | // the user could have gotten from another RPC command prior to now |
88 | 452 | pwallet->BlockUntilSyncedToCurrentChain(); |
89 | | |
90 | 452 | LOCK(pwallet->cs_wallet); |
91 | | |
92 | 452 | UniValue obj(UniValue::VOBJ); |
93 | | |
94 | 452 | const int latest_legacy_wallet_minversion{169900}; |
95 | | |
96 | 452 | size_t kpExternalSize = pwallet->KeypoolCountExternalKeys(); |
97 | 452 | obj.pushKV("walletname", pwallet->GetName()); |
98 | 452 | obj.pushKV("walletversion", latest_legacy_wallet_minversion); |
99 | 452 | obj.pushKV("format", pwallet->GetDatabase().Format()); |
100 | 452 | obj.pushKV("txcount", pwallet->mapWallet.size()); |
101 | 452 | obj.pushKV("keypoolsize", kpExternalSize); |
102 | 452 | obj.pushKV("keypoolsize_hd_internal", pwallet->GetKeyPoolSize() - kpExternalSize); |
103 | | |
104 | 452 | if (pwallet->HasEncryptionKeys()) { |
105 | 42 | obj.pushKV("unlocked_until", pwallet->nRelockTime); |
106 | 42 | } |
107 | 452 | obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); |
108 | 452 | obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)); |
109 | 452 | if (pwallet->IsScanning()) { |
110 | 0 | UniValue scanning(UniValue::VOBJ); |
111 | 0 | scanning.pushKV("duration", Ticks<std::chrono::seconds>(pwallet->ScanningDuration())); |
112 | 0 | scanning.pushKV("progress", pwallet->ScanningProgress()); |
113 | 0 | obj.pushKV("scanning", std::move(scanning)); |
114 | 452 | } else { |
115 | 452 | obj.pushKV("scanning", false); |
116 | 452 | } |
117 | 452 | obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); |
118 | 452 | obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)); |
119 | 452 | obj.pushKV("blank", pwallet->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET)); |
120 | 452 | if (int64_t birthtime = pwallet->GetBirthTime(); birthtime != UNKNOWN_TIME) { |
121 | 396 | obj.pushKV("birthtime", birthtime); |
122 | 396 | } |
123 | | |
124 | | // Push known flags |
125 | 452 | UniValue flags(UniValue::VARR); |
126 | 452 | uint64_t wallet_flags = pwallet->GetWalletFlags(); |
127 | 28.2k | for (uint64_t i = 0; i < 64; ++i) { |
128 | 27.8k | uint64_t flag = uint64_t{1} << i; |
129 | 27.8k | if (flag & wallet_flags) { |
130 | 1.09k | if (flag & KNOWN_WALLET_FLAGS) { |
131 | 1.09k | flags.push_back(WALLET_FLAG_TO_STRING.at(WalletFlags{flag})); |
132 | 1.09k | } else { |
133 | 0 | flags.push_back(strprintf("unknown_flag_%u", i)); |
134 | 0 | } |
135 | 1.09k | } |
136 | 27.8k | } |
137 | 452 | obj.pushKV("flags", flags); |
138 | | |
139 | 452 | AppendLastProcessedBlock(obj, *pwallet); |
140 | 452 | return obj; |
141 | 452 | }, |
142 | 1.28k | }; |
143 | 1.28k | } |
144 | | |
145 | | static RPCMethod listwalletdir() |
146 | 911 | { |
147 | 911 | return RPCMethod{"listwalletdir", |
148 | 911 | "Returns a list of wallets in the wallet directory.\n", |
149 | 911 | {}, |
150 | 911 | RPCResult{ |
151 | 911 | RPCResult::Type::OBJ, "", "", |
152 | 911 | { |
153 | 911 | {RPCResult::Type::ARR, "wallets", "", |
154 | 911 | { |
155 | 911 | {RPCResult::Type::OBJ, "", "", |
156 | 911 | { |
157 | 911 | {RPCResult::Type::STR, "name", "The wallet name"}, |
158 | 911 | {RPCResult::Type::ARR, "warnings", /*optional=*/true, "Warning messages, if any, related to loading the wallet.", |
159 | 911 | { |
160 | 911 | {RPCResult::Type::STR, "", ""}, |
161 | 911 | }}, |
162 | 911 | }}, |
163 | 911 | }}, |
164 | 911 | } |
165 | 911 | }, |
166 | 911 | RPCExamples{ |
167 | 911 | HelpExampleCli("listwalletdir", "") |
168 | 911 | + HelpExampleRpc("listwalletdir", "") |
169 | 911 | }, |
170 | 911 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
171 | 911 | { |
172 | 74 | UniValue wallets(UniValue::VARR); |
173 | 1.56k | for (const auto& [path, db_type] : ListDatabases(GetWalletDir())) { |
174 | 1.56k | UniValue wallet(UniValue::VOBJ); |
175 | 1.56k | wallet.pushKV("name", path.utf8string()); |
176 | 1.56k | UniValue warnings(UniValue::VARR); |
177 | 1.56k | if (db_type == "bdb") { |
178 | 82 | warnings.push_back("This wallet is a legacy wallet and will need to be migrated with migratewallet before it can be loaded"); |
179 | 82 | } |
180 | 1.56k | wallet.pushKV("warnings", warnings); |
181 | 1.56k | wallets.push_back(std::move(wallet)); |
182 | 1.56k | } |
183 | | |
184 | 74 | UniValue result(UniValue::VOBJ); |
185 | 74 | result.pushKV("wallets", std::move(wallets)); |
186 | 74 | return result; |
187 | 74 | }, |
188 | 911 | }; |
189 | 911 | } |
190 | | |
191 | | static RPCMethod listwallets() |
192 | 920 | { |
193 | 920 | return RPCMethod{"listwallets", |
194 | 920 | "Returns a list of currently loaded wallets.\n" |
195 | 920 | "For full information on the wallet, use \"getwalletinfo\"\n", |
196 | 920 | {}, |
197 | 920 | RPCResult{ |
198 | 920 | RPCResult::Type::ARR, "", "", |
199 | 920 | { |
200 | 920 | {RPCResult::Type::STR, "walletname", "the wallet name"}, |
201 | 920 | } |
202 | 920 | }, |
203 | 920 | RPCExamples{ |
204 | 920 | HelpExampleCli("listwallets", "") |
205 | 920 | + HelpExampleRpc("listwallets", "") |
206 | 920 | }, |
207 | 920 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
208 | 920 | { |
209 | 83 | UniValue obj(UniValue::VARR); |
210 | | |
211 | 83 | WalletContext& context = EnsureWalletContext(request.context); |
212 | 562 | for (const std::shared_ptr<CWallet>& wallet : GetWallets(context)) { |
213 | 562 | LOCK(wallet->cs_wallet); |
214 | 562 | obj.push_back(wallet->GetName()); |
215 | 562 | } |
216 | | |
217 | 83 | return obj; |
218 | 83 | }, |
219 | 920 | }; |
220 | 920 | } |
221 | | |
222 | | static RPCMethod loadwallet() |
223 | 1.00k | { |
224 | 1.00k | return RPCMethod{ |
225 | 1.00k | "loadwallet", |
226 | 1.00k | "Loads a wallet from a wallet file or directory." |
227 | 1.00k | "\nNote that all wallet command-line options used when starting bitcoind will be" |
228 | 1.00k | "\napplied to the new wallet.\n", |
229 | 1.00k | { |
230 | 1.00k | {"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The path to the directory of the wallet to be loaded, either absolute or relative to the \"wallets\" directory. The \"wallets\" directory is set by the -walletdir option and defaults to the \"wallets\" folder within the data directory."}, |
231 | 1.00k | {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, |
232 | 1.00k | }, |
233 | 1.00k | RPCResult{ |
234 | 1.00k | RPCResult::Type::OBJ, "", "", |
235 | 1.00k | { |
236 | 1.00k | {RPCResult::Type::STR, "name", "The wallet name if loaded successfully."}, |
237 | 1.00k | {RPCResult::Type::ARR, "warnings", /*optional=*/true, "Warning messages, if any, related to loading the wallet.", |
238 | 1.00k | { |
239 | 1.00k | {RPCResult::Type::STR, "", ""}, |
240 | 1.00k | }}, |
241 | 1.00k | } |
242 | 1.00k | }, |
243 | 1.00k | RPCExamples{ |
244 | 1.00k | "\nLoad wallet from the wallet dir:\n" |
245 | 1.00k | + HelpExampleCli("loadwallet", "\"walletname\"") |
246 | 1.00k | + HelpExampleRpc("loadwallet", "\"walletname\"") |
247 | 1.00k | + "\nLoad wallet using absolute path (Unix):\n" |
248 | 1.00k | + HelpExampleCli("loadwallet", "\"/path/to/walletname/\"") |
249 | 1.00k | + HelpExampleRpc("loadwallet", "\"/path/to/walletname/\"") |
250 | 1.00k | + "\nLoad wallet using absolute path (Windows):\n" |
251 | 1.00k | + HelpExampleCli("loadwallet", "\"DriveLetter:\\path\\to\\walletname\\\"") |
252 | 1.00k | + HelpExampleRpc("loadwallet", R"("DriveLetter:\\path\\to\\walletname")") |
253 | 1.00k | }, |
254 | 1.00k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
255 | 1.00k | { |
256 | 163 | WalletContext& context = EnsureWalletContext(request.context); |
257 | 163 | const std::string name(request.params[0].get_str()); |
258 | | |
259 | 163 | DatabaseOptions options; |
260 | 163 | DatabaseStatus status; |
261 | 163 | ReadDatabaseArgs(*context.args, options); |
262 | 163 | options.require_existing = true; |
263 | 163 | bilingual_str error; |
264 | 163 | std::vector<bilingual_str> warnings; |
265 | 163 | std::optional<bool> load_on_start = request.params[1].isNull() ? std::nullopt : std::optional<bool>(request.params[1].get_bool()); |
266 | | |
267 | 163 | { |
268 | 163 | LOCK(context.wallets_mutex); |
269 | 677 | if (std::any_of(context.wallets.begin(), context.wallets.end(), [&name](const auto& wallet) { return wallet->GetName() == name; })) { |
270 | 3 | throw JSONRPCError(RPC_WALLET_ALREADY_LOADED, "Wallet \"" + name + "\" is already loaded."); |
271 | 3 | } |
272 | 163 | } |
273 | | |
274 | 160 | std::shared_ptr<CWallet> const wallet = LoadWallet(context, name, load_on_start, options, status, error, warnings); |
275 | | |
276 | 160 | HandleWalletError(wallet, status, error); |
277 | | |
278 | 160 | UniValue obj(UniValue::VOBJ); |
279 | 160 | obj.pushKV("name", wallet->GetName()); |
280 | 160 | PushWarnings(warnings, obj); |
281 | | |
282 | 160 | return obj; |
283 | 163 | }, |
284 | 1.00k | }; |
285 | 1.00k | } |
286 | | |
287 | | static RPCMethod setwalletflag() |
288 | 845 | { |
289 | 845 | std::string flags; |
290 | 845 | for (auto& it : STRING_TO_WALLET_FLAG) |
291 | 5.91k | if (it.second & MUTABLE_WALLET_FLAGS) |
292 | 845 | flags += (flags == "" ? "" : ", ") + it.first; |
293 | | |
294 | 845 | return RPCMethod{ |
295 | 845 | "setwalletflag", |
296 | 845 | "Change the state of the given wallet flag for a wallet.\n", |
297 | 845 | { |
298 | 845 | {"flag", RPCArg::Type::STR, RPCArg::Optional::NO, "The name of the flag to change. Current available flags: " + flags}, |
299 | 845 | {"value", RPCArg::Type::BOOL, RPCArg::Default{true}, "The new state."}, |
300 | 845 | }, |
301 | 845 | RPCResult{ |
302 | 845 | RPCResult::Type::OBJ, "", "", |
303 | 845 | { |
304 | 845 | {RPCResult::Type::STR, "flag_name", "The name of the flag that was modified"}, |
305 | 845 | {RPCResult::Type::BOOL, "flag_state", "The new state of the flag"}, |
306 | 845 | {RPCResult::Type::STR, "warnings", /*optional=*/true, "Any warnings associated with the change"}, |
307 | 845 | } |
308 | 845 | }, |
309 | 845 | RPCExamples{ |
310 | 845 | HelpExampleCli("setwalletflag", "avoid_reuse") |
311 | 845 | + HelpExampleRpc("setwalletflag", "\"avoid_reuse\"") |
312 | 845 | }, |
313 | 845 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
314 | 845 | { |
315 | 8 | std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); |
316 | 8 | if (!pwallet) return UniValue::VNULL; |
317 | | |
318 | 8 | std::string flag_str = request.params[0].get_str(); |
319 | 8 | bool value = request.params[1].isNull() || request.params[1].get_bool(); |
320 | | |
321 | 8 | if (!STRING_TO_WALLET_FLAG.contains(flag_str)) { |
322 | 1 | throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unknown wallet flag: %s", flag_str)); |
323 | 1 | } |
324 | | |
325 | 7 | auto flag = STRING_TO_WALLET_FLAG.at(flag_str); |
326 | | |
327 | 7 | if (!(flag & MUTABLE_WALLET_FLAGS)) { |
328 | 3 | throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Wallet flag is immutable: %s", flag_str)); |
329 | 3 | } |
330 | | |
331 | 4 | UniValue res(UniValue::VOBJ); |
332 | | |
333 | 4 | if (pwallet->IsWalletFlagSet(flag) == value) { |
334 | 2 | throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Wallet flag is already set to %s: %s", value ? "true" : "false", flag_str)); |
335 | 2 | } |
336 | | |
337 | 2 | res.pushKV("flag_name", flag_str); |
338 | 2 | res.pushKV("flag_state", value); |
339 | | |
340 | 2 | if (value) { |
341 | 1 | pwallet->SetWalletFlag(flag); |
342 | 1 | } else { |
343 | 1 | pwallet->UnsetWalletFlag(flag); |
344 | 1 | } |
345 | | |
346 | 2 | if (flag && value && WALLET_FLAG_CAVEATS.contains(flag)) { |
347 | 1 | res.pushKV("warnings", WALLET_FLAG_CAVEATS.at(flag)); |
348 | 1 | } |
349 | | |
350 | 2 | return res; |
351 | 4 | }, |
352 | 845 | }; |
353 | 845 | } |
354 | | |
355 | | static RPCMethod createwallet() |
356 | 1.49k | { |
357 | 1.49k | return RPCMethod{ |
358 | 1.49k | "createwallet", |
359 | 1.49k | "Creates and loads a new wallet.\n", |
360 | 1.49k | { |
361 | 1.49k | {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name for the new wallet. If this is a path, the wallet will be created at the path location."}, |
362 | 1.49k | {"disable_private_keys", RPCArg::Type::BOOL, RPCArg::Default{false}, "Disable the possibility of private keys (only watchonlys are possible in this mode)."}, |
363 | 1.49k | {"blank", RPCArg::Type::BOOL, RPCArg::Default{false}, "Create a blank wallet. A blank wallet has no keys."}, |
364 | 1.49k | {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."}, |
365 | 1.49k | {"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{false}, "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, |
366 | 1.49k | {"descriptors", RPCArg::Type::BOOL, RPCArg::Default{true}, "If set, must be \"true\""}, |
367 | 1.49k | {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, |
368 | 1.49k | {"external_signer", RPCArg::Type::BOOL, RPCArg::Default{false}, "Use an external signer such as a hardware wallet. Requires -signer to be configured. Wallet creation will fail if keys cannot be fetched. Requires disable_private_keys and descriptors set to true."}, |
369 | 1.49k | }, |
370 | 1.49k | RPCResult{ |
371 | 1.49k | RPCResult::Type::OBJ, "", "", |
372 | 1.49k | { |
373 | 1.49k | {RPCResult::Type::STR, "name", "The wallet name if created successfully. If the wallet was created using a full path, the wallet_name will be the full path."}, |
374 | 1.49k | {RPCResult::Type::ARR, "warnings", /*optional=*/true, "Warning messages, if any, related to creating and loading the wallet.", |
375 | 1.49k | { |
376 | 1.49k | {RPCResult::Type::STR, "", ""}, |
377 | 1.49k | }}, |
378 | 1.49k | } |
379 | 1.49k | }, |
380 | 1.49k | RPCExamples{ |
381 | 1.49k | HelpExampleCli("createwallet", "\"testwallet\"") |
382 | 1.49k | + HelpExampleRpc("createwallet", "\"testwallet\"") |
383 | 1.49k | + HelpExampleCliNamed("createwallet", {{"wallet_name", "descriptors"}, {"avoid_reuse", true}, {"load_on_startup", true}}) |
384 | 1.49k | + HelpExampleRpcNamed("createwallet", {{"wallet_name", "descriptors"}, {"avoid_reuse", true}, {"load_on_startup", true}}) |
385 | 1.49k | }, |
386 | 1.49k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
387 | 1.49k | { |
388 | 661 | WalletContext& context = EnsureWalletContext(request.context); |
389 | 661 | uint64_t flags = 0; |
390 | 661 | if (!request.params[1].isNull() && request.params[1].get_bool()) { |
391 | 113 | flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS; |
392 | 113 | } |
393 | | |
394 | 661 | if (!request.params[2].isNull() && request.params[2].get_bool()) { |
395 | 175 | flags |= WALLET_FLAG_BLANK_WALLET; |
396 | 175 | } |
397 | 661 | SecureString passphrase; |
398 | 661 | passphrase.reserve(100); |
399 | 661 | std::vector<bilingual_str> warnings; |
400 | 661 | if (!request.params[3].isNull()) { |
401 | 17 | passphrase = std::string_view{request.params[3].get_str()}; |
402 | 17 | if (passphrase.empty()) { |
403 | | // Empty string means unencrypted |
404 | 4 | warnings.emplace_back(Untranslated("Empty string given as passphrase, wallet will not be encrypted.")); |
405 | 4 | } |
406 | 17 | } |
407 | | |
408 | 661 | if (!request.params[4].isNull() && request.params[4].get_bool()) { |
409 | 4 | flags |= WALLET_FLAG_AVOID_REUSE; |
410 | 4 | } |
411 | 661 | flags |= WALLET_FLAG_DESCRIPTORS; |
412 | 661 | if (!self.Arg<bool>("descriptors")) { |
413 | 2 | throw JSONRPCError(RPC_WALLET_ERROR, "descriptors argument must be set to \"true\"; it is no longer possible to create a legacy wallet."); |
414 | 2 | } |
415 | 659 | if (!request.params[7].isNull() && request.params[7].get_bool()) { |
416 | 6 | #ifdef ENABLE_EXTERNAL_SIGNER |
417 | 6 | flags |= WALLET_FLAG_EXTERNAL_SIGNER; |
418 | | #else |
419 | | throw JSONRPCError(RPC_WALLET_ERROR, "Compiled without external signing support (required for external signing)"); |
420 | | #endif |
421 | 6 | } |
422 | | |
423 | 659 | DatabaseOptions options; |
424 | 659 | DatabaseStatus status; |
425 | 659 | ReadDatabaseArgs(*context.args, options); |
426 | 659 | options.require_create = true; |
427 | 659 | options.create_flags = flags; |
428 | 659 | options.create_passphrase = passphrase; |
429 | 659 | bilingual_str error; |
430 | 659 | std::optional<bool> load_on_start = request.params[6].isNull() ? std::nullopt : std::optional<bool>(request.params[6].get_bool()); |
431 | 659 | const std::shared_ptr<CWallet> wallet = CreateWallet(context, request.params[0].get_str(), load_on_start, options, status, error, warnings); |
432 | 659 | HandleWalletError(wallet, status, error); |
433 | | |
434 | 659 | UniValue obj(UniValue::VOBJ); |
435 | 659 | obj.pushKV("name", wallet->GetName()); |
436 | 659 | PushWarnings(warnings, obj); |
437 | | |
438 | 659 | return obj; |
439 | 661 | }, |
440 | 1.49k | }; |
441 | 1.49k | } |
442 | | |
443 | | static RPCMethod unloadwallet() |
444 | 1.15k | { |
445 | 1.15k | return RPCMethod{"unloadwallet", |
446 | 1.15k | "Unloads the wallet referenced by the request endpoint or the wallet_name argument.\n" |
447 | 1.15k | "If both are specified, they must be identical.", |
448 | 1.15k | { |
449 | 1.15k | {"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to unload. If provided both here and in the RPC endpoint, the two must be identical."}, |
450 | 1.15k | {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, |
451 | 1.15k | }, |
452 | 1.15k | RPCResult{RPCResult::Type::OBJ, "", "", { |
453 | 1.15k | {RPCResult::Type::ARR, "warnings", /*optional=*/true, "Warning messages, if any, related to unloading the wallet.", |
454 | 1.15k | { |
455 | 1.15k | {RPCResult::Type::STR, "", ""}, |
456 | 1.15k | }}, |
457 | 1.15k | }}, |
458 | 1.15k | RPCExamples{ |
459 | 1.15k | HelpExampleCli("unloadwallet", "wallet_name") |
460 | 1.15k | + HelpExampleRpc("unloadwallet", R"("wallet_name")") |
461 | 1.15k | }, |
462 | 1.15k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
463 | 1.15k | { |
464 | 321 | const std::string wallet_name{EnsureUniqueWalletName(request, self.MaybeArg<std::string_view>("wallet_name"))}; |
465 | | |
466 | 321 | WalletContext& context = EnsureWalletContext(request.context); |
467 | 321 | std::shared_ptr<CWallet> wallet = GetWallet(context, wallet_name); |
468 | 321 | if (!wallet) { |
469 | 4 | throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Requested wallet does not exist or is not loaded"); |
470 | 4 | } |
471 | | |
472 | 317 | std::vector<bilingual_str> warnings; |
473 | 317 | { |
474 | 317 | WalletRescanReserver reserver(*wallet); |
475 | 317 | if (!reserver.reserve()) { |
476 | 0 | throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait."); |
477 | 0 | } |
478 | | |
479 | | // Release the "main" shared pointer and prevent further notifications. |
480 | | // Note that any attempt to load the same wallet would fail until the wallet |
481 | | // is destroyed (see CheckUniqueFileid). |
482 | 317 | std::optional<bool> load_on_start{self.MaybeArg<bool>("load_on_startup")}; |
483 | 317 | if (!RemoveWallet(context, wallet, load_on_start, warnings)) { |
484 | 0 | throw JSONRPCError(RPC_MISC_ERROR, "Requested wallet already unloaded"); |
485 | 0 | } |
486 | 317 | } |
487 | | |
488 | 317 | WaitForDeleteWallet(std::move(wallet)); |
489 | | |
490 | 317 | UniValue result(UniValue::VOBJ); |
491 | 317 | PushWarnings(warnings, result); |
492 | | |
493 | 317 | return result; |
494 | 317 | }, |
495 | 1.15k | }; |
496 | 1.15k | } |
497 | | |
498 | | RPCMethod simulaterawtransaction() |
499 | 863 | { |
500 | 863 | return RPCMethod{ |
501 | 863 | "simulaterawtransaction", |
502 | 863 | "Calculate the balance change resulting in the signing and broadcasting of the given transaction(s).\n", |
503 | 863 | { |
504 | 863 | {"rawtxs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "An array of hex strings of raw transactions.\n", |
505 | 863 | { |
506 | 863 | {"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""}, |
507 | 863 | }, |
508 | 863 | }, |
509 | 863 | {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", |
510 | 863 | { |
511 | 863 | {"include_watchonly", RPCArg::Type::BOOL, RPCArg::Default{false}, "(DEPRECATED) No longer used"}, |
512 | 863 | }, |
513 | 863 | }, |
514 | 863 | }, |
515 | 863 | RPCResult{ |
516 | 863 | RPCResult::Type::OBJ, "", "", |
517 | 863 | { |
518 | 863 | {RPCResult::Type::STR_AMOUNT, "balance_change", "The wallet balance change (negative means decrease)."}, |
519 | 863 | } |
520 | 863 | }, |
521 | 863 | RPCExamples{ |
522 | 863 | HelpExampleCli("simulaterawtransaction", "[\"myhex\"]") |
523 | 863 | + HelpExampleRpc("simulaterawtransaction", "[\"myhex\"]") |
524 | 863 | }, |
525 | 863 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
526 | 863 | { |
527 | 26 | const std::shared_ptr<const CWallet> rpc_wallet = GetWalletForJSONRPCRequest(request); |
528 | 26 | if (!rpc_wallet) return UniValue::VNULL; |
529 | 26 | const CWallet& wallet = *rpc_wallet; |
530 | | |
531 | 26 | LOCK(wallet.cs_wallet); |
532 | | |
533 | 26 | const auto& txs = request.params[0].get_array(); |
534 | 26 | CAmount changes{0}; |
535 | 26 | std::map<COutPoint, CAmount> new_utxos; // UTXO:s that were made available in transaction array |
536 | 26 | std::set<COutPoint> spent; |
537 | | |
538 | 54 | for (size_t i = 0; i < txs.size(); ++i) { |
539 | 38 | CMutableTransaction mtx; |
540 | 38 | if (!DecodeHexTx(mtx, txs[i].get_str(), /*try_no_witness=*/ true, /*try_witness=*/ true)) { |
541 | 0 | throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction hex string decoding failure."); |
542 | 0 | } |
543 | | |
544 | | // Fetch previous transactions (inputs) |
545 | 38 | std::map<COutPoint, Coin> coins; |
546 | 38 | for (const CTxIn& txin : mtx.vin) { |
547 | 29 | coins[txin.prevout]; // Create empty map entry keyed by prevout. |
548 | 29 | } |
549 | 38 | wallet.chain().findCoins(coins); |
550 | | |
551 | | // Fetch debit; we are *spending* these; if the transaction is signed and |
552 | | // broadcast, we will lose everything in these |
553 | 38 | for (const auto& txin : mtx.vin) { |
554 | 29 | const auto& outpoint = txin.prevout; |
555 | 29 | if (spent.contains(outpoint)) { |
556 | 3 | throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction(s) are spending the same output more than once"); |
557 | 3 | } |
558 | 26 | if (new_utxos.contains(outpoint)) { |
559 | 6 | changes -= new_utxos.at(outpoint); |
560 | 6 | new_utxos.erase(outpoint); |
561 | 20 | } else { |
562 | 20 | if (coins.at(outpoint).IsSpent()) { |
563 | 7 | throw JSONRPCError(RPC_INVALID_PARAMETER, "One or more transaction inputs are missing or have been spent already"); |
564 | 7 | } |
565 | 13 | changes -= wallet.GetDebit(txin); |
566 | 13 | } |
567 | 19 | spent.insert(outpoint); |
568 | 19 | } |
569 | | |
570 | | // Iterate over outputs; we are *receiving* these, if the wallet considers |
571 | | // them "mine"; if the transaction is signed and broadcast, we will receive |
572 | | // everything in these |
573 | | // Also populate new_utxos in case these are spent in later transactions |
574 | | |
575 | 28 | const auto& hash = mtx.GetHash(); |
576 | 69 | for (size_t i = 0; i < mtx.vout.size(); ++i) { |
577 | 41 | const auto& txout = mtx.vout[i]; |
578 | 41 | bool is_mine = wallet.IsMine(txout); |
579 | 41 | changes += new_utxos[COutPoint(hash, i)] = is_mine ? txout.nValue : 0; |
580 | 41 | } |
581 | 28 | } |
582 | | |
583 | 16 | UniValue result(UniValue::VOBJ); |
584 | 16 | result.pushKV("balance_change", ValueFromAmount(changes)); |
585 | | |
586 | 16 | return result; |
587 | 26 | } |
588 | 863 | }; |
589 | 863 | } |
590 | | |
591 | | static RPCMethod migratewallet() |
592 | 894 | { |
593 | 894 | return RPCMethod{ |
594 | 894 | "migratewallet", |
595 | 894 | "Migrate the wallet to a descriptor wallet.\n" |
596 | 894 | "A new wallet backup will need to be made.\n" |
597 | 894 | "\nThe migration process will create a backup of the wallet before migrating. This backup\n" |
598 | 894 | "file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n" |
599 | 894 | "for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." |
600 | 894 | "\nEncrypted wallets must have the passphrase provided as an argument to this call.\n" |
601 | 894 | "\nThis RPC may take a long time to complete. Increasing the RPC client timeout is recommended.", |
602 | 894 | { |
603 | 894 | {"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to migrate. If provided both here and in the RPC endpoint, the two must be identical."}, |
604 | 894 | {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"}, |
605 | 894 | {"load_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "Load the wallet after migration."}, |
606 | 894 | }, |
607 | 894 | RPCResult{ |
608 | 894 | RPCResult::Type::OBJ, "", "", |
609 | 894 | { |
610 | 894 | {RPCResult::Type::STR, "wallet_name", "The name of the primary migrated wallet"}, |
611 | 894 | {RPCResult::Type::STR, "watchonly_name", /*optional=*/true, "The name of the migrated wallet containing the watchonly scripts"}, |
612 | 894 | {RPCResult::Type::STR, "solvables_name", /*optional=*/true, "The name of the migrated wallet containing solvable but not watched scripts"}, |
613 | 894 | {RPCResult::Type::STR, "backup_path", "The location of the backup of the original wallet"}, |
614 | 894 | } |
615 | 894 | }, |
616 | 894 | RPCExamples{ |
617 | 894 | HelpExampleCli("migratewallet", "") |
618 | 894 | + HelpExampleRpc("migratewallet", "") |
619 | 894 | }, |
620 | 894 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
621 | 894 | { |
622 | 57 | const std::string wallet_name{EnsureUniqueWalletName(request, self.MaybeArg<std::string_view>("wallet_name"))}; |
623 | | |
624 | 57 | SecureString wallet_pass; |
625 | 57 | wallet_pass.reserve(100); |
626 | 57 | if (!request.params[1].isNull()) { |
627 | 5 | wallet_pass = std::string_view{request.params[1].get_str()}; |
628 | 5 | } |
629 | | |
630 | 57 | const bool loadwallet = self.Arg<bool>("load_wallet"); |
631 | | |
632 | 57 | WalletContext& context = EnsureWalletContext(request.context); |
633 | 57 | util::Result<MigrationResult> res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context, loadwallet); |
634 | 57 | if (!res) { |
635 | 12 | throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); |
636 | 12 | } |
637 | | |
638 | 45 | UniValue r{UniValue::VOBJ}; |
639 | 45 | r.pushKV("wallet_name", res->wallet_name); |
640 | 45 | if (res->watchonly_wallet_name.has_value()) { |
641 | 12 | r.pushKV("watchonly_name", res->watchonly_wallet_name.value()); |
642 | 12 | } |
643 | 45 | if (res->solvables_wallet_name.has_value()) { |
644 | 6 | r.pushKV("solvables_name", res->solvables_wallet_name.value()); |
645 | 6 | } |
646 | 45 | r.pushKV("backup_path", res->backup_path.utf8string()); |
647 | | |
648 | 45 | return r; |
649 | 57 | }, |
650 | 894 | }; |
651 | 894 | } |
652 | | |
653 | | RPCMethod gethdkeys() |
654 | 879 | { |
655 | 879 | return RPCMethod{ |
656 | 879 | "gethdkeys", |
657 | 879 | "List all BIP 32 HD keys in the wallet and which descriptors use them.\n", |
658 | 879 | { |
659 | 879 | {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", { |
660 | 879 | {"active_only", RPCArg::Type::BOOL, RPCArg::Default{false}, "Show the keys for only active descriptors"}, |
661 | 879 | {"private", RPCArg::Type::BOOL, RPCArg::Default{false}, "Show private keys"} |
662 | 879 | }}, |
663 | 879 | }, |
664 | 879 | RPCResult{RPCResult::Type::ARR, "", "", { |
665 | 879 | { |
666 | 879 | {RPCResult::Type::OBJ, "", "", { |
667 | 879 | {RPCResult::Type::STR, "xpub", "The extended public key"}, |
668 | 879 | {RPCResult::Type::BOOL, "has_private", "Whether the wallet has the private key for this xpub"}, |
669 | 879 | {RPCResult::Type::STR, "xprv", /*optional=*/true, "The extended private key if \"private\" is true"}, |
670 | 879 | {RPCResult::Type::ARR, "descriptors", "Array of descriptor objects that use this HD key", |
671 | 879 | { |
672 | 879 | {RPCResult::Type::OBJ, "", "", { |
673 | 879 | {RPCResult::Type::STR, "desc", "Descriptor string public representation"}, |
674 | 879 | {RPCResult::Type::BOOL, "active", "Whether this descriptor is currently used to generate new addresses"}, |
675 | 879 | }}, |
676 | 879 | }}, |
677 | 879 | }}, |
678 | 879 | } |
679 | 879 | }}, |
680 | 879 | RPCExamples{ |
681 | 879 | HelpExampleCli("gethdkeys", "") + HelpExampleRpc("gethdkeys", "") |
682 | 879 | + HelpExampleCliNamed("gethdkeys", {{"active_only", "true"}, {"private", "true"}}) + HelpExampleRpcNamed("gethdkeys", {{"active_only", "true"}, {"private", "true"}}) |
683 | 879 | }, |
684 | 879 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
685 | 879 | { |
686 | 42 | const std::shared_ptr<const CWallet> wallet = GetWalletForJSONRPCRequest(request); |
687 | 42 | if (!wallet) return UniValue::VNULL; |
688 | | |
689 | 42 | LOCK(wallet->cs_wallet); |
690 | | |
691 | 42 | UniValue options{request.params[0].isNull() ? UniValue::VOBJ : request.params[0]}; |
692 | 42 | const bool active_only{options.exists("active_only") ? options["active_only"].get_bool() : false}; |
693 | 42 | const bool priv{options.exists("private") ? options["private"].get_bool() : false}; |
694 | 42 | if (priv) { |
695 | 12 | EnsureWalletIsUnlocked(*wallet); |
696 | 12 | } |
697 | | |
698 | 42 | std::map<CExtPubKey, std::set<std::tuple<std::string, bool, bool>>> wallet_xpubs; |
699 | 42 | std::map<CExtPubKey, CExtKey> wallet_xprvs; |
700 | 46 | for (const auto& [xpub, spkms] : wallet->GetHDPubKeys(active_only ? HDKeyFilter::Active : HDKeyFilter::All)) { |
701 | 280 | for (auto* desc_spkm : spkms) { |
702 | 280 | LOCK(desc_spkm->cs_desc_man); |
703 | 280 | std::string desc_str; |
704 | 280 | bool ok = desc_spkm->GetDescriptorString(desc_str, /*priv=*/false); |
705 | 280 | CHECK_NONFATAL(ok); |
706 | 280 | wallet_xpubs[xpub].emplace(desc_str, wallet->IsActiveScriptPubKeyMan(*desc_spkm), desc_spkm->HasPrivKey(xpub.pubkey.GetID())); |
707 | 280 | if (std::optional<CKey> key = priv ? desc_spkm->GetKey(xpub.pubkey.GetID()) : std::nullopt) { |
708 | 89 | wallet_xprvs[xpub] = CExtKey(xpub, *key); |
709 | 89 | } |
710 | 280 | } |
711 | 46 | } |
712 | | |
713 | 42 | UniValue response(UniValue::VARR); |
714 | 46 | for (const auto& [xpub, descs] : wallet_xpubs) { |
715 | 46 | bool has_xprv = false; |
716 | 46 | UniValue descriptors(UniValue::VARR); |
717 | 280 | for (const auto& [desc, active, has_priv] : descs) { |
718 | 280 | UniValue d(UniValue::VOBJ); |
719 | 280 | d.pushKV("desc", desc); |
720 | 280 | d.pushKV("active", active); |
721 | 280 | has_xprv |= has_priv; |
722 | | |
723 | 280 | descriptors.push_back(std::move(d)); |
724 | 280 | } |
725 | 46 | UniValue xpub_info(UniValue::VOBJ); |
726 | 46 | xpub_info.pushKV("xpub", EncodeExtPubKey(xpub)); |
727 | 46 | xpub_info.pushKV("has_private", has_xprv); |
728 | 46 | if (priv && has_xprv) { |
729 | 11 | xpub_info.pushKV("xprv", EncodeExtKey(wallet_xprvs.at(xpub))); |
730 | 11 | } |
731 | 46 | xpub_info.pushKV("descriptors", std::move(descriptors)); |
732 | | |
733 | 46 | response.push_back(std::move(xpub_info)); |
734 | 46 | } |
735 | | |
736 | 42 | return response; |
737 | 42 | }, |
738 | 879 | }; |
739 | 879 | } |
740 | | |
741 | | static RPCMethod createwalletdescriptor() |
742 | 851 | { |
743 | 851 | return RPCMethod{"createwalletdescriptor", |
744 | 851 | "Creates the wallet's descriptor for the given address type. " |
745 | 851 | "The address type must be one that the wallet does not already have a descriptor for." |
746 | 851 | + HELP_REQUIRING_PASSPHRASE, |
747 | 851 | { |
748 | 851 | {"type", RPCArg::Type::STR, RPCArg::Optional::NO, "The address type the descriptor will produce. Options are " + FormatAllOutputTypes() + "."}, |
749 | 851 | {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", { |
750 | 851 | {"internal", RPCArg::Type::BOOL, RPCArg::DefaultHint{"Both external and internal will be generated unless this parameter is specified"}, "Whether to only make one descriptor that is internal (if parameter is true) or external (if parameter is false)"}, |
751 | 851 | {"hdkey", RPCArg::Type::STR, RPCArg::DefaultHint{"The HD key used by all other active descriptors"}, "The HD key that the wallet knows the private key of, listed using 'gethdkeys', to use for this descriptor's key"}, |
752 | 851 | }}, |
753 | 851 | }, |
754 | 851 | RPCResult{ |
755 | 851 | RPCResult::Type::OBJ, "", "", |
756 | 851 | { |
757 | 851 | {RPCResult::Type::ARR, "descs", "The public descriptors that were added to the wallet", |
758 | 851 | {{RPCResult::Type::STR, "", ""}} |
759 | 851 | } |
760 | 851 | }, |
761 | 851 | }, |
762 | 851 | RPCExamples{ |
763 | 851 | HelpExampleCli("createwalletdescriptor", "bech32m") |
764 | 851 | + HelpExampleRpc("createwalletdescriptor", R"("bech32m")") |
765 | 851 | }, |
766 | 851 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
767 | 851 | { |
768 | 14 | std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); |
769 | 14 | if (!pwallet) return UniValue::VNULL; |
770 | | |
771 | 14 | std::optional<OutputType> output_type = ParseOutputType(request.params[0].get_str()); |
772 | 14 | if (!output_type) { |
773 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[0].get_str())); |
774 | 1 | } |
775 | | |
776 | 13 | UniValue options{request.params[1].isNull() ? UniValue::VOBJ : request.params[1]}; |
777 | 13 | UniValue internal_only{options["internal"]}; |
778 | 13 | UniValue hdkey{options["hdkey"]}; |
779 | | |
780 | 13 | std::vector<bool> internals; |
781 | 13 | if (internal_only.isNull()) { |
782 | 11 | internals.push_back(false); |
783 | 11 | internals.push_back(true); |
784 | 11 | } else { |
785 | 2 | internals.push_back(internal_only.get_bool()); |
786 | 2 | } |
787 | | |
788 | 13 | LOCK(pwallet->cs_wallet); |
789 | 13 | EnsureWalletIsUnlocked(*pwallet); |
790 | | |
791 | 13 | CExtPubKey xpub; |
792 | 13 | if (hdkey.isNull()) { |
793 | 7 | HDPubKeyMap active_xpubs = pwallet->GetHDPubKeys(HDKeyFilter::Active); |
794 | 7 | if (active_xpubs.size() != 1) { |
795 | 2 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to determine which HD key to use from active descriptors. Please specify with 'hdkey'"); |
796 | 2 | } |
797 | 5 | xpub = active_xpubs.begin()->first; |
798 | 6 | } else { |
799 | 6 | xpub = DecodeExtPubKey(hdkey.get_str()); |
800 | 6 | if (!xpub.pubkey.IsValid()) { |
801 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to parse HD key. Please provide a valid xpub"); |
802 | 1 | } |
803 | 6 | } |
804 | | |
805 | 10 | std::optional<CKey> key = pwallet->GetKey(xpub.pubkey.GetID()); |
806 | 10 | if (!key) { |
807 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Private key for %s is not known", EncodeExtPubKey(xpub))); |
808 | 1 | } |
809 | 9 | CExtKey active_hdkey(xpub, *key); |
810 | | |
811 | 9 | std::vector<std::reference_wrapper<DescriptorScriptPubKeyMan>> spkms; |
812 | 9 | WalletBatch batch{pwallet->GetDatabase()}; |
813 | 14 | for (bool internal : internals) { |
814 | 14 | WalletDescriptor w_desc = GenerateWalletDescriptor(xpub, *output_type, internal); |
815 | 14 | uint256 w_id = DescriptorID(*w_desc.descriptor); |
816 | 14 | if (!pwallet->GetScriptPubKeyMan(w_id)) { |
817 | 12 | spkms.emplace_back(pwallet->SetupDescriptorScriptPubKeyMan(batch, active_hdkey, *output_type, internal)); |
818 | 12 | } |
819 | 14 | } |
820 | 9 | if (spkms.empty()) { |
821 | 1 | throw JSONRPCError(RPC_WALLET_ERROR, "Descriptor already exists"); |
822 | 1 | } |
823 | | |
824 | | // Fetch each descspkm from the wallet in order to get the descriptor strings |
825 | 8 | UniValue descs{UniValue::VARR}; |
826 | 12 | for (const auto& spkm : spkms) { |
827 | 12 | std::string desc_str; |
828 | 12 | bool ok = spkm.get().GetDescriptorString(desc_str, false); |
829 | 12 | CHECK_NONFATAL(ok); |
830 | 12 | descs.push_back(desc_str); |
831 | 12 | } |
832 | 8 | UniValue out{UniValue::VOBJ}; |
833 | 8 | out.pushKV("descs", std::move(descs)); |
834 | 8 | return out; |
835 | 9 | } |
836 | 851 | }; |
837 | 851 | } |
838 | | |
839 | | RPCMethod addhdkey() |
840 | 847 | { |
841 | 847 | return RPCMethod{ |
842 | 847 | "addhdkey", |
843 | 847 | "Add a BIP 32 HD key to the wallet that can be used with 'createwalletdescriptor'\n", |
844 | 847 | { |
845 | 847 | {"hdkey", RPCArg::Type::STR, RPCArg::DefaultHint{"Automatically generated new key"}, "The BIP 32 extended private key to add. If none is provided, a randomly generated one will be added."}, |
846 | 847 | }, |
847 | 847 | RPCResult{ |
848 | 847 | RPCResult::Type::OBJ, "", "", |
849 | 847 | { |
850 | 847 | {RPCResult::Type::STR, "xpub", "The xpub of the HD key that was added to the wallet"} |
851 | 847 | }, |
852 | 847 | }, |
853 | 847 | RPCExamples{ |
854 | 847 | HelpExampleCli("addhdkey", "xprv") + HelpExampleRpc("addhdkey", R"("xprv")") |
855 | 847 | }, |
856 | 847 | [&](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
857 | 847 | { |
858 | 10 | std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request); |
859 | 10 | if (!wallet) return UniValue::VNULL; |
860 | | |
861 | 10 | if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { |
862 | 1 | throw JSONRPCError(RPC_WALLET_ERROR, "addhdkey is not available for wallets without private keys"); |
863 | 1 | } |
864 | | |
865 | 9 | EnsureWalletIsUnlocked(*wallet); |
866 | | |
867 | 9 | CExtKey hdkey; |
868 | 9 | if (request.params[0].isNull()) { |
869 | 6 | CKey seed_key = GenerateRandomKey(); |
870 | 6 | hdkey.SetSeed(seed_key); |
871 | 6 | } else { |
872 | 3 | hdkey = DecodeExtKey(request.params[0].get_str()); |
873 | 3 | if (!hdkey.key.IsValid()) { |
874 | | // Check if the user gave us an xpub and give a more descriptive error if so |
875 | 1 | CExtPubKey xpub = DecodeExtPubKey(request.params[0].get_str()); |
876 | 1 | if (xpub.pubkey.IsValid()) { |
877 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Extended public key (xpub) provided, but extended private key (xprv) is required"); |
878 | 1 | } else { |
879 | 0 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not parse HD key"); |
880 | 0 | } |
881 | 1 | } |
882 | 3 | } |
883 | | |
884 | 8 | LOCK(wallet->cs_wallet); |
885 | 8 | std::string desc_str = "unused(" + EncodeExtKey(hdkey) + ")"; |
886 | 8 | FlatSigningProvider keys; |
887 | 8 | std::string error; |
888 | 8 | std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_str, keys, error, false); |
889 | 8 | CHECK_NONFATAL(!descs.empty()); |
890 | 8 | WalletDescriptor w_desc(std::move(descs.at(0)), GetTime(), 0, 0, 0); |
891 | 8 | if (wallet->GetDescriptorScriptPubKeyMan(w_desc) != nullptr) { |
892 | 1 | throw JSONRPCError(RPC_WALLET_ERROR, "HD key already exists"); |
893 | 1 | } |
894 | | |
895 | 7 | auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false); |
896 | 7 | if (!spkm) { |
897 | 0 | throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(spkm).original); |
898 | 0 | } |
899 | | |
900 | 7 | UniValue response(UniValue::VOBJ); |
901 | 7 | const DescriptorScriptPubKeyMan& desc_spkm = spkm->get(); |
902 | 7 | LOCK(desc_spkm.cs_desc_man); |
903 | 7 | std::set<CPubKey> pubkeys; |
904 | 7 | std::set<CExtPubKey> extpubs; |
905 | 7 | desc_spkm.GetWalletDescriptor().descriptor->GetPubKeys(pubkeys, extpubs); |
906 | 7 | CHECK_NONFATAL(pubkeys.size() == 0); |
907 | 7 | CHECK_NONFATAL(extpubs.size() == 1); |
908 | 7 | response.pushKV("xpub", EncodeExtPubKey(*extpubs.begin())); |
909 | | |
910 | 7 | return response; |
911 | 7 | }, |
912 | 847 | }; |
913 | 847 | } |
914 | | |
915 | | static RPCMethod exportwatchonlywallet() |
916 | 849 | { |
917 | 849 | return RPCMethod{"exportwatchonlywallet", |
918 | 849 | "Creates a wallet file at the specified destination containing a watchonly version " |
919 | 849 | "of the current wallet. This watchonly wallet contains the wallet's public descriptors, " |
920 | 849 | "its transactions, and address book data. Descriptors that use hardened derivation will " |
921 | 849 | "only have a limited number of derived keys included in the export due to hardened " |
922 | 849 | "derivation requiring private keys. Descriptors with unhardened derivation do not have " |
923 | 849 | "this limitation. The watchonly wallet can be imported into another node using 'restorewallet'.", |
924 | 849 | { |
925 | 849 | {"destination", RPCArg::Type::STR, RPCArg::Optional::NO, "The path to the filename the exported watchonly wallet will be saved to"}, |
926 | 849 | }, |
927 | 849 | RPCResult{ |
928 | 849 | RPCResult::Type::OBJ, "", "", |
929 | 849 | { |
930 | 849 | {RPCResult::Type::STR, "exported_file", "The full path that the file has been exported to"}, |
931 | 849 | }, |
932 | 849 | }, |
933 | 849 | RPCExamples{ |
934 | 849 | HelpExampleCli("exportwatchonlywallet", "\"/path/to/export.dat\"") |
935 | 849 | + HelpExampleRpc("exportwatchonlywallet", "\"/path/to/export.dat\"") |
936 | 849 | }, |
937 | 849 | [&](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
938 | 849 | { |
939 | 12 | std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); |
940 | 12 | if (!pwallet) return UniValue::VNULL; |
941 | 12 | WalletContext& context = EnsureWalletContext(request.context); |
942 | | |
943 | 12 | std::string dest = request.params[0].get_str(); |
944 | | |
945 | 12 | LOCK(pwallet->cs_wallet); |
946 | 12 | pwallet->TopUpKeyPool(); |
947 | 12 | util::Result<std::string> exported = ExportWatchOnlyWallet(*pwallet, fs::PathFromString(dest), context); |
948 | 12 | if (!exported) { |
949 | 5 | throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(exported).original); |
950 | 5 | } |
951 | 7 | UniValue out{UniValue::VOBJ}; |
952 | 7 | out.pushKV("exported_file", *exported); |
953 | 7 | return out; |
954 | 12 | } |
955 | 849 | }; |
956 | 849 | } |
957 | | |
958 | | RPCMethod derivehdkey() |
959 | 868 | { |
960 | 868 | return RPCMethod{ |
961 | 868 | "derivehdkey", |
962 | 868 | "Derive extended public or private key from HD key in the wallet at a given path.\n" |
963 | 868 | "Derivation uses wallet private key material.\n" |
964 | 868 | + HELP_REQUIRING_PASSPHRASE, |
965 | 868 | { |
966 | 868 | {"path", RPCArg::Type::STR, RPCArg::Optional::NO, "BIP 32 derivation path with at least one hardened step."}, |
967 | 868 | {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", { |
968 | 868 | {"private", RPCArg::Type::BOOL, RPCArg::Default{false}, "Show private key"}, |
969 | 868 | {"hdkey", RPCArg::Type::STR, RPCArg::DefaultHint{"Either the HD key of an unused(KEY) descriptor, or any other active descriptor."}, "The HD key that the wallet knows the private key of, listed using 'gethdkeys', to use for derivation"}, |
970 | 868 | }}, |
971 | 868 | }, |
972 | 868 | RPCResult{ |
973 | 868 | RPCResult::Type::OBJ, "", "", { |
974 | 868 | {RPCResult::Type::STR, "origin", "Fingerprint and path for use in descriptors"}, |
975 | 868 | {RPCResult::Type::STR, "xpub", "The extended public key"}, |
976 | 868 | {RPCResult::Type::STR, "xprv", /*optional=*/true, "The extended private key if \"private\" is true"}, |
977 | 868 | }, |
978 | 868 | }, |
979 | 868 | RPCExamples{ |
980 | 868 | HelpExampleCli("derivehdkey", "m/87h/0h/0h") + HelpExampleRpc("derivehdkey", "\"m/87h/0h/0h\"") |
981 | 868 | + HelpExampleCliNamed("derivehdkey", {{"path", "m/87h/0h/0h"}, {"private", "true"}}) |
982 | 868 | + HelpExampleRpcNamed("derivehdkey", {{"path", "m/87h/0h/0h"}, {"private", "true"}}) |
983 | 868 | }, |
984 | 868 | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
985 | 868 | { |
986 | 31 | const std::shared_ptr<const CWallet> wallet = GetWalletForJSONRPCRequest(request); |
987 | 31 | if (!wallet) return UniValue::VNULL; |
988 | | |
989 | 31 | if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { |
990 | | // Watch-only wallets can't contain unused(KEY) descriptors |
991 | 1 | throw JSONRPCError(RPC_WALLET_ERROR, "derivehdkey is not available for watch-only wallets"); |
992 | 1 | } |
993 | | |
994 | 30 | std::vector<uint32_t> path = ParsePathBIP32(request.params[0].get_str()); |
995 | 30 | UniValue options{request.params[1].isNull() ? UniValue::VOBJ : request.params[1]}; |
996 | 30 | const bool priv{options.exists("private") ? options["private"].get_bool() : false}; |
997 | 30 | UniValue hdkey{options["hdkey"]}; |
998 | 30 | if (!HasHardenedDerivation(path)) { |
999 | 2 | throw JSONRPCError(RPC_INVALID_PARAMETER, "Derivation path requires at least one hardened step"); |
1000 | 2 | } |
1001 | | |
1002 | 28 | LOCK(wallet->cs_wallet); |
1003 | | |
1004 | | // The RPC requires a hardened derivation step, so always unlock |
1005 | | // the wallet. |
1006 | 28 | EnsureWalletIsUnlocked(*wallet); |
1007 | | |
1008 | 28 | CExtPubKey xpub; |
1009 | 28 | if (!hdkey.isNull()) { |
1010 | 7 | xpub = DecodeExtPubKey(hdkey.get_str()); |
1011 | 7 | if (!xpub.pubkey.IsValid()) { |
1012 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to parse HD key. Please provide a valid xpub"); |
1013 | 1 | } |
1014 | | |
1015 | | // Accept an xpub from an active or unused(KEY) descriptor, but |
1016 | | // not from a (used) inactive one. |
1017 | 6 | std::set<CExtPubKey> xpub_candidates; |
1018 | 6 | for (const auto& candidate : wallet->GetHDPubKeys(HDKeyFilter::UnusedKey)) { |
1019 | 5 | xpub_candidates.insert(candidate.first); |
1020 | 5 | } |
1021 | 6 | for (const auto& candidate : wallet->GetHDPubKeys(HDKeyFilter::Active)) { |
1022 | 3 | xpub_candidates.insert(candidate.first); |
1023 | 3 | } |
1024 | 6 | if (!xpub_candidates.contains(xpub)) { |
1025 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "HD key is not used by an active or unused(KEY) descriptor"); |
1026 | 1 | } |
1027 | 6 | } |
1028 | | |
1029 | | // If hdkey was not specified, try to look it up. First consider |
1030 | | // unused(KEY) descriptors. Otherwise look for active descriptors. |
1031 | 26 | if (hdkey.isNull()) { |
1032 | 16 | HDPubKeyMap wallet_xpubs{wallet->GetHDPubKeys(HDKeyFilter::UnusedKey)}; |
1033 | | |
1034 | 16 | if (wallet_xpubs.size() > 1) { |
1035 | 1 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to determine which HD key to use. Please specify with 'hdkey'"); |
1036 | 15 | } else if (wallet_xpubs.size() == 1) { |
1037 | 9 | xpub = wallet_xpubs.begin()->first; |
1038 | 9 | } else { |
1039 | 6 | HDPubKeyMap active_xpubs = wallet->GetHDPubKeys(HDKeyFilter::Active); |
1040 | 6 | if (active_xpubs.empty()) { |
1041 | 2 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No active or unused(KEY) descriptor found"); |
1042 | 2 | } |
1043 | | |
1044 | 4 | if (active_xpubs.size() > 1) { |
1045 | 0 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to determine which HD key to use from active descriptors. Please specify with 'hdkey'"); |
1046 | 0 | } |
1047 | | |
1048 | 4 | xpub = active_xpubs.begin()->first; |
1049 | 4 | } |
1050 | 16 | } |
1051 | | |
1052 | 23 | std::optional<CExtKey> xprv{wallet->GetExtKey(xpub)}; |
1053 | 23 | if (!xprv) { |
1054 | 0 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Private key for %s is not known", EncodeExtPubKey(xpub))); |
1055 | 0 | } |
1056 | | |
1057 | 23 | std::optional<std::pair<CExtKey, KeyOriginInfo>> child{DeriveExtKey(*xprv, path)}; |
1058 | 23 | if (!child) { |
1059 | 1 | throw JSONRPCError(RPC_INVALID_PARAMETER, "Unable to derive HD key at the requested path"); |
1060 | 1 | } |
1061 | | |
1062 | 22 | UniValue res{UniValue::VOBJ}; |
1063 | | |
1064 | 22 | const std::string fingerprint{HexStr(child->second.fingerprint)}; |
1065 | | |
1066 | 22 | res.pushKV("origin", strprintf("[%s%s]", fingerprint, FormatHDKeypath(child->second.path))); |
1067 | 22 | res.pushKV("xpub", EncodeExtPubKey(child->first.Neuter())); |
1068 | 22 | if (priv) { |
1069 | 2 | res.pushKV("xprv", EncodeExtKey(child->first)); |
1070 | 2 | } |
1071 | 22 | return res; |
1072 | 23 | }, |
1073 | 868 | }; |
1074 | 868 | } |
1075 | | |
1076 | | // addresses |
1077 | | RPCMethod getaddressinfo(); |
1078 | | RPCMethod getnewaddress(); |
1079 | | RPCMethod getrawchangeaddress(); |
1080 | | RPCMethod setlabel(); |
1081 | | RPCMethod listaddressgroupings(); |
1082 | | RPCMethod keypoolrefill(); |
1083 | | RPCMethod getaddressesbylabel(); |
1084 | | RPCMethod listlabels(); |
1085 | | #ifdef ENABLE_EXTERNAL_SIGNER |
1086 | | RPCMethod walletdisplayaddress(); |
1087 | | #endif // ENABLE_EXTERNAL_SIGNER |
1088 | | |
1089 | | // backup |
1090 | | RPCMethod importprunedfunds(); |
1091 | | RPCMethod removeprunedfunds(); |
1092 | | RPCMethod importdescriptors(); |
1093 | | RPCMethod listdescriptors(); |
1094 | | RPCMethod backupwallet(); |
1095 | | RPCMethod restorewallet(); |
1096 | | |
1097 | | // coins |
1098 | | RPCMethod getreceivedbyaddress(); |
1099 | | RPCMethod getreceivedbylabel(); |
1100 | | RPCMethod getbalance(); |
1101 | | RPCMethod lockunspent(); |
1102 | | RPCMethod listlockunspent(); |
1103 | | RPCMethod getbalances(); |
1104 | | RPCMethod listunspent(); |
1105 | | |
1106 | | // encryption |
1107 | | RPCMethod walletpassphrase(); |
1108 | | RPCMethod walletpassphrasechange(); |
1109 | | RPCMethod walletlock(); |
1110 | | RPCMethod encryptwallet(); |
1111 | | |
1112 | | // spend |
1113 | | RPCMethod sendtoaddress(); |
1114 | | RPCMethod sendmany(); |
1115 | | RPCMethod fundrawtransaction(); |
1116 | | RPCMethod bumpfee(); |
1117 | | RPCMethod psbtbumpfee(); |
1118 | | RPCMethod send(); |
1119 | | RPCMethod sendall(); |
1120 | | RPCMethod walletprocesspsbt(); |
1121 | | RPCMethod walletcreatefundedpsbt(); |
1122 | | RPCMethod signrawtransactionwithwallet(); |
1123 | | |
1124 | | // signmessage |
1125 | | RPCMethod signmessage(); |
1126 | | |
1127 | | // transactions |
1128 | | RPCMethod listreceivedbyaddress(); |
1129 | | RPCMethod listreceivedbylabel(); |
1130 | | RPCMethod listtransactions(); |
1131 | | RPCMethod listsinceblock(); |
1132 | | RPCMethod gettransaction(); |
1133 | | RPCMethod abandontransaction(); |
1134 | | RPCMethod rescanblockchain(); |
1135 | | RPCMethod abortrescan(); |
1136 | | |
1137 | | std::span<const CRPCCommand> GetWalletRPCCommands() |
1138 | 427 | { |
1139 | 427 | static const CRPCCommand commands[]{ |
1140 | 427 | {"rawtransactions", &fundrawtransaction}, |
1141 | 427 | {"wallet", &abandontransaction}, |
1142 | 427 | {"wallet", &abortrescan}, |
1143 | 427 | {"wallet", &addhdkey}, |
1144 | 427 | {"wallet", &backupwallet}, |
1145 | 427 | {"wallet", &bumpfee}, |
1146 | 427 | {"wallet", &psbtbumpfee}, |
1147 | 427 | {"wallet", &createwallet}, |
1148 | 427 | {"wallet", &createwalletdescriptor}, |
1149 | 427 | {"wallet", &derivehdkey}, |
1150 | 427 | {"wallet", &restorewallet}, |
1151 | 427 | {"wallet", &encryptwallet}, |
1152 | 427 | {"wallet", &exportwatchonlywallet}, |
1153 | 427 | {"wallet", &getaddressesbylabel}, |
1154 | 427 | {"wallet", &getaddressinfo}, |
1155 | 427 | {"wallet", &getbalance}, |
1156 | 427 | {"wallet", &gethdkeys}, |
1157 | 427 | {"wallet", &getnewaddress}, |
1158 | 427 | {"wallet", &getrawchangeaddress}, |
1159 | 427 | {"wallet", &getreceivedbyaddress}, |
1160 | 427 | {"wallet", &getreceivedbylabel}, |
1161 | 427 | {"wallet", &gettransaction}, |
1162 | 427 | {"wallet", &getbalances}, |
1163 | 427 | {"wallet", &getwalletinfo}, |
1164 | 427 | {"wallet", &importdescriptors}, |
1165 | 427 | {"wallet", &importprunedfunds}, |
1166 | 427 | {"wallet", &keypoolrefill}, |
1167 | 427 | {"wallet", &listaddressgroupings}, |
1168 | 427 | {"wallet", &listdescriptors}, |
1169 | 427 | {"wallet", &listlabels}, |
1170 | 427 | {"wallet", &listlockunspent}, |
1171 | 427 | {"wallet", &listreceivedbyaddress}, |
1172 | 427 | {"wallet", &listreceivedbylabel}, |
1173 | 427 | {"wallet", &listsinceblock}, |
1174 | 427 | {"wallet", &listtransactions}, |
1175 | 427 | {"wallet", &listunspent}, |
1176 | 427 | {"wallet", &listwalletdir}, |
1177 | 427 | {"wallet", &listwallets}, |
1178 | 427 | {"wallet", &loadwallet}, |
1179 | 427 | {"wallet", &lockunspent}, |
1180 | 427 | {"wallet", &migratewallet}, |
1181 | 427 | {"wallet", &removeprunedfunds}, |
1182 | 427 | {"wallet", &rescanblockchain}, |
1183 | 427 | {"wallet", &send}, |
1184 | 427 | {"wallet", &sendmany}, |
1185 | 427 | {"wallet", &sendtoaddress}, |
1186 | 427 | {"wallet", &setlabel}, |
1187 | 427 | {"wallet", &setwalletflag}, |
1188 | 427 | {"wallet", &signmessage}, |
1189 | 427 | {"wallet", &signrawtransactionwithwallet}, |
1190 | 427 | {"wallet", &simulaterawtransaction}, |
1191 | 427 | {"wallet", &sendall}, |
1192 | 427 | {"wallet", &unloadwallet}, |
1193 | 427 | {"wallet", &walletcreatefundedpsbt}, |
1194 | 427 | #ifdef ENABLE_EXTERNAL_SIGNER |
1195 | 427 | {"wallet", &walletdisplayaddress}, |
1196 | 427 | #endif // ENABLE_EXTERNAL_SIGNER |
1197 | 427 | {"wallet", &walletlock}, |
1198 | 427 | {"wallet", &walletpassphrase}, |
1199 | 427 | {"wallet", &walletpassphrasechange}, |
1200 | 427 | {"wallet", &walletprocesspsbt}, |
1201 | 427 | }; |
1202 | 427 | return commands; |
1203 | 427 | } |
1204 | | } // namespace wallet |