// // monero_send_routine.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 #include "wallet_errors.h" #include "string_tools.h" // #include "monero_send_routine.hpp" // #include "monero_transfer_utils.hpp" #include "monero_fork_rules.hpp" #include "monero_key_image_utils.hpp" #include "monero_address_utils.hpp" // 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; using namespace monero_key_image_utils; // for API response parsing using namespace monero_send_routine; // // optional _possible_uint64_from_json( const boost::property_tree::ptree &res, const string &fieldname ) { // throws auto optl_str = res.get_optional(fieldname); // cout << fieldname << ": " << optl_str << endl; if (optl_str != none) { return stoull(*optl_str); } auto optl_uint32 = res.get_optional(fieldname); // not uint64 b/c JSON can't store such values if (optl_uint32 != none) { return (uint64_t)*optl_uint32; // cast } return none; } // LightwalletAPI_Req_GetUnspentOuts monero_send_routine::new__req_params__get_unspent_outs( string from_address_string, string sec_viewKey_string ) { ostringstream dustT_ss; dustT_ss << dust_threshold(); return { std::move(from_address_string), std::move(sec_viewKey_string), "0", // amount - always sent as "0" fixed_mixinsize(), true, // use dust dustT_ss.str() }; } LightwalletAPI_Req_GetRandomOuts monero_send_routine::new__req_params__get_random_outs( vector &step1__using_outs ) { vector decoy_req__amounts; BOOST_FOREACH(SpendableOutput &using_out, step1__using_outs) { if (using_out.rct != none && (*(using_out.rct)).size() > 0) { decoy_req__amounts.push_back("0"); } else { ostringstream amount_ss; amount_ss << using_out.amount; decoy_req__amounts.push_back(amount_ss.str()); } } return LightwalletAPI_Req_GetRandomOuts{ decoy_req__amounts, fixed_mixinsize() + 1 // count; Add one to mixin so we can skip real output key if necessary }; } // LightwalletAPI_Res_GetUnspentOuts monero_send_routine::new__parsed_res__get_unspent_outs( const property_tree::ptree &res, const secret_key &sec_viewKey, const secret_key &sec_spendKey, const public_key &pub_spendKey ) { uint64_t final__per_byte_fee = 0; uint64_t fee_mask = 10000; // just a fallback value - no real reason to set this here normally try { optional possible__uint64 = _possible_uint64_from_json(res, "per_byte_fee"); if (possible__uint64 != none) { final__per_byte_fee = *possible__uint64; } } catch (const std::exception &e) { cout << "Unspent outs per-byte-fee parse error: " << e.what() << endl; string err_msg = "Unspent outs: Unrecognized per-byte fee format"; return { err_msg, none, none, none }; } try { optional possible__uint64 = _possible_uint64_from_json(res, "fee_mask"); if (possible__uint64 != none) { fee_mask = *possible__uint64; } } catch (const std::exception &e) { cout << "Unspent outs fee_mask parse error: " << e.what() << endl; string err_msg = "Unspent outs: Unrecognized fee_mask format"; return { err_msg, none, none, none }; } if (final__per_byte_fee == 0) { try { optional possible__uint64 = _possible_uint64_from_json(res, "per_kb_fee"); if (possible__uint64 != none) { final__per_byte_fee = (*possible__uint64) / 1024; // scale from kib to b fee_mask = 10000; // just to be explicit } } catch (const std::exception &e) { cout << "Unspent outs per-kb-fee parse error: " << e.what() << endl; string err_msg = "Unspent outs: Unrecognized per-kb fee format"; return { err_msg, none, none, none }; } } if (final__per_byte_fee == 0) { string err_msg = "Unable to get a per-byte fee from server response."; return { err_msg, none, none, none }; } vector unspent_outs; BOOST_FOREACH(const boost::property_tree::ptree::value_type &output_desc, res.get_child("outputs")) { assert(output_desc.first.empty()); // array elements have no names // auto optl__tx_pub_key = output_desc.second.get_optional("tx_pub_key"); if (optl__tx_pub_key == none) { // TODO: do we ever actually expect these not to exist? cout << "Warn: This unspent out was missing a tx_pub_key. Skipping." << endl; continue; // skip } crypto::public_key tx_pub_key{}; { bool r = epee::string_tools::hex_to_pod(*optl__tx_pub_key, tx_pub_key); if (!r) { string err_msg = "Invalid tx pub key"; return { err_msg, none, none, none }; } } uint64_t output__index; try { optional possible__uint64 = _possible_uint64_from_json(output_desc.second, "index"); if (possible__uint64 != none) { output__index = *possible__uint64; // expecting this to exist } else { // bail string err_msg = "Expected unspent output to have an \"index\""; return { err_msg, none, none, none }; } } catch (const std::exception &e) { cout << "Unspent outs output index parse error: " << e.what() << endl; string err_msg = "Unspent outs: Unrecognized output index format"; return { err_msg, none, none, none }; } bool isOutputSpent = false; // let's see… { BOOST_FOREACH(const boost::property_tree::ptree::value_type &spend_key_image_string, output_desc.second.get_child("spend_key_images")) { // cout << "spend_key_image_string: " << spend_key_image_string.second.data() << endl; KeyImageRetVals retVals; bool r = new__key_image( pub_spendKey, sec_spendKey, sec_viewKey, tx_pub_key, output__index, retVals ); if (!r) { string err_msg = "Unable to generate key image"; return { err_msg, none, none, none }; } auto calculated_key_image_string = epee::string_tools::pod_to_hex(retVals.calculated_key_image); // cout << "calculated_key_image_string: " << calculated_key_image_string << endl; auto areEqual = calculated_key_image_string == spend_key_image_string.second.data(); if (areEqual) { isOutputSpent = true; // output was spent… exclude break; // exit spend key img loop to evaluate isOutputSpent } } } if (isOutputSpent == false) { SpendableOutput out{}; out.amount = stoull(output_desc.second.get("amount")); out.public_key = output_desc.second.get("public_key"); out.rct = output_desc.second.get_optional("rct"); out.global_index = stoull(output_desc.second.get("global_index")); out.index = output__index; out.tx_pub_key = *optl__tx_pub_key; // just b/c we've already accessed it above // unspent_outs.push_back(std::move(out)); } } auto fork_version = res.get_optional("fork_version"); return LightwalletAPI_Res_GetUnspentOuts{ none, final__per_byte_fee, fee_mask, unspent_outs, fork_version ? *fork_version : static_cast(0) }; } LightwalletAPI_Res_GetRandomOuts monero_send_routine::new__parsed_res__get_random_outs( const property_tree::ptree &res ) { vector mix_outs; BOOST_FOREACH(const boost::property_tree::ptree::value_type &mix_out_desc, res.get_child("amount_outs")) { assert(mix_out_desc.first.empty()); // array elements have no names auto amountAndOuts = RandomAmountOutputs{}; try { optional possible__uint64 = _possible_uint64_from_json(mix_out_desc.second, "amount"); if (possible__uint64 != none) { amountAndOuts.amount = *possible__uint64; } } catch (const std::exception &e) { cout << "Random outs response 'amount' parse error: " << e.what() << endl; string err_msg = "Random outs: Unrecognized 'amount' format"; return {err_msg, none}; } BOOST_FOREACH(const boost::property_tree::ptree::value_type &mix_out_output_desc, mix_out_desc.second.get_child("outputs")) { assert(mix_out_output_desc.first.empty()); // array elements have no names auto amountOutput = RandomAmountOutput{}; try { optional possible__uint64 = _possible_uint64_from_json(mix_out_output_desc.second, "global_index"); if (possible__uint64 != none) { amountOutput.global_index = *possible__uint64; } } catch (const std::exception &e) { cout << "Random outs response 'global_index' parse error: " << e.what() << endl; string err_msg = "Random outs: Unrecognized 'global_index' format"; return {err_msg, none}; } amountOutput.public_key = mix_out_output_desc.second.get("public_key"); amountOutput.rct = mix_out_output_desc.second.get_optional("rct"); // amountAndOuts.outputs.push_back(std::move(amountOutput)); } mix_outs.push_back(std::move(amountAndOuts)); } return { none, mix_outs }; } // struct _SendFunds_ConstructAndSendTx_Args { const string &from_address_string; const string &sec_viewKey_string; const string &sec_spendKey_string; const string &to_address_string; optional payment_id_string; uint64_t sending_amount; bool is_sweeping; uint32_t simple_priority; const send__get_random_outs_fn_type &get_random_outs_fn; const send__submit_raw_tx_fn_type &submit_raw_tx_fn; const send__status_update_fn_type &status_update_fn; const send__error_cb_fn_type &error_cb_fn; const send__success_cb_fn_type &success_cb_fn; uint64_t unlock_time; cryptonote::network_type nettype; // const vector &unspent_outs; uint64_t fee_per_b; uint64_t fee_quantization_mask; uint8_t fork_version; // // cached const secret_key &sec_viewKey; const secret_key &sec_spendKey; // optional passedIn_attemptAt_fee; size_t constructionAttempt; }; void _reenterable_construct_and_send_tx( const _SendFunds_ConstructAndSendTx_Args &args, // // re-entry params optional passedIn_attemptAt_fee = none, size_t constructionAttempt = 0 ) { args.status_update_fn(calculatingFee); // auto use_fork_rules = monero_fork_rules::make_use_fork_rules_fn(args.fork_version); // Send_Step1_RetVals step1_retVals; monero_transfer_utils::send_step1__prepare_params_for_get_decoys( step1_retVals, // args.payment_id_string, args.sending_amount, args.is_sweeping, args.simple_priority, use_fork_rules, args.unspent_outs, args.fee_per_b, args.fee_quantization_mask, // passedIn_attemptAt_fee // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min ); if (step1_retVals.errCode != noError) { SendFunds_Error_RetVals error_retVals; error_retVals.errCode = step1_retVals.errCode; error_retVals.spendable_balance = step1_retVals.spendable_balance; error_retVals.required_balance = step1_retVals.required_balance; args.error_cb_fn(error_retVals); return; } api_fetch_cb_fn get_random_outs_fn__cb_fn = [ args, step1_retVals, constructionAttempt, use_fork_rules ] ( const property_tree::ptree &res ) -> void { auto parsed_res = new__parsed_res__get_random_outs(res); if (parsed_res.err_msg != none) { SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = std::move(*(parsed_res.err_msg)); args.error_cb_fn(error_retVals); return; } Send_Step2_RetVals step2_retVals; monero_transfer_utils::send_step2__try_create_transaction( step2_retVals, // args.from_address_string, args.sec_viewKey_string, args.sec_spendKey_string, args.to_address_string, args.payment_id_string, step1_retVals.final_total_wo_fee, step1_retVals.change_amount, step1_retVals.using_fee, args.simple_priority, step1_retVals.using_outs, args.fee_per_b, args.fee_quantization_mask, *(parsed_res.mix_outs), std::move(use_fork_rules), args.unlock_time, args.nettype ); if (step2_retVals.errCode != noError) { SendFunds_Error_RetVals error_retVals; error_retVals.errCode = step2_retVals.errCode; args.error_cb_fn(error_retVals); return; } if (step2_retVals.tx_must_be_reconstructed) { // this will update status back to .calculatingFee if (constructionAttempt > 15) { // just going to avoid an infinite loop here or particularly long stack SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = "Unable to construct a transaction with sufficient fee for unknown reason."; args.error_cb_fn(error_retVals); return; } // cout << "step2_retVals.fee_actually_needed: " << step2_retVals.fee_actually_needed << endl; _reenterable_construct_and_send_tx( args, // step2_retVals.fee_actually_needed, // -> reconstruction attempt's step1's passedIn_attemptAt_fee constructionAttempt+1 ); return; } args.status_update_fn(submittingTransaction); // api_fetch_cb_fn submit_raw_tx_fn__cb_fn = [ args, step1_retVals, step2_retVals ] ( const property_tree::ptree &res ) -> void { // not actually expecting anything in a success response, so no need to parse SendFunds_Success_RetVals success_retVals; success_retVals.used_fee = step1_retVals.using_fee; // NOTE: not the same thing as step2_retVals.fee_actually_needed success_retVals.total_sent = step1_retVals.final_total_wo_fee + step1_retVals.using_fee; success_retVals.mixin = step1_retVals.mixin; { optional returning__payment_id = args.payment_id_string; // separated from submit_raw_tx_fn so that it can be captured w/o capturing all of args (FIXME: does this matter?) if (returning__payment_id == none) { auto decoded = monero::address_utils::decodedAddress(args.to_address_string, args.nettype); if (decoded.did_error) { // would be very strange... SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = *(decoded.err_string); args.error_cb_fn(error_retVals); return; } if (decoded.paymentID_string != none) { returning__payment_id = std::move(*(decoded.paymentID_string)); // just preserving this as an original return value - this can probably eventually be removed } } success_retVals.final_payment_id = returning__payment_id; } success_retVals.signed_serialized_tx_string = std::move(*(step2_retVals.signed_serialized_tx_string)); success_retVals.tx_hash_string = std::move(*(step2_retVals.tx_hash_string)); success_retVals.tx_key_string = std::move(*(step2_retVals.tx_key_string)); success_retVals.tx_pub_key_string = std::move(*(step2_retVals.tx_pub_key_string)); // args.success_cb_fn(success_retVals); }; args.submit_raw_tx_fn(LightwalletAPI_Req_SubmitRawTx{ args.from_address_string, args.sec_viewKey_string, *(step2_retVals.signed_serialized_tx_string) }, submit_raw_tx_fn__cb_fn); }; // args.status_update_fn(fetchingDecoyOutputs); // args.get_random_outs_fn( new__req_params__get_random_outs(step1_retVals.using_outs), get_random_outs_fn__cb_fn ); } // // // Entrypoint void monero_send_routine::async__send_funds(Async_SendFunds_Args args) { uint64_t usable__sending_amount = args.is_sweeping ? 0 : args.sending_amount; crypto::secret_key sec_viewKey{}; crypto::secret_key sec_spendKey{}; crypto::public_key pub_spendKey{}; { bool r = false; r = epee::string_tools::hex_to_pod(args.sec_viewKey_string, sec_viewKey); if (!r) { SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = "Invalid secret view key"; args.error_cb_fn(error_retVals); return; } r = epee::string_tools::hex_to_pod(args.sec_spendKey_string, sec_spendKey); if (!r) { SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = "Invalid sec spend key"; args.error_cb_fn(error_retVals); return; } r = epee::string_tools::hex_to_pod(args.pub_spendKey_string, pub_spendKey); if (!r) { SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = "Invalid public spend key"; args.error_cb_fn(error_retVals); return; } } api_fetch_cb_fn get_unspent_outs_fn__cb_fn = [ args, usable__sending_amount, sec_viewKey, sec_spendKey, pub_spendKey ] ( const property_tree::ptree &res ) -> void { auto parsed_res = new__parsed_res__get_unspent_outs( res, sec_viewKey, sec_spendKey, pub_spendKey ); if (parsed_res.err_msg != none) { SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = std::move(*(parsed_res.err_msg)); args.error_cb_fn(error_retVals); return; } _reenterable_construct_and_send_tx(_SendFunds_ConstructAndSendTx_Args{ args.from_address_string, args.sec_viewKey_string, args.sec_spendKey_string, args.to_address_string, args.payment_id_string, usable__sending_amount, args.is_sweeping, args.simple_priority, args.get_random_outs_fn, args.submit_raw_tx_fn, args.status_update_fn, args.error_cb_fn, args.success_cb_fn, args.unlock_time == none ? 0 : *(args.unlock_time), args.nettype == none ? MAINNET : *(args.nettype), // *(parsed_res.unspent_outs), *(parsed_res.per_byte_fee), *(parsed_res.fee_mask), parsed_res.fork_version, // sec_viewKey, sec_spendKey }); }; args.status_update_fn(fetchingLatestBalance); // args.get_unspent_outs_fn( new__req_params__get_unspent_outs( args.from_address_string, args.sec_viewKey_string ), get_unspent_outs_fn__cb_fn ); }