Compare commits

...

14 Commits

  1. 1
      qa/pull-tester/rpc-tests.sh
  2. 58
      qa/rpc-tests/wallet_raw_shielded.py
  3. 1
      src/rpc/server.cpp
  4. 1
      src/rpc/server.h
  5. 243
      src/wallet/rpcwallet.cpp

1
qa/pull-tester/rpc-tests.sh

@ -34,6 +34,7 @@ testScripts=(
'wallet_addresses.py'
'wallet_sapling.py'
'wallet_listnotes.py'
'wallet_raw_shielded.py'
'listtransactions.py'
'mempool_resurrect_test.py'
'txn_doublespend.py'

58
qa/rpc-tests/wallet_raw_shielded.py

@ -0,0 +1,58 @@
#!/usr/bin/env python2
# Copyright (c) 2018 The Zcash developers
# Copyright (c) 2019 The Hush developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, start_nodes, wait_and_assert_operationid_status
from decimal import Decimal
class WalletRawShielded(BitcoinTestFramework):
def setup_nodes(self):
return start_nodes(4, self.options.tmpdir, [[
'-nuparams=5ba81b19:202', # Overwinter
'-nuparams=76b809bb:204', # Sapling
]] * 4)
def run_test(self):
# Current height = 200 -> Sprout
alice = self.nodes[0]
assert_equal(200, alice.getblockcount())
# test that we can create a sapling zaddr before sapling activates
zaddr = alice.z_getnewaddress('sapling')
# we've got lots of coinbase (taddr) but no shielded funds yet
assert_equal(0, Decimal(alice.z_gettotalbalance()['private']))
# Current height = 202 -> Overwinter. Default address type remains Sprout
alice.generate(2)
self.sync_all()
assert_equal(202, alice.getblockcount())
mining_addr = alice.listunspent()[0]['address']
# Shield coinbase funds
receive_amount_10 = Decimal('10.0') - Decimal('0.0001')
recipients = [{"address":zaddr, "amount":receive_amount_10}]
myopid = alice.z_sendmany(mining_addr, recipients)
txid1 = wait_and_assert_operationid_status(alice, myopid)
self.sync_all()
# Generate a block to confirm shield coinbase tx
alice.generate(1)
self.sync_all()
assert_equal(203, alice.getblockcount())
newzaddr = alice.z_getnewaddress('sapling')
# Simplest test: one input and one output, no change
inputs = [ {"txid":txid, "outindex": 0, "address": zaddr } ]
outputs = [ {"address":newzaddr, "amount": 9.9999 } ]
rawhex = alice.z_createrawtransaction(inputs,outputs)
print(rawhex)
if __name__ == '__main__':
WalletRawShielded().main()

1
src/rpc/server.cpp

@ -665,6 +665,7 @@ static const CRPCCommand vRPCCommands[] =
{ "wallet", "z_gettotalbalance", &z_gettotalbalance, false },
{ "wallet", "z_mergetoaddress", &z_mergetoaddress, false },
{ "wallet", "z_sendmany", &z_sendmany, false },
{ "wallet", "z_createrawtransaction", &z_createrawtransaction, true },
{ "wallet", "z_shieldcoinbase", &z_shieldcoinbase, false },
{ "wallet", "z_getoperationstatus", &z_getoperationstatus, true },
{ "wallet", "z_getoperationresult", &z_getoperationresult, true },

1
src/rpc/server.h

@ -492,6 +492,7 @@ extern UniValue z_getbalance(const UniValue& params, bool fHelp); // in rpcwalle
extern UniValue z_gettotalbalance(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_mergetoaddress(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_sendmany(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_createrawtransaction(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_shieldcoinbase(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_getoperationstatus(const UniValue& params, bool fHelp); // in rpcwallet.cpp
extern UniValue z_getoperationresult(const UniValue& params, bool fHelp); // in rpcwallet.cpp

243
src/wallet/rpcwallet.cpp

@ -1,5 +1,6 @@
// Copyright (c) 2010 Satoshi Nakamoto
// Copyright (c) 2009-2014 The Bitcoin Core developers
// Copyright (c) 2019 The Hush developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
@ -4528,6 +4529,248 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
return operationId;
}
UniValue z_createrawtransaction(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 2 || params.size() > 4)
throw runtime_error(
"z_createrawtransaction [{\"txid\":\"id\",\"outindex\":n,\"address\":\"address\"},...] {\"address\":\"address\",\"amount\":5.555,\"memo\":\"...\",...} ( locktime ) ( expiryheight )\n"
"\nCreate a raw shielded transaction, involving at least one shielded input or output. Amounts are decimal numbers with at most 8 digits of precision.\n"
"Returns hex-encoded raw transaction.\n"
"Note that the transaction's inputs are not signed, and it is not stored in the wallet or transmitted to the network.\n"
"\nArguments:\n"
"1. \"inputs\" (string, required) A json array of json objects, UTXOs or Sapling Notes\n"
" [\n"
" {\n"
" \"txid\":\"id\", (string, required) The transaction id\n"
" \"vout\":n (numeric, required) The transparent output number\n"
" \"sequence\":n (numeric, optional) The sequence number\n"
" },\n"
" {\n"
" \"txid\":\"id\", (string, required) The transaction id\n"
" \"outindex\":n (numeric, required) The JoinSplit output number (reported by z_listunspent)\n"
" \"address\":n (string, required) The address which owns the note\n"
" }\n"
" ,...\n"
" ]\n"
"2. \"outputs\" (string, required) A json array of json objects\n"
" [\n"
" {\n"
" \"address\":n (string, required) The address receiving funds\n"
" \"amount\":n (numeric, required) The amount to send\n"
" \"memo\":n (string, optional) Hex-encoded memo, shielded outputs only\n"
" }\n"
" ,...\n"
" ]\n"
"3. locktime (numeric, optional, default=0) Raw locktime. Non-0 value also locktime-activates inputs\n"
"4. expiryheight (numeric, optional, default=" + strprintf("%d", DEFAULT_TX_EXPIRY_DELTA) + ") Expiry height of transaction (if Overwinter is active)\n"
"\nResult:\n"
"\"transaction\" (string) hex string of the transaction\n"
"\nExamples\n"
+ HelpExampleCli("z_createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"outindex\\\":0}]\" \"{\\\"address\\\":\\\"zaddr\\\", \\\"amount\\\":5}\"")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
UniValue inputs = params[0].get_array();
UniValue outputs = params[1].get_array();
bool fromTaddr = false;
bool fromSapling = false;
uint32_t branchId = CurrentEpochBranchId(chainActive.Height(), Params().GetConsensus());
// Keep track of addresses to spot duplicates
set<std::string> setAddress;
// Recipients
std::vector<SendManyRecipient> taddrRecipients;
std::vector<SendManyRecipient> zaddrRecipients;
CAmount nTotalOut = 0;
if (inputs.size()==0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs array is empty.");
if (outputs.size()==0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, outputs array is empty.");
int nextBlockHeight = chainActive.Height() + 1;
TransactionBuilder builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, pwalletMain);
//builder.SetFee(minersFee);
// process inputs
for (const UniValue& input : inputs.getValues()) {
const UniValue& o = input.get_obj();
uint256 txid = ParseHashO(o, "txid");
const UniValue& vout_v = find_value(o, "vout");
const UniValue& outindex = find_value(o, "outindex");
const UniValue& amount = find_value(o, "amount");
const UniValue& addr = find_value(o, "address");
int nOutindex = outindex.get_int();
int nAmount = amount.get_int();
int nOutput = vout_v.get_int();
std::string fromAddress = addr.get_str();
if (!vout_v.isNum() && !outindex.isNum())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid input, must provide either vout or outindex");
if (vout_v.isNum() && outindex.isNum())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid input, cannot provide both vout and outindex");
// add input to transaction builder
if (nOutput) {
// transparent input
if (nOutput < 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout must be positive");
builder.AddTransparentInput(COutPoint(txid, nOutput), CScript(), nAmount);
} else if (nOutindex) {
// shielded input
if (nOutindex < 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, outindex must be positive");
SaplingExpandedSpendingKey expsk;
uint256 ovk;
auto address = DecodePaymentAddress(fromAddress);
if (!boost::apply_visitor(HaveSpendingKeyForPaymentAddress(pwalletMain), address)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address, no spending key found for zaddr");
}
SpendingKey spendingkey = boost::apply_visitor(GetSpendingKeyForPaymentAddress(pwalletMain), address).get();
auto sk = boost::get<libzcash::SaplingExtendedSpendingKey>(spendingkey);
expsk = sk.expsk;
ovk = expsk.full_viewing_key().ovk;
// Fetch Sapling anchor and witnesses
std::vector<SaplingOutPoint> ops;
std::vector<SaplingNote> notes;
uint256 anchor;
std::vector<SaplingNoteEntry> z_sapling_inputs;
for (auto t : z_sapling_inputs) {
ops.push_back(t.op);
notes.push_back(t.note);
}
std::vector<boost::optional<SaplingWitness>> witnesses;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->GetSaplingNoteWitnesses(ops, witnesses, anchor);
}
assert(builder.AddSaplingSpend(expsk, notes[0], anchor, witnesses[0].get()));
}
}
// Build the transaction
auto maybe_tx = builder.Build();
if (!maybe_tx) {
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build transaction.");
}
//auto tx = maybe_tx.get();
// process outputs
for (const UniValue& o : outputs.getValues()) {
if (!o.isObject())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object");
// sanity check, report error if unknown key-value pairs
for (const string& name_ : o.getKeys()) {
std::string s = name_;
if (s != "address" && s != "amount" && s!="memo")
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ")+s);
}
string address = find_value(o, "address").get_str();
bool isZaddr = false;
CTxDestination taddr = DecodeDestination(address);
if (!IsValidDestination(taddr)) {
auto res = DecodePaymentAddress(address);
if (IsValidPaymentAddress(res, branchId)) {
isZaddr = true;
bool toSapling = boost::get<libzcash::SaplingPaymentAddress>(&res) != nullptr;
bool toSprout = !toSapling;
if ( GetTime() > KOMODO_SAPLING_DEADLINE )
{
if ( toSprout )
throw JSONRPCError(RPC_INVALID_PARAMETER,"Sprout usage has expired");
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ")+address );
}
}
setAddress.insert(address);
UniValue memoValue = find_value(o, "memo");
string memo;
if (!memoValue.isNull()) {
memo = memoValue.get_str();
if (!isZaddr) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo cannot 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 ));
}
}
UniValue av = find_value(o, "amount");
CAmount nAmount = AmountFromValue( av );
if (nAmount < 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive");
if (isZaddr) {
zaddrRecipients.push_back( SendManyRecipient(address, nAmount, memo) );
} else {
taddrRecipients.push_back( SendManyRecipient(address, nAmount, memo) );
}
nTotalOut += nAmount;
}
CMutableTransaction mtx;
mtx.fOverwintered = true;
mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID;
mtx.nVersion = SAPLING_TX_VERSION;
unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING;
size_t txsize = 0;
for (int i = 0; i < zaddrRecipients.size(); i++) {
auto address = std::get<0>(zaddrRecipients[i]);
auto res = DecodePaymentAddress(address);
bool toSapling = boost::get<libzcash::SaplingPaymentAddress>(&res) != nullptr;
if (toSapling) {
mtx.vShieldedOutput.push_back(OutputDescription());
}
}
CTransaction tx(mtx);
txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion);
if (fromTaddr) {
txsize += CTXIN_SPEND_DUST_SIZE;
txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change
}
txsize += CTXOUT_REGULAR_SIZE * taddrRecipients.size();
if (txsize > max_tx_size) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", max_tx_size ));
}
// Minimum confirmations
int nMinDepth = 1;
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");
}
return EncodeHexTx( builder.Build().get() );
}
/**
When estimating the number of coinbase utxos we can shield in a single transaction:

Loading…
Cancel
Save