Merge pull request #6122 from nuttycom/wallet/listunspent_as_of

Add `asOfHeight` argument across the RPC API
This commit is contained in:
Greg Pfeil 2022-11-23 21:20:20 -07:00 committed by GitHub
commit f6a4f68115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 608 additions and 350 deletions

View File

@ -62,6 +62,7 @@ BASE_SCRIPTS= [
'wallet_overwintertx.py',
'wallet_persistence.py',
'wallet_listnotes.py',
'wallet_listunspent.py',
# vv Tests less than 60s vv
'orchard_reorg.py',
'fundrawtransaction.py',

View File

@ -7,9 +7,17 @@
# Exercise the listtransactions API
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from decimal import Decimal
def count_array_matches(object_array, to_match):
num_matched = 0
for item in object_array:
if all((item[key] == value for key,value in to_match.items())):
num_matched = num_matched+1
return num_matched
def check_array_result(object_array, to_match, expected):
"""
Pass in array of JSON objects, a dictionary with key/value pairs
@ -45,6 +53,7 @@ class ListTransactionsTest(BitcoinTestFramework):
{"category":"receive","amount":Decimal("0.1"),"amountZat":10000000,"confirmations":0})
# mine a block, confirmations should change:
old_block = self.nodes[0].getblockcount()
self.nodes[0].generate(1)
self.sync_all()
check_array_result(self.nodes[0].listtransactions(),
@ -53,6 +62,16 @@ class ListTransactionsTest(BitcoinTestFramework):
check_array_result(self.nodes[1].listtransactions(),
{"txid":txid},
{"category":"receive","amount":Decimal("0.1"),"amountZat":10000000,"confirmations":1})
# Confirmations here are -1 instead of 0 because while we only went back
# 1 block, “0” means the tx is in the mempool, but the tx has already
# been mined and `asOfHeight` ignores the mempool regardless.
check_array_result(self.nodes[0].listtransactions("*", 10, 0, False, old_block),
{"txid":txid},
{"category":"send","amount":Decimal("-0.1"),"amountZat":-10000000,"confirmations":-1})
# And while we can still see the tx we sent before the block where they
# got mined, we dont have access to ones well receive after the
# specified block.
assert_equal(0, count_array_matches(self.nodes[1].listtransactions("*", 10, 0, False, old_block), {"txid":txid}))
# send-to-self:
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 0.2)

View File

@ -56,17 +56,18 @@ class BitcoinTestFramework(object):
# Connect the nodes as a "chain". This allows us
# to split the network between nodes 1 and 2 to get
# two halves that can work on competing chains.
connect_nodes_bi(self.nodes, 0, 1)
# If we joined network halves, connect the nodes from the joint
# on outward. This ensures that chains are properly reorganised.
if len(self.nodes) >= 4:
connect_nodes_bi(self.nodes, 2, 3)
if not split:
connect_nodes_bi(self.nodes, 1, 2)
sync_blocks(self.nodes[1:3])
if do_mempool_sync:
sync_mempools(self.nodes[1:3])
connect_nodes_bi(self.nodes, 0, 1)
connect_nodes_bi(self.nodes, 2, 3)
self.is_network_split = split
self.sync_all(do_mempool_sync)

View File

@ -307,15 +307,15 @@ def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'):
wait_bitcoinds()
for i in range(MAX_NODES):
# record the system time at which the cache was regenerated
with open(log_filename(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file:
with open(node_file(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file:
cache_config = { "cache_time": time.time() }
cache_conf_json = json.dumps(cache_config, indent=4)
cache_conf_file.write(cache_conf_json)
os.remove(log_filename(cachedir, i, "debug.log"))
os.remove(log_filename(cachedir, i, "db.log"))
os.remove(log_filename(cachedir, i, "peers.dat"))
os.remove(log_filename(cachedir, i, "fee_estimates.dat"))
os.remove(node_file(cachedir, i, "debug.log"))
os.remove(node_file(cachedir, i, "db.log"))
os.remove(node_file(cachedir, i, "peers.dat"))
os.remove(node_file(cachedir, i, "fee_estimates.dat"))
def init_from_cache():
for i in range(num_nodes):
@ -351,7 +351,7 @@ def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'):
for i in range(MAX_NODES):
node_path = os.path.join(cachedir, 'node'+str(i))
if os.path.isdir(node_path):
if not os.path.isfile(log_filename(cachedir, i, 'cache_config.json')):
if not os.path.isfile(node_file(cachedir, i, 'cache_config.json')):
return True
else:
return True
@ -461,8 +461,8 @@ def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None):
raise
return rpcs
def log_filename(dirname, n_node, logname):
return os.path.join(dirname, "node"+str(n_node), "regtest", logname)
def node_file(dirname, n_node, filename):
return os.path.join(dirname, "node"+str(n_node), "regtest", filename)
def check_node(i):
bitcoind_processes[i].poll()

View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
# Copyright (c) 2016-2022 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
get_coinbase_address,
nuparams,
start_nodes,
wait_and_assert_operationid_status,
NU5_BRANCH_ID
)
from decimal import Decimal
def unspent_total(unspent):
return sum((item['amount'] for item in unspent))
class WalletListUnspent(BitcoinTestFramework):
def setup_nodes(self):
return start_nodes(4, self.options.tmpdir, [[
nuparams(NU5_BRANCH_ID, 201),
]] * 4)
def matured_at_height(self, height):
return unspent_total(self.nodes[0].listunspent(1, 999999, [], False, {}, height))
def run_test(self):
def expected_matured_at_height(height):
return (height-200)*10 + 250
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(200))
assert_equal(self.nodes[1].getbalance(), expected_matured_at_height(200))
# Activate NU5
self.nodes[1].generate(1) # height 201
self.sync_all()
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(201))
# check balances from before the latest tx
assert_equal(self.nodes[0].getbalance("", 1, False, False, 200), expected_matured_at_height(200))
assert_equal(self.matured_at_height(200), expected_matured_at_height(200))
# Shield some coinbase funds so that they become spendable
n1acct = self.nodes[1].z_getnewaccount()['account']
n1uaddr = self.nodes[1].z_getaddressforaccount(n1acct)['address']
opid = self.nodes[0].z_sendmany(
get_coinbase_address(self.nodes[0]),
[{'address': n1uaddr, 'amount': 10}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.sync_all()
self.nodes[1].generate(2)
self.sync_all() # height 203
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(203) - 10)
assert_equal(
self.nodes[1].z_getbalanceforaccount(n1acct, 1)['pools']['orchard']['valueZat'],
Decimal('1000000000'))
# Get a bare legacy transparent address for node 0
n0addr = self.nodes[0].getnewaddress()
# Send funds to the node 0 address so we have transparent funds to spend.
opid = self.nodes[1].z_sendmany(
n1uaddr,
[{'address': n0addr, 'amount': 10}],
1, 0, 'AllowRevealedRecipients')
wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
self.nodes[1].generate(2)
self.sync_all() # height 205
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(205) - 10 + 10)
# We will then perform several spends, and then check the list of
# unspent notes as of various heights.
opid = self.nodes[0].z_sendmany(
'ANY_TADDR',
# FIXME: #6262 The amount here _should_ be 2, but because of a
# bug in the selector for `ANY_TADDR`, its selecting
# transparent coinbase, which also means we cant have
# change. When that bug is fixed, the test should fail
# here, and we can switch it back to 2 (and cascade the
# corrected amounts mentioned below.
[{'address': n1uaddr, 'amount': 10}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(2)
self.sync_all() # height 207
# FIXME: #6262, should be `expected_matured_at_height(207) - 10 + 10 - 2`
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(207) - 10 + 10 - 10)
opid = self.nodes[0].z_sendmany(
'ANY_TADDR',
# FIXME: Should be 3 (see above)
[{'address': n1uaddr, 'amount': 10}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(2)
self.sync_all() # height 209
# FIXME: #6262, should be `expected_matured_at_height(209) - 10 + 10 - 2 - 3`
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(209) - 10 + 10 - 10 - 10)
opid = self.nodes[0].z_sendmany(
'ANY_TADDR',
# FIXME: Should be 5 (see above)
[{'address': n1uaddr, 'amount': 10}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(2)
self.sync_all() # height 211
# FIXME: #6262, should be `expected_matured_at_height(211) - 10 + 10 - 2 - 3 - 5`
assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(211) - 10 + 10 - 10 - 10 - 10)
# check balances at various past points in the chain
# FIXME: #6262, change the comparison amounts when the above changes are made.
assert_equal(self.matured_at_height(205), expected_matured_at_height(205) - 10 + 10)
assert_equal(self.matured_at_height(207), expected_matured_at_height(207) - 10 + 10 - 10)
assert_equal(self.matured_at_height(209), expected_matured_at_height(209) - 10 + 10 - 10 - 10)
assert_equal(self.matured_at_height(211), expected_matured_at_height(211) - 10 + 10 - 10 - 10 - 10)
if __name__ == '__main__':
WalletListUnspent().main()

View File

@ -93,7 +93,7 @@ UniValue getinfo(const UniValue& params, bool fHelp)
#ifdef ENABLE_WALLET
if (pwalletMain) {
obj.pushKV("walletversion", pwalletMain->GetVersion());
obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance()));
obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance(std::nullopt)));
}
#endif
obj.pushKV("blocks", (int)chainActive.Height());

View File

@ -550,6 +550,55 @@ std::string experimentalDisabledHelpMsg(const std::string& rpc, const std::vecto
+ config;
}
std::string asOfHeightMessage(bool hasMinconf) {
std::string minconfInteraction = hasMinconf
? " `minconf` must be at least 1 when `asOfHeight` is provided.\n"
: "";
return
"asOfHeight (numeric, optional, default=-1) Execute the query as if it\n"
" were run when the blockchain was at the height specified by\n"
" this argument. The default is to use the entire blockchain\n"
" that the node is aware of. -1 can be used as in other RPC\n"
" calls to indicate the current height (including the\n"
" mempool), but this does not support negative values in\n"
" general. A “future” height will fall back to the current\n"
" height. Any explicit value will cause the mempool to be\n"
" ignored, meaning no unconfirmed tx will be considered.\n"
+ minconfInteraction;
}
std::optional<int> parseAsOfHeight(const UniValue& params, int index) {
std::optional<int> asOfHeight;
if (params.size() > index) {
auto requestedHeight = params[index].get_int();
if (requestedHeight == -1) {
// the default, do nothing
} else if (requestedHeight < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Can not perform the query as of a negative block height");
} else if (requestedHeight == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Can not perform the query as of the genesis block");
} else {
asOfHeight = requestedHeight;
}
}
return asOfHeight;
}
int parseMinconf(int defaultValue, const UniValue& params, int index, const std::optional<int>& asOfHeight) {
int nMinDepth = defaultValue;
if (params.size() > index) {
auto requestedDepth = params[index].get_int();
if (requestedDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
} else if (requestedDepth == 0 && asOfHeight.has_value()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Require a minimum of 1 confirmation when `asOfHeight` is provided");
} else {
nMinDepth = requestedDepth;
}
}
return nMinDepth;
}
void RPCRegisterTimerInterface(RPCTimerInterface *iface)
{
timerInterfaces.push_back(iface);

View File

@ -192,6 +192,10 @@ std::string JSONRPCExecBatch(const UniValue& vReq);
extern std::string experimentalDisabledHelpMsg(const std::string& rpc, const std::vector<std::string>& enableArgs);
std::string asOfHeightMessage(bool hasMinconf);
std::optional<int> parseAsOfHeight(const UniValue& params, int index);
int parseMinconf(int defaultValue, const UniValue& params, int index, const std::optional<int>& asOfHeight);
extern int interpretHeightArg(int nHeight, int currentHeight);
extern int parseHeightArg(const std::string& strHeight, int currentHeight);

View File

@ -639,7 +639,7 @@ bool AsyncRPCOperation_mergetoaddress::main_impl()
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString()));
}
wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight;
wtxDepth = wtx.GetDepthInMainChain();
wtxDepth = wtx.GetDepthInMainChain(std::nullopt);
}
LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vJoinSplit=%d, jsoutindex=%d, amount=%s, height=%d, confirmations=%d)\n",
getId(),

View File

@ -86,7 +86,7 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
// We set minDepth to 11 to avoid unconfirmed notes and in anticipation of specifying
// an anchor at height N-10 for each Sprout JoinSplit description
// Consider, should notes be sorted?
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 11);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 11);
}
CAmount availableFunds = 0;
for (const SproutNoteEntry& sproutEntry : sproutEntries) {

View File

@ -192,7 +192,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
SpendableInputs spendable;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_);
spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_, std::nullopt);
}
if (!spendable.LimitToAmount(targetAmount, dustThreshold, recipientPools_)) {
CAmount changeAmount{spendable.Total() - targetAmount};

View File

@ -214,18 +214,18 @@ TEST(WalletTests, FindUnspentSproutNotes) {
wtx.SetSproutNoteData(noteData);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
// We currently have an unspent and unconfirmed note in the wallet (depth of -1)
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, -1);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, -1);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
@ -244,21 +244,21 @@ TEST(WalletTests, FindUnspentSproutNotes) {
wtx.SetMerkleBranch(block);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
// We now have an unspent and confirmed note in the wallet (depth of 1)
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 1);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
@ -268,7 +268,7 @@ TEST(WalletTests, FindUnspentSproutNotes) {
// Let's spend the note.
auto wtx2 = GetValidSproutSpend(sk, note, 5);
wallet.LoadWalletTx(wtx2);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
// Fake-mine a spend transaction
EXPECT_EQ(0, chainActive.Height());
@ -286,22 +286,22 @@ TEST(WalletTests, FindUnspentSproutNotes) {
wtx2.SetMerkleBranch(block2);
wallet.LoadWalletTx(wtx2);
EXPECT_TRUE(wallet.IsSproutSpent(nullifier));
EXPECT_TRUE(wallet.IsSproutSpent(nullifier, std::nullopt));
// The note has been spent. By default, GetFilteredNotes() ignores spent notes.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Let's include spent notes to retrieve it.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0, INT_MAX, false);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// The spent note has two confirmations.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2, INT_MAX, false);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
@ -328,7 +328,7 @@ TEST(WalletTests, FindUnspentSproutNotes) {
wtx.SetSproutNoteData(noteData);
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
wtx3 = wtx;
}
@ -351,25 +351,25 @@ TEST(WalletTests, FindUnspentSproutNotes) {
wallet.LoadWalletTx(wtx3);
// We now have an unspent note which has one confirmation, in addition to our spent note.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 1);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Let's return the spent note too.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 1, INT_MAX, false);
EXPECT_EQ(2, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// Increasing number of confirmations will exclude our new unspent note.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2, INT_MAX, false);
EXPECT_EQ(1, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
orchardEntries.clear();
// If we also ignore spent notes at this depth, we won't find any notes.
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, true);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2, INT_MAX, true);
EXPECT_EQ(0, sproutEntries.size());
sproutEntries.clear();
saplingEntries.clear();
@ -877,7 +877,7 @@ TEST(WalletTests, GetConflictedOrchardNotes) {
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, -1);
wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, -1);
EXPECT_EQ(0, sproutEntries.size());
EXPECT_EQ(0, saplingEntries.size());
EXPECT_EQ(1, orchardEntries.size());
@ -957,14 +957,14 @@ TEST(WalletTests, SproutNullifierIsSpent) {
auto note = GetSproutNote(sk, wtx, 0, 1);
auto nullifier = note.nullifier(sk);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
wallet.LoadWalletTx(wtx);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
auto wtx2 = GetValidSproutSpend(sk, note, 5);
wallet.LoadWalletTx(wtx2);
EXPECT_FALSE(wallet.IsSproutSpent(nullifier));
EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt));
// Fake-mine the transaction
EXPECT_EQ(-1, chainActive.Height());
@ -980,7 +980,7 @@ TEST(WalletTests, SproutNullifierIsSpent) {
wtx2.SetMerkleBranch(block);
wallet.LoadWalletTx(wtx2);
EXPECT_TRUE(wallet.IsSproutSpent(nullifier));
EXPECT_TRUE(wallet.IsSproutSpent(nullifier, std::nullopt));
// Tear down
chainActive.SetTip(NULL);
@ -1018,7 +1018,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) {
uint256 nullifier = nf.value();
// Verify note has not been spent
EXPECT_FALSE(wallet.IsSaplingSpent(nullifier));
EXPECT_FALSE(wallet.IsSaplingSpent(nullifier, std::nullopt));
// Fake-mine the transaction
EXPECT_EQ(-1, chainActive.Height());
@ -1036,7 +1036,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) {
wallet.LoadWalletTx(wtx);
// Verify note has been spent
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier));
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier, std::nullopt));
// Tear down
chainActive.SetTip(NULL);
@ -1107,7 +1107,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) {
MerkleFrontiers frontiers = { .sapling = testNote.tree };
// Verify dummy note is unspent
EXPECT_FALSE(wallet.IsSaplingSpent(nullifier));
EXPECT_FALSE(wallet.IsSaplingSpent(nullifier, std::nullopt));
// Fake-mine the transaction
EXPECT_EQ(-1, chainActive.Height());
@ -1129,7 +1129,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) {
wallet.LoadWalletTx(wtx);
// Verify dummy note is now spent, as AddToWallet invokes AddToSpends()
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier));
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier, std::nullopt));
// Test invariant: no witnesses means no nullifier.
EXPECT_EQ(0, wallet.mapSaplingNullifiersToNotes.size());
@ -1334,7 +1334,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
wallet.LoadWalletTx(wtx2);
// Verify note B is spent. LoadWalletTx invokes AddToSpends which updates mapTxSaplingNullifiers
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier2));
EXPECT_TRUE(wallet.IsSaplingSpent(nullifier2, std::nullopt));
// Verify note B belongs to wallet.
EXPECT_TRUE(wallet.IsFromMe(wtx2));

View File

@ -115,9 +115,9 @@ void ThrowIfInitialBlockDownload()
}
}
void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry)
void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry, const std::optional<int>& asOfHeight)
{
int confirms = wtx.GetDepthInMainChain();
int confirms = wtx.GetDepthInMainChain(asOfHeight);
std::string status = "waiting";
entry.pushKV("confirmations", confirms);
@ -264,7 +264,7 @@ UniValue getrawchangeaddress(const UniValue& params, bool fHelp)
static void SendMoney(const CTxDestination &address, CAmount nValue, bool fSubtractFeeFromAmount, CWalletTx& wtxNew)
{
CAmount curBalance = pwalletMain->GetBalance();
CAmount curBalance = pwalletMain->GetBalance(std::nullopt);
// Check amount
if (nValue <= 0)
@ -285,7 +285,7 @@ static void SendMoney(const CTxDestination &address, CAmount nValue, bool fSubtr
CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount};
vecSend.push_back(recipient);
if (!pwalletMain->CreateTransaction(vecSend, wtxNew, reservekey, nFeeRequired, nChangePosRet, strError)) {
if (!fSubtractFeeFromAmount && nValue + nFeeRequired > pwalletMain->GetBalance())
if (!fSubtractFeeFromAmount && nValue + nFeeRequired > pwalletMain->GetBalance(std::nullopt))
strError = strprintf("Error: This transaction requires a transaction fee of at least %s because of its amount, complexity, or use of recently received funds!", FormatMoney(nFeeRequired));
throw JSONRPCError(RPC_WALLET_ERROR, strError);
}
@ -484,7 +484,7 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
// with respect to the entries in the address book for addresses generated by this wallet,
// there is not a guarantee that an externally generated address (such as one associated with
// a future unified incoming viewing key) will have been added to the address book.
for (const std::pair<CTxDestination, CAmount>& item : pwalletMain->GetAddressBalances()) {
for (const std::pair<CTxDestination, CAmount>& item : pwalletMain->GetAddressBalances(std::nullopt)) {
if (t_generated_dests.count(item.first) == 0 &&
t_mnemonic_dests.count(item.first) == 0 &&
t_imported_dests.count(item.first) == 0 &&
@ -841,11 +841,13 @@ UniValue listaddressgroupings(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp)
if (fHelp || params.size() > 1)
throw runtime_error(
"listaddressgroupings\n"
"listaddressgroupings ( asOfHeight )\n"
"\nLists groups of transparent addresses which have had their common ownership\n"
"made public by common use as inputs or as the resulting change in past transactions.\n"
"\nArguments:\n"
"1. " + asOfHeightMessage(false) +
"\nResult:\n"
"[\n"
" [\n"
@ -857,6 +859,8 @@ UniValue listaddressgroupings(const UniValue& params, bool fHelp)
" ]\n"
" ,...\n"
"]\n"
"\nBitcoin compatibility:\n"
"The zero-argument form is compatible."
"\nExamples:\n"
+ HelpExampleCli("listaddressgroupings", "")
+ HelpExampleRpc("listaddressgroupings", "")
@ -864,9 +868,11 @@ UniValue listaddressgroupings(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
auto asOfHeight = parseAsOfHeight(params, 0);
KeyIO keyIO(Params());
UniValue jsonGroupings(UniValue::VARR);
std::map<CTxDestination, CAmount> balances = pwalletMain->GetAddressBalances();
std::map<CTxDestination, CAmount> balances = pwalletMain->GetAddressBalances(asOfHeight);
for (const std::set<CTxDestination>& grouping : pwalletMain->GetAddressGroupings()) {
UniValue jsonGrouping(UniValue::VARR);
for (const CTxDestination& address : grouping)
@ -952,16 +958,19 @@ UniValue getreceivedbyaddress(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 1 || params.size() > 3)
if (fHelp || params.size() < 1 || params.size() > 4)
throw runtime_error(
"getreceivedbyaddress \"zcashaddress\" ( minconf ) ( inZat )\n"
"getreceivedbyaddress \"zcashaddress\" ( minconf inZat asOfHeight )\n"
"\nReturns the total amount received by the given transparent Zcash address in transactions with at least minconf confirmations.\n"
"\nArguments:\n"
"1. \"zcashaddress\" (string, required) The Zcash address for transactions.\n"
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
"3. inZat (bool, optional, default=false) Get the result amount in " + MINOR_CURRENCY_UNIT + " (as an integer).\n"
"4. " + asOfHeightMessage(true) +
"\nResult:\n"
"amount (numeric) The total amount in " + CURRENCY_UNIT + "(or " + MINOR_CURRENCY_UNIT + " if inZat is true) received at this address.\n"
"\nBitcoin compatibility:\n"
"Compatible with up to two arguments."
"\nExamples:\n"
"\nThe amount from transactions with at least 1 confirmation\n"
+ HelpExampleCli("getreceivedbyaddress", "\"t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1\"") +
@ -987,10 +996,10 @@ UniValue getreceivedbyaddress(const UniValue& params, bool fHelp)
return ValueFromAmount(0);
}
auto asOfHeight = parseAsOfHeight(params, 3);
// Minimum confirmations
int nMinDepth = 1;
if (params.size() > 1)
nMinDepth = params[1].get_int();
int nMinDepth = parseMinconf(1, params, 1, asOfHeight);
// Tally
CAmount nAmount = 0;
@ -1002,7 +1011,7 @@ UniValue getreceivedbyaddress(const UniValue& params, bool fHelp)
for (const CTxOut& txout : wtx.vout)
if (txout.scriptPubKey == scriptPubKey)
if (wtx.GetDepthInMainChain() >= nMinDepth)
if (wtx.GetDepthInMainChain(asOfHeight) >= nMinDepth)
nAmount += txout.nValue;
}
@ -1019,19 +1028,22 @@ UniValue getbalance(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 4)
if (fHelp || params.size() > 5)
throw runtime_error(
"getbalance ( \"(dummy)\" minconf includeWatchonly inZat )\n"
"getbalance ( \"(dummy)\" minconf includeWatchonly inZat asOfHeight )\n"
"\nReturns the wallet's available transparent balance. This total\n"
"currently includes transparent balances associated with unified\n"
"accounts. Prefer to use `z_getbalanceforaccount` instead.\n"
"\nArguments:\n"
"1. (dummy) (string, optional) Remains for backward compatibility. Must be excluded or set to \"*\" or \"\".\n"
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
"2. minconf (numeric, optional, default=0) Only include transactions confirmed at least this many times.\n"
"3. includeWatchonly (bool, optional, default=false) Also include balance in watchonly addresses (see 'importaddress')\n"
"4. inZat (bool, optional, default=false) Get the result amount in " + MINOR_CURRENCY_UNIT + " (as an integer).\n"
"5. " + asOfHeightMessage(true) +
"\nResult:\n"
"amount (numeric) The total amount in " + CURRENCY_UNIT + "(or " + MINOR_CURRENCY_UNIT + " if inZat is true) received.\n"
"\nBitcoin compatibility:\n"
"Compatible with up to three arguments."
"\nExamples:\n"
"\nThe total amount in the wallet\n"
+ HelpExampleCli("getbalance", "*") +
@ -1048,17 +1060,16 @@ UniValue getbalance(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "dummy first argument must be excluded or set to \"*\" or \"\".");
}
int min_depth = 0;
if (!params[1].isNull()) {
min_depth = params[1].get_int();
}
auto asOfHeight = parseAsOfHeight(params, 4);
int min_depth = parseMinconf(0, params, 1, asOfHeight);
isminefilter filter = ISMINE_SPENDABLE;
if (!params[2].isNull() && params[2].get_bool()) {
filter = filter | ISMINE_WATCH_ONLY;
}
CAmount nBalance = pwalletMain->GetBalance(filter, min_depth);
CAmount nBalance = pwalletMain->GetBalance(asOfHeight, filter, min_depth);
if (!params[3].isNull() && params[3].get_bool()) {
return nBalance;
} else {
@ -1078,7 +1089,7 @@ UniValue getunconfirmedbalance(const UniValue &params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
return ValueFromAmount(pwalletMain->GetUnconfirmedBalance());
return ValueFromAmount(pwalletMain->GetUnconfirmedTransparentBalance());
}
@ -1134,9 +1145,7 @@ UniValue sendmany(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Dummy value must be set to \"\"");
}
UniValue sendTo = params[1].get_obj();
int nMinDepth = 1;
if (params.size() > 2)
nMinDepth = params[2].get_int();
int nMinDepth = parseMinconf(1, params, 2, std::nullopt);
CWalletTx wtx;
if (params.size() > 3 && !params[3].isNull() && !params[3].get_str().empty())
@ -1274,10 +1283,18 @@ struct tallyitem
UniValue ListReceived(const UniValue& params)
{
if (params.size() > 3 && params[3].get_str() != "") {
throw JSONRPCError(RPC_INVALID_PARAMETER, "addressFilter must be set to \"\"");
}
if (params.size() > 4 && params[4].get_bool() != false) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "includeImmatureCoinbase must be set to false");
}
auto asOfHeight = parseAsOfHeight(params, 5);
// Minimum confirmations
int nMinDepth = 1;
if (params.size() > 0)
nMinDepth = params[0].get_int();
int nMinDepth = parseMinconf(1, params, 0, asOfHeight);
// Whether to include empty accounts
bool fIncludeEmpty = false;
@ -1297,7 +1314,7 @@ UniValue ListReceived(const UniValue& params)
if (wtx.IsCoinBase() || !CheckFinalTx(wtx))
continue;
int nDepth = wtx.GetDepthInMainChain();
int nDepth = wtx.GetDepthInMainChain(asOfHeight);
if (nDepth < nMinDepth)
continue;
@ -1367,9 +1384,9 @@ UniValue listreceivedbyaddress(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 3)
if (fHelp || params.size() > 6)
throw runtime_error(
"listreceivedbyaddress ( minconf includeempty includeWatchonly)\n"
"listreceivedbyaddress ( minconf includeempty includeWatchonly addressFilter includeImmatureCoinbase asOfHeight )\n"
"\nList balances by transparent receiving address. This API does not provide\n"
"any information for associated with shielded addresses and should only be used\n"
"in circumstances where it is necessary to interoperate with legacy Bitcoin\n"
@ -1378,7 +1395,9 @@ UniValue listreceivedbyaddress(const UniValue& params, bool fHelp)
"1. minconf (numeric, optional, default=1) The minimum number of confirmations before payments are included.\n"
"2. includeempty (numeric, optional, default=false) Whether to include addresses that haven't received any payments.\n"
"3. includeWatchonly (bool, optional, default=false) Whether to include watchonly addresses (see 'importaddress').\n"
"4. addressFilter (string, optional, default=\"\") If present and non-empty, only return information on this address. Currently, only the default value is supported.\n"
"5. includeImmatureCoinbase (bool, optional, default=false) Include immature coinbase transactions. Currently, only the default value is supported.\n"
"6. " + asOfHeightMessage(true) +
"\nResult:\n"
"[\n"
" {\n"
@ -1390,7 +1409,8 @@ UniValue listreceivedbyaddress(const UniValue& params, bool fHelp)
" }\n"
" ,...\n"
"]\n"
"\nBitcoin compatibility:\n"
"Compatible up to five arguments, but can only use the default value for `addressFilter` and `includeImmatureCoinbase`."
"\nExamples:\n"
+ HelpExampleCli("listreceivedbyaddress", "")
+ HelpExampleCli("listreceivedbyaddress", "6 true")
@ -1418,8 +1438,9 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
* @param fLong Whether to include the JSON version of the transaction.
* @param ret The UniValue into which the result is stored.
* @param filter The "is mine" filter flags.
* @param asOfHeight The last block to look at.
*/
void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter)
void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter, const std::optional<int>& asOfHeight)
{
CAmount nFee;
std::list<COutputEntry> listReceived;
@ -1445,14 +1466,14 @@ void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue&
entry.pushKV("vout", s.vout);
entry.pushKV("fee", ValueFromAmount(-nFee));
if (fLong)
WalletTxToJSON(wtx, entry);
WalletTxToJSON(wtx, entry, asOfHeight);
entry.pushKV("size", static_cast<uint64_t>(GetSerializeSize(static_cast<CTransaction>(wtx), SER_NETWORK, PROTOCOL_VERSION)));
ret.push_back(entry);
}
}
// Received
if (listReceived.size() > 0 && wtx.GetDepthInMainChain() >= nMinDepth)
if (listReceived.size() > 0 && wtx.GetDepthInMainChain(asOfHeight) >= nMinDepth)
{
for (const COutputEntry& r : listReceived)
{
@ -1467,9 +1488,9 @@ void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue&
MaybePushAddress(entry, r.destination);
if (wtx.IsCoinBase())
{
if (wtx.GetDepthInMainChain() < 1)
if (wtx.GetDepthInMainChain(asOfHeight) < 1)
entry.pushKV("category", "orphan");
else if (wtx.GetBlocksToMaturity() > 0)
else if (wtx.GetBlocksToMaturity(asOfHeight) > 0)
entry.pushKV("category", "immature");
else
entry.pushKV("category", "generate");
@ -1482,7 +1503,7 @@ void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue&
entry.pushKV("amountZat", r.amount);
entry.pushKV("vout", r.vout);
if (fLong)
WalletTxToJSON(wtx, entry);
WalletTxToJSON(wtx, entry, asOfHeight);
entry.pushKV("size", static_cast<uint64_t>(GetSerializeSize(static_cast<CTransaction>(wtx), SER_NETWORK, PROTOCOL_VERSION)));
ret.push_back(entry);
}
@ -1494,9 +1515,9 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 4)
if (fHelp || params.size() > 5)
throw runtime_error(
"listtransactions ( \"dummy\" count from includeWatchonly)\n"
"listtransactions ( \"dummy\" count from includeWatchonly asOfHeight)\n"
"\nReturns up to 'count' of the most recent transactions associated with legacy transparent\n"
"addresses of this wallet, skipping the first 'from' transactions.\n"
"\nThis API does not provide any information about transactions containing shielded inputs\n"
@ -1508,6 +1529,7 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
"2. count (numeric, optional, default=10) The number of transactions to return\n"
"3. from (numeric, optional, default=0) The number of transactions to skip\n"
"4. includeWatchonly (bool, optional, default=false) Include transactions to watchonly addresses (see 'importaddress')\n"
"5. " + asOfHeightMessage(false) +
"\nResult:\n"
"[\n"
" {\n"
@ -1562,6 +1584,8 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
if(params[3].get_bool())
filter = filter | ISMINE_WATCH_ONLY;
auto asOfHeight = parseAsOfHeight(params, 4);
if (nCount < 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count");
if (nFrom < 0)
@ -1577,7 +1601,7 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it)
{
CWalletTx *const pwtx = (*it).second;
ListTransactions(*pwtx, 0, true, ret, filter);
ListTransactions(*pwtx, 0, true, ret, filter, asOfHeight);
if ((int)ret.size() >= (nCount+nFrom)) break;
}
}
@ -1614,14 +1638,17 @@ UniValue listsinceblock(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp)
if (fHelp || params.size() > 6)
throw runtime_error(
"listsinceblock ( \"blockhash\" target-confirmations includeWatchonly)\n"
"listsinceblock ( \"blockhash\" target-confirmations includeWatchonly includeRemoved includeChange asOfHeight )\n"
"\nGet all transactions in blocks since block [blockhash], or all transactions if omitted\n"
"\nArguments:\n"
"1. \"blockhash\" (string, optional) The block hash to list transactions since\n"
"2. target-confirmations: (numeric, optional) The confirmations required, must be 1 or more\n"
"3. includeWatchonly: (bool, optional, default=false) Include transactions to watchonly addresses (see 'importaddress')"
"4. includeRemoved (bool, optional, default=true) Show transactions that were removed due to a reorg in the \"removed\" array (not guaranteed to work on pruned nodes)\n"
"5. includeChange (bool, optional, default=false) Also add entries for change outputs. Currently, only the default value is supported.\n"
"6. " + asOfHeightMessage(false) +
"\nResult:\n"
"{\n"
" \"transactions\": [\n"
@ -1644,8 +1671,12 @@ UniValue listsinceblock(const UniValue& params, bool fHelp)
" \"comment\": \"...\", (string) If a comment is associated with the transaction.\n"
" \"to\": \"...\", (string) If a comment to is associated with the transaction.\n"
" ],\n"
" \"removed\": [...] (array of objects, optional) structure is the same as \"transactions\" above, only present if includeRemoved=true\n"
" Note: currently this only returns an empty array.\n"
" \"lastblock\": \"lastblockhash\" (string) The hash of the last block\n"
"}\n"
"\nBitcoin compatibility:\n"
"Compatible up to five arguments, but can only use the default value for `includeChange`, and only returns an empty array for \"removed\"."
"\nExamples:\n"
+ HelpExampleCli("listsinceblock", "")
+ HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6")
@ -1680,6 +1711,17 @@ UniValue listsinceblock(const UniValue& params, bool fHelp)
if(params[2].get_bool())
filter = filter | ISMINE_WATCH_ONLY;
bool includeRemoved = true;
if (params.size() > 3) {
includeRemoved = params[3].get_bool();
}
if (params.size() > 4 && params[4].get_bool() != false) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "includeChange must be set to false");
}
auto asOfHeight = parseAsOfHeight(params, 5);
int depth = pindex ? (1 + chainActive.Height() - pindex->nHeight) : -1;
UniValue transactions(UniValue::VARR);
@ -1687,8 +1729,8 @@ UniValue listsinceblock(const UniValue& params, bool fHelp)
for (const std::pair<const uint256, CWalletTx>& pairWtx : pwalletMain->mapWallet) {
CWalletTx tx = pairWtx.second;
if (depth == -1 || tx.GetDepthInMainChain() < depth) {
ListTransactions(tx, 0, true, transactions, filter);
if (depth == -1 || tx.GetDepthInMainChain(std::nullopt) < depth) {
ListTransactions(tx, 0, true, transactions, filter, asOfHeight);
}
}
@ -1697,6 +1739,9 @@ UniValue listsinceblock(const UniValue& params, bool fHelp)
UniValue ret(UniValue::VOBJ);
ret.pushKV("transactions", transactions);
if (includeRemoved) {
ret.pushKV("removed", UniValue(UniValue::VARR));
}
ret.pushKV("lastblock", lastblock.GetHex());
return ret;
@ -1707,15 +1752,17 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 1 || params.size() > 2)
if (fHelp || params.size() < 1 || params.size() > 4)
throw runtime_error(
"gettransaction \"txid\" ( includeWatchonly )\n"
"gettransaction \"txid\" ( includeWatchonly verbose asOfHeight )\n"
"\nReturns detailed information about in-wallet transaction <txid>. This does not\n"
"include complete information about shielded components of the transaction; to obtain\n"
"details about shielded components of the transaction use `z_viewtransaction`.\n"
"\nArguments:\n"
"1. \"txid\" (string, required) The transaction id\n"
"2. \"includeWatchonly\" (bool, optional, default=false) Whether to include watchonly addresses in balance calculation and details[]\n"
"2. includeWatchonly (bool, optional, default=false) Whether to include watchonly addresses in balance calculation and details[]\n"
"3. verbose (bool, optional, default=false) Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction). Currently, only the default value is supported.\n"
"4. " + asOfHeightMessage(false) +
"\nResult:\n"
"{\n"
" \"status\" : \"mined|waiting|expiringsoon|expired\", (string) The transaction status, can be 'mined', 'waiting', 'expiringsoon' or 'expired'\n"
@ -1752,7 +1799,8 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
" ],\n"
" \"hex\" : \"data\" (string) Raw data for transaction\n"
"}\n"
"\nBitcoin compatibility:\n"
"Compatible up to three arguments, but can only use the default value for `verbose`."
"\nExamples:\n"
+ HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
+ HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true")
@ -1769,12 +1817,18 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
if(params[1].get_bool())
filter = filter | ISMINE_WATCH_ONLY;
if (params.size() > 2 && params[2].get_bool() != false) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "verbose must be set to false");
}
auto asOfHeight = parseAsOfHeight(params, 3);
UniValue entry(UniValue::VOBJ);
if (!pwalletMain->mapWallet.count(hash))
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
const CWalletTx& wtx = pwalletMain->mapWallet[hash];
CAmount nCredit = wtx.GetCredit(filter);
CAmount nCredit = wtx.GetCredit(asOfHeight, filter);
CAmount nDebit = wtx.GetDebit(filter);
CAmount nNet = nCredit - nDebit;
CAmount nFee = (wtx.IsFromMe(filter) ? wtx.GetValueOut() - nDebit : 0);
@ -1785,10 +1839,10 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
if (wtx.IsFromMe(filter))
entry.pushKV("fee", ValueFromAmount(nFee));
WalletTxToJSON(wtx, entry);
WalletTxToJSON(wtx, entry, asOfHeight);
UniValue details(UniValue::VARR);
ListTransactions(wtx, 0, false, details, filter);
ListTransactions(wtx, 0, false, details, filter, asOfHeight);
entry.pushKV("details", details);
string strHex = EncodeHexTx(static_cast<CTransaction>(wtx));
@ -2303,25 +2357,29 @@ UniValue settxfee(const UniValue& params, bool fHelp)
return true;
}
CAmount getBalanceZaddr(std::optional<libzcash::PaymentAddress> address, int minDepth = 1, int maxDepth = INT_MAX, bool ignoreUnspendable=true);
CAmount getBalanceZaddr(std::optional<libzcash::PaymentAddress> address, const std::optional<int>& asOfHeight, int minDepth = 1, int maxDepth = INT_MAX, bool ignoreUnspendable=true);
UniValue getwalletinfo(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() != 0)
if (fHelp || params.size() > 1)
throw runtime_error(
"getwalletinfo\n"
"getwalletinfo ( asOfHeight )\n"
"Returns wallet state information.\n"
"\nArguments:\n"
"1. " + asOfHeightMessage(false) +
"\nResult:\n"
"{\n"
" \"walletversion\": xxxxx, (numeric) the wallet version\n"
" \"balance\": xxxxxxx, (numeric) the total confirmed transparent balance of the wallet in " + CURRENCY_UNIT + "\n"
" \"unconfirmed_balance\": xxx, (numeric) the total unconfirmed transparent balance of the wallet in " + CURRENCY_UNIT + "\n"
" \"unconfirmed_balance\": xxx, (numeric, optional) the total unconfirmed transparent balance of the wallet in " + CURRENCY_UNIT + ".\n"
" Not included if `asOfHeight` is specified.\n"
" \"immature_balance\": xxxxxx, (numeric) the total immature transparent balance of the wallet in " + CURRENCY_UNIT + "\n"
" \"shielded_balance\": xxxxxxx, (numeric) the total confirmed shielded balance of the wallet in " + CURRENCY_UNIT + "\n"
" \"shielded_unconfirmed_balance\": xxx, (numeric) the total unconfirmed shielded balance of the wallet in " + CURRENCY_UNIT + "\n"
" \"shielded_unconfirmed_balance\": xxx, (numeric, optional) the total unconfirmed shielded balance of the wallet in " + CURRENCY_UNIT + ".\n"
" Not included if `asOfHeight` is specified.\n"
" \"txcount\": xxxxxxx, (numeric) the total number of transactions in the wallet\n"
" \"keypoololdest\": xxxxxx, (numeric) the timestamp (seconds since GMT epoch) of the oldest pre-generated key in the key pool\n"
" \"keypoolsize\": xxxx, (numeric) how many new keys are pre-generated\n"
@ -2337,15 +2395,21 @@ UniValue getwalletinfo(const UniValue& params, bool fHelp)
+ HelpExampleRpc("getwalletinfo", "")
);
auto asOfHeight = parseAsOfHeight(params, 0);
LOCK2(cs_main, pwalletMain->cs_wallet);
UniValue obj(UniValue::VOBJ);
obj.pushKV("walletversion", pwalletMain->GetVersion());
obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance()));
obj.pushKV("unconfirmed_balance", ValueFromAmount(pwalletMain->GetUnconfirmedBalance()));
obj.pushKV("immature_balance", ValueFromAmount(pwalletMain->GetImmatureBalance()));
obj.pushKV("shielded_balance", FormatMoney(getBalanceZaddr(std::nullopt, 1, INT_MAX)));
obj.pushKV("shielded_unconfirmed_balance", FormatMoney(getBalanceZaddr(std::nullopt, 0, 0)));
obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance(asOfHeight)));
if (!asOfHeight.has_value()) {
obj.pushKV("unconfirmed_balance", ValueFromAmount(pwalletMain->GetUnconfirmedTransparentBalance()));
}
obj.pushKV("immature_balance", ValueFromAmount(pwalletMain->GetImmatureBalance(asOfHeight)));
obj.pushKV("shielded_balance", FormatMoney(getBalanceZaddr(std::nullopt, asOfHeight, 1, INT_MAX)));
if (!asOfHeight.has_value()) {
obj.pushKV("shielded_unconfirmed_balance", FormatMoney(getBalanceZaddr(std::nullopt, asOfHeight, 0, 0)));
}
obj.pushKV("txcount", (int)pwalletMain->mapWallet.size());
obj.pushKV("keypoololdest", pwalletMain->GetOldestKeyPoolTime());
obj.pushKV("keypoolsize", (int)pwalletMain->GetKeyPoolSize());
@ -2394,9 +2458,9 @@ UniValue listunspent(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 3)
if (fHelp || params.size() > 6)
throw runtime_error(
"listunspent ( minconf maxconf [\"address\",...] )\n"
"listunspent ( minconf maxconf [\"address\",...] includeUnsafe queryOptions asOfHeight )\n"
"\nReturns array of unspent transparent transaction outputs with between minconf and\n"
"maxconf (inclusive) confirmations. Use `z_listunspent` instead to see information\n"
"related to unspent shielded notes. Results may be optionally filtered to only include\n"
@ -2409,6 +2473,9 @@ UniValue listunspent(const UniValue& params, bool fHelp)
" \"address\" (string) Zcash address\n"
" ,...\n"
" ]\n"
"4. includeUnsafe (bool, optional, default=true) Include outputs that are not safe to spend. Currently, only the default value is supported.\n"
"5. queryOptions (object, optional, default={}) JSON with query options. Currently, only the default value is supported.\n"
"6. " + asOfHeightMessage(true) +
"\nResult\n"
"[ (array of json object)\n"
" {\n"
@ -2425,7 +2492,8 @@ UniValue listunspent(const UniValue& params, bool fHelp)
" }\n"
" ,...\n"
"]\n"
"\nBitcoin compatibility:\n"
"Compatible up to five arguments, but can only use the default value for `includeUnsafe` and `queryOptions`."
"\nExamples\n"
+ HelpExampleCli("listunspent", "")
+ HelpExampleCli("listunspent", "6 9999999 \"[\\\"t1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\",\\\"t1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"")
@ -2434,9 +2502,9 @@ UniValue listunspent(const UniValue& params, bool fHelp)
RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VARR));
int nMinDepth = 1;
if (params.size() > 0)
nMinDepth = params[0].get_int();
auto asOfHeight = parseAsOfHeight(params, 5);
int nMinDepth = parseMinconf(1, params, 0, asOfHeight);
int nMaxDepth = 9999999;
if (params.size() > 1)
@ -2458,10 +2526,28 @@ UniValue listunspent(const UniValue& params, bool fHelp)
}
}
if (params.size() > 3 && params[3].get_bool() != false) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "includeUnsafe must be set to false");
}
if (params.size() > 4 && !params[4].get_obj().empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "queryOptions must be set to {}");
}
LOCK2(cs_main, pwalletMain->cs_wallet);
UniValue results(UniValue::VARR);
vector<COutput> vecOutputs;
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->AvailableCoins(vecOutputs, false, NULL, true);
pwalletMain->AvailableCoins(
vecOutputs,
asOfHeight,
false, // fOnlyConfirmed
nullptr, // coinControl
true, // fIncludeZeroValue
true, // fIncludeCoinBase
false, // fOnlySpendable
nMinDepth,
destinations);
for (const COutput& out : vecOutputs) {
if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth)
continue;
@ -2470,9 +2556,6 @@ UniValue listunspent(const UniValue& params, bool fHelp)
const CScript& scriptPubKey = out.tx->vout[out.i].scriptPubKey;
bool fValidAddress = ExtractDestination(scriptPubKey, address);
if (destinations.size() && (!fValidAddress || !destinations.count(address)))
continue;
UniValue entry(UniValue::VOBJ);
entry.pushKV("txid", out.tx->GetHash().GetHex());
entry.pushKV("vout", out.i);
@ -2506,9 +2589,9 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 4)
if (fHelp || params.size() > 5)
throw runtime_error(
"z_listunspent ( minconf maxconf includeWatchonly [\"zaddr\",...] )\n"
"z_listunspent ( minconf maxconf includeWatchonly [\"zaddr\",...] asOfHeight )\n"
"\nReturns an array of unspent shielded notes with between minconf and maxconf (inclusive)\n"
"confirmations. Results may be optionally filtered to only include notes sent to specified\n"
"addresses.\n"
@ -2523,6 +2606,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
" \"address\" (string) Sprout, Sapling, or Unified address\n"
" ,...\n"
" ]\n"
"5. " + asOfHeightMessage(true) +
"\nResult (output indices for only one value pool will be present):\n"
"[ (array of json object)\n"
" {\n"
@ -2550,13 +2634,9 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VBOOL)(UniValue::VARR));
int nMinDepth = 1;
if (params.size() > 0) {
nMinDepth = params[0].get_int();
}
if (nMinDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
auto asOfHeight = parseAsOfHeight(params, 4);
int nMinDepth = parseMinconf(1, params, 0, asOfHeight);
int nMaxDepth = 9999999;
if (params.size() > 1) {
@ -2626,7 +2706,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, asOfHeight, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false);
for (auto & entry : sproutEntries) {
UniValue obj(UniValue::VOBJ);
@ -3759,13 +3839,13 @@ UniValue z_listunifiedreceivers(const UniValue& params, bool fHelp)
return result;
}
CAmount getBalanceTaddr(const std::optional<CTxDestination>& taddr, int minDepth=1, bool ignoreUnspendable=true) {
CAmount getBalanceTaddr(const std::optional<CTxDestination>& taddr, const std::optional<int>& asOfHeight, int minDepth=1, bool ignoreUnspendable=true) {
vector<COutput> vecOutputs;
CAmount balance = 0;
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->AvailableCoins(vecOutputs, false, NULL, true);
pwalletMain->AvailableCoins(vecOutputs, asOfHeight, false, NULL, true);
for (const COutput& out : vecOutputs) {
if (out.nDepth < minDepth) {
continue;
@ -3792,7 +3872,7 @@ CAmount getBalanceTaddr(const std::optional<CTxDestination>& taddr, int minDepth
return balance;
}
CAmount getBalanceZaddr(std::optional<libzcash::PaymentAddress> address, int minDepth, int maxDepth, bool ignoreUnspendable) {
CAmount getBalanceZaddr(std::optional<libzcash::PaymentAddress> address, const std::optional<int>& asOfHeight, int minDepth, int maxDepth, bool ignoreUnspendable) {
CAmount balance = 0;
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
@ -3804,7 +3884,7 @@ CAmount getBalanceZaddr(std::optional<libzcash::PaymentAddress> address, int min
noteFilter = NoteFilter::ForPaymentAddresses(std::vector({address.value()}));
}
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, minDepth, maxDepth, true, ignoreUnspendable);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, asOfHeight, minDepth, maxDepth, true, ignoreUnspendable);
for (auto & entry : sproutEntries) {
balance += CAmount(entry.note.value());
}
@ -3840,13 +3920,14 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size()==0 || params.size() >2)
if (fHelp || params.size() == 0 || params.size() > 3)
throw runtime_error(
"z_listreceivedbyaddress \"address\" ( minconf )\n"
"z_listreceivedbyaddress \"address\" ( minconf asOfHeight )\n"
"\nReturn a list of amounts received by a zaddr belonging to the node's wallet.\n"
"\nArguments:\n"
"1. \"address\" (string) The shielded address.\n"
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
"3. " + asOfHeightMessage(true) +
"\nResult (output indices for only one value pool will be present):\n"
"[\n"
" {\n"
@ -3873,13 +3954,10 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
int nMinDepth = 1;
if (params.size() > 1) {
nMinDepth = params[1].get_int();
}
if (nMinDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
auto asOfHeight = parseAsOfHeight(params, 2);
int nMinDepth = parseMinconf(1, params, 1, asOfHeight);
UniValue result(UniValue::VARR);
// Check that the from address is valid.
@ -3925,7 +4003,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
std::vector<OrchardNoteMetadata> orchardEntries;
auto noteFilter = NoteFilter::ForPaymentAddresses(std::vector({decoded.value()}));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, INT_MAX, false, false);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, asOfHeight, nMinDepth, INT_MAX, false, false);
auto push_transparent_result = [&](const CTxDestination& dest) -> void {
const CScript scriptPubKey{GetScriptForDestination(dest)};
@ -3933,7 +4011,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
if (!CheckFinalTx(wtx))
continue;
int nDepth = wtx.GetDepthInMainChain();
int nDepth = wtx.GetDepthInMainChain(asOfHeight);
if (nDepth < nMinDepth) continue;
for (size_t i = 0; i < wtx.vout.size(); ++i) {
const CTxOut& txout{wtx.vout[i]};
@ -4115,13 +4193,7 @@ UniValue z_getbalance(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
int nMinDepth = 1;
if (params.size() > 1) {
nMinDepth = params[1].get_int();
}
if (nMinDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
int nMinDepth = parseMinconf(1, params, 1, std::nullopt);
KeyIO keyIO(Params());
// Check that the from address is valid.
@ -4138,16 +4210,16 @@ UniValue z_getbalance(const UniValue& params, bool fHelp)
CAmount nBalance = 0;
std::visit(match {
[&](const CKeyID& addr) {
nBalance = getBalanceTaddr(addr, nMinDepth, false);
nBalance = getBalanceTaddr(addr, std::nullopt, nMinDepth, false);
},
[&](const CScriptID& addr) {
nBalance = getBalanceTaddr(addr, nMinDepth, false);
nBalance = getBalanceTaddr(addr, std::nullopt, nMinDepth, false);
},
[&](const libzcash::SproutPaymentAddress& addr) {
nBalance = getBalanceZaddr(addr, nMinDepth, INT_MAX, false);
nBalance = getBalanceZaddr(addr, std::nullopt, nMinDepth, INT_MAX, false);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
nBalance = getBalanceZaddr(addr, nMinDepth, INT_MAX, false);
nBalance = getBalanceZaddr(addr, std::nullopt, nMinDepth, INT_MAX, false);
},
[&](const libzcash::UnifiedAddress& addr) {
auto selector = pwalletMain->ZTXOSelectorForAddress(addr, true, false);
@ -4156,7 +4228,7 @@ UniValue z_getbalance(const UniValue& params, bool fHelp)
RPC_INVALID_ADDRESS_OR_KEY,
"Unified address does not correspond to an account in the wallet");
}
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, nMinDepth);
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, nMinDepth, std::nullopt);
for (const auto& t : spendableInputs.utxos) {
nBalance += t.Value();
@ -4183,15 +4255,16 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 1 || params.size() > 2)
if (fHelp || params.size() < 1 || params.size() > 3)
throw runtime_error(
"z_getbalanceforviewingkey \"fvk\" ( minconf )\n"
"z_getbalanceforviewingkey \"fvk\" ( minconf asOfHeight )\n"
"\nReturns the balance viewable by a full viewing key known to the node's wallet"
"\nfor each value pool. Sprout viewing keys may be used only if the wallet controls"
"\nthe corresponding spending key."
"\nArguments:\n"
"1. \"fvk\" (string) The selected full viewing key.\n"
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
"3. " + asOfHeightMessage(true) +
"\nResult:\n"
"{\n"
" \"pools\": {\n"
@ -4228,13 +4301,9 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
}
auto fvk = decoded.value();
int minconf = 1;
if (params.size() > 1) {
minconf = params[1].get_int();
if (minconf < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
}
auto asOfHeight = parseAsOfHeight(params, 2);
int minconf = parseMinconf(1, params, 1, asOfHeight);
LOCK2(cs_main, pwalletMain->cs_wallet);
@ -4251,7 +4320,7 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
"Error: the wallet does not recognize the specified viewing key.");
}
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf);
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf, asOfHeight);
CAmount transparentBalance = 0;
CAmount sproutBalance = 0;
@ -4295,13 +4364,14 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 1 || params.size() > 2)
if (fHelp || params.size() < 1 || params.size() > 3)
throw runtime_error(
"z_getbalanceforaccount account ( minconf )\n"
"z_getbalanceforaccount account ( minconf asOfHeight )\n"
"\nReturns the account's spendable balance for each value pool (\"transparent\", \"sapling\", and \"orchard\")."
"\nArguments:\n"
"1. account (numeric) The account number.\n"
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
"3. " + asOfHeightMessage(true) +
"\nResult:\n"
"{\n"
" \"pools\": {\n"
@ -4334,13 +4404,9 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
}
libzcash::AccountId account = accountInt;
int minconf = 1;
if (params.size() > 1) {
minconf = params[1].get_int();
if (minconf < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
}
auto asOfHeight = parseAsOfHeight(params, 2);
int minconf = parseMinconf(1, params, 1, asOfHeight);
LOCK2(cs_main, pwalletMain->cs_wallet);
@ -4352,7 +4418,7 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
tfm::format("Error: account %d has not been generated by z_getnewaccount.", account));
}
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf);
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf, asOfHeight);
// Accounts never contain Sprout notes.
assert(spendableInputs.sproutNoteEntries.empty());
@ -4428,13 +4494,7 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
int nMinDepth = 1;
if (params.size() > 0) {
nMinDepth = params[0].get_int();
}
if (nMinDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
int nMinDepth = parseMinconf(1, params, 0, std::nullopt);
bool fIncludeWatchonly = false;
if (params.size() > 1) {
@ -4445,8 +4505,8 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp)
// but they don't because wtx.GetAmounts() does not handle tx where there are no outputs
// pwalletMain->GetBalance() does not accept min depth parameter
// so we use our own method to get balance of utxos.
CAmount nBalance = getBalanceTaddr(std::nullopt, nMinDepth, !fIncludeWatchonly);
CAmount nPrivateBalance = getBalanceZaddr(std::nullopt, nMinDepth, INT_MAX, !fIncludeWatchonly);
CAmount nBalance = getBalanceTaddr(std::nullopt, std::nullopt, nMinDepth, !fIncludeWatchonly);
CAmount nPrivateBalance = getBalanceZaddr(std::nullopt, std::nullopt, nMinDepth, INT_MAX, !fIncludeWatchonly);
CAmount nTotalBalance = nBalance + nPrivateBalance;
UniValue result(UniValue::VOBJ);
result.pushKV("transparent", FormatMoney(nBalance));
@ -5258,13 +5318,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
}
// Minimum confirmations
int nMinDepth = DEFAULT_NOTE_CONFIRMATIONS;
if (params.size() > 2) {
nMinDepth = params[2].get_int();
}
if (nMinDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
int nMinDepth = parseMinconf(DEFAULT_NOTE_CONFIRMATIONS, params, 2, std::nullopt);
// Fee in Zatoshis, not currency format)
CAmount nFee = DEFAULT_FEE;
@ -5354,13 +5408,15 @@ UniValue z_setmigration(const UniValue& params, bool fHelp) {
UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) {
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() != 0)
if (fHelp || params.size() > 1)
throw runtime_error(
"z_getmigrationstatus\n"
"z_getmigrationstatus ( asOfHeight )\n"
"Returns information about the status of the Sprout to Sapling migration.\n"
"Note: A transaction is defined as finalized if it has at least ten confirmations.\n"
"Also, it is possible that manually created transactions involving this wallet\n"
"will be included in the result.\n"
"\nArguments:\n"
"1. " + asOfHeightMessage(false) +
"\nResult:\n"
"{\n"
" \"enabled\": true|false, (boolean) Whether or not migration is enabled\n"
@ -5374,6 +5430,9 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) {
"}\n"
);
LOCK2(cs_main, pwalletMain->cs_wallet);
auto asOfHeight = parseAsOfHeight(params, 0);
UniValue migrationStatus(UniValue::VOBJ);
migrationStatus.pushKV("enabled", pwalletMain->fSaplingMigrationEnabled);
// The "destination_address" field MAY be omitted if the "-migrationdestaddress"
@ -5391,7 +5450,7 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) {
std::vector<OrchardNoteMetadata> orchardEntries;
// Here we are looking for any and all Sprout notes for which we have the spending key, including those
// which are locked and/or only exist in the mempool, as they should be included in the unmigrated amount.
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, true, true, false);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, asOfHeight, 0, INT_MAX, true, true, false);
CAmount unmigratedAmount = 0;
for (const auto& sproutEntry : sproutEntries) {
unmigratedAmount += sproutEntry.note.value();
@ -5426,7 +5485,7 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) {
migrationTxids.push_back(txPair.first.ToString());
// A transaction is "finalized" iff it has at least 10 confirmations.
// TODO: subject to change, if the recommended number of confirmations changes.
if (tx.GetDepthInMainChain() >= 10) {
if (tx.GetDepthInMainChain(asOfHeight) >= 10) {
finalizedMigratedAmount -= tx.GetValueBalanceSapling();
++numFinalizedMigrationTxs;
} else {
@ -5603,7 +5662,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp)
// Get available utxos
vector<COutput> vecOutputs;
pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, true);
pwalletMain->AvailableCoins(vecOutputs, std::nullopt, true, NULL, false, true);
// Find unspent coinbase utxos and update estimated size
for (const COutput& out : vecOutputs) {
@ -5965,7 +6024,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
if (useAnyUTXO || taddrs.size() > 0) {
// Get available utxos
vector<COutput> vecOutputs;
pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, false);
pwalletMain->AvailableCoins(vecOutputs, std::nullopt, true, NULL, false, false);
// Find unspent utxos and update estimated size
for (const COutput& out : vecOutputs) {
@ -6016,7 +6075,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
useAnySprout || useAnySapling ?
std::nullopt :
std::optional(NoteFilter::ForPaymentAddresses(zaddrs));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nAnchorConfirmations);
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, std::nullopt, nAnchorConfirmations);
// If Sapling is not active, do not allow sending from a sapling addresses.
if (!saplingActive && saplingEntries.size() > 0) {
@ -6241,12 +6300,13 @@ UniValue z_getnotescount(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 1)
if (fHelp || params.size() > 2)
throw runtime_error(
"z_getnotescount\n"
"z_getnotescount ( minconf asOfHeight )\n"
"\nReturns the number of notes available in the wallet for each shielded value pool.\n"
"\nArguments:\n"
"1. minconf (numeric, optional, default=1) Only include notes in transactions confirmed at least this many times.\n"
"\nReturns the number of notes available in the wallet for each shielded value pool.\n"
"2. " + asOfHeightMessage(true) +
"\nResult:\n"
"{\n"
" \"sprout\" (numeric) the number of Sprout notes in the wallet\n"
@ -6260,15 +6320,15 @@ UniValue z_getnotescount(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
int nMinDepth = 1;
if (params.size() > 0)
nMinDepth = params[0].get_int();
auto asOfHeight = parseAsOfHeight(params, 1);
int nMinDepth = parseMinconf(1, params, 0, asOfHeight);
int sprout = 0;
int sapling = 0;
int orchard = 0;
for (auto& wtx : pwalletMain->mapWallet) {
if (wtx.second.GetDepthInMainChain() >= nMinDepth) {
if (wtx.second.GetDepthInMainChain(asOfHeight) >= nMinDepth) {
sprout += wtx.second.mapSproutNoteData.size();
sapling += wtx.second.mapSaplingNoteData.size();
orchard += wtx.second.orchardTxMeta.GetMyActionIVKs().size();

View File

@ -876,7 +876,7 @@ std::pair<PaymentAddress, RecipientType> CWallet::GetPaymentAddressForRecipient(
auto wtxPtr = mapWallet.find(txid);
if (wtxPtr != mapWallet.end()) {
const CBlockIndex* pTxIndex{nullptr};
if (wtxPtr->second.GetDepthInMainChain(pTxIndex) > 0) {
if (wtxPtr->second.GetDepthInMainChain(pTxIndex, std::nullopt) > 0) {
nHeight = pTxIndex->nHeight;
}
}
@ -2206,7 +2206,8 @@ std::optional<RecipientAddress> CWallet::GenerateChangeAddressForAccount(
SpendableInputs CWallet::FindSpendableInputs(
ZTXOSelector selector,
bool allowTransparentCoinbase,
uint32_t minDepth) const {
uint32_t minDepth,
const std::optional<int>& asOfHeight) const {
AssertLockHeld(cs_main);
AssertLockHeld(cs_wallet);
@ -2220,7 +2221,7 @@ SpendableInputs CWallet::FindSpendableInputs(
SpendableInputs unspent;
for (auto const& [wtxid, wtx] : mapWallet) {
bool isCoinbase = wtx.IsCoinBase();
auto nDepth = wtx.GetDepthInMainChain();
auto nDepth = wtx.GetDepthInMainChain(asOfHeight);
// Filter the transactions before checking for coins
if (!CheckFinalTx(wtx)) continue;
@ -2228,14 +2229,14 @@ SpendableInputs CWallet::FindSpendableInputs(
if (selectTransparent &&
// skip transparent utxo selection if coinbase spend restrictions are not met
(!isCoinbase || (allowTransparentCoinbase && wtx.GetBlocksToMaturity() <= 0))) {
(!isCoinbase || (allowTransparentCoinbase && wtx.GetBlocksToMaturity(asOfHeight) <= 0))) {
for (int i = 0; i < wtx.vout.size(); i++) {
const auto& output = wtx.vout[i];
isminetype mine = IsMine(output);
// skip spent utxos
if (IsSpent(wtxid, i)) continue;
if (IsSpent(wtxid, i, asOfHeight)) continue;
// skip utxos that don't belong to the wallet
if (mine == ISMINE_NO) continue;
// skip utxos that for which we don't have the spending keys, if
@ -2263,7 +2264,7 @@ SpendableInputs CWallet::FindSpendableInputs(
SproutPaymentAddress pa = nd.address;
// skip note which has been spent
if (nd.nullifier.has_value() && IsSproutSpent(nd.nullifier.value())) continue;
if (nd.nullifier.has_value() && IsSproutSpent(nd.nullifier.value(), asOfHeight)) continue;
// skip notes which don't match the source
if (!this->SelectorMatchesAddress(selector, pa)) continue;
// skip notes for which we don't have the spending key
@ -2297,7 +2298,7 @@ SpendableInputs CWallet::FindSpendableInputs(
(unsigned char) j);
unspent.sproutNoteEntries.push_back(SproutNoteEntry {
jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain() });
jsop, pa, plaintext.note(pa), plaintext.memo(), nDepth });
} catch (const note_decryption_failed &err) {
// Couldn't decrypt with this spending key
@ -2327,7 +2328,7 @@ SpendableInputs CWallet::FindSpendableInputs(
auto pa = maybe_pa.value();
// skip notes which have been spent
if (nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value())) continue;
if (nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value(), asOfHeight)) continue;
// skip notes which do not match the source
if (!this->SelectorMatchesAddress(selector, pa)) continue;
// skip notes if we don't have the spending key
@ -2337,7 +2338,7 @@ SpendableInputs CWallet::FindSpendableInputs(
auto note = notePt.note(nd.ivk).value();
unspent.saplingNoteEntries.push_back(SaplingNoteEntry {
op, pa, note, notePt.memo(), wtx.GetDepthInMainChain() });
op, pa, note, notePt.memo(), nDepth });
}
}
}
@ -2386,7 +2387,7 @@ SpendableInputs CWallet::FindSpendableInputs(
orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true);
for (auto& noteMeta : incomingNotes) {
if (IsOrchardSpent(noteMeta.GetOutPoint())) {
if (IsOrchardSpent(noteMeta.GetOutPoint(), asOfHeight)) {
continue;
}
@ -2396,7 +2397,7 @@ SpendableInputs CWallet::FindSpendableInputs(
// the transaction does not exist in the main wallet.
assert(mit != mapWallet.end());
int confirmations = mit->second.GetDepthInMainChain();
int confirmations = mit->second.GetDepthInMainChain(asOfHeight);
if (confirmations < 0) continue;
if (confirmations >= minDepth) {
noteMeta.SetConfirmations(confirmations);
@ -2413,7 +2414,7 @@ SpendableInputs CWallet::FindSpendableInputs(
* Outpoint is spent if any non-conflicted transaction
* spends it:
*/
bool CWallet::IsSpent(const uint256& hash, unsigned int n) const
bool CWallet::IsSpent(const uint256& hash, unsigned int n, const std::optional<int>& asOfHeight) const
{
const COutPoint outpoint(hash, n);
pair<TxSpends::const_iterator, TxSpends::const_iterator> range;
@ -2423,9 +2424,10 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const
{
const uint256& wtxid = it->second;
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid);
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0)
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) {
return true; // Spent
}
}
return false;
}
@ -2433,7 +2435,7 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const
* Note is spent if any non-conflicted transaction
* spends it:
*/
bool CWallet::IsSproutSpent(const uint256& nullifier) const {
bool CWallet::IsSproutSpent(const uint256& nullifier, const std::optional<int>& asOfHeight) const {
LOCK(cs_main);
pair<TxNullifiers::const_iterator, TxNullifiers::const_iterator> range;
range = mapTxSproutNullifiers.equal_range(nullifier);
@ -2441,14 +2443,14 @@ bool CWallet::IsSproutSpent(const uint256& nullifier) const {
for (TxNullifiers::const_iterator it = range.first; it != range.second; ++it) {
const uint256& wtxid = it->second;
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid);
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) {
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) {
return true; // Spent
}
}
return false;
}
bool CWallet::IsSaplingSpent(const uint256& nullifier) const {
bool CWallet::IsSaplingSpent(const uint256& nullifier, const std::optional<int>& asOfHeight) const {
LOCK(cs_main);
pair<TxNullifiers::const_iterator, TxNullifiers::const_iterator> range;
range = mapTxSaplingNullifiers.equal_range(nullifier);
@ -2456,17 +2458,17 @@ bool CWallet::IsSaplingSpent(const uint256& nullifier) const {
for (TxNullifiers::const_iterator it = range.first; it != range.second; ++it) {
const uint256& wtxid = it->second;
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid);
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) {
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) {
return true; // Spent
}
}
return false;
}
bool CWallet::IsOrchardSpent(const OrchardOutPoint& outpoint) const {
bool CWallet::IsOrchardSpent(const OrchardOutPoint& outpoint, const std::optional<int>& asOfHeight) const {
for (const auto& txid : orchardWallet.GetPotentialSpends(outpoint)) {
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(txid);
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) {
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) {
return true; // Spent
}
}
@ -4834,7 +4836,7 @@ void CWallet::ReacceptWalletTransactions()
CWalletTx& wtx = item.second;
assert(wtx.GetHash() == wtxid);
int nDepth = wtx.GetDepthInMainChain();
int nDepth = wtx.GetDepthInMainChain(std::nullopt);
if (!wtx.IsCoinBase() && nDepth < 0) {
mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx));
@ -4854,7 +4856,7 @@ bool CWalletTx::RelayWalletTransaction()
assert(pwallet->GetBroadcastTransactions());
if (!IsCoinBase())
{
if (GetDepthInMainChain() == 0) {
if (GetDepthInMainChain(std::nullopt) == 0) {
LogPrintf("Relaying wtx %s\n", GetHash().ToString());
RelayTransaction((CTransaction)*this);
return true;
@ -4906,10 +4908,10 @@ CAmount CWalletTx::GetDebit(const isminefilter& filter) const
return debit;
}
CAmount CWalletTx::GetCredit(const isminefilter& filter) const
CAmount CWalletTx::GetCredit(const std::optional<int>& asOfHeight, const isminefilter& filter) const
{
// Must wait until coinbase is safely deep enough in the chain before valuing it
if (IsCoinBase() && GetBlocksToMaturity() > 0)
if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0)
return 0;
int64_t credit = 0;
@ -4939,9 +4941,9 @@ CAmount CWalletTx::GetCredit(const isminefilter& filter) const
return credit;
}
CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const
CAmount CWalletTx::GetImmatureCredit(const std::optional<int>& asOfHeight, bool fUseCache) const
{
if (IsCoinBase() && GetBlocksToMaturity() > 0 && IsInMainChain())
if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0 && IsInMainChain(asOfHeight))
{
if (fUseCache && fImmatureCreditCached)
return nImmatureCreditCached;
@ -4953,13 +4955,13 @@ CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const
return 0;
}
CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const
CAmount CWalletTx::GetAvailableCredit(const std::optional<int>& asOfHeight, bool fUseCache, const isminefilter& filter) const
{
if (pwallet == nullptr)
return 0;
// Must wait until coinbase is safely deep enough in the chain before valuing it
if (IsCoinBase() && GetBlocksToMaturity() > 0)
if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0)
return 0;
CAmount* cache = nullptr;
@ -4981,7 +4983,7 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter
uint256 hashTx = GetHash();
for (unsigned int i = 0; i < vout.size(); i++)
{
if (!pwallet->IsSpent(hashTx, i))
if (!pwallet->IsSpent(hashTx, i, asOfHeight))
{
const CTxOut &txout = vout[i];
nCredit += pwallet->GetCredit(txout, filter);
@ -4997,9 +4999,9 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter
return nCredit;
}
CAmount CWalletTx::GetImmatureWatchOnlyCredit(const bool fUseCache) const
CAmount CWalletTx::GetImmatureWatchOnlyCredit(const std::optional<int>& asOfHeight, const bool fUseCache) const
{
if (IsCoinBase() && GetBlocksToMaturity() > 0 && IsInMainChain())
if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0 && IsInMainChain(asOfHeight))
{
if (fUseCache && fImmatureWatchCreditCached)
return nImmatureWatchCreditCached;
@ -5040,14 +5042,17 @@ bool CWalletTx::IsFromMe(const isminefilter& filter) const
return false;
}
bool CWalletTx::IsTrusted() const
bool CWalletTx::IsTrusted(const std::optional<int>& asOfHeight) const
{
// Quick answer in most cases
if (!CheckFinalTx(*this))
return false;
int nDepth = GetDepthInMainChain();
int nDepth = GetDepthInMainChain(asOfHeight);
if (nDepth >= 1)
return true;
if (asOfHeight.has_value() && nDepth == 0)
// dont trust mempool tx if using `asOfHeight`
return false;
if (nDepth < 0)
return false;
if (!bSpendZeroConfChange || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit
@ -5125,7 +5130,7 @@ void CWallet::ResendWalletTransactions(int64_t nBestBlockTime)
*/
CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) const
CAmount CWallet::GetBalance(const std::optional<int>& asOfHeight, const isminefilter& filter, const int min_depth) const
{
CAmount nTotal = 0;
{
@ -5133,8 +5138,8 @@ CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) con
for (const auto& entry : mapWallet)
{
const CWalletTx* pcoin = &entry.second;
if (pcoin->IsTrusted() && pcoin->GetDepthInMainChain() >= min_depth) {
nTotal += pcoin->GetAvailableCredit(true, filter);
if (pcoin->IsTrusted(asOfHeight) && pcoin->GetDepthInMainChain(asOfHeight) >= min_depth) {
nTotal += pcoin->GetAvailableCredit(asOfHeight, true, filter);
}
}
}
@ -5142,7 +5147,7 @@ CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) con
return nTotal;
}
CAmount CWallet::GetUnconfirmedBalance() const
CAmount CWallet::GetUnconfirmedTransparentBalance() const
{
CAmount nTotal = 0;
{
@ -5150,14 +5155,14 @@ CAmount CWallet::GetUnconfirmedBalance() const
for (const auto& entry : mapWallet)
{
const CWalletTx* pcoin = &entry.second;
if (!CheckFinalTx(*pcoin) || (!pcoin->IsTrusted() && pcoin->GetDepthInMainChain() == 0))
nTotal += pcoin->GetAvailableCredit();
if (!CheckFinalTx(*pcoin) || (!pcoin->IsTrusted(std::nullopt) && pcoin->GetDepthInMainChain(std::nullopt) == 0))
nTotal += pcoin->GetAvailableCredit(std::nullopt);
}
}
return nTotal;
}
CAmount CWallet::GetImmatureBalance() const
CAmount CWallet::GetImmatureBalance(const std::optional<int>& asOfHeight) const
{
CAmount nTotal = 0;
{
@ -5165,36 +5170,7 @@ CAmount CWallet::GetImmatureBalance() const
for (const auto& entry : mapWallet)
{
const CWalletTx* pcoin = &entry.second;
nTotal += pcoin->GetImmatureCredit();
}
}
return nTotal;
}
CAmount CWallet::GetUnconfirmedWatchOnlyBalance() const
{
CAmount nTotal = 0;
{
LOCK2(cs_main, cs_wallet);
for (const auto& entry : mapWallet)
{
const CWalletTx* pcoin = &entry.second;
if (!CheckFinalTx(*pcoin) || (!pcoin->IsTrusted() && pcoin->GetDepthInMainChain() == 0))
nTotal += pcoin->GetAvailableCredit(true, ISMINE_WATCH_ONLY);
}
}
return nTotal;
}
CAmount CWallet::GetImmatureWatchOnlyBalance() const
{
CAmount nTotal = 0;
{
LOCK2(cs_main, cs_wallet);
for (const auto& entry : mapWallet)
{
const CWalletTx* pcoin = &entry.second;
nTotal += pcoin->GetImmatureWatchOnlyCredit();
nTotal += pcoin->GetImmatureCredit(asOfHeight);
}
}
return nTotal;
@ -5213,8 +5189,8 @@ CAmount CWallet::GetLegacyBalance(const isminefilter& filter, int minDepth) cons
CAmount balance = 0;
for (const auto& entry : mapWallet) {
const CWalletTx& wtx = entry.second;
const int depth = wtx.GetDepthInMainChain();
if (depth < 0 || !CheckFinalTx(wtx) || wtx.GetBlocksToMaturity() > 0) {
const int depth = wtx.GetDepthInMainChain(std::nullopt);
if (depth < 0 || !CheckFinalTx(wtx) || wtx.GetBlocksToMaturity(std::nullopt) > 0) {
continue;
}
@ -5240,45 +5216,43 @@ CAmount CWallet::GetLegacyBalance(const isminefilter& filter, int minDepth) cons
}
void CWallet::AvailableCoins(vector<COutput>& vCoins,
const std::optional<int>& asOfHeight,
bool fOnlyConfirmed,
const CCoinControl *coinControl,
bool fIncludeZeroValue,
bool fIncludeCoinBase,
bool fOnlySpendable,
int nMinDepth,
std::set<CTxDestination>* onlyFilterByDests) const
const std::set<CTxDestination>& onlyFilterByDests) const
{
assert(nMinDepth >= 0);
assert(!asOfHeight.has_value() || nMinDepth > 0);
AssertLockHeld(cs_main);
AssertLockHeld(cs_wallet);
vCoins.clear();
{
for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
{
const uint256& wtxid = it->first;
const CWalletTx* pcoin = &(*it).second;
if (!CheckFinalTx(*pcoin))
for (const auto& [wtxid, pcoin] : mapWallet) {
if (!CheckFinalTx(pcoin))
continue;
if (fOnlyConfirmed && !pcoin->IsTrusted())
if (fOnlyConfirmed && !pcoin.IsTrusted(asOfHeight))
continue;
if (pcoin->IsCoinBase() && !fIncludeCoinBase)
if (pcoin.IsCoinBase() && !fIncludeCoinBase)
continue;
bool isCoinbase = pcoin->IsCoinBase();
if (isCoinbase && pcoin->GetBlocksToMaturity() > 0)
bool isCoinbase = pcoin.IsCoinBase();
if (isCoinbase && pcoin.GetBlocksToMaturity(asOfHeight) > 0)
continue;
int nDepth = pcoin->GetDepthInMainChain();
int nDepth = pcoin.GetDepthInMainChain(asOfHeight);
if (nDepth < nMinDepth)
continue;
for (unsigned int i = 0; i < pcoin->vout.size(); i++) {
const auto& output = pcoin->vout[i];
for (unsigned int i = 0; i < pcoin.vout.size(); i++) {
const auto& output = pcoin.vout[i];
isminetype mine = IsMine(output);
bool isSpendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) ||
@ -5288,17 +5262,17 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins,
continue;
// Filter by specific destinations if needed
if (onlyFilterByDests && !onlyFilterByDests->empty()) {
if (!onlyFilterByDests.empty()) {
CTxDestination address;
if (!ExtractDestination(output.scriptPubKey, address) || onlyFilterByDests->count(address) == 0) {
if (!ExtractDestination(output.scriptPubKey, address) || onlyFilterByDests.count(address) == 0) {
continue;
}
}
if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO &&
!IsLockedCoin((*it).first, i) && (pcoin->vout[i].nValue > 0 || fIncludeZeroValue) &&
(!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected((*it).first, i)))
vCoins.push_back(COutput(pcoin, i, nDepth, isSpendable, isCoinbase));
if (!(IsSpent(wtxid, i, asOfHeight)) && mine != ISMINE_NO &&
!IsLockedCoin(wtxid, i) && (pcoin.vout[i].nValue > 0 || fIncludeZeroValue) &&
(!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected(wtxid, i)))
vCoins.push_back(COutput(&pcoin, i, nDepth, isSpendable, isCoinbase));
}
}
}
@ -5455,8 +5429,8 @@ bool CWallet::SelectCoins(const CAmount& nTargetValue, set<pair<const CWalletTx*
{
// Output parameter fOnlyCoinbaseCoinsRet is set to true when the only available coins are coinbase utxos.
vector<COutput> vCoinsNoCoinbase, vCoinsWithCoinbase;
AvailableCoins(vCoinsNoCoinbase, true, coinControl, false, false);
AvailableCoins(vCoinsWithCoinbase, true, coinControl, false, true);
AvailableCoins(vCoinsNoCoinbase, std::nullopt, true, coinControl, false, false);
AvailableCoins(vCoinsWithCoinbase, std::nullopt, true, coinControl, false, true);
fOnlyCoinbaseCoinsRet = vCoinsNoCoinbase.size() == 0 && vCoinsWithCoinbase.size() > 0;
// If coinbase utxos can only be sent to zaddrs, exclude any coinbase utxos from coin selection.
@ -5725,7 +5699,7 @@ bool CWallet::CreateTransaction(const vector<CRecipient>& vecSend, CWalletTx& wt
//reflecting an assumption the user would accept a bit more delay for
//a chance at a free transaction.
//But mempool inputs might still be in the mempool, so their age stays 0
int age = pcoin.first->GetDepthInMainChain();
int age = pcoin.first->GetDepthInMainChain(std::nullopt);
if (age != 0)
age += 1;
dPriority += (double)nCredit * age;
@ -6220,7 +6194,7 @@ int64_t CWallet::GetOldestKeyPoolTime()
return keypool.nTime;
}
std::map<CTxDestination, CAmount> CWallet::GetAddressBalances()
std::map<CTxDestination, CAmount> CWallet::GetAddressBalances(const std::optional<int>& asOfHeight)
{
map<CTxDestination, CAmount> balances;
@ -6230,13 +6204,13 @@ std::map<CTxDestination, CAmount> CWallet::GetAddressBalances()
{
CWalletTx *pcoin = &walletEntry.second;
if (!CheckFinalTx(*pcoin) || !pcoin->IsTrusted())
if (!CheckFinalTx(*pcoin) || !pcoin->IsTrusted(asOfHeight))
continue;
if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity() > 0)
if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity(asOfHeight) > 0)
continue;
int nDepth = pcoin->GetDepthInMainChain();
int nDepth = pcoin->GetDepthInMainChain(asOfHeight);
if (nDepth < (pcoin->IsFromMe(ISMINE_ALL) ? 0 : 1))
continue;
@ -6248,7 +6222,7 @@ std::map<CTxDestination, CAmount> CWallet::GetAddressBalances()
if(!ExtractDestination(pcoin->vout[i].scriptPubKey, addr))
continue;
CAmount n = IsSpent(walletEntry.first, i) ? 0 : pcoin->vout[i].nValue;
CAmount n = IsSpent(walletEntry.first, i, asOfHeight) ? 0 : pcoin->vout[i].nValue;
if (!balances.count(addr))
balances[addr] = 0;
@ -6984,39 +6958,43 @@ void CMerkleTx::SetMerkleBranch(const CBlock& block)
}
}
int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const
int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet, const std::optional<int>& asOfHeight) const
{
if (hashBlock.IsNull() || nIndex == -1)
return 0;
AssertLockHeld(cs_main);
int effectiveChainHeight = min(chainActive.Height(), asOfHeight.value_or(chainActive.Height()));
// Find the block it claims to be in
BlockMap::iterator mi = mapBlockIndex.find(hashBlock);
if (mi == mapBlockIndex.end())
return 0;
CBlockIndex* pindex = (*mi).second;
if (!pindex || !chainActive.Contains(pindex))
if (!pindex ||
!chainActive.Contains(pindex) ||
pindex->nHeight > effectiveChainHeight) {
return 0;
}
pindexRet = pindex;
return chainActive.Height() - pindex->nHeight + 1;
return effectiveChainHeight - pindex->nHeight + 1;
}
int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet) const
int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet, const std::optional<int>& asOfHeight) const
{
AssertLockHeld(cs_main);
int nResult = GetDepthInMainChainINTERNAL(pindexRet);
if (nResult == 0 && !mempool.exists(GetHash()))
int nResult = GetDepthInMainChainINTERNAL(pindexRet, asOfHeight);
if (nResult == 0 && (asOfHeight.has_value() || !mempool.exists(GetHash())))
return -1; // Not in chain, not in mempool
return nResult;
}
int CMerkleTx::GetBlocksToMaturity() const
int CMerkleTx::GetBlocksToMaturity(const std::optional<int>& asOfHeight) const
{
if (!IsCoinBase())
return 0;
return max(0, (COINBASE_MATURITY+1) - GetDepthInMainChain());
return max(0, (COINBASE_MATURITY+1) - GetDepthInMainChain(asOfHeight));
}
@ -7098,6 +7076,7 @@ void CWallet::GetFilteredNotes(
std::vector<SaplingNoteEntry>& saplingEntriesRet,
std::vector<OrchardNoteMetadata>& orchardNotesRet,
const std::optional<NoteFilter>& noteFilter,
const std::optional<int>& asOfHeight,
int minDepth,
int maxDepth,
bool ignoreSpent,
@ -7116,8 +7095,8 @@ void CWallet::GetFilteredNotes(
// Filter the transactions before checking for notes
if (!CheckFinalTx(wtx) ||
wtx.GetDepthInMainChain() < minDepth ||
wtx.GetDepthInMainChain() > maxDepth) {
wtx.GetDepthInMainChain(asOfHeight) < minDepth ||
wtx.GetDepthInMainChain(asOfHeight) > maxDepth) {
continue;
}
@ -7137,7 +7116,7 @@ void CWallet::GetFilteredNotes(
}
// skip note which has been spent
if (ignoreSpent && nd.nullifier && IsSproutSpent(*nd.nullifier)) {
if (ignoreSpent && nd.nullifier && IsSproutSpent(*nd.nullifier, asOfHeight)) {
continue;
}
@ -7175,7 +7154,7 @@ void CWallet::GetFilteredNotes(
(unsigned char) j);
sproutEntriesRet.push_back(SproutNoteEntry {
jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain() });
jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain(asOfHeight) });
} catch (const note_decryption_failed &err) {
// Couldn't decrypt with this spending key
@ -7206,7 +7185,7 @@ void CWallet::GetFilteredNotes(
continue;
}
if (ignoreSpent && nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value())) {
if (ignoreSpent && nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value(), asOfHeight)) {
continue;
}
@ -7222,7 +7201,7 @@ void CWallet::GetFilteredNotes(
auto note = notePt.note(nd.ivk).value();
saplingEntriesRet.push_back(SaplingNoteEntry {
op, pa, note, notePt.memo(), wtx.GetDepthInMainChain() });
op, pa, note, notePt.memo(), wtx.GetDepthInMainChain(asOfHeight) });
}
}
@ -7248,13 +7227,13 @@ void CWallet::GetFilteredNotes(
}
for (auto& noteMeta : orchardNotes) {
if (ignoreSpent && IsOrchardSpent(noteMeta.GetOutPoint())) {
if (ignoreSpent && IsOrchardSpent(noteMeta.GetOutPoint(), asOfHeight)) {
continue;
}
auto wtx = GetWalletTx(noteMeta.GetOutPoint().hash);
if (wtx) {
auto confirmations = wtx->GetDepthInMainChain();
auto confirmations = wtx->GetDepthInMainChain(asOfHeight);
if (confirmations >= minDepth && confirmations <= maxDepth) {
noteMeta.SetConfirmations(confirmations);
orchardNotesRet.push_back(noteMeta);

View File

@ -399,7 +399,11 @@ struct SaplingNoteEntry
class CMerkleTx : public CTransaction
{
private:
int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const;
/**
* **NB**: Unlike `GetDepthInMainChain`, this returns 0 for any case where
* its not in the chain (including if its not in the mempool).
*/
int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet, const std::optional<int>& asOfHeight) const;
public:
uint256 hashBlock;
@ -438,13 +442,16 @@ public:
/**
* Return depth of transaction in blockchain:
* -1 : not in blockchain, and not in memory pool (conflicted transaction)
* 0 : in memory pool, waiting to be included in a block
* 0 : in memory pool, waiting to be included in a block (never returned if `asOfHeight` is set)
* >=1 : this many blocks deep in the main chain
*/
int GetDepthInMainChain(const CBlockIndex* &pindexRet) const;
int GetDepthInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChain(pindexRet); }
bool IsInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChainINTERNAL(pindexRet) > 0; }
int GetBlocksToMaturity() const;
int GetDepthInMainChain(const CBlockIndex* &pindexRet, const std::optional<int>& asOfHeight) const;
int GetDepthInMainChain(const std::optional<int>& asOfHeight) const {
const CBlockIndex *pindexRet;
return GetDepthInMainChain(pindexRet, asOfHeight);
}
bool IsInMainChain(const std::optional<int>& asOfHeight) const { return GetDepthInMainChain(asOfHeight) > 0; }
int GetBlocksToMaturity(const std::optional<int>& asOfHeight) const;
/** Pass this transaction to the mempool. Fails if absolute fee exceeds maxTxFee. */
bool AcceptToMemoryPool(CValidationState& state, bool fLimitFree=true, bool fRejectAbsurdFee=true);
};
@ -670,10 +677,10 @@ public:
//! filter decides which addresses will count towards the debit
CAmount GetDebit(const isminefilter& filter) const;
CAmount GetCredit(const isminefilter& filter) const;
CAmount GetImmatureCredit(bool fUseCache=true) const;
CAmount GetAvailableCredit(bool fUseCache=true, const isminefilter& filter=ISMINE_SPENDABLE) const;
CAmount GetImmatureWatchOnlyCredit(const bool fUseCache=true) const;
CAmount GetCredit(const std::optional<int>& asOfHeight, const isminefilter& filter) const;
CAmount GetImmatureCredit(const std::optional<int>& asOfHeight, bool fUseCache=true) const;
CAmount GetAvailableCredit(const std::optional<int>& asOfHeight, bool fUseCache=true, const isminefilter& filter=ISMINE_SPENDABLE) const;
CAmount GetImmatureWatchOnlyCredit(const std::optional<int>& asOfHeight, const bool fUseCache=true) const;
CAmount GetChange() const;
void GetAmounts(std::list<COutputEntry>& listReceived,
@ -681,7 +688,7 @@ public:
bool IsFromMe(const isminefilter& filter) const;
bool IsTrusted() const;
bool IsTrusted(const std::optional<int>& asOfHeight) const;
int64_t GetTxTime() const;
int GetRequestCount() const;
@ -1428,15 +1435,18 @@ public:
/**
* populate vCoins with vector of available COutputs.
*
* **NB**: If `asOfHeight` is specified, then `nMinDepth` must be `> 0`.
*/
void AvailableCoins(std::vector<COutput>& vCoins,
const std::optional<int>& asOfHeight,
bool fOnlyConfirmed=true,
const CCoinControl *coinControl = NULL,
bool fIncludeZeroValue=false,
bool fIncludeCoinBase=true,
bool fOnlySpendable=false,
int nMinDepth = 0,
std::set<CTxDestination>* onlyFilterByDests = nullptr) const;
const std::set<CTxDestination>& onlyFilterByDests = std::set<CTxDestination>()) const;
/**
* Shuffle and select coins until nTargetValue is reached while avoiding
@ -1516,16 +1526,17 @@ public:
SpendableInputs FindSpendableInputs(
ZTXOSelector paymentSource,
bool allowTransparentCoinbase,
uint32_t minDepth) const;
uint32_t minDepth,
const std::optional<int>& asOfHeight) const;
bool SelectorMatchesAddress(const ZTXOSelector& source, const CTxDestination& a0) const;
bool SelectorMatchesAddress(const ZTXOSelector& source, const libzcash::SproutPaymentAddress& a0) const;
bool SelectorMatchesAddress(const ZTXOSelector& source, const libzcash::SaplingPaymentAddress& a0) const;
bool IsSpent(const uint256& hash, unsigned int n) const;
bool IsSproutSpent(const uint256& nullifier) const;
bool IsSaplingSpent(const uint256& nullifier) const;
bool IsOrchardSpent(const OrchardOutPoint& outpoint) const;
bool IsSpent(const uint256& hash, unsigned int n, const std::optional<int>& asOfHeight) const;
bool IsSproutSpent(const uint256& nullifier, const std::optional<int>& asOfHeight) const;
bool IsSaplingSpent(const uint256& nullifier, const std::optional<int>& asOfHeight) const;
bool IsOrchardSpent(const OrchardOutPoint& outpoint, const std::optional<int>& asOfHeight) const;
bool IsLockedCoin(uint256 hash, unsigned int n) const;
void LockCoin(COutPoint& output);
@ -1793,11 +1804,14 @@ public:
void ReacceptWalletTransactions();
void ResendWalletTransactions(int64_t nBestBlockTime);
std::vector<uint256> ResendWalletTransactionsBefore(int64_t nTime);
CAmount GetBalance(const isminefilter& filter=ISMINE_SPENDABLE, const int min_depth=0) const;
CAmount GetUnconfirmedBalance() const;
CAmount GetImmatureBalance() const;
CAmount GetUnconfirmedWatchOnlyBalance() const;
CAmount GetImmatureWatchOnlyBalance() const;
CAmount GetBalance(const std::optional<int>& asOfHeight,
const isminefilter& filter=ISMINE_SPENDABLE,
const int min_depth=0) const;
/**
* Returns the balance taking into account _only_ transactions in the mempool.
*/
CAmount GetUnconfirmedTransparentBalance() const;
CAmount GetImmatureBalance(const std::optional<int>& asOfHeight) const;
CAmount GetLegacyBalance(const isminefilter& filter, int minDepth) const;
/**
@ -1872,7 +1886,7 @@ public:
void GetAllReserveKeys(std::set<CKeyID>& setAddress) const;
std::set< std::set<CTxDestination> > GetAddressGroupings();
std::map<CTxDestination, CAmount> GetAddressBalances();
std::map<CTxDestination, CAmount> GetAddressBalances(const std::optional<int>& asOfHeight);
std::optional<uint256> GetSproutNoteNullifier(
const JSDescription& jsdesc,
@ -2079,6 +2093,7 @@ public:
std::vector<SaplingNoteEntry>& saplingEntriesRet,
std::vector<OrchardNoteMetadata>& orchardNotesRet,
const std::optional<NoteFilter>& noteFilter,
const std::optional<int>& asOfHeight,
int minDepth,
int maxDepth=INT_MAX,
bool ignoreSpent=true,