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