rpc: return block hash & height in getbalances, gettransaction & getwalletinfo JSONs

Co-authored-by: Aurèle Oulès <aurele@oules.com>
This commit is contained in:
Harris 2020-04-27 10:45:03 +02:00 committed by Aurèle Oulès
parent 91ccb62faa
commit 710b83938a
9 changed files with 70 additions and 6 deletions

View file

@ -0,0 +1,6 @@
- The `getbalances` RPC now returns a `lastprocessedblock` JSON object which contains the wallet's last processed block
hash and height at the time the balances were calculated. This result shouldn't be cached because importing new keys could invalidate it.
- The `gettransaction` RPC now returns a `lastprocessedblock` JSON object which contains the wallet's last processed block
hash and height at the time the transaction information was generated.
- The `getwalletinfo` RPC now returns a `lastprocessedblock` JSON object which contains the wallet's last processed block
hash and height at the time the wallet information was generated.

View file

@ -448,6 +448,7 @@ RPCHelpMan getbalances()
{RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"},
{RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"},
}},
RESULT_LAST_PROCESSED_BLOCK,
}
},
RPCExamples{
@ -488,6 +489,8 @@ RPCHelpMan getbalances()
balances_watchonly.pushKV("immature", ValueFromAmount(bal.m_watchonly_immature));
balances.pushKV("watchonly", balances_watchonly);
}
AppendLastProcessedBlock(balances, wallet);
return balances;
},
};

View file

@ -731,6 +731,7 @@ RPCHelpMan gettransaction()
{
{RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."},
}},
RESULT_LAST_PROCESSED_BLOCK,
})
},
RPCExamples{
@ -791,6 +792,7 @@ RPCHelpMan gettransaction()
entry.pushKV("decoded", decoded);
}
AppendLastProcessedBlock(entry, *pwallet);
return entry;
},
};

View file

@ -177,4 +177,14 @@ void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& st
throw JSONRPCError(code, error.original);
}
}
void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{
AssertLockHeld(wallet.cs_wallet);
UniValue lastprocessedblock{UniValue::VOBJ};
lastprocessedblock.pushKV("hash", wallet.GetLastBlockHash().GetHex());
lastprocessedblock.pushKV("height", wallet.GetLastBlockHeight());
entry.pushKV("lastprocessedblock", lastprocessedblock);
}
} // namespace wallet

View file

@ -5,7 +5,9 @@
#ifndef BITCOIN_WALLET_RPC_UTIL_H
#define BITCOIN_WALLET_RPC_UTIL_H
#include <rpc/util.h>
#include <script/script.h>
#include <wallet/wallet.h>
#include <any>
#include <memory>
@ -17,13 +19,17 @@ class UniValue;
struct bilingual_str;
namespace wallet {
class CWallet;
class LegacyScriptPubKeyMan;
enum class DatabaseStatus;
struct WalletContext;
extern const std::string HELP_REQUIRING_PASSPHRASE;
static const RPCResult RESULT_LAST_PROCESSED_BLOCK { RPCResult::Type::OBJ, "lastprocessedblock", "hash and height of the block this information was generated on",{
{RPCResult::Type::STR_HEX, "hash", "hash of the block this information was generated on"},
{RPCResult::Type::NUM, "height", "height of the block this information was generated on"}}
};
/**
* Figures out what wallet, if any, to use for a JSONRPCRequest.
*
@ -45,8 +51,8 @@ std::string LabelFromValue(const UniValue& value);
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry);
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error);
int64_t ParseISO8601DateTime(const std::string& str);
void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
} // namespace wallet
#endif // BITCOIN_WALLET_RPC_UTIL_H

View file

@ -68,6 +68,7 @@ static RPCHelpMan getwalletinfo()
}, /*skip_type_check=*/true},
{RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"},
{RPCResult::Type::BOOL, "external_signer", "whether this wallet is configured to use an external signer such as a hardware wallet"},
RESULT_LAST_PROCESSED_BLOCK,
}},
},
RPCExamples{
@ -129,6 +130,8 @@ static RPCHelpMan getwalletinfo()
}
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
AppendLastProcessedBlock(obj, *pwallet);
return obj;
},
};

