OneBalance
OneBalance
  • Welcome to OneBalance
  • OneBalance Toolkit
    • Introduction
    • Get Started with OneBalance SCA
      • Setup OneBalance Toolkit with Privy
        • Step 1: Setting up Privy
        • Step 2: Setting configurations
        • Step 3: Initializing and Depositing onto the OneBalance Smart Account
        • Step 4: Displaying Chain-Aggregated Balances
        • Step 5: Fetch a quote for transaction execution
        • Step 6: Signing transactions with Privy
        • Step 7: Executing transactions
        • Step 8: Getting execution status
      • Contract calls guide
        • Usage code samples
  • OneBalance vision
    • Our vision
      • Mission of OneBalance
      • Use Cases
      • Credible Accounts and Credible Stack
      • Fellowship of OneBalance
      • Glossary
    • Why resource locks?
      • Technical Details
        • Resource lock
        • Permissions
        • Credible accounts
        • Credible Commitment Machine (CCM)
        • FAQ
          • How does this relate to account abstraction and 4337?
          • Where does the OneBalance account live?
          • Are OneBalance accounts non-custodial?
      • Credible Stack Deep Dive
        • Apps
        • SDK providers
        • Wallets / WaaS
        • Solver Networks
        • Oracle Providers
        • Data Providers
  • Other
    • OneBalance Demo App
      • Roadmap
      • Privacy Policy
      • Terms and Conditions
    • OneBalance Brand Assets
Powered by GitBook
On this page
  • OneBalance Call Data API Guide
  • Prerequisites
  • Setting Up
  • Authentication
  • API Client
  • Key Generation
  • Predicting Your Account Address
  • Fetching Balances
  • Type Definitions
  • Call Data Operation Flow
  • Complete Example: ERC20 Transfer on Optimism
  • Example: Swapping Assets
  • Putting It All Together
  • Conclusion
  1. OneBalance Toolkit
  2. Get Started with OneBalance SCA

Contract calls guide

PreviousStep 8: Getting execution statusNextUsage code samples

Last updated 1 month ago

OneBalance Call Data API Guide

You can find .

This developer guide explains how to use the OneBalance API to implement call data operations. You'll learn how to:

  1. Generate EOA keys

  2. Retrieve your OneBalance account address

  3. Deposit funds to your account

  4. Execute call data operations in a number of different ways

  5. Verify call data execution using the transaction history system

Prerequisites

  • Node.js environment

  • Typescript

Setting Up

Install the required dependencies:

npm install viem axios

Authentication

All API requests need to be authenticated using an API key. For this guide, we'll use the PUBLIC API key:

const PUBLIC_API_KEY = '42bb629272001ee1163ca0dbbbc07bcbb0ef57a57baf16c4b1d4672db4562c11';

// Helper function to create authenticated headers
function createAuthHeaders(): Record<string, string> {
  return {
    'x-api-key': PUBLIC_API_KEY,
  };
}

API Client

Here's a set of helper functions to interact with the OneBalance API:

import axios, { AxiosResponse } from 'axios';

const BASE_URL = 'https://be.staging.onebalance.io';

// Generic API request function
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
export async function apiPost<RequestData, ResponseData>(
  endpoint: string,
  data: RequestData
): Promise<ResponseData> {
  return apiRequest<RequestData, ResponseData>('post', endpoint, data);
}

export async function apiGet<RequestData, ResponseData>(
  endpoint: string,
  params: RequestData
): Promise<ResponseData> {
  return apiRequest<RequestData, ResponseData>('get', endpoint, params, true);
}

Key Generation

First, let's generate the necessary keys:

import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';

// Generate EOA key pair
function generateEOAKey() {
  const privateKey = generatePrivateKey();
  const account = privateKeyToAccount(privateKey);
  
  return {
    privateKey,
    address: account.address
  };
}

// Optional: Read from cache or generate new keys
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);

Predicting Your Account Address

Once you have your session and admin keys, you can predict your OneBalance account 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;
}

// Usage example
const predictedAddress = await predictAddress(sessionKey.address, adminKey.address);
console.log('Predicted Address:', predictedAddress);

Fetching Balances

Before using your account, you need to check your balances:

async function fetchBalances(address: string) {
  const response = await apiGet<
    { address: string },
    {
      balanceByAsset: {
        aggregatedAssetId: string;
        balance: string;
        individualAssetBalances: { assetType: string; balance: string; fiatValue: number }[];
      }[];
      totalBalance: string;
    }
  >('/api/balances/aggregated-balance', { address });
  return response;
}

async function fetchUSDCBalance(address: string) {
  const response = await fetchBalances(address);
  return response.balanceByAsset.find((asset) => asset.aggregatedAssetId === 'ds:usdc');
}

async function fetchEthBalance(address: string) {
  const response = await fetchBalances(address);
  return response.balanceByAsset.find((asset) => asset.aggregatedAssetId === 'ds:eth');
}

