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:

  1. Use a 3rd party library that exports an aa-sdk compatible SmartContractAccount interface.
  2. Implement your own SmartContractAccount interface using toSmartContractAccount.

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.

my-account.ts
1import { getEntryPoint, toSmartContractAccount } from "@aa-sdk/core";
2import { http, type SignableMessage, type Hash } from "viem";
3import { sepolia } from "viem/chains";
4
5const 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.

1import { createAlchemySmartAccountClient, alchemy } from "@account-kit/infra";
2import { sepolia } from "viem/chains";
3import { myAccount } from "./my-account";
4
5const 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:

1import {
2 createBundlerClient,
3 getEntryPoint,
4 type Address,
5 type EntryPointDef,
6 type SmartAccountSigner,
7} from "@aa-sdk/core";
8import {
9 concatHex,
10 encodeFunctionData,
11 type Chain,
12 type Hex,
13 type Transport,
14} from "viem";
15import { LightAccountAbi_v1 } from "../abis/LightAccountAbi_v1.js";
16import { LightAccountAbi_v2 } from "../abis/LightAccountAbi_v2.js";
17import { LightAccountFactoryAbi_v1 } from "../abis/LightAccountFactoryAbi_v1.js";
18import { LightAccountFactoryAbi_v2 } from "../abis/LightAccountFactoryAbi_v2.js";
19import type {
20 LightAccountEntryPointVersion,
21 LightAccountVersion,
22} from "../types.js";
23import {
24 AccountVersionRegistry,
25 LightAccountUnsupported1271Factories,
26 defaultLightAccountVersion,
27 getDefaultLightAccountFactoryAddress,
28} from "../utils.js";
29import {
30 createLightAccountBase,
31 type CreateLightAccountBaseParams,
32 type LightAccountBase,
33} from "./base.js";
34import { predictLightAccountAddress } from "./predictAddress.js";
35
36export 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
45export type CreateLightAccountParams<
46 TTransport extends Transport = Transport,
47 TSigner extends SmartAccountSigner = SmartAccountSigner,
48 TLightAccountVersion extends
49 LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">,
50> = Omit<
51 CreateLightAccountBaseParams<
52 "LightAccount",
53 TLightAccountVersion,
54 TTransport,
55 TSigner
56 >,
57 | "getAccountInitCode"
58 | "entryPoint"
59 | "version"
60 | "abi"
61 | "accountAddress"
62 | "type"
63> & {
64 salt?: bigint;
65 initCode?: Hex;
66 accountAddress?: Address;
67 factoryAddress?: Address;
68 version?: TLightAccountVersion;
69 entryPoint?: EntryPointDef<
70 LightAccountEntryPointVersion<"LightAccount", TLightAccountVersion>,
71 Chain
72 >;
73};
74
75export async function createLightAccount<
76 TTransport extends Transport = Transport,
77 TSigner extends SmartAccountSigner = SmartAccountSigner,
78 TLightAccountVersion extends LightAccountVersion<"LightAccount"> = "v2.0.0",
79>(
80 config: CreateLightAccountParams<TTransport, TSigner, TLightAccountVersion>,
81): Promise<LightAccount<TSigner, TLightAccountVersion>>;
82
83/**
84 * 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.
85 *
86 * @example
87 * ```ts
88 * import { createLightAccount } from "@account-kit/smart-contracts";
89 * import { LocalAccountSigner } from "@aa-sdk/core";
90 * import { sepolia } from "viem/chains";
91 * import { http, generatePrivateKey } from "viem"
92 *
93 * const account = await createLightAccount({
94 * chain: sepolia,
95 * transport: http("RPC_URL"),
96 * signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey())
97 * });
98 * ```
99 *
100 * @param {CreateLightAccountParams} config The parameters for creating a light account
101 * @returns {Promise<LightAccount>} A promise that resolves to a `LightAccount` object containing the created account information and methods
102 */
103export async function createLightAccount({
104 transport,
105 chain,
106 signer,
107 initCode,
108 version = defaultLightAccountVersion(),
109 entryPoint = getEntryPoint(chain, {
110 version: AccountVersionRegistry["LightAccount"][version]
111 .entryPointVersion as any,
112 }),
113 accountAddress,
114 factoryAddress = getDefaultLightAccountFactoryAddress(chain, version),
115 salt: salt_ = 0n,
116}: CreateLightAccountParams): Promise<LightAccount> {
117 const client = createBundlerClient({
118 transport,
119 chain,
120 });
121
122 const accountAbi =
123 version === "v2.0.0" ? LightAccountAbi_v2 : LightAccountAbi_v1;
124 const factoryAbi =
125 version === "v2.0.0"
126 ? LightAccountFactoryAbi_v1
127 : LightAccountFactoryAbi_v2;
128
129 const signerAddress = await signer.getAddress();
130
131 const salt = LightAccountUnsupported1271Factories.has(
132 factoryAddress.toLowerCase() as Address,
133 )
134 ? 0n
135 : salt_;
136
137 const getAccountInitCode = async () => {
138 if (initCode) return initCode;
139
140 return concatHex([
141 factoryAddress,
142 encodeFunctionData({
143 abi: factoryAbi,
144 functionName: "createAccount",
145 args: [signerAddress, salt],
146 }),
147 ]);
148 };
149
150 const address =
151 accountAddress ??
152 predictLightAccountAddress({
153 factoryAddress,
154 salt,
155 ownerAddress: signerAddress,
156 version,
157 });
158
159 const account = await createLightAccountBase<
160 "LightAccount",
161 LightAccountVersion<"LightAccount">,
162 Transport,
163 SmartAccountSigner
164 >({
165 transport,
166 chain,
167 signer,
168 abi: accountAbi,
169 type: "LightAccount",
170 version,
171 entryPoint,
172 accountAddress: address,
173 getAccountInitCode,
174 });
175
176 return {
177 ...account,
178
179 encodeTransferOwnership: (newOwner: Address) => {
180 return encodeFunctionData({
181 abi: accountAbi,
182 functionName: "transferOwnership",
183 args: [newOwner],
184 });
185 },
186 async getOwnerAddress(): Promise<Address> {
187 const callResult = await client.readContract({
188 address,
189 abi: accountAbi,
190 functionName: "owner",
191 });
192
193 if (callResult == null) {
194 throw new Error("could not get on-chain owner");
195 }
196
197 return callResult;
198 },
199 };
200}

