@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
text/typescript
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