updated mymonero-core-cpp; updated bridge for new stepwise sending implementation (ported majority of SendFunds to C++ as send_step*_…; updated tests and removed removed bridge methods like calculate_fee, estimate rct tx size, and create_transaction; New: added tests/sendingFunds.spec.js as integration tests for new monero_sendingFunds_utils implementation - run with 'npm test -- tests/sendingFunds.spec.js'; CMakeLists: enabled ASSERTIONS

pull/63/head
Paul Shapiro 6 years ago
parent d60df76402
commit 5611835d14

@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.4.1)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
project(MyMoneroCoreCpp)
option(MM_EM_ASMJS "Build for asmjs instead of wasm" OFF)
option(MM_EM_ASMJS "Build for asmjs instead of wasm" OFF) # NOTE: if you change this, you should delete your cmake build cache
#
include_directories("build/boost/include") # must exist already - run bin/build-boost-emscripten.sh
#
@ -43,6 +43,8 @@ set(
${MYMONERO_CORE_CPP_SRC}/monero_paymentID_utils.cpp
${MYMONERO_CORE_CPP_SRC}/monero_key_image_utils.hpp
${MYMONERO_CORE_CPP_SRC}/monero_key_image_utils.cpp
${MYMONERO_CORE_CPP_SRC}/monero_fee_utils.hpp
${MYMONERO_CORE_CPP_SRC}/monero_fee_utils.cpp
${MYMONERO_CORE_CPP_SRC}/monero_transfer_utils.hpp
${MYMONERO_CORE_CPP_SRC}/monero_transfer_utils.cpp
${MYMONERO_CORE_CPP_SRC}/monero_fork_rules.hpp
@ -190,7 +192,7 @@ set (
-Oz \
--llvm-lto 1 \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
-s ASSERTIONS=0 \
-s ASSERTIONS=1 \
-s NO_DYNAMIC_EXECUTION=1 \
-s \"BINARYEN_TRAP_MODE='clamp'\" \
-s PRECISE_F32=1 \

@ -445,10 +445,8 @@ function Parsed_UnspentOuts__sync(
}
}
// console.log("Unspent outs: " + JSON.stringify(finalized_unspentOutputs));
const unusedOuts = finalized_unspentOutputs.slice(0);
const returnValuesByKey = {
unspentOutputs: finalized_unspentOutputs,
unusedOuts: unusedOuts,
per_kb_fee: data.per_kb_fee, // String
};
return returnValuesByKey;

@ -34,7 +34,7 @@
const JSBigInt = require("../cryptonote_utils/biginteger").BigInteger;
const nettype_utils = require("../cryptonote_utils/nettype");
const monero_config = require('./monero_config');
const currency_amount_format_utils = require("../cryptonote_utils/money_format_utils")(monero_config);
const monero_amount_format_utils = require("../cryptonote_utils/money_format_utils")(monero_config);
//
function ret_val_boolstring_to_bool(boolstring)
{
@ -62,6 +62,21 @@ function api_safe_wordset_name(wordset_name)
}
return wordset_name // must be a value returned by core-cpp
}
function bridge_sanitized__spendable_out(raw__out)
{
const sanitary__output =
{
amount: raw__out.amount.toString(),
public_key: raw__out.public_key,
global_index: "" + raw__out.global_index,
index: "" + raw__out.index,
tx_pub_key: raw__out.tx_pub_key
};
if (raw__out.rct && typeof raw__out.rct !== 'undefined') {
sanitary__output.rct = raw__out.rct;
}
return sanitary__output;
}
//
class MyMoneroCoreBridge
{
@ -430,114 +445,102 @@ class MyMoneroCoreBridge
mask: ret.mask,
};
}
estimate_rct_tx_size(n_inputs, mixin, n_outputs, optl__extra_size, optl__bulletproof)
{
const args =
{
n_inputs: "" + n_inputs,
mixin: "" + mixin,
n_outputs: "" + n_outputs,
extra_size: "" + (typeof optl__extra_size !== 'undefined' && optl__extra_size ? optl__extra_size : 0),
bulletproof: "" + (optl__bulletproof == false ? false : true) /* default true */,
};
const args_str = JSON.stringify(args);
const ret_string = this.Module.estimate_rct_tx_size(args_str);
const ret = JSON.parse(ret_string);
if (typeof ret.err_msg !== 'undefined' && ret.err_msg) {
return { err_msg: ret.err_msg } // TODO: maybe return this somehow
}
return parseInt(ret.retVal); // small enough to parse
}
calculate_fee(fee_per_kb__string, num_bytes, fee_multiplier)
{
estimated_tx_network_fee(fee_per_kb__string, priority, optl__fee_per_b_string) // this is until we switch the server over to fee per b
{ // TODO update this API to take object rather than arg list
const args =
{
fee_per_kb: fee_per_kb__string,
num_bytes: "" + num_bytes,
fee_multiplier: "" + fee_multiplier
fee_per_b: typeof optl__fee_per_b_string !== undefined && optl__fee_per_b_string != null
? optl__fee_per_b_string
: (new JSBigInt(fee_per_kb__string)).divide(1024).toString()/*kib -> b*/,
priority: "" + priority,
};
const args_str = JSON.stringify(args);
const ret_string = this.Module.calculate_fee(args_str);
const ret_string = this.Module.estimated_tx_network_fee(args_str);
const ret = JSON.parse(ret_string);
if (typeof ret.err_msg !== 'undefined' && ret.err_msg) {
return { err_msg: ret.err_msg } // TODO: maybe return this somehow
}
return ret.retVal; // this is a string - pass it to new JSBigInt(…)
}
estimated_tx_network_fee(fee_per_kb__string, priority)
{
send_step1__prepare_params_for_get_decoys(
is_sweeping,
sending_amount, // this may be 0 if sweeping
fee_per_b,
priority,
unspent_outputs,
optl__payment_id_string, // this may be nil
optl__passedIn_attemptAt_fee
) {
var sanitary__unspent_outputs = [];
for (let i in unspent_outputs) {
const sanitary__output = bridge_sanitized__spendable_out(unspent_outputs[i])
sanitary__unspent_outputs.push(sanitary__output);
}
const args =
{
fee_per_kb: fee_per_kb__string,
sending_amount: sending_amount.toString(),
is_sweeping: "" + is_sweeping, // bool -> string
priority: "" + priority,
fee_per_b: fee_per_b.toString(),
unspent_outs: sanitary__unspent_outputs // outs, not outputs
};
if (typeof optl__payment_id_string !== "undefined" && optl__payment_id_string && optl__payment_id_string != "") {
args.payment_id_string = optl__payment_id_string;
}
if (typeof optl__passedIn_attemptAt_fee !== "undefined" && optl__passedIn_attemptAt_fee && optl__passedIn_attemptAt_fee != "") {
args.passedIn_attemptAt_fee = optl__passedIn_attemptAt_fee.toString(); // ought to be a string but in case it's a JSBigInt…
}
const args_str = JSON.stringify(args);
const ret_string = this.Module.estimated_tx_network_fee(args_str);
const ret_string = this.Module.send_step1__prepare_params_for_get_decoys(args_str);
const ret = JSON.parse(ret_string);
// special case: err_code of needMoreMoneyThanFound; rewrite err_msg
if (ret.err_code == "90" || ret.err_code == 90) { // declared in mymonero-core-cpp/src/monero_transfer_utils.hpp
return {
required_balance: ret.required_balance,
spendable_balance: ret.spendable_balance,
err_msg: `Spendable balance too low. Have ${
monero_amount_format_utils.formatMoney(new JSBigInt(ret.spendable_balance))
} ${monero_config.coinSymbol}; need ${
monero_amount_format_utils.formatMoney(new JSBigInt(ret.required_balance))
} ${monero_config.coinSymbol}.`
};
}
if (typeof ret.err_msg !== 'undefined' && ret.err_msg) {
return { err_msg: ret.err_msg } // TODO: maybe return this somehow
return { err_msg: ret.err_msg };
}
return ret.retVal; // this is a string - pass it to new JSBigInt(…)
return { // calling these out to set an interface
mixin: parseInt(ret.mixin), // for the server API request to RandomOuts
using_fee: ret.using_fee, // string; can be passed to step2
change_amount: ret.change_amount, // string for step2
using_outs: ret.using_outs, // this can be passed straight to step2
final_total_wo_fee: ret.final_total_wo_fee // aka sending_amount for step2
};
}
create_signed_transaction(
send_step2__try_create_transaction( // send only IPC-safe vals - no JSBigInts
from_address_string,
sec_keys,
to_address_string,
outputs,
using_outs,
mix_outs,
fake_outputs_count,
serialized__sending_amount,
serialized__change_amount,
serialized__fee_amount, // string amount
payment_id,
unlock_time,
rct,
nettype
) {
return this.create_signed_transaction__nonIPCsafe(
from_address_string,
sec_keys,
to_address_string,
outputs,
mix_outs,
fake_outputs_count,
new JSBigInt(serialized__sending_amount),
new JSBigInt(serialized__change_amount),
new JSBigInt(serialized__fee_amount), // only to be deserialized again is a bit silly but this at least exposes a JSBigInt API for others
payment_id,
unlock_time,
rct,
nettype
);
}
create_signed_transaction__nonIPCsafe( // you can use this function to pass JSBigInts
from_address_string,
sec_keys,
to_address_string,
outputs,
mix_outs,
fake_outputs_count,
sending_amount,
final_total_wo_fee,
change_amount,
fee_amount,
payment_id,
priority,
fee_per_b, // not kib - if fee_per_kb, /= 1024
unlock_time,
rct,
nettype
) {
unlock_time = unlock_time || 0;
mix_outs = mix_outs || [];
if (rct != true) {
return { err_msg: "Expected rct=true" }
}
if (mix_outs.length !== outputs.length && fake_outputs_count !== 0) {
return { err_msg: "Wrong number of mix outs provided (" +
outputs.length +
" outputs, " +
mix_outs.length +
" mix outs)" };
// NOTE: we also do this check in the C++... may as well remove it from here
if (mix_outs.length !== using_outs.length && fake_outputs_count !== 0) {
return {
err_msg: "Wrong number of mix outs provided (" +
using_outs.length + " using_outs, " +
mix_outs.length + " mix outs)"
};
}
for (var i = 0; i < mix_outs.length; i++) {
if ((mix_outs[i].outputs || []).length < fake_outputs_count) {
@ -545,34 +548,25 @@ class MyMoneroCoreBridge
}
}
//
// Now we need to convert all non-JSON-serializable objects such as JSBigInts to strings etc
var sanitary__outputs = [];
for (let i in outputs) {
const sanitary__output =
{
amount: outputs[i].amount.toString(),
public_key: outputs[i].public_key,
global_index: "" + outputs[i].global_index,
index: "" + outputs[i].index,
tx_pub_key: outputs[i].tx_pub_key
};
if (outputs[i].rct && typeof outputs[i].rct !== 'undefined') {
sanitary__output.rct = outputs[i].rct;
}
sanitary__outputs.push(sanitary__output);
// Now we need to convert all non-JSON-serializable objects such as JSBigInts to strings etc - not that there should be any!
// - and all numbers to strings - especially those which may be uint64_t on the receiving side
var sanitary__using_outs = [];
for (let i in using_outs) {
const sanitary__output = bridge_sanitized__spendable_out(using_outs[i])
sanitary__using_outs.push(sanitary__output);
}
var sanitary__mix_outs = [];
for (let i in mix_outs) {
const sanitary__mix_outs_and_amount =
{
amount: "" + mix_outs[i].amount,
amount: mix_outs[i].amount.toString(), // it should be a string, but in case it's not
outputs: []
};
if (mix_outs[i].outputs && typeof mix_outs[i].outputs !== 'undefined') {
for (let j in mix_outs[i].outputs) {
const sanitary__mix_out =
{
global_index: "" + mix_outs[i].outputs[j].global_index,
global_index: "" + mix_outs[i].outputs[j].global_index, // number to string
public_key: mix_outs[i].outputs[j].public_key
};
if (mix_outs[i].outputs[j].rct && typeof mix_outs[i].outputs[j].rct !== 'undefined') {
@ -589,10 +583,12 @@ class MyMoneroCoreBridge
sec_viewKey_string: sec_keys.view,
sec_spendKey_string: sec_keys.spend,
to_address_string: to_address_string,
sending_amount: sending_amount.toString(),
final_total_wo_fee: final_total_wo_fee.toString(),
change_amount: change_amount.toString(),
fee_amount: fee_amount.toString(),
outputs: sanitary__outputs,
priority: "" + priority,
fee_per_b: fee_per_b.toString(),
using_outs: sanitary__using_outs,
mix_outs: sanitary__mix_outs,
unlock_time: "" + unlock_time, // bridge is expecting a string
nettype_string: nettype_utils.nettype_to_API_string(nettype)
@ -601,18 +597,29 @@ class MyMoneroCoreBridge
args.payment_id_string = payment_id;
}
const args_str = JSON.stringify(args);
const ret_string = this.Module.create_transaction(args_str);
const ret_string = this.Module.send_step2__try_create_transaction(args_str);
const ret = JSON.parse(ret_string);
//
if (typeof ret.err_msg !== 'undefined' && ret.err_msg) {
return { err_msg: ret.err_msg };
return { err_msg: ret.err_msg, tx_must_be_reconstructed: false };
}
if (ret.tx_must_be_reconstructed == "true" || ret.tx_must_be_reconstructed == true) {
if (typeof ret.fee_actually_needed == 'undefined' || !ret.fee_actually_needed) {
throw "tx_must_be_reconstructed; expected non-nil fee_actually_needed"
}
return {
tx_must_be_reconstructed: ret.tx_must_be_reconstructed, // if true, re-do procedure from step1 except for requesting UnspentOuts (that can be done oncet)
fee_actually_needed: ret.fee_actually_needed // can be passed back to step1
}
}
return { // calling these out to set an interface
tx_must_be_reconstructed: false, // in case caller is not checking for nil
signed_serialized_tx: ret.serialized_signed_tx,
tx_hash: ret.tx_hash,
tx_key: ret.tx_key
};
}
}
//
module.exports = function(options)

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -49,6 +49,6 @@ exports.bridgedFn_names =
"estimate_rct_tx_size",
"calculate_fee",
"estimated_tx_network_fee",
"create_signed_transaction",
"create_signed_transaction__nonIPCsafe"
"send_step1__prepare_params_for_get_decoys",
"send_step2__try_create_transaction"
];

@ -49,11 +49,6 @@ module.exports = {
// Payment URI Prefix
coinUriPrefix: "monero:",
// Prefix code for addresses
addressPrefix: 18, // 18 => addresses start with "4"
integratedAddressPrefix: 19,
subaddressPrefix: 42,
// Dust threshold in atomic units
// 2*10^9 used for choosing outputs/change - we decompose all the way down if the receiver wants now regardless of threshold
dustThreshold: new JSBigInt("2000000000"),

@ -97,7 +97,7 @@ const SendFunds_ProcessStep_MessageSuffix = {
};
exports.SendFunds_ProcessStep_MessageSuffix = SendFunds_ProcessStep_MessageSuffix;
//
function SendFunds(
function SendFunds( // TODO: migrate this to take a map of args
target_address, // currency-ready wallet address, but not an OA address (resolve before calling)
nettype,
amount_orZeroWhenSweep, // number - value will be ignoring for sweep
@ -110,7 +110,7 @@ function SendFunds(
simple_priority,
preSuccess_nonTerminal_statusUpdate_fn, // (_ stepCode: SendFunds_ProcessStep_Code) -> Void
success_fn,
// success_fn: (
// success_fn: ( // TODO: to be migrated to args as return obj
// moneroReady_targetDescription_address?,
// sentAmount?,
// final__payment_id?,
@ -126,30 +126,9 @@ function SendFunds(
) {
monero_utils_promise.then(function(monero_utils)
{
const mixin = fixedMixin();
var isRingCT = true;
var sweeping = isSweep_orZeroWhenAmount === true; // rather than, say, undefined
const mixin = fixedMixin(); // would be nice to eliminate this dependency or grab it from C++
//
// some callback trampoline function declarations…
function __trampolineFor_success(
moneroReady_targetDescription_address,
sentAmount,
final__payment_id,
tx_hash,
tx_fee,
tx_key,
mixin,
) {
success_fn(
moneroReady_targetDescription_address,
sentAmount,
final__payment_id,
tx_hash,
tx_fee,
tx_key,
mixin,
);
}
function __trampolineFor_err_withErr(err) {
failWithErr_fn(err);
}
@ -159,668 +138,178 @@ function SendFunds(
failWithErr_fn(err);
}
//
// parse & normalize the target descriptions by mapping them to Monero addresses & amounts
var sweeping = isSweep_orZeroWhenAmount === true; // rather than, say, undefined
var amount = sweeping ? 0 : amount_orZeroWhenSweep;
const targetDescription = {
address: target_address,
amount: amount,
};
new_moneroReadyTargetDescriptions_fromTargetDescriptions(
[targetDescription], // requires a list of descriptions - but SendFunds was
// not written with multiple target support as MyMonero does not yet support it
nettype,
monero_utils,
function(err, moneroReady_targetDescriptions) {
var sending_amount; // possibly need this ; here for the JS parser
if (sweeping) {
sending_amount = 0
} else {
try {
sending_amount = monero_amount_format_utils.parseMoney(amount_orZeroWhenSweep);
} catch (e) {
__trampolineFor_err_withStr(`Couldn't parse amount ${amount_orZeroWhenSweep}: ${e}`);
return;
}
}
//
// TODO:
// const wallet__public_keys = decode_address(from_address, nettype);
//
preSuccess_nonTerminal_statusUpdate_fn(SendFunds_ProcessStep_Code.fetchingLatestBalance);
var fee_per_b__string;
var unspent_outs;
hostedMoneroAPIClient.UnspentOuts(
wallet__public_address,
wallet__private_keys.view,
wallet__public_keys.spend,
wallet__private_keys.spend,
mixin,
sweeping,
function(err, returned_unspentOuts, returned_unusedOuts, dynamic_feePerKB_JSBigInt)
{
if (err) {
__trampolineFor_err_withErr(err);
return;
}
const invalidOrZeroDestination_errStr =
"You need to enter a valid destination";
if (moneroReady_targetDescriptions.length === 0) {
__trampolineFor_err_withStr(invalidOrZeroDestination_errStr);
return;
}
const moneroReady_targetDescription =
moneroReady_targetDescriptions[0];
if (
moneroReady_targetDescription === null ||
typeof moneroReady_targetDescription === "undefined"
) {
__trampolineFor_err_withStr(invalidOrZeroDestination_errStr);
return;
console.log("Received dynamic per kb fee", monero_amount_format_utils.formatMoneySymbol(dynamic_feePerKB_JSBigInt));
{ // save some values for re-enterable function
unspent_outs = returned_unusedOuts; // TODO: which one should be used? delete the other
fee_per_b__string = dynamic_feePerKB_JSBigInt.divide(1024).toString() // TODO: soon deprecate per kib fee
}
_proceedTo_prepareToSendFundsTo_moneroReady_targetDescription(
moneroReady_targetDescription,
__reenterable_constructAndSendTx(
null, // for the first try - passedIn_attemptAt_network_minimumFee
1
);
},
);
function _proceedTo_prepareToSendFundsTo_moneroReady_targetDescription(
moneroReady_targetDescription,
) {
var moneroReady_targetDescription_address =
moneroReady_targetDescription.address;
var moneroReady_targetDescription_amount =
moneroReady_targetDescription.amount;
//
var totalAmountWithoutFee_JSBigInt = new JSBigInt(0).add(
moneroReady_targetDescription_amount,
);
console.log(
"💬 Total to send, before fee: " + sweeping
? "all"
: monero_amount_format_utils.formatMoney(totalAmountWithoutFee_JSBigInt),
);
if (!sweeping && totalAmountWithoutFee_JSBigInt.compare(0) <= 0) {
const errStr = "The amount you've entered is too low";
__trampolineFor_err_withStr(errStr);
return;
}
//
// Derive/finalize some values…
var final__payment_id = payment_id;
var address__decode_result;
);
function __reenterable_constructAndSendTx(optl__passedIn_attemptAt_fee, constructionAttempt)
{
// Now we need to establish some values for balance validation and to construct the transaction
preSuccess_nonTerminal_statusUpdate_fn(SendFunds_ProcessStep_Code.calculatingFee);
var step1_retVals;
try {
address__decode_result = monero_utils.decode_address(
moneroReady_targetDescription_address,
nettype,
step1_retVals = monero_utils.send_step1__prepare_params_for_get_decoys(
sweeping,
sending_amount.toString(), // must be a string
fee_per_b__string,
simple_priority,
unspent_outs,
payment_id, // may be nil
optl__passedIn_attemptAt_fee
);
} catch (e) {
__trampolineFor_err_withStr(
typeof e === "string" ? e : e.toString(),
);
return;
}
if (payment_id) {
if (address__decode_result.intPaymentId) {
const errStr =
"Payment ID must be blank when using an Integrated Address";
__trampolineFor_err_withStr(errStr);
return;
} else if (
monero_utils.is_subaddress(
moneroReady_targetDescription_address,
nettype,
)
) {
const errStr =
"Payment ID must be blank when using a Subaddress";
__trampolineFor_err_withStr(errStr);
return;
var errStr;
if (e) {
errStr = typeof e == "string" ? e : e.toString();
} else {
errStr = "Failed to create transaction (step 1) with unknown error.";
}
}
if (address__decode_result.intPaymentId) {
final__payment_id = address__decode_result.intPaymentId;
} else if (
monero_paymentID_utils.IsValidPaymentIDOrNoPaymentID(
final__payment_id,
) === false
) {
const errStr = "Invalid payment ID.";
__trampolineFor_err_withStr(errStr);
return;
}
//
_proceedTo_getUnspentOutsUsableForMixin(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
);
}
function _proceedTo_getUnspentOutsUsableForMixin(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id, // non-existent or valid
) {
preSuccess_nonTerminal_statusUpdate_fn(
SendFunds_ProcessStep_Code.fetchingLatestBalance,
);
hostedMoneroAPIClient.UnspentOuts(
wallet__public_address,
wallet__private_keys.view,
wallet__public_keys.spend,
wallet__private_keys.spend,
mixin,
sweeping,
function(err, unspentOuts, unusedOuts, dynamic_feePerKB_JSBigInt) {
// prep for step2
// first, grab RandomOuts, then enter step2
preSuccess_nonTerminal_statusUpdate_fn(SendFunds_ProcessStep_Code.fetchingDecoyOutputs);
hostedMoneroAPIClient.RandomOuts(
step1_retVals.using_outs,
step1_retVals.mixin,
function(err, mix_outs)
{
if (err) {
__trampolineFor_err_withErr(err);
return;
}
console.log(
"Received dynamic per kb fee",
monero_amount_format_utils.formatMoneySymbol(dynamic_feePerKB_JSBigInt),
);
_proceedTo_constructFundTransferListAndSendFundsByUsingUnusedUnspentOutsForMixin(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
unusedOuts,
dynamic_feePerKB_JSBigInt,
);
},
);
}
function _proceedTo_constructFundTransferListAndSendFundsByUsingUnusedUnspentOutsForMixin(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
unusedOuts,
dynamic_feePerKB_JSBigInt,
) {
// status: constructing transaction…
const feePerKB_JSBigInt = dynamic_feePerKB_JSBigInt;
// Transaction will need at least 1KB fee (or 13KB for RingCT)
const network_minimumTXSize_kb = /*isRingCT ? */ 13; /* : 1*/
const network_minimumTXSize_bytes = network_minimumTXSize_kb * 1000 // B -> kB
const network_minimumFee = new JSBigInt(
monero_utils.calculate_fee(
"" + feePerKB_JSBigInt,
network_minimumTXSize_bytes,
fee_multiplier_for_priority(simple_priority)
)
)
// ^-- now we're going to try using this minimum fee but the codepath has to be able to be re-entered if we find after constructing the whole tx that it is larger in kb than the minimum fee we're attempting to send it off with
__reenterable_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
unusedOuts,
feePerKB_JSBigInt, // obtained from server, so passed in
network_minimumFee,
);
}
function __reenterable_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
unusedOuts,
feePerKB_JSBigInt,
passedIn_attemptAt_network_minimumFee,
) {
// Now we need to establish some values for balance validation and to construct the transaction
preSuccess_nonTerminal_statusUpdate_fn(
SendFunds_ProcessStep_Code.calculatingFee,
);
//
var attemptAt_network_minimumFee = passedIn_attemptAt_network_minimumFee; // we may change this if isRingCT
// const hostingService_chargeAmount = hostedMoneroAPIClient.HostingServiceChargeFor_transactionWithNetworkFee(attemptAt_network_minimumFee)
var totalAmountIncludingFees;
if (sweeping) {
totalAmountIncludingFees = new JSBigInt("18450000000000000000"); //~uint64 max
console.log("Balance required: all");
} else {
totalAmountIncludingFees = totalAmountWithoutFee_JSBigInt.add(
attemptAt_network_minimumFee,
); /*.add(hostingService_chargeAmount) NOTE service fee removed for now */
console.log(
"Balance required: " +
monero_amount_format_utils.formatMoneySymbol(totalAmountIncludingFees),
);
}
const usableOutputsAndAmounts = _outputsAndAmountToUseForMixin(
totalAmountIncludingFees,
unusedOuts,
isRingCT,
sweeping,
);
// v-- now if RingCT compute fee as closely as possible before hand
var usingOuts = usableOutputsAndAmounts.usingOuts;
var usingOutsAmount = usableOutputsAndAmounts.usingOutsAmount;
var remaining_unusedOuts = usableOutputsAndAmounts.remaining_unusedOuts; // this is a copy of the pre-mutation usingOuts
if (/*usingOuts.length > 1 &&*/ isRingCT) {
var newNeededFee = new JSBigInt(
monero_utils.calculate_fee(
"" + feePerKB_JSBigInt,
monero_utils.estimate_rct_tx_size(
usingOuts.length, mixin, 2
),
fee_multiplier_for_priority(simple_priority)
)
);
// if newNeededFee < neededFee, use neededFee instead (should only happen on the 2nd or later times through (due to estimated fee being too low))
if (newNeededFee.compare(attemptAt_network_minimumFee) < 0) {
newNeededFee = attemptAt_network_minimumFee;
}
if (sweeping) {
/*
// When/if sending to multiple destinations supported, uncomment and port this:
if (dsts.length !== 1) {
deferred.reject("Sweeping to multiple accounts is not allowed");
return;
}
*/
totalAmountWithoutFee_JSBigInt = usingOutsAmount.subtract(
newNeededFee,
);
if (totalAmountWithoutFee_JSBigInt.compare(0) < 1) {
const errStr = `Your spendable balance is too low. Have ${monero_amount_format_utils.formatMoney(
usingOutsAmount,
)} ${
monero_config.coinSymbol
} spendable, need ${monero_amount_format_utils.formatMoney(
newNeededFee,
)} ${monero_config.coinSymbol}.`;
__trampolineFor_err_withStr(errStr);
return;
}
totalAmountIncludingFees = totalAmountWithoutFee_JSBigInt.add(
newNeededFee,
);
} else {
totalAmountIncludingFees = totalAmountWithoutFee_JSBigInt.add(
newNeededFee,
);
// add outputs 1 at a time till we either have them all or can meet the fee
while (
usingOutsAmount.compare(totalAmountIncludingFees) < 0 &&
remaining_unusedOuts.length > 0
) {
const out = _popAndReturnRandomElementFromList(
remaining_unusedOuts,
);
console.log(
"Using output: " +
monero_amount_format_utils.formatMoney(out.amount) +
" - " +
JSON.stringify(out),
);
// and recalculate invalidated values
newNeededFee = new JSBigInt(
monero_utils.calculate_fee(
"" + feePerKB_JSBigInt,
monero_utils.estimate_rct_tx_size(
usingOuts.length, mixin, 2
),
fee_multiplier_for_priority(simple_priority)
)
);
totalAmountIncludingFees = totalAmountWithoutFee_JSBigInt.add(
newNeededFee,
);
}
___createTxAndAttemptToSend(mix_outs);
}
console.log(
"New fee: " +
monero_amount_format_utils.formatMoneySymbol(newNeededFee) +
" for " +
usingOuts.length +
" inputs",
);
attemptAt_network_minimumFee = newNeededFee;
}
console.log(
"~ Balance required: " +
monero_amount_format_utils.formatMoneySymbol(totalAmountIncludingFees),
);
// Now we can validate available balance with usingOutsAmount (TODO? maybe this check can be done before selecting outputs?)
const usingOutsAmount_comparedTo_totalAmount = usingOutsAmount.compare(
totalAmountIncludingFees,
);
if (usingOutsAmount_comparedTo_totalAmount < 0) {
const errStr = `Your spendable balance is too low. Have ${monero_amount_format_utils.formatMoney(
usingOutsAmount,
)} ${
monero_config.coinSymbol
} spendable, need ${monero_amount_format_utils.formatMoney(
totalAmountIncludingFees,
)} ${monero_config.coinSymbol}.`;
__trampolineFor_err_withStr(errStr);
return;
}
//
// Must calculate change..
var changeAmount = JSBigInt("0"); // to initialize
if (usingOutsAmount_comparedTo_totalAmount > 0) {
if (sweeping) {
throw "Unexpected usingOutsAmount_comparedTo_totalAmount > 0 && sweeping";
}
changeAmount = usingOutsAmount.subtract(
totalAmountIncludingFees,
);
}
console.log("Calculated changeAmount:", changeAmount);
//
// first, grab RandomOuts, then enter __createTx
preSuccess_nonTerminal_statusUpdate_fn(
SendFunds_ProcessStep_Code.fetchingDecoyOutputs,
);
hostedMoneroAPIClient.RandomOuts(usingOuts, mixin, function(
err,
amount_outs,
) {
if (err) {
__trampolineFor_err_withErr(err);
return;
}
__createTxAndAttemptToSend(amount_outs);
});
//
function __createTxAndAttemptToSend(mix_outs) {
preSuccess_nonTerminal_statusUpdate_fn(
SendFunds_ProcessStep_Code.constructingTransaction,
);
function printDsts(dsts)
{
for (var i = 0; i < dsts.length; i++) {
console.log(dsts[i].address + ": " + monero_amount_format_utils.formatMoneyFull(dsts[i].amount))
}
}
var create_transaction__retVals;
function ___createTxAndAttemptToSend(mix_outs)
{
preSuccess_nonTerminal_statusUpdate_fn(SendFunds_ProcessStep_Code.constructingTransaction);
var step2_retVals;
try {
create_transaction__retVals = monero_utils.create_signed_transaction(
step2_retVals = monero_utils.send_step2__try_create_transaction(
wallet__public_address,
wallet__private_keys,
target_address,
usingOuts,
step1_retVals.using_outs, // able to read this directly from step1 JSON
mix_outs,
mixin,
totalAmountWithoutFee_JSBigInt.toString(), // even though it's in dsts, sending it directly as core C++ takes it
changeAmount.toString(),
attemptAt_network_minimumFee.toString(), // must serialize for IPC
final__payment_id,
0,
isRingCT,
nettype,
step1_retVals.mixin,
step1_retVals.final_total_wo_fee,
step1_retVals.change_amount,
step1_retVals.using_fee,
payment_id,
simple_priority,
fee_per_b__string,
0, // unlock time
nettype
);
} catch (e) {
var errStr;
if (e) {
errStr = typeof e == "string" ? e : e.toString();
} else {
errStr = "Failed to create transaction with unknown error.";
errStr = "Failed to create transaction (step 2) with unknown error.";
}
__trampolineFor_err_withStr(errStr);
return;
}
console.log("created tx: ", JSON.stringify(create_transaction__retVals));
//
if (typeof create_transaction__retVals.err_msg !== 'undefined' && create_transaction__retVals.err_msg) {
// actually not expecting this! but just in case..
__trampolineFor_err_withStr(create_transaction__retVals.err_msg);
if (typeof step2_retVals.err_msg !== 'undefined' && step2_retVals.err_msg) { // actually not expecting this! but just in case..
__trampolineFor_err_withStr(step2_retVals.err_msg);
return;
}
var serialized_signedTx = create_transaction__retVals.signed_serialized_tx;
var tx_hash = create_transaction__retVals.tx_hash;
var tx_key = create_transaction__retVals.tx_key;
console.log("tx serialized: " + serialized_signedTx);
console.log("Tx hash: " + tx_hash);
console.log("Tx key: " + tx_key);
//
// work out per-kb fee for transaction and verify that it's enough
var txBlobBytes = serialized_signedTx.length / 2;
var numKB = Math.floor(txBlobBytes / 1024);
if (txBlobBytes % 1024) {
numKB++;
}
console.log(
txBlobBytes +
" bytes; current fee: " +
monero_amount_format_utils.formatMoneyFull(attemptAt_network_minimumFee) +
"",
);
const feeActuallyNeededByNetwork = new JSBigInt(
monero_utils.calculate_fee(
"" + feePerKB_JSBigInt,
txBlobBytes,
fee_multiplier_for_priority(simple_priority)
)
)
// if we need a higher fee
if (
feeActuallyNeededByNetwork.compare(
attemptAt_network_minimumFee,
) > 0
) {
console.log(
"💬 Need to reconstruct the tx with enough of a network fee. Previous fee: " +
monero_amount_format_utils.formatMoneyFull(
attemptAt_network_minimumFee,
) +
" New fee: " +
monero_amount_format_utils.formatMoneyFull(
feeActuallyNeededByNetwork,
),
);
if (step2_retVals.tx_must_be_reconstructed === true || step2_retVals.tx_must_be_reconstructed === "true") { // TODO
console.log("Need to reconstruct the tx with enough of a network fee");
// this will update status back to .calculatingFee
__reenterable_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
unusedOuts,
feePerKB_JSBigInt,
feeActuallyNeededByNetwork, // we are re-entering this codepath after changing this feeActuallyNeededByNetwork
if (constructionAttempt > 30) { // just going to avoid an infinite loop here
__trampolineFor_err_withStr("Unable to construct a transaction with sufficient fee for unknown reason.");
return;
}
__reenterable_constructAndSendTx(
step2_retVals.fee_actually_needed, // we are re-entering the step1->step2 codepath after updating fee_actually_needed
constructionAttempt + 1
);
//
return;
}
//
// generated with correct per-kb fee
const final_networkFee = attemptAt_network_minimumFee; // just to make things clear
console.log(
"💬 Successful tx generation, submitting tx. Going with final_networkFee of ",
monero_amount_format_utils.formatMoney(final_networkFee),
);
// Generated with correct fee
// console.log("tx serialized: " + step2_retVals.signed_serialized_tx);
// console.log("Tx hash: " + step2_retVals.tx_hash);
// console.log("Tx key: " + step2_retVals.tx_key);
// console.log("Successful tx generation; submitting.");
// status: submitting…
preSuccess_nonTerminal_statusUpdate_fn(
SendFunds_ProcessStep_Code.submittingTransaction,
);
preSuccess_nonTerminal_statusUpdate_fn(SendFunds_ProcessStep_Code.submittingTransaction);
hostedMoneroAPIClient.SubmitSerializedSignedTransaction(
wallet__public_address,
wallet__private_keys.view,
serialized_signedTx,
step2_retVals.signed_serialized_tx,
function(err) {
if (err) {
__trampolineFor_err_withStr(
"Something unexpected occurred when submitting your transaction: " +
err,
);
__trampolineFor_err_withStr("Something unexpected occurred when submitting your transaction: " + err);
return;
}
const tx_fee = final_networkFee; /*.add(hostingService_chargeAmount) NOTE: Service charge removed to reduce bloat for now */
__trampolineFor_success(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
tx_hash,
tx_fee,
tx_key,
mixin,
); // 🎉
},
const final_fee_amount = new JSBigInt(step1_retVals.using_fee)
const finalTotalWOFee_amount = new JSBigInt(step1_retVals.final_total_wo_fee)
var final__payment_id = payment_id;
if (final__payment_id === null || typeof final__payment_id == "undefined" || !final__payment_id) {
const decoded = monero_utils.decode_address(target_address, nettype);
if (decoded.intPaymentId && typeof decoded.intPaymentId !== 'undefined') {
final__payment_id = decoded.intPaymentId // just preserving original return value - this retVal can eventually be removed
}
}
success_fn( // TODO: port this to returning a dictionary
target_address, // TODO: remove this
finalTotalWOFee_amount.add(final_fee_amount), // total sent
final__payment_id,
step2_retVals.tx_hash,
final_fee_amount,
step2_retVals.tx_key,
parseInt(step1_retVals.mixin)
);
}
);
}
}
});
}
exports.SendFunds = SendFunds;
//
function new_moneroReadyTargetDescriptions_fromTargetDescriptions(
targetDescriptions,
nettype,
monero_utils,
fn, // fn: (err, moneroReady_targetDescriptions) -> Void
// TODO: remove this fn - this is a sync method now
) {
// parse & normalize the target descriptions by mapping them to currency (Monero)-ready addresses & amounts
// some pure function declarations for the map we'll do on targetDescriptions
//
const moneroReady_targetDescriptions = [];
for (var i = 0 ; i < targetDescriptions.length ; i++) {
const targetDescription = targetDescriptions[i];
if (!targetDescription.address && !targetDescription.amount) {
// PSNote: is this check rigorous enough?
const errStr =
"Please supply a target address and a target amount.";
const err = new Error(errStr);
fn(err);
return;
}
const targetDescription_address = targetDescription.address;
const targetDescription_amount = "" + targetDescription.amount; // we are converting it to a string here because parseMoney expects a string
// now verify/parse address and amount
if (targetDescription_address.indexOf('.') !== -1) { // assumed to be an OA address asXMR addresses do not have periods and OA addrs must
throw "You must resolve this OA address to a Monero address before calling SendFunds";
}
// otherwise this should be a normal, single Monero public address
try {
monero_utils.decode_address(targetDescription_address, nettype); // verify that the address is valid
} catch (e) {
const errStr =
"Couldn't decode address " +
targetDescription_address +
": " +
e;
const err = new Error(errStr);
fn(err);
return;
}
// amount:
var moneroReady_amountToSend; // possibly need this ; here for the JS parser
try {
moneroReady_amountToSend = monero_amount_format_utils.parseMoney(
targetDescription_amount,
);
} catch (e) {
const errStr =
"Couldn't parse amount " +
targetDescription_amount +
": " +
e;
const err = new Error(errStr);
fn(err);
return;
}
moneroReady_targetDescriptions.push({
address: targetDescription_address,
amount: moneroReady_amountToSend,
});
}
fn(null, moneroReady_targetDescriptions);
}
function __randomIndex(list) {
return Math.floor(Math.random() * list.length);
}
function _popAndReturnRandomElementFromList(list) {
var idx = __randomIndex(list);
var val = list[idx];
list.splice(idx, 1);
//
return val;
}
function _outputsAndAmountToUseForMixin(
target_amount,
unusedOuts,
isRingCT,
sweeping,
) {
console.log(
"Selecting outputs to use. target: " +
monero_amount_format_utils.formatMoney(target_amount),
);
var toFinalize_usingOutsAmount = new JSBigInt(0);
const toFinalize_usingOuts = [];
const remaining_unusedOuts = unusedOuts.slice(); // take copy so as to prevent issue if we must re-enter tx building fn if fee too low after building
while (
toFinalize_usingOutsAmount.compare(target_amount) < 0 &&
remaining_unusedOuts.length > 0
) {
var out = _popAndReturnRandomElementFromList(remaining_unusedOuts);
if (!isRingCT && out.rct) {
// out.rct is set by the server
continue; // skip rct outputs if not creating rct tx
}
const out_amount_JSBigInt = new JSBigInt(out.amount);
if (out_amount_JSBigInt.compare(monero_config.dustThreshold) < 0) {
// amount is dusty..
if (sweeping == false) {
console.log(
"Not sweeping, and found a dusty (though maybe mixable) output... skipping it!",
);
continue;
}
if (!out.rct || typeof out.rct === "undefined") {
console.log(
"Sweeping, and found a dusty but unmixable (non-rct) output... skipping it!",
);
continue;
} else {
console.log(
"Sweeping and found a dusty but mixable (rct) amount... keeping it!",
);
}
}
toFinalize_usingOuts.push(out);
toFinalize_usingOutsAmount = toFinalize_usingOutsAmount.add(
out_amount_JSBigInt,
);
console.log(
"Using output: " +
monero_amount_format_utils.formatMoney(out_amount_JSBigInt) +
" - " +
JSON.stringify(out),
);
}
return {
usingOuts: toFinalize_usingOuts,
usingOutsAmount: toFinalize_usingOutsAmount,
remaining_unusedOuts: remaining_unusedOuts,
};
}
function decompose_amount_into_digits(amount)
{
/*if (dust_threshold === undefined) {
dust_threshold = config.dustThreshold;
}*/
amount = amount.toString();
var ret = [];
while (amount.length > 0) {
//split all the way down since v2 fork
/*var remaining = new JSBigInt(amount);
if (remaining.compare(config.dustThreshold) <= 0) {
if (remaining.compare(0) > 0) {
ret.push(remaining);
}
break;
}*/
//check so we don't create 0s
if (amount[0] !== "0") {
var digit = amount[0];
while (digit.length < amount.length) {
digit += "0";
}
ret.push(new JSBigInt(digit));
}
amount = amount.slice(1);
}
return ret;
}
function decompose_tx_destinations(dsts, rct, serializeForIPC)
{
var out = [];
if (rct) {
for (var i = 0; i < dsts.length; i++) {
out.push({
address: dsts[i].address,
amount: serializeForIPC ? dsts[i].amount.toString() : dsts[i].amount,
});
}
} else {
for (var i = 0; i < dsts.length; i++) {
var digits = decompose_amount_into_digits(dsts[i].amount);
for (var j = 0; j < digits.length; j++) {
if (digits[j].compare(0) > 0) {
out.push({
address: dsts[i].address,
amount: serializeForIPC ? digits[j].toString() : digits[j],
});
}
}
}
}
return out.sort(function(a, b) {
return a["amount"] - b["amount"];
});
};
exports.SendFunds = SendFunds;

@ -37,7 +37,8 @@
EMSCRIPTEN_BINDINGS(my_module)
{ // C++ -> JS
//
emscripten::function("create_transaction", &serial_bridge::create_transaction);
emscripten::function("send_step1__prepare_params_for_get_decoys", &serial_bridge::send_step1__prepare_params_for_get_decoys);
emscripten::function("send_step2__try_create_transaction", &serial_bridge::send_step2__try_create_transaction);
//
emscripten::function("decode_address", &serial_bridge::decode_address);
emscripten::function("is_subaddress", &serial_bridge::is_subaddress);
@ -53,8 +54,6 @@ EMSCRIPTEN_BINDINGS(my_module)
emscripten::function("validate_components_for_login", &serial_bridge::validate_components_for_login);
emscripten::function("address_and_keys_from_seed", &serial_bridge::address_and_keys_from_seed);
//
emscripten::function("estimate_rct_tx_size", &serial_bridge::estimate_rct_tx_size);
emscripten::function("calculate_fee", &serial_bridge::calculate_fee);
emscripten::function("estimated_tx_network_fee", &serial_bridge::estimated_tx_network_fee);
//
emscripten::function("generate_key_image", &serial_bridge::generate_key_image);

@ -1 +1 @@
Subproject commit 4ae115f8427621b96c8e12fac8ac90c7b2a39ea2
Subproject commit ee3eede5d20668c720c2fb86fd163f4ec0bfee9b

File diff suppressed because one or more lines are too long

@ -43,35 +43,10 @@ async function t1()
console.log(e)
}
try {
var tx_size = (await mymonero.monero_utils_promise).estimate_rct_tx_size(
2, // inputs
6,
2, // outputs
0, //optl__extra_size,
true // optl__bulletproof
);
console.log("estimate_rct_tx_size", tx_size)
} catch (e) {
console.log(e)
}
try {
var fee = new mymonero.JSBigInt((await mymonero.monero_utils_promise).calculate_fee(
"9000000", 13762, 4
// fee_per_kb__string, num_bytes, fee_multiplier
));
console.log("calculate_fee", mymonero.monero_amount_format_utils.formatMoneyFull(fee), "XMR")
} catch (e) {
console.log(e)
}
try {
var fee = new mymonero.JSBigInt((await mymonero.monero_utils_promise).estimated_tx_network_fee(
"9000000", 2
// fee_per_kb__string, priority
"0", 1, "24658"
// fee_per_kb__string, priority, fee_per_b__string
));
console.log("estimated_tx_network_fee", mymonero.monero_amount_format_utils.formatMoneyFull(fee), "XMR")
} catch (e) {

@ -82,6 +82,109 @@ describe("cryptonote_utils tests", function() {
assert.deepEqual(decoded, expected);
});
it("create tx: non-sweep single-output", async function() {
const monero_utils = await require("../monero_utils/monero_utils")
const unspent_outputs = [
{
"amount":"3000000000",
"public_key":"41be1978f58cabf69a9bed5b6cb3c8d588621ef9b67602328da42a213ee42271",
"index":1,
"global_index":7611174,
"rct":"86a2c9f1f8e66848cd99bfda7a14d4ac6c3525d06947e21e4e55fe42a368507eb5b234ccdd70beca8b1fc8de4f2ceb1374e0f1fd8810849e7f11316c2cc063060008ffa5ac9827b776993468df21af8c963d12148622354f950cbe1369a92a0c",
"tx_id":5334971,
"tx_hash":"9d37c7fdeab91abfd1e7e120f5c49eac17b7ac04a97a0c93b51c172115df21ea",
"tx_pub_key":"bd703d7f37995cc7071fb4d2929594b5e2a4c27d2b7c68a9064500ca7bc638b8"
}
]
const fee_per_b = "24658"
const step1_retVals = monero_utils.send_step1__prepare_params_for_get_decoys(
false, // sweeping
"200000000", // sending_amount
fee_per_b, // fee_per_b,
1, // priority,
unspent_outputs,
null,// optl__payment_id_string, // this may be nil
null // optl__passedIn_attemptAt_fee
)
assert.equal(
step1_retVals.mixin,
10,
);
assert.equal(
step1_retVals.using_outs.length,
1,
);
assert.equal(
step1_retVals.change_amount,
"2733990534",
);
assert.equal(
step1_retVals.final_total_wo_fee,
"200000000",
);
assert.equal(
step1_retVals.using_fee,
"66009466",
);
const mix_outs = [
{
"amount":"0",
"outputs":[
{"global_index":"7453099","public_key":"31f3a7fec0f6f09067e826b6c2904fd4b1684d7893dcf08c5b5d22e317e148bb","rct":"ea6bcb193a25ce2787dd6abaaeef1ee0c924b323c6a5873db1406261e86145fc"},
{"global_index":"7500097","public_key":"f9d923500671da05a1bf44b932b872f0c4a3c88e6b3d4bf774c8be915e25f42b","rct":"dcae4267a6c382bcd71fd1af4d2cbceb3749d576d7a3acc473dd579ea9231a52"},
{"global_index":"7548483","public_key":"839cbbb73685654b93e824c4843e745e8d5f7742e83494932307bf300641c480","rct":"aa99d492f1d6f1b20dcd95b8fff8f67a219043d0d94b4551759016b4888573e7"},
{"global_index":"7554755","public_key":"b8860f0697988c8cefd7b4285fbb8bec463f136c2b9a9cadb3e57cebee10717f","rct":"327f9b07bee9c4c25b5a990123cd2444228e5704ebe32016cd632866710279b5"},
{"global_index":"7561477","public_key":"561d734cb90bc4a64d49d37f85ea85575243e2ed749a3d6dcb4d27aa6bec6e88","rct":"b5393e038df95b94bfda62b44a29141cac9e356127270af97193460d51949841"},
{"global_index":"7567062","public_key":"db1024ef67e7e73608ef8afab62f49e2402c8da3dc3197008e3ba720ad3c94a8","rct":"1fedf95621881b77f823a70aa83ece26aef62974976d2b8cd87ed4862a4ec92c"},
{"global_index":"7567508","public_key":"6283f3cd2f050bba90276443fe04f6076ad2ad46a515bf07b84d424a3ba43d27","rct":"10e16bb8a8b7b0c8a4b193467b010976b962809c9f3e6c047335dba09daa351f"},
{"global_index":"7568716","public_key":"7a7deb4eef81c1f5ce9cbd0552891cb19f1014a03a5863d549630824c7c7c0d3","rct":"735d059dc3526334ac705ddc44c4316bb8805d2426dcea9544cde50cf6c7a850"},
{"global_index":"7571196","public_key":"535208e354cae530ed7ce752935e555d630cf2edd7f91525024ed9c332b2a347","rct":"c3cf838faa14e993536c5581ca582fb0d96b70f713cf88f7f15c89336e5853ec"},
{"global_index":"7571333","public_key":"e73f27b7eb001aa7eac13df82814cda65b42ceeb6ef36227c25d5cbf82f6a5e4","rct":"5f45f33c6800cdae202b37abe6d87b53d6873e7b30f3527161f44fa8db3104b6"},
{"global_index":"7571335","public_key":"fce982db8e7a6b71a1e632c7de8c5cbf54e8bacdfbf250f1ffc2a8d2f7055ce3","rct":"407bdcc48e70eb3ef2cc22cefee6c6b5a3c59fd17bde12fda5f1a44a0fb39d14"}
]
}
]
assert.equal(
mix_outs.length,
step1_retVals.using_outs.length
)
assert.equal(
mix_outs[0].outputs.length,
step1_retVals.mixin + 1
)
const step2_retVals = monero_utils.send_step2__try_create_transaction(
"43zxvpcj5Xv9SEkNXbMCG7LPQStHMpFCQCmkmR4u5nzjWwq5Xkv5VmGgYEsHXg4ja2FGRD5wMWbBVMijDTqmmVqm93wHGkg", // from_address_string,
{ // sec keys
view: "7bea1907940afdd480eff7c4bcadb478a0fbb626df9e3ed74ae801e18f53e104",
spend: "4e6d43cd03812b803c6f3206689f5fcc910005fc7e91d50d79b0776dbefcd803"
},
"4APbcAKxZ2KPVPMnqa5cPtJK25tr7maE7LrJe67vzumiCtWwjDBvYnHZr18wFexJpih71Mxsjv8b7EpQftpB9NjPPXmZxHN", // to_address_string,
step1_retVals.using_outs, // using_outs,
mix_outs, // mix_outs,
step1_retVals.mixin, // fake_outputs_count,
step1_retVals.final_total_wo_fee, // final sending_amount
step1_retVals.change_amount, // change_amount,
step1_retVals.using_fee, // fee_amount,
null, // payment_id,
1, // priority,
fee_per_b, // fee_per_b,
0, // unlock_time,
nettype // nettype
)
assert.equal(
step2_retVals.tx_must_be_reconstructed,
false
);
assert.notEqual(
step2_retVals.signed_serialized_tx,
null
);
assert.notEqual(
step2_retVals.signed_serialized_tx,
undefined
);
});
// not implemented
// it("hash_to_scalar", async function() {
// const monero_utils = await require("../monero_utils/monero_utils")

@ -0,0 +1,302 @@
// Copyright (c) 2014-2018, MyMonero.com
//
// 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.
"use strict";
const mymonero_core_js = require("../");
const net_service_utils = require('../hostAPI/net_service_utils')
const monero_config = require('../monero_utils/monero_config')
const JSBigInt = mymonero_core_js.JSBigInt;
const assert = require("assert");
class APIClient
{
constructor(options)
{
const self = this
self.options = options
self.fetch = options.fetch
if (self.fetch == null || typeof self.fetch == 'undefined') {
throw "APIClient requires options.fetch"
}
self.hostBaseURL = options.hostBaseURL || "http://localhost:9100/" // must include trailing /
}
//
// Getting outputs for sending funds
UnspentOuts(
address, view_key__private,
spend_key__public, spend_key__private,
mixinNumber, sweeping,
fn
) { // -> RequestHandle
const self = this
mixinNumber = parseInt(mixinNumber) // jic
//
const parameters = net_service_utils.New_ParametersForWalletRequest(address, view_key__private)
parameters.amount = '0'
parameters.mixin = mixinNumber
parameters.use_dust = true // Client now filters unmixable by dustthreshold amount (unless sweeping) + non-rct
parameters.dust_threshold = mymonero_core_js.monero_config.dustThreshold.toString()
const endpointPath = 'get_unspent_outs'
self.fetch.post(
self.hostBaseURL + endpointPath, parameters
).then(function(data) {
__proceedTo_parseAndCallBack(data)
}).catch(function(e) {
fn(e && e.Error ? e.Error : ""+e);
});
function __proceedTo_parseAndCallBack(data)
{
mymonero_core_js.monero_utils_promise.then(function(monero_utils)
{
mymonero_core_js.api_response_parser_utils.Parsed_UnspentOuts__keyImageManaged(
data,
address,
view_key__private,
spend_key__public,
spend_key__private,
monero_utils,
function(err, returnValuesByKey)
{
if (err) {
fn(err)
return
}
const per_kb_fee__String = returnValuesByKey.per_kb_fee
if (per_kb_fee__String == null || per_kb_fee__String == "" || typeof per_kb_fee__String === 'undefined') {
throw "Unexpected / missing per_kb_fee"
}
fn(
err, // no error
returnValuesByKey.unspentOutputs,
returnValuesByKey.unspentOutputs, // TODO: remove this - it was the unused 'unusedOutputs'
new JSBigInt(per_kb_fee__String)
)
}
)
}).catch(function(err)
{
fn(err)
})
}
const requestHandle =
{
abort: function()
{
console.warn("TODO: abort!")
}
}
return requestHandle
}
//
RandomOuts(using_outs, mixinNumber, fn)
{ // -> RequestHandle
const self = this
//
mixinNumber = parseInt(mixinNumber)
if (mixinNumber < 0 || isNaN(mixinNumber)) {
const errStr = "Invalid mixin - must be >= 0"
const err = new Error(errStr)
fn(err)
return
}
//
var amounts = [];
for (var l = 0; l < using_outs.length; l++) {
amounts.push(using_outs[l].rct ? "0" : using_outs[l].amount.toString())
}
//
var parameters =
{
amounts: amounts,
count: mixinNumber + 1 // Add one to mixin so we can skip real output key if necessary
}
const endpointPath = 'get_random_outs'
self.fetch.post(
self.hostBaseURL + endpointPath, parameters
).then(function(data) {
__proceedTo_parseAndCallBack(data)
}).catch(function(e) {
fn(e && e.Error ? e.Error : ""+e);
});
function __proceedTo_parseAndCallBack(data)
{
console.log("debug: info: random outs: data", data)
const amount_outs = data.amount_outs
// yield
fn(null, amount_outs)
}
const requestHandle =
{
abort: function()
{
console.warn("TODO: abort!")
}
}
return requestHandle
}
//
// Runtime - Imperatives - Public - Sending funds
SubmitSerializedSignedTransaction(
address, view_key__private,
serializedSignedTx,
fn // (err?) -> RequestHandle
) {
const self = this
//
const parameters = net_service_utils.New_ParametersForWalletRequest(address, view_key__private)
parameters.tx = serializedSignedTx
const endpointPath = 'submit_raw_tx'
self.fetch.post(
self.hostBaseURL + endpointPath, parameters
).then(function(data) {
__proceedTo_parseAndCallBack(data)
}).catch(function(e) {
fn(e && e.Error ? e.Error : ""+e);
});
function __proceedTo_parseAndCallBack(data)
{
fn(null)
}
const requestHandle =
{
abort: function()
{
console.warn("TODO: abort!")
}
}
return requestHandle
}
}
//
// This fetch API is of course not accurate
class Fetch
{
constructor()
{
}
post(url, params)
{
return new Promise(function(resolve, reject)
{
console.log("Mocked fetch url", url, params)
if (url.indexOf("get_unspent_outs") !== -1) {
resolve({
outputs: [
{
"amount":"3000000000",
"public_key":"41be1978f58cabf69a9bed5b6cb3c8d588621ef9b67602328da42a213ee42271",
"index":1,
"global_index":7611174,
"rct":"86a2c9f1f8e66848cd99bfda7a14d4ac6c3525d06947e21e4e55fe42a368507eb5b234ccdd70beca8b1fc8de4f2ceb1374e0f1fd8810849e7f11316c2cc063060008ffa5ac9827b776993468df21af8c963d12148622354f950cbe1369a92a0c",
"tx_id":5334971,
"tx_hash":"9d37c7fdeab91abfd1e7e120f5c49eac17b7ac04a97a0c93b51c172115df21ea",
"tx_pub_key":"bd703d7f37995cc7071fb4d2929594b5e2a4c27d2b7c68a9064500ca7bc638b8",
"spend_key_images": [
"3d92d42a105c231997b2fcb13b07ea1526fd4f709daaa8b9157608db387065f9"
]
} // NOTE: we'd have more in the real reply - and even the api response parser doesn't care about those values right now
],
per_kb_fee: parseInt("24658"/*for str search*/) * 1024 // scale the per b we know up to per kib (so it can be scaled back down - interrim until all clients are ready for per b fee)
})
} else if (url.indexOf("get_random_outs") !== -1) {
resolve({
amount_outs: [
{
"amount":"0",
"outputs":[
{"global_index":"7453099","public_key":"31f3a7fec0f6f09067e826b6c2904fd4b1684d7893dcf08c5b5d22e317e148bb","rct":"ea6bcb193a25ce2787dd6abaaeef1ee0c924b323c6a5873db1406261e86145fc"},
{"global_index":"7500097","public_key":"f9d923500671da05a1bf44b932b872f0c4a3c88e6b3d4bf774c8be915e25f42b","rct":"dcae4267a6c382bcd71fd1af4d2cbceb3749d576d7a3acc473dd579ea9231a52"},
{"global_index":"7548483","public_key":"839cbbb73685654b93e824c4843e745e8d5f7742e83494932307bf300641c480","rct":"aa99d492f1d6f1b20dcd95b8fff8f67a219043d0d94b4551759016b4888573e7"},
{"global_index":"7554755","public_key":"b8860f0697988c8cefd7b4285fbb8bec463f136c2b9a9cadb3e57cebee10717f","rct":"327f9b07bee9c4c25b5a990123cd2444228e5704ebe32016cd632866710279b5"},
{"global_index":"7561477","public_key":"561d734cb90bc4a64d49d37f85ea85575243e2ed749a3d6dcb4d27aa6bec6e88","rct":"b5393e038df95b94bfda62b44a29141cac9e356127270af97193460d51949841"},
{"global_index":"7567062","public_key":"db1024ef67e7e73608ef8afab62f49e2402c8da3dc3197008e3ba720ad3c94a8","rct":"1fedf95621881b77f823a70aa83ece26aef62974976d2b8cd87ed4862a4ec92c"},
{"global_index":"7567508","public_key":"6283f3cd2f050bba90276443fe04f6076ad2ad46a515bf07b84d424a3ba43d27","rct":"10e16bb8a8b7b0c8a4b193467b010976b962809c9f3e6c047335dba09daa351f"},
{"global_index":"7568716","public_key":"7a7deb4eef81c1f5ce9cbd0552891cb19f1014a03a5863d549630824c7c7c0d3","rct":"735d059dc3526334ac705ddc44c4316bb8805d2426dcea9544cde50cf6c7a850"},
{"global_index":"7571196","public_key":"535208e354cae530ed7ce752935e555d630cf2edd7f91525024ed9c332b2a347","rct":"c3cf838faa14e993536c5581ca582fb0d96b70f713cf88f7f15c89336e5853ec"},
{"global_index":"7571333","public_key":"e73f27b7eb001aa7eac13df82814cda65b42ceeb6ef36227c25d5cbf82f6a5e4","rct":"5f45f33c6800cdae202b37abe6d87b53d6873e7b30f3527161f44fa8db3104b6"},
{"global_index":"7571335","public_key":"fce982db8e7a6b71a1e632c7de8c5cbf54e8bacdfbf250f1ffc2a8d2f7055ce3","rct":"407bdcc48e70eb3ef2cc22cefee6c6b5a3c59fd17bde12fda5f1a44a0fb39d14"}
]
}
]
})
} else if (url.indexOf("submit_raw_tx") !== -1) {
resolve({}) // mocking tx submission success
} else {
reject("Fetch implementation doesn't know how to get url: " + url);
}
})
}
}
describe("sendingFunds tests", function()
{
it("can send", async function()
{
mymonero_core_js.monero_sendingFunds_utils.SendFunds(
"4L6Gcy9TAHqPVPMnqa5cPtJK25tr7maE7LrJe67vzumiCtWwjDBvYnHZr18wFexJpih71Mxsjv8b7EpQftpB9NjPaRYYBm62jmF59EWcj6", // target_address,
mymonero_core_js.nettype_utils.network_type.MAINNET,
"0.0002",// amount_orZeroWhenSweep,
false,// isSweep_orZeroWhenAmount,
"43zxvpcj5Xv9SEkNXbMCG7LPQStHMpFCQCmkmR4u5nzjWwq5Xkv5VmGgYEsHXg4ja2FGRD5wMWbBVMijDTqmmVqm93wHGkg",// wallet__public_address,
{view:"7bea1907940afdd480eff7c4bcadb478a0fbb626df9e3ed74ae801e18f53e104",spend:"4e6d43cd03812b803c6f3206689f5fcc910005fc7e91d50d79b0776dbefcd803"},// wallet__private_keys,
{view:"080a6e9b17de47ec62c8a1efe0640b554a2cde7204b9b07bdf9bd225eeeb1c47",spend:"3eb884d3440d71326e27cc07a861b873e72abd339feb654660c36a008a0028b3"},// wallet__public_keys,
new APIClient({ fetch: new Fetch() }),
null,// payment_id,
1,// simple_priority,
function(code)
{
console.log("Send funds step " + code + ": " + mymonero_core_js.monero_sendingFunds_utils.SendFunds_ProcessStep_MessageSuffix[code])
},
function(to_addr, sentAmount, final__payment_id, tx_hash, tx_fee, tx_key, mixin)
{
assert.equal(mixin, 10);
assert.equal(sentAmount.toString(), "266009466")
assert.equal(final__payment_id, "d2f602b240fbe624")
console.log("Sendfunds success")
console.log("sentAmount", sentAmount.toString())
console.log("final__payment_id", final__payment_id)
console.log("tx_hash", tx_hash)
console.log("tx_fee", tx_fee.toString())
console.log("tx_key", tx_key)
},
function(err)
{
console.error("SendFunds err:", err)
assert.notEqual(
err,
null
);
assert.notEqual(
err,
undefined
);
// ^-- I'm not confident these are tripping
}
)
});
});
Loading…
Cancel
Save