How to Execute Smart Contract Calls
Learn to prepare, sign and submit arbitrary calldata across chains using the OneBalance API
This guide walks you through implementing smart contract calls in your application. We’ll cover the complete three-step flow with production-ready code examples.
Quickstart
Execute any smart contract function across multiple blockchains with a single integration. This guide shows you how to transfer ERC20 tokens on Arbitrum using OneBalance’s calldata API.
What You’ll Build
Setup Project
Initialize a TypeScript project with OneBalance dependencies
Execute Calldata
Send arbitrary smart contract calls across any supported chain
Track Status
Monitor transaction execution in real-time
Handle Errors
Implement proper error handling
Quick Example
Here’s what you’ll accomplish - a complete working script:
// Complete working example - see full code below
async function main() {
// Step 1: Predict account address from session/admin keys
const accountAddress = await predictAddress(sessionKey.address, adminKey.address);
console.log('Predicted Address:', accountAddress);
// Step 2: Check USDC balance
const usdcBalances = await fetchUSDCBalance(accountAddress);
if (!usdcBalances) throw new Error('No USDC balance found');
// Step 3-5: Execute ERC20 transfer on Arbitrum
await transferErc20OnChain({
accountAddress,
sessionAddress: sessionKey.address,
adminAddress: adminKey.address,
}, usdcBalances);
}
Now let’s build this step by step.
Project Setup
Set up your development environment:
mkdir onebalance-calldata && cd onebalance-calldata
pnpm init
Install dependencies:
pnpm add viem axios
pnpm add -D typescript @types/node ts-node tslib
Now, let’s create and configure tsconfig.json
:
touch tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": ["./node_modules/@types"],
"types": ["node"]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
"ts-node": {
"esm": false,
"experimentalSpecifierResolution": "node"
}
}
This guide uses a public API key for testing. For production, get your API key from sales@onebalance.io.
Calldata Flow
Step 1: Generate Keys & Predict Account
Create your main file with key generation and account prediction:
touch index.ts
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import axios, { AxiosResponse } from 'axios';
import { HashTypedDataParameters, encodeFunctionData, parseAbi } from 'viem';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
const BASE_URL = 'https://be.onebalance.io';
// Note: Using the production API endpoint will produce a different predicted address
const PUBLIC_API_KEY = '42bb629272001ee1163ca0dbbbc07bcbb0ef57a57baf16c4b1d4672db4562c11';
// Helper function to create authenticated headers
function createAuthHeaders(): Record<string, string> {
return {
'x-api-key': PUBLIC_API_KEY,
};
}
async function apiRequest<RequestData, ResponseData>(
method: 'get' | 'post',
endpoint: string,
data: RequestData,
isParams = false,
): Promise<ResponseData> {
try {
const config = {
headers: createAuthHeaders(),
...(isParams ? { params: data } : {}),
};
const url = `${BASE_URL}${endpoint}`;
const response: AxiosResponse<ResponseData> =
method === 'post' ? await axios.post(url, data, config) : await axios.get(url, { ...config, params: data });
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
throw new Error(JSON.stringify(error.response.data));
}
throw error;
}
}
// API methods
async function apiPost<RequestData, ResponseData>(endpoint: string, data: RequestData): Promise<ResponseData> {
return apiRequest<RequestData, ResponseData>('post', endpoint, data);
}
async function apiGet<RequestData, ResponseData>(endpoint: string, params: RequestData): Promise<ResponseData> {
return apiRequest<RequestData, ResponseData>('get', endpoint, params, true);
}
// Generate session key pair
function generateEOAKey() {
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
return {
privateKey,
address: account.address,
};
}
function readOrCacheEOAKey(key: string) {
if (existsSync(`${key}-key.json`)) {
const cachedKeys = readFileSync(`${key}-key.json`, 'utf8');
return JSON.parse(cachedKeys);
}
const keys = generateEOAKey();
writeFileSync(`${key}-key.json`, JSON.stringify(keys, null, 2));
return keys;
}
// Usage example
const sessionKey = readOrCacheEOAKey('session');
console.log('Session Address:', sessionKey.address);
const adminKey = readOrCacheEOAKey('admin');
console.log('Admin Address:', adminKey.address);
async function predictAddress(sessionAddress: string, adminAddress: string): Promise<string> {
const response = await apiPost<{ sessionAddress: string; adminAddress: string }, { predictedAddress: string }>(
'/api/account/predict-address',
{
sessionAddress,
adminAddress,
},
);
return response.predictedAddress;
}
Step 2: Check Balance
Add balance checking functionality with proper TypeScript interfaces:
async function fetchBalances(address: string) {
const response = await apiGet<
{ address: string },
{
balanceByAggregatedAsset: {
aggregatedAssetId: string;
balance: string;
individualAssetBalances: {
assetType: string;
balance: string;
fiatValue: number
}[];
fiatValue: number;
}[];
balanceBySpecificAsset: {
assetType: string;
balance: string;
fiatValue: number;
}[];
totalBalance: {
fiatValue: number;
};
}
>('/api/v2/balances/aggregated-balance', { address });
return response;
}
async function fetchUSDCBalance(address: string) {
const response = await fetchBalances(address);
return response.balanceByAggregatedAsset.find((asset) => asset.aggregatedAssetId === 'ds:usdc');
}
Step 3: Add TypeScript Interfaces
Add TypeScript interfaces for type safety:
type Hex = `0x${string}`;
interface EvmAccount {
accountAddress: Hex;
sessionAddress: Hex;
adminAddress: Hex;
}
interface EvmCall {
to: Hex;
value?: Hex;
data?: Hex;
}
interface TokenRequirement {
assetType: string;
amount: string;
}
interface TokenAllowanceRequirement extends TokenRequirement {
spender: Hex;
}
type StateMapping = {
[slot: Hex]: Hex;
};
type StateDiff = {
stateDiff?: StateMapping;
code?: Hex;
balance?: Hex;
};
type Override = StateDiff & {
address: Hex;
};
interface PrepareCallRequest {
account: EvmAccount;
targetChain: string; // CAIP-2
calls: EvmCall[];
tokensRequired: TokenRequirement[];
allowanceRequirements?: TokenAllowanceRequirement[];
overrides?: Override[];
// permits
validAfter?: string;
validUntil?: string;
}
interface SerializedUserOperation {
sender: Hex;
nonce: string;
factory?: Hex;
factoryData?: Hex;
callData: Hex;
callGasLimit: string;
verificationGasLimit: string;
preVerificationGas: string;
maxFeePerGas: string;
maxPriorityFeePerGas: string;
paymaster?: Hex;
paymasterVerificationGasLimit?: string;
paymasterPostOpGasLimit?: string;
paymasterData?: Hex;
signature: Hex;
initCode?: Hex;
paymasterAndData?: Hex;
}
interface ChainOperationBasic {
userOp: SerializedUserOperation;
typedDataToSign: HashTypedDataParameters;
}
interface ChainOperation extends ChainOperationBasic {
assetType: string;
amount: string;
}
interface TargetCallQuote {
account: EvmAccount;
chainOperation: ChainOperation;
tamperProofSignature: string;
}
interface CallRequest {
account: EvmAccount;
chainOperation: ChainOperation;
tamperProofSignature: string;
fromAggregatedAssetId: string;
}
interface AssetUsed {
aggregatedAssetId: string;
assetType: string[] | string;
amount: string;
minimumAmount?: string;
}
interface FiatValue {
fiatValue: string;
amount: string;
}
interface OriginAssetUsed extends AssetUsed {
assetType: string[];
fiatValue: FiatValue[];
}
interface DestinationAssetUsed extends AssetUsed {
assetType: string;
fiatValue: string;
minimumAmount?: string;
minimumFiatValue?: string;
}
interface Quote {
id: string;
account: EvmAccount;
originChainsOperations: ChainOperation[];
destinationChainOperation?: ChainOperation;
originToken?: OriginAssetUsed;
destinationToken?: DestinationAssetUsed;
validUntil?: string; // block number, if empty the valid until will be MAX_UINT256
validAfter?: string; // block number, if empty the valid after will be 0
expirationTimestamp: string;
tamperProofSignature: string;
}
interface OpGuarantees {
non_equivocation: boolean;
reorg_protection: boolean;
valid_until?: number;
valid_after?: number;
}
type BundleGuarantees = Record<Hex, OpGuarantees>;
interface BundleResponse {
success: boolean;
guarantees: BundleGuarantees | null;
error: string | null;
}
type TransactionType = 'SWAP' | 'TRANSFER' | 'CALL';
type OperationStatus =
| 'PENDING' // not yet begun processing but has been submitted
| 'IN_PROGRESS' // processing the execution steps of the operation
| 'COMPLETED' // all steps completed with success
| 'REFUNDED' // none or some steps completed, some required step failed causing the whole operation to be refunded
| 'FAILED'; // all steps failed
interface OperationDetails {
hash?: Hex;
chainId?: number;
explorerUrl?: string;
}
interface HistoryTransaction {
quoteId: string;
type: TransactionType;
originToken?: OriginAssetUsed;
destinationToken?: DestinationAssetUsed;
status: OperationStatus;
user: Hex;
recipientAccountId: string; // the caip-10 address of the recipient
// if type is SWAP or TRANSFER
originChainOperations?: OperationDetails[]; // the asset(s) that were sent from the source
destinationChainOperations?: OperationDetails[]; // the asset that was received to the final destination
}
interface HistoryResponse {
transactions: HistoryTransaction[];
continuation?: string;
}
Step 4: Prepare Quote
Add the quote preparation logic with proper type safety:
async function prepareCallQuote(quoteRequest: PrepareCallRequest): Promise<TargetCallQuote> {
return apiPost<PrepareCallRequest, TargetCallQuote>('/api/quotes/prepare-call-quote', quoteRequest);
}
async function fetchCallQuote(callRequest: CallRequest): Promise<Quote> {
return apiPost<CallRequest, Quote>('/api/quotes/call-quote', callRequest);
}
async function executeQuote(quote: Quote): Promise<BundleResponse> {
return apiPost<Quote, BundleResponse>('/api/quotes/execute-quote', quote);
}
See all quote options in our Quotes API reference.
Step 5: Sign Operations
Add signing functionality:
async function signOperation(operation: ChainOperation, key: Hex): Promise<ChainOperation> {
return {
...operation,
userOp: { ...operation.userOp, signature: await privateKeyToAccount(key).signTypedData(operation.typedDataToSign) },
};
}
Step 6: Track Status
Add transaction monitoring with proper error handling:
async function fetchTransactionHistory(address: string): Promise<HistoryResponse> {
return apiGet<{ user: string; limit: number; sortBy: string }, HistoryResponse>('/api/status/get-tx-history', {
user: address,
limit: 1,
sortBy: 'createdAt',
});
}
Track all transaction states in our Status API reference.
Put It Together
Add the main transfer function and execution:
async function transferErc20OnChain(
account: EvmAccount,
usdcBalances: {
aggregatedAssetId: string;
balance: string;
individualAssetBalances: { assetType: string; balance: string; fiatValue: number }[];
},
) {
const largestUsdcBalanceEntry = usdcBalances.individualAssetBalances.reduce((max, current) => {
return Number(current.balance) > Number(max.balance) ? current : max;
});
const chain = 'eip155:42161'; // Arbitrum
const usdcAddress = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; // Arbitrum USDC address
if (largestUsdcBalanceEntry.balance === '0') {
throw new Error('No USDC balance found');
}
const transferDefinition = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
const transferCallData = encodeFunctionData({
abi: transferDefinition,
functionName: 'transfer',
args: [adminKey.address, 1n],
});
const quoteRequest: PrepareCallRequest = {
account,
targetChain: chain,
calls: [
{
to: usdcAddress as Hex,
data: transferCallData,
value: '0x0',
},
],
tokensRequired: [
{
assetType: `${chain}/erc20:${usdcAddress}`,
amount: '100000',
},
],
};
console.log(quoteRequest);
const preparedQuote = await prepareCallQuote(quoteRequest);
const signedChainOp = await signOperation(preparedQuote.chainOperation, sessionKey.privateKey);
const callRequest: CallRequest = {
fromAggregatedAssetId: 'ds:usdc',
account,
tamperProofSignature: preparedQuote.tamperProofSignature,
chainOperation: signedChainOp,
};
console.log('callRequest', callRequest);
const quote = await fetchCallQuote(callRequest);
for (let i = 0; i < quote.originChainsOperations.length; i++) {
const callQuoteSignedChainOperation = await signOperation(quote.originChainsOperations[i], sessionKey.privateKey);
quote.originChainsOperations[i] = callQuoteSignedChainOperation;
}
console.log('quote', quote);
const bundle = await executeQuote(quote);
if (bundle.success) {
console.log('Bundle executed');
const timeout = 60_000;
let completed = false;
const startTime = Date.now();
while (!completed) {
try {
console.log('fetching transaction history...');
const transactionHistory = await fetchTransactionHistory(quote.account.accountAddress);
console.log('transactionHistory', transactionHistory);
if (transactionHistory.transactions.length > 0) {
const [tx] = transactionHistory.transactions;
if (tx.quoteId === quote.id) {
if (tx.status === 'COMPLETED') {
console.log('Transaction completed and operation executed');
completed = true;
break;
}
console.log('Transaction status: ', tx.status);
}
}
} catch {}
if (Date.now() - startTime > timeout) {
throw new Error('Transaction not completed in time');
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
} else {
console.log('Bundle execution failed');
}
}
async function main() {
const predictedAddress = await predictAddress(sessionKey.address, adminKey.address);
console.log('Predicted Address:', predictedAddress);
const usdcBalances = await fetchUSDCBalance(predictedAddress);
console.log('USDC Balances:', usdcBalances);
if (!usdcBalances) {
throw new Error('No USDC balance found');
}
await transferErc20OnChain(
{
accountAddress: predictedAddress as Hex,
sessionAddress: sessionKey.address as Hex,
adminAddress: adminKey.address as Hex,
},
usdcBalances,
);
}
main();
Add scripts to your package.json
to run the index.ts
file:
{
"name": "onebalance-calldata",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"calldata": "ts-node index.ts",
"build": "tsc",
"clean": "rm -rf dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.9.0",
"dependencies": {
"axios": "^1.9.0",
"viem": "^2.30.5"
},
"devDependencies": {
"@types/node": "^22.15.28",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.8.3"
}
}
Run your calldata execution:
pnpm run calldata
After running the script for the first time, you’ll see a predicted address printed in the console. You need to fund this address with USDC on any supported chain before the transaction will succeed. Transfer some USDC to the predicted address, then run the script again.
Checkpoint: You should see session/admin addresses printed, account prediction, balance check, and transaction execution in your console.
Next Steps
Token Approvals
Understanding token allowances and approval management
Advanced Patterns
Complex scenarios, batch operations, and optimization techniques
Working Examples
Production-ready examples for DeFi, NFT, and gaming use cases
Error Handling
Handle common errors and implement retry logic
Common Issues
- No USDC balance found - User needs to fund their
predicted address
with USDC. - Balance Too Low - Ensure your account has sufficient USDC. The API requires tokens for both gas and the transfer amount.
- Transaction Timeout - Network congestion can delay execution. Increase timeout or check status manually using the quote ID.
- Invalid Calldata - Verify your ABI encoding matches the target contract’s interface exactly.
Complete Example
The complete implementation is available in our OneBalance Examples repository.
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import axios, { AxiosResponse } from 'axios';
import { HashTypedDataParameters, encodeFunctionData, parseAbi } from 'viem';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
const BASE_URL = 'https://be.onebalance.io';
// Note: Using the production API endpoint will produce a different predicted address
const PUBLIC_API_KEY = '42bb629272001ee1163ca0dbbbc07bcbb0ef57a57baf16c4b1d4672db4562c11';
// Helper function to create authenticated headers
function createAuthHeaders(): Record<string, string> {
return {
'x-api-key': PUBLIC_API_KEY,
};
}
async function apiRequest<RequestData, ResponseData>(
method: 'get' | 'post',
endpoint: string,
data: RequestData,
isParams = false,
): Promise<ResponseData> {
try {
const config = {
headers: createAuthHeaders(),
...(isParams ? { params: data } : {}),
};
const url = `${BASE_URL}${endpoint}`;
const response: AxiosResponse<ResponseData> =
method === 'post' ? await axios.post(url, data, config) : await axios.get(url, { ...config, params: data });
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
throw new Error(JSON.stringify(error.response.data));
}
throw error;
}
}
// API methods
async function apiPost<RequestData, ResponseData>(endpoint: string, data: RequestData): Promise<ResponseData> {
return apiRequest<RequestData, ResponseData>('post', endpoint, data);
}
async function apiGet<RequestData, ResponseData>(endpoint: string, params: RequestData): Promise<ResponseData> {
return apiRequest<RequestData, ResponseData>('get', endpoint, params, true);
}
// Generate session key pair
function generateEOAKey() {
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
return {
privateKey,
address: account.address,
};
}
function readOrCacheEOAKey(key: string) {
if (existsSync(`${key}-key.json`)) {
const cachedKeys = readFileSync(`${key}-key.json`, 'utf8');
return JSON.parse(cachedKeys);
}
const keys = generateEOAKey();
writeFileSync(`${key}-key.json`, JSON.stringify(keys, null, 2));
return keys;
}
// Usage example
const sessionKey = readOrCacheEOAKey('session');
console.log('Session Address:', sessionKey.address);
const adminKey = readOrCacheEOAKey('admin');
console.log('Admin Address:', adminKey.address);
async function predictAddress(sessionAddress: string, adminAddress: string): Promise<string> {
const response = await apiPost<{ sessionAddress: string; adminAddress: string }, { predictedAddress: string }>(
'/api/account/predict-address',
{
sessionAddress,
adminAddress,
},
);
return response.predictedAddress;
}
async function fetchBalances(address: string) {
const response = await apiGet<
{ address: string },
{
balanceByAggregatedAsset: {
aggregatedAssetId: string;
balance: string;
individualAssetBalances: {
assetType: string;
balance: string;
fiatValue: number
}[];
fiatValue: number;
}[];
balanceBySpecificAsset: {
assetType: string;
balance: string;
fiatValue: number;
}[];
totalBalance: {
fiatValue: number;
};
}
>('/api/v2/balances/aggregated-balance', { address });
return response;
}
async function fetchUSDCBalance(address: string) {
const response = await fetchBalances(address);
return response.balanceByAggregatedAsset.find((asset) => asset.aggregatedAssetId === 'ds:usdc');
}
type Hex = `0x${string}`;
interface EvmAccount {
accountAddress: Hex;
sessionAddress: Hex;
adminAddress: Hex;
}
interface EvmCall {
to: Hex;
value?: Hex;
data?: Hex;
}
interface TokenRequirement {
assetType: string;
amount: string;
}
interface TokenAllowanceRequirement extends TokenRequirement {
spender: Hex;
}
type StateMapping = {
[slot: Hex]: Hex;
};
type StateDiff = {
stateDiff?: StateMapping;
code?: Hex;
balance?: Hex;
};
type Override = StateDiff & {
address: Hex;
};
interface PrepareCallRequest {
account: EvmAccount;
targetChain: string; // CAIP-2
calls: EvmCall[];
tokensRequired: TokenRequirement[];
allowanceRequirements?: TokenAllowanceRequirement[];
overrides?: Override[];
// permits
validAfter?: string;
validUntil?: string;
}
interface SerializedUserOperation {
sender: Hex;
nonce: string;
factory?: Hex;
factoryData?: Hex;
callData: Hex;
callGasLimit: string;
verificationGasLimit: string;
preVerificationGas: string;
maxFeePerGas: string;
maxPriorityFeePerGas: string;
paymaster?: Hex;
paymasterVerificationGasLimit?: string;
paymasterPostOpGasLimit?: string;
paymasterData?: Hex;
signature: Hex;
initCode?: Hex;
paymasterAndData?: Hex;
}
interface ChainOperationBasic {
userOp: SerializedUserOperation;
typedDataToSign: HashTypedDataParameters;
}
interface ChainOperation extends ChainOperationBasic {
assetType: string;
amount: string;
}
interface TargetCallQuote {
account: EvmAccount;
chainOperation: ChainOperation;
tamperProofSignature: string;
}
interface CallRequest {
account: EvmAccount;
chainOperation: ChainOperation;
tamperProofSignature: string;
fromAggregatedAssetId: string;
}
interface AssetUsed {
aggregatedAssetId: string;
assetType: string[] | string;
amount: string;
minimumAmount?: string;
}
interface FiatValue {
fiatValue: string;
amount: string;
}
interface OriginAssetUsed extends AssetUsed {
assetType: string[];
fiatValue: FiatValue[];
}
interface DestinationAssetUsed extends AssetUsed {
assetType: string;
fiatValue: string;
minimumAmount?: string;
minimumFiatValue?: string;
}
interface Quote {
id: string;
account: EvmAccount;
originChainsOperations: ChainOperation[];
destinationChainOperation?: ChainOperation;
originToken?: OriginAssetUsed;
destinationToken?: DestinationAssetUsed;
validUntil?: string; // block number, if empty the valid until will be MAX_UINT256
validAfter?: string; // block number, if empty the valid after will be 0
expirationTimestamp: string;
tamperProofSignature: string;
}
interface OpGuarantees {
non_equivocation: boolean;
reorg_protection: boolean;
valid_until?: number;
valid_after?: number;
}
type BundleGuarantees = Record<Hex, OpGuarantees>;
interface BundleResponse {
success: boolean;
guarantees: BundleGuarantees | null;
error: string | null;
}
type TransactionType = 'SWAP' | 'TRANSFER' | 'CALL';
type OperationStatus =
| 'PENDING' // not yet begun processing but has been submitted
| 'IN_PROGRESS' // processing the execution steps of the operation
| 'COMPLETED' // all steps completed with success
| 'REFUNDED' // none or some steps completed, some required step failed causing the whole operation to be refunded
| 'FAILED'; // all steps failed
interface OperationDetails {
hash?: Hex;
chainId?: number;
explorerUrl?: string;
}
interface HistoryTransaction {
quoteId: string;
type: TransactionType;
originToken?: OriginAssetUsed;
destinationToken?: DestinationAssetUsed;
status: OperationStatus;
user: Hex;
recipientAccountId: string; // the caip-10 address of the recipient
// if type is SWAP or TRANSFER
originChainOperations?: OperationDetails[]; // the asset(s) that were sent from the source
destinationChainOperations?: OperationDetails[]; // the asset that was received to the final destination
}
interface HistoryResponse {
transactions: HistoryTransaction[];
continuation?: string;
}
async function prepareCallQuote(quoteRequest: PrepareCallRequest): Promise<TargetCallQuote> {
return apiPost<PrepareCallRequest, TargetCallQuote>('/api/quotes/prepare-call-quote', quoteRequest);
}
async function fetchCallQuote(callRequest: CallRequest): Promise<Quote> {
return apiPost<CallRequest, Quote>('/api/quotes/call-quote', callRequest);
}
async function executeQuote(quote: Quote): Promise<BundleResponse> {
return apiPost<Quote, BundleResponse>('/api/quotes/execute-quote', quote);
}
async function fetchTransactionHistory(address: string): Promise<HistoryResponse> {
return apiGet<{ user: string; limit: number; sortBy: string }, HistoryResponse>('/api/status/get-tx-history', {
user: address,
limit: 1,
sortBy: 'createdAt',
});
}
async function signOperation(operation: ChainOperation, key: Hex): Promise<ChainOperation> {
return {
...operation,
userOp: { ...operation.userOp, signature: await privateKeyToAccount(key).signTypedData(operation.typedDataToSign) },
};
}
async function transferErc20OnChain(
account: EvmAccount,
usdcBalances: {
aggregatedAssetId: string;
balance: string;
individualAssetBalances: { assetType: string; balance: string; fiatValue: number }[];
},
) {
const largestUsdcBalanceEntry = usdcBalances.individualAssetBalances.reduce((max, current) => {
return Number(current.balance) > Number(max.balance) ? current : max;
});
const chain = 'eip155:42161'; // Arbitrum
const usdcAddress = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; // Arbitrum USDC address
if (largestUsdcBalanceEntry.balance === '0') {
throw new Error('No USDC balance found');
}
const transferDefinition = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
const transferCallData = encodeFunctionData({
abi: transferDefinition,
functionName: 'transfer',
args: [adminKey.address, 1n],
});
const quoteRequest: PrepareCallRequest = {
account,
targetChain: chain,
calls: [
{
to: usdcAddress as Hex,
data: transferCallData,
value: '0x0',
},
],
tokensRequired: [
{
assetType: `${chain}/erc20:${usdcAddress}`,
amount: '100000',
},
],
};
console.log(quoteRequest);
const preparedQuote = await prepareCallQuote(quoteRequest);
const signedChainOp = await signOperation(preparedQuote.chainOperation, sessionKey.privateKey);
const callRequest: CallRequest = {
fromAggregatedAssetId: 'ds:usdc',
account,
tamperProofSignature: preparedQuote.tamperProofSignature,
chainOperation: signedChainOp,
};
console.log('callRequest', callRequest);
const quote = await fetchCallQuote(callRequest);
for (let i = 0; i < quote.originChainsOperations.length; i++) {
const callQuoteSignedChainOperation = await signOperation(quote.originChainsOperations[i], sessionKey.privateKey);
quote.originChainsOperations[i] = callQuoteSignedChainOperation;
}
console.log('quote', quote);
const bundle = await executeQuote(quote);
if (bundle.success) {
console.log('Bundle executed');
const timeout = 60_000;
let completed = false;
const startTime = Date.now();
while (!completed) {
try {
console.log('fetching transaction history...');
const transactionHistory = await fetchTransactionHistory(quote.account.accountAddress);
console.log('transactionHistory', transactionHistory);
if (transactionHistory.transactions.length > 0) {
const [tx] = transactionHistory.transactions;
if (tx.quoteId === quote.id) {
if (tx.status === 'COMPLETED') {
console.log('Transaction completed and operation executed');
completed = true;
break;
}
console.log('Transaction status: ', tx.status);
}
}
} catch {}
if (Date.now() - startTime > timeout) {
throw new Error('Transaction not completed in time');
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
} else {
console.log('Bundle execution failed');
}
}
async function main() {
const predictedAddress = await predictAddress(sessionKey.address, adminKey.address);
console.log('Predicted Address:', predictedAddress);
const usdcBalances = await fetchUSDCBalance(predictedAddress);
console.log('USDC Balances:', usdcBalances);
if (!usdcBalances) {
throw new Error('No USDC balance found');
}
await transferErc20OnChain(
{
accountAddress: predictedAddress as Hex,
sessionAddress: sessionKey.address as Hex,
adminAddress: adminKey.address as Hex,
},
usdcBalances,
);
}
main();
Integration Checklist
Before going to production, ensure you have:
- Stored API keys securely (environment variables)
- Implemented proper error handling
- Added transaction monitoring
- Tested with small amounts first
- Implemented retry logic for network failures
- Added logging for debugging
- Validated all addresses and amounts
- Handled signature rejection cases
Was this page helpful?