@xmr-core/xmr-mymonero-libs
Version:
A collection of utilities for building ontop of a MyMonero compatible API
324 lines (281 loc) • 7.28 kB
text/typescript
import {
calculateFee,
multiplyFeePriority,
calculateFeeKb,
DEFAULT_FEE_PRIORITY,
} from "./internal_libs/fee_utils";
import { minMixin, fixedMixin } 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 {
getRestOfTxData,
createTxAndAttemptToSend,
} from "./internal_libs/construct_tx_and_send";
import { BigInt } from "@xmr-core/biginteger";
import { estimateRctSize } from "@xmr-core/xmr-transaction";
import { formatMoneyFull } from "@xmr-core/xmr-money";
import { RawTarget, Pid, ViewSendKeys } from "@xmr-core/xmr-transaction";
import { MyMoneroApi } from "../";
import { selectOutputsAndAmountForMixin } from "./internal_libs/output_selection";
import {
isRealDevice,
getAddressString,
DefaultDevice,
NetType,
HWDevice,
} from "@xmr-core/xmr-crypto-utils";
import { JSONPrettyPrint } from "@xmr-core/xmr-str-utils";
export function estimatedTransactionNetworkFee(
nonZeroMixin: number,
feePerKB: BigInt,
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 = 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: BigInt;
};
export async function sendFundsSimple(
targetAddress: string,
amount: number,
pid: Pid,
updateStatus: (status: Status) => void,
hwdev: HWDevice,
) {
if (!isRealDevice(hwdev)) {
throw Error(
"[sendFundsSimple] This function can only be used with a real hardware device",
);
}
const {
spend_public_key,
view_public_key,
} = await hwdev.get_public_address();
const address = await getAddressString(hwdev);
const senderPrivateKeys: ViewSendKeys = {
spend: (await hwdev.get_secret_keys()).spendKey,
view: await hwdev.export_private_view_key(),
};
const senderPublicKeys: ViewSendKeys = {
spend: spend_public_key,
view: view_public_key,
};
return await sendFunds(
targetAddress,
NetType.MAINNET,
amount,
false,
address,
senderPrivateKeys,
senderPublicKeys,
pid,
fixedMixin(),
DEFAULT_FEE_PRIORITY,
hwdev,
updateStatus,
);
}
export async function sendFundsRawKeys(
targetAddress: string,
amount: number,
senderAddress: string,
senderPrivateKeys: ViewSendKeys,
senderPublicKeys: ViewSendKeys,
updateStatus: (status: Status) => void,
) {
return await sendFunds(
targetAddress,
NetType.MAINNET,
amount,
false,
senderAddress,
senderPrivateKeys,
senderPublicKeys,
null,
fixedMixin(),
DEFAULT_FEE_PRIORITY,
new DefaultDevice(),
updateStatus,
);
}
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,
pidToParse: Pid,
mixin: number,
simplePriority: number,
hwdev: HWDevice,
updateStatus: (status: Status) => void,
outputAndAmountSelector = selectOutputsAndAmountForMixin,
api = MyMoneroApi,
): Promise<SendFundsRet> {
JSONPrettyPrint(
"sendFunds",
{
targetAddress,
nettype,
amountOrZeroWhenSweep,
isSweeping,
senderAddress,
senderPrivateKeys,
senderPublicKeys,
pidToParse,
mixin,
simplePriority,
},
"args",
);
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 BigInt(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 { per_kb_fee: feePerKB, unusedOuts } = await api.unspentOutputs(
senderAddress,
senderPrivateKeys.view,
senderPublicKeys.spend,
senderPrivateKeys.spend,
mixin,
hwdev,
);
// 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,
},
outputAndAmountSelector,
);
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,
hwdev,
});
if (success) {
const sentAmount = isSweeping
? parseFloat(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;
}
}
}