diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1090,7 +1090,7 @@ RPCHelpMan{ "addmultisigaddress", - "Add a nrequired-to-sign multisignature address to the wallet. " + "Add an nrequired-to-sign multisignature address to the wallet. " "Requires a new wallet backup.\n" "Each key is a Bitcoin address or hex-encoded public key.\n" "If 'label' is specified (DEPRECATED), assign address to that label.\n", @@ -4149,85 +4149,95 @@ RPCHelpMan{ "getaddressinfo", - "Return information about the given bitcoin address. Some " - "information requires the address\n" - "to be in the wallet.\n", + "\nReturn information about the given bitcoin address.\n" + "Some of the information will only be present if the address is in the " + "active wallet.\n", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, - "The bitcoin address to get the information of."}, + "The bitcoin address for which to get information."}, }, RPCResult{ "{\n" - " \"address\" : \"address\", (string) The bitcoin address " - "validated\n" - " \"scriptPubKey\" : \"hex\", (string) The hex-encoded " - "scriptPubKey generated by the address\n" - " \"ismine\" : true|false, (boolean) If the address is " - "yours or not\n" - " \"iswatchonly\" : true|false, (boolean) If the address is " - "watchonly\n" - " \"solvable\" : true|false, (boolean) Whether we know how " + " \"address\" : \"address\", (string) The bitcoin " + "address validated.\n" + " \"scriptPubKey\" : \"hex\", (string) The " + "hex-encoded scriptPubKey generated by the address.\n" + " \"ismine\" : true|false, (boolean) If the address " + "is yours.\n" + " \"iswatchonly\" : true|false, (boolean) If the address " + "is watchonly.\n" + " \"solvable\" : true|false, (boolean) If we know how " "to spend coins sent to this address, ignoring the possible lack " - "of private keys\n" - " \"desc\" : \"desc\", (string, optional) A descriptor " - "for spending coins sent to this address (only when solvable)\n" - " \"isscript\" : true|false, (boolean) If the key is a " - "script\n" - " \"ischange\" : true|false, (boolean) If the address was " - "used for change output\n" - " \"script\" : \"type\" (string, optional) The output " - "script type. Only if \"isscript\" is true and the redeemscript is " - "known. Possible types: nonstandard, pubkey, pubkeyhash, " - "scripthash, multisig, nulldata\n" - " \"hex\" : \"hex\", (string, optional) The " - "redeemscript for the p2sh address\n" - " \"pubkeys\" (string, optional) Array of " - "pubkeys associated with the known redeemscript (only if " - "\"script\" is \"multisig\")\n" + "of private keys.\n" + " \"desc\" : \"desc\", (string, optional) A " + "descriptor for spending coins sent to this address (only when " + "solvable).\n" + " \"isscript\" : true|false, (boolean) If the key is a " + "script.\n" + " \"ischange\" : true|false, (boolean) If the address " + "was used for change output.\n" + " \"script\" : \"type\" (string, optional) The " + "output script type. Only if isscript is true and the redeemscript " + "is known. Possible\n" + " types: " + "nonstandard, pubkey, pubkeyhash, scripthash, multisig, nulldata\n" + " \"hex\" : \"hex\", (string, optional) The " + "redeemscript for the p2sh address.\n" + " \"pubkeys\" (array, optional) Array " + "of pubkeys associated with the known redeemscript (only if script " + "is multisig).\n" " [\n" - " \"pubkey\"\n" + " \"pubkey\" (string)\n" " ,...\n" " ]\n" - " \"sigsrequired\" : xxxxx (numeric, optional) Number of " - "signatures required to spend multisig output (only if \"script\" " - "is \"multisig\")\n" - " \"pubkey\" : \"publickeyhex\", (string, optional) The hex " - "value of the raw public key, for single-key addresses (possibly " - "embedded in P2SH)\n" - " \"embedded\" : {...}, (object, optional) Information " - "about the address embedded in P2SH, if relevant and known. It " - "includes all getaddressinfo output fields for the embedded " - "address, excluding metadata (\"timestamp\", \"hdkeypath\", " - "\"hdseedid\") and relation to the wallet (\"ismine\", " - "\"iswatchonly\").\n" - " \"iscompressed\" : true|false, (boolean) If the address is " - "compressed\n" - " \"label\" : \"label\" (string) The label associated " - "with the address, \"\" is the default label\n" - " \"timestamp\" : timestamp, (number, optional) The creation " - "time of the key if available in seconds since epoch (Jan 1 1970 " - "GMT)\n" - " \"hdkeypath\" : \"keypath\" (string, optional) The HD " - "keypath if the key is HD and available\n" - " \"hdseedid\" : \"\" (string, optional) The Hash160 " - "of the HD seed\n" + " \"sigsrequired\" : xxxxx (numeric, optional) The " + "number of signatures required to spend multisig output (only if " + "script is multisig).\n" + " \"pubkey\" : \"publickeyhex\", (string, optional) The " + "hex value of the raw public key for single-key addresses " + "(possibly embedded in P2SH or P2WSH).\n" + " \"embedded\" : {...}, (object, optional) " + "Information about the address embedded in P2SH or P2WSH, if " + "relevant and known. Includes all\n" + " " + "getaddressinfo output fields for the embedded address, excluding " + "metadata (timestamp, hdkeypath,\n" + " " + "hdseedid) and relation to the wallet (ismine, iswatchonly).\n" + " \"iscompressed\" : true|false, (boolean, optional) If " + "the pubkey is compressed.\n" + " \"label\" : \"label\" (string) The label " + "associated with the address. Defaults to \"\". Equivalent to the " + "name field in the labels array.\n" + " \"timestamp\" : timestamp, (number, optional) The " + "creation time of the key if available, expressed in seconds since " + "Epoch Time (Jan 1 1970 GMT).\n" + " \"hdkeypath\" : \"keypath\" (string, optional) The " + "HD keypath, if the key is HD and available.\n" + " \"hdseedid\" : \"\" (string, optional) The " + "Hash160 of the HD seed.\n" " \"hdmasterfingerprint\" : \"\" (string, optional) The " - "fingperint of the master key.\n" - " \"labels\" (object) Array of labels " - "associated with the address.\n" + "fingerprint of the master key.\n" + " \"labels\" (object) An array of " + "labels associated with the address. Currently limited to one " + "label but returned\n" + " as an array to " + "keep the API stable if multiple labels are enabled in the " + "future.\n" " [\n" " { (json object of label data)\n" - " \"name\": \"labelname\" (string) The label\n" - " \"purpose\": \"string\" (string) Purpose of address " - "(\"send\" for sending address, \"receive\" for receiving " - "address)\n" + " \"name\": \"label name\" (string) The label name. " + "Defaults to \"\". Equivalent to the label field above.\n" + " \"purpose\": \"purpose\" (string) The purpose of the " + "associated address (send or receive).\n" " },...\n" " ]\n" "}\n"}, - RPCExamples{HelpExampleCli("getaddressinfo", - "\"1PSSGeFHDnKNxiEyFrD1wcEaHr9hrQDDWc\"") + - HelpExampleRpc("getaddressinfo", - "\"1PSSGeFHDnKNxiEyFrD1wcEaHr9hrQDDWc\"")}, + RPCExamples{ + HelpExampleCli("getaddressinfo", + "\"qrmzys48glkpevp2l4t24jtcltc9hyzx9cep2qffm4\"") + + HelpExampleRpc("getaddressinfo", + "\"qrmzys48glkpevp2l4t24jtcltc9hyzx9cep2qffm4\"")}, } .Check(request); @@ -4248,24 +4258,40 @@ CScript scriptPubKey = GetScriptForDestination(dest); ret.pushKV("scriptPubKey", HexStr(scriptPubKey.begin(), scriptPubKey.end())); + const SigningProvider *provider = pwallet->GetSigningProvider(scriptPubKey); isminetype mine = pwallet->IsMine(dest); ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE)); + bool solvable = provider && IsSolvable(*provider, scriptPubKey); ret.pushKV("solvable", solvable); + if (solvable) { ret.pushKV("desc", InferDescriptor(scriptPubKey, *provider)->ToString()); } + ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY)); + + // Return DescribeWalletAddress fields. + // Always returned: isscript, ischange. + // Optional: script, hex, pubkeys (array), sigsrequired, pubkey, embedded, + // iscompressed. UniValue detail = DescribeWalletAddress(pwallet, dest); ret.pushKVs(detail); + + // Return label field if existing. Currently only one label can be + // associated with an address, so the label should be equivalent to the + // value of the name key/value pair in the labels hash array below. if (pwallet->mapAddressBook.count(dest)) { ret.pushKV("label", pwallet->mapAddressBook[dest].name); } + ret.pushKV("ischange", pwallet->IsChange(scriptPubKey)); + // Fetch KeyMetadata, if present, for the timestamp, hdkeypath, hdseedid, + // and hdmasterfingerprint fields. ScriptPubKeyMan *spk_man = pwallet->GetScriptPubKeyMan(scriptPubKey); if (spk_man) { if (const CKeyMetadata *meta = spk_man->GetMetadata(dest)) { @@ -4280,9 +4306,11 @@ } } - // Currently only one label can be associated with an address, return an - // array so the API remains stable if we allow multiple labels to be - // associated with an address. + // Return a labels array containing a hash of key/value pairs for the label + // name and address purpose. The name value is equivalent to the label field + // above. Currently only one label can be associated with an address, but we + // return an array so the API remains stable if we allow multiple labels to + // be associated with an address in the future. UniValue labels(UniValue::VARR); std::map::iterator mi = pwallet->mapAddressBook.find(dest); diff --git a/test/functional/test_framework/wallet_util.py b/test/functional/test_framework/wallet_util.py --- a/test/functional/test_framework/wallet_util.py +++ b/test/functional/test_framework/wallet_util.py @@ -69,6 +69,12 @@ redeem_script=script_code.hex()) +def labels_value(name="", purpose="receive"): + """Generate a getaddressinfo labels array from a name and purpose. + Often used as the value of a labels kwarg for calling test_address below.""" + return [{"name": name, "purpose": purpose}] + + def test_address(node, address, **kwargs): """Get address info for `address` and test whether the returned values are as expected.""" addr_info = node.getaddressinfo(address) diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -17,6 +17,10 @@ count_bytes, wait_until, ) +from test_framework.wallet_util import ( + labels_value, + test_address, +) class WalletTest(BitcoinTestFramework): @@ -460,8 +464,12 @@ for label in [u'рыба', u'𝅘𝅥𝅯']: addr = self.nodes[0].getnewaddress() self.nodes[0].setlabel(addr, label) - assert_equal(self.nodes[0].getaddressinfo( - addr)['label'], label) + test_address( + self.nodes[0], + addr, + label=label, + labels=labels_value( + name=label)) assert label in self.nodes[0].listlabels() # restore to default self.nodes[0].rpc.ensure_ascii = True diff --git a/test/functional/wallet_import_with_label.py b/test/functional/wallet_import_with_label.py --- a/test/functional/wallet_import_with_label.py +++ b/test/functional/wallet_import_with_label.py @@ -22,7 +22,10 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import hex_str_to_bytes -from test_framework.wallet_util import test_address +from test_framework.wallet_util import ( + labels_value, + test_address, +) class ImportWithLabel(BitcoinTestFramework): @@ -47,7 +50,8 @@ address, iswatchonly=True, ismine=False, - label=label) + label=label, + labels=labels_value(name=label)) self.log.info( "Import the watch-only address's private key without a " @@ -58,7 +62,8 @@ test_address(self.nodes[1], address, - label=label) + label=label, + labels=labels_value(name=label)) self.log.info( "Test importaddress without label and importprivkey with label." @@ -70,7 +75,8 @@ address2, iswatchonly=True, ismine=False, - label="") + label="", + labels=labels_value()) self.log.info( "Import the watch-only address's private key with a " @@ -82,7 +88,8 @@ test_address(self.nodes[1], address2, - label=label2) + label=label2, + labels=labels_value(name=label2)) self.log.info( "Test importaddress with label and importprivkey with label.") @@ -94,7 +101,8 @@ address3, iswatchonly=True, ismine=False, - label=label3_addr) + label=label3_addr, + labels=labels_value(name=label3_addr)) self.log.info( "Import the watch-only address's private key with a " @@ -106,7 +114,8 @@ test_address(self.nodes[1], address3, - label=label3_priv) + label=label3_priv, + labels=labels_value(name=label3_priv)) self.log.info( "Test importprivkey won't label new dests with the same " @@ -121,6 +130,7 @@ iswatchonly=True, ismine=False, label=label4_addr, + labels=labels_value(name=label4_addr), embedded=None) self.log.info( @@ -147,13 +157,15 @@ test_address(self.nodes[1], p2shaddr4, - label="") + label="", + labels=labels_value()) embedded_addr = self.nodes[1].getaddressinfo( p2shaddr4)['embedded']['address'] test_address(self.nodes[1], embedded_addr, - label=label4_addr) + label=label4_addr, + labels=labels_value(name=label4_addr)) self.stop_nodes() diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -29,6 +29,7 @@ from test_framework.wallet_util import ( get_key, get_multisig, + labels_value, test_address, ) @@ -126,7 +127,7 @@ self.test_importmulti({"scriptPubKey": key.p2pkh_script, "timestamp": "now", "internal": True, - "label": "Example label"}, + "label": "Unsuccessful labelling for internal addresses"}, success=False, error_code=-8, error_message='Internal addresses should not have a label') @@ -502,17 +503,19 @@ # Test importing of a P2PKH address via descriptor key = get_key(self.nodes[0]) + p2pkh_label = "P2PKH descriptor import" self.log.info("Should import a p2pkh address from descriptor") self.test_importmulti({"desc": descsum_create("pkh(" + key.pubkey + ")"), "timestamp": "now", - "label": "Descriptor import test"}, + "label": p2pkh_label}, success=True, warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) test_address(self.nodes[1], key.p2pkh_addr, solvable=True, ismine=False, - label="Descriptor import test") + label=p2pkh_label, + labels=labels_value(name=p2pkh_label)) # Test import fails if both desc and scriptPubKey are provided key = get_key(self.nodes[0]) diff --git a/test/functional/wallet_labels.py b/test/functional/wallet_labels.py --- a/test/functional/wallet_labels.py +++ b/test/functional/wallet_labels.py @@ -13,6 +13,10 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.wallet_util import ( + labels_value, + test_address, +) class WalletLabelsTest(BitcoinTestFramework): @@ -165,14 +169,15 @@ def verify(self, node): if self.receive_address is not None: assert self.receive_address in self.addresses - for address in self.addresses: - assert_equal( - node.getaddressinfo(address)['labels'][0], - {"name": self.name, - "purpose": self.purpose[address]}) - assert_equal(node.getaddressinfo(address)['label'], self.name) - + test_address( + node, + address, + label=self.name, + labels=labels_value( + name=self.name, purpose=self.purpose[address]) + ) + assert self.name in node.listlabels() assert_equal( node.getaddressesbylabel(self.name), {address: {"purpose": self.purpose[address]} for address in self.addresses}) diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py --- a/test/functional/wallet_listreceivedby.py +++ b/test/functional/wallet_listreceivedby.py @@ -12,6 +12,10 @@ assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet_util import ( + labels_value, + test_address, +) class ReceivedByTest(BitcoinTestFramework): @@ -145,7 +149,12 @@ # set pre-state label = '' address = self.nodes[1].getnewaddress() - assert_equal(self.nodes[1].getaddressinfo(address)['label'], label) + test_address( + self.nodes[1], + address, + label=label, + labels=labels_value( + name=label)) received_by_label_json = [ r for r in self.nodes[1].listreceivedbylabel() if r["label"] == label][0] balance_by_label = self.nodes[1].getreceivedbylabel(label)