diff --git a/packages/alchemy/src/__tests__/provider.test.ts b/packages/alchemy/src/__tests__/provider.test.ts index 7ec1b6681a..fdd18f8942 100644 --- a/packages/alchemy/src/__tests__/provider.test.ts +++ b/packages/alchemy/src/__tests__/provider.test.ts @@ -24,16 +24,21 @@ describe("Alchemy Provider Tests", () => { apiKey: "test", chain, entryPointAddress: "0xENTRYPOINT_ADDRESS", - }).connect( - (provider) => - new SimpleSmartContractAccount({ - entryPointAddress: "0xENTRYPOINT_ADDRESS", - chain, - owner, - factoryAddress: "0xSIMPLE_ACCOUNT_FACTORY_ADDRESS", - rpcClient: provider, - }) - ); + }).connect((provider) => { + const account = new SimpleSmartContractAccount({ + entryPointAddress: "0xENTRYPOINT_ADDRESS", + chain, + owner, + factoryAddress: "0xSIMPLE_ACCOUNT_FACTORY_ADDRESS", + rpcClient: provider, + }); + + account.getAddress = vi.fn( + async () => "0xb856DBD4fA1A79a46D426f537455e7d3E79ab7c4" + ); + + return account; + }); it("should correctly sign the message", async () => { expect( diff --git a/packages/core/package.json b/packages/core/package.json index 88ef9e0483..9ddbe0da7f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,7 +51,8 @@ "vitest": "^0.31.0" }, "dependencies": { - "abitype": "^0.8.3" + "abitype": "^0.8.3", + "eventemitter3": "^5.0.1" }, "peerDependencies": { "viem": "^1.1.7" diff --git a/packages/core/src/account/__tests__/simple.test.ts b/packages/core/src/account/__tests__/simple.test.ts index 8f279289c7..cf506c8764 100644 --- a/packages/core/src/account/__tests__/simple.test.ts +++ b/packages/core/src/account/__tests__/simple.test.ts @@ -1,12 +1,12 @@ import { polygonMumbai } from "viem/chains"; import { describe, it } from "vitest"; +import { SmartAccountProvider } from "../../provider/base.js"; +import { LocalAccountSigner } from "../../signer/local-account.js"; +import type { BatchUserOperationCallData } from "../../types.js"; import { SimpleSmartContractAccount, type SimpleSmartAccountOwner, } from "../simple.js"; -import { LocalAccountSigner } from "../../signer/local-account.js"; -import type { BatchUserOperationCallData } from "../../types.js"; -import { SmartAccountProvider } from "../../provider/base.js"; describe("Account Simple Tests", () => { const dummyMnemonic = @@ -16,18 +16,23 @@ describe("Account Simple Tests", () => { const chain = polygonMumbai; const signer = new SmartAccountProvider( `${chain.rpcUrls.alchemy.http[0]}/${"test"}`, - "0xENTRYPOINT_ADDRESS", + "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", chain - ).connect( - (provider) => - new SimpleSmartContractAccount({ - entryPointAddress: "0xENTRYPOINT_ADDRESS", - chain, - owner, - factoryAddress: "0xSIMPLE_ACCOUNT_FACTORY_ADDRESS", - rpcClient: provider, - }) - ); + ).connect((provider) => { + const account = new SimpleSmartContractAccount({ + entryPointAddress: "0xENTRYPOINT_ADDRESS", + chain, + owner, + factoryAddress: "0xSIMPLE_ACCOUNT_FACTORY_ADDRESS", + rpcClient: provider, + }); + + account.getAddress = vi.fn( + async () => "0xb856DBD4fA1A79a46D426f537455e7d3E79ab7c4" + ); + + return account; + }); it("should correctly sign the message", async () => { expect( @@ -41,7 +46,6 @@ describe("Account Simple Tests", () => { }); it("should correctly encode batch transaction data", async () => { - const account = signer.account; const data = [ { target: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", @@ -53,7 +57,7 @@ describe("Account Simple Tests", () => { }, ] satisfies BatchUserOperationCallData; - expect(await account.encodeBatchExecute(data)).toMatchInlineSnapshot( + expect(await signer.account.encodeBatchExecute(data)).toMatchInlineSnapshot( '"0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000"' ); }); diff --git a/packages/core/src/provider/__tests__/base.test.ts b/packages/core/src/provider/__tests__/base.test.ts index 6ad57da52f..e134c0f163 100644 --- a/packages/core/src/provider/__tests__/base.test.ts +++ b/packages/core/src/provider/__tests__/base.test.ts @@ -1,4 +1,4 @@ -import type { Transaction } from "viem"; +import { type Transaction } from "viem"; import { polygonMumbai } from "viem/chains"; import { afterEach, @@ -8,8 +8,8 @@ import { vi, type SpyInstance, } from "vitest"; -import { SmartAccountProvider } from "../base.js"; import type { UserOperationReceipt } from "../../types.js"; +import { SmartAccountProvider } from "../base.js"; describe("Base Tests", () => { let retryMsDelays: number[] = []; @@ -95,4 +95,52 @@ describe("Base Tests", () => { getUserOperationReceiptMock ); }); + + it("should emit connected event on connected", async () => { + const spy = vi.spyOn(providerMock, "emit"); + const account = { + chain: polygonMumbai, + entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + rpcClient: providerMock.rpcClient, + getAddress: async () => "0xMOCK_ADDRESS", + } as any; + + // This says the await is not important... it is. the method is not marked sync because we don't need it to be, + // but the address is emited from an async method so we want to await that + await providerMock.connect(() => account); + + expect(spy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "connect", + { + "chainId": "0x13881", + }, + ], + [ + "accountsChanged", + [ + "0xMOCK_ADDRESS", + ], + ], + ] + `); + }); + + it("should emit disconnected event on disconnect", async () => { + const spy = vi.spyOn(providerMock, "emit"); + providerMock.disconnect(); + + expect(spy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "disconnect", + ], + [ + "accountsChanged", + [], + ], + ] + `); + }); }); diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts index 5069377469..b4139a7422 100644 --- a/packages/core/src/provider/base.ts +++ b/packages/core/src/provider/base.ts @@ -1,5 +1,7 @@ +import { default as EventEmitter } from "eventemitter3"; import { fromHex, + toHex, type Address, type Chain, type Hash, @@ -37,6 +39,7 @@ import type { GasEstimatorMiddleware, ISmartAccountProvider, PaymasterAndDataMiddleware, + ProviderEvents, SendUserOperationResult, } from "./types.js"; @@ -77,8 +80,10 @@ export type ConnectedSmartAccountProvider< }; export class SmartAccountProvider< - TTransport extends SupportedTransports = Transport -> implements ISmartAccountProvider + TTransport extends SupportedTransports = Transport + > + extends EventEmitter + implements ISmartAccountProvider { private txMaxRetries: number; private txRetryIntervalMs: number; @@ -94,6 +99,8 @@ export class SmartAccountProvider< readonly account?: BaseSmartContractAccount, opts?: SmartAccountProviderOpts ) { + super(); + this.txMaxRetries = opts?.txMaxRetries ?? 5; this.txRetryIntervalMs = opts?.txRetryIntervalMs ?? 2000; this.txRetryMulitplier = opts?.txRetryMulitplier ?? 1.5; @@ -411,9 +418,29 @@ export class SmartAccountProvider< ): this & { account: BaseSmartContractAccount } { const account = fn(this.rpcClient); defineReadOnly(this, "account", account); + + this.emit("connect", { + chainId: toHex(this.chain.id), + }); + + account + .getAddress() + .then((address) => this.emit("accountsChanged", [address])); + return this as this & { account: typeof account }; } + disconnect(): this & { account: undefined } { + if (this.account) { + this.emit("disconnect"); + this.emit("accountsChanged", []); + } + + defineReadOnly(this, "account", undefined); + + return this as this & { account: undefined }; + } + isConnected(): this is ConnectedSmartAccountProvider { return this.account !== undefined; } diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index ab61a788ca..87a8865718 100644 --- a/packages/core/src/provider/types.ts +++ b/packages/core/src/provider/types.ts @@ -1,5 +1,5 @@ import type { Address } from "abitype"; -import type { Hash, RpcTransactionRequest, Transport } from "viem"; +import type { Hash, Hex, RpcTransactionRequest, Transport } from "viem"; import type { BaseSmartContractAccount } from "../account/base.js"; import type { PublicErc4337Client, @@ -17,6 +17,19 @@ import type { type WithRequired = Required>; type WithOptional = Pick, K>; +export type ConnectorData = { + chainId?: Hex; +}; + +export interface ProviderEvents { + chainChanged(chainId: Hex): void; + accountsChanged(accounts: Address[]): void; + connect(data: ConnectorData): void; + message({ type, data }: { type: string; data?: unknown }): void; + disconnect(): void; + error(error: Error): void; +} + export type SendUserOperationResult = { hash: string; request: UserOperationRequest; @@ -204,4 +217,11 @@ export interface ISmartAccountProvider< connect( fn: (provider: PublicErc4337Client) => BaseSmartContractAccount ): this & { account: BaseSmartContractAccount }; + + /** + * Allows for disconnecting the account from the provider so you can connect the provider to another account instance + * + * @returns the provider with the account disconnected + */ + disconnect(): this & { account: undefined }; } diff --git a/packages/ethers/src/__tests__/provider-adapter.test.ts b/packages/ethers/src/__tests__/provider-adapter.test.ts index 55fc94c011..aca95313f1 100644 --- a/packages/ethers/src/__tests__/provider-adapter.test.ts +++ b/packages/ethers/src/__tests__/provider-adapter.test.ts @@ -17,16 +17,21 @@ describe("Simple Account Tests", async () => { const signer = EthersProviderAdapter.fromEthersProvider( alchemyProvider, "0xENTRYPOINT_ADDRESS" - ).connectToAccount( - (rpcClient) => - new SimpleSmartContractAccount({ - entryPointAddress: "0xENTRYPOINT_ADDRESS", - chain: getChain(alchemyProvider.network.chainId), - owner: convertWalletToAccountSigner(owner), - factoryAddress: "0xSIMPLE_ACCOUNT_FACTORY_ADDRESS", - rpcClient, - }) - ); + ).connectToAccount((rpcClient) => { + const account = new SimpleSmartContractAccount({ + entryPointAddress: "0xENTRYPOINT_ADDRESS", + chain: getChain(alchemyProvider.network.chainId), + owner: convertWalletToAccountSigner(owner), + factoryAddress: "0xSIMPLE_ACCOUNT_FACTORY_ADDRESS", + rpcClient, + }); + + account.getAddress = vi.fn( + async () => "0xb856DBD4fA1A79a46D426f537455e7d3E79ab7c4" + ); + + return account; + }); it("should correctly sign the message", async () => { expect( diff --git a/yarn.lock b/yarn.lock index e357098253..aef35c5901 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,7 +8,7 @@ integrity sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ== "@alchemy/aa-core@file:packages/core": - version "0.1.0-alpha.17" + version "0.1.0-alpha.18" dependencies: abitype "^0.8.3" @@ -6805,6 +6805,11 @@ eventemitter3@^4.0.4, eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"