You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
679 lines
24 KiB
679 lines
24 KiB
//
|
|
// monero_transfer_utils.cpp
|
|
// Copyright © 2018 MyMonero. All rights reserved.
|
|
//
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without modification, are
|
|
// permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
|
// conditions and the following disclaimer.
|
|
//
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
|
// of conditions and the following disclaimer in the documentation and/or other
|
|
// materials provided with the distribution.
|
|
//
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
|
// used to endorse or promote products derived from this software without specific
|
|
// prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
//
|
|
//
|
|
//
|
|
//
|
|
#include "monero_transfer_utils.hpp"
|
|
#include "wallet_errors.h"
|
|
#include "string_tools.h"
|
|
#include "monero_paymentID_utils.hpp"
|
|
//
|
|
using namespace std;
|
|
using namespace crypto;
|
|
using namespace std;
|
|
using namespace boost;
|
|
using namespace epee;
|
|
using namespace cryptonote;
|
|
using namespace tools; // for error::
|
|
using namespace monero_transfer_utils;
|
|
using namespace monero_fork_rules;
|
|
|
|
//
|
|
// Protocol / Defaults
|
|
uint32_t monero_transfer_utils::fixed_ringsize()
|
|
{
|
|
return 7; // best practice is to conform to fixed default ring size
|
|
}
|
|
uint32_t monero_transfer_utils::fixed_mixinsize()
|
|
{
|
|
return monero_transfer_utils::fixed_ringsize() - 1;
|
|
}
|
|
uint32_t monero_transfer_utils::default_priority()
|
|
{
|
|
return 1; // lowest (TODO: declare centrally)
|
|
}
|
|
//
|
|
// Fee estimation
|
|
uint64_t monero_transfer_utils::estimated_tx_network_fee(
|
|
uint64_t fee_per_kb,
|
|
uint32_t priority,
|
|
use_fork_rules_fn_type use_fork_rules_fn
|
|
) {
|
|
bool bulletproof = use_fork_rules_fn(get_bulletproof_fork(), 0);
|
|
uint64_t fee_multiplier = get_fee_multiplier(priority, default_priority(), get_fee_algorithm(use_fork_rules_fn), use_fork_rules_fn);
|
|
std::vector<uint8_t> extra; // blank extra
|
|
size_t est_tx_size = estimate_rct_tx_size(2, fixed_mixinsize(), 2, extra.size(), bulletproof); // typically ~14kb post-rct, pre-bulletproofs
|
|
uint64_t estimated_fee = calculate_fee(fee_per_kb, est_tx_size, fee_multiplier);
|
|
//
|
|
return estimated_fee;
|
|
}
|
|
uint64_t monero_transfer_utils::get_upper_transaction_size_limit(
|
|
uint64_t upper_transaction_size_limit__or_0_for_default,
|
|
use_fork_rules_fn_type use_fork_rules_fn
|
|
) {
|
|
if (upper_transaction_size_limit__or_0_for_default > 0)
|
|
return upper_transaction_size_limit__or_0_for_default;
|
|
uint64_t full_reward_zone = use_fork_rules_fn(5, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : use_fork_rules_fn(2, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 : CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1;
|
|
return full_reward_zone - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE;
|
|
}
|
|
uint64_t monero_transfer_utils::get_fee_multiplier(
|
|
uint32_t priority,
|
|
uint32_t default_priority,
|
|
int fee_algorithm,
|
|
use_fork_rules_fn_type use_fork_rules_fn
|
|
) {
|
|
static const uint64_t old_multipliers[3] = {1, 2, 3};
|
|
static const uint64_t new_multipliers[3] = {1, 20, 166};
|
|
static const uint64_t newer_multipliers[4] = {1, 4, 20, 166};
|
|
|
|
if (fee_algorithm == -1)
|
|
fee_algorithm = get_fee_algorithm(use_fork_rules_fn);
|
|
|
|
// 0 -> default (here, x1 till fee algorithm 2, x4 from it)
|
|
if (priority == 0)
|
|
priority = default_priority;
|
|
if (priority == 0)
|
|
{
|
|
if (fee_algorithm >= 2)
|
|
priority = 2;
|
|
else
|
|
priority = 1;
|
|
}
|
|
|
|
// 1 to 3/4 are allowed as priorities
|
|
uint32_t max_priority = (fee_algorithm >= 2) ? 4 : 3;
|
|
if (priority >= 1 && priority <= max_priority)
|
|
{
|
|
switch (fee_algorithm)
|
|
{
|
|
case 0: return old_multipliers[priority-1];
|
|
case 1: return new_multipliers[priority-1];
|
|
case 2: return newer_multipliers[priority-1];
|
|
default: THROW_WALLET_EXCEPTION_IF (true, error::invalid_priority);
|
|
}
|
|
}
|
|
|
|
THROW_WALLET_EXCEPTION_IF (false, error::invalid_priority);
|
|
return 1;
|
|
}
|
|
int monero_transfer_utils::get_fee_algorithm(use_fork_rules_fn_type use_fork_rules_fn)
|
|
{
|
|
// changes at v3 and v5
|
|
if (use_fork_rules_fn(5, 0))
|
|
return 2;
|
|
if (use_fork_rules_fn(3, -720 * 14))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
size_t monero_transfer_utils::estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof)
|
|
{
|
|
size_t size = 0;
|
|
|
|
// tx prefix
|
|
|
|
// first few bytes
|
|
size += 1 + 6;
|
|
|
|
// vin
|
|
size += n_inputs * (1+6+(mixin+1)*2+32);
|
|
|
|
// vout
|
|
size += n_outputs * (6+32);
|
|
|
|
// extra
|
|
size += extra_size;
|
|
|
|
// rct signatures
|
|
|
|
// type
|
|
size += 1;
|
|
|
|
// rangeSigs
|
|
if (bulletproof)
|
|
size += ((2*6 + 4 + 5)*32 + 3) * n_outputs;
|
|
else
|
|
size += (2*64*32+32+64*32) * n_outputs;
|
|
|
|
// MGs
|
|
size += n_inputs * (64 * (mixin+1) + 32);
|
|
|
|
// mixRing - not serialized, can be reconstructed
|
|
/* size += 2 * 32 * (mixin+1) * n_inputs; */
|
|
|
|
// pseudoOuts
|
|
size += 32 * n_inputs;
|
|
// ecdhInfo
|
|
size += 2 * 32 * n_outputs;
|
|
// outPk - only commitment is saved
|
|
size += 32 * n_outputs;
|
|
// txnFee
|
|
size += 4;
|
|
|
|
LOG_PRINT_L2("estimated rct tx size for " << n_inputs << " with ring size " << (mixin+1) << " and " << n_outputs << ": " << size << " (" << ((32 * n_inputs/*+1*/) + 2 * 32 * (mixin+1) * n_inputs + 32 * n_outputs) << " saved)");
|
|
return size;
|
|
}
|
|
size_t monero_transfer_utils::estimate_tx_size(bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof)
|
|
{
|
|
if (use_rct)
|
|
return estimate_rct_tx_size(n_inputs, mixin, n_outputs + 1, extra_size, bulletproof);
|
|
else
|
|
return n_inputs * (mixin+1) * APPROXIMATE_INPUT_BYTES + extra_size;
|
|
}
|
|
//
|
|
uint64_t monero_transfer_utils::calculate_fee(uint64_t fee_per_kb, size_t bytes, uint64_t fee_multiplier)
|
|
{
|
|
uint64_t kB = (bytes + 1023) / 1024;
|
|
//
|
|
return kB * fee_per_kb * fee_multiplier;
|
|
}
|
|
uint64_t monero_transfer_utils::calculate_fee(uint64_t fee_per_kb, const cryptonote::blobdata &blob, uint64_t fee_multiplier)
|
|
{
|
|
return calculate_fee(fee_per_kb, blob.size(), fee_multiplier);
|
|
}
|
|
//
|
|
// Transfer parsing/derived properties
|
|
bool monero_transfer_utils::is_transfer_unlocked(
|
|
uint64_t unlock_time,
|
|
uint64_t block_height,
|
|
uint64_t blockchain_size, /* extracting wallet2->m_blockchain.size() / m_local_bc_height */
|
|
network_type nettype
|
|
) {
|
|
if(!is_tx_spendtime_unlocked(unlock_time, block_height, blockchain_size, nettype))
|
|
return false;
|
|
|
|
if(block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > blockchain_size)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
bool monero_transfer_utils::is_tx_spendtime_unlocked(
|
|
uint64_t unlock_time,
|
|
uint64_t block_height,
|
|
uint64_t blockchain_size,
|
|
network_type nettype
|
|
) {
|
|
if(unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER)
|
|
{
|
|
//interpret as block index
|
|
if(blockchain_size-1 + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS >= unlock_time)
|
|
return true;
|
|
else
|
|
return false;
|
|
}else
|
|
{
|
|
//interpret as time
|
|
uint64_t current_time = static_cast<uint64_t>(time(NULL));
|
|
// XXX: this needs to be fast, so we'd need to get the starting heights
|
|
// from the daemon to be correct once voting kicks in
|
|
uint64_t v2height = nettype == TESTNET ? 624634 : nettype == STAGENET ? (uint64_t)-1/*TODO*/ : 1009827;
|
|
uint64_t leeway = block_height < v2height ? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 : CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2;
|
|
if(current_time + leeway >= unlock_time)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
//
|
|
// Constructing transactions
|
|
std::string monero_transfer_utils::new_dummy_address_string_for_rct_tx(network_type nettype)
|
|
{
|
|
cryptonote::account_base account;
|
|
account.generate();
|
|
//
|
|
return account.get_public_address_str(nettype);
|
|
}
|
|
bool _rct_hex_to_rct_commit(
|
|
const std::string &rct_string,
|
|
rct::key &rct_commit
|
|
) {
|
|
// rct string is empty if output is non RCT
|
|
if (rct_string.empty()) {
|
|
return false;
|
|
}
|
|
// rct_string is a string with length 64+64+64 (<rct commit> + <encrypted mask> + <rct amount>)
|
|
std::string rct_commit_str = rct_string.substr(0,64);
|
|
THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, rct_commit_str), error::wallet_internal_error, "Invalid rct commit hash: " + rct_commit_str);
|
|
string_tools::hex_to_pod(rct_commit_str, rct_commit);
|
|
return true;
|
|
}
|
|
bool _rct_hex_to_decrypted_mask(
|
|
const std::string &rct_string,
|
|
const crypto::secret_key &view_secret_key,
|
|
const crypto::public_key& tx_pub_key,
|
|
uint64_t internal_output_index,
|
|
rct::key &decrypted_mask
|
|
) {
|
|
// rct string is empty if output is non RCT
|
|
if (rct_string.empty()) {
|
|
return false;
|
|
}
|
|
// rct_string is a string with length 64+64+64 (<rct commit> + <encrypted mask> + <rct amount>)
|
|
rct::key encrypted_mask;
|
|
std::string encrypted_mask_str = rct_string.substr(64,64);
|
|
THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, encrypted_mask_str), error::wallet_internal_error, "Invalid rct mask: " + encrypted_mask_str);
|
|
string_tools::hex_to_pod(encrypted_mask_str, encrypted_mask);
|
|
//
|
|
// Decrypt the mask
|
|
crypto::key_derivation derivation;
|
|
bool r = generate_key_derivation(tx_pub_key, view_secret_key, derivation);
|
|
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation");
|
|
crypto::secret_key scalar;
|
|
crypto::derivation_to_scalar(derivation, internal_output_index, scalar);
|
|
sc_sub(decrypted_mask.bytes,encrypted_mask.bytes,rct::hash_to_scalar(rct::sk2rct(scalar)).bytes);
|
|
|
|
return true;
|
|
}
|
|
bool _verify_sec_key(const crypto::secret_key &secret_key, const crypto::public_key &public_key)
|
|
{ // borrowed from device_default.cpp
|
|
crypto::public_key calculated_pub;
|
|
bool r = crypto::secret_key_to_public_key(secret_key, calculated_pub);
|
|
return r && public_key == calculated_pub;
|
|
}
|
|
//
|
|
void monero_transfer_utils::create_transaction(
|
|
TransactionConstruction_RetVals &retVals,
|
|
const account_keys& sender_account_keys, // this will reference a particular hw::device
|
|
const uint32_t subaddr_account_idx,
|
|
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses,
|
|
const account_public_address &to_addr,
|
|
uint64_t sending_amount,
|
|
uint64_t change_amount,
|
|
uint64_t fee_amount,
|
|
vector<SpendableOutput> &outputs,
|
|
vector<RandomAmountOutputs> &mix_outs,
|
|
std::vector<uint8_t> &extra,
|
|
use_fork_rules_fn_type use_fork_rules_fn,
|
|
uint64_t unlock_time, // or 0
|
|
bool rct,
|
|
cryptonote::network_type nettype
|
|
) {
|
|
retVals.errCode = noError;
|
|
//
|
|
// TODO: do we need to sort destinations by amount, here, according to 'decompose_destinations'?
|
|
//
|
|
uint32_t fake_outputs_count = fixed_mixinsize();
|
|
bool bulletproof = use_fork_rules_fn(get_bulletproof_fork(), 0);
|
|
//
|
|
if (mix_outs.size() != outputs.size() && fake_outputs_count != 0) {
|
|
retVals.errCode = wrongNumberOfMixOutsProvided;
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < mix_outs.size(); i++) {
|
|
if (mix_outs[i].outputs.size() < fake_outputs_count) {
|
|
retVals.errCode = notEnoughOutputsForMixing;
|
|
return;
|
|
}
|
|
}
|
|
if (!sender_account_keys.get_device().verify_keys(sender_account_keys.m_spend_secret_key, sender_account_keys.m_account_address.m_spend_public_key)
|
|
|| !sender_account_keys.get_device().verify_keys(sender_account_keys.m_view_secret_key, sender_account_keys.m_account_address.m_view_public_key)) {
|
|
retVals.errCode = invalidSecretKeys;
|
|
return;
|
|
}
|
|
uint64_t needed_money = sending_amount + change_amount + fee_amount; // TODO: is this correct?
|
|
if (needed_money > UINT64_MAX) {
|
|
retVals.errCode = outputAmountOverflow;
|
|
return;
|
|
}
|
|
//
|
|
uint64_t found_money = 0;
|
|
std::vector<tx_source_entry> sources;
|
|
// TODO: log: "Selected transfers: " << outputs
|
|
for (size_t out_index = 0; out_index < outputs.size(); out_index++) {
|
|
found_money += outputs[out_index].amount;
|
|
if (found_money > UINT64_MAX) {
|
|
retVals.errCode = inputAmountOverflow;
|
|
}
|
|
auto src = tx_source_entry{};
|
|
src.amount = outputs[out_index].amount;
|
|
src.rct = outputs[out_index].rct != none;
|
|
//
|
|
typedef cryptonote::tx_source_entry::output_entry tx_output_entry;
|
|
if (mix_outs.size() != 0) {
|
|
// Sort fake outputs by global index
|
|
std::sort(mix_outs[out_index].outputs.begin(), mix_outs[out_index].outputs.end(), [] (
|
|
RandomAmountOutput const& a,
|
|
RandomAmountOutput const& b
|
|
) {
|
|
return a.global_index < b.global_index;
|
|
});
|
|
for (
|
|
size_t j = 0;
|
|
src.outputs.size() < fake_outputs_count && j < mix_outs[out_index].outputs.size();
|
|
j++
|
|
) {
|
|
auto mix_out__output = mix_outs[out_index].outputs[j];
|
|
if (mix_out__output.global_index == outputs[out_index].global_index) {
|
|
LOG_PRINT_L2("got mixin the same as output, skipping");
|
|
continue;
|
|
}
|
|
auto oe = tx_output_entry{};
|
|
oe.first = mix_out__output.global_index;
|
|
//
|
|
crypto::public_key public_key = AUTO_VAL_INIT(public_key);
|
|
if(!string_tools::validate_hex(64, mix_out__output.public_key)) {
|
|
retVals.errCode = givenAnInvalidPubKey;
|
|
return;
|
|
}
|
|
string_tools::hex_to_pod(mix_out__output.public_key, public_key);
|
|
oe.second.dest = rct::pk2rct(public_key);
|
|
//
|
|
if (mix_out__output.rct != boost::none) {
|
|
rct::key commit;
|
|
_rct_hex_to_rct_commit(*mix_out__output.rct, commit);
|
|
oe.second.mask = commit;
|
|
} else {
|
|
if (outputs[out_index].rct != boost::none) {
|
|
retVals.errCode = mixRCTOutsMissingCommit;
|
|
return;
|
|
}
|
|
oe.second.mask = rct::zeroCommit(src.amount); //create identity-masked commitment for non-rct mix input
|
|
}
|
|
src.outputs.push_back(oe);
|
|
}
|
|
}
|
|
auto real_oe = tx_output_entry{};
|
|
real_oe.first = outputs[out_index].global_index;
|
|
//
|
|
crypto::public_key public_key = AUTO_VAL_INIT(public_key);
|
|
if(!string_tools::validate_hex(64, outputs[out_index].public_key)) {
|
|
retVals.errCode = givenAnInvalidPubKey;
|
|
return;
|
|
}
|
|
if (!string_tools::hex_to_pod(outputs[out_index].public_key, public_key)) {
|
|
retVals.errCode = givenAnInvalidPubKey;
|
|
return;
|
|
}
|
|
real_oe.second.dest = rct::pk2rct(public_key);
|
|
//
|
|
if (outputs[out_index].rct != none) {
|
|
rct::key commit;
|
|
_rct_hex_to_rct_commit(*(outputs[out_index].rct), commit);
|
|
real_oe.second.mask = commit; //add commitment for real input
|
|
} else {
|
|
real_oe.second.mask = rct::zeroCommit(src.amount/*aka outputs[out_index].amount*/); //create identity-masked commitment for non-rct input
|
|
}
|
|
//
|
|
// Add real_oe to outputs
|
|
uint64_t real_output_index = src.outputs.size();
|
|
for (size_t j = 0; j < src.outputs.size(); j++) {
|
|
if (real_oe.first < src.outputs[j].first) {
|
|
real_output_index = j;
|
|
break;
|
|
}
|
|
}
|
|
src.outputs.insert(src.outputs.begin() + real_output_index, real_oe);
|
|
//
|
|
crypto::public_key tx_pub_key = AUTO_VAL_INIT(tx_pub_key);
|
|
if(!string_tools::validate_hex(64, outputs[out_index].tx_pub_key)) {
|
|
retVals.errCode = givenAnInvalidPubKey;
|
|
return;
|
|
}
|
|
string_tools::hex_to_pod(outputs[out_index].tx_pub_key, tx_pub_key);
|
|
src.real_out_tx_key = tx_pub_key;
|
|
//
|
|
add_tx_pub_key_to_extra(extra, tx_pub_key); // TODO: is this necessary? it doesn't seem to affect is_out_to_acc checks..
|
|
//
|
|
src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(extra);
|
|
//
|
|
src.real_output = real_output_index;
|
|
uint64_t internal_output_index = outputs[out_index].index;
|
|
src.real_output_in_tx_index = internal_output_index;
|
|
//
|
|
src.rct = outputs[out_index].rct != boost::none && (*(outputs[out_index].rct)).empty() == false;
|
|
if (src.rct) {
|
|
rct::key decrypted_mask;
|
|
_rct_hex_to_decrypted_mask(
|
|
*(outputs[out_index].rct),
|
|
sender_account_keys.m_view_secret_key,
|
|
tx_pub_key,
|
|
internal_output_index,
|
|
decrypted_mask
|
|
);
|
|
src.mask = decrypted_mask;
|
|
// rct::key calculated_commit = rct::commit(outputs[out_index].amount, decrypted_mask);
|
|
// rct::key parsed_commit;
|
|
// _rct_hex_to_rct_commit(*(outputs[out_index].rct), parsed_commit);
|
|
// if (!(real_oe.second.mask == calculated_commit)) { // real_oe.second.mask==parsed_commit(outputs[out_index].rct)
|
|
// retVals.errCode = invalidCommitOrMaskOnOutputRCT;
|
|
// return;
|
|
// }
|
|
} else {
|
|
rct::key I;
|
|
rct::identity(I);
|
|
src.mask = I; // in JS MM code this was left as null for generate_key_image_helper_rct to, I think, fill in with identity I
|
|
}
|
|
// not doing multisig here yet
|
|
src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()});
|
|
sources.push_back(src);
|
|
}
|
|
//
|
|
// TODO: if this is a multisig wallet, create a list of multisig signers we can use
|
|
std::vector<cryptonote::tx_destination_entry> splitted_dsts;
|
|
tx_destination_entry to_dst = AUTO_VAL_INIT(to_dst);
|
|
to_dst.addr = to_addr;
|
|
to_dst.amount = sending_amount;
|
|
splitted_dsts.push_back(to_dst);
|
|
//
|
|
cryptonote::tx_destination_entry change_dst = AUTO_VAL_INIT(change_dst);
|
|
change_dst.amount = change_amount;
|
|
//
|
|
if (change_dst.amount == 0) {
|
|
if (splitted_dsts.size() == 1) {
|
|
// If the change is 0, send it to a random address, to avoid confusing
|
|
// the sender with a 0 amount output. We send a 0 amount in order to avoid
|
|
// letting the destination be able to work out which of the inputs is the
|
|
// real one in our rings
|
|
LOG_PRINT_L2("generating dummy address for 0 change");
|
|
cryptonote::account_base dummy;
|
|
dummy.generate();
|
|
change_dst.addr = dummy.get_keys().m_account_address;
|
|
LOG_PRINT_L2("generated dummy address for 0 change");
|
|
splitted_dsts.push_back(change_dst);
|
|
}
|
|
} else {
|
|
change_dst.addr = sender_account_keys.m_account_address;
|
|
splitted_dsts.push_back(change_dst);
|
|
}
|
|
//
|
|
// TODO: log: "sources: " << sources
|
|
if (found_money > needed_money) {
|
|
if (change_dst.amount != fee_amount) {
|
|
retVals.errCode = resultFeeNotEqualToGiven; // aka "early fee calculation != later"
|
|
return; // early
|
|
}
|
|
} else if (found_money < needed_money) {
|
|
retVals.errCode = needMoreMoneyThanFound; // TODO: return actual found_money and needed_money in generalized err params in return val
|
|
return;
|
|
}
|
|
cryptonote::transaction tx;
|
|
// TODO: need to initialize tx here?
|
|
//
|
|
auto sources_copy = sources;
|
|
crypto::secret_key tx_key;
|
|
std::vector<crypto::secret_key> additional_tx_keys;
|
|
bool r = cryptonote::construct_tx_and_get_tx_key(
|
|
sender_account_keys, subaddresses,
|
|
sources, splitted_dsts, change_dst.addr, extra,
|
|
tx, unlock_time, tx_key, additional_tx_keys,
|
|
true, bulletproof,
|
|
/*m_multisig ? &msout : */NULL
|
|
);
|
|
LOG_PRINT_L2("constructed tx, r="<<r);
|
|
if (!r) {
|
|
// TODO: return error::tx_not_constructed, sources, dsts, unlock_time, nettype
|
|
retVals.errCode = transactionNotConstructed;
|
|
return;
|
|
}
|
|
if (get_object_blobsize(tx) >= get_upper_transaction_size_limit(0, use_fork_rules_fn)) {
|
|
// TODO: return error::tx_too_big, tx, upper_transaction_size_limit
|
|
retVals.errCode = transactionTooBig;
|
|
return;
|
|
}
|
|
bool use_bulletproofs = !tx.rct_signatures.p.bulletproofs.empty();
|
|
THROW_WALLET_EXCEPTION_IF(use_bulletproofs != bulletproof, error::wallet_internal_error, "Expected tx use_bulletproofs to equal bulletproof flag");
|
|
//
|
|
retVals.tx = tx;
|
|
retVals.tx_key = tx_key;
|
|
retVals.additional_tx_keys = additional_tx_keys;
|
|
}
|
|
//
|
|
//
|
|
void monero_transfer_utils::convenience__create_transaction(
|
|
Convenience_TransactionConstruction_RetVals &retVals,
|
|
const string &from_address_string,
|
|
const string &sec_viewKey_string,
|
|
const string &sec_spendKey_string,
|
|
const string &to_address_string,
|
|
optional<string> payment_id_string,
|
|
uint64_t sending_amount,
|
|
uint64_t change_amount,
|
|
uint64_t fee_amount,
|
|
vector<SpendableOutput> &outputs,
|
|
vector<RandomAmountOutputs> &mix_outs,
|
|
use_fork_rules_fn_type use_fork_rules_fn,
|
|
uint64_t unlock_time,
|
|
network_type nettype
|
|
) {
|
|
retVals.errCode = noError;
|
|
//
|
|
cryptonote::address_parse_info from_addr_info;
|
|
THROW_WALLET_EXCEPTION_IF(!cryptonote::get_account_address_from_str(from_addr_info, nettype, from_address_string), error::wallet_internal_error, "Couldn't parse from-address");
|
|
cryptonote::account_keys account_keys;
|
|
{
|
|
account_keys.m_account_address = from_addr_info.address;
|
|
//
|
|
crypto::secret_key sec_viewKey;
|
|
THROW_WALLET_EXCEPTION_IF(!string_tools::hex_to_pod(sec_viewKey_string, sec_viewKey), error::wallet_internal_error, "Couldn't parse view key");
|
|
account_keys.m_view_secret_key = sec_viewKey;
|
|
//
|
|
crypto::secret_key sec_spendKey;
|
|
THROW_WALLET_EXCEPTION_IF(!string_tools::hex_to_pod(sec_spendKey_string, sec_spendKey), error::wallet_internal_error, "Couldn't parse spend key");
|
|
account_keys.m_spend_secret_key = sec_spendKey;
|
|
}
|
|
//
|
|
cryptonote::address_parse_info to_addr_info;
|
|
THROW_WALLET_EXCEPTION_IF(!cryptonote::get_account_address_from_str(to_addr_info, nettype, to_address_string), error::wallet_internal_error, "Couldn't parse to-address");
|
|
//
|
|
std::vector<uint8_t> extra;
|
|
bool payment_id_seen = false;
|
|
{ // Detect hash8 or hash32 char hex string as pid and configure 'extra' accordingly
|
|
bool r = false;
|
|
if (payment_id_string != none) {
|
|
crypto::hash payment_id;
|
|
r = monero_paymentID_utils::parse_long_payment_id(*payment_id_string, payment_id);
|
|
if (r) {
|
|
std::string extra_nonce;
|
|
cryptonote::set_payment_id_to_tx_extra_nonce(extra_nonce, payment_id);
|
|
r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce);
|
|
} else {
|
|
crypto::hash8 payment_id8;
|
|
r = monero_paymentID_utils::parse_short_payment_id(*payment_id_string, payment_id8);
|
|
if (r) {
|
|
std::string extra_nonce;
|
|
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id8);
|
|
r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce);
|
|
}
|
|
}
|
|
payment_id_seen = true;
|
|
} else {
|
|
if (to_addr_info.is_subaddress && payment_id_seen) {
|
|
retVals.errCode = cantUsePIDWithSubAddress; // Never use a subaddress with a payment ID
|
|
return;
|
|
}
|
|
if (to_addr_info.has_payment_id) {
|
|
if (payment_id_seen) {
|
|
retVals.errCode = nonZeroPIDWithIntAddress; // can't use int addr at same time as supplying manual pid
|
|
return;
|
|
}
|
|
if (to_addr_info.is_subaddress) {
|
|
THROW_WALLET_EXCEPTION_IF(false, error::wallet_internal_error, "Unexpected is_subaddress && has_payment_id"); // should never happen
|
|
return;
|
|
}
|
|
std::string extra_nonce;
|
|
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, to_addr_info.payment_id);
|
|
bool r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce);
|
|
if (!r) {
|
|
retVals.errCode = couldntSetPIDToTXExtra;
|
|
return;
|
|
}
|
|
payment_id_seen = true;
|
|
}
|
|
}
|
|
}
|
|
//
|
|
uint32_t subaddr_account_idx = 0;
|
|
std::unordered_map<crypto::public_key, cryptonote::subaddress_index> subaddresses;
|
|
subaddresses[account_keys.m_account_address.m_spend_public_key] = {0,0};
|
|
//
|
|
TransactionConstruction_RetVals actualCall_retVals;
|
|
create_transaction(
|
|
actualCall_retVals,
|
|
account_keys,
|
|
subaddr_account_idx,
|
|
subaddresses,
|
|
to_addr_info.address,
|
|
sending_amount,
|
|
change_amount,
|
|
fee_amount,
|
|
outputs,
|
|
mix_outs,
|
|
extra,
|
|
use_fork_rules_fn,
|
|
unlock_time,
|
|
true, // rct
|
|
nettype
|
|
);
|
|
if (actualCall_retVals.errCode != noError) {
|
|
retVals.errCode = actualCall_retVals.errCode; // pass-through
|
|
return; // already set the error
|
|
}
|
|
auto txBlob = t_serializable_object_to_blob(*actualCall_retVals.tx);
|
|
size_t txBlob_byteLength = txBlob.size();
|
|
// cout << "txBlob: " << txBlob << endl;
|
|
cout << "txBlob_byteLength: " << txBlob_byteLength << endl;
|
|
THROW_WALLET_EXCEPTION_IF(txBlob_byteLength <= 0, error::wallet_internal_error, "Expected tx blob byte length > 0");
|
|
//
|
|
// tx hash
|
|
retVals.tx_hash_string = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(*actualCall_retVals.tx));
|
|
// signed serialized tx
|
|
retVals.signed_serialized_tx_string = epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(*actualCall_retVals.tx));
|
|
// (concatenated) tx key
|
|
ostringstream oss;
|
|
{
|
|
oss << epee::string_tools::pod_to_hex(*actualCall_retVals.tx_key);
|
|
for (size_t i = 0; i < (*actualCall_retVals.additional_tx_keys).size(); ++i) {
|
|
oss << epee::string_tools::pod_to_hex((*actualCall_retVals.additional_tx_keys)[i]);
|
|
}
|
|
}
|
|
retVals.tx_key_string = oss.str();
|
|
}
|