Skip to content

Commit

Permalink
feat: add alchemy accounts context
Browse files Browse the repository at this point in the history
  • Loading branch information
moldy530 committed Mar 30, 2024
1 parent 8880e6d commit 7b451ff
Show file tree
Hide file tree
Showing 34 changed files with 875 additions and 12 deletions.
30 changes: 27 additions & 3 deletions packages/alchemy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "MIT",
"private": false,
"type": "module",
"main": "./dist/cjs/index.js",
"main": "./dist/esm/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
Expand All @@ -23,8 +23,15 @@
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.js",
"default": "./dist/cjs/index.js"
"default": "./dist/esm/index.js"
},
"./config": {
"types": "./dist/types/config/index.d.ts",
"default": "./dist/esm/config/index.js"
},
"./react": {
"types": "./dist/types/react/index.d.ts",
"default": "./dist/esm/react/index.js"
},
"./package.json": "./package.json"
},
Expand All @@ -41,6 +48,9 @@
},
"devDependencies": {
"@alchemy/aa-accounts": "*",
"@tanstack/react-query": "^5.28.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.4",
"typescript-template": "*",
"vitest": "^0.31.0"
Expand All @@ -56,8 +66,22 @@
"zustand": "^4.5.2"
},
"peerDependencies": {
"@tanstack/react-query": "^5.28.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"viem": "2.8.6"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"@tanstack/react-query": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
Expand Down
96 changes: 96 additions & 0 deletions packages/alchemy/src/config/actions/createAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
createLightAccount,
createMultiOwnerModularAccount,
type CreateLightAccountParams,
type CreateMultiOwnerModularAccountParams,
} from "@alchemy/aa-accounts";
import { custom } from "viem";
import { AlchemySignerStatus } from "../../signer/index.js";
import { ClientOnlyPropertyError } from "../errors.js";
import type {
AlchemyAccountsConfig,
SupportedAccountTypes,
SupportedAccounts,
} from "../types";
import { getSignerStatus } from "./getSignerStatus.js";

export type AccountConfig<TAccount extends SupportedAccountTypes> =
TAccount extends "LightAccount"
? Omit<CreateLightAccountParams, "signer" | "transport" | "chain">
: Omit<
CreateMultiOwnerModularAccountParams,
"signer" | "transport" | "chain"
>;

export type CreateAccountParams<TAccount extends SupportedAccountTypes> = {
type: TAccount;
params?: AccountConfig<TAccount>;
};

export async function createAccount<TAccount extends SupportedAccountTypes>(
{ type, params }: CreateAccountParams<TAccount>,
config: AlchemyAccountsConfig
): Promise<SupportedAccounts> {
const clientStore = config.clientStore;
if (!clientStore) {
throw new ClientOnlyPropertyError("account");
}

const transport = custom(config.bundlerClient);
const chain = config.bundlerClient.chain;
const signer = config.signer;
const signerStatus = getSignerStatus(config);

if (signerStatus !== AlchemySignerStatus.CONNECTED) {
throw new Error("Signer not connected");
}

const cachedAccount = clientStore.getState().accounts[type];
if (cachedAccount) {
return cachedAccount.account;
}

const accountPromise = (() => {
switch (type) {
case "LightAccount":
return createLightAccount({
...params,
signer,
transport: (opts) => transport({ ...opts, retryCount: 0 }),
chain,
});
case "MultiOwnerModularAccount":
return createMultiOwnerModularAccount({
...params,
signer,
transport: (opts) => transport({ ...opts, retryCount: 0 }),
chain,
});
default:
throw new Error("Unsupported account type");
}
})();

clientStore.setState((state) => ({
accounts: {
...state.accounts,
[type]: {
status: "INITIALIZING",
account: accountPromise,
},
},
}));

const account = await accountPromise;
clientStore.setState((state) => ({
accounts: {
...state.accounts,
[type]: {
status: "READY",
account,
},
},
}));

return accountPromise;
}
23 changes: 23 additions & 0 deletions packages/alchemy/src/config/actions/getAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ClientOnlyPropertyError } from "../errors.js";
import type { AccountState } from "../store/types.js";
import type { AlchemyAccountsConfig, SupportedAccountTypes } from "../types";
import { type CreateAccountParams } from "./createAccount.js";

export type GetAccountResult<TAccount extends SupportedAccountTypes> =
| AccountState<TAccount>
| undefined;

export type GetAccountParams<TAccount extends SupportedAccountTypes> =
CreateAccountParams<TAccount>;

export const getAccount = <TAccount extends SupportedAccountTypes>(
{ type }: GetAccountParams<TAccount>,
config: AlchemyAccountsConfig
): GetAccountResult<TAccount> => {
const clientStore = config.clientStore;
if (!clientStore) {
throw new ClientOnlyPropertyError("account");
}

return clientStore.getState().accounts[type];
};
8 changes: 8 additions & 0 deletions packages/alchemy/src/config/actions/getBundlerClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ClientWithAlchemyMethods } from "../../client/types";
import type { AlchemyAccountsConfig } from "../types";

export const getBundlerClient = (
config: AlchemyAccountsConfig
): ClientWithAlchemyMethods => {
return config.coreStore.getState().bundlerClient;
};
11 changes: 11 additions & 0 deletions packages/alchemy/src/config/actions/getSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { AlchemySigner } from "../../signer/signer.js";
import { ClientOnlyPropertyError } from "../errors.js";
import type { AlchemyAccountsConfig } from "../types";

