@biconomy/abstractjs
Version:
SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.
532 lines • 22 kB
JavaScript
import { concatHex, createPublicClient, domainSeparator, encodeAbiParameters, encodeFunctionData, encodePacked, keccak256, parseAbi, parseAbiParameters, toBytes, toHex, validateTypedData, zeroAddress } from "viem";
import { entryPoint07Address, getUserOperationHash, toSmartAccount } from "viem/account-abstraction";
import { ENTRY_POINT_ADDRESS, MEEVersion } from "../constants/index.js";
// Constants
import { COMPOSABILITY_MODULE_ABI, EntrypointAbi } from "../constants/abi/index.js";
import { toComposableExecutor, toComposableFallback } from "../modules/index.js";
import { toEmptyHook } from "../modules/toEmptyHook.js";
import { toDefaultModule } from "../modules/validators/default/toDefaultModule.js";
import { toMeeK1Module } from "../modules/validators/meeK1/toMeeK1Module.js";
import { getFactoryData, getInitDataNoRegistry, getInitDataWithRegistry } from "./decorators/getFactoryData.js";
import { getNexusAddress } from "./decorators/getNexusAddress.js";
import { getDefaultNonceKey, getNonceWithKeyUtil } from "./decorators/getNonceWithKey.js";
import { toInitData } from "./utils/index.js";
import { EXECUTE_BATCH, EXECUTE_SINGLE, PARENT_TYPEHASH } from "./utils/Constants.js";
import { addressEquals, eip712WrapHash, getAccountDomainStructFields, getTypesForEIP712Domain, isNullOrUndefined, supportsCancun, typeToString } from "./utils/Utils.js";
import { isVersionOlder } from "./utils/getVersion.js";
import { toSigner } from "./utils/toSigner.js";
import { toWalletClient } from "./utils/toWalletClient.js";
const prepareValidators = async (signer, meeConfig, customValidators) => {
let validators = [];
if (customValidators && customValidators.length > 0) {
return customValidators;
}
if (isVersionOlder(meeConfig.version, MEEVersion.V2_0_0)) {
validators = [
toMeeK1Module({
signer: await toSigner({ signer }),
module: meeConfig.defaultValidatorAddress
})
];
}
else {
// No need to explicitly add validator for 1.2.X versions. default validator will be used which is
// mee k1 validator
validators = [];
}
return validators;
};
const prepareExecutors = (meeConfig, customExecutors) => {
let executors = [];
if (isVersionOlder(meeConfig.version, MEEVersion.V2_0_0)) {
if (!meeConfig.composableModuleAddress) {
throw new Error("Composable module address is missing");
}
// if using <=1.0.0, add the composable executor
const composableExecutor = toComposableExecutor(meeConfig.composableModuleAddress);
executors = [composableExecutor];
for (const executor of customExecutors || []) {
if (!addressEquals(executor.module, composableExecutor.module)) {
executors.push(executor);
}
}
}
else {
executors = customExecutors || [];
}
return executors;
};
const prepareFallbacks = (meeConfig, customFallbacks) => {
let fallbacks = [];
if (isVersionOlder(meeConfig.version, MEEVersion.V2_0_0)) {
if (!meeConfig.composableModuleAddress) {
throw new Error("Composable module address is missing");
}
// if nexus version <=1.0.0, add the composable fallback
const composableFallback = toComposableFallback(meeConfig.composableModuleAddress);
fallbacks = [composableFallback];
for (const fallback of customFallbacks || []) {
if (!addressEquals(fallback.module, composableFallback.module)) {
fallbacks.push(fallback);
}
}
}
else {
fallbacks = customFallbacks || [];
}
return fallbacks;
};
const prepareFactoryData = (meeConfig, initDataParams) => {
let factoryData = "0x";
let initData = "0x";
switch (meeConfig.version) {
case MEEVersion.V1_0_0:
case MEEVersion.V1_1_0: {
if (!meeConfig.moduleRegistry) {
throw new Error("Module registry not found in nexus config");
}
initData =
initDataParams.customInitData ||
getInitDataWithRegistry({
bootStrapAddress: meeConfig.bootStrapAddress,
validators: initDataParams.validators,
registryAddress: meeConfig.moduleRegistry.registryAddress,
attesters: meeConfig.moduleRegistry.attesters,
attesterThreshold: meeConfig.moduleRegistry.attesterThreshold,
meeVersion: meeConfig.version
});
factoryData = getFactoryData({
initData,
index: initDataParams.accountIndex
});
break;
}
default: {
// All the nexus version 1.2.x will be deployed with no registry
initData =
initDataParams.customInitData ||
getInitDataNoRegistry({
defaultValidator: initDataParams.defaultValidator,
prevalidationHooks: initDataParams.prevalidationHooks,
validators: initDataParams.validators,
executors: initDataParams.executors,
hook: initDataParams.hook,
fallbacks: initDataParams.fallbacks,
bootStrapAddress: meeConfig.bootStrapAddress
});
factoryData = getFactoryData({
initData,
index: initDataParams.accountIndex
});
break;
}
}
return { initData, factoryData };
};
/**
* @description Create a Nexus Smart Account.
*
* @param parameters - {@link ToNexusSmartAccountParameters}
* @returns Nexus Smart Account. {@link NexusAccount}
*
* @example
* import { toNexusAccount } from '@biconomy/abstractjs'
* import { createWalletClient, http } from 'viem'
* import { mainnet } from 'viem/chains'
*
* const account = await toNexusAccount({
* signer: '0x...',
* chainConfiguration: {
* chain: mainnet,
* transport: http(),
* version: getMEEVersion(DEFAULT_MEE_VERSION),
* }
* })
*/
export const toNexusAccount = async (parameters) => {
const { signer: _signer, chainConfiguration: { chain, version: meeConfig, transport: transportConfig }, index = 0n, validators: customValidators, executors: customExecutors, hook: customHook, fallbacks: customFallbacks, prevalidationHooks: customPrevalidationHooks, accountAddress: accountAddress_, initData: customInitData } = parameters;
// if the MEE version is not older than 2.0.0 ? SDK checks for cancun support and throw error if not
if (!isVersionOlder(meeConfig.version, MEEVersion.V2_0_0)) {
// check if the chain supports > 1.2.0
const hasCancun = await supportsCancun({
chain,
transport: transportConfig
});
if (!hasCancun) {
throw new Error(`MEE version (${meeConfig.version}) is not supported for the ${chain.name} chain. Please use a version earlier than 2.0.0 or a chain that supports Cancun.`);
}
}
const publicClient = createPublicClient({ chain, transport: transportConfig });
// All these version specific contract addresses were checked whether it was deployed or not.
const addressesToDeploymentSet = new Set([
meeConfig.bootStrapAddress,
meeConfig.defaultValidatorAddress,
meeConfig.validatorAddress,
meeConfig.factoryAddress,
meeConfig.implementationAddress
]);
if (meeConfig.moduleRegistry) {
addressesToDeploymentSet.add(meeConfig.moduleRegistry.registryAddress);
}
if (meeConfig.composableModuleAddress) {
addressesToDeploymentSet.add(meeConfig.composableModuleAddress);
}
// Filtering zero address because sometimes the default validator is zeroAddress which needs to be excluded
const addressesToDeploymentCheck = [...addressesToDeploymentSet].filter((address) => address !== zeroAddress);
await Promise.all(addressesToDeploymentCheck.map(async (address) => {
// Checks if the MEE contracts are deployed or not
// This ensures the MEE version suite is supported or not for the chain
const bytecode = await publicClient.getCode({
address
});
if (!bytecode || bytecode === "0x") {
throw new Error(`MEE version (${meeConfig.version}) is not supported for the ${chain.name} chain.`);
}
}));
const signer = await toSigner({ signer: _signer });
const walletClient = toWalletClient({
unresolvedSigner: _signer,
resolvedSigner: signer,
chain,
transport: transportConfig
});
// Prepare validator modules
const validators = await prepareValidators(signer, meeConfig, customValidators);
const defaultValidator = toDefaultModule({ signer });
// For 1.2.x accounts, no explicit validators will be added. So default validator will be used
let module = validators[0] || defaultValidator;
// Prepare executor modules
const executors = prepareExecutors(meeConfig, customExecutors);
// Prepare hook module
const hook = customHook || toEmptyHook();
// Prepare fallback modules
const fallbacks = prepareFallbacks(meeConfig, customFallbacks);
// Generate the initialization data for the account using the initNexus function
const prevalidationHooks = customPrevalidationHooks || [];
// prepare factory data
const { initData, factoryData } = prepareFactoryData(meeConfig, {
accountIndex: index,
defaultValidator: toInitData(defaultValidator),
prevalidationHooks,
validators: validators.map(toInitData),
executors: executors.map(toInitData),
hook: toInitData(hook),
fallbacks: fallbacks.map(toInitData),
customInitData
});
/**
* @description Gets the init code for the account
* @returns The init code as a hexadecimal string
*/
const getInitCode = () => concatHex([meeConfig.factoryAddress, factoryData]);
let _accountAddress = accountAddress_;
const accountId = (await publicClient.readContract({
address: meeConfig.implementationAddress,
abi: parseAbi(["function accountId() public view returns (string)"]),
functionName: "accountId",
args: []
}));
/**
* @description Gets the counterfactual address of the account
* @returns The counterfactual address
* @throws {Error} If unable to get the counterfactual address
*/
const getAddress = async () => {
if (!isNullOrUndefined(_accountAddress))
return _accountAddress;
const addressFromFactory = await getNexusAddress({
factoryAddress: meeConfig.factoryAddress,
index,
initData,
publicClient
});
if (!addressEquals(addressFromFactory, zeroAddress)) {
_accountAddress = addressFromFactory;
return addressFromFactory;
}
throw new Error("Failed to get account address");
};
/**
* @description Calculates the hash of a user operation
* @param userOp - The user operation
* @returns The hash of the user operation
*/
const getUserOpHash = (userOp) => getUserOperationHash({
chainId: chain.id,
entryPointAddress: entryPoint07Address,
entryPointVersion: "0.7",
userOperation: userOp
});
/**
* @description Encodes a batch of calls for execution
* @param calls - An array of calls to encode
* @param mode - The execution mode
* @returns The encoded calls
*/
const encodeExecuteBatch = async (calls, mode = EXECUTE_BATCH) => {
const executionAbiParams = {
type: "tuple[]",
components: [
{ name: "target", type: "address" },
{ name: "value", type: "uint256" },
{ name: "callData", type: "bytes" }
]
};
const executions = calls.map((tx) => ({
target: tx.to,
callData: tx.data ?? "0x",
value: BigInt(tx.value ?? 0n)
}));
const executionCalldataPrep = encodeAbiParameters([executionAbiParams], [executions]);
return encodeFunctionData({
abi: parseAbi([
"function execute(bytes32 mode, bytes calldata executionCalldata) external"
]),
functionName: "execute",
args: [mode, executionCalldataPrep]
});
};
/**
* @description Encodes a single call for execution
* @param call - The call to encode
* @param mode - The execution mode
* @returns The encoded call
*/
const encodeExecute = async (call, mode = EXECUTE_SINGLE) => {
const executionCalldata = encodePacked(["address", "uint256", "bytes"], [call.to, BigInt(call.value ?? 0n), (call.data ?? "0x")]);
return encodeFunctionData({
abi: parseAbi([
"function execute(bytes32 mode, bytes calldata executionCalldata) external"
]),
functionName: "execute",
args: [mode, executionCalldata]
});
};
/**
* @description Encodes a composable calls for execution
* @param call - The calls to encode
* @returns The encoded composable compatible call
*/
const encodeExecuteComposable = async (calls) => {
const composableCalls = calls.map((call) => {
return {
to: call.to,
value: call.value ?? 0n,
functionSig: call.functionSig,
inputParams: call.inputParams,
outputParams: call.outputParams
};
});
return encodeFunctionData({
abi: COMPOSABILITY_MODULE_ABI,
functionName: "executeComposable", // Function selector in Composability feature which executes the composable calls.
args: [composableCalls] // Multiple composable calls can be batched here.
});
};
/**
* @description Gets the nonce for the account along with modified key
* @param parameters - Optional parameters for getting the nonce
* @returns The nonce and the key
*/
const getNonceWithKey = async (accountAddress, parameters) => {
const defaultNonceKey = await getDefaultNonceKey(accountAddress, chain.id);
const { key = defaultNonceKey, validationMode = "0x00", moduleAddress = module.module } = parameters ?? {};
return getNonceWithKeyUtil(publicClient, accountAddress, {
key,
validationMode,
moduleAddress
});
};
/**
* @description Gets the nonce for the account
* @param parameters - Optional parameters for getting the nonce
* @returns The nonce
*/
const getNonce = async (parameters) => {
const accountAddress = await getAddress();
const { nonce } = await getNonceWithKey(accountAddress, parameters);
return nonce;
};
/**
* @description Signs typed data
* @param parameters - The typed data parameters
* @returns The signature
*/
async function signTypedData(parameters) {
const { message, primaryType, types: _types, domain } = parameters;
if (!domain)
throw new Error("Missing domain");
if (!message)
throw new Error("Missing message");
const types = {
EIP712Domain: getTypesForEIP712Domain({ domain }),
..._types
};
// @ts-ignore: Comes from nexus parent typehash
const messageStuff = message.stuff;
// @ts-ignore
validateTypedData({
domain,
message,
primaryType,
types
});
const appDomainSeparator = domainSeparator({ domain });
const accountDomainStructFields = await getAccountDomainStructFields(publicClient, await getAddress());
const parentStructHash = keccak256(encodePacked(["bytes", "bytes"], [
encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [
keccak256(toBytes(PARENT_TYPEHASH)),
messageStuff
]),
accountDomainStructFields
]));
const wrappedTypedHash = eip712WrapHash(parentStructHash, appDomainSeparator);
let signature = await module.signMessage({ raw: toBytes(wrappedTypedHash) });
const contentsType = toBytes(typeToString(types)[1]);
const signatureData = concatHex([
signature,
appDomainSeparator,
messageStuff,
toHex(contentsType),
toHex(contentsType.length, { size: 2 })
]);
signature = encodePacked(["address", "bytes"], [module.module, signatureData]);
return signature;
}
/**
* @description Changes the active module for the account
* @param module - The new module to set as active
* @returns void
*/
const setModule = (validationModule) => {
module = validationModule;
};
/**
* @description Get authorization data for the EOA to Nexus Account
* @param forMee - Whether to return the authorization data formatted for MEE. Defaults to false.
* @param delegatedContract - The contract address to delegate the authorization to. Defaults to the implementation address.
*
* @example
* const eip7702Auth = await nexusAccount.toDelegation() // Returns MeeAuthorization
*/
async function toDelegation(params) {
const { authorization: authorization_, multiChain, delegatedContract } = params || {};
const contractAddress = delegatedContract || meeConfig.implementationAddress;
const authorization = authorization_ ||
(await walletClient.signAuthorization({
contractAddress
}));
const eip7702Auth = {
chainId: `0x${(multiChain ? 0 : chain.id).toString(16)}`,
address: authorization.address,
nonce: `0x${authorization.nonce.toString(16)}`,
r: authorization.r,
s: authorization.s,
v: `0x${authorization.v.toString(16)}`,
yParity: `0x${authorization.yParity.toString(16)}`
};
return eip7702Auth;
}
async function isDelegated() {
const code = await publicClient.getCode({ address: signer.address });
return (!!code &&
code
?.toLowerCase()
.includes(meeConfig.implementationAddress.substring(2).toLowerCase()));
}
/**
* @description Get authorization data to unauthorize the account
* @returns Hex of the transaction hash
*
* @example
* const eip7702Auth = await nexusAccount.unDelegate()
*/
async function unDelegate(params) {
const { authorization } = params || {};
const deAuthorization = authorization ||
(await walletClient.signAuthorization({
address: zeroAddress,
executor: "self"
}));
return await walletClient.sendTransaction({
to: signer.address,
data: "0xdeadbeef",
type: "eip7702",
authorizationList: [deAuthorization]
});
}
// ================================================
// Return the Nexus Account
// ================================================
return toSmartAccount({
client: publicClient,
entryPoint: {
abi: EntrypointAbi,
address: ENTRY_POINT_ADDRESS,
version: "0.7"
},
getAddress,
encodeCalls: (calls) => {
return calls.length === 1
? encodeExecute(calls[0])
: encodeExecuteBatch(calls);
},
getFactoryArgs: async () => ({
factory: meeConfig.factoryAddress,
factoryData
}),
getStubSignature: async () => module.getStubSignature(),
/**
* @description Signs a message
* @param params - The parameters for signing
* @param params.message - The message to sign
* @returns The signature
*/
async signMessage({ message }) {
const tempSignature = await module.signMessage(message);
return encodePacked(["address", "bytes"], [module.module, tempSignature]);
},
signTypedData,
signUserOperation: async (parameters) => {
const { chainId = publicClient.chain.id, ...userOpWithoutSender } = parameters;
const address = await getAddress();
const userOperation = {
...userOpWithoutSender,
sender: address
};
const hash = getUserOperationHash({
chainId,
entryPointAddress: entryPoint07Address,
entryPointVersion: "0.7",
userOperation
});
return await module.signUserOpHash(hash);
},
getNonce,
extend: {
isDelegated,
toDelegation,
unDelegate,
entryPointAddress: entryPoint07Address,
getAddress,
accountId,
getInitCode,
getNonceWithKey,
encodeExecute,
encodeExecuteBatch,
encodeExecuteComposable,
getUserOpHash,
factoryData,
factoryAddress: meeConfig.factoryAddress,
registryAddress: meeConfig.moduleRegistry?.registryAddress || zeroAddress,
signer,
walletClient,
publicClient,
chain,
setModule,
getModule: () => module,
version: meeConfig
}
});
};
//# sourceMappingURL=toNexusAccount.js.map