Skip to content

Commit

Permalink
feat: add alchemy accounts context (#539)
Browse files Browse the repository at this point in the history
* feat: add alchemy accounts context

* style: address PR feedback

Co-authored-by: Ajay Vasisht <43521356+avasisht23@users.noreply.github.com>

* fix: address a few PR comments

* docs(hooks): add initial react hooks docs (#561)

* docs(hooks): add initial react hooks docs

* refactor: apply suggestions from code review

Co-authored-by: Ajay Vasisht <43521356+avasisht23@users.noreply.github.com>

---------

Co-authored-by: Ajay Vasisht <43521356+avasisht23@users.noreply.github.com>

---------

Co-authored-by: Ajay Vasisht <43521356+avasisht23@users.noreply.github.com>
  • Loading branch information
moldy530 and avasisht23 authored Apr 10, 2024
1 parent 12c02e9 commit f92469e
Show file tree
Hide file tree
Showing 58 changed files with 1,782 additions and 8 deletions.
2 changes: 2 additions & 0 deletions packages/accounts/src/light-account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type LightAccount<
getOwnerAddress: () => Promise<Address>;
};

//#region CreateLightAccountParams
export type CreateLightAccountParams<
TTransport extends Transport = Transport,
TSigner extends SmartAccountSigner = SmartAccountSigner
Expand All @@ -52,6 +53,7 @@ export type CreateLightAccountParams<
initCode?: Hex;
version?: LightAccountVersion;
};
//#endregion CreateLightAccountParams

export async function createLightAccount<
TTransport extends Transport = Transport,
Expand Down
2 changes: 2 additions & 0 deletions packages/accounts/src/msca/account/multiOwnerAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type MultiOwnerModularAccount<
TSigner extends SmartAccountSigner = SmartAccountSigner
> = SmartContractAccountWithSigner<"MultiOwnerModularAccount", TSigner>;

// #region CreateMultiOwnerModularAccountParams
export type CreateMultiOwnerModularAccountParams<
TTransport extends Transport = Transport,
TSigner extends SmartAccountSigner = SmartAccountSigner
Expand All @@ -40,6 +41,7 @@ export type CreateMultiOwnerModularAccountParams<
accountAddress?: Address;
initCode?: Hex;
};
// #endregion CreateMultiOwnerModularAccountParams

export async function createMultiOwnerModularAccount<
TTransport extends Transport = Transport,
Expand Down
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
}
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
Expand Down
2 changes: 2 additions & 0 deletions packages/alchemy/src/client/smartAccountClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createAlchemySmartAccountClientFromRpcClient } from "./internal/smartAc
import { createAlchemyPublicRpcClient } from "./rpcClient.js";
import type { AlchemyRpcSchema } from "./types.js";

// #region AlchemySmartAccountClientConfig
export type AlchemySmartAccountClientConfig<
transport extends Transport = Transport,
chain extends Chain | undefined = Chain | undefined,
Expand All @@ -34,6 +35,7 @@ export type AlchemySmartAccountClientConfig<
SmartAccountClientConfig<transport, chain, account, context>,
"customMiddleware" | "feeEstimator" | "gasEstimator" | "signUserOperation"
>;
// #endregion AlchemySmartAccountClientConfig

export type BaseAlchemyActions<
chain extends Chain | undefined = Chain | undefined,
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"
>;

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

0 comments on commit f92469e

Please sign in to comment.