Update z_mergetoaddress to use WalletTxBuilder
Co-authored-by: Kris Nuttycombe <kris@nutty.land> Co-authored-by: Jack Grigg <jack@z.cash>
This commit is contained in:
parent
250a3f049b
commit
94f5bfc595
|
@ -32,6 +32,13 @@ RPC Changes
|
|||
- The `estimatepriority` RPC call has been removed.
|
||||
- The `priority_delta` argument to the `prioritisetransaction` RPC call now has
|
||||
no effect and must be set to a dummy value (0 or null).
|
||||
- The `z_shieldcoinbase` and `z_mergetoaddress` RPC methods no longer support
|
||||
transfers of funds to Sprout recipients. While Sprout change may still be
|
||||
produced in the process of spending Sprout funds, it is no longer possible
|
||||
to transfer funds between Sprout addresses. Also, some failures have moved
|
||||
from synchronous to asynchronous, so while you should already be checking
|
||||
the async operation status, there are now more cases that may trigger
|
||||
failure at that stage.
|
||||
|
||||
Changes to Transaction Fee Selection
|
||||
------------------------------------
|
||||
|
|
|
@ -21,6 +21,7 @@ class MergeToAddressMixedNotes(BitcoinTestFramework):
|
|||
'-minrelaytxfee=0',
|
||||
'-anchorconfirmations=1',
|
||||
'-allowdeprecated=getnewaddress',
|
||||
'-allowdeprecated=legacy_privacy',
|
||||
'-allowdeprecated=z_getnewaddress',
|
||||
'-allowdeprecated=z_getbalance',
|
||||
'-allowdeprecated=z_gettotalbalance',
|
||||
|
|
|
@ -14,7 +14,10 @@ class MergeToAddressSapling (BitcoinTestFramework):
|
|||
self.helper.setup_chain(self)
|
||||
|
||||
def setup_network(self, split=False):
|
||||
self.helper.setup_network(self, ['-anchorconfirmations=1'])
|
||||
self.helper.setup_network(self, [
|
||||
'-anchorconfirmations=1',
|
||||
'-allowdeprecated=legacy_privacy',
|
||||
])
|
||||
|
||||
def run_test(self):
|
||||
self.helper.run_test(self)
|
||||
|
|
|
@ -49,7 +49,6 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
self.sync_all()
|
||||
|
||||
n0_sprout_addr0 = self.nodes[0].listaddresses()[0]['sprout']['addresses'][0]
|
||||
n2_sprout_addr = self.nodes[2].listaddresses()[0]['sprout']['addresses'][0]
|
||||
|
||||
# Attempt to shield coinbase to Sprout on node 0. Should fail;
|
||||
# transfers to Sprout are no longer supported
|
||||
|
@ -77,20 +76,21 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
self.sync_all()
|
||||
assert_equal(self.nodes[0].z_getbalance(n0_taddr0), Decimal('3'))
|
||||
|
||||
# Create mergetoaddress taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass. This will spend the available funds in taddr0
|
||||
# Create mergetoaddress taddr -> Sprout transaction, should fail
|
||||
n1_sprout_addr0 = self.nodes[1].z_getnewaddress('sprout')
|
||||
merge_tx_0 = self.nodes[0].z_mergetoaddress(["ANY_TADDR"], n1_sprout_addr0, 0)
|
||||
wait_and_assert_operationid_status(self.nodes[0], merge_tx_0['opid'])
|
||||
print("taddr -> Sprout z_mergetoaddress tx accepted before Canopy on node 0")
|
||||
assert_raises_message(
|
||||
JSONRPCException,
|
||||
"Sending funds into the Sprout pool is no longer supported.",
|
||||
self.nodes[0].z_mergetoaddress,
|
||||
["ANY_TADDR"], n1_sprout_addr0, 0)
|
||||
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[1].z_getbalance(n1_sprout_addr0), Decimal('3'))
|
||||
|
||||
# Send some funds back to n0_taddr0
|
||||
recipients = [{"address": n0_taddr0, "amount": Decimal('1')}]
|
||||
myopid = self.nodes[1].z_sendmany(n1_sprout_addr0, recipients, 1, 0, 'AllowRevealedRecipients')
|
||||
wait_and_assert_operationid_status(self.nodes[1], myopid)
|
||||
myopid = self.nodes[0].z_sendmany(n0_sprout_addr0, recipients, 1, 0, 'AllowRevealedRecipients')
|
||||
wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
|
||||
# Mine to one block before Canopy activation on node 0; adding value
|
||||
# to the Sprout pool will fail now since the transaction must be
|
||||
|
@ -99,7 +99,7 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
self.nodes[0].generate(4)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[0].getblockchaininfo()['upgrades']['e9ff75a6']['status'], 'pending')
|
||||
assert_equal(self.nodes[0].z_getbalance(n0_taddr0), Decimal('1'))
|
||||
assert_equal(self.nodes[0].z_getbalance(n0_taddr0), Decimal('4'))
|
||||
|
||||
# Shield coinbase to Sprout on node 0. Should fail
|
||||
n0_coinbase_taddr = get_coinbase_address(self.nodes[0])
|
||||
|
@ -121,15 +121,9 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
# Create z_mergetoaddress [taddr, Sprout] -> Sprout transaction on node 0. Should fail
|
||||
assert_raises_message(
|
||||
JSONRPCException,
|
||||
"Sprout shielding is not supported after Canopy",
|
||||
"Sending funds into the Sprout pool is no longer supported.",
|
||||
self.nodes[0].z_mergetoaddress,
|
||||
["ANY_TADDR", "ANY_SPROUT"], self.nodes[1].z_getnewaddress('sprout'))
|
||||
print("[taddr, Sprout] -> Sprout z_mergetoaddress tx rejected at Canopy activation on node 0")
|
||||
|
||||
# Create z_mergetoaddress Sprout -> Sprout transaction on node 0. Should pass
|
||||
merge_tx_1 = self.nodes[0].z_mergetoaddress(["ANY_SPROUT"], self.nodes[1].z_getnewaddress('sprout'))
|
||||
wait_and_assert_operationid_status(self.nodes[0], merge_tx_1['opid'])
|
||||
print("Sprout -> Sprout z_mergetoaddress tx accepted at Canopy activation on node 0")
|
||||
|
||||
# Activate Canopy
|
||||
self.nodes[0].generate(1)
|
||||
|
@ -149,17 +143,5 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
print("taddr -> Sapling z_shieldcoinbase tx accepted after Canopy on node 0")
|
||||
|
||||
# Mine to one block before NU5 activation.
|
||||
self.nodes[0].generate(4)
|
||||
self.sync_all()
|
||||
|
||||
# Create z_mergetoaddress Sprout -> Sprout transaction on node 1. Should pass
|
||||
merge_tx_2 = self.nodes[1].z_mergetoaddress(["ANY_SPROUT"], n2_sprout_addr)
|
||||
wait_and_assert_operationid_status(self.nodes[1], merge_tx_2['opid'])
|
||||
print("Sprout -> Sprout z_mergetoaddress tx accepted at NU5 activation on node 1")
|
||||
|
||||
self.nodes[1].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
if __name__ == '__main__':
|
||||
RemoveSproutShieldingTest().main()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,8 +9,10 @@
|
|||
#include "asyncrpcoperation.h"
|
||||
#include "primitives/transaction.h"
|
||||
#include "transaction_builder.h"
|
||||
#include "uint256.h"
|
||||
#include "wallet.h"
|
||||
#include "wallet/paymentdisclosure.h"
|
||||
#include "wallet/wallet_tx_builder.h"
|
||||
#include "zcash/Address.hpp"
|
||||
#include "zcash/JoinSplit.hpp"
|
||||
|
||||
|
@ -25,16 +27,8 @@
|
|||
|
||||
using namespace libzcash;
|
||||
|
||||
// Input UTXO is a tuple of txid, vout, amount, script
|
||||
typedef std::tuple<COutPoint, CAmount, CScript> MergeToAddressInputUTXO;
|
||||
|
||||
// Input JSOP is a tuple of JSOutpoint, note, amount, spending key
|
||||
typedef std::tuple<JSOutPoint, SproutNote, CAmount, SproutSpendingKey> MergeToAddressInputSproutNote;
|
||||
|
||||
typedef std::tuple<SaplingOutPoint, SaplingNote, CAmount, SaplingExpandedSpendingKey> MergeToAddressInputSaplingNote;
|
||||
|
||||
// A recipient is a tuple of address, memo (optional if zaddr)
|
||||
typedef std::pair<libzcash::PaymentAddress, std::string> MergeToAddressRecipient;
|
||||
typedef std::pair<libzcash::PaymentAddress, std::optional<Memo>> MergeToAddressRecipient;
|
||||
|
||||
// Package of info which is passed to perform_joinsplit methods.
|
||||
struct MergeToAddressJSInfo {
|
||||
|
@ -56,12 +50,11 @@ class AsyncRPCOperation_mergetoaddress : public AsyncRPCOperation
|
|||
{
|
||||
public:
|
||||
AsyncRPCOperation_mergetoaddress(
|
||||
std::optional<TransactionBuilder> builder,
|
||||
CMutableTransaction contextualTx,
|
||||
std::vector<MergeToAddressInputUTXO> utxoInputs,
|
||||
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs,
|
||||
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs,
|
||||
WalletTxBuilder builder,
|
||||
ZTXOSelector ztxoSelector,
|
||||
SpendableInputs allInputs,
|
||||
MergeToAddressRecipient recipient,
|
||||
TransactionStrategy strategy,
|
||||
CAmount fee = DEFAULT_FEE,
|
||||
UniValue contextInfo = NullUniValue);
|
||||
virtual ~AsyncRPCOperation_mergetoaddress();
|
||||
|
@ -72,6 +65,8 @@ public:
|
|||
AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress const&) = delete; // Copy assign
|
||||
AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress&&) = delete; // Move assign
|
||||
|
||||
tl::expected<void, InputSelectionError> prepare(const CChainParams& chainparams, CWallet& wallet);
|
||||
|
||||
virtual void main();
|
||||
|
||||
virtual UniValue getStatus() const;
|
||||
|
@ -85,15 +80,13 @@ private:
|
|||
|
||||
UniValue contextinfo_; // optional data to include in return value from getStatus()
|
||||
|
||||
bool isUsingBuilder_; // Indicates that no Sprout addresses are involved
|
||||
uint32_t consensusBranchId_;
|
||||
CAmount fee_;
|
||||
int mindepth_;
|
||||
bool isToTaddr_;
|
||||
bool isToZaddr_;
|
||||
CTxDestination toTaddr_;
|
||||
PaymentAddress toPaymentAddress_;
|
||||
std::string memo_;
|
||||
std::optional<Memo> memo_;
|
||||
|
||||
ed25519::VerificationKey joinSplitPubKey_;
|
||||
ed25519::SigningKey joinSplitPrivKey_;
|
||||
|
@ -101,35 +94,14 @@ private:
|
|||
// The key is the result string from calling JSOutPoint::ToString()
|
||||
std::unordered_map<std::string, MergeToAddressWitnessAnchorData> jsopWitnessAnchorMap;
|
||||
|
||||
std::vector<MergeToAddressInputUTXO> utxoInputs_;
|
||||
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs_;
|
||||
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs_;
|
||||
WalletTxBuilder builder_;
|
||||
ZTXOSelector ztxoSelector_;
|
||||
SpendableInputs allInputs_;
|
||||
TransactionStrategy strategy_;
|
||||
|
||||
TransactionBuilder builder_;
|
||||
CTransaction tx_;
|
||||
std::optional<TransactionEffects> effects_;
|
||||
|
||||
std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s);
|
||||
bool main_impl();
|
||||
|
||||
// JoinSplit without any input notes to spend
|
||||
UniValue perform_joinsplit(MergeToAddressJSInfo&);
|
||||
|
||||
// JoinSplit with input notes to spend (JSOutPoints))
|
||||
UniValue perform_joinsplit(MergeToAddressJSInfo&, std::vector<JSOutPoint>&);
|
||||
|
||||
// JoinSplit where you have the witnesses and anchor
|
||||
UniValue perform_joinsplit(
|
||||
MergeToAddressJSInfo& info,
|
||||
std::vector<std::optional<SproutWitness>> witnesses,
|
||||
uint256 anchor);
|
||||
|
||||
void lock_utxos();
|
||||
|
||||
void unlock_utxos();
|
||||
|
||||
void lock_notes();
|
||||
|
||||
void unlock_notes();
|
||||
uint256 main_impl(CWallet& wallet, const CChainParams& chainparams);
|
||||
|
||||
// payment disclosure!
|
||||
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
|
||||
|
@ -144,44 +116,11 @@ public:
|
|||
|
||||
TEST_FRIEND_AsyncRPCOperation_mergetoaddress(std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr) : delegate(ptr) {}
|
||||
|
||||
CTransaction getTx()
|
||||
{
|
||||
return delegate->tx_;
|
||||
}
|
||||
|
||||
void setTx(CTransaction tx)
|
||||
{
|
||||
delegate->tx_ = tx;
|
||||
}
|
||||
|
||||
// Delegated methods
|
||||
|
||||
std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s)
|
||||
uint256 main_impl(CWallet& wallet, const CChainParams& chainparams)
|
||||
{
|
||||
return delegate->get_memo_from_hex_string(s);
|
||||
}
|
||||
|
||||
bool main_impl()
|
||||
{
|
||||
return delegate->main_impl();
|
||||
}
|
||||
|
||||
UniValue perform_joinsplit(MergeToAddressJSInfo& info)
|
||||
{
|
||||
return delegate->perform_joinsplit(info);
|
||||
}
|
||||
|
||||
UniValue perform_joinsplit(MergeToAddressJSInfo& info, std::vector<JSOutPoint>& v)
|
||||
{
|
||||
return delegate->perform_joinsplit(info, v);
|
||||
}
|
||||
|
||||
UniValue perform_joinsplit(
|
||||
MergeToAddressJSInfo& info,
|
||||
std::vector<std::optional<SproutWitness>> witnesses,
|
||||
uint256 anchor)
|
||||
{
|
||||
return delegate->perform_joinsplit(info, witnesses, anchor);
|
||||
return delegate->main_impl(wallet, chainparams);
|
||||
}
|
||||
|
||||
void set_state(OperationStatus state)
|
||||
|
|
|
@ -17,10 +17,21 @@
|
|||
#include <librustzcash.h>
|
||||
#include <rust/ed25519.h>
|
||||
|
||||
namespace {
|
||||
|
||||
bool find_error(const UniValue& objError, const std::string& expected) {
|
||||
return find_value(objError, "message").get_str().find(expected) != string::npos;
|
||||
}
|
||||
|
||||
CWalletTx FakeWalletTx() {
|
||||
CMutableTransaction mtx;
|
||||
mtx.vout.resize(1);
|
||||
mtx.vout[0].nValue = 1;
|
||||
return CWalletTx(nullptr, mtx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: test private methods
|
||||
TEST(WalletRPCTests, RPCZMergeToAddressInternals)
|
||||
{
|
||||
|
@ -46,128 +57,26 @@ TEST(WalletRPCTests, RPCZMergeToAddressInternals)
|
|||
auto taddr = pwalletMain->GenerateNewKey(true).GetID();
|
||||
std::string taddr_string = keyIO.EncodeDestination(taddr);
|
||||
|
||||
MergeToAddressRecipient taddr1(keyIO.DecodePaymentAddress(taddr_string).value(), "");
|
||||
MergeToAddressRecipient taddr1(keyIO.DecodePaymentAddress(taddr_string).value(), Memo());
|
||||
auto pa = pwalletMain->GenerateNewSproutZKey();
|
||||
MergeToAddressRecipient zaddr1(pa, "DEADBEEF");
|
||||
MergeToAddressRecipient zaddr1(pa, Memo());
|
||||
|
||||
WalletTxBuilder builder(Params(), minRelayTxFee);
|
||||
auto selector = CWallet::LegacyTransparentZTXOSelector(
|
||||
true,
|
||||
TransparentCoinbasePolicy::Disallow);
|
||||
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders);
|
||||
|
||||
// Insufficient funds
|
||||
{
|
||||
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},0, CScript()} };
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, {}, {}, zaddr1) );
|
||||
operation->main();
|
||||
EXPECT_TRUE(operation->isFailed());
|
||||
std::string msg = operation->getErrorMessage();
|
||||
EXPECT_TRUE(msg.find("Insufficient funds, have 0.00 and miners fee is 0.00001") != string::npos);
|
||||
}
|
||||
|
||||
// get_memo_from_hex_string())
|
||||
{
|
||||
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000, CScript()} };
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, {}, {}, zaddr1) );
|
||||
std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_mergetoaddress> (operation);
|
||||
TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr);
|
||||
|
||||
std::string memo = "DEADBEEF";
|
||||
std::array<unsigned char, ZC_MEMO_SIZE> array = proxy.get_memo_from_hex_string(memo);
|
||||
EXPECT_EQ(array[0], 0xDE);
|
||||
EXPECT_EQ(array[1], 0xAD);
|
||||
EXPECT_EQ(array[2], 0xBE);
|
||||
EXPECT_EQ(array[3], 0xEF);
|
||||
for (int i=4; i<ZC_MEMO_SIZE; i++) {
|
||||
EXPECT_EQ(array[i], 0x00); // zero padding
|
||||
}
|
||||
|
||||
// memo is longer than allowed
|
||||
std::vector<char> v (2 * (ZC_MEMO_SIZE+1));
|
||||
std::fill(v.begin(), v.end(), 'A');
|
||||
std::string bigmemo(v.begin(), v.end());
|
||||
|
||||
try {
|
||||
proxy.get_memo_from_hex_string(bigmemo);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const UniValue& objError) {
|
||||
EXPECT_TRUE(find_error(objError, "too big"));
|
||||
}
|
||||
|
||||
// invalid hexadecimal string
|
||||
std::fill(v.begin(), v.end(), '@'); // not a hex character
|
||||
std::string badmemo(v.begin(), v.end());
|
||||
|
||||
try {
|
||||
proxy.get_memo_from_hex_string(badmemo);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const UniValue& objError) {
|
||||
EXPECT_TRUE(find_error(objError, "hexadecimal format"));
|
||||
}
|
||||
|
||||
// odd length hexadecimal string
|
||||
std::fill(v.begin(), v.end(), 'A');
|
||||
v.resize(v.size() - 1);
|
||||
assert(v.size() % 2 == 1); // odd length
|
||||
std::string oddmemo(v.begin(), v.end());
|
||||
try {
|
||||
proxy.get_memo_from_hex_string(oddmemo);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const UniValue& objError) {
|
||||
EXPECT_TRUE(find_error(objError, "hexadecimal format"));
|
||||
}
|
||||
}
|
||||
|
||||
// Test the perform_joinsplit methods.
|
||||
{
|
||||
// Dummy input so the operation object can be instantiated.
|
||||
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000, CScript()} };
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, {}, {}, zaddr1) );
|
||||
std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_mergetoaddress> (operation);
|
||||
TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr);
|
||||
|
||||
// Enable test mode so tx is not sent and proofs are not generated
|
||||
static_cast<AsyncRPCOperation_mergetoaddress *>(operation.get())->testmode = true;
|
||||
|
||||
MergeToAddressJSInfo info;
|
||||
std::vector<std::optional < SproutWitness>> witnesses;
|
||||
uint256 anchor;
|
||||
try {
|
||||
proxy.perform_joinsplit(info, witnesses, anchor);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const std::runtime_error & e) {
|
||||
EXPECT_TRUE(string(e.what()).find("anchor is null") != string::npos);
|
||||
}
|
||||
|
||||
try {
|
||||
std::vector<JSOutPoint> v;
|
||||
proxy.perform_joinsplit(info, v);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const std::runtime_error & e) {
|
||||
EXPECT_TRUE(string(e.what()).find("anchor is null") != string::npos);
|
||||
}
|
||||
|
||||
info.notes.push_back(SproutNote());
|
||||
try {
|
||||
proxy.perform_joinsplit(info);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const std::runtime_error & e) {
|
||||
EXPECT_TRUE(string(e.what()).find("number of notes") != string::npos);
|
||||
}
|
||||
|
||||
info.notes.clear();
|
||||
info.vjsin.push_back(JSInput());
|
||||
info.vjsin.push_back(JSInput());
|
||||
info.vjsin.push_back(JSInput());
|
||||
try {
|
||||
proxy.perform_joinsplit(info);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const std::runtime_error & e) {
|
||||
EXPECT_TRUE(string(e.what()).find("unsupported joinsplit input") != string::npos);
|
||||
}
|
||||
|
||||
info.vjsin.clear();
|
||||
try {
|
||||
proxy.perform_joinsplit(info);
|
||||
FAIL() << "Should have caused an error";
|
||||
} catch (const std::runtime_error & e) {
|
||||
EXPECT_TRUE(string(e.what()).find("error verifying joinsplit") != string::npos);
|
||||
}
|
||||
SpendableInputs inputs;
|
||||
auto wtx = FakeWalletTx();
|
||||
inputs.utxos.emplace_back(COutput(&wtx, 0, 100, true));
|
||||
auto operation = AsyncRPCOperation_mergetoaddress(std::move(builder), selector, inputs, zaddr1, strategy, 0);
|
||||
operation.main();
|
||||
EXPECT_TRUE(operation.isFailed());
|
||||
std::string msg = operation.getErrorMessage();
|
||||
EXPECT_EQ(msg, "Sending funds into the Sprout pool is no longer supported.");
|
||||
}
|
||||
}
|
||||
UnloadGlobalWallet();
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include "util/time.h"
|
||||
#include "asyncrpcoperation.h"
|
||||
#include "asyncrpcqueue.h"
|
||||
#include "wallet/asyncrpcoperation_common.h"
|
||||
#include "wallet/asyncrpcoperation_mergetoaddress.h"
|
||||
#include "wallet/asyncrpcoperation_saplingmigration.h"
|
||||
#include "wallet/asyncrpcoperation_sendmany.h"
|
||||
|
@ -5342,9 +5343,9 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
if (!EnsureWalletIsAvailable(fHelp))
|
||||
return NullUniValue;
|
||||
|
||||
if (fHelp || params.size() < 2 || params.size() > 6)
|
||||
if (fHelp || params.size() < 2 || params.size() > 7)
|
||||
throw runtime_error(
|
||||
"z_mergetoaddress [\"fromaddress\", ... ] \"toaddress\" ( fee ) ( transparent_limit ) ( shielded_limit ) ( memo )\n"
|
||||
"z_mergetoaddress [\"fromaddress\", ... ] \"toaddress\" ( fee ) ( transparent_limit ) ( shielded_limit ) ( memo ) ( privacyPolicy )\n"
|
||||
"\nMerge multiple UTXOs and notes into a single UTXO or note. Coinbase UTXOs are ignored; use `z_shieldcoinbase`"
|
||||
"\nto combine those into a single note."
|
||||
"\n\nThis is an asynchronous operation, and UTXOs selected for merging will be locked. If there is an error, they"
|
||||
|
@ -5376,6 +5377,26 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
"5. shielded_limit (numeric, optional, default="
|
||||
+ strprintf("%d Sprout or %d Sapling Notes", MERGE_TO_ADDRESS_DEFAULT_SPROUT_LIMIT, MERGE_TO_ADDRESS_DEFAULT_SAPLING_LIMIT) + ") Limit on the maximum number of notes to merge. Set to 0 to merge as many as will fit in the transaction.\n"
|
||||
"6. \"memo\" (string, optional) Encoded as hex. When toaddress is a zaddr, this will be stored in the memo field of the new note.\n"
|
||||
"7. privacyPolicy (string, optional, default=\"LegacyCompat\") Policy for what information leakage is acceptable.\n"
|
||||
" One of the following strings:\n"
|
||||
" - \"FullPrivacy\": Only allow fully-shielded transactions (involving a single shielded value pool).\n"
|
||||
" - \"LegacyCompat\": If the transaction involves any Unified Addresses, this is equivalent to\n"
|
||||
" \"FullPrivacy\". Otherwise, this is equivalent to \"AllowFullyTransparent\".\n"
|
||||
" - \"AllowRevealedAmounts\": Allow funds to cross between shielded value pools, revealing the amount\n"
|
||||
" that crosses pools.\n"
|
||||
" - \"AllowRevealedRecipients\": Allow transparent recipients. This also implies revealing\n"
|
||||
" information described under \"AllowRevealedAmounts\".\n"
|
||||
" - \"AllowRevealedSenders\": Allow transparent funds to be spent, revealing the sending\n"
|
||||
" addresses and amounts. This implies revealing information described under \"AllowRevealedAmounts\".\n"
|
||||
" - \"AllowFullyTransparent\": Allow transaction to both spend transparent funds and have\n"
|
||||
" transparent recipients. This implies revealing information described under \"AllowRevealedSenders\"\n"
|
||||
" and \"AllowRevealedRecipients\".\n"
|
||||
" - \"AllowLinkingAccountAddresses\": Allow selecting transparent coins from the full account,\n"
|
||||
" rather than just the funds sent to the transparent receiver in the provided Unified Address.\n"
|
||||
" This implies revealing information described under \"AllowRevealedSenders\".\n"
|
||||
" - \"NoPrivacy\": Allow the transaction to reveal any information necessary to create it.\n"
|
||||
" This implies revealing information described under \"AllowFullyTransparent\" and\n"
|
||||
" \"AllowLinkingAccountAddresses\".\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"remainingUTXOs\": xxx (numeric) Number of UTXOs still available for merging.\n"
|
||||
|
@ -5409,10 +5430,10 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
|
||||
// Keep track of addresses to spot duplicates
|
||||
std::set<std::string> setAddress;
|
||||
std::set<ReceiverType> receiverTypes;
|
||||
|
||||
bool isFromNonSprout = false;
|
||||
|
||||
KeyIO keyIO(Params());
|
||||
const auto chainparams = Params();
|
||||
KeyIO keyIO(chainparams);
|
||||
// Sources
|
||||
for (const UniValue& o : addresses.getValues()) {
|
||||
if (!o.isStr())
|
||||
|
@ -5421,28 +5442,27 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
std::string address = o.get_str();
|
||||
|
||||
if (address == "ANY_TADDR") {
|
||||
receiverTypes.insert({ReceiverType::P2PKH, ReceiverType::P2SH});
|
||||
useAnyUTXO = true;
|
||||
isFromNonSprout = true;
|
||||
} else if (address == "ANY_SPROUT") {
|
||||
// TODO: How can we add sprout addresses?
|
||||
// receiverTypes.insert(ReceiverType::Sprout);
|
||||
useAnySprout = true;
|
||||
} else if (address == "ANY_SAPLING") {
|
||||
receiverTypes.insert(ReceiverType::Sapling);
|
||||
useAnySapling = true;
|
||||
isFromNonSprout = true;
|
||||
} else {
|
||||
auto addr = keyIO.DecodePaymentAddress(address);
|
||||
if (addr.has_value()) {
|
||||
examine(addr.value(), match {
|
||||
[&](const CKeyID& taddr) {
|
||||
taddrs.insert(taddr);
|
||||
isFromNonSprout = true;
|
||||
},
|
||||
[&](const CScriptID& taddr) {
|
||||
taddrs.insert(taddr);
|
||||
isFromNonSprout = true;
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& zaddr) {
|
||||
zaddrs.push_back(zaddr);
|
||||
isFromNonSprout = true;
|
||||
},
|
||||
[&](const libzcash::SproutPaymentAddress& zaddr) {
|
||||
zaddrs.push_back(zaddr);
|
||||
|
@ -5450,7 +5470,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
[&](libzcash::UnifiedAddress) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Unified addresses are not supported in z_mergetoaddress");
|
||||
"Funds belonging to unified addresses can not be merged in z_mergetoaddress");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -5471,16 +5491,16 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
}
|
||||
|
||||
const int nextBlockHeight = chainActive.Height() + 1;
|
||||
const bool overwinterActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER);
|
||||
const bool saplingActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING);
|
||||
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
|
||||
const bool overwinterActive = chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER);
|
||||
const bool saplingActive = chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING);
|
||||
const bool canopyActive = chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
|
||||
|
||||
// Validate the destination address
|
||||
auto destStr = params[1].get_str();
|
||||
auto destaddress = keyIO.DecodePaymentAddress(destStr);
|
||||
bool isToTaddr = false;
|
||||
bool isToSproutZaddr = false;
|
||||
bool isToSaplingZaddr = false;
|
||||
|
||||
if (destaddress.has_value()) {
|
||||
examine(destaddress.value(), match {
|
||||
[&](CKeyID addr) {
|
||||
|
@ -5496,10 +5516,8 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, Sapling has not activated");
|
||||
}
|
||||
},
|
||||
[&](libzcash::SproutPaymentAddress addr) {
|
||||
isToSproutZaddr = true;
|
||||
},
|
||||
[&](libzcash::UnifiedAddress) {
|
||||
[](libzcash::SproutPaymentAddress) { },
|
||||
[](libzcash::UnifiedAddress) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Invalid parameter, unified addresses are not yet supported.");
|
||||
|
@ -5511,11 +5529,6 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
string("Invalid parameter, unknown address format: ") + destStr);
|
||||
}
|
||||
|
||||
if (canopyActive && isFromNonSprout && isToSproutZaddr) {
|
||||
// Value can be moved within Sprout, but not into Sprout.
|
||||
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy");
|
||||
}
|
||||
|
||||
// Convert fee from currency format to zatoshis
|
||||
CAmount nFee = DEFAULT_FEE;
|
||||
if (params.size() > 2) {
|
||||
|
@ -5536,34 +5549,24 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
|
||||
int sproutNoteLimit = MERGE_TO_ADDRESS_DEFAULT_SPROUT_LIMIT;
|
||||
int saplingNoteLimit = MERGE_TO_ADDRESS_DEFAULT_SAPLING_LIMIT;
|
||||
int shieldedNoteLimit = 0;
|
||||
if (params.size() > 4) {
|
||||
int nNoteLimit = params[4].get_int();
|
||||
if (nNoteLimit < 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Limit on maximum number of notes cannot be negative");
|
||||
}
|
||||
sproutNoteLimit = nNoteLimit;
|
||||
saplingNoteLimit = nNoteLimit;
|
||||
shieldedNoteLimit = nNoteLimit;
|
||||
}
|
||||
|
||||
std::string memo;
|
||||
std::optional<Memo> memo;
|
||||
if (params.size() > 5) {
|
||||
memo = params[5].get_str();
|
||||
if (!(isToSproutZaddr || isToSaplingZaddr)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo can not be used with a taddr. It can only be used with a zaddr.");
|
||||
} else if (!IsHex(memo)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format.");
|
||||
}
|
||||
if (memo.length() > ZC_MEMO_SIZE*2) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", ZC_MEMO_SIZE ));
|
||||
}
|
||||
memo = Memo::FromHexOrThrow(params[5].get_str());
|
||||
}
|
||||
|
||||
MergeToAddressRecipient recipient(destaddress.value(), memo);
|
||||
|
||||
// Prepare to get UTXOs and notes
|
||||
std::vector<MergeToAddressInputUTXO> utxoInputs;
|
||||
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs;
|
||||
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs;
|
||||
SpendableInputs allInputs;
|
||||
CAmount mergedUTXOValue = 0;
|
||||
CAmount mergedNoteValue = 0;
|
||||
CAmount remainingUTXOValue = 0;
|
||||
|
@ -5576,9 +5579,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
|
||||
unsigned int max_tx_size = saplingActive ? MAX_TX_SIZE_AFTER_SAPLING : MAX_TX_SIZE_BEFORE_SAPLING;
|
||||
size_t estimatedTxSize = 200; // tx overhead + wiggle room
|
||||
if (isToSproutZaddr) {
|
||||
estimatedTxSize += JOINSPLIT_SIZE(SAPLING_TX_VERSION); // We assume that sapling has activated
|
||||
} else if (isToSaplingZaddr) {
|
||||
if (isToSaplingZaddr) {
|
||||
estimatedTxSize += OUTPUTDESCRIPTION_SIZE;
|
||||
}
|
||||
|
||||
|
@ -5615,8 +5616,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
maxedOutUTXOsFlag = true;
|
||||
} else {
|
||||
estimatedTxSize += increase;
|
||||
COutPoint utxo(out.tx->GetHash(), out.i);
|
||||
utxoInputs.emplace_back(utxo, nValue, scriptPubKey);
|
||||
allInputs.utxos.push_back(out);
|
||||
mergedUTXOValue += nValue;
|
||||
}
|
||||
}
|
||||
|
@ -5629,51 +5629,59 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
|
||||
if (useAnySprout || useAnySapling || zaddrs.size() > 0) {
|
||||
// Get available notes
|
||||
std::vector<SproutNoteEntry> sproutEntries;
|
||||
std::vector<SaplingNoteEntry> saplingEntries;
|
||||
std::vector<OrchardNoteMetadata> orchardEntries;
|
||||
std::optional<NoteFilter> noteFilter =
|
||||
useAnySprout || useAnySapling ?
|
||||
std::nullopt :
|
||||
std::optional(NoteFilter::ForPaymentAddresses(zaddrs));
|
||||
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, std::nullopt, nAnchorConfirmations);
|
||||
|
||||
std::vector<SproutNoteEntry> sproutCandidateNotes;
|
||||
std::vector<SaplingNoteEntry> saplingCandidateNotes;
|
||||
std::vector<OrchardNoteMetadata> orchardCandidateNotes;
|
||||
pwalletMain->GetFilteredNotes(
|
||||
sproutCandidateNotes,
|
||||
saplingCandidateNotes,
|
||||
orchardCandidateNotes,
|
||||
noteFilter,
|
||||
std::nullopt,
|
||||
nAnchorConfirmations);
|
||||
|
||||
// If Sapling is not active, do not allow sending from a sapling addresses.
|
||||
if (!saplingActive && saplingEntries.size() > 0) {
|
||||
if (!saplingActive && saplingCandidateNotes.size() > 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, Sapling has not activated");
|
||||
}
|
||||
// Do not include Sprout/Sapling notes if using "ANY_SAPLING"/"ANY_SPROUT" respectively
|
||||
if (useAnySprout) {
|
||||
saplingEntries.clear();
|
||||
saplingCandidateNotes.clear();
|
||||
}
|
||||
if (useAnySapling) {
|
||||
sproutEntries.clear();
|
||||
sproutCandidateNotes.clear();
|
||||
}
|
||||
// Sending from both Sprout and Sapling is currently unsupported using z_mergetoaddress
|
||||
if ((sproutEntries.size() > 0 && saplingEntries.size() > 0) || (useAnySprout && useAnySapling)) {
|
||||
if ((sproutCandidateNotes.size() > 0 && saplingCandidateNotes.size() > 0) || (useAnySprout && useAnySapling)) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Cannot send from both Sprout and Sapling addresses using z_mergetoaddress");
|
||||
}
|
||||
// If sending between shielded addresses, they must be within the same value pool
|
||||
if ((saplingEntries.size() > 0 && isToSproutZaddr) || (sproutEntries.size() > 0 && isToSaplingZaddr)) {
|
||||
if (sproutCandidateNotes.size() > 0 && isToSaplingZaddr) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Cannot send between Sprout and Sapling addresses using z_mergetoaddress");
|
||||
}
|
||||
|
||||
// Find unspent notes and update estimated size
|
||||
for (const SproutNoteEntry& entry : sproutEntries) {
|
||||
for (const SproutNoteEntry& entry : sproutCandidateNotes) {
|
||||
noteCounter++;
|
||||
CAmount nValue = entry.note.value();
|
||||
|
||||
if (!maxedOutNotesFlag) {
|
||||
// If we haven't added any notes yet and the merge is to a
|
||||
// z-address, we have already accounted for the first JoinSplit.
|
||||
size_t increase = (sproutNoteInputs.empty() && !isToSproutZaddr) || (sproutNoteInputs.size() % 2 == 0) ?
|
||||
size_t increase = allInputs.sproutNoteEntries.empty() || allInputs.sproutNoteEntries.size() % 2 == 0 ?
|
||||
JOINSPLIT_SIZE(SAPLING_TX_VERSION) : 0;
|
||||
if (estimatedTxSize + increase >= max_tx_size ||
|
||||
(sproutNoteLimit > 0 && noteCounter > sproutNoteLimit))
|
||||
(sproutNoteLimit > 0 && noteCounter > sproutNoteLimit) ||
|
||||
(shieldedNoteLimit > 0 && noteCounter > shieldedNoteLimit))
|
||||
{
|
||||
maxedOutNotesFlag = true;
|
||||
} else {
|
||||
|
@ -5681,7 +5689,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
auto zaddr = entry.address;
|
||||
SproutSpendingKey zkey;
|
||||
pwalletMain->GetSproutSpendingKey(zaddr, zkey);
|
||||
sproutNoteInputs.emplace_back(entry.jsop, entry.note, nValue, zkey);
|
||||
allInputs.sproutNoteEntries.push_back(entry);
|
||||
mergedNoteValue += nValue;
|
||||
}
|
||||
}
|
||||
|
@ -5691,13 +5699,14 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
}
|
||||
}
|
||||
|
||||
for (const SaplingNoteEntry& entry : saplingEntries) {
|
||||
for (const SaplingNoteEntry& entry : saplingCandidateNotes) {
|
||||
noteCounter++;
|
||||
CAmount nValue = entry.note.value();
|
||||
if (!maxedOutNotesFlag) {
|
||||
size_t increase = SPENDDESCRIPTION_SIZE;
|
||||
if (estimatedTxSize + increase >= max_tx_size ||
|
||||
(saplingNoteLimit > 0 && noteCounter > saplingNoteLimit))
|
||||
(saplingNoteLimit > 0 && noteCounter > saplingNoteLimit) ||
|
||||
(shieldedNoteLimit > 0 && noteCounter > shieldedNoteLimit))
|
||||
{
|
||||
maxedOutNotesFlag = true;
|
||||
} else {
|
||||
|
@ -5706,7 +5715,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
if (!pwalletMain->GetSaplingExtendedSpendingKey(entry.address, extsk)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Could not find spending key for payment address.");
|
||||
}
|
||||
saplingNoteInputs.emplace_back(entry.op, entry.note, nValue, extsk.expsk);
|
||||
allInputs.saplingNoteEntries.push_back(entry);
|
||||
mergedNoteValue += nValue;
|
||||
}
|
||||
}
|
||||
|
@ -5717,8 +5726,8 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
}
|
||||
}
|
||||
|
||||
size_t numUtxos = utxoInputs.size();
|
||||
size_t numNotes = sproutNoteInputs.size() + saplingNoteInputs.size();
|
||||
size_t numUtxos = allInputs.utxos.size();
|
||||
size_t numNotes = allInputs.sproutNoteEntries.size() + allInputs.saplingNoteEntries.size();
|
||||
|
||||
if (numUtxos == 0 && numNotes == 0) {
|
||||
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any funds to merge.");
|
||||
|
@ -5751,45 +5760,80 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
|
|||
contextInfo.pushKV("toaddress", params[1]);
|
||||
contextInfo.pushKV("fee", ValueFromAmount(nFee));
|
||||
|
||||
if (!sproutNoteInputs.empty() || !saplingNoteInputs.empty() || !isToTaddr) {
|
||||
// We have shielded inputs or the recipient is a shielded address, and
|
||||
// therefore we cannot create transactions before Sapling activates.
|
||||
if (!saplingActive) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER, "Cannot create shielded transactions before Sapling has activated");
|
||||
}
|
||||
}
|
||||
// The privacy policy is determined early so as to be able to use it
|
||||
// for selector construction.
|
||||
auto strategy =
|
||||
ResolveTransactionStrategy(
|
||||
ReifyPrivacyPolicy(
|
||||
std::nullopt,
|
||||
params.size() > 6 ? std::optional(params[6].get_str()) : std::nullopt),
|
||||
InterpretLegacyCompat(std::nullopt, {recipient.first}));
|
||||
|
||||
bool isSproutShielded = sproutNoteInputs.size() > 0 || isToSproutZaddr;
|
||||
bool isSproutShielded = allInputs.sproutNoteEntries.size() > 0;
|
||||
// Contextual transaction we will build on
|
||||
CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(
|
||||
Params().GetConsensus(),
|
||||
chainparams.GetConsensus(),
|
||||
nextBlockHeight,
|
||||
isSproutShielded || nPreferredTxVersion < ZIP225_MIN_TX_VERSION);
|
||||
if (contextualTx.nVersion == 1 && isSproutShielded) {
|
||||
contextualTx.nVersion = 2; // Tx format should support vJoinSplit
|
||||
}
|
||||
|
||||
// Builder (used if Sapling addresses are involved)
|
||||
std::optional<TransactionBuilder> builder;
|
||||
if (isToSaplingZaddr || saplingNoteInputs.size() > 0) {
|
||||
if (isToSaplingZaddr || allInputs.saplingNoteEntries.size() > 0) {
|
||||
std::optional<uint256> orchardAnchor;
|
||||
if (!isSproutShielded && nPreferredTxVersion >= ZIP225_MIN_TX_VERSION && nAnchorConfirmations > 0) {
|
||||
// Allow Orchard recipients by setting an Orchard anchor.
|
||||
auto orchardAnchorHeight = nextBlockHeight - nAnchorConfirmations;
|
||||
if (Params().GetConsensus().NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
|
||||
if (chainparams.GetConsensus().NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
|
||||
auto anchorBlockIndex = chainActive[orchardAnchorHeight];
|
||||
assert(anchorBlockIndex != nullptr);
|
||||
orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
|
||||
}
|
||||
}
|
||||
builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain);
|
||||
}
|
||||
|
||||
WalletTxBuilder builder(Params(), minRelayTxFee);
|
||||
|
||||
// This currently isn’t too critical since we don’t yet use it for note selection and we never
|
||||
// need a change address. The one thing this does is indicate whether or not Sprout can be
|
||||
// selected. However, we eventually want a `ZTXOSelector` that can support this operation fully.
|
||||
// I.e., be able to select from multiple pools in the legacy account.
|
||||
std::optional<ZTXOSelector> ztxoSelector;
|
||||
if (isSproutShielded) {
|
||||
ztxoSelector = pwalletMain->ZTXOSelectorForAddress(
|
||||
allInputs.sproutNoteEntries[0].address,
|
||||
true,
|
||||
TransparentCoinbasePolicy::Disallow,
|
||||
strategy.AllowLinkingAccountAddresses());
|
||||
} else if (allInputs.saplingNoteEntries.size() > 0) {
|
||||
ztxoSelector = pwalletMain->ZTXOSelectorForAddress(
|
||||
allInputs.saplingNoteEntries[0].address,
|
||||
true,
|
||||
TransparentCoinbasePolicy::Disallow,
|
||||
strategy.AllowLinkingAccountAddresses());
|
||||
} else {
|
||||
ztxoSelector = CWallet::LegacyTransparentZTXOSelector(true, TransparentCoinbasePolicy::Disallow);
|
||||
}
|
||||
|
||||
if (!ztxoSelector.has_value()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Missing spending key for an address to be merged.");
|
||||
}
|
||||
|
||||
// Create operation and add to global queue
|
||||
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> operation(
|
||||
new AsyncRPCOperation_mergetoaddress(
|
||||
std::move(builder), contextualTx, utxoInputs, sproutNoteInputs, saplingNoteInputs, recipient, nFee, contextInfo) );
|
||||
auto mergeOp = new AsyncRPCOperation_mergetoaddress(
|
||||
std::move(builder),
|
||||
ztxoSelector.value(),
|
||||
allInputs,
|
||||
recipient,
|
||||
strategy,
|
||||
nFee,
|
||||
contextInfo);
|
||||
mergeOp->prepare(chainparams, *pwalletMain).map_error([&](const InputSelectionError& err) {
|
||||
ThrowInputSelectionError(err, ztxoSelector.value(), strategy);
|
||||
});
|
||||
|
||||
std::shared_ptr<AsyncRPCOperation> operation(mergeOp);
|
||||
q->addOperation(operation);
|
||||
AsyncRPCOperationId operationId = operation->getId();
|
||||
|
||||
|
|
|
@ -69,6 +69,13 @@ private:
|
|||
fs::path old_cwd;
|
||||
};
|
||||
|
||||
CWalletTx FakeWalletTx() {
|
||||
CMutableTransaction mtx;
|
||||
mtx.vout.resize(1);
|
||||
mtx.vout[0].nValue = 1;
|
||||
return CWalletTx(nullptr, mtx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static UniValue ValueFromString(const std::string &str)
|
||||
|
@ -1620,7 +1627,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters)
|
|||
std::fill(v.begin(),v.end(), 'A');
|
||||
std::string badmemo(v.begin(), v.end());
|
||||
CheckRPCThrows("z_mergetoaddress [\"" + taddr1 + "\"] " + aSproutAddr + " 0.00001 100 100 " + badmemo,
|
||||
"Invalid parameter, size of memo is larger than maximum allowed 512");
|
||||
strprintf("Invalid parameter, memo is longer than the maximum allowed %d characters.", ZC_MEMO_SIZE));
|
||||
|
||||
// Mutable tx containing contextual information we need to build tx
|
||||
UniValue retValue = CallRPC("getblockcount");
|
||||
|
@ -1631,42 +1638,36 @@ BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters)
|
|||
KeyIO keyIO(Params());
|
||||
MergeToAddressRecipient testnetzaddr(
|
||||
keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value(),
|
||||
"testnet memo");
|
||||
Memo());
|
||||
WalletTxBuilder builder(Params(), minRelayTxFee);
|
||||
auto selector = CWallet::LegacyTransparentZTXOSelector(
|
||||
true,
|
||||
TransparentCoinbasePolicy::Disallow);
|
||||
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients);
|
||||
|
||||
try {
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, {}, {}, {}, testnetzaddr, -1 ));
|
||||
BOOST_FAIL("Should have caused an error");
|
||||
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, {}, testnetzaddr, strategy, -1);
|
||||
BOOST_FAIL("Fee value of -1 expected to be out of the valid range of values.");
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "Fee is out of range"));
|
||||
}
|
||||
|
||||
try {
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, {}, {}, {}, testnetzaddr, 1));
|
||||
BOOST_FAIL("Should have caused an error");
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "No inputs"));
|
||||
{
|
||||
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, {}, testnetzaddr, strategy, 1);
|
||||
operation.main();
|
||||
BOOST_CHECK_EQUAL(operation.getErrorMessage(), "Insufficient funds: have -0.00000001, need 0.00000001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.");
|
||||
}
|
||||
|
||||
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{ COutPoint{uint256(), 0}, 0, CScript()} };
|
||||
auto wtx = FakeWalletTx();
|
||||
SpendableInputs inputs;
|
||||
inputs.utxos.emplace_back(COutput(&wtx, 0, 100, true));
|
||||
inputs.sproutNoteEntries.emplace_back(SproutNoteEntry {JSOutPoint(), SproutPaymentAddress(), SproutNote(), "", 0});
|
||||
inputs.saplingNoteEntries.emplace_back(SaplingNoteEntry {SaplingOutPoint(), SaplingPaymentAddress(), SaplingNote({}, uint256(), 0, uint256(), Zip212Enabled::BeforeZip212), "", 0});
|
||||
|
||||
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs =
|
||||
{MergeToAddressInputSproutNote{JSOutPoint(), SproutNote(), 0, SproutSpendingKey()}};
|
||||
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs =
|
||||
{MergeToAddressInputSaplingNote{SaplingOutPoint(), SaplingNote({}, uint256(), 0, uint256(), Zip212Enabled::BeforeZip212), 0, SaplingExpandedSpendingKey()}};
|
||||
|
||||
// Sprout and Sapling inputs -> throw
|
||||
try {
|
||||
auto operation = new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, sproutNoteInputs, saplingNoteInputs, testnetzaddr, 1);
|
||||
BOOST_FAIL("Should have caused an error");
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK(find_error(objError, "Cannot send from both Sprout and Sapling addresses using z_mergetoaddress"));
|
||||
}
|
||||
// Sprout inputs and TransactionBuilder -> throw
|
||||
try {
|
||||
auto operation = new AsyncRPCOperation_mergetoaddress(TransactionBuilder(), mtx, inputs, sproutNoteInputs, {}, testnetzaddr, 1);
|
||||
BOOST_FAIL("Should have caused an error");
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK(find_error(objError, "Sprout notes are not supported by the TransactionBuilder"));
|
||||
{
|
||||
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, inputs, testnetzaddr, strategy, 1);
|
||||
operation.main();
|
||||
BOOST_CHECK_EQUAL(operation.getErrorMessage(), "Insufficient funds: have 0.00, need 0.00000001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ using namespace libzcash;
|
|||
int GetAnchorHeight(const CChain& chain, uint32_t anchorConfirmations)
|
||||
{
|
||||
int nextBlockHeight = chain.Height() + 1;
|
||||
return nextBlockHeight - anchorConfirmations;
|
||||
return std::max(0, nextBlockHeight - (int) anchorConfirmations);
|
||||
}
|
||||
|
||||
static size_t PadCount(size_t n)
|
||||
|
|
|
@ -49,7 +49,7 @@ public:
|
|||
CAmount amount,
|
||||
std::optional<Memo> memo) :
|
||||
address(address), amount(amount), memo(memo) {
|
||||
assert(amount >= 0);
|
||||
assert(MoneyRange(amount));
|
||||
}
|
||||
|
||||
const PaymentAddress& GetAddress() const {
|
||||
|
|
Loading…
Reference in New Issue