Custom User Operation
Build and execute a sequence of contract calls in a single UserOperation.
Overview
Use this guide to create a simple custom UserOperation that transfers tokens from your smart wallet to a recipient. You'll then sign and execute it.
Steps
Install dependencies and initialize Wallet Account
Install viem to sign messages and interact with the blockchain, then initialize your wallet account.
npm install viem
pnpm add viem
yarn add viem
bun add viem
Use your private key to initialize a wallet account.
import { privateKeyToAccount } from 'viem/accounts'
const BASE_URL = 'https://api.notus.team/api/v1'
const API_KEY = '<api-key>'
const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'
const privateKey = '0x<private-key>'
const account = privateKeyToAccount(privateKey)
Register a Smart Wallet Address
Register and retrieve the smart wallet address used for Account Abstraction.
async function setupSmartWallet() {
const FACTORY_ADDRESS = '0x0000000000400CdFef5E2714E63d8040b700BC24'
const externallyOwnedAccount = account.address
const res = await fetch(`${BASE_URL}/wallets/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
externallyOwnedAccount,
factory: FACTORY_ADDRESS,
salt: '0',
}),
})
if (!res.ok) return
const data = await res.json()
return data.wallet.accountAbstraction
}
The smart wallet is deployed automatically on the first onchain transaction via a UserOperation.
Create a Custom User Operation
Compose a single ERC-20 transfer operation. In this example, we transfer 5 USDC on Polygon to a recipient.
const smartWalletAddress = await setupSmartWallet()
const customUserOperationResponse = await fetch(`${BASE_URL}/crypto/custom-user-operation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
chainId: 137, // Polygon
walletAddress: smartWalletAddress,
payGasFeeToken: USDC_POLYGON, // token used to pay fees
operations: [
{
address: USDC_POLYGON, // ERC-20 token address
methodSignature: 'function transfer(address to, uint256 amount)',
arguments: [
'0x<recipient-address>', // recipient
'5000000', // 5 USDC (6 decimals)
],
},
],
metadata: { useCase: 'custom-flow', requestId: 'req_123' },
}),
}).then((r) => r.json())
const { userOperation } = customUserOperationResponse
console.log('userOperationHash:', userOperation.userOperationHash)
- payGasFeeToken must be a token the smart wallet holds; it pays partner fees and gas.
- For a native token transfer, omitmethodSignature
and set onlyaddress
(recipient) andvalue
(in wei).
Example: Native Token Transfer
const nativeTransferResponse = await fetch(`${BASE_URL}/crypto/custom-user-operation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
chainId: 137, // Polygon
walletAddress: smartWalletAddress,
payGasFeeToken: USDC_POLYGON, // token used to pay gas fees (separate from the native token being transferred)
operations: [
{
address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', // recipient address
value: '1000000000000000000', // 1 MATIC in wei (18 decimals)
},
],
metadata: { useCase: 'native-transfer', requestId: 'req_124' }, // optional metadata for tracking transactions
}),
}).then((r) => r.json())
Execute the User Operation
Sign the returned userOperationHash
with the EOA, then queue execution.
const signature = await account.signMessage({
message: { raw: userOperation.userOperationHash },
})
const executionResponse = await fetch(`${BASE_URL}/crypto/execute-user-op`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
userOperationHash: userOperation.userOperationHash,
signature,
}),
}).then((r) => r.json())
console.log(executionResponse)
If your wallet uses EIP-7702, include the authorization
signature when executing.