permissionless
Version:
A utility library for working with ERC-4337
377 lines (346 loc) • 12.4 kB
text/typescript
import type {
Account,
Assign,
Chain,
JsonRpcAccount,
OneOf,
Prettify,
Transport,
WalletClient
} from "viem"
import {
type Address,
type Client,
type Hex,
type LocalAccount,
decodeFunctionData,
encodeAbiParameters,
encodeFunctionData,
encodePacked,
getContractAddress,
hexToBigInt,
keccak256,
parseAbiParameters
} from "viem"
import {
type SmartAccount,
type SmartAccountImplementation,
entryPoint06Abi,
entryPoint06Address,
getUserOperationHash,
toSmartAccount
} from "viem/account-abstraction"
import { signMessage } from "viem/actions"
import { getAccountNonce } from "../../actions/public/getAccountNonce.js"
import { type EthereumProvider, toOwner } from "../../utils/toOwner.js"
import { BiconomyAbi, FactoryAbi } from "./abi/BiconomySmartAccountAbi.js"
const BICONOMY_PROXY_CREATION_CODE =
"0x6080346100aa57601f61012038819003918201601f19168301916001600160401b038311848410176100af578084926020946040528339810103126100aa57516001600160a01b0381168082036100aa5715610065573055604051605a90816100c68239f35b60405162461bcd60e51b815260206004820152601e60248201527f496e76616c696420696d706c656d656e746174696f6e206164647265737300006044820152606490fd5b600080fd5b634e487b7160e01b600052604160045260246000fdfe608060405230546000808092368280378136915af43d82803e156020573d90f35b3d90fdfea2646970667358221220a03b18dce0be0b4c9afe58a9eb85c35205e2cf087da098bbf1d23945bf89496064736f6c63430008110033"
/**
* The account creation ABI for Biconomy Smart Account (from the biconomy SmartAccountFactory)
*/
/**
* Default addresses for Biconomy Smart Account
*/
const BICONOMY_ADDRESSES: {
ECDSA_OWNERSHIP_REGISTRY_MODULE: Address
FACTORY_ADDRESS: Address
ACCOUNT_V2_0_LOGIC: Address
DEFAULT_FALLBACK_HANDLER_ADDRESS: Address
} = {
ECDSA_OWNERSHIP_REGISTRY_MODULE:
"0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e",
FACTORY_ADDRESS: "0x000000a56Aaca3e9a4C479ea6b6CD0DbcB6634F5",
ACCOUNT_V2_0_LOGIC: "0x0000002512019Dafb59528B82CB92D3c5D2423aC",
DEFAULT_FALLBACK_HANDLER_ADDRESS:
"0x0bBa6d96BD616BedC6BFaa341742FD43c60b83C1"
}
/**
* Get the account initialization code for Biconomy smart account with ECDSA as default authorization module
* @param owner
* @param index
* @param factoryAddress
* @param ecdsaValidatorAddress
*/
const getAccountInitCode = async ({
owner,
index,
ecdsaModuleAddress
}: {
owner: Address
index: bigint
ecdsaModuleAddress: Address
}): Promise<Hex> => {
if (!owner) throw new Error("Owner account not found")
// Build the module setup data
const ecdsaOwnershipInitData = encodeFunctionData({
abi: BiconomyAbi,
functionName: "initForSmartAccount",
args: [owner]
})
// Build the account init code
return encodeFunctionData({
abi: FactoryAbi,
functionName: "deployCounterFactualAccount",
args: [ecdsaModuleAddress, ecdsaOwnershipInitData, index]
})
}
export type ToBiconomySmartAccountParameters = Prettify<{
client: Client<
Transport,
Chain | undefined,
JsonRpcAccount | LocalAccount | undefined
>
owners: [
OneOf<
| EthereumProvider
| WalletClient<Transport, Chain | undefined, Account>
| LocalAccount
>
]
address?: Address | undefined
entryPoint: {
address: Address
version: "0.6"
}
nonceKey?: bigint
index?: bigint
factoryAddress?: Address
ecdsaModuleAddress?: Address
accountLogicAddress?: Address
fallbackHandlerAddress?: Address
}>
export type BiconomySmartAccountImplementation = Assign<
SmartAccountImplementation<typeof entryPoint06Abi, "0.6">,
{ sign: NonNullable<SmartAccountImplementation["sign"]> }
>
export type ToBiconomySmartAccountReturnType = Prettify<
SmartAccount<BiconomySmartAccountImplementation>
>
const getAccountAddress = async ({
factoryAddress,
accountLogicAddress,
fallbackHandlerAddress,
ecdsaModuleAddress,
owner,
index = BigInt(0)
}: {
factoryAddress: Address
accountLogicAddress: Address
fallbackHandlerAddress: Address
ecdsaModuleAddress: Address
owner: Address
index?: bigint
}): Promise<Address> => {
// Build the module setup data
const ecdsaOwnershipInitData = encodeFunctionData({
abi: BiconomyAbi,
functionName: "initForSmartAccount",
args: [owner]
})
// Build account init code
const initialisationData = encodeFunctionData({
abi: BiconomyAbi,
functionName: "init",
args: [
fallbackHandlerAddress,
ecdsaModuleAddress,
ecdsaOwnershipInitData
]
})
const deploymentCode = encodePacked(
["bytes", "uint256"],
[BICONOMY_PROXY_CREATION_CODE, hexToBigInt(accountLogicAddress)]
)
const salt = keccak256(
encodePacked(
["bytes32", "uint256"],
[keccak256(encodePacked(["bytes"], [initialisationData])), index]
)
)
return getContractAddress({
from: factoryAddress,
salt,
bytecode: deploymentCode,
opcode: "CREATE2"
})
}
/**
* @deprecated Biconomy Smart Account is deprecated. Please use toNexusSmartAccount instead.
* @see toNexusSmartAccount
*/
export async function toBiconomySmartAccount(
parameters: ToBiconomySmartAccountParameters
): Promise<ToBiconomySmartAccountReturnType> {
const {
owners,
client,
index = 0n,
address,
accountLogicAddress = BICONOMY_ADDRESSES.ACCOUNT_V2_0_LOGIC,
fallbackHandlerAddress = BICONOMY_ADDRESSES.DEFAULT_FALLBACK_HANDLER_ADDRESS,
ecdsaModuleAddress = BICONOMY_ADDRESSES.ECDSA_OWNERSHIP_REGISTRY_MODULE,
factoryAddress = BICONOMY_ADDRESSES.FACTORY_ADDRESS
} = parameters
const localOwner = await toOwner({ owner: owners[0] })
const entryPoint = {
address: parameters.entryPoint?.address ?? entryPoint06Address,
abi: entryPoint06Abi,
version: parameters.entryPoint?.version ?? "0.6"
}
let accountAddress: Address | undefined = address
const getFactoryArgs = async () => {
return {
factory: factoryAddress,
factoryData: await getAccountInitCode({
owner: localOwner.address,
index,
ecdsaModuleAddress
})
}
}
return toSmartAccount({
client,
entryPoint,
getFactoryArgs,
async getAddress() {
if (accountAddress) return accountAddress
// Get the sender address based on the init code
accountAddress = await getAccountAddress({
owner: localOwner.address,
ecdsaModuleAddress,
factoryAddress,
accountLogicAddress,
fallbackHandlerAddress,
index
})
return accountAddress
},
async getNonce(args) {
const address = await this.getAddress()
return getAccountNonce(client, {
address,
entryPointAddress: entryPoint.address,
key: parameters?.nonceKey ?? args?.key
})
},
encodeCalls: async (calls) => {
if (calls.length > 1) {
// Encode a batched call
return encodeFunctionData({
abi: BiconomyAbi,
functionName: "executeBatch_y6U",
args: [
calls.map((a) => a.to),
calls.map((a) => a.value ?? 0n),
calls.map((a) => a.data ?? "0x")
]
})
}
const call = calls.length === 0 ? undefined : calls[0]
if (!call) {
throw new Error("No calls to encode")
}
// Encode a simple call
return encodeFunctionData({
abi: BiconomyAbi,
functionName: "execute_ncC",
args: [call.to, call.value ?? 0n, call.data ?? "0x"]
})
},
decodeCalls: async (data) => {
const decoded = decodeFunctionData({
abi: BiconomyAbi,
data
})
if (decoded.functionName === "execute_ncC") {
return [
{
to: decoded.args[0],
value: decoded.args[1],
data: decoded.args[2]
}
]
}
if (decoded.functionName === "executeBatch_y6U") {
const calls: { to: Address; value: bigint; data: Hex }[] = []
for (let i = 0; i < decoded.args[0].length; i++) {
calls.push({
to: decoded.args[0][i],
value: decoded.args[1][i],
data: decoded.args[2][i]
})
}
return calls
}
throw new Error("Invalid function name")
},
// Get simple dummy signature for ECDSA module authorization
async getStubSignature() {
const dynamicPart = ecdsaModuleAddress.substring(2).padEnd(40, "0")
return `0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000${dynamicPart}000000000000000000000000000000000000000000000000000000000000004181d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b00000000000000000000000000000000000000000000000000000000000000`
},
async sign({ hash }) {
return this.signMessage({ message: hash })
},
async signMessage({ message }) {
let signature = await localOwner.signMessage({
message
})
const potentiallyIncorrectV = Number.parseInt(
signature.slice(-2),
16
)
if (![27, 28].includes(potentiallyIncorrectV)) {
const correctV = potentiallyIncorrectV + 27
signature = (signature.slice(0, -2) +
correctV.toString(16)) as Hex
}
return encodeAbiParameters(
[{ type: "bytes" }, { type: "address" }],
[signature, ecdsaModuleAddress]
)
},
async signTypedData(typedData) {
let signature = await localOwner.signTypedData(typedData)
const potentiallyIncorrectV = Number.parseInt(
signature.slice(-2),
16
)
if (![27, 28].includes(potentiallyIncorrectV)) {
const correctV = potentiallyIncorrectV + 27
signature = (signature.slice(0, -2) +
correctV.toString(16)) as Hex
}
return encodeAbiParameters(
[{ type: "bytes" }, { type: "address" }],
[signature, ecdsaModuleAddress]
)
},
// Sign a user operation
async signUserOperation(parameters) {
const { chainId = client.chain?.id, ...userOperation } = parameters
if (!chainId) throw new Error("Chain id not found")
const hash = getUserOperationHash({
userOperation: {
...userOperation,
sender: userOperation.sender ?? (await this.getAddress()),
signature: "0x"
},
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
chainId: chainId
})
const signature = await signMessage(client, {
account: localOwner,
message: { raw: hash }
})
// userOp signature is encoded module signature + module address
const signatureWithModuleAddress = encodeAbiParameters(
parseAbiParameters("bytes, address"),
[signature, ecdsaModuleAddress]
)
return signatureWithModuleAddress
}
})
}