Skip to content

feat: add use send calls hook #1649

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

Draft
wants to merge 1 commit into
base: moldy/wallet-api-base
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions account-kit/core/src/createConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const createConfig = (
const config: AlchemyAccountsConfig = {
store: store,
accountCreationHint: params.accountCreationHint,
mode: params.mode ?? "local",
_internal: {
ssr,
createSigner: createSigner ?? createWebSigner, // <-- Default to web signer if not provided
Expand Down
25 changes: 10 additions & 15 deletions account-kit/core/src/experimental/actions/getSmartWalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
createSmartWalletClient,
type SmartWalletClient,
} from "@account-kit/wallet-client";
import type { Address, IsUndefined, JsonRpcAccount } from "viem";
import type { Address, JsonRpcAccount } from "viem";
import { getAlchemyTransport } from "../../actions/getAlchemyTransport.js";
import { getConnection } from "../../actions/getConnection.js";
import { getSigner } from "../../actions/getSigner.js";
Expand All @@ -11,23 +11,17 @@ import { SignerNotConnectedError } from "../../errors.js";
import type { AlchemyAccountsConfig } from "../../types.js";

export type GetSmartWalletClientResult<
TAccount extends JsonRpcAccount<Address> | undefined =
| JsonRpcAccount<`0x${string}`>
| undefined,
> = SmartWalletClient<TAccount>;
TAccount extends Address | undefined = Address | undefined,
> = SmartWalletClient<
TAccount extends Address ? JsonRpcAccount<TAccount> : undefined
>;

export type GetSmartWalletClientParams<
TAccount extends JsonRpcAccount<Address> | undefined =
| JsonRpcAccount<Address>
| undefined,
> = { mode?: "local" | "remote" } & (IsUndefined<TAccount> extends true
? { account?: never }
: { account: Address });
TAccount extends Address | undefined = Address | undefined,
> = { account?: TAccount };

export function getSmartWalletClient<
TAccount extends JsonRpcAccount<Address> | undefined =
| JsonRpcAccount<Address>
| undefined,
TAccount extends Address | undefined = Address | undefined,
>(
config: AlchemyAccountsConfig,
params?: GetSmartWalletClientParams<TAccount>,
Expand Down Expand Up @@ -64,8 +58,9 @@ export function getSmartWalletClient(
transport,
chain: connection.chain,
signer,
// @ts-expect-error need to fix this in the wallet-client sdk
account: params?.account,
mode: params?.mode ?? "local",
mode: config.mode,
});

config.store.setState((state) => ({
Expand Down
2 changes: 2 additions & 0 deletions account-kit/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type SupportedAccount<T extends SupportedAccountTypes> =
export type AlchemyAccountsConfig = {
store: Store;
accountCreationHint?: CreateConfigProps["accountCreationHint"];
mode: "local" | "remote";
_internal: {
// if not provided, the default signer will be used
createSigner: (config: ClientStoreConfig) => AlchemySigner;
Expand Down Expand Up @@ -142,6 +143,7 @@ export type BaseCreateConfigProps = RpcConnectionConfig & {
accountCreationHint?: NonNullable<
Parameters<SmartWalletClient["requestAccount"]>[0]
>["creationHint"];
mode?: "local" | "remote";

/**
* If set, calls `preparePopupOauth` immediately upon initializing the signer.
Expand Down
141 changes: 141 additions & 0 deletions account-kit/react/src/experimental/hooks/useSendCalls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
clientHeaderTrack,
type EntryPointVersion,
type UserOperationRequest,
} from "@aa-sdk/core";
import type { GetSmartWalletClientResult } from "@account-kit/core/experimental";
import type { SmartWalletClient } from "@account-kit/wallet-client";
import {
useMutation,
type UseMutateAsyncFunction,
type UseMutateFunction,
} from "@tanstack/react-query";
import { sendTransaction as wagmi_sendTransaction } from "@wagmi/core";
import { fromHex, type Address, type Hex, type JsonRpcAccount } from "viem";
import { useAccount as wagmi_useAccount } from "wagmi";
import {
ClientUndefinedHookError,
UnsupportedEOAActionError,
} from "../../errors.js";
import { useAlchemyAccountContext } from "../../hooks/useAlchemyAccountContext.js";
import { ReactLogger } from "../../metrics.js";

export type UseSendCallsParams = {
client?: GetSmartWalletClientResult<Address>;
};

type MutationResult<
TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
> = {
id: Hex;
// TODO: deprecate this
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... not sure if we should deprecate this. we will need it for drop and replace unless we come up with a better API for drop and replace

request?: UserOperationRequest<TEntryPointVersion>;
};

export type UseSendCallsResult<
TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
> = {
sendCalls: UseMutateFunction<
MutationResult<TEntryPointVersion>,
Error,
Parameters<SmartWalletClient<JsonRpcAccount<Address>>["prepareCalls"]>[0],
unknown
>;
sendCallsAsync: UseMutateAsyncFunction<
MutationResult<TEntryPointVersion>,
Error,
Parameters<SmartWalletClient<JsonRpcAccount<Address>>["prepareCalls"]>[0],
unknown
>;
sendCallsResult: MutationResult | undefined;
isSendingCalls: boolean;
sendCallsError: Error | null;
};

// TODO: remove the entrypoint version generic when we don't need it anymore
export function useSendCalls<
TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
>(params: UseSendCallsParams): UseSendCallsResult<TEntryPointVersion> {
const { client: _client } = params;
const {
queryClient,
config: {
_internal: { wagmiConfig },
},
} = useAlchemyAccountContext();
const { isConnected } = wagmi_useAccount({ config: wagmiConfig });

const {
mutate: sendCalls,
mutateAsync: sendCallsAsync,
data: sendCallsResult,
isPending: isSendingCalls,
error: sendCallsError,
} = useMutation(
{
mutationFn: async (
params: Parameters<
SmartWalletClient<JsonRpcAccount<Address>>["prepareCalls"]
>[0],
): Promise<MutationResult<TEntryPointVersion>> => {
if (isConnected) {
console.warn(
"useSendCalls: connected to an EOA, sending as a transaction instead",
);
if (params.calls.length !== 1) {
throw new UnsupportedEOAActionError(
"useSendCalls",
"batch execute",
);
}

const [call] = params.calls;

const tx = await wagmi_sendTransaction(wagmiConfig, {
to: call.to,
data: call.data,
value: call.value ? fromHex(call.value, "bigint") : undefined,
});

return {
id: tx,
};
}

if (!_client) {
throw new ClientUndefinedHookError("useSendCalls");
}

const client = clientHeaderTrack(_client, "reactUseSendCalls");

const preparedCalls = await client.prepareCalls(params);

const signature = await client.signSignatureRequest(
preparedCalls.signatureRequest,
);

const { preparedCallIds } = await client.sendPreparedCalls({
...preparedCalls,
signature,
});
Comment on lines +113 to +120
Copy link
Contributor

Choose a reason for hiding this comment

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

i assume we are going to need to handle the 7702 stuff in here too, and include the eip7702Auth in the returned request below.

Copy link
Contributor

@jakehobbs jakehobbs Jun 2, 2025

Choose a reason for hiding this comment

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

if we end up doing the "multi" signature type though, i think it would work w/ no changes here other than how we construct the returned request: https://github.com/alchemyplatform/wallet-server/pull/187/files#diff-f9efb2f119f513d936133e68e1f0faa125d4b2b5400ec33cbc170c5ba4fcc406R46


return {
id: preparedCallIds[0],
Copy link
Contributor

Choose a reason for hiding this comment

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

this might be a problem if we introduce sending multiple UOs in one request, as proposed here: https://github.com/alchemyplatform/wallet-server/pull/199/files#diff-c00cc16915cfc4387914060aa01512d771fe33d56927f69c95111ceebebc45abR123

request: {
...preparedCalls.data,
signature: signature.signature,
} as UserOperationRequest<TEntryPointVersion>,
};
},
},
queryClient,
);

return {
sendCalls: ReactLogger.profiled("sendCalls", sendCalls),
sendCallsAsync: ReactLogger.profiled("sendCallsAsync", sendCallsAsync),
sendCallsResult,
isSendingCalls,
sendCallsError,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
type GetSmartWalletClientParams,
} from "@account-kit/core/experimental";
import { useMemo, useSyncExternalStore } from "react";
import type { Address } from "viem";
import { useAccount as wagmi_useAccount } from "wagmi";
import { useAlchemyAccountContext } from "../../hooks/useAlchemyAccountContext.js";

export function useSmartWalletClient(params: GetSmartWalletClientParams) {
export function useSmartWalletClient<
TAccount extends Address | undefined = Address | undefined,
>(params: GetSmartWalletClientParams<TAccount>) {
const {
config: {
_internal: { wagmiConfig },
Expand Down
83 changes: 40 additions & 43 deletions account-kit/react/src/hooks/useSendUserOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@ import type {
SendUserOperationParameters,
SendUserOperationResult,
} from "@aa-sdk/core";
import { WaitForUserOperationError, clientHeaderTrack } from "@aa-sdk/core";
import { WaitForUserOperationError } from "@aa-sdk/core";
import type { SupportedAccounts } from "@account-kit/core";
import {
useMutation,
type UseMutateAsyncFunction,
type UseMutateFunction,
} from "@tanstack/react-query";
import { sendTransaction as wagmi_sendTransaction } from "@wagmi/core";
import type { Hex } from "viem";
import { slice, toHex, type Hex } from "viem";
import { useAccount as wagmi_useAccount } from "wagmi";
import { useAlchemyAccountContext } from "./useAlchemyAccountContext.js";
import {
ClientUndefinedHookError,
UnsupportedEOAActionError,
} from "../errors.js";
import { ClientUndefinedHookError } from "../errors.js";
import { useSendCalls } from "../experimental/hooks/useSendCalls.js";
import { useSmartWalletClient } from "../experimental/hooks/useSmartWalletClient.js";
import { ReactLogger } from "../metrics.js";
import type { BaseHookMutationArgs } from "../types.js";
import { useAlchemyAccountContext } from "./useAlchemyAccountContext.js";
import { type UseSmartAccountClientResult } from "./useSmartAccountClient.js";

export type SendUserOperationWithEOA<
Expand Down Expand Up @@ -133,6 +131,13 @@ export function useSendUserOperation<
params: UseSendUserOperationArgs<TEntryPointVersion, TAccount>,
): UseSendUserOperationResult<TEntryPointVersion, TAccount> {
const { client: _client, waitForTxn = false, ...mutationArgs } = params;
const smartWalletClient = useSmartWalletClient({
account: params.client?.account.address,
});

const { sendCallsAsync } = useSendCalls<TEntryPointVersion>({
client: smartWalletClient,
});

const {
queryClient,
Expand All @@ -151,56 +156,48 @@ export function useSendUserOperation<
} = useMutation(
{
mutationFn: async (params: SendUserOperationParameters<TAccount>) => {
if (typeof params.uo === "string") {
throw new Error("need to support hex calls probably");
}

const { id, request } = await sendCallsAsync({
calls: (Array.isArray(params.uo) ? params.uo : [params.uo]).map(
(x) => ({
to: x.target,
data: x.data,
value: x.value ? toHex(x.value) : undefined,
}),
),
});

if (isConnected) {
console.warn(
"useSendUserOperation: connected to an EOA, sending as a transaction instead",
);
const { uo } = params;

if (Array.isArray(uo)) {
throw new UnsupportedEOAActionError(
"useSendUserOperation",
"batch execute",
);
}

if (typeof uo === "string") {
throw new UnsupportedEOAActionError(
"useSendUserOperation",
"hex user operation",
);
}

const tx = await wagmi_sendTransaction(wagmiConfig, {
to: uo.target,
data: uo.data,
value: uo.value,
});
return {
hash: id,
};
}

const uoHash = slice(id, 32);
if (!waitForTxn) {
return {
hash: tx,
hash: uoHash,
request: request!,
};
}

if (!_client) {
throw new ClientUndefinedHookError("useSendUserOperation");
}
const client = clientHeaderTrack(_client, "reactUseSendUserOperation");

if (!waitForTxn) {
return client.sendUserOperation(params);
}

const { hash, request } = await client.sendUserOperation(params);
const txnHash = await client
.waitForUserOperationTransaction({ hash })
// TODO: this should really use useCallsStatusHook instead (once it exists)
const txnHash = await _client
.waitForUserOperationTransaction({ hash: uoHash })
.catch((e) => {
throw new WaitForUserOperationError(request, e);
throw new WaitForUserOperationError(request!, e);
});

return {
hash: txnHash,
request,
request: request!,
};
},
...mutationArgs,
Expand Down
Loading