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 alchemy accounts context #539

Merged
merged 4 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
27 changes: 27 additions & 0 deletions packages/alchemy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
"import": "./dist/esm/index.js",
"default": "./dist/cjs/index.js"
},
"./config": {
"types": "./dist/types/config/index.d.ts",
"import": "./dist/esm/config/index.js",
"default": "./dist/cjs/config/index.js"
},
"./react": {
"types": "./dist/types/react/index.d.ts",
"import": "./dist/esm/react/index.js",
"default": "./dist/cjs/react/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
Expand All @@ -41,6 +51,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 +69,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
}
},
avasisht23 marked this conversation as resolved.
Show resolved Hide resolved
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
Expand Down
107 changes: 107 additions & 0 deletions packages/alchemy/src/config/actions/createAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
createLightAccount,
createMultiOwnerModularAccount,
type CreateLightAccountParams,
type CreateMultiOwnerModularAccountParams,
} from "@alchemy/aa-accounts";
import { custom } from "viem";
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"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, we shouldn't omit the signer here and instead make it optional. that would allow someone to create a session key signer based account

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not gonna do this now actually as it requires customizing signer in a lot of other places. opting for simplicity over flexibility / power in this first approach

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want session keys, we can always add a hook for that later

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want this to be with the CreateModularAccountParams, since we made CreateMultiOwnerModularAccountParams different from the base modular account?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah for now, I think we just want MultiOwner and LA and not MultiSig or other MA types.

>;

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

export async function createAccount<TAccount extends SupportedAccountTypes>(
{ type, accountParams: 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.isConnected) {
throw new Error("Signer not connected");
}

const cachedAccount = clientStore.getState().accounts[type];
if (cachedAccount?.account) {
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,
},
},
}));

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

return accountPromise;
}
22 changes: 22 additions & 0 deletions packages/alchemy/src/config/actions/getAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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>;

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;
};
10 changes: 10 additions & 0 deletions packages/alchemy/src/config/actions/getUser.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 getUser = (config: AlchemyAccountsConfig) => {
if (config.clientStore == null) {
throw new ClientOnlyPropertyError("user");
}

return config.clientStore.getState().user;
};
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 === "READY" &&
b?.status === "READY" &&
a.account.address === b.account.address) ||
a?.status === b?.status
moldy530 marked this conversation as resolved.
Show resolved Hide resolved
);
},
}
);
};
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 { ClientOnlyPropertyError } from "../errors.js";
import type { SignerStatus } from "../store/types.js";
import type { AlchemyAccountsConfig } from "../types";

export const watchSignerStatus =
(config: AlchemyAccountsConfig) =>
(onChange: (status: SignerStatus) => 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`);
}
}
14 changes: 14 additions & 0 deletions packages/alchemy/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export { createAccount } from "./actions/createAccount.js";
export { getAccount } from "./actions/getAccount.js";
export { getBundlerClient } from "./actions/getBundlerClient.js";
export { getSigner } from "./actions/getSigner.js";
export { getSignerStatus } from "./actions/getSignerStatus.js";
export { getUser } from "./actions/getUser.js";
export { watchAccount } from "./actions/watchAccount.js";
export { watchBundlerClient } from "./actions/watchBundlerClient.js";
export { watchSigner } from "./actions/watchSigner.js";
export { watchSignerStatus } from "./actions/watchSignerStatus.js";
export { watchUser } from "./actions/watchUser.js";
export { DEFAULT_IFRAME_CONTAINER_ID, createConfig } from "./createConfig.js";
export { defaultAccountState } from "./store/client.js";
export type * from "./types.js";
Loading
Loading