The toSmartContractAccount Method

For your reference, this is the definition of the toSmartContractAccount interface as pulled from the source code:

1import {
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";
15import { toAccount } from "viem/accounts";
16import { createBundlerClient } from "../client/bundlerClient.js";
17import type {
18 EntryPointDef,
19 EntryPointRegistryBase,
20 EntryPointVersion,
21} from "../entrypoint/types.js";
22import {
23 BatchExecutionNotSupportedError,
24 FailedToGetStorageSlotError,
25 GetCounterFactualAddressError,
26 SignTransactionNotSupportedError,
27 UpgradesNotSupportedError,
28} from "../errors/account.js";
29import { InvalidRpcUrlError } from "../errors/client.js";
30import { InvalidEntryPointError } from "../errors/entrypoint.js";
31import { Logger } from "../logger.js";
32import type { SmartAccountSigner } from "../signer/types.js";
33import { wrapSignatureWith6492 } from "../signer/utils.js";
34import type { NullAddress } from "../types.js";
35import type { IsUndefined, Never } from "../utils/types.js";
36
37export type AccountOp = {
38 target: Address;
39 value?: bigint;
40 data: Hex | "0x";
41};
42
43export enum DeploymentState {
44 UNDEFINED = "0x0",
45 NOT_DEPLOYED = "0x1",
46 DEPLOYED = "0x2",
47}
48
49export type SignatureRequest =
50 | {
51 type: "personal_sign";
52 data: SignableMessage;
53 }
54 | {
55 type: "eth_signTypedData_v4";
56 data: TypedDataDefinition;
57 };
58
59export type SigningMethods = {
60 prepareSign: (request: SignatureRequest) => Promise<SignatureRequest>;
61 formatSign: (signature: Hex) => Promise<Hex>;
62};
63
64export 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
75export 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
85export type UpgradeToAndCallParams = {
86 upgradeToAddress: Address;
87 upgradeToInitData: Hex;
88};
89
90export 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 */
113export const isSmartAccountWithSigner = (
114 account: SmartContractAccount,
115): account is SmartContractAccountWithSigner => {
116 return "getSigner" in account;
117};
118
119export type SmartContractAccount<
120 Name extends string = string,
121 TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
122> = LocalAccount<Name> & {
123 source: Name;
124 getDummySignature: () => Hex | Promise<Hex>;
125 encodeExecute: (tx: AccountOp) => Promise<Hex>;
126 encodeBatchExecute: (txs: AccountOp[]) => Promise<Hex>;
127 signUserOperationHash: (uoHash: Hex) => Promise<Hex>;
128 signMessageWith6492: (params: { message: SignableMessage }) => Promise<Hex>;
129 signTypedDataWith6492: <
130 const typedData extends TypedData | Record<string, unknown>,
131 primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
132 >(
133 typedDataDefinition: TypedDataDefinition<typedData, primaryType>,
134 ) => Promise<Hex>;
135 encodeUpgradeToAndCall: (params: UpgradeToAndCallParams) => Promise<Hex>;
136 getAccountNonce(nonceKey?: bigint): Promise<bigint>;
137 getInitCode: () => Promise<Hex>;
138 isAccountDeployed: () => Promise<boolean>;
139 getFactoryAddress: () => Promise<Address>;
140 getFactoryData: () => Promise<Hex>;
141 getEntryPoint: () => EntryPointDef<TEntryPointVersion>;
142 getImplementationAddress: () => Promise<NullAddress | Address>;
143} & SigningMethods;
144
145export interface AccountEntryPointRegistry<Name extends string = string>
146 extends EntryPointRegistryBase<
147 SmartContractAccount<Name, EntryPointVersion>
148 > {
149 "0.6.0": SmartContractAccount<Name, "0.6.0">;
150 "0.7.0": SmartContractAccount<Name, "0.7.0">;
151}
152
153export type ToSmartContractAccountParams<
154 Name extends string = string,
155 TTransport extends Transport = Transport,
156 TChain extends Chain = Chain,
157 TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
158> = {
159 source: Name;
160 transport: TTransport;
161 chain: TChain;
162 entryPoint: EntryPointDef<TEntryPointVersion, TChain>;
163 accountAddress?: Address;
164 getAccountInitCode: () => Promise<Hex>;
165 getDummySignature: () => Hex | Promise<Hex>;
166 encodeExecute: (tx: AccountOp) => Promise<Hex>;
167 encodeBatchExecute?: (txs: AccountOp[]) => Promise<Hex>;
168 getNonce?: (nonceKey?: bigint) => Promise<bigint>;
169 // if not provided, will default to just using signMessage over the Hex
170 signUserOperationHash?: (uoHash: Hex) => Promise<Hex>;
171 encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise<Hex>;
172 getImplementationAddress?: () => Promise<NullAddress | Address>;
173} & Omit<CustomSource, "signTransaction" | "address"> &
174 (SigningMethods | Never<SigningMethods>);
175
176/**
177 * Parses the factory address and factory calldata from the provided account initialization code (initCode).
178 *
179 * @example
180 * ```ts
181 * import { parseFactoryAddressFromAccountInitCode } from "@aa-sdk/core";
182 *
183 * const [address, calldata] = parseFactoryAddressFromAccountInitCode("0xAddressCalldata");
184 * ```
185 *
186 * @param {Hex} initCode The initialization code from which to parse the factory address and calldata
187 * @returns {[Address, Hex]} A tuple containing the parsed factory address and factory calldata
188 */
189export const parseFactoryAddressFromAccountInitCode = (
190 initCode: Hex,
191): [Address, Hex] => {
192 const factoryAddress: Address = `0x${initCode.substring(2, 42)}`;
193 const factoryCalldata: Hex = `0x${initCode.substring(42)}`;
194 return [factoryAddress, factoryCalldata];
195};
196
197export type GetAccountAddressParams = {
198 client: PublicClient;
199 entryPoint: EntryPointDef;
200 accountAddress?: Address;
201 getAccountInitCode: () => Promise<Hex>;
202};
203
204/**
205 * Retrieves the account address. Uses a provided `accountAddress` if available; otherwise, it computes the address using the entry point contract and the initial code.
206 *
207 * @example
208 * ```ts
209 * import { getEntryPoint, getAccountAddress } from "@aa-sdk/core";
210 *
211 * const accountAddress = await getAccountAddress({
212 * client,
213 * entryPoint: getEntryPoint(chain),
214 * getAccountInitCode: async () => "0x{factoryAddress}{factoryCallData}",
215 * });
216 * ```
217 *
218 * @param {GetAccountAddressParams} params The configuration object
219 * @param {PublicClient} params.client A public client instance to interact with the blockchain
220 * @param {EntryPointDef} params.entryPoint The entry point definition which includes the address and ABI
221 * @param {Address} params.accountAddress Optional existing account address
222 * @param {() => Promise<Hex>} params.getAccountInitCode A function that returns a Promise resolving to a Hex string representing the initial code of the account
223 * @returns {Promise<Address>} A promise that resolves to the account address
224 */
225export const getAccountAddress = async ({
226 client,
227 entryPoint,
228 accountAddress,
229 getAccountInitCode,
230}: GetAccountAddressParams) => {
231 if (accountAddress) return accountAddress;
232
233 const entryPointContract = getContract({
234 address: entryPoint.address,
235 abi: entryPoint.abi,
236 client,
237 });
238
239 const initCode = await getAccountInitCode();
240 Logger.verbose("[BaseSmartContractAccount](getAddress) initCode: ", initCode);
241
242 try {
243 await entryPointContract.simulate.getSenderAddress([initCode]);
244 } catch (err: any) {
245 Logger.verbose(
246 "[BaseSmartContractAccount](getAddress) getSenderAddress err: ",
247 err,
248 );
249 if (err.cause?.data?.errorName === "SenderAddressResult") {
250 Logger.verbose(
251 "[BaseSmartContractAccount](getAddress) entryPoint.getSenderAddress result:",
252 err.cause.data.args[0],
253 );
254
255 return err.cause.data.args[0] as Address;
256 }
257
258 if (err.details === "Invalid URL") {
259 throw new InvalidRpcUrlError();
260 }
261 }
262
263 throw new GetCounterFactualAddressError();
264};
265
266export async function toSmartContractAccount<
267 Name extends string = string,
268 TTransport extends Transport = Transport,
269 TChain extends Chain = Chain,
270 TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
271>({
272 transport,
273 chain,
274 entryPoint,
275 source,
276 accountAddress,
277 getAccountInitCode,
278 getNonce,
279 signMessage,
280 signTypedData,
281 encodeBatchExecute,
282 encodeExecute,
283 getDummySignature,
284 signUserOperationHash,
285 encodeUpgradeToAndCall,
286}: ToSmartContractAccountParams<
287 Name,
288 TTransport,
289 TChain,
290 TEntryPointVersion
291>): Promise<SmartContractAccount<Name, TEntryPointVersion>>;
292
293/**
294 * 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.
295 *
296 * @example
297 * ```ts
298 * import { http, type SignableMessage } from "viem";
299 * import { sepolia } from "viem/chains";
300 *
301 * const myAccount = await toSmartContractAccount({
302 * /// REQUIRED PARAMS ///
303 * source: "MyAccount",
304 * transport: http("RPC_URL"),
305 * chain: sepolia,
306 * // The EntryPointDef that your account is com"patible with
307 * entryPoint: getEntryPoint(sepolia, { version: "0.6.0" }),
308 * // This should return a concatenation of your `factoryAddress` and the `callData` for your factory's create account method
309 * getAccountInitCode: async () => "0x{factoryAddress}{callData}",
310 * // an invalid signature that doesn't cause your account to revert during validation
311 * getDummySignature: () => "0x1234...",
312 * // given a UO in the form of {target, data, value} should output the calldata for calling your contract's execution method
313 * encodeExecute: async (uo) => "0xcalldata",
314 * signMessage: async ({ message }: { message: SignableMessage }) => "0x...",
315 * signTypedData: async (typedData) => "0x000",
316 *
317 * /// OPTIONAL PARAMS ///
318 * // if you already know your account's address, pass that in here to avoid generating a new counterfactual
319 * accountAddress: "0xaddressoverride",
320 * // if your account supports batching, this should take an array of UOs and return the calldata for calling your contract's batchExecute method
321 * encodeBatchExecute: async (uos) => "0x...",
322 * // if your contract expects a different signing scheme than the default signMessage scheme, you can override that here
323 * signUserOperationHash: async (hash) => "0x...",
324 * // allows you to define the calldata for upgrading your account
325 * encodeUpgradeToAndCall: async (params) => "0x...",
326 * });
327 * ```
328 *
329 * @param {ToSmartContractAccountParams} params the parameters required for converting to a smart contract account
330 * @param {Transport} params.transport the transport mechanism used for communication
331 * @param {Chain} params.chain the blockchain chain used in the account
332 * @param {EntryPoint} params.entryPoint the entry point of the smart contract
333 * @param {string} params.source the source identifier for the account
334 * @param {Address} [params.accountAddress] the address of the account
335 * @param {() => Promise<Hex>} params.getAccountInitCode a function to get the initial state code of the account
336 * @param {(message: { message: SignableMessage }) => Promise<Hex>} params.signMessage a function to sign a message
337 * @param {(typedDataDefinition: TypedDataDefinition<typedData, primaryType>) => Promise<Hex>} params.signTypedData a function to sign typed data
338 * @param {(transactions: Transaction[]) => Hex} [params.encodeBatchExecute] a function to encode batch transactions
339 * @param {(tx: Transaction) => Hex} params.encodeExecute a function to encode a single transaction
340 * @param {() => Promise<Hex>} params.getDummySignature a function to get a dummy signature
341 * @param {(uoHash: Hex) => Promise<Hex>} [params.signUserOperationHash] a function to sign user operations
342 * @param {(implementationAddress: Address, implementationCallData: Hex) => Hex} [params.encodeUpgradeToAndCall] a function to encode upgrade call
343 * @returns {Promise<SmartContractAccount>} a promise that resolves to a SmartContractAccount object with methods and properties for interacting with the smart contract account
344 */
345export async function toSmartContractAccount(
346 params: ToSmartContractAccountParams,
347): Promise<SmartContractAccount> {
348 const {
349 transport,
350 chain,
351 entryPoint,
352 source,
353 accountAddress,
354 getAccountInitCode,
355 signMessage,
356 signTypedData,
357 encodeExecute,
358 encodeBatchExecute,
359 getNonce,
360 getDummySignature,
361 signUserOperationHash,
362 encodeUpgradeToAndCall,
363 getImplementationAddress,
364 prepareSign: prepareSign_,
365 formatSign: formatSign_,
366 } = params;
367
368 const client = createBundlerClient({
369 // we set the retry count to 0 so that viem doesn't retry during
370 // getting the address. That call always reverts and without this
371 // viem will retry 3 times, making this call very slow
372 transport: (opts) => transport({ ...opts, chain, retryCount: 0 }),
373 chain,
374 });
375
376 const entryPointContract = getContract({
377 address: entryPoint.address,
378 abi: entryPoint.abi,
379 client,
380 });
381
382 const accountAddress_ = await getAccountAddress({
383 client,
384 entryPoint: entryPoint,
385 accountAddress,
386 getAccountInitCode,
387 });
388
389 let deploymentState = DeploymentState.UNDEFINED;
390
391 const getInitCode = async () => {
392 if (deploymentState === DeploymentState.DEPLOYED) {
393 return "0x";
394 }
395 const contractCode = await client.getCode({
396 address: accountAddress_,
397 });
398
399 if ((contractCode?.length ?? 0) > 2) {
400 deploymentState = DeploymentState.DEPLOYED;
401 return "0x";
402 } else {
403 deploymentState = DeploymentState.NOT_DEPLOYED;
404 }
405
406 return getAccountInitCode();
407 };
408
409 const signUserOperationHash_ =
410 signUserOperationHash ??
411 (async (uoHash: Hex) => {
412 return signMessage({ message: { raw: hexToBytes(uoHash) } });
413 });
414
415 const getFactoryAddress = async (): Promise<Address> =>
416 parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[0];
417
418 const getFactoryData = async (): Promise<Hex> =>
419 parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[1];
420
421 const encodeUpgradeToAndCall_ =
422 encodeUpgradeToAndCall ??
423 (() => {
424 throw new UpgradesNotSupportedError(source);
425 });
426
427 const isAccountDeployed = async () => {
428 const initCode = await getInitCode();
429 return initCode === "0x";
430 };
431
432 const getNonce_ =
433 getNonce ??
434 (async (nonceKey = 0n): Promise<bigint> => {
435 return entryPointContract.read.getNonce([
436 accountAddress_,
437 nonceKey,
438 ]) as Promise<bigint>;
439 });
440
441 const account = toAccount({
442 address: accountAddress_,
443 signMessage,
444 signTypedData,
445 signTransaction: () => {
446 throw new SignTransactionNotSupportedError();
447 },
448 });
449
450 const create6492Signature = async (isDeployed: boolean, signature: Hex) => {
451 if (isDeployed) {
452 return signature;
453 }
454
455 const [factoryAddress, factoryCalldata] =
456 parseFactoryAddressFromAccountInitCode(await getAccountInitCode());
457
458 return wrapSignatureWith6492({
459 factoryAddress,
460 factoryCalldata,
461 signature,
462 });
463 };
464
465 const signMessageWith6492 = async (message: { message: SignableMessage }) => {
466 const [isDeployed, signature] = await Promise.all([
467 isAccountDeployed(),
468 account.signMessage(message),
469 ]);
470
471 return create6492Signature(isDeployed, signature);
472 };
473
474 const signTypedDataWith6492 = async <
475 const typedData extends TypedData | Record<string, unknown>,
476 primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
477 >(
478 typedDataDefinition: TypedDataDefinition<typedData, primaryType>,
479 ): Promise<Hex> => {
480 const [isDeployed, signature] = await Promise.all([
481 isAccountDeployed(),
482 account.signTypedData(typedDataDefinition),
483 ]);
484
485 return create6492Signature(isDeployed, signature);
486 };
487
488 const getImplementationAddress_ =
489 getImplementationAddress ??
490 (async () => {
491 const storage = await client.getStorageAt({
492 address: account.address,
493 // This is the default slot for the implementation address for Proxies
494 slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
495 });
496
497 if (storage == null) {
498 throw new FailedToGetStorageSlotError(
499 "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
500 "Proxy Implementation Address",
501 );
502 }
503
504 // The storage slot contains a full bytes32, but we want only the last 20 bytes.
505 // So, slice off the leading `0x` and the first 12 bytes (24 characters), leaving the last 20 bytes, then prefix with `0x`.
506 return `0x${storage.slice(26)}`;
507 });
508
509 if (entryPoint.version !== "0.6.0" && entryPoint.version !== "0.7.0") {
510 throw new InvalidEntryPointError(chain, entryPoint.version);
511 }
512
513 if ((prepareSign_ && !formatSign_) || (!prepareSign_ && formatSign_)) {
514 throw new Error(
515 "Must implement both prepareSign and formatSign or neither",
516 );
517 }
518
519 const prepareSign =
520 prepareSign_ ??
521 (() => {
522 throw new Error("prepareSign not implemented");
523 });
524
525 const formatSign =
526 formatSign_ ??
527 (() => {
528 throw new Error("formatSign not implemented");
529 });
530
531 return {
532 ...account,
533 source,
534 // TODO: I think this should probably be signUserOperation instead
535 // and allow for generating the UO hash based on the EP version
536 signUserOperationHash: signUserOperationHash_,
537 getFactoryAddress,
538 getFactoryData,
539 encodeBatchExecute:
540 encodeBatchExecute ??
541 (() => {
542 throw new BatchExecutionNotSupportedError(source);
543 }),
544 encodeExecute,
545 getDummySignature,
546 getInitCode,
547 encodeUpgradeToAndCall: encodeUpgradeToAndCall_,
548 getEntryPoint: () => entryPoint,
549 isAccountDeployed,
550 getAccountNonce: getNonce_,
551 signMessageWith6492,
552 signTypedDataWith6492,
553 getImplementationAddress: getImplementationAddress_,
554 prepareSign,
555 formatSign,
556 };
557}