UNPKG

@xmr-core/xmr-mymonero-libs

Version:

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

431 lines (347 loc) 11.4 kB
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 { BigInt } from "@xmr-core/biginteger"; import { serialize_rct_tx_with_hash, estimateRctSize, serialize_non_rct_tx, create_transaction, } from "@xmr-core/xmr-transaction"; import { cn_fast_hash } from "@xmr-core/xmr-fast-hash"; import { SignedTransaction, ParsedTarget } from "@xmr-core/xmr-transaction"; import { create_address, random_scalar } from "@xmr-core/xmr-crypto-utils"; import { config } from "@xmr-core/xmr-constants"; import { decompose_tx_destinations } from "@xmr-core/xmr-money"; import { isRealDevice } from "@xmr-core/xmr-crypto-utils"; import { JSONPrettyPrint } from "@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 */ 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, 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: BigInt, fee: BigInt) { /* // 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: BigInt) { 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, 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( 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(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 = create_address(random_scalar(), nettype) .public_addr; Log.Output.uniformity(fakeAddress); fundTargets.push({ address: fakeAddress, amount: BigInt.ZERO, }); } } return { fundTargets }; } // #endregion validateAndConstructFundTargets //#region constructTx export async function constructTx(params: ConstructTxParams) { const { signedTx } = await makeSignedTx(params); JSONPrettyPrint("constructTx", { signedTx }, "signedTx"); const { serializedSignedTx, txHash } = getSerializedTxAndHash(signedTx); JSONPrettyPrint( "constructTx", { serializedSignedTx, txHash }, "serializedTxAndHash", ); const { numOfKB } = getTxSize(serializedSignedTx, params.networkFee); JSONPrettyPrint("constructTx", { numOfKB }, "numOfKb"); return { numOfKB, txHash, serializedSignedTx }; } async function makeSignedTx(params: ConstructTxParams) { try { const { senderPublicKeys, senderPrivateKeys, targetAddress, fundTargets, pid, encryptPid, mixOuts, mixin, usingOuts, networkFee, isRingCT, nettype, hwdev, } = params; Log.Target.fullDisplay(fundTargets); const targetViewKey = getTargetPubViewKey( encryptPid, targetAddress, nettype, ); const splitDestinations: ParsedTarget[] = decompose_tx_destinations( fundTargets, isRingCT, ); Log.Target.displayDecomposed(splitDestinations); const signedTx = await create_transaction( senderPublicKeys, { ...senderPrivateKeys, view: isRealDevice(hwdev) ? (await hwdev.get_secret_keys()).viewKey : senderPrivateKeys.view, }, splitDestinations, usingOuts, mixOuts, mixin, networkFee, pid, encryptPid, targetViewKey, 0, isRingCT, nettype, hwdev, ); Log.Transaction.signed(signedTx); return { signedTx }; } catch (e) { throw ERR.TX.failure(e); } } function getSerializedTxAndHash(signedTx: SignedTransaction) { type ReturnVal = { serializedSignedTx: string; txHash: string; }; // pre rct if (signedTx.version === 1) { const serializedSignedTx = serialize_non_rct_tx(signedTx); const txHash = cn_fast_hash(serializedSignedTx); const ret: ReturnVal = { serializedSignedTx, txHash, }; Log.Transaction.serializedAndHash(serializedSignedTx, txHash); return ret; } // rct else { const { raw, hash } = 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: BigInt) { // 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