UNPKG

@biconomy/abstractjs

Version:

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

318 lines 14.6 kB
import { concatHex, encodeAbiParameters, encodeFunctionData, erc20Abi, isAddress } from "viem"; import { isNativeToken } from "../../../account/utils/index.js"; import { ComposabilityVersion } from "../../../constants/index.js"; import { functionNameToLabel } from "../../../modules/utils/Helpers.js"; import { InputParamFetcherType, InputParamType, prepareComposableInputCalldataParams, prepareInputParam } from "../../../modules/utils/composabilityCalls.js"; import { isRuntimeComposableValue } from "../../../modules/utils/composabilityCalls.js"; import { createConditionInputParam } from "../../../modules/utils/conditions.js"; import { getFunctionContextFromAbi } from "../../../modules/utils/runtimeAbiEncoding.js"; import { encodeAddress } from "../../../modules/utils/runtimeAbiEncoding.js"; export const buildComposableCall = async (parameters, composabilityParameters) => { const { to, gasLimit, value, functionName, args, abi, conditions } = parameters; const { efficientMode = true, // saving gas by default composabilityVersion } = composabilityParameters; if (!composabilityVersion) { throw new Error(`Composability version is required to build a composable call. This error may be caused by using a non-composable .build decorator with a composable call. Please use buildComposable instead.`); } if (!functionName || !args) { throw new Error("Invalid params for composable call"); } if (!abi) { throw new Error("Invalid ABI"); } if (args.length <= 0) { throw new Error("Composable call is not required for a instruction which has zero args"); } const functionContext = getFunctionContextFromAbi(functionName, abi); if (functionContext?.inputs?.length !== args?.length) { throw new Error(`Invalid arguments for the ${functionName} function`); } const versionAgnosticComposableInputParams = prepareComposableInputCalldataParams([...functionContext.inputs], args); // Append condition InputParams if conditions are specified const allInputParams = conditions?.length ? [ ...versionAgnosticComposableInputParams, ...conditions.map(createConditionInputParam) ] : versionAgnosticComposableInputParams; const composableCall = formatComposableCallWithVersion(composabilityVersion, efficientMode, allInputParams, functionContext.functionSig, to, value, gasLimit); return [composableCall]; }; /** * Formats the composable call version based on the composability version * @param composabilityVersion * @param efficientMode * @param versionAgnosticComposableInputParams * @param functionContext * @param to * @param value * @param gasLimit * @returns */ export const formatComposableCallWithVersion = (composabilityVersion, efficientMode, versionAgnosticComposableInputParams, functionSig, to, value, gasLimit) => { let composableCall; // Handle different composability versions if (composabilityVersion === ComposabilityVersion.V1_0_0) { if (isRuntimeComposableValue(to)) { throw new Error("Runtime injected target is not supported for Composability v1.0.0"); } if (!isAddress(to)) { throw new Error("Invalid target contract address"); } if (isRuntimeComposableValue(value)) { throw new Error("Runtime injected value is not supported for Composability v1.0.0"); } // format composable call for composability version 1.0.0 with to and value composableCall = { to: to, value: value ?? BigInt(0), functionSig, inputParams: formatCallDataInputParamsWithVersion(composabilityVersion, efficientMode, versionAgnosticComposableInputParams), outputParams: [], // In the current scope, output params are not handled. When more composability functions are added, this will change ...(gasLimit ? { gasLimit } : {}) }; } else { const callDataInputParams = formatCallDataInputParamsWithVersion(composabilityVersion, efficientMode, versionAgnosticComposableInputParams); const { targetInputParam, valueInputParam } = prepareTargetAndValueInputParams(to, value); const inputParams = [ ...callDataInputParams, targetInputParam, ...(valueInputParam ? [valueInputParam] : []) // do not add valueInputParam if it is undefined ]; // format composable call for composability version 1.1.0+ with target and value as input params composableCall = { functionSig, inputParams: inputParams, outputParams: [], // In the current scope, output params are not handled. When more composability functions are added, this will change ...(gasLimit ? { gasLimit } : {}) }; } return composableCall; }; /** * Formats the call data input params based on the composability version * @param composabilityVersion * @param efficientMode * @param versionAgnosticInputParams * @returns */ export const formatCallDataInputParamsWithVersion = (composabilityVersion, efficientMode, versionAgnosticInputParams) => { const compressedVersionAgnosticInputParams = efficientMode ? compressCalldataInputParams(versionAgnosticInputParams) : versionAgnosticInputParams; if (composabilityVersion === ComposabilityVersion.V1_0_0) { // backwards compatibility for composability version 1.0.0 // for composability version 1.0.0, we need to back convert // input params with fetcherType BALANCE to input params with fetcherType STATIC_CALL // since the BALANCE fetcher type is not supported in composability version 1.0.0 return compressedVersionAgnosticInputParams.map((param) => { if (param.fetcherType === InputParamFetcherType.BALANCE) { // param data for Balance is abi.encodePacked([tokenAddress, targetAddress]) // slice it accordingly to get the tokenAddress and targetAddress const tokenAddress = `0x${param.paramData.slice(2, 42)}`; const targetAddress = `0x${param.paramData.slice(42, 82)}`; if (isNativeToken(tokenAddress)) { throw new Error("Native token balance as a runtime value is not supported for Composability v1.0.0"); } const encodedParam = encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [ tokenAddress, encodeFunctionData({ abi: erc20Abi, functionName: "balanceOf", args: [targetAddress] }) ]); return prepareInputParam(InputParamFetcherType.STATIC_CALL, encodedParam, param.constraints); } // for other input params, return them as is return param; }); } // for composability version 1.1.0+, we need to add paramType: CALL_DATA to the input params // since the input param type field is required for composability version 1.1.0+ return compressedVersionAgnosticInputParams.map((param) => ({ ...param, paramType: InputParamType.CALL_DATA })); }; /** * Builds an instruction for composable transaction. This is a generic function which creates the composable instructions * to execute against composability stack * * @param baseParams - Base configuration for the instruction * @param baseParams.account - The account that will execute the composable transaction * @param baseParams.currentInstructions - Optional array of existing instructions to append to * @param parameters - Parameters for generate composable instruction * @param parameters.to - Address of the target contract address * @param parameters.functionName - Function signature of the composable transaction call * @param parameters.args - Function arguments of the composable transaction call * @param parameters.abi - ABI of the contract where the composable transaction call is being generated from * @param parameters.chainId - Chain where the composable transaction will be executed * @param composabilityParams.composabilityVersion - Composability version to use * @param composabilityParams.efficientMode - boolean whether to compress the calldata input params or not * @param composabilityParams.forceComposableEncoding - boolean whether to force use composability or not * @param [parameters.gasLimit] - Optional gas limit * @param [parameters.value] - Optional native token value * * @returns Promise resolving to array of instructions * * @example * ```typescript * const instructions = buildComposable( * { accountAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }, * { * to: targetContractAddress, * functionName: 'exactInputSingle', * args: [ * { * tokenIn: inToken.addressOn(baseSepolia.id), * tokenOut: outToken.addressOn(baseSepolia.id), * fee: 3000, * recipient: recipient, * deadline: BigInt(Math.floor(Date.now() / 1000) + 900), * amountIn: runtimeERC20BalanceOf({ targetAddress: recipient, tokenAddress: testnetMcTestUSDCP.addressOn(baseSepolia.id), constraints: [] }), * amountOutMinimum: BigInt(1), * sqrtPriceLimitX96: BigInt(0), * }, * ] * chainId: baseSepolia.id, * abi: UniswapSwapRouterAbi * }, * { * composabilityVersion: ComposabilityVersion.V1_0_0 * efficientMode: true * forceComposableEncoding: false * } * ) * ``` */ export const buildComposableUtil = async (baseParams, parameters, composabilityParams) => { const { currentInstructions = [] } = baseParams; const { metadata, lowerBoundTimestamp, upperBoundTimestamp, executionSimulationRetryDelay, simulationOverrides } = parameters; const calls = await buildComposableCall(parameters, composabilityParams); const defaultMetadata = [ { type: "CUSTOM", description: `${functionNameToLabel(parameters.functionName)} on-chain action`, chainId: parameters.chainId } ]; return [ ...currentInstructions, { calls: calls, chainId: parameters.chainId, isComposable: true, metadata: metadata || defaultMetadata, lowerBoundTimestamp, upperBoundTimestamp, executionSimulationRetryDelay, simulationOverrides } ]; }; export default buildComposableUtil; /** * Compresses the input params by merging the input params with InputParamFetcherType.RAW_BYTES * and no constraints together * It does this by creating a new InputParam with InputParamFetcherType.RAW_BYTES and no constraints * and paramData as the concat of paramData's * It allows for less input params in the composable call => less iterations in the composable smart contract * => less gas used */ const compressCalldataInputParams = (inputParams) => { const compressedParams = []; let currentParam = { fetcherType: InputParamFetcherType.RAW_BYTES, constraints: [], paramData: "" }; // compress only calldata input params for (const param of inputParams) { if (param.paramType === InputParamType.TARGET || param.paramType === InputParamType.VALUE) { throw new Error("Target or value input params should not be compressed"); } // Static call, balance or constraint based params are left as is if (param.fetcherType === InputParamFetcherType.STATIC_CALL || param.fetcherType === InputParamFetcherType.BALANCE || param.constraints.length > 0) { // If there is a current param, push it to the compressed params // and reset the current param if (currentParam.paramData.length > 0) { compressedParams.push(currentParam); currentParam = { fetcherType: InputParamFetcherType.RAW_BYTES, constraints: [], paramData: "" }; } compressedParams.push(param); continue; } // If the current param is a raw bytes param with no constraints, merge it with the current param currentParam.paramData = concatHex([ currentParam.paramData, param.paramData ]); } // If there is a non-empty current param, push it to the compressed params if (currentParam.paramData.length > 0) { compressedParams.push(currentParam); } return compressedParams; }; const prepareTargetAndValueInputParams = (to, value) => { // Prepare target and value input params // if to is of type Address, then we need to prepare the target input param as raw_bytes // else if to is of type RuntimeValue, then we need to prepare the target input param let targetInputParam; if (isAddress(to)) { targetInputParam = { paramType: InputParamType.TARGET, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: encodeAddress(to).data[0], constraints: [] }; } else { targetInputParam = { ...to.inputParams[0], paramType: InputParamType.TARGET }; } let valueInputParam; if (!value) { // value not provided, default to 0 valueInputParam = undefined; // undefined valueInputParam would not be added to the composable call // and then the smart contract will use the default value of 0 // thus saving gas on processing one input param } else if (value.isRuntime && value.inputParams.length > 0) { // value is a runtime value, use the first input param valueInputParam = { ...value.inputParams[0], paramType: InputParamType.VALUE }; } else { // value is a static value, use it as raw_bytes if (value !== 0n) { valueInputParam = { paramType: InputParamType.VALUE, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: value .toString(16) .padStart(64, "0"), constraints: [] }; } } return { targetInputParam, valueInputParam }; }; //# sourceMappingURL=buildComposable.js.map