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 a useClientActions hook #571

Merged
merged 1 commit into from Apr 23, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
166 changes: 166 additions & 0 deletions packages/alchemy/src/react/hooks/useClientActions.ts
@@ -0,0 +1,166 @@
import { useMutation } from "@tanstack/react-query";
import { useCallback } from "react";
import type { Chain, Client, Transport } from "viem";
import type { SupportedAccounts } from "../../config";
import type { UseSmartAccountClientResult } from "./useSmartAccountClient";

export type UseClientActionsProps<
TTransport extends Transport = Transport,
TChain extends Chain | undefined = Chain | undefined,
TActions extends { [x: string]: (...args: any[]) => unknown } = {
[x: string]: (...args: any[]) => unknown;
}
> = {
client?: UseSmartAccountClientResult<
TTransport,
TChain,
SupportedAccounts
>["client"];
actions: (client: Client<TTransport, TChain, SupportedAccounts>) => TActions;
};

export type UseClientActionsResult<
TActions extends { [x: string]: (...args: any[]) => unknown } = {
[x: string]: (...args: any[]) => unknown;
}
> = {
executeAction: <TFunctionName extends ExecutableFunctionName<TActions>>(
params: ClientActionParameters<TActions, TFunctionName>
) => void;
executeActionAsync: <TFunctionName extends ExecutableFunctionName<TActions>>(
params: ClientActionParameters<TActions, TFunctionName>
) => Promise<ExecuteableFunctionResult<TFunctionName>>;
data: ReturnType<TActions[keyof TActions]> | undefined;
isExecutingAction: boolean;
error?: Error | null;
};

export type ExecutableFunctionName<
TActions extends { [x: string]: (...args: any[]) => unknown } = {
[x: string]: (...args: any[]) => unknown;
}
> = keyof TActions extends infer functionName extends string
? [functionName] extends [never]
? string
: functionName
: string;

export type ExecuteableFunctionResult<
TFunctionName extends ExecutableFunctionName<TActions>,
TActions extends { [x: string]: (...args: any[]) => unknown } = {
[x: string]: (...args: any[]) => unknown;
}
> = ReturnType<TActions[TFunctionName]>;

export type ExecutableFunctionArgs<
TActions extends { [x: string]: (...args: any[]) => unknown } = {
[x: string]: (...args: any[]) => unknown;
},
TFunctionName extends ExecutableFunctionName<TActions> = ExecutableFunctionName<TActions>
> = Parameters<TActions[TFunctionName]>;

// All of this is based one how viem's `encodeFunctionData` works
export type ClientActionParameters<
TActions extends { [x: string]: (...args: any[]) => unknown } = {
[x: string]: (...args: any[]) => unknown;
},
TFunctionName extends ExecutableFunctionName<TActions> = ExecutableFunctionName<TActions>,
allArgs = ExecutableFunctionArgs<
TActions,
TFunctionName extends ExecutableFunctionName<TActions>
? TFunctionName
: ExecutableFunctionName<TActions>
>
> = {
functionName: TFunctionName;
args: allArgs;
};
denniswon marked this conversation as resolved.
Show resolved Hide resolved

