UNPKG

@xmr-core/xmr-mymonero-libs

Version:

A collection of utilities for building ontop of a MyMonero compatible API

341 lines (340 loc) 17.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); var key_utils_1 = require("../key_utils"); var errors_1 = require("../errors"); var logger_1 = require("../logger"); var arr_utils_1 = require("../arr_utils"); var fee_utils_1 = require("../fee_utils"); var biginteger_1 = require("@xmr-core/biginteger"); var xmr_transaction_1 = require("@xmr-core/xmr-transaction"); var xmr_fast_hash_1 = require("@xmr-core/xmr-fast-hash"); var xmr_crypto_utils_1 = require("@xmr-core/xmr-crypto-utils"); var xmr_constants_1 = require("@xmr-core/xmr-constants"); var xmr_money_1 = require("@xmr-core/xmr-money"); var xmr_crypto_utils_2 = require("@xmr-core/xmr-crypto-utils"); var xmr_str_utils_1 = require("@xmr-core/xmr-str-utils"); // #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 */ function totalAmtAndEstFee(params) { var baseTotalAmount = params.baseTotalAmount, networkFee = params.networkFee, isRingCT = params.isRingCT, usingOuts = params.usingOuts; if (!isRingCT) { return { newFee: networkFee, totalAmount: baseTotalAmount }; } /* if (usingOuts.length > 1 && isRingCT )*/ var _a = estRctFeeAndAmt(params), newFee = _a.newFee, totalAmount = _a.totalAmount; logger_1.Log.Fee.basedOnInputs(newFee, usingOuts); return { newFee: newFee, totalAmount: totalAmount }; } exports.totalAmtAndEstFee = totalAmtAndEstFee; function estRctFeeAndAmt(params) { var mixin = params.mixin, usingOuts = params.usingOuts, usingOutsAmount = params.usingOutsAmount, simplePriority = params.simplePriority, feePerKB = params.feePerKB, // obtained from server, so passed in networkFee = params.networkFee, isSweeping = params.isSweeping; var feeBasedOnOuts = fee_utils_1.calculateFee(feePerKB, xmr_transaction_1.estimateRctSize(usingOuts.length, mixin, 2), fee_utils_1.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; } var _a = isSweeping ? estRctSwpingAmt(usingOutsAmount, feeBasedOnOuts) : estRctNonSwpAmt(params, feeBasedOnOuts), totalAmount = _a[0], newFee = _a[1]; return { totalAmount: totalAmount, newFee: newFee }; } function estRctSwpingAmt(usingOutsAmount, fee) { /* // 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) var _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 errors_1.ERR.BAL.insuff(usingOutsAmount, fee); } // otherwise make the total amount the feeless total + the new fee var totalAmount = _feelessTotal.add(fee); return [totalAmount, fee]; } function estRctNonSwpAmt(params, fee) { var mixin = params.mixin, remainingUnusedOuts = params.remainingUnusedOuts, usingOuts = params.usingOuts, usingOutsAmount = params.usingOutsAmount, simplePriority = params.simplePriority, feelessTotal = params.feelessTotal, feePerKB = params.feePerKB; // make the current total amount equivalent to the feeless total and the new needed fee var 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 var newFee = fee; while (usingOutsAmount.compare(currTotalAmount) < 0 && remainingUnusedOuts.length > 0) { var out = arr_utils_1.popRandElement(remainingUnusedOuts); logger_1.Log.Output.display(out); // and recalculate invalidated values newFee = fee_utils_1.calculateFee(feePerKB, xmr_transaction_1.estimateRctSize(usingOuts.length, mixin, 2), fee_utils_1.multiplyFeePriority(simplePriority)); currTotalAmount = feelessTotal.add(newFee); } var 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 */ function validateAndConstructFundTargets(params) { var senderAddress = params.senderAddress, targetAddress = params.targetAddress, feelessTotal = params.feelessTotal, totalAmount = params.totalAmount, usingOutsAmount = params.usingOutsAmount, isSweeping = params.isSweeping, isRingCT = params.isRingCT, nettype = params.nettype; // Now we can validate available balance with usingOutsAmount (TODO? maybe this check can be done before selecting outputs?) var outsCmpToTotalAmounts = usingOutsAmount.compare(totalAmount); var outsLessThanTotal = outsCmpToTotalAmounts < 0; var outsGreaterThanTotal = outsCmpToTotalAmounts > 0; var 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 errors_1.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 var fundTargets = []; // 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 errors_1.ERR.SWEEP.TOTAL_NEQ_OUTS; } // where the change amount is whats left after sending to other addresses + fee var changeAmount = usingOutsAmount.subtract(totalAmount); logger_1.Log.Amount.change(changeAmount); if (isRingCT) { // for RCT we don't presently care about dustiness so add entire change amount logger_1.Log.Amount.toSelf(changeAmount, senderAddress); fundTargets.push({ address: senderAddress, amount: changeAmount, }); } else { // pre-ringct // do not give ourselves change < dust threshold var _a = changeAmount.divRem(xmr_constants_1.config.dustThreshold), quotient = _a[0], remainder = _a[1]; logger_1.Log.Amount.changeAmountDivRem([quotient, remainder]); if (!remainder.isZero()) { // miners will add dusty change to fee logger_1.Log.Fee.belowDustThreshold(remainder); } if (!quotient.isZero()) { // send non-dusty change to our address var usableChange = quotient.multiply(xmr_constants_1.config.dustThreshold); logger_1.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) var fakeAddress = xmr_crypto_utils_1.create_address(xmr_crypto_utils_1.random_scalar(), nettype) .public_addr; logger_1.Log.Output.uniformity(fakeAddress); fundTargets.push({ address: fakeAddress, amount: biginteger_1.BigInt.ZERO, }); } } return { fundTargets: fundTargets }; } exports.validateAndConstructFundTargets = validateAndConstructFundTargets; // #endregion validateAndConstructFundTargets //#region constructTx function constructTx(params) { return __awaiter(this, void 0, void 0, function () { var signedTx, _a, serializedSignedTx, txHash, numOfKB; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, makeSignedTx(params)]; case 1: signedTx = (_b.sent()).signedTx; xmr_str_utils_1.JSONPrettyPrint("constructTx", { signedTx: signedTx }, "signedTx"); _a = getSerializedTxAndHash(signedTx), serializedSignedTx = _a.serializedSignedTx, txHash = _a.txHash; xmr_str_utils_1.JSONPrettyPrint("constructTx", { serializedSignedTx: serializedSignedTx, txHash: txHash }, "serializedTxAndHash"); numOfKB = getTxSize(serializedSignedTx, params.networkFee).numOfKB; xmr_str_utils_1.JSONPrettyPrint("constructTx", { numOfKB: numOfKB }, "numOfKb"); return [2 /*return*/, { numOfKB: numOfKB, txHash: txHash, serializedSignedTx: serializedSignedTx }]; } }); }); } exports.constructTx = constructTx; function makeSignedTx(params) { return __awaiter(this, void 0, void 0, function () { var senderPublicKeys, senderPrivateKeys, targetAddress, fundTargets, pid, encryptPid, mixOuts, mixin, usingOuts, networkFee, isRingCT, nettype, hwdev, targetViewKey, splitDestinations, signedTx, _a, _b, _c, _d, _e, e_1; return __generator(this, function (_f) { switch (_f.label) { case 0: _f.trys.push([0, 5, , 6]); senderPublicKeys = params.senderPublicKeys, senderPrivateKeys = params.senderPrivateKeys, targetAddress = params.targetAddress, fundTargets = params.fundTargets, pid = params.pid, encryptPid = params.encryptPid, mixOuts = params.mixOuts, mixin = params.mixin, usingOuts = params.usingOuts, networkFee = params.networkFee, isRingCT = params.isRingCT, nettype = params.nettype, hwdev = params.hwdev; logger_1.Log.Target.fullDisplay(fundTargets); targetViewKey = key_utils_1.getTargetPubViewKey(encryptPid, targetAddress, nettype); splitDestinations = xmr_money_1.decompose_tx_destinations(fundTargets, isRingCT); logger_1.Log.Target.displayDecomposed(splitDestinations); _a = xmr_transaction_1.create_transaction; _b = [senderPublicKeys]; _c = [{}, senderPrivateKeys]; _d = {}; if (!xmr_crypto_utils_2.isRealDevice(hwdev)) return [3 /*break*/, 2]; return [4 /*yield*/, hwdev.get_secret_keys()]; case 1: _e = (_f.sent()).viewKey; return [3 /*break*/, 3]; case 2: _e = senderPrivateKeys.view; _f.label = 3; case 3: return [4 /*yield*/, _a.apply(void 0, _b.concat([__assign.apply(void 0, _c.concat([(_d.view = _e, _d)])), splitDestinations, usingOuts, mixOuts, mixin, networkFee, pid, encryptPid, targetViewKey, 0, isRingCT, nettype, hwdev]))]; case 4: signedTx = _f.sent(); logger_1.Log.Transaction.signed(signedTx); return [2 /*return*/, { signedTx: signedTx }]; case 5: e_1 = _f.sent(); throw errors_1.ERR.TX.failure(e_1); case 6: return [2 /*return*/]; } }); }); } function getSerializedTxAndHash(signedTx) { // pre rct if (signedTx.version === 1) { var serializedSignedTx = xmr_transaction_1.serialize_non_rct_tx(signedTx); var txHash = xmr_fast_hash_1.cn_fast_hash(serializedSignedTx); var ret = { serializedSignedTx: serializedSignedTx, txHash: txHash, }; logger_1.Log.Transaction.serializedAndHash(serializedSignedTx, txHash); return ret; } // rct else { var _a = xmr_transaction_1.serialize_rct_tx_with_hash(signedTx), raw = _a.raw, hash = _a.hash; var ret = { serializedSignedTx: raw, txHash: hash, }; logger_1.Log.Transaction.serializedAndHash(raw, hash); return ret; } } function getTxSize(serializedSignedTx, estMinNetworkFee) { // work out per-kb fee for transaction and verify that it's enough var txBlobBytes = serializedSignedTx.length / 2; var numOfKB = Math.floor(txBlobBytes / 1024); if (txBlobBytes % 1024) { numOfKB++; } logger_1.Log.Fee.txKB(txBlobBytes, numOfKB, estMinNetworkFee); return { numOfKB: numOfKB }; } // #endregion constructTx //# sourceMappingURL=tx_utils.js.map