diff --git a/account-kit/smart-contracts/src/msca/plugins/multi-owner/plugin.ts b/account-kit/smart-contracts/src/msca/plugins/multi-owner/plugin.ts index 91cf9f321f..45db31a09b 100644 --- a/account-kit/smart-contracts/src/msca/plugins/multi-owner/plugin.ts +++ b/account-kit/smart-contracts/src/msca/plugins/multi-owner/plugin.ts @@ -340,16 +340,8 @@ export const MultiOwnerPluginExecutionFunctionAbi = [ type: "function", name: "updateOwners", inputs: [ - { - name: "ownersToAdd", - type: "address[]", - internalType: "address[]", - }, - { - name: "ownersToRemove", - type: "address[]", - internalType: "address[]", - }, + { name: "ownersToAdd", type: "address[]", internalType: "address[]" }, + { name: "ownersToRemove", type: "address[]", internalType: "address[]" }, ], outputs: [], stateMutability: "nonpayable", @@ -363,17 +355,9 @@ export const MultiOwnerPluginExecutionFunctionAbi = [ { name: "name", type: "string", internalType: "string" }, { name: "version", type: "string", internalType: "string" }, { name: "chainId", type: "uint256", internalType: "uint256" }, - { - name: "verifyingContract", - type: "address", - internalType: "address", - }, + { name: "verifyingContract", type: "address", internalType: "address" }, { name: "salt", type: "bytes32", internalType: "bytes32" }, - { - name: "extensions", - type: "uint256[]", - internalType: "uint256[]", - }, + { name: "extensions", type: "uint256[]", internalType: "uint256[]" }, ], stateMutability: "view", }, @@ -399,17 +383,9 @@ export const MultiOwnerPluginAbi = [ { name: "name", type: "string", internalType: "string" }, { name: "version", type: "string", internalType: "string" }, { name: "chainId", type: "uint256", internalType: "uint256" }, - { - name: "verifyingContract", - type: "address", - internalType: "address", - }, + { name: "verifyingContract", type: "address", internalType: "address" }, { name: "salt", type: "bytes32", internalType: "bytes32" }, - { - name: "extensions", - type: "uint256[]", - internalType: "uint256[]", - }, + { name: "extensions", type: "uint256[]", internalType: "uint256[]" }, ], stateMutability: "view", }, @@ -484,11 +460,7 @@ export const MultiOwnerPluginAbi = [ type: "tuple", internalType: "struct PluginManifest", components: [ - { - name: "interfaceIds", - type: "bytes4[]", - internalType: "bytes4[]", - }, + { name: "interfaceIds", type: "bytes4[]", internalType: "bytes4[]" }, { name: "dependencyInterfaceIds", type: "bytes4[]", @@ -509,11 +481,7 @@ export const MultiOwnerPluginAbi = [ type: "bool", internalType: "bool", }, - { - name: "canSpendNativeToken", - type: "bool", - internalType: "bool", - }, + { name: "canSpendNativeToken", type: "bool", internalType: "bool" }, { name: "permittedExternalCalls", type: "tuple[]", @@ -524,16 +492,8 @@ export const MultiOwnerPluginAbi = [ type: "address", internalType: "address", }, - { - name: "permitAnySelector", - type: "bool", - internalType: "bool", - }, - { - name: "selectors", - type: "bytes4[]", - internalType: "bytes4[]", - }, + { name: "permitAnySelector", type: "bool", internalType: "bool" }, + { name: "selectors", type: "bytes4[]", internalType: "bytes4[]" }, ], }, { @@ -556,11 +516,7 @@ export const MultiOwnerPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -590,11 +546,7 @@ export const MultiOwnerPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -624,11 +576,7 @@ export const MultiOwnerPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -658,11 +606,7 @@ export const MultiOwnerPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -692,11 +636,7 @@ export const MultiOwnerPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -714,11 +654,7 @@ export const MultiOwnerPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -812,19 +748,11 @@ export const MultiOwnerPluginAbi = [ type: "tuple", internalType: "struct UserOperation", components: [ - { - name: "sender", - type: "address", - internalType: "address", - }, + { name: "sender", type: "address", internalType: "address" }, { name: "nonce", type: "uint256", internalType: "uint256" }, { name: "initCode", type: "bytes", internalType: "bytes" }, { name: "callData", type: "bytes", internalType: "bytes" }, - { - name: "callGasLimit", - type: "uint256", - internalType: "uint256", - }, + { name: "callGasLimit", type: "uint256", internalType: "uint256" }, { name: "verificationGasLimit", type: "uint256", @@ -835,21 +763,13 @@ export const MultiOwnerPluginAbi = [ type: "uint256", internalType: "uint256", }, - { - name: "maxFeePerGas", - type: "uint256", - internalType: "uint256", - }, + { name: "maxFeePerGas", type: "uint256", internalType: "uint256" }, { name: "maxPriorityFeePerGas", type: "uint256", internalType: "uint256", }, - { - name: "paymasterAndData", - type: "bytes", - internalType: "bytes", - }, + { name: "paymasterAndData", type: "bytes", internalType: "bytes" }, { name: "signature", type: "bytes", internalType: "bytes" }, ], }, @@ -881,16 +801,8 @@ export const MultiOwnerPluginAbi = [ type: "function", name: "updateOwners", inputs: [ - { - name: "ownersToAdd", - type: "address[]", - internalType: "address[]", - }, - { - name: "ownersToRemove", - type: "address[]", - internalType: "address[]", - }, + { name: "ownersToAdd", type: "address[]", internalType: "address[]" }, + { name: "ownersToRemove", type: "address[]", internalType: "address[]" }, ], outputs: [], stateMutability: "nonpayable", @@ -905,19 +817,11 @@ export const MultiOwnerPluginAbi = [ type: "tuple", internalType: "struct UserOperation", components: [ - { - name: "sender", - type: "address", - internalType: "address", - }, + { name: "sender", type: "address", internalType: "address" }, { name: "nonce", type: "uint256", internalType: "uint256" }, { name: "initCode", type: "bytes", internalType: "bytes" }, { name: "callData", type: "bytes", internalType: "bytes" }, - { - name: "callGasLimit", - type: "uint256", - internalType: "uint256", - }, + { name: "callGasLimit", type: "uint256", internalType: "uint256" }, { name: "verificationGasLimit", type: "uint256", @@ -928,21 +832,13 @@ export const MultiOwnerPluginAbi = [ type: "uint256", internalType: "uint256", }, - { - name: "maxFeePerGas", - type: "uint256", - internalType: "uint256", - }, + { name: "maxFeePerGas", type: "uint256", internalType: "uint256" }, { name: "maxPriorityFeePerGas", type: "uint256", internalType: "uint256", }, - { - name: "paymasterAndData", - type: "bytes", - internalType: "bytes", - }, + { name: "paymasterAndData", type: "bytes", internalType: "bytes" }, { name: "signature", type: "bytes", internalType: "bytes" }, ], }, diff --git a/account-kit/smart-contracts/src/msca/plugins/session-key/plugin.ts b/account-kit/smart-contracts/src/msca/plugins/session-key/plugin.ts index a57ab60fd7..fea9ce6730 100644 --- a/account-kit/smart-contracts/src/msca/plugins/session-key/plugin.ts +++ b/account-kit/smart-contracts/src/msca/plugins/session-key/plugin.ts @@ -481,11 +481,7 @@ export const SessionKeyPluginExecutionFunctionAbi = [ type: "tuple[]", internalType: "struct Call[]", components: [ - { - name: "target", - type: "address", - internalType: "address", - }, + { name: "target", type: "address", internalType: "address" }, { name: "value", type: "uint256", internalType: "uint256" }, { name: "data", type: "bytes", internalType: "bytes" }, ], @@ -501,11 +497,7 @@ export const SessionKeyPluginExecutionFunctionAbi = [ inputs: [ { name: "sessionKey", type: "address", internalType: "address" }, { name: "tag", type: "bytes32", internalType: "bytes32" }, - { - name: "permissionUpdates", - type: "bytes[]", - internalType: "bytes[]", - }, + { name: "permissionUpdates", type: "bytes[]", internalType: "bytes[]" }, ], outputs: [], stateMutability: "nonpayable", @@ -550,11 +542,7 @@ export const SessionKeyPluginAbi = [ inputs: [ { name: "sessionKey", type: "address", internalType: "address" }, { name: "tag", type: "bytes32", internalType: "bytes32" }, - { - name: "permissionUpdates", - type: "bytes[]", - internalType: "bytes[]", - }, + { name: "permissionUpdates", type: "bytes[]", internalType: "bytes[]" }, ], outputs: [], stateMutability: "nonpayable", @@ -568,11 +556,7 @@ export const SessionKeyPluginAbi = [ type: "tuple[]", internalType: "struct Call[]", components: [ - { - name: "target", - type: "address", - internalType: "address", - }, + { name: "target", type: "address", internalType: "address" }, { name: "value", type: "uint256", internalType: "uint256" }, { name: "data", type: "bytes", internalType: "bytes" }, ], @@ -598,11 +582,7 @@ export const SessionKeyPluginAbi = [ inputs: [ { name: "account", type: "address", internalType: "address" }, { name: "sessionKey", type: "address", internalType: "address" }, - { - name: "contractAddress", - type: "address", - internalType: "address", - }, + { name: "contractAddress", type: "address", internalType: "address" }, ], outputs: [ { name: "isOnList", type: "bool", internalType: "bool" }, @@ -642,21 +622,9 @@ export const SessionKeyPluginAbi = [ components: [ { name: "hasLimit", type: "bool", internalType: "bool" }, { name: "limit", type: "uint256", internalType: "uint256" }, - { - name: "limitUsed", - type: "uint256", - internalType: "uint256", - }, - { - name: "refreshInterval", - type: "uint48", - internalType: "uint48", - }, - { - name: "lastUsedTime", - type: "uint48", - internalType: "uint48", - }, + { name: "limitUsed", type: "uint256", internalType: "uint256" }, + { name: "refreshInterval", type: "uint48", internalType: "uint48" }, + { name: "lastUsedTime", type: "uint48", internalType: "uint48" }, ], }, ], @@ -677,21 +645,9 @@ export const SessionKeyPluginAbi = [ components: [ { name: "hasLimit", type: "bool", internalType: "bool" }, { name: "limit", type: "uint256", internalType: "uint256" }, - { - name: "limitUsed", - type: "uint256", - internalType: "uint256", - }, - { - name: "refreshInterval", - type: "uint48", - internalType: "uint48", - }, - { - name: "lastUsedTime", - type: "uint48", - internalType: "uint48", - }, + { name: "limitUsed", type: "uint256", internalType: "uint256" }, + { name: "refreshInterval", type: "uint48", internalType: "uint48" }, + { name: "lastUsedTime", type: "uint48", internalType: "uint48" }, ], }, { name: "shouldReset", type: "bool", internalType: "bool" }, @@ -726,21 +682,9 @@ export const SessionKeyPluginAbi = [ components: [ { name: "hasLimit", type: "bool", internalType: "bool" }, { name: "limit", type: "uint256", internalType: "uint256" }, - { - name: "limitUsed", - type: "uint256", - internalType: "uint256", - }, - { - name: "refreshInterval", - type: "uint48", - internalType: "uint48", - }, - { - name: "lastUsedTime", - type: "uint48", - internalType: "uint48", - }, + { name: "limitUsed", type: "uint256", internalType: "uint256" }, + { name: "refreshInterval", type: "uint48", internalType: "uint48" }, + { name: "lastUsedTime", type: "uint48", internalType: "uint48" }, ], }, ], @@ -762,11 +706,7 @@ export const SessionKeyPluginAbi = [ inputs: [ { name: "account", type: "address", internalType: "address" }, { name: "sessionKey", type: "address", internalType: "address" }, - { - name: "contractAddress", - type: "address", - internalType: "address", - }, + { name: "contractAddress", type: "address", internalType: "address" }, { name: "selector", type: "bytes4", internalType: "bytes4" }, ], outputs: [{ name: "isOnList", type: "bool", internalType: "bool" }], @@ -806,11 +746,7 @@ export const SessionKeyPluginAbi = [ type: "tuple", internalType: "struct PluginManifest", components: [ - { - name: "interfaceIds", - type: "bytes4[]", - internalType: "bytes4[]", - }, + { name: "interfaceIds", type: "bytes4[]", internalType: "bytes4[]" }, { name: "dependencyInterfaceIds", type: "bytes4[]", @@ -831,11 +767,7 @@ export const SessionKeyPluginAbi = [ type: "bool", internalType: "bool", }, - { - name: "canSpendNativeToken", - type: "bool", - internalType: "bool", - }, + { name: "canSpendNativeToken", type: "bool", internalType: "bool" }, { name: "permittedExternalCalls", type: "tuple[]", @@ -846,16 +778,8 @@ export const SessionKeyPluginAbi = [ type: "address", internalType: "address", }, - { - name: "permitAnySelector", - type: "bool", - internalType: "bool", - }, - { - name: "selectors", - type: "bytes4[]", - internalType: "bytes4[]", - }, + { name: "permitAnySelector", type: "bool", internalType: "bool" }, + { name: "selectors", type: "bytes4[]", internalType: "bytes4[]" }, ], }, { @@ -878,11 +802,7 @@ export const SessionKeyPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -912,11 +832,7 @@ export const SessionKeyPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -946,11 +862,7 @@ export const SessionKeyPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -980,11 +892,7 @@ export const SessionKeyPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -1014,11 +922,7 @@ export const SessionKeyPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -1036,11 +940,7 @@ export const SessionKeyPluginAbi = [ type: "uint8", internalType: "enum ManifestAssociatedFunctionType", }, - { - name: "functionId", - type: "uint8", - internalType: "uint8", - }, + { name: "functionId", type: "uint8", internalType: "uint8" }, { name: "dependencyIndex", type: "uint256", @@ -1134,19 +1034,11 @@ export const SessionKeyPluginAbi = [ type: "tuple", internalType: "struct UserOperation", components: [ - { - name: "sender", - type: "address", - internalType: "address", - }, + { name: "sender", type: "address", internalType: "address" }, { name: "nonce", type: "uint256", internalType: "uint256" }, { name: "initCode", type: "bytes", internalType: "bytes" }, { name: "callData", type: "bytes", internalType: "bytes" }, - { - name: "callGasLimit", - type: "uint256", - internalType: "uint256", - }, + { name: "callGasLimit", type: "uint256", internalType: "uint256" }, { name: "verificationGasLimit", type: "uint256", @@ -1157,21 +1049,13 @@ export const SessionKeyPluginAbi = [ type: "uint256", internalType: "uint256", }, - { - name: "maxFeePerGas", - type: "uint256", - internalType: "uint256", - }, + { name: "maxFeePerGas", type: "uint256", internalType: "uint256" }, { name: "maxPriorityFeePerGas", type: "uint256", internalType: "uint256", }, - { - name: "paymasterAndData", - type: "bytes", - internalType: "bytes", - }, + { name: "paymasterAndData", type: "bytes", internalType: "bytes" }, { name: "signature", type: "bytes", internalType: "bytes" }, ], }, @@ -1257,19 +1141,11 @@ export const SessionKeyPluginAbi = [ type: "tuple", internalType: "struct UserOperation", components: [ - { - name: "sender", - type: "address", - internalType: "address", - }, + { name: "sender", type: "address", internalType: "address" }, { name: "nonce", type: "uint256", internalType: "uint256" }, { name: "initCode", type: "bytes", internalType: "bytes" }, { name: "callData", type: "bytes", internalType: "bytes" }, - { - name: "callGasLimit", - type: "uint256", - internalType: "uint256", - }, + { name: "callGasLimit", type: "uint256", internalType: "uint256" }, { name: "verificationGasLimit", type: "uint256", @@ -1280,21 +1156,13 @@ export const SessionKeyPluginAbi = [ type: "uint256", internalType: "uint256", }, - { - name: "maxFeePerGas", - type: "uint256", - internalType: "uint256", - }, + { name: "maxFeePerGas", type: "uint256", internalType: "uint256" }, { name: "maxPriorityFeePerGas", type: "uint256", internalType: "uint256", }, - { - name: "paymasterAndData", - type: "bytes", - internalType: "bytes", - }, + { name: "paymasterAndData", type: "bytes", internalType: "bytes" }, { name: "signature", type: "bytes", internalType: "bytes" }, ], }, @@ -1344,12 +1212,7 @@ export const SessionKeyPluginAbi = [ indexed: true, internalType: "address", }, - { - name: "tag", - type: "bytes32", - indexed: true, - internalType: "bytes32", - }, + { name: "tag", type: "bytes32", indexed: true, internalType: "bytes32" }, ], anonymous: false, }, diff --git a/site/pages/infra/drop-and-replace.mdx b/site/pages/infra/drop-and-replace.mdx index 49769c0372..b7159edd67 100644 --- a/site/pages/infra/drop-and-replace.mdx +++ b/site/pages/infra/drop-and-replace.mdx @@ -3,32 +3,11 @@ title: Drop and Replace description: Learn how to use Drop and Replace to retry failing user operations. --- -# Drop and replace failing user operations - -In the previous guides, we learned how to send user operations with gas sponsorship, but what happens when a user operation fails to mine? In this guide, -we'll cover how to use drop and replace to resend failing user operations and ensure they get mined. - -## What is drop and replace? - -If fees change and your user operation gets stuck in the mempool, you can use drop and replace to resend the user operation with higher fees. This is most useful -when used in combination with [`waitForUserOperationTransaction`](/reference/aa-sdk/core/functions/waitForUserOperationTransaction) to ensure the transaction is mined -and then resend the user operation with higher fees if waiting times out. - -Drop and replace works by resubmitting a user operation with the greater of: +import Snippet from "../../shared/infra/drop-and-replace-description.mdx"; -1. 10% higher fees -2. The current minimum fees - -We export a `dropAndReplace` function from `@aa-sdk/core` that you can use to handle this flow for you and is automatically added to the Smart Account Client. - -## How to drop and replace effectively - -Let's run through an example that uses drop and replace if waiting for a user operation to mine times out. +# Drop and replace failing user operations -:::warning -If sponsoring gas, like in the example below, each call to `sendUserOperation` and `dropAndReplace` will generate pending gas sponsorships in your dashboard. This can result in you hitting your gas manager limit. -At the moment, pending sponsorships expire after 10 minutes of inactivity, but it is possible that retrying excessively can temporarily exhaust your sponsorship limits. -::: + :::code-group diff --git a/site/pages/react-native/using-smart-accounts/retry-user-operations.mdx b/site/pages/react-native/using-smart-accounts/retry-user-operations.mdx new file mode 100644 index 0000000000..9cb6f1b6dd --- /dev/null +++ b/site/pages/react-native/using-smart-accounts/retry-user-operations.mdx @@ -0,0 +1,101 @@ +--- +title: Drop and Replace +description: Learn how to use Drop and Replace to retry failing user operations. +--- + +import Snippet from "../../../shared/infra/drop-and-replace-description.mdx"; + +# Drop and replace failing user operations + + + +:::code-group + +```tsx twoslash [retry-user-operations.tsx] +import React, { useState, useEffect } from "react"; +import { Alert } from "react-native"; +import { + createLightAccountClient, + LightAccount, +} from "@account-kit/smart-contracts"; +import { sepolia, alchemy } from "@account-kit/infra"; +import { User } from "@account-kit/signer"; +import { SmartAccountClient } from "@aa-sdk/core"; +// import the signer +import { signer } from "./signer.ts"; + +export default function MyComponent() { + const [client, setClient] = useState(null); + const [user, setUser] = useState(null); + const [isSendingUserOperation, setIsSendingUserOperation] = useState(false); + + // Assume the user is already authenticated + useEffect(() => { + signer.getAuthDetails().then(setUser); + }, []); + + useEffect(() => { + if (user) { + // Create a smart account client for the authenticated user + createLightAccountClient({ + signer, + chain: sepolia, + transport: alchemy({ apiKey: "API_KEY" }), + }).then((client) => { + setClient(client); + }); + } + }, [user]); + + const handleSendUserOperation = async () => { + // 0. Ensure the client is available before sending a user operation + if (!client) return; + + // 1. Send a sponsored User Operation + setIsSendingUserOperation(true); + const { hash, request } = await client?.sendUserOperation({ + uo: { + target: "0xTARGET_ADDRESS", + data: "0x", + value: 0n, + }, + account: client.account, + }); + + try { + // 2. Wait for the user operation to be mined + const txHash = await client.waitForUserOperationTransaction({ + hash, + }); + + Alert.alert("User Operation Sent"); + } catch (error) { + // 3. If the user operation fails, drop and replace it + const { hash: newHash } = await client.dropAndReplaceUserOperation({ + uoToDrop: request, + }); + + // 4. Wait for the new user operation to be mined + await client.waitForUserOperationTransaction({ + hash: newHash, + }); + } finally { + setIsSendingUserOperation(false); + } + }; + + return ( + + ); +} +``` + +```ts twoslash [signer.ts] filename="signer.ts" +// [!include ~/shared/react-native/signer.ts] +``` + +::: + +In the above example, we only try to drop and replace once before failing completely, but you can build more complex retry logic using this combination of `waitForUserOperationTransaction` and `dropAndReplace`. diff --git a/site/pages/react-native/using-smart-accounts/send-user-operations.mdx b/site/pages/react-native/using-smart-accounts/send-user-operations.mdx new file mode 100644 index 0000000000..59f89db2af --- /dev/null +++ b/site/pages/react-native/using-smart-accounts/send-user-operations.mdx @@ -0,0 +1,193 @@ +--- +title: Send User Operations +description: Learn how to send user operations using Account Kit in a React Native application. +--- + +# Send user operations + +Once your users have been [authenticated](/react-native/signer/authenticating-users/authenticating-with-otp), you can start sending user operations! Account Kit makes it really easy to send user operations using React hooks. + +To send user operations, you need to first create a `SmartAccountClient` using the [`createLightAccountClient`](/reference/account-kit/smart-contracts/functions/createLightAccountClient) function. +Once you have a `SmartAccountClient`, you can use the [`sendUserOperation`](/reference/aa-sdk/core/functions/sendUserOperation#senduseroperation) function to send user operations. + +If you want to sponsor the gas for a user, see our [guide](/react-native/using-smart-accounts/sponsor-gas). + +## Single user operation + +:::tip +In the below example, we use `LightAccount` as the underlying Smart Contract type. You can also use `MultiOwnerModularAccount` if you want to provide your users with an ERC-6900 compliant modular account, +or you can use `MultiOwnerLightAccount` if you want to support an account with multiple owners. +::: + +:::code-group + +```tsx twoslash [send-user-operation.tsx] +import React, { useState, useEffect } from "react"; +import { Alert } from "react-native"; +import { User } from "@account-kit/signer"; +import { + createLightAccountClient, + LightAccount, +} from "@account-kit/smart-contracts"; +import { sepolia, alchemy } from "@account-kit/infra"; +import { SmartAccountClient } from "@aa-sdk/core"; + +// import the signer +import { signer } from "./signer"; + +export default function MyOpSenderComponent() { + const [user, setUser] = useState(null); + const [client, setClient] = useState(null); + const [isSendingUserOperation, setIsSendingUserOperation] = useState(false); + + useEffect(() => { + // get the user if they are already authenticated + signer.getAuthDetails().then(setUser); + }, []); + + useEffect(() => { + if (user) { + // Create a light account client for the authenticated user + createLightAccountClient({ + signer, + chain: sepolia, + transport: alchemy({ apiKey: "YOUR_API_KEY" }), + }).then((client) => { + setClient(client); + }); + } + }, [user]); + + const handleSendUserOperation = async () => { + if (!client) return; + + try { + setIsSendingUserOperation(true); + const { hash } = await client.sendUserOperation({ + uo: { + target: "0xTARGET_ADDRESS", + data: "0x", + value: 0n, + }, + account: client.account, + }); + + Alert.alert("User Operation Sent", hash); + } catch (error) { + Alert.alert("Error", "Error sending user operation"); + } finally { + setIsSendingUserOperation(false); + } + }; + + return ( +
+ +
+ ); +} +``` + +```ts twoslash [signer.ts] filename="signer.ts" +// [!include ~/shared/react-native/signer.ts] +``` + +::: + +## Batch user operation + +To send multiple user operations in a single call, simply pass an array of user operations to the `sendUserOperation` method. + +:::code-group + +```tsx twoslash [batch-user-operation-component.tsx] +import React, { useState, useEffect } from "react"; +import { Alert } from "react-native"; +import { User } from "@account-kit/signer"; +import { + createLightAccountClient, + LightAccount, +} from "@account-kit/smart-contracts"; +import { sepolia, alchemy } from "@account-kit/infra"; +import { SmartAccountClient } from "@aa-sdk/core"; + +// import the signer +import { signer } from "./signer"; + +export default function MyOpSenderComponent() { + const [user, setUser] = useState(null); + const [client, setClient] = useState(null); + const [isSendingUserOperation, setIsSendingUserOperation] = useState(false); + + useEffect(() => { + // get the user if they are already authenticated + signer.getAuthDetails().then(setUser); + }, []); + + useEffect(() => { + if (user) { + // Create a light account client for the authenticated user + createLightAccountClient({ + signer, + chain: sepolia, + transport: alchemy({ apiKey: "YOUR_API_KEY" }), + }).then((client) => { + setClient(client); + setAccount(client.account); + }); + } + }, [user]); + + const handleSendUserOperation = async () => { + if (!client) return; + + try { + setIsSendingUserOperation(true); + + const { hash } = await client.sendUserOperation({ + uo: [ + { + target: "0xTARGET_ADDRESS_1", + data: "0x", + value: 0n, + }, + { + target: "0xTARGET_ADDRESS_2", + data: "0x", + value: 0n, + }, + ], + account: client.account, + }); + + Alert.alert("User Operation Sent", hash); + } catch (error) { + Alert.alert("Error", "Error sending user operation"); + } finally { + setIsSendingUserOperation(false); + } + }; + + return ( +
+ +
+ ); +} +``` + +```ts twoslash [signer.ts] filename="signer.ts" +// [!include ~/shared/react-native/signer.ts] +``` + +::: diff --git a/site/pages/react-native/using-smart-accounts/sponsor-gas.mdx b/site/pages/react-native/using-smart-accounts/sponsor-gas.mdx new file mode 100644 index 0000000000..6775fa84de --- /dev/null +++ b/site/pages/react-native/using-smart-accounts/sponsor-gas.mdx @@ -0,0 +1,97 @@ +--- +title: How to sponsor gas for a User Operation +description: Follow this guide to sponsor gas for UserOperations from any + ERC-4337 smart account. Account Kit is a vertically integrated stack for + building apps that support ERC-4337. +--- + +import Snippet from "../../../shared/create-gas-policy.mdx"; + +# How to sponsor gas for a User Operation + +Gas fees are a significant barrier to entry for new user of your app. With Account Kit you can remove this barrier by sponsoring gas fees for transactions via the [Gas Manager](https://docs.alchemy.com/docs/gas-manager-services/?a=ak-docs). This guide explains how to sponsor gas by creating a gas policy, linking it to your client, and sending sponsored `UserOperations` (UOs) from a smart account. + +After [setting up Account Kit](/react-native/signer/setup-guide) in your project, follow these steps to sponsor gas. + +## Create a Gas Manager policy + + + +## Send a sponsored User Operation with the Policy ID + +Now you can send a sponsored User Operation with the Policy ID. + +:::code-group + +```tsx twoslash [gas-sponsorship.tsx] +import React, { useState, useEffect } from "react"; +import { + createLightAccountClient, + LightAccount, +} from "@account-kit/smart-contracts"; +import { sepolia, alchemy } from "@account-kit/infra"; +import { User } from "@account-kit/signer"; + +// import the signer +import { signer } from "./signer.ts"; + +export default function MyComponent() { + const [client, setClient] = useState(null); + const [user, setUser] = useState(null); + const [isSendingUserOperation, setIsSendingUserOperation] = useState(false); + + // Assume the user is already authenticated + useEffect(() => { + signer.getAuthDetails().then(setUser); + }, []); + + useEffect(() => { + if (user) { + // Create a smart account client for the authenticated user with the policy ID + createLightAccountClient({ + signer, + chain: sepolia, + policyId: "YOUR_GAS_POLICY_ID", // [!code ++] + transport: alchemy({ apiKey: "API_KEY" }), + }).then((client) => { + setClient(client); + }); + } + }, [user]); + + const handleSendUserOperation = async () => { + if (!client) return; + // Send a sponsored User Operation + try { + setIsSendingUserOperation(true); + + const { hash } = await client.sendUserOperation({ + uo: { + target: "0xTARGET_ADDRESS", + data: "0x", + value: 0n, + }, + account: client.account, + }); + + Alert.alert("User Operation Sent", hash); + } catch (error) { + Alert.alert("Error", "Error sending user operation"); + } finally { + setIsSendingUserOperation(false); + } + }; + + return ( + + ); +} +``` + +```ts twoslash [signer.ts] filename="signer.ts" +// [!include ~/shared/react-native/signer.ts] +``` + +::: diff --git a/site/shared/infra/drop-and-replace-description.mdx b/site/shared/infra/drop-and-replace-description.mdx new file mode 100644 index 0000000000..e0efd2fd71 --- /dev/null +++ b/site/shared/infra/drop-and-replace-description.mdx @@ -0,0 +1,24 @@ +In the previous guides, we learned how to send user operations with gas sponsorship, but what happens when a user operation fails to mine? In this guide, +we'll cover how to use drop and replace to resend failing user operations and ensure they get mined. + +## What is drop and replace? + +If fees change and your user operation gets stuck in the mempool, you can use drop and replace to resend the user operation with higher fees. This is most useful +when used in combination with [`waitForUserOperationTransaction`](/reference/aa-sdk/core/functions/waitForUserOperationTransaction) to ensure the transaction is mined +and then resend the user operation with higher fees if waiting times out. + +Drop and replace works by resubmitting a user operation with the greater of: + +1. 10% higher fees +2. The current minimum fees + +We export a `dropAndReplace` function from `@aa-sdk/core` that you can use to handle this flow for you and is automatically added to the Smart Account Client. + +## How to drop and replace effectively + +Let's run through an example that uses drop and replace if waiting for a user operation to mine times out. + +:::warning +If sponsoring gas, like in the example below, each call to `sendUserOperation` and `dropAndReplace` will generate pending gas sponsorships in your dashboard. This can result in you hitting your gas manager limit. +At the moment, pending sponsorships expire after 10 minutes of inactivity, but it is possible that retrying excessively can temporarily exhaust your sponsorship limits. +::: diff --git a/site/sidebar/react-native.ts b/site/sidebar/react-native.ts index 6a60d07aef..7239b6a6a8 100644 --- a/site/sidebar/react-native.ts +++ b/site/sidebar/react-native.ts @@ -38,10 +38,16 @@ export const reactNativeSidebar: SidebarItem[] = [ items: [ { text: "Send user operations", - link: "/react-native/#TODO", + link: "/react-native/using-smart-accounts/send-user-operations", + }, + { + text: "Sponsor gas", + link: "/react-native/using-smart-accounts/sponsor-gas", + }, + { + text: "Retry user operations", + link: "/react-native/using-smart-accounts/retry-user-operations", }, - { text: "Sponsor gas", link: "/react-native/#TODO" }, - { text: "Retry user operations", link: "/react-native/#TODO" }, ], }, ];