From e599578273842721460aeff91493ef6b591a5f1c Mon Sep 17 00:00:00 2001 From: Paul Shapiro Date: Mon, 6 Aug 2018 09:53:00 -0400 Subject: [PATCH] implemented monero_transfer_utils::create_transaction --- src/monero_transfer_utils.cpp | 307 +++++++++++++++++++++++++++++++++- src/monero_transfer_utils.hpp | 97 +++++++++-- test/test_all.cpp | 214 +++++++++++++++++++++++- 3 files changed, 603 insertions(+), 15 deletions(-) diff --git a/src/monero_transfer_utils.cpp b/src/monero_transfer_utils.cpp index 3988a8a..bc7db74 100644 --- a/src/monero_transfer_utils.cpp +++ b/src/monero_transfer_utils.cpp @@ -33,14 +33,19 @@ // #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 epee; +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 tools; // for error:: using namespace monero_transfer_utils; using namespace monero_fork_rules; + // // Protocol / Defaults uint32_t monero_transfer_utils::fixed_ringsize() @@ -246,3 +251,297 @@ std::string monero_transfer_utils::new_dummy_address_string_for_rct_tx(network_t // 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 ( + + ) + 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::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 &subaddresses, + const vector &dsts, + vector &outputs, + vector &mix_outs, + uint64_t fee_amount, + std::vector &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; // (does this need to be initialized?) + // + // TODO: 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 (dsts.size() == 0) { + retVals.errCode = noDestinations; + return; + } + 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 = 0; + for (size_t i = 0; i < dsts.size(); i++) { + needed_money += dsts[i].amount; + if (needed_money > UINT64_MAX) { + retVals.errCode = outputAmountOverflow; + return; + } + } + uint64_t found_money = 0; + std::vector 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 splitted_dsts = dsts; + cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); + change_dts.amount = found_money - needed_money; + // + /* This is commented because it's presently supplied by whoever is calling this function.... But there's a good argument for bringing it in, here, especially after MyMonero clients integrate with this code and soon, share an implementation of SendFunds() (the analog of create_transactions_2 + transfer_selected*) + if (change_dts.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_dts.addr = dummy.get_keys().m_account_address; + LOG_PRINT_L2("generated dummy address for 0 change"); + splitted_dsts.push_back(change_dts); + } + } else { + change_dts.addr = sender_account_keys.m_account_address; + splitted_dsts.push_back(change_dts); + } + */ + // + // TODO: log: "sources: " << sources + if (found_money > needed_money) { + if (change_dts.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 additional_tx_keys; + bool r = cryptonote::construct_tx_and_get_tx_key( + sender_account_keys, subaddresses, + sources, splitted_dsts, change_dts.addr, extra, + tx, unlock_time, tx_key, additional_tx_keys, + true, bulletproof, + /*m_multisig ? &msout : */NULL + ); + LOG_PRINT_L2("constructed tx, r="<= 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; +} diff --git a/src/monero_transfer_utils.hpp b/src/monero_transfer_utils.hpp index 3687ea4..8fdb154 100644 --- a/src/monero_transfer_utils.hpp +++ b/src/monero_transfer_utils.hpp @@ -32,11 +32,16 @@ #ifndef monero_transfer_utils_hpp #define monero_transfer_utils_hpp // +#include +// #include "string_tools.h" +// #include "crypto.h" #include "cryptonote_basic.h" #include "cryptonote_format_utils.h" #include "cryptonote_tx_utils.h" +#include "ringct/rctSigs.h" +// #include "monero_fork_rules.hpp" // using namespace tools; @@ -47,32 +52,104 @@ using namespace tools; // namespace monero_transfer_utils { + using namespace std; + using namespace boost; + using namespace cryptonote; + using namespace monero_fork_rules; + using namespace crypto; // - uint64_t get_upper_transaction_size_limit(uint64_t upper_transaction_size_limit__or_0_for_default, monero_fork_rules::use_fork_rules_fn_type use_fork_rules_fn); - uint64_t get_fee_multiplier(uint32_t priority, uint32_t default_priority, int fee_algorithm, monero_fork_rules::use_fork_rules_fn_type use_fork_rules_fn); - int get_fee_algorithm(monero_fork_rules::use_fork_rules_fn_type use_fork_rules_fn); + uint64_t get_upper_transaction_size_limit(uint64_t upper_transaction_size_limit__or_0_for_default, use_fork_rules_fn_type use_fork_rules_fn); + uint64_t get_fee_multiplier(uint32_t priority, uint32_t default_priority, int fee_algorithm, use_fork_rules_fn_type use_fork_rules_fn); + int get_fee_algorithm(use_fork_rules_fn_type use_fork_rules_fn); // uint64_t calculate_fee(uint64_t fee_per_kb, size_t bytes, uint64_t fee_multiplier); - uint64_t calculate_fee(uint64_t fee_per_kb, const cryptonote::blobdata &blob, uint64_t fee_multiplier); + uint64_t calculate_fee(uint64_t fee_per_kb, const blobdata &blob, uint64_t fee_multiplier); // size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof); size_t estimate_tx_size(bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof); uint64_t estimated_tx_network_fee( // convenience function for size + calc uint64_t fee_per_kb, uint32_t priority, // when priority=0, falls back to monero_transfer_utils::default_priority() - cryptonote::network_type nettype, - monero_fork_rules::use_fork_rules_fn_type use_fork_rules_fn // this is extracted to a function so that implementations can optionally query the daemon (although this presently implies that such a call remains blocking) + network_type nettype, + use_fork_rules_fn_type use_fork_rules_fn // this is extracted to a function so that implementations can optionally query the daemon (although this presently implies that such a call remains blocking) ); // - bool is_transfer_unlocked(uint64_t unlock_time, uint64_t block_height, uint64_t blockchain_size, cryptonote::network_type nettype = cryptonote::MAINNET); - bool is_tx_spendtime_unlocked(uint64_t unlock_time, uint64_t block_height, uint64_t blockchain_size, cryptonote::network_type nettype = cryptonote::MAINNET); + bool is_transfer_unlocked(uint64_t unlock_time, uint64_t block_height, uint64_t blockchain_size, network_type nettype = MAINNET); + bool is_tx_spendtime_unlocked(uint64_t unlock_time, uint64_t block_height, uint64_t blockchain_size, network_type nettype = MAINNET); // uint32_t fixed_ringsize(); // not mixinsize, which would be ringsize-1 uint32_t fixed_mixinsize(); // not ringsize, which would be mixinsize+1 + uint32_t default_priority(); // - std::string new_dummy_address_string_for_rct_tx(cryptonote::network_type nettype = cryptonote::MAINNET); + string new_dummy_address_string_for_rct_tx(network_type nettype = MAINNET); // - uint32_t default_priority(); + // Types - Arguments + struct SpendableOutput + { + uint64_t amount; + string public_key; + optional rct; + uint64_t global_index; + uint64_t index; + string tx_pub_key; + }; + struct RandomAmountOutput + { + uint64_t global_index; // this is, I believe, presently supplied as a string by the API, probably to avoid overflow + string public_key; + optional rct; + }; + struct RandomAmountOutputs + { + uint64_t amount; + vector outputs; + }; + // + // Types - Return value + enum CreateTransactionErrorCode // TODO: switch to enum class to fix namespacing + { // These codes have values for serialization + noError = 0, + // + noDestinations = 1, + wrongNumberOfMixOutsProvided = 2, + notEnoughOutputsForMixing = 3, + invalidSecretKeys = 4, + outputAmountOverflow = 5, + inputAmountOverflow = 6, + mixRCTOutsMissingCommit = 7, + resultFeeNotEqualToGiven = 8, + needMoreMoneyThanFound = 9, + invalidDestinationAddress = 10, + nonZeroPIDWithIntAddress = 11, + cantUsePIDWithSubAddress = 12, + couldntSetPIDToTXExtra = 13, + givenAnInvalidPubKey = 14, + invalidCommitOrMaskOnOutputRCT = 15, + transactionNotConstructed = 16, + transactionTooBig = 17 + }; + struct TransactionConstruction_RetVals + { + CreateTransactionErrorCode errCode; + // + cryptonote::transaction tx; + }; + // TODO: add priority + void create_transaction( + TransactionConstruction_RetVals &retVals, + const account_keys& sender_account_keys, // this will reference a particular hw::device + const uint32_t subaddr_account_idx, // pass 0 for no subaddrs + const std::unordered_map &subaddresses, + const vector &dsts, // presently, this must include change as well as, if necessary, dummy output + vector &outputs, + vector &mix_outs, + uint64_t fee_amount, + std::vector &extra, // this is not declared const b/c it may have the output tx pub key appended to it + use_fork_rules_fn_type use_fork_rules_fn, + uint64_t unlock_time = 0, // or 0 + bool rct = true, + network_type nettype = MAINNET + ); } #endif /* monero_transfer_utils_hpp */ diff --git a/test/test_all.cpp b/test/test_all.cpp index 3dc6ddf..6aa8b5b 100644 --- a/test/test_all.cpp +++ b/test/test_all.cpp @@ -42,7 +42,9 @@ #include using namespace std; #include "string_tools.h" -// using namespace boost; +using namespace epee; +using namespace boost; +#include "cryptonote_format_utils.h" // // Shared code // @@ -106,3 +108,213 @@ BOOST_AUTO_TEST_CASE(transfers__fee) std::cout << "est_fee with fee_per_kb " << fee_per_kb << ": " << est_fee << std::endl; BOOST_REQUIRE(est_fee > 0); } +BOOST_AUTO_TEST_CASE(transfers__create) +{ + using namespace monero_transfer_utils; + using namespace monero_fork_rules; + + TransactionConstruction_RetVals retVals; + monero_fork_rules::use_fork_rules_fn_type use_fork_rules_fn = [] (uint8_t version, int64_t early_blocks) -> bool + { + return monero_fork_rules::lightwallet_hardcoded__use_fork_rules(version, early_blocks); + }; + + cryptonote::network_type nettype = cryptonote::MAINNET; + + string from_addressString = "43zxvpcj5Xv9SEkNXbMCG7LPQStHMpFCQCmkmR4u5nzjWwq5Xkv5VmGgYEsHXg4ja2FGRD5wMWbBVMijDTqmmVqm93wHGkg"; + string sec_viewKey_string = "......"; + string sec_spendKey_string = "......"; + + cryptonote::address_parse_info from_addr_info; + BOOST_REQUIRE(cryptonote::get_account_address_from_str(from_addr_info, nettype, from_addressString)); + cryptonote::account_keys account_keys; + { + account_keys.m_account_address = from_addr_info.address; + // + crypto::secret_key sec_viewKey; + BOOST_REQUIRE(string_tools::hex_to_pod(sec_viewKey_string, sec_viewKey)); + account_keys.m_view_secret_key = sec_viewKey; + // + crypto::secret_key sec_spendKey; + BOOST_REQUIRE(string_tools::hex_to_pod(sec_spendKey_string, sec_spendKey)); + account_keys.m_spend_secret_key = sec_spendKey; + } + cout << "spend sec key: " << string_tools::pod_to_hex(account_keys.m_spend_secret_key) << endl; + cout << "spend pub key: " << string_tools::pod_to_hex(from_addr_info.address.m_spend_public_key) << endl; + // + optional payment_id_string = string("b79f8efc81f58f67"); + bool pid_encrypt = true; + uint64_t amount = 10000000000; + uint64_t fee_amount = 2167750000; + string to_address_string("43zxvpcj5Xv9SEkNXbMCG7LPQStHMpFCQCmkmR4u5nzjWwq5Xkv5VmGgYEsHXg4ja2FGRD5wMWbBVMijDTqmmVqm93wHGkg"); + cryptonote::address_parse_info to_addr_info; + BOOST_REQUIRE(cryptonote::get_account_address_from_str(to_addr_info, nettype, to_address_string)); + // + std::vector 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; + } + } + // + std::vector dsts; // without change this would normally require a dummy addr with 0 amount pushed as a fake 'change' output ... that should probably be moved into the function + { // 0. actual destination address + cryptonote::tx_destination_entry de; + de.addr = to_addr_info.address; + de.amount = amount; + de.is_subaddress = to_addr_info.is_subaddress; + if (to_addr_info.is_subaddress && payment_id_seen) { + BOOST_REQUIRE_MESSAGE(false, "Illegal: Never supply a pid with a subaddress."); // TODO: is this true? + return; + } + if (to_addr_info.has_payment_id) { + if (payment_id_seen) { + BOOST_REQUIRE(false); // can't use int addr at same time as supplying manual pid + return; + } + if (to_addr_info.is_subaddress) { + BOOST_REQUIRE(false); // should never happen .. logic err? + 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) { + BOOST_REQUIRE(false); + return; + } + payment_id_seen = true; + } + dsts.push_back(de); + } + { // 1. change + cryptonote::tx_destination_entry de; + de.addr = from_addr_info.address; + de.amount = 112832250000; + de.is_subaddress = from_addr_info.is_subaddress; // not + dsts.push_back(de); + } + // + vector outputs; + { // required info from output + auto out = SpendableOutput{}; + out.amount = 125000000000; + out.public_key = "596fa47b6b3905269503435099a05e3ede54564026c93cbe5285e2df074c7118"; + out.rct = "920ee8d99299f304d17fdb104720d1f62be0b03383c7bb466ff39c6a264d80d616ce1eccd6c4de1cc0fba87e463f2e0c373146c475e8a1517f36e7a37351d50034688cc8cb528c14188cae45d89b313d444e583c9d68a32cb80938a5e2aa200b"; + out.global_index = 6451664; + out.index = 0; + out.tx_pub_key = "0a86e588dc67ca11993737e003a9e60c57174a663a47495e3b1d764f486fc88f"; + outputs.push_back(out); + } + // + vector mix_outs; + { + auto amountAndOuts = RandomAmountOutputs{}; + amountAndOuts.amount = 0; + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 5260585; // this is, I believe, presently supplied as a string by the API, probably to avoid overflow + amountOutput.public_key = "da77082624fce921891c4fb80a1e7076a6714ca8c9fc547311737926a0b85a46"; + amountOutput.rct = "bb227b27e36b7f3e695dffb641c29bb60bfd991accdb5ef4b580c9acd48c16b6"; + amountAndOuts.outputs.push_back(amountOutput); + } + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 1929918; + amountOutput.public_key = "8c983e7053d7a1dc9de8ac00468bcf11836a787d712dc0c02bd54a3ee00a55e8"; + amountOutput.rct = "8dec45867644d1a76aafe4487292d7cf401302e6bbbb99a61c2f3b6cef4f4f34"; + amountAndOuts.outputs.push_back(amountOutput); + } + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 3921094; + amountOutput.public_key = "0133219bd5e247eef51003921ec792784c41fc34289c703e9326d46f78d9b10a"; + amountOutput.rct = "75082f4ce31904acba4af37699c28d8d4f0f74fdf63b1e4a8069ebed50df3220"; + amountAndOuts.outputs.push_back(amountOutput); + } + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 6627106; + amountOutput.public_key = "daef1663dd1084bd7fe585c3d493480ee1c4cefb93254eac5855afdf38f662b1"; + amountOutput.rct = "1d96763c5bc3300090c286705b7d544f02c185d9be8c32baac6bbfb8e0d0d283"; + amountAndOuts.outputs.push_back(amountOutput); + } + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 3308654; + amountOutput.public_key = "ae135f58762b1133667002538f8c353a1869db815aa686e2544b5243c2d2212f"; + amountOutput.rct = "15046b93bb181189f2917eed38173202fbbb9cdbfcf3d1bc3e432df999ae1b1c"; + amountAndOuts.outputs.push_back(amountOutput); + } + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 1972531; + amountOutput.public_key = "39e44fa88d684d71762c40eb64ac80ddc694b74a99ac445667bf433536c09c8f"; + amountOutput.rct = "66a42d0e8123768b392ad4a230759258d9156fab1aea00a19b041832326aca0a"; + amountAndOuts.outputs.push_back(amountOutput); + } + { + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = 3274424; + amountOutput.public_key = "a89b91648645ba6f32e214ba5720f5387376e5a144e698d5d5d1ebac971de349"; + amountOutput.rct = "815a6b1da6fc6a3bd791c4342782381cf948ee822ac9da7149f1b3717e0266d2"; + amountAndOuts.outputs.push_back(amountOutput); + } + mix_outs.push_back(amountAndOuts); + } + // + uint32_t subaddr_account_idx = 0; + std::unordered_map subaddresses; + subaddresses[account_keys.m_account_address.m_spend_public_key] = {0,0}; + cout << "account_keys.m_account_address.m_spend_public_key: " << string_tools::pod_to_hex(account_keys.m_account_address.m_spend_public_key) << endl; + // + monero_transfer_utils::create_transaction( + retVals, + account_keys, + subaddr_account_idx, + subaddresses, + dsts, + outputs, + mix_outs, + fee_amount, + extra, + use_fork_rules_fn, + 0, // unlock_time + true, // rct + nettype + ); + if (retVals.errCode != noError) { + BOOST_REQUIRE_MESSAGE(false, "create_transaction failed"); + return; + } + auto txBlob = t_serializable_object_to_blob(retVals.tx); + size_t txBlob_byteLength = txBlob.size(); +// cout << "txBlob: " << txBlob << endl; + cout << "txBlob_byteLength: " << txBlob_byteLength << endl; + BOOST_REQUIRE(txBlob_byteLength > 0); + + // tx hash + auto tx_hash_string = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(retVals.tx)); + auto signed_serialized_tx_string = epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(retVals.tx)); + + cout << "tx_hash_string: " << tx_hash_string << endl; + cout << "signed_serialized_tx_string: " << signed_serialized_tx_string << endl; + +}