[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 commentpull/37/head
parent
6cdb432c2c
commit
1ad8ff2436
@ -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;
|
@ -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 @@
|
||||
export * from "./construct_tx_and_send";
|
@ -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 @@
|
||||
export * from "./tx_utils";
|
@ -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];
|
Loading…
Reference in new issue