UNPKG

@biconomy/abstractjs

Version:

SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.

242 lines 11.2 kB
import { encodeAbiParameters, encodeFunctionData, erc20Abi, parseAbi } from "viem"; import { ENTRY_POINT_ADDRESS } from "../../constants/index.js"; import { encodeRuntimeFunctionData } from "./runtimeAbiEncoding.js"; export const InputParamFetcherType = { RAW_BYTES: 0, STATIC_CALL: 1 }; export const OutputParamFetcherType = { EXEC_RESULT: 0, STATIC_CALL: 1 }; export const ConstraintType = { EQ: 0, GTE: 1, LTE: 2, IN: 3 }; // Detects whether the value is runtime injected value or not export const isRuntimeComposableValue = (value) => { if (value && typeof value === "object" && !Array.isArray(value) && value.isRuntime) { return true; } return false; }; export const prepareInputParam = (fetcherType, paramData, constraints = []) => { return { fetcherType, paramData, constraints }; }; export const prepareOutputParam = (fetcherType, paramData) => { return { fetcherType, paramData }; }; export const prepareConstraint = (constraintType, referenceData) => { return { constraintType, referenceData }; }; // type any is being implicitly used. The appropriate value validation happens in the runtime function export const greaterThanOrEqualTo = (value) => { return { type: ConstraintType.GTE, value }; }; // type any is being implicitly used. The appropriate value validation happens in the runtime function export const lessThanOrEqualTo = (value) => { return { type: ConstraintType.LTE, value }; }; // type any is being implicitly used. The appropriate value validation happens in the runtime function export const equalTo = (value) => { return { type: ConstraintType.EQ, value }; }; export const runtimeNonceOf = ({ smartAccountAddress, nonceKey, constraints = [] }) => { const defaultFunctionSig = "getNonce"; const entryPointNonceAbi = parseAbi([ "function getNonce(address sender, uint192 key) public view returns (uint256)" ]); const encodedParam = encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [ ENTRY_POINT_ADDRESS, encodeFunctionData({ abi: entryPointNonceAbi, functionName: defaultFunctionSig, args: [smartAccountAddress, nonceKey] }) ]); const constraintsToAdd = []; if (constraints.length > 0) { for (const constraint of constraints) { // Contraint type IN is ignored for the runtimeBalanceOf // This is mostly a number/unit/int, so it makes sense to only have EQ, GTE, LTE if (!Object.values(ConstraintType).slice(0, 3).includes(constraint.type)) { throw new Error("Invalid contraint type"); } // Handle value validation in a appropriate to runtime function if (typeof constraint.value !== "bigint" || constraint.value < BigInt(0)) { throw new Error("Invalid contraint value"); } const valueHex = `0x${constraint.value.toString(16).padStart(64, "0")}`; const encodedConstraintValue = encodeAbiParameters([{ type: "bytes32" }], [valueHex]); constraintsToAdd.push(prepareConstraint(constraint.type, encodedConstraintValue)); } } return { isRuntime: true, inputParams: [ prepareInputParam(InputParamFetcherType.STATIC_CALL, encodedParam, constraintsToAdd) ], outputParams: [] }; }; /** * Returns the runtime value for the ERC20 allowance of the owner for the spender * @param owner - The owner of the tokens * @param spender - The spender of the tokens * @param tokenAddress - The address of the ERC20 token * @returns The runtime value for the ERC20 allowance of the owner for the spender */ export const runtimeERC20AllowanceOf = ({ owner, spender, tokenAddress, constraints = [] }) => { const encodedParam = encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [ tokenAddress, encodeFunctionData({ abi: erc20Abi, functionName: "allowance", args: [owner, spender] }) ]); const constraintsToAdd = []; if (constraints.length > 0) { for (const constraint of constraints) { // Constraint type IN is ignored for the runtimeBalanceOf // This is mostly a number/unit/int, so it makes sense to only have EQ, GTE, LTE if (!Object.values(ConstraintType).slice(0, 3).includes(constraint.type)) { throw new Error("Invalid constraint type"); } // Handle value validation in a appropriate to runtime function if (typeof constraint.value !== "bigint" || constraint.value < BigInt(0)) { throw new Error("Invalid constraint value"); } const valueHex = `0x${constraint.value.toString(16).padStart(64, "0")}`; const encodedConstraintValue = encodeAbiParameters([{ type: "bytes32" }], [valueHex]); constraintsToAdd.push(prepareConstraint(constraint.type, encodedConstraintValue)); } } return { isRuntime: true, inputParams: [ prepareInputParam(InputParamFetcherType.STATIC_CALL, encodedParam, constraintsToAdd) ], outputParams: [] }; }; export const runtimeERC20BalanceOf = ({ targetAddress, tokenAddress, constraints = [] }) => { const defaultFunctionSig = "balanceOf"; const encodedParam = encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [ tokenAddress, encodeFunctionData({ abi: erc20Abi, functionName: defaultFunctionSig, args: [targetAddress] }) ]); const constraintsToAdd = []; if (constraints.length > 0) { for (const constraint of constraints) { // Constraint type IN is ignored for the runtimeBalanceOf // This is mostly a number/unit/int, so it makes sense to only have EQ, GTE, LTE if (!Object.values(ConstraintType).slice(0, 3).includes(constraint.type)) { throw new Error("Invalid constraint type"); } // Handle value validation in a appropriate to runtime function if (typeof constraint.value !== "bigint" || constraint.value < BigInt(0)) { throw new Error("Invalid constraint value"); } const valueHex = `0x${constraint.value.toString(16).padStart(64, "0")}`; const encodedConstraintValue = encodeAbiParameters([{ type: "bytes32" }], [valueHex]); constraintsToAdd.push(prepareConstraint(constraint.type, encodedConstraintValue)); } } return { isRuntime: true, inputParams: [ prepareInputParam(InputParamFetcherType.STATIC_CALL, encodedParam, constraintsToAdd) ], outputParams: [] }; }; /// @dev This is a helper function for composable pseudo-dynamic `bytes` values. /// which are in fact several static values abi.encoded together /// and we want one of those static values to be runtime value /// so what we do here is we just treat runtimeAbiEncode as pseudo-function composable call /// and just mimic the process of encoding the params for it. /// it prepares the independent encoding with internal offsets for dynamic params, so /// every `runtimeAbiEncode` can has nested `runtimeAbiEncode`-s inside it export const runtimeEncodeAbiParameters = ( // mimics the interface of the og encodeAbiParameters // but is able to work with runtime values inputs, args) => { // prepare functionContext and args out of what this helper is expecting const inputParams = prepareComposableParams(inputs, args); // so in the upper level function call encoding, there will be a runtime dynamic `bytes` argument // wrapped into a RuntimeValue object with several InputParam's. // Some of those params will be runtime values (fetcherType: STATIC_CALL) // and some of them will be raw bytes (fetcherType: RAW_BYTES) // So we should account for that in the `encodeParams` method return { isRuntime: true, inputParams: inputParams, outputParams: [] }; }; export const isComposableCallRequired = (functionContext, args) => { if (!functionContext.inputs || functionContext.inputs.length <= 0) return false; const isComposableCall = functionContext.inputs.some((input, inputIndex) => { // Only struct and arrays has child elements and require iterating them internally. // String and bytes are also dynamic but they are mostly treated as one single value for detection if (input.type === "tuple") { // Struct arguments are handled here // Composable call detection const isComposableCallDetected = Object.values(args[inputIndex]).some((internalArg) => isRuntimeComposableValue(internalArg)); return isComposableCallDetected; } if (input.type.match(/^(.*)\[(\d+)?\]$/)) { // matches against both static and dynamic arrays. // Array arguments are handled here // Composable call detection const isComposableCallDetected = args[inputIndex].some((internalArg) => isRuntimeComposableValue(internalArg)); return isComposableCallDetected; } // Below mentioned common values are handled here. // intX, uintX, bytesX, bytes, string, bool, address are direct values and doesn't need iteration on child elements. // Composable call detection return isRuntimeComposableValue(args[inputIndex]); }); return isComposableCall; }; export const prepareComposableParams = (inputs, args) => { const composableParams = encodeRuntimeFunctionData(inputs, args).map((calldata) => { if (isRuntimeComposableValue(calldata)) { // Just handling input params here. In future, we may need to add support for output params as well return calldata?.inputParams; } // These are non runtime values which are encoded by the encodeRuntimeFunctionData helper. // These params are injected are individual raw bytes which will be combined on the composable contract return [ prepareInputParam(InputParamFetcherType.RAW_BYTES, calldata) ]; }); // Head Params,Head Params,Head Params + (len + Tail Params),(len + Tail Params),(len + Tail Params) // Static type doesn't have tail // Dynamic types have tail params where the head only have offset which points the dynamic param in tail return composableParams.flat(); }; export const prepareRawComposableParams = (calldata) => { const composableParams = [ prepareInputParam(InputParamFetcherType.RAW_BYTES, calldata) ]; // Head Params,Head Params,Head Params + (len + Tail Params),(len + Tail Params),(len + Tail Params) // Static type doesn't have tail // Dynamic types have tail params where the head only have offset which points the dynamic param in tail return composableParams.flat(); }; //# sourceMappingURL=composabilityCalls.js.map