Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rpc: introduce 'label' API for wallet #7729

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 188 additions & 26 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ UniValue getnewaddress(const JSONRPCRequest& request)
return EncodeDestination(dest);
}

void DeleteLabel(CWallet& wallet, std::string label)
{
CWalletDB walletdb(wallet.GetDBHandle());
walletdb.EraseAccount(label);
}

CTxDestination GetLabelDestination(CWallet* const pwallet, const std::string& label, bool bForceNew=false)
{
Expand All @@ -208,11 +213,11 @@ UniValue getlabeladdress(const JSONRPCRequest& request)
if (request.fHelp || request.params.size() != 1)
throw std::runtime_error(
"getlabeladdress \"label\"\n"
"\nReturns the current Bitcoin address for receiving payments to this label.\n"
"\nReturns the current 'label address' for this label.\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behaviour for this RPC is weird. If called for a label that doesn't exist, it creates a new label, and then adds a new address as the 'label address' for that label. That's not very intuitive, and I think it's a bad experience (for example if a user typos an existing label name). Can we change this so that the rpc returns an error if called with a non-existent label name?

"\nArguments:\n"
"1. \"label\" (string, required) The label name 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"
Copy link
Member

@Sjors Sjors Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with removing this remark, but note that the behavior of creating a new address still exists (though without an address type param). It might make sense to deprecate that behavior in a followup PR, and require getnewaddress if getlabeladdress doesn't return anything. That also makes the choice of potentially reusing an address more explicit.

"1. \"label\" (string, required) The label for the address. It can also be set to the empty string \"\" to represent the default label.\n"
"\nResult:\n"
"\"address\" (string) The label bitcoin address\n"
"\"bitcoinaddress\" (string) The 'label address' for the label\n"
"\nExamples:\n"
+ HelpExampleCli("getlabeladdress", "")
+ HelpExampleCli("getlabeladdress", "\"\"")
Expand Down Expand Up @@ -290,14 +295,14 @@ UniValue setlabel(const JSONRPCRequest& request)

if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error(
"setlabel \"address\" \"label\"\n"
"setlabel \"bitcoinaddress\" \"label\"\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why rename the arguments/return values to bitcoinaddress everywhere? Seems like a gratuitous API break.

address is used in many RPCs for a bitcoin address. Why not continue that convention? (and if you really must change this, current style guidelines call for snake_case for args)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why rename the arguments/return values to bitcoinaddress everywhere?

Agree it would be better to stick to address, but just as a historical note, this wasn't a "gratuitious" API break when it was originally written in 8571fee, because it preceded #11536, so the setlabel RPC was brand new.

"\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"
"1. \"bitcoinaddress\" (string, required) The bitcoin address to be associated with a label.\n"
"2. \"label\" (string, required) The label to assign to the address.\n"
"\nExamples:\n"
+ HelpExampleCli("setlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\" \"tabby\"")
+ HelpExampleRpc("setlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\", \"tabby\"")
+ HelpExampleCli("setlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XZ\" \"tabby\"")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change?

If you really wanted to change that DummyAddress(Params()) would be a better choice.

+ HelpExampleRpc("setlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XZ\", \"tabby\"")
);

LOCK2(cs_main, pwallet->cs_wallet);
Expand All @@ -308,22 +313,24 @@ UniValue setlabel(const JSONRPCRequest& request)
}

std::string label;
if (!request.params[1].isNull())
if (!request.params[1].isNull()) {
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 'label 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)) {
DeleteLabel(*pwallet, old_label);
}
}
pwallet->SetAddressBook(dest, label, "receive");
} else {
pwallet->SetAddressBook(dest, label, "send");
}
else
throw JSONRPCError(RPC_MISC_ERROR, "setlabel can only be used with own address");

return NullUniValue;
}
Expand Down Expand Up @@ -3809,6 +3816,152 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
return ret;
}

/** Convert CAddressBookData to JSON record.
* The verbosity of the output is configurable based on the command.
*/
static UniValue AddressBookDataToJSON(const CAddressBookData& data, bool verbose)
{
UniValue ret(UniValue::VOBJ);
if (verbose) {
ret.push_back(Pair("name", data.name));
}
ret.push_back(Pair("purpose", data.purpose));
if (verbose) {
UniValue ddata(UniValue::VOBJ);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, rename ddata to dest_data.

for (const std::pair<std::string, std::string>& item : data.destdata) {
ddata.push_back(Pair(item.first, item.second));
}
ret.push_back(Pair("destdata", ddata));
}
return ret;
}

