@kiroboio/fct-core
Version:
Kirobo.io FCT Core library
286 lines • 11.5 kB
JavaScript
import { utils } from "ethers";
// fctCall overhead (1st call) - 40k
// fctCall overhead (other calls) - 11k
const pluginCache = new Map();
// TODO: This needs to be heavily optimized.
const WHOLE_IN_BPS = 10000n;
const fees = {
beforeCallingBatchMultiSigCall: 5000n,
FCTControllerOverhead: 43000n,
gasBeforeEncodedLoop: 3000n,
gasForEncodingCall: 8000n,
additionalGasForEncodingCall: 100n,
FCTControllerRegisterCall: 43000n,
signatureRecovery: 6000n,
miscGasBeforeMcallLoop: 1700n,
paymentApproval: 9000n,
paymentsOutBase: 24500n,
paymentsOutPerPayment: 1300n,
totalCallsChecker: 16000n,
estimateExtraCommmonGasCost: 4000n,
mcallOverheadFirstCall: 40000n,
mcallOverheadOtherCalls: 11000n,
defaultGasLimit: 30000n,
};
// Arbitrum fees are 13x higher than Ethereum fees. Multiply all fees by 13.
const arbitrumFees = Object.fromEntries(Object.entries(fees).map(([key, value]) => [key, BigInt(value) * 13n]));
export function getFee(key, chainId) {
if (chainId === "42161" || chainId === "421613") {
return arbitrumFees[key];
}
return fees[key];
}
const getEncodingMcallCost = (callCount, chainId) => {
return (BigInt(callCount) * getFee("gasForEncodingCall", chainId) +
(BigInt(callCount) * BigInt(callCount - 1) * getFee("additionalGasForEncodingCall", chainId)) / 2n);
};
const getSignatureRecoveryCost = (signatureCount, chainId) => {
return BigInt(signatureCount) * getFee("signatureRecovery", chainId);
};
const getPaymentsOutCost = (callCount, chainId) => {
return getFee("paymentsOutBase", chainId) + BigInt(callCount) * getFee("paymentsOutPerPayment", chainId);
};
const getExtraCommonGas = (payersCount, msgDataLength) => {
return 23100n + 4600n * BigInt(payersCount) + (77600n * BigInt(msgDataLength)) / 10000n;
};
const getPayers = (calls, pathIndexes) => {
return pathIndexes.reduce((acc, pathIndex) => {
const call = calls[Number(pathIndex)].get();
const payerIndex = call.options.payerIndex;
if (payerIndex === 0)
return acc;
if (payerIndex === +pathIndex + 1) {
const payer = call.from;
if (payer && payer !== acc[acc.length - 1] && typeof payer === "string") {
acc.push(payer);
}
return acc;
}
// Else we dont know the from
const payer = calls[payerIndex - 1].get().from;
if (payer && payer !== acc[acc.length - 1] && typeof payer === "string") {
acc.push(payer);
}
return acc;
}, []);
};
const getAllSigners = (calls) => {
return calls.reduce((acc, call) => {
const from = call.get().from;
if (typeof from === "string" && !acc.includes(from)) {
acc.push(from);
}
return acc;
}, []);
};
export function getPayersForRoute({ FCT, chainId, calls, pathIndexes, calldata, fctID, }) {
const payers = getPayers(calls, pathIndexes);
const allSigners = getAllSigners(calls);
const batchMultiSigCallOverhead = getFee("FCTControllerOverhead", chainId) +
getFee("gasBeforeEncodedLoop", chainId) +
getEncodingMcallCost(calls.length, chainId) +
getFee("FCTControllerRegisterCall", chainId) +
getSignatureRecoveryCost(allSigners.length + 1, chainId) + // +1 because verification signature
getFee("miscGasBeforeMcallLoop", chainId);
const overhead = getFee("beforeCallingBatchMultiSigCall", chainId) +
batchMultiSigCallOverhead +
getPaymentsOutCost(calls.length, chainId) +
getFee("totalCallsChecker", chainId) +
getFee("estimateExtraCommmonGasCost", chainId);
const commonGas = getExtraCommonGas(payers.length, calldata.length) + overhead;
const commonGasPerCall = commonGas / BigInt(pathIndexes.length);
const gasForFCTCall = pathIndexes.reduce((acc, path) => {
const call = calls[Number(path)];
const _call = call.get();
const options = _call.options;
const payerIndex = options.payerIndex;
// If payer is the activator, dont add it to the needed fuel
if (payerIndex === 0)
return acc;
const payer = calls[payerIndex - 1].get().from;
if (typeof payer !== "string")
return acc;
let gas;
if (options.gasLimit === "0") {
gas = getFee("defaultGasLimit", chainId);
}
else {
gas = BigInt(options.gasLimit);
}
const amount = gas + commonGasPerCall;
if (acc[payer]) {
acc[payer] += amount;
}
else {
acc[payer] = amount;
}
return acc;
}, {});
const gasForPaymentApprovals = payers.reduce((acc, address) => {
const fee = getFee("paymentApproval", chainId);
if (acc[address]) {
acc[address] += fee;
}
else {
acc[address] = fee;
}
return acc;
}, {});
return payers.map((payer) => {
const gas = gasForFCTCall[payer] + gasForPaymentApprovals[payer];
return {
payer,
gas: gas || 0n,
};
});
}
export function getEffectiveGasPrice({ maxGasPrice, gasPrice, baseFeeBPS, bonusFeeBPS, }) {
return ((BigInt(gasPrice) * (WHOLE_IN_BPS + baseFeeBPS) + (BigInt(maxGasPrice) - BigInt(gasPrice)) * bonusFeeBPS) /
WHOLE_IN_BPS).toString();
}
export function getCostInKiro({ ethPriceInKIRO, ethCost, }) {
return (((ethCost || 0n) * BigInt(ethPriceInKIRO)) / 10n ** 18n).toString();
}
export function getGasPrices({ maxGasPrice, gasPrice, baseFeeBPS, bonusFeeBPS, }) {
// NOTE: Calculation for base
// uint256 l1GasUsed = (dataLength * 84500) / WHOLE_IN_BPS;
// uint256 effectiveGasPrice = (OPGasOracle.baseFeeScalar() * 16 * OPGasOracle.l1BaseFee()) +
// (OPGasOracle.blobBaseFeeScalar() * OPGasOracle.blobBaseFee());
// uint256 fee = l1GasUsed * effectiveGasPrice;
const txGasPrice = gasPrice ? BigInt(gasPrice) : maxGasPrice;
const effectiveGasPrice = BigInt(getEffectiveGasPrice({
gasPrice: txGasPrice,
maxGasPrice,
baseFeeBPS,
bonusFeeBPS,
}));
return {
txGasPrice,
effectiveGasPrice,
};
}
export function getPayerMap({ FCT, fctID, paths, calldata, calls, gasPrice, maxGasPrice, baseFeeBPS, bonusFeeBPS, payableGasLimit, penalty, opStackGasFees, }) {
const chainId = FCT.chainId;
const { txGasPrice, effectiveGasPrice } = getGasPrices({
maxGasPrice,
gasPrice,
baseFeeBPS,
bonusFeeBPS,
});
let additionalGas = 0n;
// If the chain is OP Stack chain
if (chainId === "10" || chainId === "8453") {
if (!opStackGasFees) {
console.warn("For OP Stack chains (Base, Optimism) opStackGasFees should be provided to estimate gas more precisely.");
}
else {
// From Arch.sol:
// uint256 l1GasUsed = (dataLength * 84500) / WHOLE_IN_BPS;
// uint256 effectiveGasPrice = (OPGasOracle.baseFeeScalar() * 16 * OPGasOracle.l1BaseFee()) +
// (OPGasOracle.blobBaseFeeScalar() * OPGasOracle.blobBaseFee());
// uint256 fee = l1GasUsed * effectiveGasPrice;
// additionalGas = fee / (16 * 10 ** 6 * tx.gasprice);
const dataLength = BigInt(calldata.length / 2 - 1);
const l1GasUsed = (dataLength * 84500n) / 10000n;
const baseFeeScalar = BigInt(opStackGasFees.baseFeeScalar);
const l1BaseFee = BigInt(opStackGasFees.l1BaseFee);
const blobBaseFeeScalar = BigInt(opStackGasFees.blobBaseFeeScalar);
const blobBaseFee = BigInt(opStackGasFees.blobBaseFee);
const opStackEffectiveGasPrice = baseFeeScalar * 16n * l1BaseFee + blobBaseFeeScalar * blobBaseFee;
const fee = l1GasUsed * opStackEffectiveGasPrice;
additionalGas = fee / (16n * 10n ** 6n * txGasPrice);
}
}
return paths.map((path) => {
const payers = getPayersForRoute({
chainId,
calldata,
calls,
pathIndexes: path,
FCT,
fctID,
});
return payers.reduce((acc, payer) => {
let gas = payer.gas + additionalGas;
if (payableGasLimit) {
gas = gas > payableGasLimit ? payableGasLimit : gas;
}
const base = gas * txGasPrice;
const fee = gas * (effectiveGasPrice - txGasPrice);
const ethCost = base + fee;
return {
...acc,
[payer.payer]: {
...payer,
gas: gas,
pureEthCost: ethCost,
ethCost: (ethCost * BigInt(penalty || 10_000)) / 10000n,
},
};
}, {});
});
}
export function preparePaymentPerPayerResult({ payerMap, senders, ethPriceInKIRO, }) {
return senders.map((payer) => {
const { largest, smallest } = payerMap.reduce((currentValues, pathData) => {
if (!pathData[payer]) {
return currentValues;
}
const currentLargestValue = currentValues.largest?.ethCost;
const currentSmallestValue = currentValues.smallest?.ethCost;
const value = pathData[payer]?.pureEthCost || 0n;
if (value > currentLargestValue) {
currentValues.largest = pathData[payer];
}
if (currentSmallestValue == 0n || value < currentSmallestValue) {
currentValues.smallest = pathData[payer];
}
return currentValues;
}, {
largest: {
payer,
gas: 0n,
ethCost: 0n,
pureEthCost: 0n,
},
smallest: {
payer,
gas: 0n,
ethCost: 0n,
pureEthCost: 0n,
},
});
const largestKiroCost = getCostInKiro({ ethPriceInKIRO, ethCost: largest?.pureEthCost });
const smallestKiroCost = getCostInKiro({ ethPriceInKIRO, ethCost: smallest?.pureEthCost });
return {
payer,
largestPayment: {
gas: largest.gas.toString(),
tokenAmountInWei: largestKiroCost,
nativeAmountInWei: largest.ethCost.toString(),
tokenAmount: utils.formatEther(largestKiroCost),
nativeAmount: utils.formatEther(largest.ethCost.toString()),
},
smallestPayment: {
gas: smallest.gas.toString(),
tokenAmountInWei: smallestKiroCost,
nativeAmountInWei: smallest.ethCost.toString(),
tokenAmount: utils.formatEther(smallestKiroCost),
nativeAmount: utils.formatEther(smallest.ethCost.toString()),
},
};
});
}
// function getPlugin({ FCT, fctID, index }: { FCT: BatchMultiSigCall; fctID: string; index: number }) {
// const plugin = pluginCache.get(fctID + index);
// if (plugin || plugin === null) return plugin;
// try {
// const plugin = FCT.getPlugin(index);
// pluginCache.set(fctID + index, plugin);
// return plugin;
// } catch (e) {
// pluginCache.set(fctID + index, null);
// return null;
// }
// }
//# sourceMappingURL=getPaymentPerPayer.js.map