@kiroboio/fct-core
Version:
Kirobo.io FCT Core library
355 lines • 12.1 kB
JavaScript
import { ethers } from "ethers";
import { BatchMultiSigCall } from "../batchMultiSigCall";
import { Interfaces } from "../helpers/Interfaces";
const ErrorFunctionSignature = "0x08c379a0"; // keccak256("Error(string)")
const BatchMultiSigErrorSignature = "0xb2685b70";
const BatchMultiSigErrorInterface = new ethers.utils.Interface([
{
inputs: [
{
internalType: "string",
name: "message",
type: "string",
},
{
components: [
{
internalType: "enum CallStatus",
name: "status",
type: "uint8",
},
{
internalType: "bytes",
name: "values",
type: "bytes",
},
],
internalType: "struct CallReturn[]",
name: "callsReturn",
type: "tuple[]",
},
{
components: [
{
internalType: "address",
name: "target",
type: "address",
},
{
internalType: "bytes32",
name: "ensHash",
type: "bytes32",
},
{
internalType: "uint256",
name: "value",
type: "uint256",
},
{
internalType: "uint256",
name: "sessionId",
type: "uint256",
},
{
internalType: "uint256",
name: "callId",
type: "uint256",
},
{
internalType: "bytes",
name: "data",
type: "bytes",
},
],
internalType: "struct CallData[]",
name: "callsData",
type: "tuple[]",
},
],
name: "ErrorThrowMulti",
type: "error",
},
]);
export const transactionValidatorV2 = async (txVal) => {
const { actuatorContractAddress, activator, rpcUrl, activateForFree, FCT, signatures, optionalExecutionValues } = txVal;
let { gasPrice } = txVal;
const { dryRun } = FCT.options;
const version = FCT.version;
gasPrice = manageGasPrice({ gasPrice, dryRun });
const exportedFct = prepareFCT({
FCT,
forceDryRun: true,
optionalExecutionValues,
signatures,
});
const callData = BatchMultiSigCall.utils.getCalldataForActuator({
signedFCT: exportedFct,
activator,
investor: ethers.constants.AddressZero,
purgedFCT: ethers.constants.HashZero,
version,
});
try {
const gas = await estimateGas({
rpcUrl,
actuatorContractAddress,
activateForFree,
callData,
activator,
gasPrice,
});
if (gas.lt(40000)) {
throw new Error("Unknown error - FCT execution finalized too quickly. Is there enough power for user?");
}
// Add 20% to gasUsed value, calculate with BigInt
const gasUsed = Math.round(gas.toNumber() + gas.toNumber() * 0.75);
return {
isValid: true,
callData,
txData: { gas: gasUsed, ...gasPrice, type: 2 },
prices: { gas: gasUsed, gasPrice: gasPrice.maxFeePerGas },
error: null,
};
}
catch (err) {
return await handleTxValidatorError({
err,
gasPrice,
failingCallData: callData,
errorIsValid: txVal.errorIsValid,
version,
FCT,
txVal,
});
}
};
function manageGasPrice({ dryRun, gasPrice }) {
if (dryRun) {
return { maxFeePerGas: "0", maxPriorityFeePerGas: "0" };
}
// if (BigInt(maxGasPrice) < BigInt(gasPrice.maxFeePerGas)) {
// return { maxFeePerGas: maxGasPrice.toString(), maxPriorityFeePerGas: "0" };
// }
return gasPrice;
}
async function estimateGas({ rpcUrl, actuatorContractAddress, activateForFree, callData, activator, gasPrice, }) {
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const data = Interfaces.FCT_Actuator.encodeFunctionData(activateForFree ? "activateForFree" : "activate", [
callData,
activator,
]);
return await provider.estimateGas({
to: actuatorContractAddress,
data,
...gasPrice,
from: activator,
});
}
async function handleTxValidatorError({ FCT, err, gasPrice, failingCallData, errorIsValid, version, txVal, }) {
const vAsNumber = parseInt(version, 16);
if (vAsNumber >= 0x20201) {
return await handleTxValidatorErrorV2({ err, gasPrice, errorIsValid, FCT, txVal, failingCallData });
}
return handleTxValidatorError_oldVersion({ err, gasPrice, errorIsValid, failingCallData });
}
async function handleTxValidatorErrorV2({ FCT, txVal, err, failingCallData, gasPrice, errorIsValid, }) {
const message = (err?.error?.error?.data || "");
let errorMessage;
let executionData = {
callsReturn: [],
callsData: [],
};
if (message.startsWith(BatchMultiSigErrorSignature)) {
const error = BatchMultiSigErrorInterface.decodeErrorResult("ErrorThrowMulti", message);
errorMessage = error.message;
executionData = {
callsReturn: error.callsReturn.map((callReturn) => ({
status: callReturn.status,
values: callReturn[1],
})),
callsData: error.callsData.map((callData) => ({
target: callData.target,
ensHash: callData.ensHash,
value: callData.value.toString(),
sessionId: callData.sessionId.toHexString(),
callId: callData.callId.toHexString(),
data: callData.data,
})),
};
}
else if (message.startsWith(ErrorFunctionSignature)) {
const innerMessage = message.slice(10);
const decoded = ethers.utils.defaultAbiCoder.decode(["string"], "0x" + innerMessage);
errorMessage = decoded[0];
executionData = {
callsReturn: [],
callsData: [],
};
}
else {
errorMessage = err?.error?.reason;
// If error doesnt have reason, try to take
// error.body, parse it and take message.
// Else just use error message worst case scenario.
//
// error decoding done in order of accuracy and precision.
if (!errorMessage) {
const parsedErrorMessage = err?.error?.error?.body ? JSON.parse(err.error.error.body)?.error?.message : null;
errorMessage = parsedErrorMessage ?? err.error.message;
}
executionData = {
callsReturn: [],
callsData: [],
};
}
if (errorMessage.includes("dry run success")) {
const exportedFct = prepareFCT({
FCT,
forceDryRun: false,
optionalExecutionValues: txVal.optionalExecutionValues,
signatures: txVal.signatures,
});
const callData = BatchMultiSigCall.utils.getCalldataForActuator({
signedFCT: exportedFct,
activator: txVal.activator,
investor: ethers.constants.AddressZero,
purgedFCT: ethers.constants.HashZero,
version: FCT.version,
});
const gas = await estimateGas({
rpcUrl: txVal.rpcUrl,
actuatorContractAddress: txVal.actuatorContractAddress,
activateForFree: txVal.activateForFree,
callData,
activator: txVal.activator,
gasPrice,
});
const gasUsed = Math.round(gas.toNumber() + gas.toNumber() * 0.25);
return {
isValid: true,
txData: { gas: gasUsed, ...gasPrice, type: 2 },
callData,
prices: {
gas: gasUsed,
gasPrice: gasPrice.maxFeePerGas,
},
error: null,
executionData,
};
}
if (errorIsValid) {
return {
isValid: true,
txData: { gas: 1_000_000, ...gasPrice, type: 2 },
callData: failingCallData,
prices: {
gas: 1_000_000, // 900k is the default gas limit
gasPrice: gasPrice.maxFeePerGas,
},
error: null,
executionData,
};
}
return {
isValid: false,
txData: { gas: 0, ...gasPrice, type: 2 },
callData: failingCallData,
prices: {
gas: 0,
gasPrice: gasPrice.maxFeePerGas,
},
error: errorMessage,
executionData,
};
}
function handleTxValidatorError_oldVersion({ err, gasPrice, errorIsValid, failingCallData, }) {
if (err.reason.includes("dry run success")) {
return {
isValid: true,
txData: { gas: 0, ...gasPrice, type: 2 },
callData: failingCallData,
prices: {
gas: 0,
gasPrice: gasPrice.maxFeePerGas,
},
error: null,
executionData: {
callsReturn: [],
callsData: [],
},
};
}
if (err.code === "SERVER_ERROR") {
return {
isValid: false,
txData: { gas: 0, ...gasPrice, type: 2 },
callData: failingCallData,
prices: {
gas: 0,
gasPrice: gasPrice.maxFeePerGas,
},
error: err.error.message || err.error,
executionData: {
callsReturn: [],
callsData: [],
},
};
}
if (errorIsValid) {
return {
isValid: true,
txData: { gas: 1_000_000, ...gasPrice, type: 2 },
callData: failingCallData,
prices: {
gas: 1_000_000, // 900k is the default gas limit
gasPrice: gasPrice.maxFeePerGas,
},
error: null,
executionData: {
callsReturn: [],
callsData: [],
},
};
}
let error = err.reason;
// If error.data starts with the Error(string) sig, decode it
if (!error && err.data.startsWith(ErrorFunctionSignature)) {
// Need to decode the error message from the data
// 1. Remove the Error(string) signature
// 2. Decode the message
const message = err.data.slice(10);
const decoded = ethers.utils.defaultAbiCoder.decode(["string"], "0x" + message);
error = decoded;
}
else if (!error) {
error = err.message;
}
return {
isValid: false,
txData: { gas: 0, ...gasPrice, type: 2 },
callData: failingCallData,
prices: {
gas: 0,
gasPrice: gasPrice.maxFeePerGas,
},
error: err.reason ? err.reason : err.message,
executionData: {
callsReturn: [],
callsData: [],
},
};
}
function prepareFCT({ FCT, forceDryRun = false, optionalExecutionValues, signatures, }) {
const exportedFCT = FCT.export({ forceDryRun });
exportedFCT.signatures = signatures;
if (optionalExecutionValues) {
if (optionalExecutionValues.externalSigners) {
exportedFCT.externalSigners = optionalExecutionValues.externalSigners;
}
if (optionalExecutionValues.variables) {
exportedFCT.variables = optionalExecutionValues.variables;
}
}
return exportedFCT;
}
//# sourceMappingURL=transactionValidatorV2.js.map