mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-05 14:06:27 -05:00
Merge bitcoin/bitcoin#26094: rpc: Return block hash & height in getbalances, gettransaction and getwalletinfo
710b83938a
rpc: return block hash & height in getbalances, gettransaction & getwalletinfo JSONs (Harris) Pull request description: Reopens #18570 and closes #18567. I have rebased the original PR. Not sure why the original got closed as it was about to get merged. ACKs for top commit: achow101: ACK710b83938a
Tree-SHA512: d4478d990be98b1642e9ffb2930987f4a224e8bd64e2e35a5dda927a54c509ec9d712cd0eac35dc2bb89f00a1678e530ce14d7445f1dd93aa3a4cce2bc9b130d
This commit is contained in:
commit
da9f62f912
9 changed files with 70 additions and 6 deletions
6
doc/release-notes-26094.md
Normal file
6
doc/release-notes-26094.md
Normal 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.
|
|
@ -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, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"},
|
||||||
{RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"},
|
{RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"},
|
||||||
}},
|
}},
|
||||||
|
RESULT_LAST_PROCESSED_BLOCK,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RPCExamples{
|
RPCExamples{
|
||||||
|
@ -488,6 +489,8 @@ RPCHelpMan getbalances()
|
||||||
balances_watchonly.pushKV("immature", ValueFromAmount(bal.m_watchonly_immature));
|
balances_watchonly.pushKV("immature", ValueFromAmount(bal.m_watchonly_immature));
|
||||||
balances.pushKV("watchonly", balances_watchonly);
|
balances.pushKV("watchonly", balances_watchonly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppendLastProcessedBlock(balances, wallet);
|
||||||
return balances;
|
return balances;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -731,6 +731,7 @@ RPCHelpMan gettransaction()
|
||||||
{
|
{
|
||||||
{RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."},
|
{RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."},
|
||||||
}},
|
}},
|
||||||
|
RESULT_LAST_PROCESSED_BLOCK,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
RPCExamples{
|
RPCExamples{
|
||||||
|
@ -791,6 +792,7 @@ RPCHelpMan gettransaction()
|
||||||
entry.pushKV("decoded", decoded);
|
entry.pushKV("decoded", decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppendLastProcessedBlock(entry, *pwallet);
|
||||||
return entry;
|
return entry;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -177,4 +177,14 @@ void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& st
|
||||||
throw JSONRPCError(code, error.original);
|
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
|
} // namespace wallet
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
#ifndef BITCOIN_WALLET_RPC_UTIL_H
|
#ifndef BITCOIN_WALLET_RPC_UTIL_H
|
||||||
#define BITCOIN_WALLET_RPC_UTIL_H
|
#define BITCOIN_WALLET_RPC_UTIL_H
|
||||||
|
|
||||||
|
#include <rpc/util.h>
|
||||||
#include <script/script.h>
|
#include <script/script.h>
|
||||||
|
#include <wallet/wallet.h>
|
||||||
|
|
||||||
#include <any>
|
#include <any>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -17,13 +19,17 @@ class UniValue;
|
||||||
struct bilingual_str;
|
struct bilingual_str;
|
||||||
|
|
||||||
namespace wallet {
|
namespace wallet {
|
||||||
class CWallet;
|
|
||||||
class LegacyScriptPubKeyMan;
|
class LegacyScriptPubKeyMan;
|
||||||
enum class DatabaseStatus;
|
enum class DatabaseStatus;
|
||||||
struct WalletContext;
|
struct WalletContext;
|
||||||
|
|
||||||
extern const std::string HELP_REQUIRING_PASSPHRASE;
|
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.
|
* 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 PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry);
|
||||||
|
|
||||||
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error);
|
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error);
|
||||||
|
|
||||||
int64_t ParseISO8601DateTime(const std::string& str);
|
int64_t ParseISO8601DateTime(const std::string& str);
|
||||||
|
void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
|
||||||
} // namespace wallet
|
} // namespace wallet
|
||||||
|
|
||||||
#endif // BITCOIN_WALLET_RPC_UTIL_H
|
#endif // BITCOIN_WALLET_RPC_UTIL_H
|
||||||
|
|
|
@ -68,6 +68,7 @@ static RPCHelpMan getwalletinfo()
|
||||||
}, /*skip_type_check=*/true},
|
}, /*skip_type_check=*/true},
|
||||||
{RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"},
|
{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"},
|
{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{
|
RPCExamples{
|
||||||
|
@ -129,6 +130,8 @@ static RPCHelpMan getwalletinfo()
|
||||||
}
|
}
|
||||||
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
|
obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
|
||||||
obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
|
obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
|
||||||
|
|
||||||
|
AppendLastProcessedBlock(obj, *pwallet);
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ from test_framework.blocktools import COINBASE_MATURITY
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework.util import (
|
from test_framework.util import (
|
||||||
assert_equal,
|
assert_equal,
|
||||||
|
assert_is_hash_string,
|
||||||
assert_raises_rpc_error,
|
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
|
'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:
|
if self.options.descriptors:
|
||||||
del expected_balances_0["watchonly"]
|
del expected_balances_0["watchonly"]
|
||||||
assert_equal(self.nodes[0].getbalances(), expected_balances_0)
|
balances_0 = self.nodes[0].getbalances()
|
||||||
assert_equal(self.nodes[1].getbalances(), expected_balances_1)
|
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
|
# 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[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
|
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'))
|
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__':
|
if __name__ == '__main__':
|
||||||
WalletTest().main()
|
WalletTest().main()
|
||||||
|
|
|
@ -680,7 +680,7 @@ class WalletTest(BitcoinTestFramework):
|
||||||
"category": baz["category"],
|
"category": baz["category"],
|
||||||
"vout": baz["vout"]}
|
"vout": baz["vout"]}
|
||||||
expected_fields = frozenset({'amount', 'bip125-replaceable', 'confirmations', 'details', 'fee',
|
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"
|
verbose_field = "decoded"
|
||||||
expected_verbose_fields = expected_fields | {verbose_field}
|
expected_verbose_fields = expected_fields | {verbose_field}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,10 @@ class OrphanedBlockRewardTest(BitcoinTestFramework):
|
||||||
assert_equal(self.nodes[0].getbestblockhash(), orig_chain_tip)
|
assert_equal(self.nodes[0].getbestblockhash(), orig_chain_tip)
|
||||||
self.generate(self.nodes[0], 3)
|
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)
|
assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue