@kiroboio/fct-core
Version:
Kirobo.io FCT Core library
387 lines • 16 kB
JavaScript
import { recoverTypedSignature, SignTypedDataVersion, TypedDataUtils } from "@metamask/eth-sig-util";
import { ethers, utils } from "ethers";
import { Graph } from "graphlib";
import { addresses } from "../../../constants";
import { InstanceOf } from "../../../helpers";
import { getAuthenticatorSignature } from "../../utils";
import { getAllRequiredApprovals } from "../../utils/getAllRequiredApprovals";
import { getVersionClass } from "../../versions/getVersion";
import { EIP712 } from "../EIP712";
import { FCTBase } from "../FCTBase";
import { SecureStorageAddressesSet } from "./constants";
import { getEffectiveGasPrice, getPayerMap, preparePaymentPerPayerResult } from "./utils/getPaymentPerPayer";
import { getPathsFromGraph, manageFCTNodesInGraph } from "./utils/paths";
import { executedCallsFromLogs, executedCallsFromRawLogs, getCallsFromTrace, getTraceData, manageValidationAndComputed, verifyMessageHash, } from "./utils/transactionTrace";
export class FCTUtils extends FCTBase {
_eip712;
_cache = new Map();
constructor(FCT) {
super(FCT);
this._eip712 = new EIP712(FCT);
}
get FCTData() {
return this.FCT.exportFCT();
}
async getAllRequiredApprovals() {
return getAllRequiredApprovals(this.FCT);
}
getCalldataForActuator({ signatures, purgedFCT = ethers.constants.HashZero, investor = ethers.constants.AddressZero, activator, externalSigners = [], variables = [], }) {
const Version = getVersionClass(this.FCT);
return Version.Utils.getCalldataForActuator({
signatures,
purgedFCT,
investor,
activator,
externalSigners,
variables,
});
}
getAuthenticatorSignature() {
return getAuthenticatorSignature(this._eip712.getTypedData());
}
recoverAddress(signature) {
try {
const FCT = this.FCT;
const data = new EIP712(FCT).getTypedData();
return recoverTypedSignature({
data,
version: SignTypedDataVersion.V4,
signature: utils.joinSignature(signature),
});
}
catch (e) {
return null;
}
}
getMessageHash() {
const buffer = TypedDataUtils.eip712Hash(this.FCTData.typedData, SignTypedDataVersion.V4);
return `0x${buffer.toString("hex")}`;
}
isValid(softValidation = false) {
const options = this.FCT.options;
const currentDate = new Date().getTime() / 1000;
const validFrom = typeof options.validFrom === "number" ? options.validFrom : parseInt(options.validFrom);
const expiresAt = typeof options.expiresAt === "number" ? options.expiresAt : parseInt(options.expiresAt);
if (!softValidation && validFrom > currentDate) {
return { valid: false, message: `FCT is not valid yet. FCT is valid from ${validFrom}` };
}
if (expiresAt < currentDate) {
return { valid: false, message: `FCT has expired. FCT expired at ${expiresAt}` };
}
// if (gasPriceLimit === "0") {
// return { valid: false, message: `FCT gas price limit cannot be 0` };
// }
return { valid: true, message: null };
}
getSigners() {
return this.FCT.calls.reduce((acc, call) => {
const from = call.get().from;
if (typeof from !== "string")
return acc;
// const doNotReturn = secureStorageAddresses.find(
// (address) => address.address.toLowerCase() === from.toLowerCase() && address.chainId === this.FCT.chainId,
// );
const doNotReturn = SecureStorageAddressesSet.has(from.toLowerCase());
if (!acc.includes(from) && !doNotReturn) {
acc.push(from);
}
return acc;
}, []);
}
getAllPaths() {
const FCT = this.FCT;
const g = new Graph({ directed: true });
const ends = manageFCTNodesInGraph({
calls: this.FCT.calls,
FCT,
g,
});
return getPathsFromGraph({
g,
ends,
});
}
async getAssetFlow() {
const allPaths = this.getAllPaths();
const allCalls = this.FCT.calls;
const assetFlow = await Promise.all(allPaths.map(async (path) => {
const calls = path.map((index) => allCalls[Number(index)]);
const assetFlow = [];
for (const call of calls) {
const plugin = call.plugin;
if (!plugin) {
return [];
}
const callAssetFlow = await plugin.getAssetFlow();
// Check if the address is already in the accumulator
for (const flow of callAssetFlow) {
const index = assetFlow.findIndex((accAsset) => accAsset.address === flow.address);
if (index === -1) {
assetFlow.push(flow);
}
else {
const data = assetFlow[index];
for (const token of flow.toReceive) {
const tokenIndex = data.toReceive.findIndex((accToken) => accToken.token === token.token);
if (tokenIndex !== -1) {
data.toReceive[tokenIndex].amount = (BigInt(data.toReceive[tokenIndex].amount) +
BigInt(InstanceOf.Variable(token.amount) || InstanceOf.PluginVariable(token.amount) ? 0 : token.amount)).toString();
}
else {
data.toReceive.push(token);
}
}
for (const token of flow.toSpend) {
const tokenIndex = data.toSpend.findIndex((accToken) => accToken.token === token.token);
if (tokenIndex !== -1) {
data.toSpend[tokenIndex].amount = (BigInt(data.toSpend[tokenIndex].amount) +
BigInt(InstanceOf.Variable(token.amount) || InstanceOf.PluginVariable(token.amount) ? 0 : token.amount)).toString();
}
else {
data.toSpend.push(token);
}
}
}
}
}
return {
path,
assetFlow,
};
}));
return assetFlow;
}
kiroPerPayerGas = ({ gas, gasPrice, penalty, ethPriceInKIRO, fees, }) => {
const baseFeeBPS = fees?.baseFeeBPS ? BigInt(fees.baseFeeBPS) : 1000n;
const bonusFeeBPS = fees?.bonusFeeBPS ? BigInt(fees.bonusFeeBPS) : 5000n;
const gasBigInt = BigInt(gas);
const limits = this.FCTData.typedData.message.limits;
// TODO: Support multi-versioning
const maxGasPrice = BigInt(limits.max_payable_gas_price);
const gasPriceBigInt = BigInt(gasPrice) > maxGasPrice ? maxGasPrice : BigInt(gasPrice);
const effectiveGasPrice = BigInt(getEffectiveGasPrice({
gasPrice: gasPriceBigInt,
maxGasPrice,
baseFeeBPS,
bonusFeeBPS,
}));
const base = gasBigInt * gasPriceBigInt;
const fee = gasBigInt * (effectiveGasPrice - gasPriceBigInt);
const ethCost = base + fee;
const kiroCost = (ethCost * BigInt(ethPriceInKIRO)) / 10n ** 18n;
return {
ethCost: ((ethCost * BigInt(penalty || 10_000)) / 10000n).toString(),
kiroCost: kiroCost.toString(),
};
};
getPaymentPerPayer = ({ signatures, gasPrice, maxGasPrice, ethPriceInKIRO, penalty, fees, ignoreCalldata = false, opStackGasFees, }) => {
const calls = this.FCT.calls;
const options = this.FCT.options;
signatures = signatures || [];
const fctID = JSON.stringify(options) + JSON.stringify(this.FCT.callsAsObjects);
let allPaths = this._cache.get(fctID + "allPaths");
let calldata = this._cache.get(fctID + "calldata");
if (!allPaths) {
allPaths = this.getAllPaths();
this._cache.set(fctID + "allPaths", allPaths);
}
if (ignoreCalldata) {
calldata = "";
}
else if (!calldata) {
calldata = this.getCalldataForActuator({
activator: ethers.constants.AddressZero,
investor: ethers.constants.AddressZero,
purgedFCT: ethers.constants.HashZero,
signatures,
});
this._cache.set(fctID + "calldata", calldata);
}
const payerMap = getPayerMap({
FCT: this.FCT,
fctID,
paths: allPaths,
calldata,
calls,
payableGasLimit: options.payableGasLimit === undefined ? undefined : BigInt(options.payableGasLimit),
maxGasPrice: maxGasPrice ? BigInt(maxGasPrice) : BigInt(options.maxGasPrice),
gasPrice: gasPrice ? BigInt(gasPrice) : BigInt("0"),
baseFeeBPS: fees?.baseFeeBPS ? BigInt(fees.baseFeeBPS) : 1000n,
bonusFeeBPS: fees?.bonusFeeBPS ? BigInt(fees.bonusFeeBPS) : 5000n,
penalty,
opStackGasFees,
});
const sendersSet = new Set();
for (const call of calls) {
const sender = call.call.from;
if (typeof sender === "string") {
sendersSet.add(sender);
}
}
const senders = [...sendersSet];
const result = preparePaymentPerPayerResult({
payerMap,
senders,
ethPriceInKIRO,
});
return result;
};
getPaymentPerSender = this.getPaymentPerPayer;
getMaxGas = () => {
const allPayers = this.getPaymentPerSender({ ethPriceInKIRO: "0" });
return allPayers.reduce((acc, payer) => {
const largestGas = payer.largestPayment.gas;
if (BigInt(largestGas) > BigInt(acc)) {
return largestGas;
}
return acc;
}, "0");
};
getMaxGasIgnoreCalldata = () => {
// If OP Stack, 0
// TODO: Think of a solution for this
if (this.FCT.chainId === "10" || this.FCT.chainId === "8453") {
return "0";
}
const allPayers = this.getPaymentPerSender({ ethPriceInKIRO: "0", ignoreCalldata: true });
return allPayers.reduce((acc, payer) => {
const largestGas = payer.largestPayment.gas;
if (BigInt(largestGas) > BigInt(acc)) {
return largestGas;
}
return acc;
}, "0");
};
getCallResults = async ({ rpcUrl, provider, txHash, }) => {
if (!provider && !rpcUrl) {
throw new Error("Either provider or rpcUrl is required");
}
if (!provider) {
provider = new ethers.providers.JsonRpcProvider(rpcUrl);
}
const txReceipt = await provider.getTransactionReceipt(txHash);
const Version = getVersionClass(this.FCT);
const batchMultiSigInterface = Version.Utils.getBatchMultiSigCallABI();
verifyMessageHash(txReceipt.logs, this.getMessageHash());
const mapLog = (log) => {
const parsedLog = batchMultiSigInterface.parseLog(log);
return {
id: parsedLog.args.id,
caller: parsedLog.args.caller,
callIndex: parsedLog.args.callIndex.toString(),
};
};
const successCalls = txReceipt.logs
.filter((log) => {
try {
return batchMultiSigInterface.parseLog(log).name === "FCTE_CallSucceed";
}
catch (e) {
return false;
}
})
.map(mapLog);
const failedCalls = txReceipt.logs
.filter((log) => {
try {
return batchMultiSigInterface.parseLog(log).name === "FCTE_CallFailed";
}
catch (e) {
return false;
}
})
.map(mapLog);
const callResultConstants = {
success: "SUCCESS",
failed: "FAILED",
skipped: "SKIPPED",
};
const manageResult = (index) => {
if (successCalls.find((successCall) => successCall.callIndex === index))
return callResultConstants.success;
if (failedCalls.find((failedCall) => failedCall.callIndex === index))
return callResultConstants.failed;
return callResultConstants.skipped;
};
return this.FCT.calls.map((_, index) => {
const indexString = (index + 1).toString();
return {
index: indexString,
result: manageResult(indexString),
};
});
};
getTransactionTrace = async ({ txHash, tenderlyRpcUrl, tries = 3, }) => {
const provider = new ethers.providers.JsonRpcProvider(tenderlyRpcUrl);
do {
try {
const data = await provider.send("tenderly_traceTransaction", [txHash]);
if (!data?.trace || !data?.logs) {
throw new Error("Tenderly trace is not working");
}
const executedCalls = executedCallsFromLogs(data.logs, this.getMessageHash());
const FCT_BatchMultiSigAddress = addresses[+this.FCT.chainId].FCT_BatchMultiSig;
const traceData = getTraceData({
FCT_BatchMultiSigAddress,
calls: this.FCT.calls,
callsFromTenderlyTrace: getCallsFromTrace(data.trace),
executedCalls,
computedVariables: this.FCT.computed,
});
return traceData;
}
catch (e) {
if (tries > 0) {
await new Promise((resolve) => setTimeout(resolve, 3000));
}
else {
throw e;
}
}
} while (tries-- > 0);
};
getSimpleTransactionTrace = async ({ txHash, rpcUrl }) => {
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const txReceipt = await provider.getTransactionReceipt(txHash);
verifyMessageHash(txReceipt.logs, this.getMessageHash());
const executedCalls = executedCallsFromRawLogs(txReceipt.logs, this.getMessageHash());
const fctCalls = this.FCT.calls;
const allFCTComputed = this.FCT.computed;
return executedCalls.reduce((acc, executedCall) => {
const fctCall = fctCalls[Number(executedCall.callIndex) - 1];
acc.calls = [
...acc.calls,
{
isSuccess: executedCall.isSuccess,
id: fctCall.nodeId,
},
];
manageValidationAndComputed(acc, fctCall, allFCTComputed);
return acc;
}, {
calls: [],
validations: [],
computed: [],
});
};
usesExternalVariables() {
// External Variables can be in 3 places:
// - calls
// - computed variables
// - validations
let result = false;
if (!result) {
result = this.FCT.calls.some((call) => {
return call.isExternalVariableUsed();
});
}
if (!result) {
result = this.FCT.variables.isExternalVariableUsed();
}
if (!result) {
result = this.FCT.validation.isExternalVariableUsed();
}
return result;
}
}
//# sourceMappingURL=index.js.map