Skip to main content
The TransferForm component provides a complete interface for transferring tokens to other addresses across different blockchain networks. It handles recipient validation, quote fetching, and transfer execution.

Import

import { TransferForm } from '@/components/TransferForm';

Overview

The TransferForm is a self-contained component that manages the complete token transfer workflow:
  • Asset Selection: Choose which token to transfer
  • Amount Input: Specify transfer amount with balance validation
  • Recipient Address: Enter destination wallet address
  • Network Selection: Choose destination blockchain network
  • Quote Fetching: Automatically calculates transfer costs
  • Transaction Execution: Handles the complete transfer flow

Component Structure

This component does not accept props - it’s a standalone form that manages its own state.
export const TransferForm: React.FC = () => {
  // Component implementation
}

Key Features

Recipient Management

The component validates recipient addresses and supports ENS names:
const [recipientAddress, setRecipientAddress] = useState<string>('');
const [recipientChain, setRecipientChain] = useState<string>('eip155:42161'); // Default to Arbitrum

const isValidAddress = (address: string): boolean => {
  return /^0x[a-fA-F0-9]{40}$/.test(address) || address.endsWith('.eth');
};

Cross-Chain Support

Users can select the destination network from all supported chains:
<Select value={recipientChain} onValueChange={handleRecipientChainChange}>
  <SelectContent>
    {chains.map(chain => (
      <SelectItem key={chain.chain.reference} value={chain.chain.chain}>
        {getChainName(chain.chain.reference)}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

Balance Validation

Built-in balance checking prevents insufficient balance errors:
const hasSufficientBalance = (amount: string) => {
  if (!assetBalance || !selectedAssetData || !amount) return false;
  
  const parsedAmount = parseTokenAmount(amount, selectedAssetData.decimals || 18);
  return BigInt(assetBalance.balance) >= BigInt(parsedAmount);
};

Usage Example

import { TransferForm } from '@/components/TransferForm';

function TransferPage() {
  return (
    <div className="container mx-auto py-8">
      <TransferForm />
    </div>
  );
}

State Management

Asset State

selectedAsset
string
default:"'ob:usdc'"
The aggregated asset ID of the token to transfer
amount
string
The human-readable amount to transfer
parsedAmount
string
The amount converted to the token’s base unit (wei for ETH, etc.)

Recipient State

recipientAddress
string
The destination wallet address (supports 0x addresses and ENS names)
recipientChain
string
The CAIP-2 chain identifier for the destination network (e.g., ‘eip155:42161’ for Arbitrum)

Quote Lifecycle

1. Quote Request

Triggered when amount, asset, recipient address, or destination chain changes:
if (authenticated && embeddedWallet && selectedAsset && 
    recipientAddress && isValidAddress(recipientAddress) &&
    hasSufficientBalance(value)) {
  debouncedGetQuote({
    fromTokenAmount: parsed,
    fromAggregatedAssetId: selectedAsset,
    toAggregatedAssetId: selectedAsset, // Same asset for transfers
    recipientAddress: `${recipientChain}:${recipientAddress}`,
  });
}

2. Quote Display

Shows transfer quote details including:
  • Transfer cost
  • Gas estimates
  • Quote expiration countdown
  • Net amount recipient receives

3. Quote Execution

Executes the transfer when the user confirms:
<Button
  className="w-full"
  onClick={executeQuote}
  disabled={getTransferButtonState().disabled}
>
  <Send className="mr-2 h-4 w-4" />
  {getTransferButtonState().text}
</Button>

User Interactions

Percentage Buttons

Quick-select buttons for common transfer amounts:
onPercentageClick={percentage => {
  if (assetBalance && selectedAssetData) {
    const balance = assetBalance.balance;
    const decimals = selectedAssetData.decimals || 18;
    const maxAmount = formatTokenAmount(balance, decimals);
    const targetAmount = ((parseFloat(maxAmount) * percentage) / 100).toString();
    
    setAmount(targetAmount);
    // Trigger quote fetch
  }
}}

Address Validation

Real-time validation feedback:
{recipientAddress && !isValidAddress(recipientAddress) && (
  <div className="mt-2 text-sm text-red-600 dark:text-red-400">
    Please enter a valid Ethereum address
  </div>
)}

Network Selection

Visual network selector with chain logos:
<SelectValue>
  {recipientChain && (
    <div className="flex items-center gap-2">
      <img
        src={getChainLogoUrl(extractChainIdFromCAIP(recipientChain))}
        alt={getChainName(extractChainIdFromCAIP(recipientChain))}
        className="w-5 h-5 rounded-full"
      />
      <span>{getChainName(extractChainIdFromCAIP(recipientChain))}</span>
    </div>
  )}
</SelectValue>

Button States

The transfer button dynamically updates based on application state:
StateButton TextDisabled
Not authenticated”Login to Transfer”Yes
Loading quote”Getting Quote…”Yes
Executing”Executing Transfer…”Yes
Insufficient balance”Insufficient Balance”Yes
Invalid address”Transfer”Yes
Ready”Transfer”No

Error Handling

Address Validation Errors

{recipientAddress && !isValidAddress(recipientAddress) && (
  <div className="mt-2 text-sm text-red-600">
    Please enter a valid Ethereum address
  </div>
)}

Quote Errors

{quote?.error && (
  <Alert variant="destructive">
    <TriangleAlert className="h-4 w-4" />
    <AlertTitle>Quote Error</AlertTitle>
    <AlertDescription>{quote?.message}</AlertDescription>
  </Alert>
)}

API Errors

{error && (
  <Alert variant="destructive">
    <TriangleAlert className="h-4 w-4" />
    <AlertTitle>An error occurred</AlertTitle>
    <AlertDescription>{error}</AlertDescription>
  </Alert>
)}

Transaction Completion

After a successful transfer:
  1. Form inputs are cleared
  2. Quote state is reset
  3. User balances are refreshed
const handleTransactionComplete = useCallback(() => {
  setAmount('');
  setParsedAmount('');
  setRecipientAddress('');
  resetQuote();
  
  if (predictedAddress) {
    fetchBalances();
  }
}, [predictedAddress, fetchBalances, resetQuote]);

Onboarding Support

The component includes data attributes for onboarding tooltips:
  • data-onboarding="transfer-card": Main card container
  • data-onboarding="transfer-token": Token selection and amount input
  • data-onboarding="transfer-recipient": Recipient address input
  • data-onboarding="transfer-network": Network selection dropdown
  • data-onboarding="transfer-button": Execute transfer button

Dependencies

import { TokenInput } from '@/components/TokenInput';
import { QuoteDetails } from '@/components/QuoteDetails';
import { QuoteCountdown } from '@/components/QuoteCountdown';
import { TransactionStatus } from '@/components/TransactionStatus';

Address Format

The recipient address is formatted as a CAIP-10 account identifier:
{chainId}:{address}
Example:
eip155:42161:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb

Network Support

All chains returned by the useChains hook are supported as destination networks. The default network is Arbitrum (eip155:42161).

Build docs developers (and LLMs) love