Type Definitions

Let's define the types we'll be using:

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;
}

interface ChainOperation {
  userOp: SerializedUserOperation;
  typedDataToSign: HashTypedDataParameters;
  assetType: string;
  amount: 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 PrepareCallRequest {
  account: EvmAccount;
  targetChain: string; // CAIP-2
  calls: EvmCall[];
  tokensRequired: TokenRequirement[];
  allowanceRequirements?: TokenAllowanceRequirement[];
  overrides?: Override[];
  validAfter?: string;
  validUntil?: string;
}

interface TargetCallQuote {
  account: EvmAccount;
  chainOperation: ChainOperation;
  tamperProofSignature: string;
}

interface CallRequest {
  account: EvmAccount;
  chainOperation: ChainOperation;
  tamperProofSignature: string;
  fromAggregatedAssetId: string;
}

interface Quote {
  id: string;
  account: EvmAccount;
  originChainsOperations: ChainOperation[];
  destinationChainOperation?: ChainOperation;
  originToken?: OriginAssetUsed;
  destinationToken?: DestinationAssetUsed;
  validUntil?: string;
  validAfter?: string;
  expirationTimestamp: string;
  tamperProofSignature: string;
}

interface BundleResponse {
  success: boolean;
  guarantees: BundleGuarantees | null;
  error: string | null;
}

Call Data Operation Flow

The call data operation flow involves several steps:

  1. Prepare a call quote

  2. Sign the chain operation

  3. Request a call quote

  4. Execute the quote

  5. Verify transaction status

Here's how to implement each step:

1. Prepare a Call Quote

async function prepareCallQuote(quoteRequest: PrepareCallRequest): Promise<TargetCallQuote> {
  return apiPost<PrepareCallRequest, TargetCallQuote>('/api/quotes/prepare-call-quote', quoteRequest);
}

2. Sign Chain Operation

async function signOperation(operation: ChainOperation, key: Hex): Promise<ChainOperation> {
  return {
    ...operation,
    userOp: { ...operation.userOp, signature: await privateKeyToAccount(key).signTypedData(operation.typedDataToSign) },
  };
}

3. Request a Call Quote

async function fetchCallQuote(callRequest: CallRequest): Promise<Quote> {
  return apiPost<CallRequest, Quote>('/api/quotes/call-quote', callRequest);
}

4. Execute the Quote

async function executeQuote(quote: Quote): Promise<BundleResponse> {
  return apiPost<Quote, BundleResponse>('/api/quotes/execute-quote', quote);
}

5. Check Transaction History

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',
  });
}

Complete Example: ERC20 Transfer on Optimism

Let's put it all together with a complete example of transferring USDC on Optimism:

import { parseAbi, encodeFunctionData } from 'viem';

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:10'; // optimism destination chain target
  const usdcAddress = '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85'; // optimism USDC implementation 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');
  }
}

Example: Swapping Assets

Here's an example of swapping assets:

async function fetchSwapQuote(
  account: EvmAccount,
  fromAggregatedAssetId: string,
  toAggregatedAssetId: string,
  amount: bigint,
): Promise<Quote> {
  return apiPost<
    {
      account: EvmAccount;
      fromAggregatedAssetId: string;
      toAggregatedAssetId: string;
      fromTokenAmount: string;
    },
    Quote
  >('/api/quotes/swap-quote', {
    account,
    fromAggregatedAssetId,
    toAggregatedAssetId,
    fromTokenAmount: amount.toString(),
  });
}

async function swapAnyUsdcToEth(account: EvmAccount) {
  const usdcBalance = await fetchUSDCBalance(account.accountAddress);

  if (usdcBalance) {
    if (BigInt(usdcBalance.balance) === 0n) {
      return false;
    }
  } else {
    return false;
  }

  const quote = await fetchSwapQuote(account, 'ds:usdc', 'ds:eth', BigInt(usdcBalance.balance));

  const signedChainOp = await signOperation(quote.originChainsOperations[0], sessionKey.privateKey);

  const signedQuote = {
    ...quote,
    originChainsOperations: [signedChainOp],
  };

  const bundle = await executeQuote(signedQuote);

  if (bundle.success) {
    console.log('Swap USDC to ETH executed');
    return true;
  }

  console.log('Swap USDC to ETH failed');
  return false;
}

Putting It All Together

Here's how to tie everything together:

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();

Conclusion

This guide has covered the complete flow for implementing call data operations using the OneBalance API. You've learned how to:

  1. Generate cryptographic keys

  2. Predict your OneBalance account address

  3. Check account balances

  4. Execute different types of call data operations including:

    • ERC20 transfers via smart contract call on destination chain

    • Asset swaps

  5. Verify transaction execution through history

For more information and support, please refer to the OneBalance API documentation or contact our developer support team.

swagger documentation here