[WIP] Refactor/second pass sending funds (#5)

* Add BigInteger type definitions

* Begin typescript refactor

* Add JSBigInt notation where possible

* Refactor exports

* Add error annotations

* Convert nettype to ts

* Continute manually filling out types

* Split out seperated concerns into their own files

* Progress commit -- Split out logging and errors to their own files

* Finish seperating errors and logging

* var -> let/const, arrow functions for callbacks

* Snake -> camelCase

* Split out more send utils

* Flatten parse target

* Refactor out PID

* Commenting + formatting

* Remove async dep

* Convert sendfunds to an async function

* Split up tx construction into smaller functions

* Add function comments, split up types, simplify function flow

* Add barrel export

* Update types based on captured network requests

* Update comment
pull/37/head
HenryNguyen5 6 years ago committed by GitHub
parent 6cdb432c2c
commit 1ad8ff2436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -2,4 +2,5 @@
node_modules/
coverage
.vscode
yarn-error.log
yarn-error.log
tests/fixtures

@ -0,0 +1,615 @@
declare namespace BigInteger {
type ParsableValues =
| BigInteger.BigInteger
| number
| string
| Buffer
| (number)[];
class BigInteger {
/**
* @description Constant: ZERO
* <BigInteger> 0.
* @static
* @type {BigInteger}
*/
public static ZERO: BigInteger;
/**
* @description Constant: ONE
* <BigInteger> 1.
*
* @static
* @type {BigInteger}
* @memberof BigInteger
*/
public static ONE: BigInteger;
/**
* @description Constant: M_ONE
*<BigInteger> -1.
*
* @static
* @type {BigInteger}
* @memberof BigInteger
*/
public static M_ONE: BigInteger;
/**
* @description Constant: _0
* Shortcut for <ZERO>.
*
* @static
* @type {BigInteger}
* @memberof BigInteger
*/
public static _0: BigInteger;
/**
*
* @description Constant: _1
* Shortcut for <ONE>.
* @static
* @type {BigInteger}
* @memberof BigInteger
*/
public static _1: BigInteger;
/**
* @description Constant: small
* Array of <BigIntegers> from 0 to 36.
* These are used internally for parsing, but useful when you need a "small"
* <BigInteger>.
* @see <ZERO>, <ONE>, <_0>, <_1>
* @static
* @type {[
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger,
* BigInteger ]}
* @memberof BigInteger
*/
public static small: [
BigInteger,
BigInteger,
/* Assuming BigInteger_base > 36 */
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger,
BigInteger
];
/**
*
* @description Used for parsing/radix conversion
* @static
* @type {[
* "0",
* "1",
* "2",
* "3",
* "4",
* "5",
* "6",
* "7",
* "8",
* "9",
* "A",
* "B",
* "C",
* "D",
* "E",
* "F",
* "G",
* "H",
* "I",
* "J",
* "K",
* "L",
* "M",
* "N",
* "O",
* "P",
* "Q",
* "R",
* "S",
* "T",
* "U",
* "V",
* "W",
* "X",
* "Y",
* "Z"
* ]}
* @memberof BigInteger
*/
public static digits: [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z"
];
/**
* @description
* Convert a value to a <BigInteger>.
*
* Although <BigInteger()> is the constructor for <BigInteger> objects, it is
* best not to call it as a constructor. If *n* is a <BigInteger> object, it is
* simply returned as-is. Otherwise, <BigInteger()> is equivalent to <parse>
* without a radix argument.
*
* > var n0 = BigInteger(); // Same as <BigInteger.ZERO>
* > var n1 = BigInteger("123"); // Create a new <BigInteger> with value 123
* > var n2 = BigInteger(123); // Create a new <BigInteger> with value 123
* > var n3 = BigInteger(n2); // Return n2, unchanged
*
* The constructor form only takes an array and a sign. *n* must be an
* array of numbers in little-endian order, where each digit is between 0
* and BigInteger.base. The second parameter sets the sign: -1 for
* negative, +1 for positive, or 0 for zero. The array is *not copied and
* may be modified*. If the array contains only zeros, the sign parameter
* is ignored and is forced to zero.
*
* > new BigInteger([5], -1): create a new BigInteger with value -5
* @param {ParsableValues} n Value to convert to a <BigInteger>.
* @see parse, BigInteger
* @memberof BigInteger
*/
constructor(n: ParsableValues, sign?: 0 | -1 | 1);
/**
* @description Convert a <BigInteger> to a string.
*
* When *base* is greater than 10, letters are upper case.
* @param {number} [base] Optional base to represent the number in (default is base 10). Must be between 2 and 36 inclusive, or an Error will be thrown
* @returns {string} The string representation of the <BigInteger>.
* @memberof BigInteger
*/
toString(base?: number): string;
/**
* @description
* Function: parse
* Parse a string into a <BigInteger>.
*
* *base* is optional but, if provided, must be from 2 to 36 inclusive. If
* *base* is not provided, it will be guessed based on the leading characters
* of *s* as follows:
*
* - "0x" or "0X": *base* = 16
* - "0c" or "0C": *base* = 8
* - "0b" or "0B": *base* = 2
* - else: *base* = 10
*
* If no base is provided, or *base* is 10, the number can be in exponential
* form. For example, these are all valid:
*
* > BigInteger.parse("1e9"); // Same as "1000000000"
* > BigInteger.parse("1.234*10^3"); // Same as 1234
* > BigInteger.parse("56789 * 10 ** -2"); // Same as 567
*
* If any characters fall outside the range defined by the radix, an exception
* will be thrown.
*
* @param {string} s the string to parse.
* @param {number} [base] Optional radix (default is to guess based on *s*).
* @returns {BigInteger}
* @memberof BigInteger
*/
parse(s: string, base?: number): BigInteger;
/**
* @description Add two <BigIntegers>.
* @param {ParsableValues} n The number to add to *this*. Will be converted to a <BigInteger>.
* @returns {BigInteger} The numbers added together.
* @see <subtract>,<multiply>,<quotient>,<next>
* @memberof BigInteger
*/
add(n: ParsableValues): BigInteger;
/**
*
* @description Get the additive inverse of a <BigInteger>.
* @returns {BigInteger} A <BigInteger> with the same magnatude, but with the opposite sign.
* @see <abs>
* @memberof BigInteger
*
*/
negate(): BigInteger;
/**
* @description Get the absolute value of a <BigInteger>.
* @returns {BigInteger} A <BigInteger> with the same magnatude, but always positive (or zero).
* @see <negate>
* @memberof BigInteger
*
*/
abs(): BigInteger;
/**
* @description Subtract two <BigIntegers>.
*
* @param {ParsableValues} n The number to subtract from *this*. Will be converted to a <BigInteger>.
* @returns {BigInteger} The *n* subtracted from *this*.
* @see <add>, <multiply>, <quotient>, <prev>
* @memberof BigInteger
*/
subtract(n: ParsableValues): BigInteger;
/**
* @description Get the next <BigInteger> (add one).
*
* @returns {BigInteger} *this* + 1.
* @see <add>, <prev>
* @memberof BigInteger
*/
next(): BigInteger;
/**
* @description Get the previous <BigInteger> (subtract one).
*
* @returns {BigInteger} *this* - 1.
* @see <next>, <subtract>
* @memberof BigInteger
*/
prev(): BigInteger;
/**
* @description Compare the absolute value of two <BigIntegers>.
*
* Calling <compareAbs> is faster than calling <abs> twice, then <compare>.
* @param {ParsableValues} n The number to compare to *this*. Will be converted to a <BigInteger>.
* @returns {number} -1, 0, or +1 if *|this|* is less than, equal to, or greater than *|n|*.
* @see <compare>, <abs>
* @memberof BigInteger
*/
compareAbs(n: ParsableValues): 1 | 0 | -1;
/**
* @description Compare two <BigIntegers>.
*
* @param {ParsableValues} n The number to compare to *this*. Will be converted to a <BigInteger>.
* @returns {(1 | 0 | -1)} -1, 0, or +1 if *this* is less than, equal to, or greater than *n*.
* @see <compareAbs>, <isPositive>, <isNegative>, <isUnit>
* @memberof BigInteger
*/
compare(n: ParsableValues): 1 | 0 | -1;
/**
* @description Return true iff *this* is either 1 or -1.
*
* @returns {boolean} true if *this* compares equal to <BigInteger.ONE> or <BigInteger.M_ONE>.
* @see <isZero>, <isNegative>, <isPositive>, <compareAbs>, <compare>,
* <BigInteger.ONE>, <BigInteger.M_ONE>
* @memberof BigInteger
*/
isUnit(): boolean;
/**
* @description Multiply two <BigIntegers>.
*
* @param {ParsableValues} n The number to multiply *this* by. Will be converted to a <BigInteger>.
* @returns {BigInteger} The numbers multiplied together.
* @see <add>, <subtract>, <quotient>, <square>
* @memberof BigInteger
*/
multiply(n: ParsableValues): BigInteger;
/**
* @description Multiply a <BigInteger> by itself.
* This is slightly faster than regular multiplication, since it removes the
* duplicated multiplcations.
* @returns {BigInteger} > this.multiply(this)
* @see <multiply>
* @memberof BigInteger
*/
square(): BigInteger;
/**
* @description Divide two <BigIntegers> and truncate towards zero.
*
* <quotient> throws an exception if *n* is zero.
* @param {ParsableValues} n The number to divide *this* by. Will be converted to a <BigInteger>.
* @returns {BigInteger} The *this* / *n*, truncated to an integer.
* @see <add>, <subtract>, <multiply>, <divRem>, <remainder>
* @memberof BigInteger
*/
quotient(n: ParsableValues): BigInteger;
/**
*
* @description Deprecated synonym for <quotient>.
* @param {ParsableValues} n
* @returns {BigInteger}
* @memberof BigInteger
*/
divide(n: ParsableValues): BigInteger;
/**
* @description Calculate the remainder of two <BigIntegers>.
*
* <remainder> throws an exception if *n* is zero.
*
* @param {ParsableValues} n The remainder after *this* is divided *this* by *n*. Will be converted to a <BigInteger>.
* @returns {BigInteger}*this* % *n*.
* @see <divRem>, <quotient>
* @memberof BigInteger
*/
remainder(n: ParsableValues): BigInteger;
/**
* @description Calculate the integer quotient and remainder of two <BigIntegers>.
*
* <divRem> throws an exception if *n* is zero.
*
* @param {ParsableValues} n The number to divide *this* by. Will be converted to a <BigInteger>.
* @returns {[BigInteger, BigInteger]} A two-element array containing the quotient and the remainder.
*
* > a.divRem(b)
*
* is exactly equivalent to
*
* > [a.quotient(b), a.remainder(b)]
*
* except it is faster, because they are calculated at the same time.
* @see <quotient>, <remainder>
* @memberof BigInteger
*/
divRem(n: ParsableValues): [BigInteger, BigInteger];
/**
* @description Return true iff *this* is divisible by two.
*
* Note that <BigInteger.ZERO> is even.
* @returns {boolean} true if *this* is even, false otherwise.
* @see <isOdd>
* @memberof BigInteger
*/
isEven(): boolean;
/**
* @description Return true iff *this* is not divisible by two.
*
* @returns {boolean} true if *this* is odd, false otherwise.
* @see <isEven>
* @memberof BigInteger
*/
isOdd(): boolean;
/**
* @description Get the sign of a <BigInteger>.
*
* @returns {(-1 | 0 | 1)} * -1 if *this* < 0
* * 0 if *this* == 0
* * +1 if *this* > 0
* @see <isZero>, <isPositive>, <isNegative>, <compare>, <BigInteger.ZERO>
* @memberof BigInteger
*/
sign(): -1 | 0 | 1;
/**
* @description Return true iff *this* > 0.
*
* @returns {boolean} true if *this*.compare(<BigInteger.ZERO>) == 1.
* @see <sign>, <isZero>, <isNegative>, <isUnit>, <compare>, <BigInteger.ZERO>
* @memberof BigInteger
*/
isPositive(): boolean;
/**
* @description Return true iff *this* < 0.
*
* @returns {boolean} true if *this*.compare(<BigInteger.ZERO>) == -1.
* @see <sign>, <isPositive>, <isZero>, <isUnit>, <compare>, <BigInteger.ZERO>
* @memberof BigInteger
*/
isNegative(): boolean;
/**
* @description Return true iff *this* == 0.
*
* @returns {boolean} true if *this*.compare(<BigInteger.ZERO>) == 0.
* @see <sign>, <isPositive>, <isNegative>, <isUnit>, <BigInteger.ZERO>
* @memberof BigInteger
*/
isZero(): boolean;
/**
* @description Multiply a <BigInteger> by a power of 10.
*
* This is equivalent to, but faster than
*
* > if (n >= 0) {
* > return this.multiply(BigInteger("1e" + n));
* > }
* > else { // n <= 0
* > return this.quotient(BigInteger("1e" + -n));
* > }
*
* @param {ParsableValues} n The power of 10 to multiply *this* by. *n* is converted to a
javascipt number and must be no greater than <BigInteger.MAX_EXP>
(0x7FFFFFFF), or an exception will be thrown.
* @returns {BigInteger} *this* * (10 ** *n*), truncated to an integer if necessary.
* @see <pow>, <multiply>
* @memberof BigInteger
*/
exp10(n: ParsableValues): BigInteger;
/**
* @description Raise a <BigInteger> to a power.
*
* In this implementation, 0**0 is 1.
*
* @param {ParsableValues} n The exponent to raise *this* by. *n* must be no greater than
* <BigInteger.MAX_EXP> (0x7FFFFFFF), or an exception will be thrown.
* @returns {BigInteger} *this* raised to the *nth* power.
* @see <modPow>
* @memberof BigInteger
*/
pow(n: ParsableValues): BigInteger;
/**
* @description Raise a <BigInteger> to a power (mod m).
*
* Because it is reduced by a modulus, <modPow> is not limited by
* <BigInteger.MAX_EXP> like <pow>.
*
* @param {BigInteger} exponent The exponent to raise *this* by. Must be positive.
* @param {ParsableValues} modulus The modulus.
* @returns {BigInteger} *this* ^ *exponent* (mod *modulus*).
* @see <pow>, <mod>
* @memberof BigInteger
*/
modPow(exponent: BigInteger, modulus: ParsableValues): BigInteger;
/**
* @description Get the natural logarithm of a <BigInteger> as a native JavaScript number.
*
* This is equivalent to
*
* > Math.log(this.toJSValue())
*
* but handles values outside of the native number range.
*
* @returns {number} log( *this* )
* @see <toJSValue>
* @memberof BigInteger
*/
log(): number;
/**
* @description Convert a <BigInteger> to a native JavaScript integer.
*
* This is called automatically by JavaScipt to convert a <BigInteger> to a
* native value.
* @returns {number} > parseInt(this.toString(), 10)
* @see <toString>, <toJSValue>
* @memberof BigInteger
*/
valueOf(): number;
/**
* @description Convert a <BigInteger> to a native JavaScript integer.
*
* This is the same as valueOf, but more explicitly named.
* @returns {number} > parseInt(this.toString(), 10)
* @see <toString>, <valueOf>
* @memberof BigInteger
*/
toJSValue(): number;
/**
* @description Get the first byte of the Biginteger
*
* @returns {number} the first byte of the Biginteger
* @memberof BigInteger
*/
lowVal(): number;
/**
*
* @description Constant: MAX_EXP The largest exponent allowed in <pow> and <exp10> (0x7FFFFFFF or 2147483647).
* @static
* @type {BigInteger}
* @memberof BigInteger
*/
public static MAX_EXP: BigInteger;
}
}
export = BigInteger;

@ -2752,6 +2752,10 @@ var cnUtil = function(currencyConfig) {
return this.formatMoney(units) + " " + config.coinSymbol;
};
/**
*
* @param {string} str
*/
this.parseMoney = function(str) {
if (!str) return JSBigInt.ZERO;
var negative = str[0] === "-";

@ -26,67 +26,63 @@
// 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";
export enum NetType {
MAINNET = 0,
TESTNET = 1,
STAGENET = 2,
}
var network_type = {
MAINNET: 0,
TESTNET: 1,
STAGENET: 2,
};
exports.network_type = network_type;
//
var __MAINNET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 18;
var __MAINNET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 19;
var __MAINNET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 42;
//
var __TESTNET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 53;
var __TESTNET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 54;
var __TESTNET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 63;
//
var __STAGENET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 24;
var __STAGENET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 25;
var __STAGENET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 36;
//
function cryptonoteBase58PrefixForStandardAddressOn(nettype) {
if (nettype == null || typeof nettype === "undefined") {
const __MAINNET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 18;
const __MAINNET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 19;
const __MAINNET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 42;
const __TESTNET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 53;
const __TESTNET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 54;
const __TESTNET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 63;
const __STAGENET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 24;
const __STAGENET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 25;
const __STAGENET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 36;
export function cryptonoteBase58PrefixForStandardAddressOn(nettype: NetType) {
if (!nettype) {
console.warn("Unexpected nil nettype");
}
if (nettype == network_type.MAINNET) {
if (nettype == NetType.MAINNET) {
return __MAINNET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX;
} else if (nettype == network_type.TESTNET) {
} else if (nettype == NetType.TESTNET) {
return __TESTNET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX;
} else if (nettype == network_type.STAGENET) {
} else if (nettype == NetType.STAGENET) {
return __STAGENET_CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX;
}
throw "Illegal nettype";
}
function cryptonoteBase58PrefixForIntegratedAddressOn(nettype) {
if (nettype == null || typeof nettype === "undefined") {
export function cryptonoteBase58PrefixForIntegratedAddressOn(nettype: NetType) {
if (!nettype) {
console.warn("Unexpected nil nettype");
}
if (nettype == network_type.MAINNET) {
if (nettype == NetType.MAINNET) {
return __MAINNET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX;
} else if (nettype == network_type.TESTNET) {
} else if (nettype == NetType.TESTNET) {
return __TESTNET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX;
} else if (nettype == network_type.STAGENET) {
} else if (nettype == NetType.STAGENET) {
return __STAGENET_CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX;
}
throw "Illegal nettype";
}
function cryptonoteBase58PrefixForSubAddressOn(nettype) {
if (nettype == null || typeof nettype === "undefined") {
export function cryptonoteBase58PrefixForSubAddressOn(nettype: NetType) {
if (!nettype) {
console.warn("Unexpected nil nettype");
}
if (nettype == network_type.MAINNET) {
if (nettype == NetType.MAINNET) {
return __MAINNET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX;
} else if (nettype == network_type.TESTNET) {
} else if (nettype == NetType.TESTNET) {
return __TESTNET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX;
} else if (nettype == network_type.STAGENET) {
} else if (nettype == NetType.STAGENET) {
return __STAGENET_CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX;
}
throw "Illegal nettype";
}
//
exports.cryptonoteBase58PrefixForStandardAddressOn = cryptonoteBase58PrefixForStandardAddressOn;
exports.cryptonoteBase58PrefixForIntegratedAddressOn = cryptonoteBase58PrefixForIntegratedAddressOn;
exports.cryptonoteBase58PrefixForSubAddressOn = cryptonoteBase58PrefixForSubAddressOn;

@ -1,833 +0,0 @@
// 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_paymentID_utils = require("./monero_paymentID_utils");
const JSBigInt = require("../cryptonote_utils/biginteger").BigInteger;
const V7_MIN_MIXIN = 6;
function _mixinToRingsize(mixin) {
return mixin + 1;
}
function minMixin() {
return V7_MIN_MIXIN;
}
function minRingSize() {
return _mixinToRingsize(minMixin());
}
exports.minMixin = minMixin;
exports.minRingSize = minRingSize;
function fixedMixin() {
return minMixin(); /* using the monero app default to remove MM user identifiers */
}
function fixedRingsize() {
return _mixinToRingsize(fixedMixin());
}
exports.fixedMixin = fixedMixin;
exports.fixedRingsize = fixedRingsize;
const DEFAULT_FEE_PRIORITY = 1;
exports.DEFAULT_FEE_PRIORITY = DEFAULT_FEE_PRIORITY;
function calculateFee(feePerKBJSBigInt, numOfBytes, feeMultiplier) {
const numberOf_kB_JSBigInt = new JSBigInt((numOfBytes + 1023.0) / 1024.0); // i.e. ceil
return calculateFeeKb(
feePerKBJSBigInt,
numberOf_kB_JSBigInt,
feeMultiplier,
);
}
function calculateFeeKb(feePerKBJSBigInt, numOfBytes, feeMultiplier) {
const numberOf_kB_JSBigInt = new JSBigInt(numOfBytes);
const fee = feePerKBJSBigInt
.multiply(feeMultiplier)
.multiply(numberOf_kB_JSBigInt);
return fee;
}
function multiplyFeePriority(prio) {
const fee_multiplier = [1, 4, 20, 166];
const priority = prio || DEFAULT_FEE_PRIORITY;
if (priority <= 0 || priority > fee_multiplier.length) {
throw "fee_multiplier_for_priority: simple_priority out of bounds";
}
const priority_idx = priority - 1;
return fee_multiplier[priority_idx];
}
function estimatedTransactionNetworkFee(
nonZeroMixinInt,
feePerKBJSBigInt,
simplePriority,
) {
const numOfInputs = 2; // this might change -- might select inputs
const numOfOutputs =
1 /*dest*/ + 1 /*change*/ + 0; /*no mymonero fee presently*/
// TODO: update est tx size for bulletproofs
// TODO: normalize est tx size fn naming
const estimatedTxSize = monero_utils.estimateRctSize(
numOfInputs,
nonZeroMixinInt,
numOfOutputs,
);
const estFee = calculateFee(
feePerKBJSBigInt,
estimatedTxSize,
multiplyFeePriority(simplePriority),
);
//
return estFee;
}
exports.EstimatedTransaction_networkFee = estimatedTransactionNetworkFee;
//
const sendFundStatus = {
fetching_latest_balance: 1,
calculating_fee: 2,
fetching_decoy_outputs: 3, // may get skipped if 0 mixin
constructing_transaction: 4, // may go back to .calculatingFee
submitting_transaction: 5,
};
exports.SendFunds_ProcessStep_Code = sendFundStatus;
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(
targetAddress, // currency-ready wallet address, but not an OpenAlias address (resolve before calling)
nettype,
amountorZeroWhenSweep, // number - value will be ignoring for sweep
isSweeporZeroWhenAmount, // send true to sweep - amount_orZeroWhenSweep will be ignored
senderPublicAddress,
senderPrivateKeys,
senderPublicKeys,
nodeAPI, // TODO: possibly factor this dependency
moneroOpenaliasUtils,
pid,
mixin,
simplePriority,
updateStatusCb, // (_ stepCode: SendFunds_ProcessStep_Code) -> Void
successCb,
// success_fn: (
// moneroReady_targetDescription_address?,
// sentAmount?,
// final__payment_id?,
// tx_hash?,
// tx_fee?
// )
errCb,
// failWithErr_fn: (
// err
// )
) {
const isRingCT = true;
const sweeping = isSweeporZeroWhenAmount === true; // rather than, say, undefined
if (mixin < minMixin()) {
return errCb(Error("Ringsize is below the minimum."));
}
//
// parse & normalize the target descriptions by mapping them to Monero addresses & amounts
const target_amount = sweeping ? 0 : amountorZeroWhenSweep;
const target = {
address: targetAddress,
amount: target_amount,
};
resolveTargets(
moneroOpenaliasUtils,
[target], // requires a list of descriptions - but SendFunds was
// not written with multiple target support as MyMonero does not yet support it
nettype,
function(_err, _resolved_targets) {
if (_err) {
return errCb(_err);
}
if (_resolved_targets.length === 0) {
return errCb(Error("You need to enter a valid destination"));
}
const single_target = _resolved_targets[0];
if (!single_target) {
return errCb(Error("You need to enter a valid destination"));
}
_prepare_to_send_to_target(single_target);
},
);
function _prepare_to_send_to_target(resolvedTarget) {
const _targetAddress = resolvedTarget.address;
const _target_amount = resolvedTarget.amount;
//
var feelessTotalJSBigInt = new JSBigInt(_target_amount);
const feeless_total = sweeping
? "all"
: monero_utils.formatMoney(feelessTotalJSBigInt);
console.log(`💬 Total to send, before fee: ${feeless_total}`);
if (!sweeping && feelessTotalJSBigInt.compare(0) <= 0) {
return errCb(Error("The amount you've entered is too low"));
}
//
// Derive/finalize some values…
let _pid = pid;
let encryptPid = false; // we don't want to encrypt payment ID unless we find an integrated one
// NOTE: refactor this out, its already done in resolve_targets
var decoded_address;
try {
decoded_address = monero_utils.decode_address(
_targetAddress,
nettype,
);
} catch (e) {
return errCb(Error(e.toString()));
}
// assert that the target address is not of type integrated nor subaddress
// if a payment id is included
if (pid) {
if (decoded_address.intPaymentId) {
return errCb(
Error(
"Payment ID must be blank when using an Integrated Address",
),
);
} else if (monero_utils.is_subaddress(_targetAddress, nettype)) {
return errCb(
Error("Payment ID must be blank when using a Subaddress"),
);
}
}
// if the target address is integrated
// then encrypt the payment id
// and make sure its also valid
if (decoded_address.intPaymentId) {
_pid = decoded_address.intPaymentId;
encryptPid = true;
} else if (
!monero_paymentID_utils.IsValidPaymentIDOrNoPaymentID(_pid)
) {
return errCb(Error("Invalid payment ID."));
}
_getUsableUnspentOutsForMixin(
_targetAddress,
feelessTotalJSBigInt,
_pid,
encryptPid,
);
}
function _getUsableUnspentOutsForMixin(
_targetAddress,
_feelessTotalJSBigInt,
_pid, // non-existent or valid
_encryptPid, // true or false
) {
updateStatusCb(sendFundStatus.fetching_latest_balance);
nodeAPI.UnspentOuts(
senderPublicAddress,
senderPrivateKeys.view,
senderPublicKeys.spend,
senderPrivateKeys.spend,
mixin,
sweeping,
function(err, unspentOuts, __unusedOuts, __dynFeePerKBJSBigInt) {
if (err) {
return errCb(err);
}
console.log(
"Received dynamic per kb fee",
monero_utils.formatMoneySymbol(__dynFeePerKBJSBigInt),
);
_proceedTo_constructFundTransferListAndSendFundsByUsingUnusedUnspentOutsForMixin(
_targetAddress,
_feelessTotalJSBigInt,
_pid,
_encryptPid,
__unusedOuts,
__dynFeePerKBJSBigInt,
);
},
);
}
function _proceedTo_constructFundTransferListAndSendFundsByUsingUnusedUnspentOutsForMixin(
_targetAddress,
_feelessTotalAmountJSBigInt,
_pid,
_encryptPid,
_unusedOuts,
_dynamicFeePerKBJSBigInt,
) {
// status: constructing transaction…
const _feePerKBJSBigInt = _dynamicFeePerKBJSBigInt;
// Transaction will need at least 1KB fee (or 13KB for RingCT)
const _minNetworkTxSizeKb = /*isRingCT ? */ 13; /* : 1*/
const _estMinNetworkFee = calculateFeeKb(
_feePerKBJSBigInt,
_minNetworkTxSizeKb,
multiplyFeePriority(simplePriority),
);
// now we're going to try using this minimum fee but the function will be called again
// 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
_attempt_to_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
_targetAddress,
_feelessTotalAmountJSBigInt,
_pid,
_encryptPid,
_unusedOuts,
_feePerKBJSBigInt, // obtained from server, so passed in
_estMinNetworkFee,
);
}
function _attempt_to_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
_targetAddress,
_feelessTotalJSBigInt,
_pid,
_encryptPid,
_unusedOuts,
_feePerKBJSBigInt,
_estMinNetworkFee,
) {
// Now we need to establish some values for balance validation and to construct the transaction
updateStatusCb(sendFundStatus.calculating_fee);
let estMinNetworkFee = _estMinNetworkFee; // we may change this if isRingCT
// const hostingService_chargeAmount = hostedMoneroAPIClient.HostingServiceChargeFor_transactionWithNetworkFee(attemptAt_network_minimumFee)
let total_amount;
if (sweeping) {
total_amount = new JSBigInt("18450000000000000000"); //~uint64 max
console.log("Balance required: all");
} else {
total_amount = _feelessTotalJSBigInt.add(
estMinNetworkFee,
); /*.add(hostingService_chargeAmount) NOTE service fee removed for now */
console.log(
"Balance required: " +
monero_utils.formatMoneySymbol(total_amount),
);
}
const usableOutputsAndAmounts = outputsAndAmountForMixin(
total_amount,
_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.remainingUnusedOuts; // this is a copy of the pre-mutation usingOuts
if (/*usingOuts.length > 1 &&*/ isRingCT) {
var newNeededFee = calculateFee(
_feePerKBJSBigInt,
monero_utils.estimateRctSize(usingOuts.length, mixin, 2),
multiplyFeePriority(simplePriority),
);
// 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(estMinNetworkFee) < 0) {
newNeededFee = estMinNetworkFee;
}
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;
}
*/
_feelessTotalJSBigInt = usingOutsAmount.subtract(newNeededFee);
if (_feelessTotalJSBigInt.compare(0) < 1) {
const { coinSymbol } = monero_config;
const outsAmountStr = monero_utils.formatMoney(
usingOutsAmount,
);
const newNeededFeeStr = monero_utils.formatMoney(
newNeededFee,
);
const errStr = `Your spendable balance is too low. Have ${outsAmountStr} ${coinSymbol} spendable, need ${newNeededFeeStr} ${coinSymbol}.`;
return errCb(Error(errStr));
}
total_amount = _feelessTotalJSBigInt.add(newNeededFee);
} else {
total_amount = _feelessTotalJSBigInt.add(newNeededFee);
// add outputs 1 at a time till we either have them all or can meet the fee
while (
usingOutsAmount.compare(total_amount) < 0 &&
remaining_unusedOuts.length > 0
) {
const out = popRandElement(remaining_unusedOuts);
console.log(
"Using output: " +
monero_utils.formatMoney(out.amount) +
" - " +
JSON.stringify(out),
);
// and recalculate invalidated values
newNeededFee = calculateFee(
_feePerKBJSBigInt,
monero_utils.estimateRctSize(
usingOuts.length,
mixin,
2,
),
multiplyFeePriority(simplePriority),
);
total_amount = _feelessTotalJSBigInt.add(newNeededFee);
}
}
console.log(
"New fee: " +
monero_utils.formatMoneySymbol(newNeededFee) +
" for " +
usingOuts.length +
" inputs",
);
estMinNetworkFee = newNeededFee;
}
console.log(
"~ Balance required: " +
monero_utils.formatMoneySymbol(total_amount),
);
// Now we can validate available balance with usingOutsAmount (TODO? maybe this check can be done before selecting outputs?)
const usingOutsAmount_comparedTo_totalAmount = usingOutsAmount.compare(
total_amount,
);
if (usingOutsAmount_comparedTo_totalAmount < 0) {
const { coinSymbol } = monero_config;
const usingOutsAmountStr = monero_utils.formatMoney(
usingOutsAmount,
);
const totalAmountIncludingFeesStr = monero_utils.formatMoney(
total_amount,
);
const errStr = `Your spendable balance is too low. Have ${usingOutsAmountStr} ${coinSymbol} spendable, need ${totalAmountIncludingFeesStr} ${coinSymbol}.`;
return errCb(Error(errStr));
}
// 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: _targetAddress,
amount: _feelessTotalJSBigInt,
});
// 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 change_amount = usingOutsAmount.subtract(total_amount);
console.log("changeAmount", change_amount);
if (isRingCT) {
// for RCT we don't presently care about dustiness so add entire change amount
console.log(
"Sending change of " +
monero_utils.formatMoneySymbol(change_amount) +
" to " +
senderPublicAddress,
);
fundTransferDescriptions.push({
address: senderPublicAddress,
amount: change_amount,
});
} else {
// pre-ringct
// do not give ourselves change < dust threshold
var changeAmountDivRem = change_amount.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_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_utils.formatMoneySymbol(usableChange) +
" to " +
senderPublicAddress,
);
fundTransferDescriptions.push({
address: senderPublicAddress,
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
const 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)) {
return errCb(Error("Invalid mixin"));
}
if (mixin > 0) {
// first, grab RandomOuts, then enter __createTx
updateStatusCb(sendFundStatus.fetching_decoy_outputs);
nodeAPI.RandomOuts(usingOuts, mixin, function(_err, _amount_outs) {
if (_err) {
errCb(_err);
return;
}
_createTxAndAttemptToSend(_amount_outs);
});
return;
} else {
// mixin === 0: -- PSNOTE: is that even allowed?
_createTxAndAttemptToSend();
}
function _createTxAndAttemptToSend(mixOuts) {
updateStatusCb(sendFundStatus.constructing_transaction);
var signedTx;
try {
console.log("Destinations: ");
monero_utils.printDsts(fundTransferDescriptions);
//
var realDestViewKey; // need to get viewkey for encrypting here, because of splitting and sorting
if (_encryptPid) {
realDestViewKey = monero_utils.decode_address(
_targetAddress,
nettype,
).view;
console.log("got realDestViewKey", realDestViewKey);
}
var splitDestinations = monero_utils.decompose_tx_destinations(
fundTransferDescriptions,
isRingCT,
);
console.log("Decomposed destinations:");
monero_utils.printDsts(splitDestinations);
//
signedTx = monero_utils.create_transaction(
senderPublicKeys,
senderPrivateKeys,
splitDestinations,
usingOuts,
mixOuts,
mixin,
_estMinNetworkFee,
_pid,
_encryptPid,
realDestViewKey,
0,
isRingCT,
nettype,
);
} catch (e) {
let errStr;
if (e) {
errStr = e.toString();
} else {
errStr = "Failed to create transaction with unknown error.";
}
return errCb(Error(errStr));
}
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_utils.formatMoneyFull(_estMinNetworkFee) +
")",
);
const feeActuallyNeededByNetwork = calculateFeeKb(
_feePerKBJSBigInt,
numKB,
multiplyFeePriority(simplePriority),
);
// if we need a higher fee
if (feeActuallyNeededByNetwork.compare(_estMinNetworkFee) > 0) {
console.log(
"💬 Need to reconstruct the tx with enough of a network fee. Previous fee: " +
monero_utils.formatMoneyFull(_estMinNetworkFee) +
" New fee: " +
monero_utils.formatMoneyFull(
feeActuallyNeededByNetwork,
),
);
// this will update status back to .calculatingFee
_attempt_to_constructFundTransferListAndSendFunds_findingLowestNetworkFee(
_targetAddress,
_feelessTotalJSBigInt,
_pid,
_encryptPid,
_unusedOuts,
_feePerKBJSBigInt,
feeActuallyNeededByNetwork, // we are re-entering this codepath after changing this feeActuallyNeededByNetwork
);
//
return;
}
// generated with correct per-kb fee
const final_networkFee = _estMinNetworkFee; // just to make things clear
console.log(
"💬 Successful tx generation, submitting tx. Going with final_networkFee of ",
monero_utils.formatMoney(final_networkFee),
);
updateStatusCb(sendFundStatus.submitting_transaction);
nodeAPI.SubmitSerializedSignedTransaction(
senderPublicAddress,
senderPrivateKeys.view,
serialized_signedTx,
function(err) {
if (err) {
return errCb(
Error(
"Something unexpected occurred when submitting your transaction: " +
err,
),
);
}
const tx_fee = final_networkFee; /*.add(hostingService_chargeAmount) NOTE: Service charge removed to reduce bloat for now */
successCb(
_targetAddress,
sweeping
? parseFloat(
monero_utils.formatMoneyFull(
_feelessTotalJSBigInt,
),
)
: target_amount,
_pid,
tx_hash,
tx_fee,
); // 🎉
},
);
}
}
}
exports.SendFunds = SendFunds;
//
/**
*
* @description Validate & Normalize passed in target descriptions of {address, amount}.
*
* Checks if address is valid along with the amount.
*
* parse & normalize the target descriptions by mapping them to currency (Monero)-ready addresses & amounts
* @param {*} moneroOpenaliasUtils
* @param {{address,amount}[]} targetsToResolve
* @param {*} nettype
* @param {(err: Error | null, resolved_targets?: {address,amount}[]) => void } cb
*/
function resolveTargets(moneroOpenaliasUtils, targetsToResolve, nettype, cb) {
async.mapSeries(
targetsToResolve,
(target, _cb) => {
if (!target.address && !target.amount) {
// PSNote: is this check rigorous enough?
return _cb(
Error(
"Please supply a target address and a target amount.",
),
);
}
const target_address = target.address;
const target_amount = target.amount.toString(); // we are converting it to a string here because parseMoney expects a string
// now verify/parse address and amount
if (
moneroOpenaliasUtils.DoesStringContainPeriodChar_excludingAsXMRAddress_qualifyingAsPossibleOAAddress(
target_address,
)
) {
return _cb(
Error(
"You must resolve this OpenAlias address to a Monero address before calling SendFunds",
),
);
}
// otherwise this should be a normal, single Monero public address
try {
monero_utils.decode_address(target_address, nettype); // verify that the address is valid
} catch (e) {
return _cb(
Error(`Couldn't decode address ${target_address} : ${e}`),
);
}
// amount
try {
const parsed_amount = monero_utils.parseMoney(target_amount);
return _cb(null, {
address: target_address,
amount: parsed_amount,
});
} catch (e) {
return _cb(
Error(`Couldn't parse amount ${target_amount} : ${e}`),
);
}
},
(err, resolved_targets) => {
cb(err, resolved_targets);
},
);
}
function popRandElement(list) {
var idx = Math.floor(Math.random() * list.length);
var val = list[idx];
list.splice(idx, 1);
return val;
}
function outputsAndAmountForMixin(
targetAmount,
unusedOuts,
isRingCT,
sweeping,
) {
console.log(
"Selecting outputs to use. target: " +
monero_utils.formatMoney(targetAmount),
);
var usingOutsAmount = new JSBigInt(0);
const usingOuts = [];
const remainingUnusedOuts = unusedOuts.slice(); // take copy so as to prevent issue if we must re-enter tx building fn if fee too low after building
while (
usingOutsAmount.compare(targetAmount) < 0 &&
remainingUnusedOuts.length > 0
) {
var out = popRandElement(remainingUnusedOuts);
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) {
console.log(
"Not sweeping, and found a dusty (though maybe mixable) output... skipping it!",
);
continue;
}
if (!out.rct) {
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!",
);
}
}
usingOuts.push(out);
usingOutsAmount = usingOutsAmount.add(out_amount_JSBigInt);
console.log(
`Using output: ${monero_utils.formatMoney(
out_amount_JSBigInt,
)} - ${JSON.stringify(out)}`,
);
}
return {
usingOuts,
usingOutsAmount,
remainingUnusedOuts,
};
}

@ -0,0 +1,3 @@
export * from "./mixin_utils";
export * from "./sending_funds";
export * from "./status_update_constants";

@ -0,0 +1,32 @@
import { JSBigInt } from "./types";
/**
* @description Gets a starting total amount.
*
* In the case of a sweeping transaction, the maximum possible amount (based on 64 bits unsigned integer) is chosen,
* since all of the user's outputs will be used regardless.
*
* Otherwise, the feeless total (also known as the amount the sender actually wants to send to the given target address)
* is summed with the network fee to give the total amount returned
*
* @export
* @param {boolean} isSweeping
* @param {JSBigInt} feelessTotal
* @param {JSBigInt} networkFee
* @returns
*/
export function getBaseTotalAmount(
isSweeping: boolean,
feelessTotal: JSBigInt,
networkFee: JSBigInt,
) {
// const hostingService_chargeAmount = hostedMoneroAPIClient.HostingServiceChargeFor_transactionWithNetworkFee(attemptAt_network_minimumFee)
if (isSweeping) {
return new JSBigInt("18450000000000000000"); //~uint64 max
} else {
return feelessTotal.add(
networkFee,
); /*.add(hostingService_chargeAmount) NOTE service fee removed for now */
}
}

@ -0,0 +1,6 @@
export function popRandElement<T>(list: T[]) {
const idx = Math.floor(Math.random() * list.length);
const val = list[idx];
list.splice(idx, 1);
return val;
}

@ -0,0 +1,81 @@
import { ViewSendKeys, JSBigInt, Output, AmountOutput } from "./types";
import { Log } from "./logger";
import { ERR } from "./errors";
export class WrappedNodeApi {
private api: any;
constructor(api: any) {
this.api = api;
}
public unspentOuts(
address: string,
privateKeys: ViewSendKeys,
publicKeys: ViewSendKeys,
mixin: number,
isSweeping: boolean,
) {
type ResolveVal = {
unusedOuts: Output[];
dynamicFeePerKB: JSBigInt;
};
return new Promise<ResolveVal>((resolve, reject) => {
const { spend: xSpend, view: xView } = privateKeys;
const { spend: pubSend } = publicKeys;
const handler = (
err: Error,
_: Output[], // unspent outs, the original copy of unusedOuts
unusedOuts: Output[],
dynamicFeePerKB: JSBigInt,
) => {
if (err) {
return reject(err);
}
Log.Fee.dynPerKB(dynamicFeePerKB);
return resolve({
unusedOuts,
dynamicFeePerKB,
});
};
this.api.UnspentOuts(
address,
xView,
pubSend,
xSpend,
mixin,
isSweeping,
handler,
);
});
}
public randomOuts(usingOuts: Output[], mixin: number) {
return new Promise<{ mixOuts: AmountOutput[] }>((resolve, reject) => {
this.api.RandomOuts(
usingOuts,
mixin,
(err: Error, mixOuts: AmountOutput[]) =>
err ? reject(err) : resolve({ mixOuts }),
);
});
}
public submitSerializedSignedTransaction(
address: string,
privateKeys: ViewSendKeys,
serializedSignedTx: string,
) {
return new Promise<void>((resolve, reject) => {
this.api.SubmitSerializedSignedTransaction(
address,
privateKeys.view,
serializedSignedTx,
(err: Error) =>
err ? reject(ERR.TX.submitUnknown(err)) : resolve(),
);
});
}
}

@ -0,0 +1,191 @@
import { sendFundStatus } from "../../status_update_constants";
import { selectOutputsAndAmountForMixin } from "../output_selection";
import { multiplyFeePriority, calculateFeeKb } from "../fee_utils";
import { ERR } from "../errors";
import { Log } from "../logger";
import {
constructTx,
totalAmtAndEstFee,
validateAndConstructFundTargets,
} from "../tx_utils/tx_utils";
import { getBaseTotalAmount } from "../amt_utils";
import {
CreateTxAndAttemptToSendParams,
GetFundTargetsAndFeeParams,
} from "./types";
/**
*
* @description
* 1. Recalculates the fee and total amount needed for the transaction to be sent. RCT + non sweeping transactions will have their
* network fee increased if fee calculation based on the number of outputs needed is higher than the passed-in fee. RCT+ sweeping transactions
* are just checked if they have enough balance to proceed with the transaction. Non-RCT transactions will have no fee recalculation done on them.
*
*
* 2. The resulting return values from step 1 will then be validated so that the sender has sufficient balances to proceed with sending the transaction.
* Then, a list of sending targets will be constructed, always consisting of the target address and amount they want to send to, and possibly a change address,
* if the sum of outs is greater than the amount sent + fee needed, and possibly a fake address + 0 amount to keep output uniformity if no change address
* was generated.
*
*
* 3. Finally, a list of random outputs is fetched from API to be mixed into the transaction (for generation of the ring signature) to provide anonymity for the sender.
*
*
* NOTE: This function may be called more than once (although I believe two times is the maximum) if the recalculated fee is lower than the
* actual transaction fee needed when the final fee is calculated from the size of the transaction itself. In the case that the previously mentioned
* condition is true, then this function will be re-called with the updated higher fee based on the transaction size in kb.
* @export
* @param {GetFundTargetsAndFeeParams} params
*/
export async function getRestOfTxData(params: GetFundTargetsAndFeeParams) {
const {
senderAddress,
targetAddress,
mixin,
unusedOuts,
simplePriority,
feelessTotal,
feePerKB, // obtained from server, so passed in
networkFee,
isRingCT,
isSweeping,
updateStatus,
api,
nettype,
} = params;
// Now we need to establish some values for balance validation and to construct the transaction
updateStatus(sendFundStatus.calculatingFee);
const baseTotalAmount = getBaseTotalAmount(
isSweeping,
feelessTotal,
networkFee,
);
Log.Balance.requiredBase(baseTotalAmount, isSweeping);
const {
remainingUnusedOuts, // this is a copy of the pre-mutation usingOuts
usingOuts,
usingOutsAmount,
} = selectOutputsAndAmountForMixin(
baseTotalAmount,
unusedOuts,
isRingCT,
isSweeping,
);
// v-- now if RingCT compute fee as closely as possible before hand
const { newFee, totalAmount } = totalAmtAndEstFee({
baseTotalAmount,
feelessTotal,
feePerKB,
isRingCT,
isSweeping,
mixin,
networkFee,
remainingUnusedOuts,
simplePriority,
usingOuts,
usingOutsAmount,
});
Log.Balance.requiredPostRct(totalAmount);
const { fundTargets } = validateAndConstructFundTargets({
senderAddress,
targetAddress,
feelessTotal,
totalAmount,
usingOutsAmount,
isRingCT,
isSweeping,
nettype,
});
Log.Target.display(fundTargets);
// check for invalid mixin level
if (mixin < 0 || isNaN(mixin)) {
throw ERR.MIXIN.INVAL;
}
// if we want to have mixin for anonyminity
if (mixin > 0) {
updateStatus(sendFundStatus.fetchingDecoyOutputs);
// grab random outputs to make a ring signature with
const { mixOuts } = await api.randomOuts(usingOuts, mixin);
return { mixOuts, fundTargets, newFee, usingOuts };
}
// mixin === 0: -- PSNOTE: is that even allowed?
return { mixOuts: undefined, fundTargets, newFee, usingOuts };
}
/**
* @description Creates the transaction blob and attempts to send it.
*
*
* The transaction blob will be not sent if the resulting fee calculated based on the blobs size
* is higher than the provided fee to the function, instead itll return a failure result, along
* with the fee based on the transaction blob.
*
*
* Otherwise, the serialized transaction blob will be sent to the API endpoint, along with
* a success return value with the fee + transaction blobs' hash
*
* @export
* @param {CreateTxAndAttemptToSendParams} params
*/
export async function createTxAndAttemptToSend(
params: CreateTxAndAttemptToSendParams,
) {
const {
senderAddress,
senderPrivateKeys,
simplePriority,
feePerKB,
networkFee,
updateStatus,
api,
} = params;
updateStatus(sendFundStatus.constructingTransaction);
const { numOfKB, serializedSignedTx, txHash } = constructTx(params);
const txFee = calculateFeeKb(
feePerKB,
numOfKB,
multiplyFeePriority(simplePriority),
);
// if we need a higher fee
if (txFee.compare(networkFee) > 0) {
Log.Fee.estLowerThanReal(networkFee, txFee);
return { success: false, txFee, txHash };
}
// generated with correct per-kb fee
Log.Fee.successfulTx(networkFee);
updateStatus(sendFundStatus.submittingTransaction);
await api.submitSerializedSignedTransaction(
senderAddress,
senderPrivateKeys,
serializedSignedTx,
);
return { success: true, txFee: networkFee, txHash };
}

@ -0,0 +1,65 @@
import { WrappedNodeApi } from "../async_node_api";
import { NetType } from "cryptonote_utils/nettype";
import {
ViewSendKeys,
JSBigInt,
ParsedTarget,
Pid,
Output,
AmountOutput,
} from "../types";
import { Status } from "../../status_update_constants";
export type GetFundTargetsAndFeeParams = {
senderAddress: string;
senderPublicKeys: ViewSendKeys;
senderPrivateKeys: ViewSendKeys;
targetAddress: string;
targetAmount: number;
mixin: number;
unusedOuts: Output[];
simplePriority: number;
feelessTotal: JSBigInt;
feePerKB: JSBigInt; // obtained from server, so passed in
networkFee: JSBigInt;
isSweeping: boolean;
isRingCT: boolean;
updateStatus: (status: Status) => void;
api: WrappedNodeApi;
nettype: NetType;
};
export type CreateTxAndAttemptToSendParams = {
targetAddress: string;
targetAmount: number;
senderAddress: string;
senderPublicKeys: ViewSendKeys;
senderPrivateKeys: ViewSendKeys;
fundTargets: ParsedTarget[];
pid: Pid; // unused
encryptPid: boolean;
mixOuts?: AmountOutput[];
mixin: number;
usingOuts: Output[];
simplePriority: number;
feelessTotal: JSBigInt;
feePerKB: JSBigInt; // obtained from server, so passed in
networkFee: JSBigInt;
isSweeping: boolean;
isRingCT: boolean;
updateStatus: (status: Status) => void;
api: WrappedNodeApi;
nettype: NetType;
};

@ -0,0 +1,79 @@
import { JSBigInt } from "./types";
import monero_config from "monero_utils/monero_config";
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
export namespace ERR {
export namespace RING {
export const INSUFF = Error("Ringsize is below the minimum.");
}
export namespace MIXIN {
export const INVAL = Error("Invalid mixin");
}
export namespace DEST {
export const INVAL = Error("You need to enter a valid destination");
}
export namespace AMT {
export const INSUFF = Error("The amount you've entered is too low");
}
export namespace PID {
export const NO_INTEG_ADDR = Error(
"Payment ID must be blank when using an Integrated Address",
);
export const NO_SUB_ADDR = Error(
"Payment ID must be blank when using a Subaddress",
);
export const INVAL = Error("Invalid payment ID.");
}
export namespace BAL {
export function insuff(amtAvail: JSBigInt, requiredAmt: JSBigInt) {
const { coinSymbol } = monero_config;
const amtAvailStr = monero_utils.formatMoney(amtAvail);
const requiredAmtStr = monero_utils.formatMoney(requiredAmt);
const errStr = `Your spendable balance is too low. Have ${amtAvailStr} ${coinSymbol} spendable, need ${requiredAmtStr} ${coinSymbol}.`;
return Error(errStr);
}
}
export namespace SWEEP {
export const TOTAL_NEQ_OUTS = Error(
"The sum of all outputs should be equal to the total amount for sweeping transactions",
);
}
export namespace TX {
export function failure(err?: Error) {
const errStr = err
? err.toString()
: "Failed to create transaction with unknown error.";
return Error(errStr);
}
export function submitUnknown(err: Error) {
return Error(
"Something unexpected occurred when submitting your transaction: " +
err,
);
}
}
export namespace PARSE_TRGT {
export const EMPTY = Error(
"Please supply a target address and a target amount.",
);
export const OA_RES = Error(
"You must resolve this OpenAlias address to a Monero address before calling SendFunds",
);
export function decodeAddress(targetAddress: string, err: Error) {
return Error(`Couldn't decode address ${targetAddress} : ${err}`);
}
export function amount(targetAmount: string, err: Error) {
return Error(`Couldn't parse amount ${targetAmount} : ${err}`);
}
}
}

@ -0,0 +1,36 @@
import { JSBigInt } from "./types";
export const DEFAULT_FEE_PRIORITY = 1;
export function calculateFee(
feePerKB: JSBigInt,
numOfBytes: number,
feeMultiplier: number,
) {
const numberOf_kB = new JSBigInt((numOfBytes + 1023.0) / 1024.0); // i.e. ceil
return calculateFeeKb(feePerKB, numberOf_kB, feeMultiplier);
}
export function calculateFeeKb(
feePerKB: JSBigInt,
numOfBytes: JSBigInt | number,
feeMultiplier: number,
) {
const numberOf_kB = new JSBigInt(numOfBytes);
const fee = feePerKB.multiply(feeMultiplier).multiply(numberOf_kB);
return fee;
}
export function multiplyFeePriority(prio: number) {
const fee_multiplier = [1, 4, 20, 166];
const priority = prio || DEFAULT_FEE_PRIORITY;
if (priority <= 0 || priority > fee_multiplier.length) {
throw "fee_multiplier_for_priority: simple_priority out of bounds";
}
const priority_idx = priority - 1;
return fee_multiplier[priority_idx];
}

@ -0,0 +1,16 @@
import { NetType } from "cryptonote_utils/nettype";
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
import { Log } from "./logger";
export function getTargetPubViewKey(
encPid: boolean,
targetAddress: string,
nettype: NetType,
): string | undefined {
if (encPid) {
const key = monero_utils.decode_address(targetAddress, nettype).view;
Log.Target.viewKey(key);
return key;
}
}

@ -0,0 +1,202 @@
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
import { JSBigInt, ParsedTarget, Output } from "./types";
export namespace Log {
export namespace Amount {
export function beforeFee(feelessTotal: JSBigInt, isSweeping: boolean) {
const feeless_total = isSweeping
? "all"
: monero_utils.formatMoney(feelessTotal);
console.log(`💬 Total to send, before fee: ${feeless_total}`);
}
export function change(changeAmount: JSBigInt) {
console.log("changeAmount", changeAmount);
}
export function changeAmountDivRem(amt: [JSBigInt, JSBigInt]) {
console.log("💬 changeAmountDivRem", amt);
}
export function toSelf(changeAmount: JSBigInt, selfAddress: string) {
console.log(
"Sending change of " +
monero_utils.formatMoneySymbol(changeAmount) +
" to " +
selfAddress,
);
}
}
export namespace Fee {
export function dynPerKB(dynFeePerKB: JSBigInt) {
console.log(
"Received dynamic per kb fee",
monero_utils.formatMoneySymbol(dynFeePerKB),
);
}
export function basedOnInputs(
newNeededFee: JSBigInt,
usingOuts: Output[],
) {
console.log(
"New fee: " +
monero_utils.formatMoneySymbol(newNeededFee) +
" for " +
usingOuts.length +
" inputs",
);
}
export function belowDustThreshold(changeDivDustRemainder: JSBigInt) {
console.log(
"💬 Miners will add change of " +
monero_utils.formatMoneyFullSymbol(changeDivDustRemainder) +
" to transaction fee (below dust threshold)",
);
}
export function estLowerThanReal(
estMinNetworkFee: JSBigInt,
feeActuallyNeededByNetwork: JSBigInt,
) {
console.log(
"💬 Need to reconstruct the tx with enough of a network fee. Previous fee: " +
monero_utils.formatMoneyFull(estMinNetworkFee) +
" New fee: " +
monero_utils.formatMoneyFull(feeActuallyNeededByNetwork),
);
console.log("Reconstructing tx....");
}
export function txKB(
txBlobBytes: number,
numOfKB: number,
estMinNetworkFee: JSBigInt,
) {
console.log(
txBlobBytes +
" bytes <= " +
numOfKB +
" KB (current fee: " +
monero_utils.formatMoneyFull(estMinNetworkFee) +
")",
);
}
export function successfulTx(finalNetworkFee: JSBigInt) {
console.log(
"💬 Successful tx generation, submitting tx. Going with final_networkFee of ",
monero_utils.formatMoney(finalNetworkFee),
);
}
}
export namespace Balance {
export function requiredBase(
totalAmount: JSBigInt,
isSweeping: boolean,
) {
if (isSweeping) {
console.log("Balance required: all");
} else {
console.log(
"Balance required: " +
monero_utils.formatMoneySymbol(totalAmount),
);
}
}
export function requiredPostRct(totalAmount: JSBigInt) {
console.log(
"~ Balance required: " +
monero_utils.formatMoneySymbol(totalAmount),
);
}
}
export namespace Output {
export function uniformity(fakeAddress: string) {
console.log(
"Sending 0 XMR to a fake address to keep tx uniform (no change exists): " +
fakeAddress,
);
}
export function display(out: Output) {
console.log(
"Using output: " +
monero_utils.formatMoney(out.amount) +
" - " +
JSON.stringify(out),
);
}
}
export namespace Target {
export function display(fundTargets: ParsedTarget[]) {
console.log("fundTransferDescriptions so far", fundTargets);
}
export function fullDisplay(fundTargets: ParsedTarget[]) {
console.log("Destinations: ");
monero_utils.printDsts(fundTargets);
}
export function displayDecomposed(splitDestinations: ParsedTarget[]) {
console.log("Decomposed destinations:");
monero_utils.printDsts(splitDestinations);
}
export function viewKey(viewKey: string) {
console.log("got target address's view key", viewKey);
}
}
export namespace Transaction {
export function signed(signedTx) {
console.log("signed tx: ", JSON.stringify(signedTx));
}
export function serializedAndHash(
serializedTx: string,
txHash: string,
) {
console.log("tx serialized: " + serializedTx);
console.log("Tx hash: " + txHash);
}
}
export namespace SelectOutsAndAmtForMix {
export function target(targetAmount: JSBigInt) {
console.log(
"Selecting outputs to use. target: " +
monero_utils.formatMoney(targetAmount),
);
}
export namespace Dusty {
export function notSweeping() {
console.log(
"Not sweeping, and found a dusty (though maybe mixable) output... skipping it!",
);
}
export function nonRct() {
console.log(
"Sweeping, and found a dusty but unmixable (non-rct) output... skipping it!",
);
}
export function rct() {
console.log(
"Sweeping and found a dusty but mixable (rct) amount... keeping it!",
);
}
}
export function usingOut(outAmount: JSBigInt, out: Output) {
console.log(
`Using output: ${monero_utils.formatMoney(
outAmount,
)} - ${JSON.stringify(out)}`,
);
}
}
}

@ -0,0 +1,3 @@
export function possibleOAAddress(address: string) {
return address.includes(".");
}

@ -0,0 +1,50 @@
import { JSBigInt, Output } from "./types";
import { popRandElement } from "./arr_utils";
import { Log } from "./logger";
import monero_config from "monero_utils/monero_config";
export function selectOutputsAndAmountForMixin(
targetAmount: JSBigInt,
unusedOuts: Output[],
isRingCT: boolean,
sweeping: boolean,
) {
Log.SelectOutsAndAmtForMix.target(targetAmount);
let usingOutsAmount = new JSBigInt(0);
const usingOuts: Output[] = [];
const remainingUnusedOuts = unusedOuts.slice(); // take copy so as to prevent issue if we must re-enter tx building fn if fee too low after building
while (
usingOutsAmount.compare(targetAmount) < 0 &&
remainingUnusedOuts.length > 0
) {
const out = popRandElement(remainingUnusedOuts);
if (!isRingCT && out.rct) {
// out.rct is set by the server
continue; // skip rct outputs if not creating rct tx
}
const outAmount = new JSBigInt(out.amount);
if (outAmount.compare(monero_config.dustThreshold) < 0) {
// amount is dusty..
if (!sweeping) {
Log.SelectOutsAndAmtForMix.Dusty.notSweeping();
continue;
}
if (!out.rct) {
Log.SelectOutsAndAmtForMix.Dusty.rct();
continue;
} else {
Log.SelectOutsAndAmtForMix.Dusty.nonRct();
}
}
usingOuts.push(out);
usingOutsAmount = usingOutsAmount.add(outAmount);
Log.SelectOutsAndAmtForMix.usingOut(outAmount, out);
}
return {
usingOuts,
usingOutsAmount,
remainingUnusedOuts,
};
}

@ -0,0 +1,49 @@
import { ParsedTarget, RawTarget, JSBigInt } from "./types";
import { NetType } from "cryptonote_utils/nettype";
import { ERR } from "./errors";
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
import { possibleOAAddress } from "./open_alias_lite";
/**
* @description Map through the provided targets and normalize each address/amount pair
*
* Addresses are checked to see if they may belong to an OpenAlias address, and rejected if so.
* Then they are validated by attempting to decode them.
*
* Amounts are attempted to be parsed from string value to BigInt value
*
* The validated address / parsed amount pairs are then returned
*
* @export
* @param {RawTarget[]} targetsToParse
* @param {NetType} nettype
* @returns {ParsedTarget[]}
*/
export function parseTargets(
targetsToParse: RawTarget[],
nettype: NetType,
): ParsedTarget[] {
return targetsToParse.map(({ address, amount }) => {
if (!address && !amount) {
throw ERR.PARSE_TRGT.EMPTY;
}
if (possibleOAAddress(address)) {
throw ERR.PARSE_TRGT.OA_RES;
}
const amountStr = amount.toString();
try {
monero_utils.decode_address(address, nettype);
} catch (e) {
throw ERR.PARSE_TRGT.decodeAddress(address, e);
}
try {
const parsedAmount: JSBigInt = monero_utils.parseMoney(amountStr);
return { address, amount: parsedAmount };
} catch (e) {
throw ERR.PARSE_TRGT.amount(amountStr, e);
}
});
}

@ -0,0 +1,60 @@
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
import monero_paymentID_utils from "monero_utils/monero_paymentID_utils";
import { NetType } from "cryptonote_utils/nettype";
import { ERR } from "./errors";
/**
*
* @description
* Attempts to decode the provided address based on its nettype to break it down into its components
* {pubSend, pubView, integratedPaymentId}
*
* Then based on the decoded values, determines if the payment ID (if supplied) should be encrypted or not.
*
* If a payment ID is not supplied, it may be grabbed from the integratedPaymentId component of the decoded
* address if provided.
*
* At each step, invariants are enforced to prevent the following scenarios.
*
*
* 1. Supplied PID + Integrated PID
* 2. Supplied PID + Sending to subaddress
* 3. Invalid supplied PID
*
*
* @export
* @param {string} address
* @param {NetType} nettype
* @param {(string | null)} pid
*/
export function checkAddressAndPidValidity(
address: string,
nettype: NetType,
pid: string | null,
) {
let retPid = pid;
let encryptPid = false;
const decodedAddress = monero_utils.decode_address(address, nettype);
// assert that the target address is not of type integrated nor subaddress
// if a payment id is included
if (retPid) {
if (decodedAddress.intPaymentId) {
throw ERR.PID.NO_INTEG_ADDR;
} else if (monero_utils.is_subaddress(address, nettype)) {
throw ERR.PID.NO_SUB_ADDR;
}
}
// if the target address is integrated
// then encrypt the payment id
// and make sure its also valid
if (decodedAddress.intPaymentId) {
retPid = decodedAddress.intPaymentId;
encryptPid = true;
} else if (!monero_paymentID_utils.IsValidPaymentIDOrNoPaymentID(retPid)) {
throw ERR.PID.INVAL;
}
return { pid: retPid, encryptPid };
}

@ -0,0 +1,408 @@
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
import monero_config from "monero_utils/monero_config";
import { getTargetPubViewKey } from "../key_utils";
import {
TotalAmtAndEstFeeParams,
ConstructTxParams,
EstRctFeeAndAmtParams,
ConstructFundTargetsParams,
} from "./types";
import { ERR } from "../errors";
import { Log } from "../logger";
import { popRandElement } from "../arr_utils";
import { calculateFee, multiplyFeePriority } from "../fee_utils";
import { JSBigInt, ParsedTarget } from "../types";
// #region totalAmtAndEstFee
/**
* @description
* Recalculates the fee and total amount needed for the transaction to be sent. RCT + non sweeping transactions will have their
* network fee increased if fee calculation based on the number of outputs needed is higher than the passed-in fee. RCT+ sweeping transactions
* are just checked if they have enough balance to proceed with the transaction. Non-RCT transactions will have no fee recalculation done on them.
* @export
* @param {TotalAmtAndEstFeeParams} params
* @returns
*/
export function totalAmtAndEstFee(params: TotalAmtAndEstFeeParams) {
const {
baseTotalAmount,
networkFee,
isRingCT,
usingOuts,
} = params;
if (!isRingCT) {
return { newFee: networkFee, totalAmount: baseTotalAmount };
}
/* if (usingOuts.length > 1 && isRingCT )*/
const { newFee, totalAmount } = estRctFeeAndAmt(params);
Log.Fee.basedOnInputs(newFee, usingOuts);
return { newFee, totalAmount };
}
function estRctFeeAndAmt(params: EstRctFeeAndAmtParams) {
const {
mixin,
usingOuts,
usingOutsAmount,
simplePriority,
feePerKB, // obtained from server, so passed in
networkFee,
isSweeping,
} = params;
let feeBasedOnOuts = calculateFee(
feePerKB,
monero_utils.estimateRctSize(usingOuts.length, mixin, 2),
multiplyFeePriority(simplePriority),
);
// if newNeededFee < neededFee, use neededFee instead
//(should only happen on the 2nd or later times through(due to estimated fee being too low))
if (feeBasedOnOuts.compare(networkFee) < 0) {
feeBasedOnOuts = networkFee;
}
const [totalAmount, newFee] = isSweeping
? estRctSwpingAmt(usingOutsAmount, feeBasedOnOuts)
: estRctNonSwpAmt(params, feeBasedOnOuts);
return { totalAmount, newFee };
}
function estRctSwpingAmt(usingOutsAmount: JSBigInt, fee: JSBigInt) {
/*
// 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;
}
*/
// feeless total is equivalent to all outputs (since its a sweeping tx)
// subtracted from the newNeededFee (either from min tx cost or calculated cost based on outputs)
const _feelessTotal = usingOutsAmount.subtract(fee);
// if the feeless total is less than 0 (so sum of all outputs is still less than network fee)
// then reject tx
if (_feelessTotal.compare(0) < 1) {
throw ERR.BAL.insuff(usingOutsAmount, fee);
}
// otherwise make the total amount the feeless total + the new fee
const totalAmount = _feelessTotal.add(fee);
return [totalAmount, fee];
}
function estRctNonSwpAmt(params: EstRctFeeAndAmtParams, fee: JSBigInt) {
const {
mixin,
remainingUnusedOuts,
usingOuts,
usingOutsAmount,
simplePriority,
feelessTotal,
feePerKB, // obtained from server, so passed in
} = params;
// make the current total amount equivalent to the feeless total and the new needed fee
let currTotalAmount = feelessTotal.add(fee);
// add outputs 1 at a time till we either have them all or can meet the fee
// this case can happen when the fee calculated via outs size
// is greater than the minimum tx fee size,
// requiring a higher fee, so more outputs (if available)
// need to be selected to fufill the difference
let newFee = fee;
while (
usingOutsAmount.compare(currTotalAmount) < 0 &&
remainingUnusedOuts.length > 0
) {
const out = popRandElement(remainingUnusedOuts);
Log.Output.display(out);
// and recalculate invalidated values
newFee = calculateFee(
feePerKB,
monero_utils.estimateRctSize(usingOuts.length, mixin, 2),
multiplyFeePriority(simplePriority),
);
currTotalAmount = feelessTotal.add(newFee);
}
const totalAmount = currTotalAmount;
return [totalAmount, newFee];
}
// #endregion totalAmtAndEstFee
// #region validateAndConstructFundTargets
/**
* @description
* 1. Validates the total amount needed for the tx against the available amounts via the sum of all outputs
* to see if the sender has sufficient funds.
*
* 2. Then, a list of sending targets will be constructed, always consisting of the target address and amount they want to send to, and possibly a change address,
* if the sum of outs is greater than the amount sent + fee needed, and possibly a fake address + 0 amount to keep output uniformity if no change address
* was generated.
*
* @export
* @param {ConstructFundTargetsParams} params
* @returns
*/
export function validateAndConstructFundTargets(
params: ConstructFundTargetsParams,
) {
const {
senderAddress,
targetAddress,
feelessTotal,
totalAmount,
usingOutsAmount,
isSweeping,
isRingCT,
nettype,
} = params;
// Now we can validate available balance with usingOutsAmount (TODO? maybe this check can be done before selecting outputs?)
const outsCmpToTotalAmounts = usingOutsAmount.compare(totalAmount);
const outsLessThanTotal = outsCmpToTotalAmounts < 0;
const outsGreaterThanTotal = outsCmpToTotalAmounts > 0;
const outsEqualToTotal = outsCmpToTotalAmounts === 0;
// what follows is comparision of the sum of outs amounts
// vs the total amount actually needed
// while also building up a list of addresses to send to
// along with the amounts
if (outsLessThanTotal) {
throw ERR.BAL.insuff(usingOutsAmount, totalAmount);
}
// Now we can put together the list of fund transfers we need to perform
// excluding the tx fee
// since that is included in the tx in its own field
const fundTargets: ParsedTarget[] = []; // to build…
// I. the actual transaction the user is asking to do
fundTargets.push({
address: targetAddress,
amount: feelessTotal,
});
// 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
// })
// some amount of the total outputs will likely need to be returned to the user as "change":
if (outsGreaterThanTotal) {
if (isSweeping) {
throw ERR.SWEEP.TOTAL_NEQ_OUTS;
}
// where the change amount is whats left after sending to other addresses + fee
const changeAmount = usingOutsAmount.subtract(totalAmount);
Log.Amount.change(changeAmount);
if (isRingCT) {
// for RCT we don't presently care about dustiness so add entire change amount
Log.Amount.toSelf(changeAmount, senderAddress);
fundTargets.push({
address: senderAddress,
amount: changeAmount,
});
} else {
// pre-ringct
// do not give ourselves change < dust threshold
const [quotient, remainder] = changeAmount.divRem(
monero_config.dustThreshold,
);
Log.Amount.changeAmountDivRem([quotient, remainder]);
if (!remainder.isZero()) {
// miners will add dusty change to fee
Log.Fee.belowDustThreshold(remainder);
}
if (!quotient.isZero()) {
// send non-dusty change to our address
const usableChange = quotient.multiply(
monero_config.dustThreshold,
);
Log.Amount.toSelf(usableChange, senderAddress);
fundTargets.push({
address: senderAddress,
amount: usableChange,
});
}
}
} else if (outsEqualToTotal) {
// if outputs are equivalent to the total amount
// this should always fire when sweeping
// since we want to spend all outputs anyway
if (isRingCT) {
// then create random destination to keep 2 outputs always in case of 0 change
// so we dont create 1 output (outlier)
const fakeAddress = monero_utils.create_address(
monero_utils.random_scalar(),
nettype,
).public_addr;
Log.Output.uniformity(fakeAddress);
fundTargets.push({
address: fakeAddress,
amount: JSBigInt.ZERO,
});
}
}
return { fundTargets };
}
// #endregion validateAndConstructFundTargets
//#region constructTx
export function constructTx(params: ConstructTxParams) {
const { signedTx } = makeSignedTx(params);
const { serializedSignedTx, txHash } = getSerializedTxAndHash(signedTx);
const { numOfKB } = getTxSize(serializedSignedTx, params.networkFee);
return { numOfKB, txHash, serializedSignedTx };
}
function makeSignedTx(params: ConstructTxParams) {
try {
const {
senderPublicKeys,
senderPrivateKeys,
targetAddress,
fundTargets,
pid,
encryptPid,
mixOuts,
mixin,
usingOuts,
networkFee,
isRingCT,
nettype,
} = params;
Log.Target.fullDisplay(fundTargets);
const targetViewKey = getTargetPubViewKey(
encryptPid,
targetAddress,
nettype,
);
const splitDestinations: ParsedTarget[] = monero_utils.decompose_tx_destinations(
fundTargets,
isRingCT,
);
Log.Target.displayDecomposed(splitDestinations);
const signedTx = monero_utils.create_transaction(
senderPublicKeys,
senderPrivateKeys,
splitDestinations,
usingOuts,
mixOuts,
mixin,
networkFee,
pid,
encryptPid,
targetViewKey,
0,
isRingCT,
nettype,
);
Log.Transaction.signed(signedTx);
return { signedTx };
} catch (e) {
throw ERR.TX.failure(e);
}
}
function getSerializedTxAndHash(signedTx) {
type ReturnVal = {
serializedSignedTx: string;
txHash: string;
};
// pre rct
if (signedTx.version === 1) {
const serializedSignedTx = monero_utils.serialize_tx(signedTx);
const txHash = monero_utils.cn_fast_hash(serializedSignedTx);
const ret: ReturnVal = {
serializedSignedTx,
txHash,
};
Log.Transaction.serializedAndHash(serializedSignedTx, txHash);
return ret;
}
// rct
else {
const { raw, hash } = monero_utils.serialize_rct_tx_with_hash(signedTx);
const ret: ReturnVal = {
serializedSignedTx: raw,
txHash: hash,
};
Log.Transaction.serializedAndHash(raw, hash);
return ret;
}
}
function getTxSize(serializedSignedTx: string, estMinNetworkFee: JSBigInt) {
// work out per-kb fee for transaction and verify that it's enough
const txBlobBytes = serializedSignedTx.length / 2;
let numOfKB = Math.floor(txBlobBytes / 1024);
if (txBlobBytes % 1024) {
numOfKB++;
}
Log.Fee.txKB(txBlobBytes, numOfKB, estMinNetworkFee);
return { numOfKB };
}
// #endregion constructTx

@ -0,0 +1,75 @@
import { NetType } from "cryptonote_utils/nettype";
import {
ParsedTarget,
JSBigInt,
Pid,
ViewSendKeys,
Output,
AmountOutput,
} from "../types";
export type ConstructTxParams = {
senderPublicKeys: ViewSendKeys;
senderPrivateKeys: ViewSendKeys;
targetAddress: string;
fundTargets: ParsedTarget[];
pid: Pid;
encryptPid: boolean;
mixOuts?: AmountOutput[];
mixin: number;
usingOuts: Output[];
networkFee: JSBigInt;
isRingCT: boolean;
nettype: NetType;
};
export type TotalAmtAndEstFeeParams = {
usingOutsAmount: JSBigInt;
baseTotalAmount: JSBigInt;
mixin: number;
remainingUnusedOuts: Output[];
usingOuts: Output[];
simplePriority: number;
feelessTotal: JSBigInt;
feePerKB: JSBigInt; // obtained from server, so passed in
networkFee: JSBigInt;
isSweeping: boolean;
isRingCT: boolean;
};
export type EstRctFeeAndAmtParams = {
mixin: number;
usingOutsAmount: JSBigInt;
remainingUnusedOuts: Output[];
usingOuts: Output[];
simplePriority: number;
feelessTotal: JSBigInt;
feePerKB: JSBigInt; // obtained from server, so passed in
networkFee: JSBigInt;
isSweeping: boolean;
};
export type ConstructFundTargetsParams = {
senderAddress: string;
targetAddress: string;
feelessTotal: JSBigInt;
totalAmount: JSBigInt;
usingOutsAmount: JSBigInt;
isSweeping: boolean;
isRingCT: boolean;
nettype: NetType;
};

@ -0,0 +1,45 @@
import BigInt = require("cryptonote_utils/biginteger");
export const JSBigInt = BigInt.BigInteger;
export type JSBigInt = BigInt.BigInteger;
export type ViewSendKeys = {
view: string;
spend: string;
};
export type RawTarget = {
address: string;
amount: number;
};
export type ParsedTarget = {
address: string;
amount: JSBigInt;
};
export type Pid = string | null;
export type Output = {
amount: string;
public_key: string;
index: number;
global_index: number;
rct: string;
tx_id: number;
tx_hash: string;
tx_pub_key: string;
tx_prefix_hash: string;
spend_key_images: string;
timestamp: string;
height: number;
};
export type AmountOutput = {
amount: string;
outputs: RandomOutput[];
};
type RandomOutput = {
global_index: string;
public_key: string;
rct: string;
};

@ -0,0 +1,19 @@
export const V7_MIN_MIXIN = 6;
function _mixinToRingsize(mixin: number) {
return mixin + 1;
}
export function minMixin() {
return V7_MIN_MIXIN;
}
export function minRingSize() {
return _mixinToRingsize(minMixin());
}
export function fixedMixin() {
return minMixin(); /* using the monero app default to remove MM user identifiers */
}
export function fixedRingsize() {
return _mixinToRingsize(fixedMixin());
}

@ -0,0 +1,247 @@
// 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.
//
import monero_utils from "monero_utils/monero_cryptonote_utils_instance";
import { NetType } from "cryptonote_utils/nettype";
import { RawTarget, JSBigInt, Pid, ViewSendKeys } from "./internal_libs/types";
import {
calculateFee,
multiplyFeePriority,
calculateFeeKb,
} from "./internal_libs/fee_utils";
import { minMixin } from "./mixin_utils";
import { Status, sendFundStatus } from "./status_update_constants";
import { ERR } from "./internal_libs/errors";
import { Log } from "./internal_libs/logger";
import { parseTargets } from "./internal_libs/parse_target";
import { checkAddressAndPidValidity } from "./internal_libs/pid_utils";
import { WrappedNodeApi } from "./internal_libs/async_node_api";
import {
getRestOfTxData,
createTxAndAttemptToSend,
} from "./internal_libs/construct_tx_and_send";
export function estimatedTransactionNetworkFee(
nonZeroMixin: number,
feePerKB: JSBigInt,
simplePriority: number,
) {
const numOfInputs = 2; // this might change -- might select inputs
const numOfOutputs =
1 /*dest*/ + 1 /*change*/ + 0; /*no mymonero fee presently*/
// TODO: update est tx size for bulletproofs
// TODO: normalize est tx size fn naming
const estimatedTxSize = monero_utils.estimateRctSize(
numOfInputs,
nonZeroMixin,
numOfOutputs,
);
const estFee = calculateFee(
feePerKB,
estimatedTxSize,
multiplyFeePriority(simplePriority),
);
return estFee;
}
export type SendFundsRet = {
targetAddress: string;
sentAmount: number;
pid: Pid;
txHash: string;
txFee: JSBigInt;
};
export async function SendFunds(
targetAddress: string, // currency-ready wallet address, but not an OpenAlias address (resolve before calling)
nettype: NetType,
amountorZeroWhenSweep: number, // n value will be ignored for sweep
isSweeping: boolean, // send true to sweep - amountorZeroWhenSweep will be ignored
senderAddress: string,
senderPrivateKeys: ViewSendKeys,
senderPublicKeys: ViewSendKeys,
nodeAPI: any, // TODO: possibly factor this dependency
pidToParse: Pid,
mixin: number,
simplePriority: number,
updateStatus: (status: Status) => void,
): Promise<SendFundsRet> {
const api = new WrappedNodeApi(nodeAPI);
const isRingCT = true;
if (mixin < minMixin()) {
throw ERR.RING.INSUFF;
}
// parse & normalize the target descriptions by mapping them to Monero addresses & amounts
const targetAmount = isSweeping ? 0 : amountorZeroWhenSweep;
const target: RawTarget = {
address: targetAddress,
amount: targetAmount,
};
const [singleTarget] = parseTargets(
[target], // requires a list of descriptions - but SendFunds was
// not written with multiple target support as MyMonero does not yet support it
nettype,
);
if (!singleTarget) {
throw ERR.DEST.INVAL;
}
const { address, amount } = singleTarget;
const feelessTotal = new JSBigInt(amount);
Log.Amount.beforeFee(feelessTotal, isSweeping);
if (!isSweeping && feelessTotal.compare(0) <= 0) {
throw ERR.AMT.INSUFF;
}
const pidData = checkAddressAndPidValidity(address, nettype, pidToParse);
updateStatus(sendFundStatus.fetchingLatestBalance);
const { dynamicFeePerKB, unusedOuts } = await api.unspentOuts(
senderAddress,
senderPrivateKeys,
senderPublicKeys,
mixin,
isSweeping,
);
const feePerKB = dynamicFeePerKB;
// Transaction will need at least 1KB fee (or 13KB for RingCT)
const minNetworkTxSizeKb = /*isRingCT ? */ 13; /* : 1*/
const estMinNetworkFee = calculateFeeKb(
feePerKB,
minNetworkTxSizeKb,
multiplyFeePriority(simplePriority),
);
// construct commonly used parameters
const senderkeys = {
senderAddress,
senderPublicKeys,
senderPrivateKeys,
};
const targetData = {
targetAddress,
targetAmount,
};
const feeMeta = {
simplePriority,
feelessTotal,
feePerKB, // obtained from server, so passed in
};
const txMeta = {
isRingCT,
isSweeping,
nettype,
};
const externApis = {
updateStatus,
api,
};
// begin the network fee with the smallest fee possible
let networkFee = estMinNetworkFee;
// this loop should only execute at most twice
// 1st execution to generate the inital transaction
// 2nd execution if the initial transaction's fee is greater than
// what the predicted tx fee would be
while (true) {
// now we're going to try using this minimum fee but the function will be called again
// 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
const {
mixOuts,
fundTargets,
newFee,
usingOuts,
} = await getRestOfTxData({
...senderkeys,
...targetData,
mixin,
unusedOuts,
...feeMeta,
networkFee,
...txMeta,
...externApis,
});
networkFee = newFee; // reassign network fee to the new fee returned
const { txFee, txHash, success } = await createTxAndAttemptToSend({
...senderkeys,
...targetData,
fundTargets,
...pidData,
mixin,
mixOuts,
usingOuts,
...feeMeta,
networkFee,
...txMeta,
...externApis,
});
if (success) {
const sentAmount = isSweeping
? parseFloat(monero_utils.formatMoneyFull(feelessTotal))
: targetAmount;
return {
pid: pidData.pid,
sentAmount,
targetAddress,
txFee,
txHash,
};
} else {
// if the function call failed
// means that we need a higher fee that was returned
// so reassign network fee to it
networkFee = txFee;
}
}
}

@ -0,0 +1,17 @@
export const sendFundStatus = {
fetchingLatestBalance: 1,
calculatingFee: 2,
fetchingDecoyOutputs: 3, // may get skipped if 0 mixin
constructingTransaction: 4, // may go back to .calculatingFee
submittingTransaction: 5,
};
export const sendFundsStatusToMessage = {
1: "Fetching latest balance.",
2: "Calculating fee.",
3: "Fetching decoy outputs.",
4: "Constructing transaction.", // may go back to .calculatingFee
5: "Submitting transaction.",
};
export type Status = typeof sendFundStatus[keyof typeof sendFundStatus];

@ -32,7 +32,6 @@
},
"homepage": "https://github.com/mymonero/mymonero-core-js#readme",
"dependencies": {
"async": "^2.6.0",
"keccakjs": "^0.2.1"
},
"devDependencies": {

@ -165,7 +165,7 @@ async@^1.4.0:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
async@^2.1.4, async@^2.6.0:
async@^2.1.4:
version "2.6.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
dependencies:

Loading…
Cancel
Save