You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mymonero-core-js/monero_utils/monero_sendingFunds_utils.js

927 lines
29 KiB

// 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 monero_config = require("./monero_config");
const monero_utils_promise = require('./monero_utils')
const monero_amount_format_utils = require("./monero_amount_format_utils");
const monero_paymentID_utils = require("./monero_paymentID_utils");
const JSBigInt = require("../cryptonote_utils/biginteger").BigInteger;
//
const hostAPI_net_service_utils = require("../hostAPI/net_service_utils");
//
function _forkv7_minimumMixin() {
return 6;
}
function _mixinToRingsize(mixin) {
return mixin + 1;
}
//
function thisFork_minMixin() {
return _forkv7_minimumMixin();
}
function thisFork_minRingSize() {
return _mixinToRingsize(thisFork_minMixin());
}
exports.thisFork_minMixin = thisFork_minMixin;
exports.thisFork_minRingSize = thisFork_minRingSize;
//
function fixedMixin() {
return thisFork_minMixin(); /* using the monero app default to remove MM user identifiers */
}
function fixedRingsize() {
return _mixinToRingsize(fixedMixin());
}
exports.fixedMixin = fixedMixin;
exports.fixedRingsize = fixedRingsize;
//
//
function default_priority() {
return 1;
} // aka .low
exports.default_priority = default_priority;
//
function estimateRctSize(inputs, mixin, outputs, extra_size, bulletproof)
{
// keeping this in JS instead of C++ for now b/c it's much faster to access, and we don't have to make it asynchronous by waiting for the module to load
bulletproof = bulletproof == true ? true : false
extra_size = extra_size || 40
//
var size = 0;
// tx prefix
// first few bytes
size += 1 + 6;
size += inputs * (1 + 6 + (mixin + 1) * 2 + 32);
// vout
size += outputs * (6 + 32);
// extra
size += extra_size;
// rct signatures
// type
size += 1;
// rangeSigs
if (bulletproof)
size += ((2*6 + 4 + 5)*32 + 3) * outputs;
else
size += (2*64*32+32+64*32) * outputs;
// MGs
size += inputs * (64 * (mixin + 1) + 32);
// mixRing - not serialized, can be reconstructed
/* size += 2 * 32 * (mixin+1) * inputs; */
// pseudoOuts
size += 32 * inputs;
// ecdhInfo
size += 2 * 32 * outputs;
// outPk - only commitment is saved
size += 32 * outputs;
// txnFee
size += 4;
// const logStr = `estimated rct tx size for ${inputs} at mixin ${mixin} and ${outputs} : ${size} (${((32 * inputs/*+1*/) + 2 * 32 * (mixin+1) * inputs + 32 * outputs)}) saved)`
// console.log(logStr)
return size;
};
function calculate_fee(fee_per_kb_JSBigInt, numberOf_bytes, fee_multiplier) {
const numberOf_kB_JSBigInt = new JSBigInt(
(numberOf_bytes + 1023.0) / 1024.0,
); // i.e. ceil
//
return calculate_fee__kb(
fee_per_kb_JSBigInt,
numberOf_kB_JSBigInt,
fee_multiplier,
);
}
function calculate_fee__kb(fee_per_kb_JSBigInt, numberOf_kb, fee_multiplier) {
const numberOf_kB_JSBigInt = new JSBigInt(numberOf_kb);
const fee = fee_per_kb_JSBigInt
.multiply(fee_multiplier)
.multiply(numberOf_kB_JSBigInt);
//
return fee;
}
const newer_multipliers = [1, 4, 20, 166];
function fee_multiplier_for_priority(priority__or0ForDefault) {
const final_priorityInt =
!priority__or0ForDefault || priority__or0ForDefault == 0
? default_priority()
: priority__or0ForDefault;
if (
final_priorityInt <= 0 ||
final_priorityInt > newer_multipliers.length
) {
throw "fee_multiplier_for_priority: simple_priority out of bounds";
}
const priority_as_idx = final_priorityInt - 1;
return newer_multipliers[priority_as_idx];
}
function EstimatedTransaction_networkFee(
nonZero_mixin_int,
feePerKB_JSBigInt,
simple_priority,
) {
const numberOf_inputs = 2; // this might change -- might select inputs
const numberOf_outputs =
1 /*dest*/ + 1 /*change*/ + 0; /*no mymonero fee presently*/
// TODO: update est tx size for bulletproofs
// TODO: normalize est tx size fn naming
const estimated_txSize = estimateRctSize(
numberOf_inputs,
nonZero_mixin_int,
numberOf_outputs,
);
const estimated_fee = calculate_fee(
feePerKB_JSBigInt,
estimated_txSize,
fee_multiplier_for_priority(simple_priority),
);
//
return estimated_fee;
}
exports.EstimatedTransaction_networkFee = EstimatedTransaction_networkFee;
//
const SendFunds_ProcessStep_Code = {
fetchingLatestBalance: 1,
calculatingFee: 2,
fetchingDecoyOutputs: 3, // may get skipped if 0 mixin
constructingTransaction: 4, // may go back to .calculatingFee
submittingTransaction: 5,
};
exports.SendFunds_ProcessStep_Code = SendFunds_ProcessStep_Code;
const SendFunds_ProcessStep_MessageSuffix = {
1: "Fetching latest balance.",
2: "Calculating fee.",
3: "Fetching decoy outputs.",
4: "Constructing transaction.", // may go back to .calculatingFee
5: "Submitting transaction.",
};
exports.SendFunds_ProcessStep_MessageSuffix = SendFunds_ProcessStep_MessageSuffix;
//
function SendFunds(
target_address, // currency-ready wallet address, but not an OA address (resolve before calling)
nettype,
amount_orZeroWhenSweep, // number - value will be ignoring for sweep
isSweep_orZeroWhenAmount, // send true to sweep - amount_orZeroWhenSweep will be ignored
wallet__public_address,
wallet__private_keys,
wallet__public_keys,
hostedMoneroAPIClient, // TODO: possibly factor this dependency
monero_openalias_utils,
payment_id,
simple_priority,
preSuccess_nonTerminal_statusUpdate_fn, // (_ stepCode: SendFunds_ProcessStep_Code) -> Void
success_fn,
// success_fn: (
// moneroReady_targetDescription_address?,
// sentAmount?,
// final__payment_id?,
// tx_hash?,
// tx_fee?,
// tx_key?,
// mixin?,
// )
failWithErr_fn,
// failWithErr_fn: (
// err
// )
) {
monero_utils_promise.then(function(monero_utils)
{
const mixin = fixedMixin();
var isRingCT = true;
var sweeping = isSweep_orZeroWhenAmount === true; // rather than, say, undefined
//
// 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);
}
function __trampolineFor_err_withStr(errStr) {
const err = new Error(errStr);
console.error(errStr);
failWithErr_fn(err);
}
//
// parse & normalize the target descriptions by mapping them to Monero addresses & amounts
var amount = sweeping ? 0 : amount_orZeroWhenSweep;
const targetDescription = {
address: target_address,
amount: amount,
};
new_moneroReadyTargetDescriptions_fromTargetDescriptions(
monero_openalias_utils,
[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) {
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;
}
_proceedTo_prepareToSendFundsTo_moneroReady_targetDescription(
moneroReady_targetDescription,
);
},
);
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 final__pid_encrypt = false; // we don't want to encrypt payment ID unless we find an integrated one
var address__decode_result;
try {
address__decode_result = monero_utils.decode_address(
moneroReady_targetDescription_address,
nettype,
);
} 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;
}
}
if (address__decode_result.intPaymentId) {
final__payment_id = address__decode_result.intPaymentId;
final__pid_encrypt = true; // we do want to encrypt if using an integrated address
} 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,
final__pid_encrypt,
);
}
function _proceedTo_getUnspentOutsUsableForMixin(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id, // non-existent or valid
final__pid_encrypt, // true or false
) {
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) {
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,
final__pid_encrypt,
unusedOuts,
dynamic_feePerKB_JSBigInt,
);
},
);
}
function _proceedTo_constructFundTransferListAndSendFundsByUsingUnusedUnspentOutsForMixin(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
final__pid_encrypt,
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_minimumFee = calculate_fee__kb(
feePerKB_JSBigInt,
network_minimumTXSize_kb,
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,
final__pid_encrypt,
unusedOuts,
feePerKB_JSBigInt, // obtained from server, so passed in
network_minimumFee,
);
}
function __reenterable_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
final__pid_encrypt,
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 = calculate_fee(
feePerKB_JSBigInt,
estimateRctSize(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 = calculate_fee(
feePerKB_JSBigInt,
estimateRctSize(
usingOuts.length,
mixin,
2,
),
fee_multiplier_for_priority(simple_priority),
);
totalAmountIncludingFees = totalAmountWithoutFee_JSBigInt.add(
newNeededFee,
);
}
}
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;
try {
create_transaction__retVals = monero_utils.create_signed_transaction(
wallet__public_address,
wallet__private_keys,
target_address,
usingOuts,
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,
);
console.log("got back", create_transaction__retVals)
} catch (e) {
var errStr;
if (e) {
errStr = typeof e == "string" ? e : e.toString();
} else {
errStr = "Failed to create transaction 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);
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 <= " +
numKB +
" KB (current fee: " +
monero_amount_format_utils.formatMoneyFull(attemptAt_network_minimumFee) +
")",
);
const feeActuallyNeededByNetwork = calculate_fee__kb(
feePerKB_JSBigInt,
numKB,
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,
),
);
// this will update status back to .calculatingFee
__reenterable_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
moneroReady_targetDescription_address,
totalAmountWithoutFee_JSBigInt,
final__payment_id,
final__pid_encrypt,
unusedOuts,
feePerKB_JSBigInt,
feeActuallyNeededByNetwork, // we are re-entering this codepath after changing this feeActuallyNeededByNetwork
);
//
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),
);
// status: submitting…
preSuccess_nonTerminal_statusUpdate_fn(
SendFunds_ProcessStep_Code.submittingTransaction,
);
hostedMoneroAPIClient.SubmitSerializedSignedTransaction(
wallet__public_address,
wallet__private_keys.view,
serialized_signedTx,
function(err) {
if (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,
sweeping
? parseFloat(
monero_amount_format_utils.formatMoneyFull(
totalAmountWithoutFee_JSBigInt,
),
)
: amount,
final__payment_id,
tx_hash,
tx_fee,
tx_key,
mixin,
); // 🎉
},
);
}
}
});
}
exports.SendFunds = SendFunds;
//
function new_moneroReadyTargetDescriptions_fromTargetDescriptions(
monero_openalias_utils,
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 (
monero_openalias_utils.DoesStringContainPeriodChar_excludingAsXMRAddress_qualifyingAsPossibleOAAddress(
targetDescription_address,
) == true
) {
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"];
});
};