diff --git a/doc/release-notes.md b/doc/release-notes.md
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -16,3 +16,36 @@
- Block storage can be limited under Preferences, in the Main tab. Undoing
this setting requires downloading the full blockchain again. This mode is
incompatible with -txindex and -rescan.
+
+'label' API for wallet
+----------------------
+
+ A new 'label' API has been introduced for the wallet. This is intended as a
+ replacement for the deprecated 'account' API.
+
+ The label RPC methods mirror the account functionality, with the following functional differences:
+
+ - Labels can be set on any address, not just receiving addresses. This functionality was previously only available through the GUI.
+ - Labels can be deleted by reassigning all addresses using the `setlabel` RPC method.
+ - There isn't support for sending transactions _from_ a label, or for determining which label a transaction was sent from.
+ - Labels do not have a balance.
+
+ Here are the changes to RPC methods:
+
+ | Deprecated Method | New Method | Notes |
+ | :---------------------- | :-------------------- | :-----------|
+ | `getaccount` | `getaddressinfo` | `getaddressinfo` returns a json object with address information instead of just the name of the account as a string. |
+ | `getaccountaddress` | `getlabeladdress` | `getlabeladdress` throws an error by default if the label does not already exist, but provides a `force` option for compatibility with existing applications. |
+ | `getaddressesbyaccount` | `getaddressesbylabel` | `getaddressesbylabel` returns a json object with the addresses as keys, instead of a list of strings. |
+ | `getreceivedbyaccount` | `getreceivedbylabel` | _no change in behavior_ |
+ | `listaccounts` | `listlabels` | `listlabels` does not return a balance or accept `minconf` and `watchonly` arguments. |
+ | `listreceivedbyaccount` | `listreceivedbylabel` | Both methods return new `label` fields, along with `account` fields for backward compatibility. |
+ | `move` | n/a | _no replacement_ |
+ | `sendfrom` | n/a | _no replacement_ |
+ | `setaccount` | `setlabel` | Both methods now:
- allow assigning labels to any address, instead of raising an error if the address is not receiving address.
- delete the previous label associated with an address when the final address using that label is reassigned to a different label, instead of making an implicit `getaccountaddress` call to ensure the previous label still has a receiving address. |
+
+ | Changed Method | Notes |
+ | :--------------------- | :------ |
+ | `addmultisigaddress` | Renamed `account` named parameter to `label`. Still accepts `account` for backward compatibility. |
+ | `getnewaddress` | Renamed `account` named parameter to `label`. Still accepts `account` for backward compatibility. |
+ | `listunspent` | Returns new `label` fields, along with `account` fields for backward compatibility. |
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -50,6 +50,7 @@
{"listreceivedbylabel", 0, "minconf"},
{"listreceivedbylabel", 1, "include_empty"},
{"listreceivedbylabel", 2, "include_watchonly"},
+ {"getlabeladdress", 1, "force"},
{"getbalance", 1, "minconf"},
{"getbalance", 2, "include_watchonly"},
{"getblockhash", 0, "height"},
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -240,19 +240,26 @@
return NullUniValue;
}
- if (request.fHelp || request.params.size() != 1) {
+ if (request.fHelp || request.params.size() < 1 ||
+ request.params.size() > 2) {
throw std::runtime_error(
- "getlabeladdress \"label\"\n"
- "\nReturns the current Bitcoin address for receiving payments to "
- "this label.\n"
+ "getlabeladdress \"label\" ( force ) \n"
+ "\nReturns the default receiving address for this label. This will "
+ "reset to a fresh address once there's a transaction that spends "
+ "to it.\n"
"\nArguments:\n"
- "1. \"label\" (string, required) The label name for the "
+ "1. \"label\" (string, required) The label for the "
"address. It can also be set to the empty string \"\" to represent "
- "the default label. The label does not need to exist, it will be "
- "created and a new address created if there is no label by the "
- "given name.\n"
+ "the default label.\n"
+ "2. \"force\" (bool, optional) Whether the label should be "
+ "created if it does not yet exist. If False, the RPC will return "
+ "an error if called with a label that doesn't exist.\n"
+ " Defaults to false (unless the "
+ "getaccountaddress method alias is being called, in which case "
+ "defaults to true for backwards compatibility).\n"
"\nResult:\n"
- "\"address\" (string) The label bitcoin address\n"
+ "\"address\" (string) The current receiving address for "
+ "the label.\n"
"\nExamples:\n" +
HelpExampleCli("getlabeladdress", "") +
HelpExampleCli("getlabeladdress", "\"\"") +
@@ -263,6 +270,23 @@
// Parse the label first so we don't generate a key if there's an error
std::string label = LabelFromValue(request.params[0]);
+ bool force = request.strMethod == "getaccountaddress" ? true : false;
+ if (!request.params[1].isNull()) {
+ force = request.params[1].get_bool();
+ }
+
+ bool label_found = false;
+ for (const std::pair &item :
+ pwallet->mapAddressBook) {
+ if (item.second.name == label) {
+ label_found = true;
+ break;
+ }
+ }
+ if (!force && !label_found) {
+ throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME,
+ std::string("No addresses with label " + label));
+ }
UniValue ret(UniValue::VSTR);
@@ -333,16 +357,15 @@
return NullUniValue;
}
- if (request.fHelp || request.params.size() < 1 ||
- request.params.size() > 2) {
+ if (request.fHelp || request.params.size() != 2) {
throw std::runtime_error(
"setlabel \"address\" \"label\"\n"
"\nSets the label associated with the given address.\n"
"\nArguments:\n"
"1. \"address\" (string, required) The bitcoin address to "
"be associated with a label.\n"
- "2. \"label\" (string, required) The label to assign the "
- "address to.\n"
+ "2. \"label\" (string, required) The label to assign to "
+ "the address.\n"
"\nExamples:\n" +
HelpExampleCli("setlabel",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\" \"tabby\"") +
@@ -359,26 +382,24 @@
"Invalid Bitcoin address");
}
- std::string label;
- if (!request.params[1].isNull()) {
- label = LabelFromValue(request.params[1]);
- }
+ std::string label = LabelFromValue(request.params[1]);
- // Only add the label if the address is yours.
if (IsMine(*pwallet, dest)) {
- // Detect when changing the label of an address that is the 'unused
- // current key' of another label:
+ // Detect when changing the label of an address that is the receiving
+ // address of another label. If so, delete the account record for it.
+ // Labels, unlike addresses, can be deleted, and if we wouldn't do this,
+ // the record would stick around forever.
if (pwallet->mapAddressBook.count(dest)) {
std::string old_label = pwallet->mapAddressBook[dest].name;
- if (dest == GetLabelDestination(pwallet, old_label)) {
- GetLabelDestination(pwallet, old_label, true);
+ if (old_label != label &&
+ dest == GetLabelDestination(pwallet, old_label)) {
+ pwallet->DeleteLabel(old_label);
}
}
pwallet->SetAddressBook(dest, label, "receive");
} else {
- throw JSONRPCError(RPC_MISC_ERROR,
- "setlabel can only be used with own address");
+ pwallet->SetAddressBook(dest, label, "send");
}
return NullUniValue;
@@ -4367,6 +4388,17 @@
return ret;
}
+/** Convert CAddressBookData to JSON record. */
+static UniValue AddressBookDataToJSON(const CAddressBookData &data,
+ const bool verbose) {
+ UniValue ret(UniValue::VOBJ);
+ if (verbose) {
+ ret.pushKV("name", data.name);
+ }
+ ret.pushKV("purpose", data.purpose);
+ return ret;
+}
+
UniValue getaddressinfo(const Config &config, const JSONRPCRequest &request) {
std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request);
CWallet *const pwallet = wallet.get();
@@ -4433,6 +4465,16 @@
"keypath if the key is HD and available\n"
" \"hdmasterkeyid\" : \"\" (string, optional) The "
"Hash160 of the HD master pubkey\n"
+ " \"labels\" (object) Array of labels "
+ "associated with the address.\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"
+ " },...\n"
+ " ]\n"
"}\n"
"\nExamples:\n" +
HelpExampleCli("getaddressinfo",
@@ -4488,6 +4530,120 @@
ret.pushKV("hdmasterkeyid", meta->hdMasterKeyID.GetHex());
}
}
+
+ // 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.
+ UniValue labels(UniValue::VARR);
+ std::map::iterator mi =
+ pwallet->mapAddressBook.find(dest);
+ if (mi != pwallet->mapAddressBook.end()) {
+ labels.push_back(AddressBookDataToJSON(mi->second, true));
+ }
+ ret.pushKV("labels", std::move(labels));
+
+ return ret;
+}
+
+UniValue getaddressesbylabel(const Config &config,
+ const JSONRPCRequest &request) {
+ std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request);
+ CWallet *const pwallet = wallet.get();
+ if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) {
+ return NullUniValue;
+ }
+
+ if (request.fHelp || request.params.size() != 1)
+ throw std::runtime_error(
+ "getaddressesbylabel \"label\"\n"
+ "\nReturns the list of addresses assigned the specified label.\n"
+ "\nArguments:\n"
+ "1. \"label\" (string, required) The label.\n"
+ "\nResult:\n"
+ "{ (json object with addresses as keys)\n"
+ " \"address\": { (json object with information about address)\n"
+ " \"purpose\": \"string\" (string) Purpose of address "
+ "(\"send\" for sending address, \"receive\" for receiving "
+ "address)\n"
+ " },...\n"
+ "}\n"
+ "\nExamples:\n" +
+ HelpExampleCli("getaddressesbylabel", "\"tabby\"") +
+ HelpExampleRpc("getaddressesbylabel", "\"tabby\""));
+
+ LOCK(pwallet->cs_wallet);
+
+ std::string label = LabelFromValue(request.params[0]);
+
+ // Find all addresses that have the given label
+ UniValue ret(UniValue::VOBJ);
+ for (const std::pair &item :
+ pwallet->mapAddressBook) {
+ if (item.second.name == label) {
+ ret.pushKV(EncodeDestination(item.first, config),
+ AddressBookDataToJSON(item.second, false));
+ }
+ }
+
+ if (ret.empty()) {
+ throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME,
+ std::string("No addresses with label " + label));
+ }
+
+ return ret;
+}
+
+UniValue listlabels(const Config &config, const JSONRPCRequest &request) {
+ std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request);
+ CWallet *const pwallet = wallet.get();
+ if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) {
+ return NullUniValue;
+ }
+
+ if (request.fHelp || request.params.size() > 1)
+ throw std::runtime_error(
+ "listlabels ( \"purpose\" )\n"
+ "\nReturns the list of all labels, or labels that are assigned to "
+ "addresses with a specific purpose.\n"
+ "\nArguments:\n"
+ "1. \"purpose\" (string, optional) Address purpose to list "
+ "labels for ('send','receive'). An empty string is the same as not "
+ "providing this argument.\n"
+ "\nResult:\n"
+ "[ (json array of string)\n"
+ " \"label\", (string) Label name\n"
+ " ...\n"
+ "]\n"
+ "\nExamples:\n"
+ "\nList all labels\n" +
+ HelpExampleCli("listlabels", "") +
+ "\nList labels that have receiving addresses\n" +
+ HelpExampleCli("listlabels", "receive") +
+ "\nList labels that have sending addresses\n" +
+ HelpExampleCli("listlabels", "send") + "\nAs json rpc call\n" +
+ HelpExampleRpc("listlabels", "receive"));
+
+ LOCK(pwallet->cs_wallet);
+
+ std::string purpose;
+ if (!request.params[0].isNull()) {
+ purpose = request.params[0].get_str();
+ }
+
+ // Add to a set to sort by label name, then insert into Univalue array
+ std::set label_set;
+ for (const std::pair &entry :
+ pwallet->mapAddressBook) {
+ if (purpose.empty() || entry.second.purpose == purpose) {
+ label_set.insert(entry.second.name);
+ }
+ }
+
+ UniValue ret(UniValue::VARR);
+ for (const std::string &name : label_set) {
+ ret.push_back(name);
+ }
+
return ret;
}
@@ -4595,22 +4751,15 @@
{ "wallet", "backupwallet", backupwallet, {"destination"} },
{ "wallet", "createwallet", createwallet, {"wallet_name"} },
{ "wallet", "encryptwallet", encryptwallet, {"passphrase"} },
- { "wallet", "getaccountaddress", getlabeladdress, {"account"} },
- { "wallet", "getlabeladdress", getlabeladdress, {"label"} },
- { "wallet", "getaccount", getaccount, {"address"} },
- { "wallet", "getaddressesbyaccount", getaddressesbyaccount, {"account"} },
{ "wallet", "getaddressinfo", getaddressinfo, {"address"} },
{ "wallet", "getbalance", getbalance, {"account","minconf","include_watchonly"} },
{ "wallet", "getnewaddress", getnewaddress, {"label|account", "address_type"} },
{ "wallet", "getrawchangeaddress", getrawchangeaddress, {"address_type"} },
- { "wallet", "getreceivedbylabel", getreceivedbylabel, {"label","minconf"} },
- { "wallet", "getreceivedbyaccount", getreceivedbylabel, {"account","minconf"} },
{ "wallet", "getreceivedbyaddress", getreceivedbyaddress, {"address","minconf"} },
{ "wallet", "gettransaction", gettransaction, {"txid","include_watchonly"} },
{ "wallet", "getunconfirmedbalance", getunconfirmedbalance, {} },
{ "wallet", "getwalletinfo", getwalletinfo, {} },
{ "wallet", "keypoolrefill", keypoolrefill, {"newsize"} },
- { "wallet", "listaccounts", listaccounts, {"minconf","include_watchonly"} },
{ "wallet", "listaddressgroupings", listaddressgroupings, {} },
{ "wallet", "listlockunspent", listlockunspent, {} },
{ "wallet", "listreceivedbylabel", listreceivedbylabel, {"minconf","include_empty","include_watchonly"} },
@@ -4622,14 +4771,11 @@
{ "wallet", "listwallets", listwallets, {} },
{ "wallet", "loadwallet", loadwallet, {"filename"} },
{ "wallet", "lockunspent", lockunspent, {"unlock","transactions"} },
- { "wallet", "move", movecmd, {"fromaccount","toaccount","amount","minconf","comment"} },
{ "wallet", "rescanblockchain", rescanblockchain, {"start_height", "stop_height"} },
{ "wallet", "sethdseed", sethdseed, {"newkeypool","seed"} },
{ "wallet", "sendfrom", sendfrom, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} },
{ "wallet", "sendmany", sendmany, {"fromaccount","amounts","minconf","comment","subtractfeefrom"} },
{ "wallet", "sendtoaddress", sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount"} },
- { "wallet", "setlabel", setlabel, {"address","label"} },
- { "wallet", "setaccount", setlabel, {"address","account"} },
{ "wallet", "settxfee", settxfee, {"amount"} },
{ "wallet", "signmessage", signmessage, {"address","message"} },
{ "wallet", "signrawtransactionwithwallet", signrawtransactionwithwallet, {"hextring","prevtxs","sighashtype"} },
@@ -4637,6 +4783,25 @@
{ "wallet", "walletlock", walletlock, {} },
{ "wallet", "walletpassphrasechange", walletpassphrasechange, {"oldpassphrase","newpassphrase"} },
{ "wallet", "walletpassphrase", walletpassphrase, {"passphrase","timeout"} },
+
+ /** Account functions (deprecated) */
+ { "wallet", "getaccountaddress", getlabeladdress, {"account"} },
+ { "wallet", "getaccount", getaccount, {"address"} },
+ { "wallet", "getaddressesbyaccount", getaddressesbyaccount, {"account"} },
+ { "wallet", "getreceivedbyaccount", getreceivedbylabel, {"account","minconf"} },
+ { "wallet", "listaccounts", listaccounts, {"minconf","include_watchonly"} },
+ { "wallet", "listreceivedbyaccount", listreceivedbylabel, {"minconf","include_empty","include_watchonly"} },
+ { "wallet", "setaccount", setlabel, {"address","account"} },
+ { "wallet", "move", movecmd, {"fromaccount","toaccount","amount","minconf","comment"} },
+
+ /** Label functions (to replace non-balance account functions) */
+ { "wallet", "getlabeladdress", getlabeladdress, {"label","force"} },
+ { "wallet", "getaddressesbylabel", getaddressesbylabel, {"label"} },
+ { "wallet", "getreceivedbylabel", getreceivedbylabel, {"label","minconf"} },
+ { "wallet", "listlabels", listlabels, {"purpose"} },
+ { "wallet", "listreceivedbylabel", listreceivedbylabel, {"minconf","include_empty","include_watchonly"} },
+ { "wallet", "setlabel", setlabel, {"address","label"} },
+
{ "generating", "generate", generate, {"nblocks","maxtries"} },
};
// clang-format on
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -575,7 +575,7 @@
};
/**
- * Internal transfers.
+ * DEPRECATED Internal transfers.
* Database key is acentry.
*/
class CAccountingEntry {
@@ -1154,6 +1154,7 @@
EXCLUSIVE_LOCKS_REQUIRED(cs_main);
std::set GetLabelAddresses(const std::string &label) const;
+ void DeleteLabel(const std::string &label);
isminetype IsMine(const CTxIn &txin) const;
/**
@@ -1366,7 +1367,7 @@
};
/**
- * Account information.
+ * DEPRECATED Account information.
* Stored in wallet with key "acc"+string account name.
*/
class CAccount {
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -3969,6 +3969,11 @@
return result;
}
+void CWallet::DeleteLabel(const std::string &label) {
+ WalletBatch batch(*database);
+ batch.EraseAccount(label);
+}
+
bool CReserveKey::GetReservedKey(CPubKey &pubkey, bool internal) {
if (nIndex == -1) {
CKeyPool keypool;
diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h
--- a/src/wallet/walletdb.h
+++ b/src/wallet/walletdb.h
@@ -202,6 +202,7 @@
const CAccountingEntry &acentry);
bool ReadAccount(const std::string &strAccount, CAccount &account);
bool WriteAccount(const std::string &strAccount, const CAccount &account);
+ bool EraseAccount(const std::string &strAccount);
/// Write destination data key,value tuple to database.
bool WriteDestData(const CTxDestination &address, const std::string &key,
diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp
--- a/src/wallet/walletdb.cpp
+++ b/src/wallet/walletdb.cpp
@@ -179,6 +179,10 @@
return WriteIC(std::make_pair(std::string("acc"), strAccount), account);
}
+bool WalletBatch::EraseAccount(const std::string &strAccount) {
+ return EraseIC(std::make_pair(std::string("acc"), strAccount));
+}
+
bool WalletBatch::WriteAccountingEntry(const uint64_t nAccEntryNum,
const CAccountingEntry &acentry) {
return WriteIC(
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
@@ -12,6 +12,7 @@
- sendfrom (with account arguments)
- move (with account arguments)
"""
+from collections import defaultdict
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
@@ -75,9 +76,13 @@
# recognize the label/address associations.
labels = [Label(name) for name in ("a", "b", "c", "d", "e")]
for label in labels:
- label.add_receive_address(node.getlabeladdress(label.name))
+ label.add_receive_address(
+ node.getlabeladdress(label=label.name, force=True))
label.verify(node)
+ # Check all labels are returned by listlabels.
+ assert_equal(node.listlabels(), [label.name for label in labels])
+
# Send a transaction to each label, and make sure this forces
# getlabeladdress to generate a new receiving address.
for label in labels:
@@ -112,7 +117,7 @@
# Check that setlabel can assign a label to a new unused address.
for label in labels:
- address = node.getlabeladdress("")
+ address = node.getlabeladdress(label="", force=True)
node.setlabel(address, label.name)
label.add_address(address)
label.verify(node)
@@ -126,6 +131,7 @@
multisig_address = node.addmultisigaddress(
5, addresses, label.name)['address']
label.add_address(multisig_address)
+ label.purpose[multisig_address] = "send"
label.verify(node)
node.sendfrom("", multisig_address, 50)
node.generate(101)
@@ -145,9 +151,7 @@
change_label(node, labels[2].addresses[0], labels[2], labels[2])
# Check that setlabel can set the label of an address which is
- # already the receiving address of the label. It would probably make
- # sense for this to be a no-op, but right now it resets the receiving
- # address, causing getlabeladdress to return a brand new address.
+ # already the receiving address of the label. This is a no-op.
change_label(node, labels[2].receive_address, labels[2], labels[2])
@@ -159,6 +163,8 @@
self.receive_address = None
# List of all addresses assigned with this label
self.addresses = []
+ # Map of address to address purpose
+ self.purpose = defaultdict(lambda: "receive")
def add_address(self, address):
assert_equal(address not in self.addresses, True)
@@ -174,8 +180,15 @@
assert_equal(node.getlabeladdress(self.name), self.receive_address)
for address in self.addresses:
+ assert_equal(
+ node.getaddressinfo(address)['labels'][0],
+ {"name": self.name,
+ "purpose": self.purpose[address]})
assert_equal(node.getaccount(address), self.name)
+ assert_equal(
+ node.getaddressesbylabel(self.name),
+ {address: {"purpose": self.purpose[address]} for address in self.addresses})
assert_equal(
set(node.getaddressesbyaccount(self.name)), set(self.addresses))
@@ -191,7 +204,7 @@
# address of a different label should reset the receiving address of
# the old label, causing getlabeladdress to return a brand new
# address.
- if address == old_label.receive_address:
+ if old_label.name != new_label.name and address == old_label.receive_address:
new_address = node.getlabeladdress(old_label.name)
assert_equal(new_address not in old_label.addresses, True)
assert_equal(new_address not in new_label.addresses, True)
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
@@ -154,7 +154,7 @@
assert_equal(balance, balance_by_label + Decimal("0.1"))
# Create a new label named "mynewlabel" that has a 0 balance
- self.nodes[1].getlabeladdress("mynewlabel")
+ self.nodes[1].getlabeladdress(label="mynewlabel", force=True)
received_by_label_json = [r for r in self.nodes[1].listreceivedbylabel(
0, True) if r["label"] == "mynewlabel"][0]