/**
* A hook that allows you to leverage client decorators to execute actions
* and await them in your UX. This is particularly useful for using Plugins
* with Modular Accounts.
*
* @example
* ```tsx
* const Foo = () => {
* const { client } = useSmartAccountClient({ type: "MultiOwnerModularAccount" });
* const { executePluginAction } = useClientActions({
* client,
* pluginActions: sessionKeyPluginActions,
* });
*
* executePluginAction({
* functionName: "isAccountSessionKey",
* args: [{ key: "0x0" }],
* });
* };
* ```
*/
export function useClientActions<
TTransport extends Transport = Transport,
TChain extends Chain | undefined = Chain | undefined,
TActions extends { [x: string]: (...args: any[]) => any } = {
[x: string]: (...args: any[]) => any;
}
>({
client,
actions,
}: UseClientActionsProps<
TTransport,
TChain,
TActions
>): UseClientActionsResult<TActions> {
const {
mutate,
isPending: isExecutingAction,
error,
mutateAsync,
data,
} = useMutation<
ReturnType<TActions[keyof TActions]>,
Error,
ClientActionParameters<TActions, ExecutableFunctionName<TActions>>
>({
mutationFn: async <TFunctionName extends ExecutableFunctionName<TActions>>({
functionName,
args,
}: ClientActionParameters<TActions, TFunctionName>) => {
if (!client) {
// TODO: use the strongly typed error here
denniswon marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("no client");
}

const actions_ = actions(client);
return actions_[functionName](...args);
},
});

const executeAction = useCallback(
<TFunctionName extends ExecutableFunctionName<TActions>>(
params: ClientActionParameters<TActions, TFunctionName>
) => {
const { functionName, args } = params;
return mutate({ functionName, args });
},
[mutate]
);

const executeActionAsync = useCallback(
async <TFunctionName extends ExecutableFunctionName<TActions>>(
params: ClientActionParameters<TActions, TFunctionName>
) => {
const { functionName, args } = params;
return mutateAsync({ functionName, args });
},
[mutateAsync]
);

return {
executeAction,
executeActionAsync,
data,
isExecutingAction,
error,
};
}
10 changes: 6 additions & 4 deletions packages/alchemy/src/react/index.ts
Expand Up @@ -13,6 +13,8 @@ export type * from "./hooks/useAuthenticate.js";
export { useAuthenticate } from "./hooks/useAuthenticate.js";
export type * from "./hooks/useBundlerClient.js";
export { useBundlerClient } from "./hooks/useBundlerClient.js";
export type * from "./hooks/useClientActions.js";
export { useClientActions } from "./hooks/useClientActions.js";
export type * from "./hooks/useDropAndReplaceUserOperation.js";
export { useDropAndReplaceUserOperation } from "./hooks/useDropAndReplaceUserOperation.js";
export type * from "./hooks/useExportAccount.js";
Expand All @@ -25,14 +27,14 @@ export type * from "./hooks/useSendTransactions.js";
export { useSendTransactions } from "./hooks/useSendTransactions.js";
export type * from "./hooks/useSendUserOperation.js";
export { useSendUserOperation } from "./hooks/useSendUserOperation.js";
export type * from "./hooks/useSignMessage.js";
export { useSignMessage } from "./hooks/useSignMessage.js";
export type * from "./hooks/useSignTypedData.js";
export { useSignTypedData } from "./hooks/useSignTypedData.js";
export type * from "./hooks/useSigner.js";
export { useSigner } from "./hooks/useSigner.js";
export type * from "./hooks/useSignerStatus.js";
export { useSignerStatus } from "./hooks/useSignerStatus.js";
export type * from "./hooks/useSignMessage.js";
export { useSignMessage } from "./hooks/useSignMessage.js";
export type * from "./hooks/useSignTypedData.js";
export { useSignTypedData } from "./hooks/useSignTypedData.js";
export type * from "./hooks/useSmartAccountClient.js";
export { useSmartAccountClient } from "./hooks/useSmartAccountClient.js";
export type * from "./hooks/useUser.js";
Expand Down
1 change: 1 addition & 0 deletions site/.vitepress/sidebar/index.ts
Expand Up @@ -131,6 +131,7 @@ export const sidebar: DefaultTheme.Sidebar = [
{ text: "createConfig", link: "/createConfig" },
{ text: "useAuthenticate", link: "/useAuthenticate" },
{ text: "useSmartAccountClient", link: "/useSmartAccountClient" },
{ text: "useClientActions", link: "/useClientActions" },
{ text: "useAccount", link: "/useAccount" },
{ text: "useSigner", link: "/useSigner" },
{ text: "useSignerStatus", link: "/useSignerStatus" },
Expand Down
91 changes: 91 additions & 0 deletions site/react/useClientActions.md
@@ -0,0 +1,91 @@
---
outline: deep
head:
- - meta
- property: og:title
content: useClientActions
- - meta
- name: description
content: An overview of the useClientActions hook
- - meta
- property: og:description
content: An overview of the useClientActions hook
- - meta
- name: twitter:title
content: useClientActions
- - meta
- name: twitter:description
content: An overview of the useClientActions hook
---

# useClientActions

## Import

```ts
import { useClientActions } from "@alchemy/aa-alchemy/react";
```

## Usage

<<< @/snippets/react/useClientActions.tsx

## Params

```ts
import { type UseClientActionsProps } from "@alchemy/aa-alchemy/react";
```

### client

The SmartAccountClient instance returned from `useSmartAccountClient`

### actions

a function that accepts as input the above client and returns an object containing functions that you can call to execute actions on the client.

<!--@include: ./BaseHookMutationArgs.md-->

## Return Type

```ts
import { type UseClientActionsResult } from "@alchemy/aa-alchemy/react";
```

### executeAction

A function that allows you to execute one of the client actions passed into the hook.

#### functionName

`string`
one of the functions passed into the hook

#### args

an array of the arguments to pass to the function

### executeActionAsync

A function that allows you to execute one of the client actions passed into the hook. It also returns a `Promise` containing the result of the executed action.

#### functionName

`string`
one of the functions passed into the hook

#### args

an array of the arguments to pass to the function

### data

The result of the executed action if one was executed

### isExecutingAction

A boolean that is true when an action is executing.

### error

An error object that is populated when the authentication fails.
32 changes: 32 additions & 0 deletions site/snippets/react/useClientActions.tsx
@@ -0,0 +1,32 @@
import { sessionKeyPluginActions } from "@alchemy/aa-accounts";
import {
useClientActions,
useSmartAccountClient,
} from "@alchemy/aa-alchemy/react";
import { zeroAddress } from "viem";

export function ComponentWithClientActions() {
const { client } = useSmartAccountClient({
type: "MultiOwnerModularAccount",
});
const { executeAction, data } = useClientActions({
client,
actions: sessionKeyPluginActions,
});

return (
<div>
<p>Is Session Key: {data != null ? !!data : "Click Button"}</p>
<button
onClick={() =>
executeAction({
functionName: "isAccountSessionKey",
args: [{ key: zeroAddress }],
})
}
>
Check Key
</button>
</div>
);
}