export const getSigner = (config: AlchemyAccountsConfig): AlchemySigner => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("signer");
}

return config.clientStore.getState().signer;
};
10 changes: 10 additions & 0 deletions packages/alchemy/src/config/actions/getSignerStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ClientOnlyPropertyError } from "../errors.js";
import type { AlchemyAccountsConfig } from "../types";

export const getSignerStatus = (config: AlchemyAccountsConfig) => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("signerStatus");
}

return config.clientStore.getState().signerStatus;
};
30 changes: 30 additions & 0 deletions packages/alchemy/src/config/actions/watchAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ClientOnlyPropertyError } from "../errors.js";
import type { AlchemyAccountsConfig, SupportedAccountTypes } from "../types";
import type { GetAccountResult } from "./getAccount";

export const watchAccount =
<TAccount extends SupportedAccountTypes>(
type: TAccount,
config: AlchemyAccountsConfig
) =>
(onChange: (account: GetAccountResult<TAccount>) => void) => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("account");
}

return config.clientStore.subscribe(
({ accounts }) => accounts[type],
onChange,
{
fireImmediately: true,
equalityFn(a, b) {
return (
a?.status === b?.status ||
(a?.status === "READY" &&
b?.status === "READY" &&
a.account.address === b.account.address)
);
},
}
);
};
12 changes: 12 additions & 0 deletions packages/alchemy/src/config/actions/watchBundlerClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ClientWithAlchemyMethods } from "../../client/types";
import type { AlchemyAccountsConfig } from "../types";

export const watchBundlerClient =
(config: AlchemyAccountsConfig) =>
(onChange: (bundlerClient: ClientWithAlchemyMethods) => void) => {
return config.coreStore.subscribe(
({ bundlerClient }) => bundlerClient,
onChange,
{ fireImmediately: true }
);
};
15 changes: 15 additions & 0 deletions packages/alchemy/src/config/actions/watchSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { AlchemySigner } from "../../signer";
import { ClientOnlyPropertyError } from "../errors.js";
import type { AlchemyAccountsConfig } from "../types";

export const watchSigner =
(config: AlchemyAccountsConfig) =>
(onChange: (signer: AlchemySigner) => void) => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("signer");
}

return config.clientStore.subscribe(({ signer }) => signer, onChange, {
fireImmediately: true,
});
};
17 changes: 17 additions & 0 deletions packages/alchemy/src/config/actions/watchSignerStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { AlchemySignerStatus } from "../../signer/types.js";
import { ClientOnlyPropertyError } from "../errors.js";
import type { AlchemyAccountsConfig } from "../types";

export const watchSignerStatus =
(config: AlchemyAccountsConfig) =>
(onChange: (status: AlchemySignerStatus) => void) => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("signerStatus");
}

return config.clientStore.subscribe(
({ signerStatus }) => signerStatus,
onChange,
{ fireImmediately: true }
);
};
14 changes: 14 additions & 0 deletions packages/alchemy/src/config/actions/watchUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { User } from "../../signer";
import { ClientOnlyPropertyError } from "../errors.js";
import type { AlchemyAccountsConfig } from "../types";

export const watchUser =
(config: AlchemyAccountsConfig) => (onChange: (user?: User) => void) => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("user");
}

return config.clientStore.subscribe(({ user }) => user, onChange, {
fireImmediately: true,
});
};
44 changes: 44 additions & 0 deletions packages/alchemy/src/config/createConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { ConnectionConfigSchema } from "@alchemy/aa-core";
import { getBundlerClient } from "./actions/getBundlerClient.js";
import { getSigner } from "./actions/getSigner.js";
import { createClientStore } from "./store/client.js";
import { createCoreStore } from "./store/core.js";
import type { AlchemyAccountsConfig, CreateConfigProps } from "./types";

export const DEFAULT_IFRAME_CONTAINER_ID = "alchemy-signer-iframe-container";

export const createConfig = ({
chain,
iframeConfig,
rootOrgId,
rpId,
sessionConfig,
signerConnection,
...connectionConfig
}: CreateConfigProps): AlchemyAccountsConfig => {
const connection = ConnectionConfigSchema.parse(connectionConfig);

const config: AlchemyAccountsConfig = {
coreStore: createCoreStore({ connection, chain }),
clientStore: createClientStore({
client: {
connection: signerConnection ?? connection,
iframeConfig,
rootOrgId,
rpId,
},
sessionConfig,
}),
// these are just here for convenience right now, but you can do all of this with actions on the stores as well
get bundlerClient() {
return getBundlerClient(config);
},
get signer() {
return getSigner(config);
},
};

return config;
};
9 changes: 9 additions & 0 deletions packages/alchemy/src/config/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BaseError } from "../errors/base.js";

export class ClientOnlyPropertyError extends BaseError {
name: string = "ClientOnlyPropertyError";

constructor(property: string) {
super(`${property} is only available on the client`);
}
}
2 changes: 2 additions & 0 deletions packages/alchemy/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DEFAULT_IFRAME_CONTAINER_ID, createConfig } from "./createConfig.js";
export type * from "./types.js";
Loading

0 comments on commit 7b451ff

Please sign in to comment.