diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 5bbb5088e24..fc5005d0af5 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -152,6 +152,11 @@ UniValue getnewaddress(const JSONRPCRequest& request) return CBitcoinAddress(keyID).ToString(); } +void DeleteAccount(CWallet * const pwallet, std::string strAccount) +{ + CWalletDB walletdb(pwallet->GetDBHandle()); + walletdb.EraseAccount(strAccount); +} CBitcoinAddress GetAccountAddress(CWallet* const pwallet, std::string strAccount, bool bForceNew=false) { @@ -2922,6 +2927,236 @@ UniValue bumpfee(const JSONRPCRequest& request) return result; } +UniValue getlabeladdress(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( + "getlabeladdress \"label\"\n" + "\nReturns the current 'label address' for this label.\n" + "\nArguments:\n" + "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" + "\"bitcoinaddress\" (string) The 'label address' for the label\n" + "\nExamples:\n" + + HelpExampleCli("getlabeladdress", "") + + HelpExampleCli("getlabeladdress", "\"\"") + + HelpExampleCli("getlabeladdress", "\"mylabel\"") + + HelpExampleRpc("getlabeladdress", "\"mylabel\"") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + // Parse the label first so we don't generate a key if there's an error + std::string strLabel = AccountFromValue(request.params[0]); + + UniValue ret(UniValue::VSTR); + + ret = GetAccountAddress(pwallet, strLabel).ToString(); + 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); + for (const std::pair& 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" + " \"name\": \"labelname\" (string) The label\n" + " \"purpose\": \"string\" (string) Purpose of address (\"send\" for sending address, \"receive\" for receiving address)\n" + " },...\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); + + CBitcoinAddress address(request.params[0].get_str()); + if (!address.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); + } + + std::map::iterator mi = pwallet->mapAddressBook.find(address.Get()); + 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 = AccountFromValue(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 == strLabel) { + ret.push_back(Pair(item.first.ToString(), AddressBookDataToJSON(item.second, false))); + } + } + 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" + "\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 setLabels; + for (const std::pair& 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) { + ret.push_back(name); + } + + return ret; +} + +UniValue setlabel(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) + throw std::runtime_error( + "setlabel \"bitcoinaddress\" \"label\"\n" + "\nSets the label associated with the given address.\n" + "\nArguments:\n" + "1. \"bitcoinaddress\" (string, required) The bitcoin address to be associated with an label.\n" + "2. \"label\" (string, required) The label to assign to the address.\n" + "\nExamples:\n" + + HelpExampleCli("setlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XZ\" \"tabby\"") + + HelpExampleRpc("setlabel", "\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XZ\", \"tabby\"") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + CBitcoinAddress address(request.params[0].get_str()); + if (!address.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); + } + + std::string strLabel; + if (request.params.size() > 1){ + strLabel = AccountFromValue(request.params[1]); + } + + if (IsMine(*pwallet, address.Get())) + { + // 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 we wouldn't do this, the record would stick around forever. + if (pwallet->mapAddressBook.count(address.Get())) + { + std::string strOldLabel = pwallet->mapAddressBook[address.Get()].name; + if (strOldLabel != strLabel && address == GetAccountAddress(pwallet, strOldLabel)) { + DeleteAccount(pwallet, strOldLabel); + } + } + pwallet->SetAddressBook(address.Get(), strLabel, "receive"); + } else { + pwallet->SetAddressBook(address.Get(), strLabel, "send"); + } + + return NullUniValue; +} + extern UniValue abortrescan(const JSONRPCRequest& request); // in rpcdump.cpp extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp extern UniValue importprivkey(const JSONRPCRequest& request); @@ -2947,13 +3182,9 @@ static const CRPCCommand commands[] = { "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} }, { "wallet", "dumpwallet", &dumpwallet, true, {"filename"} }, { "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} }, - { "wallet", "getaccountaddress", &getaccountaddress, true, {"account"} }, - { "wallet", "getaccount", &getaccount, true, {"address"} }, - { "wallet", "getaddressesbyaccount", &getaddressesbyaccount, true, {"account"} }, { "wallet", "getbalance", &getbalance, false, {"account","minconf","include_watchonly"} }, { "wallet", "getnewaddress", &getnewaddress, true, {"account"} }, { "wallet", "getrawchangeaddress", &getrawchangeaddress, true, {} }, - { "wallet", "getreceivedbyaccount", &getreceivedbyaccount, false, {"account","minconf"} }, { "wallet", "getreceivedbyaddress", &getreceivedbyaddress, false, {"address","minconf"} }, { "wallet", "gettransaction", &gettransaction, false, {"txid","include_watchonly"} }, { "wallet", "getunconfirmedbalance", &getunconfirmedbalance, false, {} }, @@ -2965,26 +3196,39 @@ static const CRPCCommand commands[] = { "wallet", "importprunedfunds", &importprunedfunds, true, {"rawtransaction","txoutproof"} }, { "wallet", "importpubkey", &importpubkey, true, {"pubkey","label","rescan"} }, { "wallet", "keypoolrefill", &keypoolrefill, true, {"newsize"} }, - { "wallet", "listaccounts", &listaccounts, false, {"minconf","include_watchonly"} }, { "wallet", "listaddressgroupings", &listaddressgroupings, false, {} }, { "wallet", "listlockunspent", &listlockunspent, false, {} }, - { "wallet", "listreceivedbyaccount", &listreceivedbyaccount, false, {"minconf","include_empty","include_watchonly"} }, { "wallet", "listreceivedbyaddress", &listreceivedbyaddress, false, {"minconf","include_empty","include_watchonly"} }, { "wallet", "listsinceblock", &listsinceblock, false, {"blockhash","target_confirmations","include_watchonly"} }, { "wallet", "listtransactions", &listtransactions, false, {"account","count","skip","include_watchonly"} }, { "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, { "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} }, - { "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, { "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, { "wallet", "sendmany", &sendmany, false, {"fromaccount","amounts","minconf","comment","subtractfeefrom"} }, { "wallet", "sendtoaddress", &sendtoaddress, false, {"address","amount","comment","comment_to","subtractfeefromamount"} }, - { "wallet", "setaccount", &setaccount, true, {"address","account"} }, { "wallet", "settxfee", &settxfee, true, {"amount"} }, { "wallet", "signmessage", &signmessage, true, {"address","message"} }, { "wallet", "walletlock", &walletlock, true, {} }, { "wallet", "walletpassphrasechange", &walletpassphrasechange, true, {"oldpassphrase","newpassphrase"} }, { "wallet", "walletpassphrase", &walletpassphrase, true, {"passphrase","timeout"} }, { "wallet", "removeprunedfunds", &removeprunedfunds, true, {"txid"} }, + + /** Account functions (deprecated) */ + { "wallet", "getaccountaddress", &getaccountaddress, true, {"account"} }, + { "wallet", "getaccount", &getaccount, true, {"address"} }, + { "wallet", "getaddressesbyaccount", &getaddressesbyaccount, true, {"account"} }, + { "wallet", "getreceivedbyaccount", &getreceivedbyaccount, false, {"account","minconf"} }, + { "wallet", "listaccounts", &listaccounts, false, {"minconf","include_watchonly"} }, + { "wallet", "listreceivedbyaccount", &listreceivedbyaccount, false, {"minconf","include_empty","include_watchonly"} }, + { "wallet", "setaccount", &setaccount, true, {"address","account"} }, + { "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, + + /** Label functions (to replace non-balance account functions) */ + { "wallet", "getlabeladdress", &getlabeladdress, true, {"label"} }, + { "wallet", "getlabel", &getlabel, true, {"bitcoinaddress"} }, + { "wallet", "getaddressesbylabel", &getaddressesbylabel, true, {"label"} }, + { "wallet", "listlabels", &listlabels, false, {"purpose"} }, + { "wallet", "setlabel", &setlabel, true, {"bitcoinaddress","label"} }, }; void RegisterWalletRPCCommands(CRPCTable &t) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index a3fd7408a0e..8cbc74556b5 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -561,7 +561,7 @@ class CWalletKey }; /** - * Internal transfers. + * DEPRECATED Internal transfers. * Database key is acentry. */ class CAccountingEntry @@ -1158,7 +1158,7 @@ class CReserveKey : public CReserveScript /** - * Account information. + * DEPRECATED Account information. * Stored in wallet with key "acc"+string account name. */ class CAccount diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 8321719b560..bcb12b788b9 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -167,6 +167,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); diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index d78f143ebd6..b109176938c 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -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); /// Write destination data key,value tuple to database bool WriteDestData(const std::string &address, const std::string &key, const std::string &value); diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 54f625514bd..0d96f74ef65 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -114,6 +114,7 @@ 'p2p-leaktests.py', 'wallet-encryption.py', 'uptime.py', + 'wallet-labels.py', ] EXTENDED_SCRIPTS = [ diff --git a/test/functional/wallet-labels.py b/test/functional/wallet-labels.py new file mode 100755 index 00000000000..e2399f33b85 --- /dev/null +++ b/test/functional/wallet-labels.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the wallet label API""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +class Label: + def __init__(self, name): + self.name = name + # Current default "label address" associated with this label. + self.label_address = None + # List of all addresses assigned with this label + self.addresses = [] + + def add_address(self, address, is_label_address): + assert_equal(address not in self.addresses, True) + if is_label_address: + self.label_address = address + self.addresses.append(address) + + def verify(self, node): + if self.label_address is not None: + assert self.label_address in self.addresses + assert_equal(node.getlabeladdress(self.name), self.label_address) + + for address in self.addresses: + assert_equal( + node.getlabel(address), + {"name": self.name, + "purpose": "receive", + "destdata": {}}) + + assert_equal( + node.getaddressesbylabel(self.name), + {address: { + "purpose": "receive" + } for address in self.addresses}) + +def overwrite_label(node, old_label, address_idx, is_label_address, new_label): + address = old_label.addresses[address_idx] + assert_equal(is_label_address, address == old_label.label_address) + + node.setlabel(address, new_label.name) + + if old_label.name != new_label.name: + del old_label.addresses[address_idx] + new_label.add_address(address, False) + + # Calling setlabel on an address which was previously the default + # "label address" of a different label should cause that label to no + # longer have any default "label address" at all, and for + # getlabeladdress to return a brand new address. + if is_label_address: + new_address = node.getlabeladdress(old_label.name) + assert_equal(new_address not in new_label.addresses, True) + old_label.add_address(new_address, True) + + old_label.verify(node) + new_label.verify(node) + +class WalletLabelsTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 1 + + def run_test(self): + node = self.nodes[0] + amount_to_send = 1.0 + labels = [Label(name) for name in ("a", "b", "c", "d", "e")] + + # Check that there's no UTXO on the node, and generate a spendable + # balance. + assert_equal(len(node.listunspent()), 0) + node.generate(101) + assert_equal(node.getbalance(), 50) + + # Create an address for each label and make sure subsequent label API + # calls recognize the association. + for label in labels: + label.add_address(node.getlabeladdress(label.name), 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 address, and make sure this forces + # getlabeladdress to generate new unused addresses. + for label in labels: + node.sendtoaddress(label.label_address, amount_to_send) + label.add_address(node.getlabeladdress(label.name), True) + label.verify(node) + + # Mine block to confirm sends, then check the amounts received. + node.generate(1) + for label in labels: + assert_equal( + node.getreceivedbyaddress(label.addresses[0]), amount_to_send) + # FIXME: Uncomment after API getreceivedbylabel is added + # (see https://github.com/bitcoin/bitcoin/pull/7729#issuecomment-206927479) + # assert_equal(node.getreceivedbylabel(label.name), amount_to_send) + + # Check that setlabel can add a label to a new unused address. + for label in labels: + address = node.getlabeladdress("") + node.setlabel(address, label.name) + label.add_address(address, False) + label.verify(node) + + # Check that setlabel can safely overwrite the label of an address with + # a different label. + overwrite_label(node, labels[0], 0, False, labels[1]) + + # Check that setlabel can safely overwrite the label of an address + # which is the default "label address" of a different label. + overwrite_label(node, labels[0], 0, True, labels[1]) + + # Check that setlabel can safely overwrite the label of an address + # which already has the same label, effectively performing a no-op. + overwrite_label(node, labels[2], 0, False, labels[2]) + + # Check that setlabel can safely overwrite the label of an address + # which already has the same label, and which is the default "label + # address" of that label, effectively performing a no-op. + overwrite_label(node, labels[2], 1, True, labels[2]) + +if __name__ == '__main__': + WalletLabelsTest().main()