View file

@ -11,6 +11,7 @@ from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_is_hash_string,
assert_raises_rpc_error,
)
@ -183,8 +184,13 @@ class WalletTest(BitcoinTestFramework):
'untrusted_pending': Decimal('30.0') - fee_node_1}} # Doesn't include output of node 0's send since it was spent
if self.options.descriptors:
del expected_balances_0["watchonly"]
assert_equal(self.nodes[0].getbalances(), expected_balances_0)
assert_equal(self.nodes[1].getbalances(), expected_balances_1)
balances_0 = self.nodes[0].getbalances()
balances_1 = self.nodes[1].getbalances()
# remove lastprocessedblock keys (they will be tested later)
del balances_0['lastprocessedblock']
del balances_1['lastprocessedblock']
assert_equal(balances_0, expected_balances_0)
assert_equal(balances_1, expected_balances_1)
# getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions
assert_equal(self.nodes[0].getbalance(), Decimal('9.99')) # change from node 0's send
assert_equal(self.nodes[1].getbalance(), Decimal('0')) # node 1's send had an unsafe input
@ -309,5 +315,30 @@ class WalletTest(BitcoinTestFramework):
assert_equal(self.nodes[0].getbalances()['mine']['untrusted_pending'], Decimal('0.1'))
# Tests the lastprocessedblock JSON object in getbalances, getwalletinfo
# and gettransaction by checking for valid hex strings and by comparing
# the hashes & heights between generated blocks.
self.log.info("Test getbalances returns expected lastprocessedblock json object")
prev_hash = self.nodes[0].getbestblockhash()
prev_height = self.nodes[0].getblock(prev_hash)['height']
self.generatetoaddress(self.nodes[0], 5, self.nodes[0].get_deterministic_priv_key().address)
lastblock = self.nodes[0].getbalances()['lastprocessedblock']
assert_is_hash_string(lastblock['hash'])
assert_equal((prev_hash == lastblock['hash']), False)
assert_equal(lastblock['height'], prev_height + 5)
prev_hash = self.nodes[0].getbestblockhash()
prev_height = self.nodes[0].getblock(prev_hash)['height']
self.log.info("Test getwalletinfo returns expected lastprocessedblock json object")
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(walletinfo['lastprocessedblock']['height'], prev_height)
assert_equal(walletinfo['lastprocessedblock']['hash'], prev_hash)
self.log.info("Test gettransaction returns expected lastprocessedblock json object")
txid = self.nodes[1].sendtoaddress(self.nodes[1].getnewaddress(), 0.01)
tx_info = self.nodes[1].gettransaction(txid)
assert_equal(tx_info['lastprocessedblock']['height'], prev_height)
assert_equal(tx_info['lastprocessedblock']['hash'], prev_hash)
if __name__ == '__main__':
WalletTest().main()

View file

@ -666,7 +666,7 @@ class WalletTest(BitcoinTestFramework):
"category": baz["category"],
"vout": baz["vout"]}
expected_fields = frozenset({'amount', 'bip125-replaceable', 'confirmations', 'details', 'fee',
'hex', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts'})
'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts'})
verbose_field = "decoded"
expected_verbose_fields = expected_fields | {verbose_field}

View file

@ -65,7 +65,10 @@ class OrphanedBlockRewardTest(BitcoinTestFramework):
assert_equal(self.nodes[0].getbestblockhash(), orig_chain_tip)
self.generate(self.nodes[0], 3)
assert_equal(self.nodes[1].getbalances(), pre_reorg_conf_bals)
balances = self.nodes[1].getbalances()
del balances["lastprocessedblock"]
del pre_reorg_conf_bals["lastprocessedblock"]
assert_equal(balances, pre_reorg_conf_bals)
assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True)