// 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 async = require ( "async" ) ;
//
const monero _config = require ( "./monero_config" ) ;
const monero _utils = require ( "./monero_cryptonote_utils_instance" ) ;
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 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 = monero _utils . 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 ,
mixin ,
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?
// )
failWithErr _fn ,
// failWithErr_fn: (
// err
// )
) {
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 ,
) {
success _fn (
moneroReady _targetDescription _address ,
sentAmount ,
final _ _payment _id ,
tx _hash ,
tx _fee ,
) ;
}
function _ _trampolineFor _err _withErr ( err ) {
failWithErr _fn ( err ) ;
}
function _ _trampolineFor _err _withStr ( errStr ) {
const err = new Error ( errStr ) ;
console . error ( errStr ) ;
failWithErr _fn ( err ) ;
}
if ( mixin < thisFork _minMixin ( ) ) {
_ _trampolineFor _err _withStr ( "Ringsize is below the minimum." ) ;
return ;
}
//
// 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 ,
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 ,
monero _utils . 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 ,
monero _utils . 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 ;
}
// Now we can put together the list of fund transfers we need to perform
const fundTransferDescriptions = [ ] ; // to build…
// I. the actual transaction the user is asking to do
fundTransferDescriptions . push ( {
address : moneroReady _targetDescription _address ,
amount : totalAmountWithoutFee _JSBigInt ,
} ) ;
// II. the fee that the hosting provider charges
// NOTE: The fee has been removed for RCT until a later date
// fundTransferDescriptions.push({
// address: hostedMoneroAPIClient.HostingServiceFeeDepositAddress(),
// amount: hostingService_chargeAmount
// })
// III. some amount of the total outputs will likely need to be returned to the user as "change":
if ( usingOutsAmount _comparedTo _totalAmount > 0 ) {
if ( sweeping ) {
throw "Unexpected usingOutsAmount_comparedTo_totalAmount > 0 && sweeping" ;
}
var changeAmount = usingOutsAmount . subtract (
totalAmountIncludingFees ,
) ;
console . log ( "changeAmount" , changeAmount ) ;
if ( isRingCT ) {
// for RCT we don't presently care about dustiness so add entire change amount
console . log (
"Sending change of " +
monero _amount _format _utils . formatMoneySymbol ( changeAmount ) +
" to " +
wallet _ _public _address ,
) ;
fundTransferDescriptions . push ( {
address : wallet _ _public _address ,
amount : changeAmount ,
} ) ;
} else {
// pre-ringct
// do not give ourselves change < dust threshold
var changeAmountDivRem = changeAmount . divRem (
monero _config . dustThreshold ,
) ;
console . log ( "💬 changeAmountDivRem" , changeAmountDivRem ) ;
if ( changeAmountDivRem [ 1 ] . toString ( ) !== "0" ) {
// miners will add dusty change to fee
console . log (
"💬 Miners will add change of " +
monero _amount _format _utils . formatMoneyFullSymbol (
changeAmountDivRem [ 1 ] ,
) +
" to transaction fee (below dust threshold)" ,
) ;
}
if ( changeAmountDivRem [ 0 ] . toString ( ) !== "0" ) {
// send non-dusty change to our address
var usableChange = changeAmountDivRem [ 0 ] . multiply (
monero _config . dustThreshold ,
) ;
console . log (
"💬 Sending change of " +
monero _amount _format _utils . formatMoneySymbol ( usableChange ) +
" to " +
wallet _ _public _address ,
) ;
fundTransferDescriptions . push ( {
address : wallet _ _public _address ,
amount : usableChange ,
} ) ;
}
}
} else if ( usingOutsAmount _comparedTo _totalAmount == 0 ) {
// this should always fire when sweeping
if ( isRingCT ) {
// then create random destination to keep 2 outputs always in case of 0 change
var fakeAddress = monero _utils . create _address (
monero _utils . random _scalar ( ) ,
nettype ,
) . public _addr ;
console . log (
"Sending 0 XMR to a fake address to keep tx uniform (no change exists): " +
fakeAddress ,
) ;
fundTransferDescriptions . push ( {
address : fakeAddress ,
amount : 0 ,
} ) ;
}
}
console . log (
"fundTransferDescriptions so far" ,
fundTransferDescriptions ,
) ;
if ( mixin < 0 || isNaN ( mixin ) ) {
_ _trampolineFor _err _withStr ( "Invalid mixin" ) ;
return ;
}
if ( mixin > 0 ) {
// 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 ) ;
} ) ;
return ;
} else {
// mixin === 0: -- PSNOTE: is that even allowed?
_ _createTxAndAttemptToSend ( ) ;
}
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 signedTx ;
try {
console . log ( "Destinations: " ) ;
printDsts ( fundTransferDescriptions ) ; // TODO: port this out
//
var realDestViewKey ; // need to get viewkey for encrypting here, because of splitting and sorting
if ( final _ _pid _encrypt ) {
realDestViewKey = monero _utils . decode _address (
moneroReady _targetDescription _address ,
nettype ,
) . view ;
console . log ( "got realDestViewKey" , realDestViewKey ) ;
}
console . log ( "fundTransferDescriptions" , fundTransferDescriptions )
var IPCsafe _splitDestinations = decompose _tx _destinations ( // TODO: port this out
fundTransferDescriptions ,
isRingCT ,
true // serialize (convert JSBigInts to strings for IPC)
) ;
printDsts ( IPCsafe _splitDestinations ) ;
//
signedTx = monero _utils . create _transaction _ _IPCsafe (
wallet _ _public _keys ,
wallet _ _private _keys ,
IPCsafe _splitDestinations ,
usingOuts ,
mix _outs ,
mixin ,
attemptAt _network _minimumFee . toString ( ) , // must serialize for IPC
final _ _payment _id ,
final _ _pid _encrypt ,
realDestViewKey ,
0 ,
isRingCT ,
nettype ,
) ;
} 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 ( "signed tx: " , JSON . stringify ( signedTx ) ) ;
//
var serialized _signedTx ;
var tx _hash ;
if ( signedTx . version === 1 ) {
serialized _signedTx = monero _utils . serialize _tx ( signedTx ) ;
tx _hash = monero _utils . cn _fast _hash ( serialized _signedTx ) ;
} else {
const raw _tx _and _hash = monero _utils . serialize _rct _tx _with _hash (
signedTx ,
) ;
serialized _signedTx = raw _tx _and _hash . raw ;
tx _hash = raw _tx _and _hash . hash ;
}
console . log ( "tx serialized: " + serialized _signedTx ) ;
console . log ( "Tx hash: " + tx _hash ) ;
//
// 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 ,
) ; // 🎉
} ,
) ;
}
}
}
exports . SendFunds = SendFunds ;
//
function new _moneroReadyTargetDescriptions _fromTargetDescriptions (
monero _openalias _utils ,
targetDescriptions ,
nettype ,
fn , // fn: (err, moneroReady_targetDescriptions) -> Void
) {
// 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
async . mapSeries (
targetDescriptions ,
function ( targetDescription , cb ) {
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 ) ;
cb ( 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 ) ;
cb ( 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 ) ;
cb ( err ) ;
return ;
}
cb ( null , {
address : targetDescription _address ,
amount : moneroReady _amountToSend ,
} ) ;
} ,
function ( err , moneroReady _targetDescriptions ) {
fn ( err , 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 )
{
/ * i f ( d u s t _ t h r e s h o l d = = = u n d e f i n e d ) {
dust _threshold = config . dustThreshold ;
} * /
amount = amount . toString ( ) ;
var ret = [ ] ;
while ( amount . length > 0 ) {
//split all the way down since v2 fork
/ * v a r r e m a i n i n g = n e w J S B i g I n t ( a m o u n t ) ;
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" ] ;
} ) ;
} ;