Skip to content

Commit 71b4900

Browse files
committed
wallet: Add addhdkey RPC
1 parent fb2eb22 commit 71b4900

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

src/wallet/rpc/wallet.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,82 @@ static RPCHelpMan createwalletdescriptor()
929929
};
930930
}
931931

932+
RPCHelpMan addhdkey()
933+
{
934+
return RPCHelpMan{
935+
"addhdkey",
936+
"Add a BIP 32 HD key to the wallet that can be used with 'createwalletdescriptor'\n",
937+
{
938+
{"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."},
939+
},
940+
RPCResult{
941+
RPCResult::Type::OBJ, "", "",
942+
{
943+
{RPCResult::Type::STR, "xpub", "The xpub of the HD key that was added to the wallet"}
944+
},
945+
},
946+
RPCExamples{
947+
HelpExampleCli("addhdkey", "xprv") + HelpExampleRpc("addhdkey", "xprv")
948+
},
949+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
950+
{
951+
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
952+
if (!wallet) return UniValue::VNULL;
953+
954+
if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
955+
throw JSONRPCError(RPC_WALLET_ERROR, "addhdkey is not available for wallets without private keys");
956+
}
957+
958+
EnsureWalletIsUnlocked(*wallet);
959+
960+
CExtKey hdkey;
961+
if (request.params[0].isNull()) {
962+
CKey seed_key = GenerateRandomKey();
963+
hdkey.SetSeed(seed_key);
964+
} else {
965+
hdkey = DecodeExtKey(request.params[0].get_str());
966+
if (!hdkey.key.IsValid()) {
967+
// Check if the user gave us an xpub and give a more descriptive error if so
968+
CExtPubKey xpub = DecodeExtPubKey(request.params[0].get_str());
969+
if (xpub.pubkey.IsValid()) {
970+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Extended public key (xpub) provided, but extended private key (xprv) is required");
971+
} else {
972+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not parse HD key");
973+
}
974+
}
975+
}
976+
977+
LOCK(wallet->cs_wallet);
978+
std::string desc_str = "unused(" + EncodeExtKey(hdkey) + ")";
979+
FlatSigningProvider keys;
980+
std::string error;
981+
std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_str, keys, error, false);
982+
CHECK_NONFATAL(!descs.empty());
983+
WalletDescriptor w_desc(std::move(descs.at(0)), GetTime(), 0, 0, 0);
984+
if (wallet->GetDescriptorScriptPubKeyMan(w_desc) != nullptr) {
985+
throw JSONRPCError(RPC_WALLET_ERROR, "HD key already exists");
986+
}
987+
988+
auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false);
989+
if (!spkm) {
990+
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(spkm).original);
991+
}
992+
993+
UniValue response(UniValue::VOBJ);
994+
const DescriptorScriptPubKeyMan& desc_spkm = spkm->get();
995+
LOCK(desc_spkm.cs_desc_man);
996+
std::set<CPubKey> pubkeys;
997+
std::set<CExtPubKey> extpubs;
998+
desc_spkm.GetWalletDescriptor().descriptor->GetPubKeys(pubkeys, extpubs);
999+
CHECK_NONFATAL(pubkeys.size() == 0);
1000+
CHECK_NONFATAL(extpubs.size() == 1);
1001+
response.pushKV("xpub", EncodeExtPubKey(*extpubs.begin()));
1002+
1003+
return response;
1004+
},
1005+
};
1006+
}
1007+
9321008
// addresses
9331009
RPCHelpMan getaddressinfo();
9341010
RPCHelpMan getnewaddress();
@@ -998,6 +1074,7 @@ std::span<const CRPCCommand> GetWalletRPCCommands()
9981074
{"rawtransactions", &fundrawtransaction},
9991075
{"wallet", &abandontransaction},
10001076
{"wallet", &abortrescan},
1077+
{"wallet", &addhdkey},
10011078
{"wallet", &backupwallet},
10021079
{"wallet", &bumpfee},
10031080
{"wallet", &psbtbumpfee},

test/functional/wallet_hd.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import shutil
88

99
from test_framework.blocktools import COINBASE_MATURITY
10+
from test_framework.descriptors import descsum_create
1011
from test_framework.test_framework import BitcoinTestFramework
1112
from test_framework.util import (
1213
assert_equal,
1314
wallet_importprivkey,
15+
assert_raises_rpc_error,
1416
)
1517

1618

@@ -27,6 +29,56 @@ def set_test_params(self):
2729
def skip_test_if_missing_module(self):
2830
self.skip_if_no_wallet()
2931

32+
def test_addhdkey(self):
33+
self.log.info("Test addhdkey")
34+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
35+
self.nodes[0].createwallet("hdkey")
36+
wallet = self.nodes[0].get_wallet_rpc("hdkey")
37+
38+
assert_equal(len(wallet.gethdkeys()), 1)
39+
40+
wallet.addhdkey()
41+
xpub_info = wallet.gethdkeys()
42+
assert_equal(len(xpub_info), 2)
43+
for x in xpub_info:
44+
if len(x["descriptors"]) == 1 and x["descriptors"][0]["desc"].startswith("unused("):
45+
break
46+
else:
47+
assert False, "Did not find HD key with no descriptors"
48+
49+
imp_xpub_info = def_wallet.gethdkeys(private=True)[0]
50+
imp_xpub = imp_xpub_info["xpub"]
51+
imp_xprv = imp_xpub_info["xprv"]
52+
53+
assert_raises_rpc_error(-5, "Extended public key (xpub) provided, but extended private key (xprv) is required", wallet.addhdkey, imp_xpub)
54+
add_res = wallet.addhdkey(imp_xprv)
55+
expected_unused_desc = descsum_create(f"unused({imp_xpub})")
56+
assert_equal(add_res["xpub"], imp_xpub)
57+
xpub_info = wallet.gethdkeys()
58+
assert_equal(len(xpub_info), 3)
59+
for x in xpub_info:
60+
if x["xpub"] == imp_xpub:
61+
assert_equal(len(x["descriptors"]), 1)
62+
assert_equal(x["descriptors"][0]["desc"], expected_unused_desc)
63+
break
64+
else:
65+
assert False, "Added HD key was not found in wallet"
66+
67+
for d in wallet.listdescriptors()["descriptors"]:
68+
if d["desc"] == expected_unused_desc:
69+
assert_equal(d["active"], False)
70+
break
71+
else:
72+
assert False, "Added HD key's descriptor was not found in wallet"
73+
74+
assert_raises_rpc_error(-4, "HD key already exists", wallet.addhdkey, imp_xprv)
75+
76+
def test_addhdkey_noprivs(self):
77+
self.log.info("Test addhdkey is not available for wallets without privkeys")
78+
self.nodes[0].createwallet("hdkey_noprivs", disable_private_keys=True)
79+
wallet = self.nodes[0].get_wallet_rpc("hdkey_noprivs")
80+
assert_raises_rpc_error(-4, "addhdkey is not available for wallets without private keys", wallet.addhdkey)
81+
3082
def run_test(self):
3183
# Make sure we use hd, keep masterkeyid
3284
hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
@@ -126,6 +178,8 @@ def run_test(self):
126178

127179
assert_equal(keypath[0:14], "m/84h/1h/0h/1/")
128180

181+
self.test_addhdkey()
182+
self.test_addhdkey_noprivs()
129183

130184
if __name__ == '__main__':
131185
WalletHDTest(__file__).main()

0 commit comments

Comments
 (0)