UniValue getlabel(const JSONRPCRequest& request)
{
CWallet * const pwallet = GetWalletForJSONRPCRequest(request);
if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) {
return NullUniValue;
}

if (request.fHelp || request.params.size() != 1)
throw std::runtime_error(
"getlabel \"bitcoinaddress\"\n"
"\nReturns the label associated with the given address.\n"
"\nArguments:\n"
"1. \"bitcoinaddress\" (string, required) The bitcoin address for label lookup.\n"
"\nResult:\n"
" { (json object with information about address)\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this RPC return an array? We may want to be able to attach multiple labels to addresses in the future (#7729 (comment)), and making this RPC return an array will allow us to do that without a breaking API change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this RPC return an array?

It might be better to not add a getlabel RPC at all but instead just return this information in getaddressinfo (recently added in #10583).

" \"name\": \"labelname\" (string) The label\n"
" \"purpose\": \"string\" (string) Purpose of address (\"send\" for sending address, \"receive\" for receiving address)\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding AddressBookDataToJSON() correctly, then the result will also include an array of destdata.

Copy link
Contributor

@jnewbery jnewbery Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: https://github.com/bitcoin/bitcoin/pull/7729/files#r121986789 not addressed. Result also includes an array of destdata. Please update help text.

EDIT: I think we should just drop the destdata from the response. It wasn't available in the old account RPCs, and it appears to me that the only place we add to destdata is in saveReceiveRequest().

We can always add destdata to the response in a later PR if required.

" },...\n"
" Result is null if there is no record for this address.\n"
"\nExamples:\n"
+ HelpExampleCli("getlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XZ\"")
+ HelpExampleRpc("getlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XZ\"")
);

LOCK2(cs_main, pwallet->cs_wallet);

CTxDestination dest = DecodeDestination(request.params[0].get_str());
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address");
}

std::map<CTxDestination, CAddressBookData>::iterator mi = pwallet->mapAddressBook.find(dest);
if (mi != pwallet->mapAddressBook.end()) {
return AddressBookDataToJSON(mi->second, true);
}
return NullUniValue;
}

UniValue getaddressesbylabel(const JSONRPCRequest& request)
{
CWallet * const pwallet = GetWalletForJSONRPCRequest(request);
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"
" \"bitcoinaddress\": { (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\"")
);

LOCK2(cs_main, pwallet->cs_wallet);

std::string strLabel = LabelFromValue(request.params[0]);

// Find all addresses that have the given label
UniValue ret(UniValue::VOBJ);
for (const std::pair<CTxDestination, CAddressBookData>& item : pwallet->mapAddressBook) {
if (item.second.name == strLabel) {
ret.push_back(Pair(EncodeDestination(item.first), AddressBookDataToJSON(item.second, false)));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this throw an error if the label doesn't exist? Currently it returns an empty object.

return ret;
}

UniValue listlabels(const JSONRPCRequest& request)
{
CWallet * const pwallet = GetWalletForJSONRPCRequest(request);
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think An empty string is the same as not providing this argument. is required

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @jnewbery, empty value should be an error then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, why is this argument needed? I mean, the client can filter the result. IMO pagination is more interesting.

"\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")
);

LOCK2(cs_main, pwallet->cs_wallet);

std::string purpose;
if (request.params.size() > 0) {
purpose = request.params[0].get_str();
}

std::set<std::string> setLabels;
for (const std::pair<CTxDestination, CAddressBookData>& entry : pwallet->mapAddressBook) {
if (purpose.empty() || entry.second.purpose == purpose){
setLabels.insert(entry.second.name);
}
}
UniValue ret(UniValue::VARR);
for (const std::string &name : setLabels) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we sort the label names before returning?

ret.push_back(name);
}

return ret;
}

