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 Account Kit, 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 Account Kit.

[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 getAccountAddress,
4 getEntryPoint,
5 type Address,
6 type EntryPointDef,
7 type SmartAccountSigner,
8} from "@aa-sdk/core";
9import {
10 concatHex,
11 encodeFunctionData,
12 type Chain,
13 type Hex,
14 type Transport,
15} from "viem";
16import { LightAccountAbi_v1 } from "../abis/LightAccountAbi_v1.js";
17import { LightAccountAbi_v2 } from "../abis/LightAccountAbi_v2.js";
18import { LightAccountFactoryAbi_v1 } from "../abis/LightAccountFactoryAbi_v1.js";
19import { LightAccountFactoryAbi_v2 } from "../abis/LightAccountFactoryAbi_v2.js";
20import type {
21 LightAccountEntryPointVersion,
22 LightAccountVersion,
23} from "../types.js";
24import {
25 AccountVersionRegistry,
26 LightAccountUnsupported1271Factories,
27 defaultLightAccountVersion,
28 getDefaultLightAccountFactoryAddress,
29} from "../utils.js";
30import {
31 createLightAccountBase,
32 type CreateLightAccountBaseParams,
33 type LightAccountBase,
34} from "./base.js";
35
36export type LightAccount<
37 TSigner extends SmartAccountSigner = SmartAccountSigner,
38 TLightAccountVersion extends LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">
39> = LightAccountBase<TSigner, "LightAccount", TLightAccountVersion> & {
40 encodeTransferOwnership: (newOwner: Address) => Hex;
41 getOwnerAddress: () => Promise<Address>;
42};
43
44// [!region CreateLightAccountParams]
45export type CreateLightAccountParams<
46 TTransport extends Transport = Transport,
47 TSigner extends SmartAccountSigner = SmartAccountSigner,
48 TLightAccountVersion extends LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">
49> = Omit<
50 CreateLightAccountBaseParams<
51 "LightAccount",
52 TLightAccountVersion,
53 TTransport,
54 TSigner
55 >,
56 | "getAccountInitCode"
57 | "entryPoint"
58 | "version"
59 | "abi"
60 | "accountAddress"
61 | "type"
62> & {
63 salt?: bigint;
64 initCode?: Hex;
65 accountAddress?: Address;
66 factoryAddress?: Address;
67 version?: TLightAccountVersion;
68 entryPoint?: EntryPointDef<
69 LightAccountEntryPointVersion<"LightAccount", TLightAccountVersion>,
70 Chain
71 >;
72};
73// [!endregion CreateLightAccountParams]
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 getAccountInitCode = async () => {
130 if (initCode) return initCode;
131
132 const salt = LightAccountUnsupported1271Factories.has(
133 factoryAddress.toLowerCase() as Address
134 )
135 ? 0n
136 : salt_;
137
138 return concatHex([
139 factoryAddress,
140 encodeFunctionData({
141 abi: factoryAbi,
142 functionName: "createAccount",
143 args: [await signer.getAddress(), salt],
144 }),
145 ]);
146 };
147
148 const address = await getAccountAddress({
149 client,
150 entryPoint,
151 accountAddress,
152 getAccountInitCode,
153 });
154
155 const account = await createLightAccountBase<
156 "LightAccount",
157 LightAccountVersion<"LightAccount">,
158 Transport,
159 SmartAccountSigner
160 >({
161 transport,
162 chain,
163 signer,
164 abi: accountAbi,
165 type: "LightAccount",
166 version,
167 entryPoint,
168 accountAddress: address,
169 getAccountInitCode,
170 });
171
172 return {
173 ...account,
174
175 encodeTransferOwnership: (newOwner: Address) => {
176 return encodeFunctionData({
177 abi: accountAbi,
178 functionName: "transferOwnership",
179 args: [newOwner],
180 });
181 },
182 async getOwnerAddress(): Promise<Address> {
183 const callResult = await client.readContract({
184 address,
185 abi: accountAbi,
186 functionName: "owner",
187 });
188
189 if (callResult == null) {
190 throw new Error("could not get on-chain owner");
191 }
192
193 return callResult;
194 },
195 };
196}

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