3rd Party Smart Contracts
You are not limited to the accounts defined in @account-kit/smart-contracts
.
If you’d like to bring your own smart account, you have a couple options:
- Use a 3rd party library that exports an
aa-sdk
compatibleSmartContractAccount
interface. - Implement your own
SmartContractAccount
interface usingtoSmartContractAccount
.
Third Party SDKs
If you’ve built an SDK or Guide that leverages any of the methods above to use as a Smart Contract within Smart Wallets, we’re happy to include you in this list!
Please open a PR to add a link to your content in this section.
Using toSmartContractAccount
The SmartAccountClient
can be used with any smart account because it only relies on the SmartContractAccount
interface. This means you can use your own smart account implementation with Smart Wallets.
1 import { getEntryPoint, toSmartContractAccount } from "@aa-sdk/core"; 2 import { http, type SignableMessage, type Hash } from "viem"; 3 import { sepolia } from "viem/chains"; 4 5 const myAccount = await toSmartContractAccount({ 6 /// REQUIRED PARAMS /// 7 source: "MyAccount", 8 transport: http("RPC_URL"), 9 chain: sepolia, 10 // The EntryPointDef that your account is compatible with 11 entryPoint: getEntryPoint(sepolia), 12 // This should return a concatenation of your `factoryAddress` and the `callData` for your factory's create account method 13 getAccountInitCode: async (): Promise<Hash> => "0x{factoryAddress}{callData}", 14 // an invalid signature that doesn't cause your account to revert during validation 15 getDummySignature: async (): Promise<Hash> => "0x1234...", 16 // given a UO in the form of {target, data, value} should output the calldata for calling your contract's execution method 17 encodeExecute: async (uo): Promise<Hash> => "0x....", 18 signMessage: async ({ message }): Promise<Hash> => "0x...", 19 signTypedData: async (typedData): Promise<Hash> => "0x000", 20 21 /// OPTIONAL PARAMS /// 22 // if you already know your account's address, pass that in here to avoid generating a new counterfactual 23 accountAddress: "0x...", 24 // if your account supports batching, this should take an array of UOs and return the calldata for calling your contract's batchExecute method 25 encodeBatchExecute: async (uos): Promise<Hash> => "0x...", 26 // if your contract expects a different signing scheme than the default signMessage scheme, you can override that here 27 signUserOperationHash: async (hash): Promise<Hash> => "0x...", 28 // allows you to define the calldata for upgrading your account 29 encodeUpgradeToAndCall: async (params): Promise<Hash> => "0x...", 30 });
To use your account, you will need to pass it into a SmartAccountClient
.
1 import { createAlchemySmartAccountClient, alchemy } from "@account-kit/infra"; 2 import { sepolia } from "viem/chains"; 3 import { myAccount } from "./my-account"; 4 5 const client = createAlchemySmartAccountClient({ 6 // created above 7 account: myAccount, 8 chain: sepolia, 9 transport: alchemy({ 10 apiKey: "YOUR_API_KEY", 11 }), 12 });
LightSmartContractAccount
as an Example
We have built an extension of the eth-infinitism SimpleAccount
called LightAccount.sol. You can learn more about Light Account in the Light Account documentation.
We provide an implementation of SmartContractAccount
that works with LightAccount.sol
, which can be used as an example of how to implement your own Smart Contract Account:
LightSmartContractAccount
1 import { 2 createBundlerClient, 3 getEntryPoint, 4 type Address, 5 type EntryPointDef, 6 type SmartAccountSigner, 7 } from "@aa-sdk/core"; 8 import { 9 concatHex, 10 encodeFunctionData, 11 type Chain, 12 type Hex, 13 type Transport, 14 } from "viem"; 15 import { LightAccountAbi_v1 } from "../abis/LightAccountAbi_v1.js"; 16 import { LightAccountAbi_v2 } from "../abis/LightAccountAbi_v2.js"; 17 import { LightAccountFactoryAbi_v1 } from "../abis/LightAccountFactoryAbi_v1.js"; 18 import { LightAccountFactoryAbi_v2 } from "../abis/LightAccountFactoryAbi_v2.js"; 19 import type { 20 LightAccountEntryPointVersion, 21 LightAccountVersion, 22 } from "../types.js"; 23 import { 24 AccountVersionRegistry, 25 LightAccountUnsupported1271Factories, 26 defaultLightAccountVersion, 27 getDefaultLightAccountFactoryAddress, 28 } from "../utils.js"; 29 import { 30 createLightAccountBase, 31 type CreateLightAccountBaseParams, 32 type LightAccountBase, 33 } from "./base.js"; 34 import { predictLightAccountAddress } from "./predictAddress.js"; 35 36 export type LightAccount< 37 TSigner extends SmartAccountSigner = SmartAccountSigner, 38 TLightAccountVersion extends 39 LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">, 40 > = LightAccountBase<TSigner, "LightAccount", TLightAccountVersion> & { 41 encodeTransferOwnership: (newOwner: Address) => Hex; 42 getOwnerAddress: () => Promise<Address>; 43 }; 44 45 // [!region CreateLightAccountParams] 46 export type CreateLightAccountParams< 47 TTransport extends Transport = Transport, 48 TSigner extends SmartAccountSigner = SmartAccountSigner, 49 TLightAccountVersion extends 50 LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">, 51 > = Omit< 52 CreateLightAccountBaseParams< 53 "LightAccount", 54 TLightAccountVersion, 55 TTransport, 56 TSigner 57 >, 58 | "getAccountInitCode" 59 | "entryPoint" 60 | "version" 61 | "abi" 62 | "accountAddress" 63 | "type" 64 > & { 65 salt?: bigint; 66 initCode?: Hex; 67 accountAddress?: Address; 68 factoryAddress?: Address; 69 version?: TLightAccountVersion; 70 entryPoint?: EntryPointDef< 71 LightAccountEntryPointVersion<"LightAccount", TLightAccountVersion>, 72 Chain 73 >; 74 }; 75 // [!endregion CreateLightAccountParams] 76 77 export async function createLightAccount< 78 TTransport extends Transport = Transport, 79 TSigner extends SmartAccountSigner = SmartAccountSigner, 80 TLightAccountVersion extends LightAccountVersion<"LightAccount"> = "v2.0.0", 81 >( 82 config: CreateLightAccountParams<TTransport, TSigner, TLightAccountVersion>, 83 ): Promise<LightAccount<TSigner, TLightAccountVersion>>; 84 85 /** 86 * Creates a light account based on the provided parameters such as transport, chain, signer, init code, and more. Ensures that an account is configured and returned with various capabilities, such as transferring ownership and retrieving the owner's address. 87 * 88 * @example 89 * ```ts 90 * import { createLightAccount } from "@account-kit/smart-contracts"; 91 * import { LocalAccountSigner } from "@aa-sdk/core"; 92 * import { sepolia } from "viem/chains"; 93 * import { http, generatePrivateKey } from "viem" 94 * 95 * const account = await createLightAccount({ 96 * chain: sepolia, 97 * transport: http("RPC_URL"), 98 * signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()) 99 * }); 100 * ``` 101 * 102 * @param {CreateLightAccountParams} config The parameters for creating a light account 103 * @returns {Promise<LightAccount>} A promise that resolves to a `LightAccount` object containing the created account information and methods 104 */ 105 export async function createLightAccount({ 106 transport, 107 chain, 108 signer, 109 initCode, 110 version = defaultLightAccountVersion(), 111 entryPoint = getEntryPoint(chain, { 112 version: AccountVersionRegistry["LightAccount"][version] 113 .entryPointVersion as any, 114 }), 115 accountAddress, 116 factoryAddress = getDefaultLightAccountFactoryAddress(chain, version), 117 salt: salt_ = 0n, 118 }: CreateLightAccountParams): Promise<LightAccount> { 119 const client = createBundlerClient({ 120 transport, 121 chain, 122 }); 123 124 const accountAbi = 125 version === "v2.0.0" ? LightAccountAbi_v2 : LightAccountAbi_v1; 126 const factoryAbi = 127 version === "v2.0.0" 128 ? LightAccountFactoryAbi_v1 129 : LightAccountFactoryAbi_v2; 130 131 const signerAddress = await signer.getAddress(); 132 133 const salt = LightAccountUnsupported1271Factories.has( 134 factoryAddress.toLowerCase() as Address, 135 ) 136 ? 0n 137 : salt_; 138 139 const getAccountInitCode = async () => { 140 if (initCode) return initCode; 141 142 return concatHex([ 143 factoryAddress, 144 encodeFunctionData({ 145 abi: factoryAbi, 146 functionName: "createAccount", 147 args: [signerAddress, salt], 148 }), 149 ]); 150 }; 151 152 const address = 153 accountAddress ?? 154 predictLightAccountAddress({ 155 factoryAddress, 156 salt, 157 ownerAddress: signerAddress, 158 version, 159 }); 160 161 const account = await createLightAccountBase< 162 "LightAccount", 163 LightAccountVersion<"LightAccount">, 164 Transport, 165 SmartAccountSigner 166 >({ 167 transport, 168 chain, 169 signer, 170 abi: accountAbi, 171 type: "LightAccount", 172 version, 173 entryPoint, 174 accountAddress: address, 175 getAccountInitCode, 176 }); 177 178 return { 179 ...account, 180 181 encodeTransferOwnership: (newOwner: Address) => { 182 return encodeFunctionData({ 183 abi: accountAbi, 184 functionName: "transferOwnership", 185 args: [newOwner], 186 }); 187 }, 188 async getOwnerAddress(): Promise<Address> { 189 const callResult = await client.readContract({ 190 address, 191 abi: accountAbi, 192 functionName: "owner", 193 }); 194 195 if (callResult == null) { 196 throw new Error("could not get on-chain owner"); 197 } 198 199 return callResult; 200 }, 201 }; 202 }
The toSmartContractAccount
Method
For your reference, this is the definition of the toSmartContractAccount
interface as pulled from the source code:
SmartContractAccount
1 import { 2 getContract, 3 hexToBytes, 4 type Address, 5 type Chain, 6 type CustomSource, 7 type Hex, 8 type LocalAccount, 9 type PublicClient, 10 type SignableMessage, 11 type Transport, 12 type TypedData, 13 type TypedDataDefinition, 14 } from "viem"; 15 import { toAccount } from "viem/accounts"; 16 import { createBundlerClient } from "../client/bundlerClient.js"; 17 import type { 18 EntryPointDef, 19 EntryPointRegistryBase, 20 EntryPointVersion, 21 } from "../entrypoint/types.js"; 22 import { 23 BatchExecutionNotSupportedError, 24 FailedToGetStorageSlotError, 25 GetCounterFactualAddressError, 26 SignTransactionNotSupportedError, 27 UpgradesNotSupportedError, 28 } from "../errors/account.js"; 29 import { InvalidRpcUrlError } from "../errors/client.js"; 30 import { InvalidEntryPointError } from "../errors/entrypoint.js"; 31 import { Logger } from "../logger.js"; 32 import type { SmartAccountSigner } from "../signer/types.js"; 33 import { wrapSignatureWith6492 } from "../signer/utils.js"; 34 import type { NullAddress } from "../types.js"; 35 import type { IsUndefined, Never } from "../utils/types.js"; 36 37 export type AccountOp = { 38 target: Address; 39 value?: bigint; 40 data: Hex | "0x"; 41 }; 42 43 export enum DeploymentState { 44 UNDEFINED = "0x0", 45 NOT_DEPLOYED = "0x1", 46 DEPLOYED = "0x2", 47 } 48 49 export type SignatureRequest = 50 | { 51 type: "personal_sign"; 52 data: SignableMessage; 53 } 54 | { 55 type: "eth_signTypedData_v4"; 56 data: TypedDataDefinition; 57 }; 58 59 export type SigningMethods = { 60 prepareSign: (request: SignatureRequest) => Promise<SignatureRequest>; 61 formatSign: (signature: Hex) => Promise<Hex>; 62 }; 63 64 export type GetEntryPointFromAccount< 65 TAccount extends SmartContractAccount | undefined, 66 TAccountOverride extends SmartContractAccount = SmartContractAccount, 67 > = 68 GetAccountParameter<TAccount, TAccountOverride> extends SmartContractAccount< 69 string, 70 infer TEntryPointVersion 71 > 72 ? TEntryPointVersion 73 : EntryPointVersion; 74 75 export type GetAccountParameter< 76 TAccount extends SmartContractAccount | undefined = 77 | SmartContractAccount 78 | undefined, 79 TAccountOverride extends SmartContractAccount = SmartContractAccount, 80 > = 81 IsUndefined<TAccount> extends true 82 ? { account: TAccountOverride } 83 : { account?: TAccountOverride }; 84 85 export type UpgradeToAndCallParams = { 86 upgradeToAddress: Address; 87 upgradeToInitData: Hex; 88 }; 89 90 export type SmartContractAccountWithSigner< 91 Name extends string = string, 92 TSigner extends SmartAccountSigner = SmartAccountSigner, 93 TEntryPointVersion extends EntryPointVersion = EntryPointVersion, 94 > = SmartContractAccount<Name, TEntryPointVersion> & { 95 getSigner: () => TSigner; 96 }; 97 98 /** 99 * Determines if the given SmartContractAccount has a signer associated with it. 100 * 101 * @example 102 * ```ts 103 * import { toSmartContractAccount } from "@aa-sdk/core"; 104 * 105 * const account = await toSmartContractAccount(...); 106 * 107 * console.log(isSmartAccountWithSigner(account)); // false: the base account does not have a publicly accessible signer 108 * ``` 109 * 110 * @param {SmartContractAccount} account The account to check. 111 * @returns {boolean} true if the account has a signer, otherwise false. 112 */ 113 export const isSmartAccountWithSigner = ( 114 account: SmartContractAccount, 115 ): account is SmartContractAccountWithSigner => { 116 return "getSigner" in account; 117 }; 118 119 // [!region SmartContractAccount] 120 export type SmartContractAccount< 121 Name extends string = string, 122 TEntryPointVersion extends EntryPointVersion = EntryPointVersion, 123 > = LocalAccount<Name> & { 124 source: Name; 125 getDummySignature: () => Hex | Promise<Hex>; 126 encodeExecute: (tx: AccountOp) => Promise<Hex>; 127 encodeBatchExecute: (txs: AccountOp[]) => Promise<Hex>; 128 signUserOperationHash: (uoHash: Hex) => Promise<Hex>; 129 signMessageWith6492: (params: { message: SignableMessage }) => Promise<Hex>; 130 signTypedDataWith6492: < 131 const typedData extends TypedData | Record<string, unknown>, 132 primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, 133 >( 134 typedDataDefinition: TypedDataDefinition<typedData, primaryType>, 135 ) => Promise<Hex>; 136 encodeUpgradeToAndCall: (params: UpgradeToAndCallParams) => Promise<Hex>; 137 getAccountNonce(nonceKey?: bigint): Promise<bigint>; 138 getInitCode: () => Promise<Hex>; 139 isAccountDeployed: () => Promise<boolean>; 140 getFactoryAddress: () => Promise<Address>; 141 getFactoryData: () => Promise<Hex>; 142 getEntryPoint: () => EntryPointDef<TEntryPointVersion>; 143 getImplementationAddress: () => Promise<NullAddress | Address>; 144 } & SigningMethods; 145 // [!endregion SmartContractAccount] 146 147 export interface AccountEntryPointRegistry<Name extends string = string> 148 extends EntryPointRegistryBase< 149 SmartContractAccount<Name, EntryPointVersion> 150 > { 151 "0.6.0": SmartContractAccount<Name, "0.6.0">; 152 "0.7.0": SmartContractAccount<Name, "0.7.0">; 153 } 154 155 // [!region ToSmartContractAccountParams] 156 export type ToSmartContractAccountParams< 157 Name extends string = string, 158 TTransport extends Transport = Transport, 159 TChain extends Chain = Chain, 160 TEntryPointVersion extends EntryPointVersion = EntryPointVersion, 161 > = { 162 source: Name; 163 transport: TTransport; 164 chain: TChain; 165 entryPoint: EntryPointDef<TEntryPointVersion, TChain>; 166 accountAddress?: Address; 167 getAccountInitCode: () => Promise<Hex>; 168 getDummySignature: () => Hex | Promise<Hex>; 169 encodeExecute: (tx: AccountOp) => Promise<Hex>; 170 encodeBatchExecute?: (txs: AccountOp[]) => Promise<Hex>; 171 getNonce?: (nonceKey?: bigint) => Promise<bigint>; 172 // if not provided, will default to just using signMessage over the Hex 173 signUserOperationHash?: (uoHash: Hex) => Promise<Hex>; 174 encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise<Hex>; 175 getImplementationAddress?: () => Promise<NullAddress | Address>; 176 } & Omit<CustomSource, "signTransaction" | "address"> & 177 (SigningMethods | Never<SigningMethods>); 178 // [!endregion ToSmartContractAccountParams] 179 180 /** 181 * Parses the factory address and factory calldata from the provided account initialization code (initCode). 182 * 183 * @example 184 * ```ts 185 * import { parseFactoryAddressFromAccountInitCode } from "@aa-sdk/core"; 186 * 187 * const [address, calldata] = parseFactoryAddressFromAccountInitCode("0xAddressCalldata"); 188 * ``` 189 * 190 * @param {Hex} initCode The initialization code from which to parse the factory address and calldata 191 * @returns {[Address, Hex]} A tuple containing the parsed factory address and factory calldata 192 */ 193 export const parseFactoryAddressFromAccountInitCode = ( 194 initCode: Hex, 195 ): [Address, Hex] => { 196 const factoryAddress: Address = `0x${initCode.substring(2, 42)}`; 197 const factoryCalldata: Hex = `0x${initCode.substring(42)}`; 198 return [factoryAddress, factoryCalldata]; 199 }; 200 201 export type GetAccountAddressParams = { 202 client: PublicClient; 203 entryPoint: EntryPointDef; 204 accountAddress?: Address; 205 getAccountInitCode: () => Promise<Hex>; 206 }; 207 208 /** 209 * Retrieves the account address. Uses a provided `accountAddress` if available; otherwise, it computes the address using the entry point contract and the initial code. 210 * 211 * @example 212 * ```ts 213 * import { getEntryPoint, getAccountAddress } from "@aa-sdk/core"; 214 * 215 * const accountAddress = await getAccountAddress({ 216 * client, 217 * entryPoint: getEntryPoint(chain), 218 * getAccountInitCode: async () => "0x{factoryAddress}{factoryCallData}", 219 * }); 220 * ``` 221 * 222 * @param {GetAccountAddressParams} params The configuration object 223 * @param {PublicClient} params.client A public client instance to interact with the blockchain 224 * @param {EntryPointDef} params.entryPoint The entry point definition which includes the address and ABI 225 * @param {Address} params.accountAddress Optional existing account address 226 * @param {() => Promise<Hex>} params.getAccountInitCode A function that returns a Promise resolving to a Hex string representing the initial code of the account 227 * @returns {Promise<Address>} A promise that resolves to the account address 228 */ 229 export const getAccountAddress = async ({ 230 client, 231 entryPoint, 232 accountAddress, 233 getAccountInitCode, 234 }: GetAccountAddressParams) => { 235 if (accountAddress) return accountAddress; 236 237 const entryPointContract = getContract({ 238 address: entryPoint.address, 239 abi: entryPoint.abi, 240 client, 241 }); 242 243 const initCode = await getAccountInitCode(); 244 Logger.verbose("[BaseSmartContractAccount](getAddress) initCode: ", initCode); 245 246 try { 247 await entryPointContract.simulate.getSenderAddress([initCode]); 248 } catch (err: any) { 249 Logger.verbose( 250 "[BaseSmartContractAccount](getAddress) getSenderAddress err: ", 251 err, 252 ); 253 if (err.cause?.data?.errorName === "SenderAddressResult") { 254 Logger.verbose( 255 "[BaseSmartContractAccount](getAddress) entryPoint.getSenderAddress result:", 256 err.cause.data.args[0], 257 ); 258 259 return err.cause.data.args[0] as Address; 260 } 261 262 if (err.details === "Invalid URL") { 263 throw new InvalidRpcUrlError(); 264 } 265 } 266 267 throw new GetCounterFactualAddressError(); 268 }; 269 270 // [!region toSmartContractAccount] 271 export async function toSmartContractAccount< 272 Name extends string = string, 273 TTransport extends Transport = Transport, 274 TChain extends Chain = Chain, 275 TEntryPointVersion extends EntryPointVersion = EntryPointVersion, 276 >({ 277 transport, 278 chain, 279 entryPoint, 280 source, 281 accountAddress, 282 getAccountInitCode, 283 getNonce, 284 signMessage, 285 signTypedData, 286 encodeBatchExecute, 287 encodeExecute, 288 getDummySignature, 289 signUserOperationHash, 290 encodeUpgradeToAndCall, 291 }: ToSmartContractAccountParams< 292 Name, 293 TTransport, 294 TChain, 295 TEntryPointVersion 296 >): Promise<SmartContractAccount<Name, TEntryPointVersion>>; 297 // [!endregion toSmartContractAccount] 298 299 /** 300 * Converts an account to a smart contract account and sets up various account-related methods using the provided parameters like transport, chain, entry point, and other utilities. 301 * 302 * @example 303 * ```ts 304 * import { http, type SignableMessage } from "viem"; 305 * import { sepolia } from "viem/chains"; 306 * 307 * const myAccount = await toSmartContractAccount({ 308 * /// REQUIRED PARAMS /// 309 * source: "MyAccount", 310 * transport: http("RPC_URL"), 311 * chain: sepolia, 312 * // The EntryPointDef that your account is com"patible with 313 * entryPoint: getEntryPoint(sepolia, { version: "0.6.0" }), 314 * // This should return a concatenation of your `factoryAddress` and the `callData` for your factory's create account method 315 * getAccountInitCode: async () => "0x{factoryAddress}{callData}", 316 * // an invalid signature that doesn't cause your account to revert during validation 317 * getDummySignature: () => "0x1234...", 318 * // given a UO in the form of {target, data, value} should output the calldata for calling your contract's execution method 319 * encodeExecute: async (uo) => "0xcalldata", 320 * signMessage: async ({ message }: { message: SignableMessage }) => "0x...", 321 * signTypedData: async (typedData) => "0x000", 322 * 323 * /// OPTIONAL PARAMS /// 324 * // if you already know your account's address, pass that in here to avoid generating a new counterfactual 325 * accountAddress: "0xaddressoverride", 326 * // if your account supports batching, this should take an array of UOs and return the calldata for calling your contract's batchExecute method 327 * encodeBatchExecute: async (uos) => "0x...", 328 * // if your contract expects a different signing scheme than the default signMessage scheme, you can override that here 329 * signUserOperationHash: async (hash) => "0x...", 330 * // allows you to define the calldata for upgrading your account 331 * encodeUpgradeToAndCall: async (params) => "0x...", 332 * }); 333 * ``` 334 * 335 * @param {ToSmartContractAccountParams} params the parameters required for converting to a smart contract account 336 * @param {Transport} params.transport the transport mechanism used for communication 337 * @param {Chain} params.chain the blockchain chain used in the account 338 * @param {EntryPoint} params.entryPoint the entry point of the smart contract 339 * @param {string} params.source the source identifier for the account 340 * @param {Address} [params.accountAddress] the address of the account 341 * @param {() => Promise<Hex>} params.getAccountInitCode a function to get the initial state code of the account 342 * @param {(message: { message: SignableMessage }) => Promise<Hex>} params.signMessage a function to sign a message 343 * @param {(typedDataDefinition: TypedDataDefinition<typedData, primaryType>) => Promise<Hex>} params.signTypedData a function to sign typed data 344 * @param {(transactions: Transaction[]) => Hex} [params.encodeBatchExecute] a function to encode batch transactions 345 * @param {(tx: Transaction) => Hex} params.encodeExecute a function to encode a single transaction 346 * @param {() => Promise<Hex>} params.getDummySignature a function to get a dummy signature 347 * @param {(uoHash: Hex) => Promise<Hex>} [params.signUserOperationHash] a function to sign user operations 348 * @param {(implementationAddress: Address, implementationCallData: Hex) => Hex} [params.encodeUpgradeToAndCall] a function to encode upgrade call 349 * @returns {Promise<SmartContractAccount>} a promise that resolves to a SmartContractAccount object with methods and properties for interacting with the smart contract account 350 */ 351 export async function toSmartContractAccount( 352 params: ToSmartContractAccountParams, 353 ): Promise<SmartContractAccount> { 354 const { 355 transport, 356 chain, 357 entryPoint, 358 source, 359 accountAddress, 360 getAccountInitCode, 361 signMessage, 362 signTypedData, 363 encodeExecute, 364 encodeBatchExecute, 365 getNonce, 366 getDummySignature, 367 signUserOperationHash, 368 encodeUpgradeToAndCall, 369 getImplementationAddress, 370 prepareSign: prepareSign_, 371 formatSign: formatSign_, 372 } = params; 373 374 const client = createBundlerClient({ 375 // we set the retry count to 0 so that viem doesn't retry during 376 // getting the address. That call always reverts and without this 377 // viem will retry 3 times, making this call very slow 378 transport: (opts) => transport({ ...opts, chain, retryCount: 0 }), 379 chain, 380 }); 381 382 const entryPointContract = getContract({ 383 address: entryPoint.address, 384 abi: entryPoint.abi, 385 client, 386 }); 387 388 const accountAddress_ = await getAccountAddress({ 389 client, 390 entryPoint: entryPoint, 391 accountAddress, 392 getAccountInitCode, 393 }); 394 395 let deploymentState = DeploymentState.UNDEFINED; 396 397 const getInitCode = async () => { 398 if (deploymentState === DeploymentState.DEPLOYED) { 399 return "0x"; 400 } 401 const contractCode = await client.getCode({ 402 address: accountAddress_, 403 }); 404 405 if ((contractCode?.length ?? 0) > 2) { 406 deploymentState = DeploymentState.DEPLOYED; 407 return "0x"; 408 } else { 409 deploymentState = DeploymentState.NOT_DEPLOYED; 410 } 411 412 return getAccountInitCode(); 413 }; 414 415 const signUserOperationHash_ = 416 signUserOperationHash ?? 417 (async (uoHash: Hex) => { 418 return signMessage({ message: { raw: hexToBytes(uoHash) } }); 419 }); 420 421 const getFactoryAddress = async (): Promise<Address> => 422 parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[0]; 423 424 const getFactoryData = async (): Promise<Hex> => 425 parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[1]; 426 427 const encodeUpgradeToAndCall_ = 428 encodeUpgradeToAndCall ?? 429 (() => { 430 throw new UpgradesNotSupportedError(source); 431 }); 432 433 const isAccountDeployed = async () => { 434 const initCode = await getInitCode(); 435 return initCode === "0x"; 436 }; 437 438 const getNonce_ = 439 getNonce ?? 440 (async (nonceKey = 0n): Promise<bigint> => { 441 return entryPointContract.read.getNonce([ 442 accountAddress_, 443 nonceKey, 444 ]) as Promise<bigint>; 445 }); 446 447 const account = toAccount({ 448 address: accountAddress_, 449 signMessage, 450 signTypedData, 451 signTransaction: () => { 452 throw new SignTransactionNotSupportedError(); 453 }, 454 }); 455 456 const create6492Signature = async (isDeployed: boolean, signature: Hex) => { 457 if (isDeployed) { 458 return signature; 459 } 460 461 const [factoryAddress, factoryCalldata] = 462 parseFactoryAddressFromAccountInitCode(await getAccountInitCode()); 463 464 return wrapSignatureWith6492({ 465 factoryAddress, 466 factoryCalldata, 467 signature, 468 }); 469 }; 470 471 const signMessageWith6492 = async (message: { message: SignableMessage }) => { 472 const [isDeployed, signature] = await Promise.all([ 473 isAccountDeployed(), 474 account.signMessage(message), 475 ]); 476 477 return create6492Signature(isDeployed, signature); 478 }; 479 480 const signTypedDataWith6492 = async < 481 const typedData extends TypedData | Record<string, unknown>, 482 primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, 483 >( 484 typedDataDefinition: TypedDataDefinition<typedData, primaryType>, 485 ): Promise<Hex> => { 486 const [isDeployed, signature] = await Promise.all([ 487 isAccountDeployed(), 488 account.signTypedData(typedDataDefinition), 489 ]); 490 491 return create6492Signature(isDeployed, signature); 492 }; 493 494 const getImplementationAddress_ = 495 getImplementationAddress ?? 496 (async () => { 497 const storage = await client.getStorageAt({ 498 address: account.address, 499 // This is the default slot for the implementation address for Proxies 500 slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", 501 }); 502 503 if (storage == null) { 504 throw new FailedToGetStorageSlotError( 505 "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", 506 "Proxy Implementation Address", 507 ); 508 } 509 510 // The storage slot contains a full bytes32, but we want only the last 20 bytes. 511 // So, slice off the leading `0x` and the first 12 bytes (24 characters), leaving the last 20 bytes, then prefix with `0x`. 512 return `0x${storage.slice(26)}`; 513 }); 514 515 if (entryPoint.version !== "0.6.0" && entryPoint.version !== "0.7.0") { 516 throw new InvalidEntryPointError(chain, entryPoint.version); 517 } 518 519 if ((prepareSign_ && !formatSign_) || (!prepareSign_ && formatSign_)) { 520 throw new Error( 521 "Must implement both prepareSign and formatSign or neither", 522 ); 523 } 524 525 const prepareSign = 526 prepareSign_ ?? 527 (() => { 528 throw new Error("prepareSign not implemented"); 529 }); 530 531 const formatSign = 532 formatSign_ ?? 533 (() => { 534 throw new Error("formatSign not implemented"); 535 }); 536 537 return { 538 ...account, 539 source, 540 // TODO: I think this should probably be signUserOperation instead 541 // and allow for generating the UO hash based on the EP version 542 signUserOperationHash: signUserOperationHash_, 543 getFactoryAddress, 544 getFactoryData, 545 encodeBatchExecute: 546 encodeBatchExecute ?? 547 (() => { 548 throw new BatchExecutionNotSupportedError(source); 549 }), 550 encodeExecute, 551 getDummySignature, 552 getInitCode, 553 encodeUpgradeToAndCall: encodeUpgradeToAndCall_, 554 getEntryPoint: () => entryPoint, 555 isAccountDeployed, 556 getAccountNonce: getNonce_, 557 signMessageWith6492, 558 signTypedDataWith6492, 559 getImplementationAddress: getImplementationAddress_, 560 prepareSign, 561 formatSign, 562 }; 563 }