Send User Operations
Once your users have been authenticated, you can start sending user operations! Account Kit makes it really easy to send user operations using React hooks.
Sending user operations is pretty straightforward. All you have to do is use the useSendUserOperation()
hook from the @account-kit/react-native
package.
If you want to sponsor the gas for a user, see our guide.
Single user operation
In the below example, we use ModularAccountV2
as the underlying Smart
Contract type. You can also use a different account type (see other options
here).
import React, { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
useState, function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect } from "react";
import { type Alert = AlertStatic
const Alert: AlertStaticAlert, class ViewView, class ButtonButton } from "react-native";
import { type User = {
email?: string;
orgId: string;
userId: string;
address: Address;
solanaAddress?: string;
credentialId?: string;
idToken?: string;
claims?: Record<string, unknown>;
}User } from "@account-kit/signer";
import {
function createModularAccountV2Client<TChain extends Chain = Chain, TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(args: CreateModularAccountV2AlchemyClientParams<AlchemyTransport, TChain, TSigner>): Promise<ModularAccountV2Client<TSigner, TChain, AlchemyTransport>> (+1 overload)createModularAccountV2Client,
type ModularAccountV2<TSigner extends SmartAccountSigner = SmartAccountSigner<any>> = {
address: Address;
nonceManager?: NonceManager | undefined;
sign?: ((parameters: {
hash: Hash;
}) => Promise<Hex>) | undefined | undefined;
... 6 more ...;
type: "local";
} & {
...;
} & {
...;
} & {
...;
}ModularAccountV2,
} from "@account-kit/smart-contracts";
import {
function useSendUserOperation<TEntryPointVersion extends GetEntryPointFromAccount<TAccount>, TAccount extends SupportedAccounts = SupportedAccounts>(params: UseSendUserOperationArgs<TEntryPointVersion, TAccount>): UseSendUserOperationResult<TEntryPointVersion, TAccount>A hook that returns functions for sending user operations. You can also optionally wait for a user operation to be mined and get the transaction hash before returning using waitForTx
. Like any method that takes a smart account client, throws an error if client undefined or is signer not authenticated.
useSendUserOperation,
function useSmartAccountClient<TChain extends Chain | undefined = Chain | undefined, TAccount extends SupportedAccountTypes | undefined = "ModularAccountV2">(args: UseSmartAccountClientProps<TChain, TAccount>): UseSmartAccountClientResult<TChain, SupportedAccount<TAccount extends undefined ? "ModularAccountV2" : TAccount>>useSmartAccountClient,
} from "@account-kit/react-native";
import { const sepolia: Chainsepolia, function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy } from "@account-kit/infra";
import { type SmartAccountClient<transport extends Transport = Transport, chain extends Chain | undefined = Chain | undefined, account extends SmartContractAccount | undefined = SmartContractAccount | undefined, actions extends Record<string, unknown> = Record<...>, rpcSchema extends RpcSchema = [...], context extends UserOperationContext | undefined = UserOperationContext | undefined> = { [K in keyof Client<transport, chain, account, rpcSchema, actions & {
buildUserOperation: (args: BuildUserOperationParameters<account, context>) => Promise<...>;
... 11 more ...;
signTypedData: <const TTypedData extends {
...;
} | {
...;
}, TPrimaryType extends string = string>(args: SignTypedDataParameters<...>) => Promise<Hex>;
} & (IsUndefined<...> extends false ? {
...;
} : {
...;
}) & BundlerActions & PublicActions>]: Client<...>[K]; }SmartAccountClient } from "@aa-sdk/core";
export default function function MyOpSenderComponent(): JSX.ElementMyOpSenderComponent() {
const { const client: {
account: ModularAccountV2<AlchemySigner>;
batch?: {
multicall?: boolean | Prettify<MulticallBatchOptions> | undefined;
} | undefined;
... 84 more ...;
extend: <const client extends {
...;
} & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>;
} | undefinedclient } = useSmartAccountClient<Chain | undefined, "ModularAccountV2">(args: UseSmartAccountClientProps<Chain | undefined, "ModularAccountV2">): UseSmartAccountClientResult<Chain | undefined, ModularAccountV2<...>>useSmartAccountClient({});
const { const sendUserOperation: UseMutateFunction<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<SupportedAccounts>, unknown>sendUserOperation, const isSendingUserOperation: booleanisSendingUserOperation } = useSendUserOperation<keyof EntryPointRegistryBase<unknown>, SupportedAccounts>(params: UseSendUserOperationArgs<keyof EntryPointRegistryBase<unknown>, SupportedAccounts>): UseSendUserOperationResult<...>A hook that returns functions for sending user operations. You can also optionally wait for a user operation to be mined and get the transaction hash before returning using waitForTx
. Like any method that takes a smart account client, throws an error if client undefined or is signer not authenticated.
useSendUserOperation({
client: {
account: SupportedAccounts;
batch?: {
multicall?: boolean | Prettify<MulticallBatchOptions> | undefined;
} | undefined;
... 84 more ...;
extend: <const client extends {
...;
} & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>;
} | {
...;
} | {
...;
} | {
...;
} | undefinedclient,
waitForTxn?: boolean | undefinedwaitForTxn: true,
onSuccess?: ((data: SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, variables: SendUserOperationParameters<SupportedAccounts>, context: unknown) => Promise<unknown> | unknown) | undefinedonSuccess: ({ hash: `0x${string}`hash, request: UserOperationRequest<keyof EntryPointRegistryBase<unknown>> | undefinedrequest }) => {
// [optional] Do something with the hash and request
},
onError?: ((error: Error, variables: SendUserOperationParameters<SupportedAccounts>, context: unknown) => Promise<unknown> | unknown) | undefinedonError: (error: Errorerror) => {
// [optional] Do something with the error
},
});
const const handleSendUserOperation: () => voidhandleSendUserOperation = () => {
const sendUserOperation: (variables: SendUserOperationParameters<SupportedAccounts>, options?: MutateOptions<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<...>, unknown> | undefined) => voidsendUserOperation({
uo: UserOperationCallData | BatchUserOperationCallDatauo: {
target: `0x${string}`target: "0xTARGET_ADDRESS",
data: `0x${string}`data: "0x",
value?: bigint | undefinedvalue: 0n,
},
});
};
return (
<class ViewView>
<class ButtonButton
onPress?: ((event: GestureResponderEvent) => void) | undefinedCalled when the touch is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
onPress={const handleSendUserOperation: () => voidhandleSendUserOperation}
disabled?: boolean | undefinedIf true, disable all interactions for this component.
disabled={const isSendingUserOperation: booleanisSendingUserOperation}
title: stringText to display inside the button. On Android the given title will be converted to the uppercased form.
title={const isSendingUserOperation: booleanisSendingUserOperation ? "Sending..." : "Send UO"}
/>
</class ViewView>
);
}
Batch user operation
To send multiple user operations in a single call, simply pass an array of user operations to the sendUserOperation
method.
import React, { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
useState, function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect } from "react";
import { type Alert = AlertStatic
const Alert: AlertStaticAlert, class ViewView, class ButtonButton } from "react-native";
import { type User = {
email?: string;
orgId: string;
userId: string;
address: Address;
solanaAddress?: string;
credentialId?: string;
idToken?: string;
claims?: Record<string, unknown>;
}User } from "@account-kit/signer";
import {
function createModularAccountV2Client<TChain extends Chain = Chain, TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(args: CreateModularAccountV2AlchemyClientParams<AlchemyTransport, TChain, TSigner>): Promise<ModularAccountV2Client<TSigner, TChain, AlchemyTransport>> (+1 overload)createModularAccountV2Client,
type ModularAccountV2<TSigner extends SmartAccountSigner = SmartAccountSigner<any>> = {
address: Address;
nonceManager?: NonceManager | undefined;
sign?: ((parameters: {
hash: Hash;
}) => Promise<Hex>) | undefined | undefined;
... 6 more ...;
type: "local";
} & {
...;
} & {
...;
} & {
...;
}ModularAccountV2,
} from "@account-kit/smart-contracts";
import {
function useSendUserOperation<TEntryPointVersion extends GetEntryPointFromAccount<TAccount>, TAccount extends SupportedAccounts = SupportedAccounts>(params: UseSendUserOperationArgs<TEntryPointVersion, TAccount>): UseSendUserOperationResult<TEntryPointVersion, TAccount>A hook that returns functions for sending user operations. You can also optionally wait for a user operation to be mined and get the transaction hash before returning using waitForTx
. Like any method that takes a smart account client, throws an error if client undefined or is signer not authenticated.
useSendUserOperation,
function useSmartAccountClient<TChain extends Chain | undefined = Chain | undefined, TAccount extends SupportedAccountTypes | undefined = "ModularAccountV2">(args: UseSmartAccountClientProps<TChain, TAccount>): UseSmartAccountClientResult<TChain, SupportedAccount<TAccount extends undefined ? "ModularAccountV2" : TAccount>>useSmartAccountClient,
} from "@account-kit/react-native";
import { const sepolia: Chainsepolia, function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy } from "@account-kit/infra";
import { type SmartAccountClient<transport extends Transport = Transport, chain extends Chain | undefined = Chain | undefined, account extends SmartContractAccount | undefined = SmartContractAccount | undefined, actions extends Record<string, unknown> = Record<...>, rpcSchema extends RpcSchema = [...], context extends UserOperationContext | undefined = UserOperationContext | undefined> = { [K in keyof Client<transport, chain, account, rpcSchema, actions & {
buildUserOperation: (args: BuildUserOperationParameters<account, context>) => Promise<...>;
... 11 more ...;
signTypedData: <const TTypedData extends {
...;
} | {
...;
}, TPrimaryType extends string = string>(args: SignTypedDataParameters<...>) => Promise<Hex>;
} & (IsUndefined<...> extends false ? {
...;
} : {
...;
}) & BundlerActions & PublicActions>]: Client<...>[K]; }SmartAccountClient } from "@aa-sdk/core";
export default function function MyOpSenderComponent(): JSX.ElementMyOpSenderComponent() {
const { const client: {
account: ModularAccountV2<AlchemySigner>;
batch?: {
multicall?: boolean | Prettify<MulticallBatchOptions> | undefined;
} | undefined;
... 84 more ...;
extend: <const client extends {
...;
} & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>;
} | undefinedclient } = useSmartAccountClient<Chain | undefined, "ModularAccountV2">(args: UseSmartAccountClientProps<Chain | undefined, "ModularAccountV2">): UseSmartAccountClientResult<Chain | undefined, ModularAccountV2<...>>useSmartAccountClient({});
const { const sendUserOperation: UseMutateFunction<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<SupportedAccounts>, unknown>sendUserOperation, const isSendingUserOperation: booleanisSendingUserOperation } = useSendUserOperation<keyof EntryPointRegistryBase<unknown>, SupportedAccounts>(params: UseSendUserOperationArgs<keyof EntryPointRegistryBase<unknown>, SupportedAccounts>): UseSendUserOperationResult<...>A hook that returns functions for sending user operations. You can also optionally wait for a user operation to be mined and get the transaction hash before returning using waitForTx
. Like any method that takes a smart account client, throws an error if client undefined or is signer not authenticated.
useSendUserOperation({
client: {
account: SupportedAccounts;
batch?: {
multicall?: boolean | Prettify<MulticallBatchOptions> | undefined;
} | undefined;
... 84 more ...;
extend: <const client extends {
...;
} & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>;
} | {
...;
} | {
...;
} | {
...;
} | undefinedclient,
waitForTxn?: boolean | undefinedwaitForTxn: true,
onSuccess?: ((data: SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, variables: SendUserOperationParameters<SupportedAccounts>, context: unknown) => Promise<unknown> | unknown) | undefinedonSuccess: ({ hash: `0x${string}`hash, request: UserOperationRequest<keyof EntryPointRegistryBase<unknown>> | undefinedrequest }) => {
// [optional] Do something with the hash and request
},
onError?: ((error: Error, variables: SendUserOperationParameters<SupportedAccounts>, context: unknown) => Promise<unknown> | unknown) | undefinedonError: (error: Errorerror) => {
// [optional] Do something with the error
},
});
const const handleSendBatchUserOperations: () => voidhandleSendBatchUserOperations = () => {
const sendUserOperation: (variables: SendUserOperationParameters<SupportedAccounts>, options?: MutateOptions<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<...>, unknown> | undefined) => voidsendUserOperation({
uo: UserOperationCallData | BatchUserOperationCallDatauo: [
{
target: `0x${string}`target: "0xTARGET_ADDRESS",
data: `0x${string}`data: "0x",
value?: bigint | undefinedvalue: 0n,
},
{
target: `0x${string}`target: "0xTARGET_ADDRESS",
data: `0x${string}`data: "0x",
value?: bigint | undefinedvalue: 0n,
},
],
});
};
return (
<class ViewView>
<class ButtonButton
onPress?: ((event: GestureResponderEvent) => void) | undefinedCalled when the touch is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
onPress={const handleSendBatchUserOperations: () => voidhandleSendBatchUserOperations}
disabled?: boolean | undefinedIf true, disable all interactions for this component.
disabled={const isSendingUserOperation: booleanisSendingUserOperation}
title: stringText to display inside the button. On Android the given title will be converted to the uppercased form.
title={const isSendingUserOperation: booleanisSendingUserOperation ? "Sending..." : "Send UO"}
/>
</class ViewView>
);
}