Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add event emitter to the provider so that we can listen to connected events in dapps #65

Merged
merged 1 commit into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions packages/alchemy/src/__tests__/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
36 changes: 20 additions & 16 deletions packages/core/src/account/__tests__/simple.test.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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(
Expand All @@ -41,7 +46,6 @@ describe("Account Simple Tests", () => {
});

it("should correctly encode batch transaction data", async () => {
const account = signer.account;
const data = [
{
target: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
Expand All @@ -53,7 +57,7 @@ describe("Account Simple Tests", () => {
},
] satisfies BatchUserOperationCallData;

expect(await account.encodeBatchExecute(data)).toMatchInlineSnapshot(
expect(await signer.account.encodeBatchExecute(data)).toMatchInlineSnapshot(
'"0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000"'
);
});
Expand Down
52 changes: 50 additions & 2 deletions packages/core/src/provider/__tests__/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Transaction } from "viem";
import { type Transaction } from "viem";
import { polygonMumbai } from "viem/chains";
import {
afterEach,
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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",
[],
],
]
`);
});
});
31 changes: 29 additions & 2 deletions packages/core/src/provider/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { default as EventEmitter } from "eventemitter3";
import {
fromHex,
toHex,
type Address,
type Chain,
type Hash,
Expand Down Expand Up @@ -37,6 +39,7 @@ import type {
GasEstimatorMiddleware,
ISmartAccountProvider,
PaymasterAndDataMiddleware,
ProviderEvents,
SendUserOperationResult,
} from "./types.js";

Expand Down Expand Up @@ -77,8 +80,10 @@ export type ConnectedSmartAccountProvider<
};

export class SmartAccountProvider<
TTransport extends SupportedTransports = Transport
> implements ISmartAccountProvider<TTransport>
TTransport extends SupportedTransports = Transport
>
extends EventEmitter<ProviderEvents>
implements ISmartAccountProvider<TTransport>
{
private txMaxRetries: number;
private txRetryIntervalMs: number;
Expand All @@ -94,6 +99,8 @@ export class SmartAccountProvider<
readonly account?: BaseSmartContractAccount<TTransport>,
opts?: SmartAccountProviderOpts
) {
super();

this.txMaxRetries = opts?.txMaxRetries ?? 5;
this.txRetryIntervalMs = opts?.txRetryIntervalMs ?? 2000;
this.txRetryMulitplier = opts?.txRetryMulitplier ?? 1.5;
Expand Down Expand Up @@ -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<TTransport> {
return this.account !== undefined;
}
Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/provider/types.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -17,6 +17,19 @@ import type {
type WithRequired<T, K extends keyof T> = Required<Pick<T, K>>;
type WithOptional<T, K extends keyof T> = Pick<Partial<T>, 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;
Expand Down Expand Up @@ -204,4 +217,11 @@ export interface ISmartAccountProvider<
connect(
fn: (provider: PublicErc4337Client<TTransport>) => 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 };
}
25 changes: 15 additions & 10 deletions packages/ethers/src/__tests__/provider-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down