extern UniValue abortrescan(const JSONRPCRequest& request); // in rpcdump.cpp
extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp
extern UniValue importprivkey(const JSONRPCRequest& request);
Expand All @@ -3835,16 +3988,9 @@ static const CRPCCommand commands[] =
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
{ "wallet", "getlabeladdress", &getlabeladdress, {"label"} },
{ "wallet", "getaccountaddress", &getlabeladdress, {"account"} },
{ "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, {} },
Expand All @@ -3856,7 +4002,6 @@ static const CRPCCommand commands[] =
{ "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },
{ "wallet", "importpubkey", &importpubkey, {"pubkey","label","rescan"} },
{ "wallet", "keypoolrefill", &keypoolrefill, {"newsize"} },
{ "wallet", "listaccounts", &listaccounts, {"minconf","include_watchonly"} },
{ "wallet", "listaddressgroupings", &listaddressgroupings, {} },
{ "wallet", "listlockunspent", &listlockunspent, {} },
{ "wallet", "listreceivedbylabel", &listreceivedbylabel, {"minconf","include_empty","include_watchonly"} },
Expand All @@ -3867,12 +4012,9 @@ static const CRPCCommand commands[] =
{ "wallet", "listunspent", &listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} },
{ "wallet", "listwallets", &listwallets, {} },
{ "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} },
{ "wallet", "move", &movecmd, {"fromaccount","toaccount","amount","minconf","comment"} },
{ "wallet", "sendfrom", &sendfrom, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} },
{ "wallet", "sendmany", &sendmany, {"fromaccount","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode"} },
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode"} },
{ "wallet", "setlabel", &setlabel, {"address","label"} },
{ "wallet", "setaccount", &setlabel, {"address","account"} },
{ "wallet", "settxfee", &settxfee, {"amount"} },
{ "wallet", "signmessage", &signmessage, {"address","message"} },
{ "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} },
Expand All @@ -3882,6 +4024,26 @@ static const CRPCCommand commands[] =
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
{ "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} },

/** Account functions (deprecated) */
{ "wallet", "getaccountaddress", &getlabeladdress, {"account"} },
{ "wallet", "getaccount", &getaccount, {"address"} },
{ "wallet", "getaddressesbyaccount", &getaddressesbyaccount, {"account"} },
{ "wallet", "getaddressinfo", &getaddressinfo, {"address"} },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think getaddressinfo should be deprecated. It's not an account rpc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think getaddressinfo should be deprecated. It's not an account rpc

Good catch. getaddressinfo should remain where it was above "getbalance". It's my fault for accidentally moving it into this section during a rebase.

{ "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"} },
{ "wallet", "getlabel", &getlabel, {"bitcoinaddress"} },
{ "wallet", "getaddressesbylabel", &getaddressesbylabel, {"label"} },
{ "wallet", "getreceivedbylabel", &getreceivedbylabel, {"label","minconf"} },
{ "wallet", "listlabels", &listlabels, {"purpose"} },
{ "wallet", "listreceivedbylabel", &listreceivedbylabel, {"minconf","include_empty","include_watchonly"} },
{ "wallet", "setlabel", &setlabel, {"bitcoinaddress","label"} },

{ "generating", "generate", &generate, {"nblocks","maxtries"} },
};

Expand Down
4 changes: 2 additions & 2 deletions src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ class CWalletKey
};

/**
* Internal transfers.
* DEPRECATED Internal transfers.
* Database key is acentry<account><counter>.
*/
class CAccountingEntry
Expand Down Expand Up @@ -1202,7 +1202,7 @@ class CReserveKey final : public CReserveScript


/**
* Account information.
* DEPRECATED Account information.
* Stored in wallet with key "acc"+string account name.
*/
class CAccount
Expand Down
5 changes: 5 additions & 0 deletions src/wallet/walletdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ bool CWalletDB::WriteAccount(const std::string& strAccount, const CAccount& acco
return WriteIC(std::make_pair(std::string("acc"), strAccount), account);
}

bool CWalletDB::EraseAccount(const std::string& strAccount)
{
return EraseIC(std::make_pair(std::string("acc"), strAccount));
}

bool CWalletDB::WriteAccountingEntry(const uint64_t nAccEntryNum, const CAccountingEntry& acentry)
{
return WriteIC(std::make_pair(std::string("acentry"), std::make_pair(acentry.strAccount, nAccEntryNum)), acentry);
Expand Down
1 change: 1 addition & 0 deletions src/wallet/walletdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ class CWalletDB
bool WriteAccountingEntry(const uint64_t nAccEntryNum, 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comment above may need updating to:

  • remove comments about accounting entries (or say they're deprecated)
  • comment that 'acc' database entries are for labels.


/// Write destination data key,value tuple to database
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
Expand Down
Loading