Welcome to Part 2 of the Nexus SDK tutorial! In this part, you'll add seamless cross-chain transfer functionality to your existing portfolio viewer, allowing users to send assets directly to any address on any supported blockchain network. All this can be done just from inside your dApp, no need to use custom third party bridges or manage multiple networks or network switching, it's all handled by the Nexus SDK!
This is a follow on from part 1, if you haven't done that yet, we recommend implementing it first.
What You'll Build
By the end of this tutorial, your app will have:
- Integrated transfer interface on the same page as your portfolio
- Smart chain selection with source and destination chain pickers
- Real-time transfer simulation with cost estimation
- Transaction history with status tracking and explorer links
- Seamless user experience with tab navigation between portfolio and transfer views
Prerequisites
Before we begin, ensure you have:
- Node.js (v16 or higher) - Download here
- A package manager (npm, yarn, or pnpm)
- Basic knowledge of React/Next.js - hooks, components, state management
- A wallet extension like MetaMask installed and set up
- Some testnet tokens for testing (we'll show you how to get them)
Just want the code?
You can skip the tutorial and run the complete application here!
Step 1: Update the NexusProvider
Replace your components/NexusProvider.tsx
with the updated version:
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { useAccount } from 'wagmi';
// Global window extension for wallet provider
declare global {
interface Window {
ethereum?: any;
}
}
interface TransferParams {
token: string;
amount: string;
chainId: number;
recipient: string;
}
interface NexusContextType {
sdk: any; // Replace with actual SDK type when available
isInitialized: boolean;
balances: any[];
isLoading: boolean;
error: string | null;
refreshBalances: () => Promise<void>;
transfer: (params: TransferParams) => Promise<any>;
simulateTransfer: (params: TransferParams) => Promise<any>;
}
const NexusContext = createContext<NexusContextType | undefined>(undefined);
interface NexusProviderProps {
children: ReactNode;
}
export function NexusProvider({ children }: NexusProviderProps) {
const { isConnected, address } = useAccount();
const [sdk, setSdk] = useState<any>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [balances, setBalances] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Initialize SDK when wallet connects
useEffect(() => {
if (isConnected && window.ethereum && !isInitialized && !isLoading) {
initializeSDK();
}
}, [isConnected, isInitialized, isLoading]);
const initializeSDK = async () => {
try {
setIsLoading(true);
setError(null);
// Dynamic import with updated package name
const { NexusSDK } = await import('@avail-project/nexus');
const nexusSDK = new NexusSDK({ network: 'testnet' });
// Initialize with the wallet provider
await nexusSDK.initialize(window.ethereum);
// Set up allowance hook for token approvals
nexusSDK.setOnAllowanceHook(async ({ allow, deny, sources }) => {
console.log('Allowance required for sources:', sources);
// For tutorial, we'll auto-approve with minimum allowances
// In production, show proper approval modals
const allowances = sources.map(() => 'min');
allow(allowances);
});
// Set up intent hook for transaction previews
nexusSDK.setOnIntentHook(({ intent, allow, deny, refresh }) => {
console.log('Transaction intent:', intent);
// For tutorial, we'll auto-approve
// In production, show transaction preview modals
allow();
});
setSdk(nexusSDK);
setIsInitialized(true);
// Fetch initial balances
await fetchBalances(nexusSDK);
} catch (error) {
console.error('Failed to initialize Nexus SDK:', error);
setError(error instanceof Error ? error.message : 'Failed to initialize SDK');
} finally {
setIsLoading(false);
}
};
const fetchBalances = async (sdkInstance = sdk) => {
if (!sdkInstance || !isInitialized) return;
try {
setIsLoading(true);
setError(null);
const unifiedBalances = await sdkInstance.getUnifiedBalances();
setBalances(unifiedBalances);
console.log('Unified balances fetched:', unifiedBalances);
} catch (error) {
console.error('Failed to fetch balances:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch balances');
} finally {
setIsLoading(false);
}
};
const refreshBalances = async () => {
await fetchBalances();
};
// Transfer function for Part 2
const transfer = async (params: TransferParams) => {
if (!sdk) {
throw new Error('SDK not initialized');
}
try {
console.log('Starting transfer transaction:', params);
const result = await sdk.transfer(params);
console.log('Transfer transaction result:', result);
// Save transaction to history
const transaction = {
id: Date.now().toString(),
type: 'transfer' as const,
token: params.token,
amount: params.amount,
toChain: params.chainId,
recipient: params.recipient,
status: 'pending' as const,
timestamp: new Date(),
hash: result.hash || result.transactionHash || undefined
};
// Store in localStorage
const existingHistory = localStorage.getItem('nexus-transfer-transactions');
const history = existingHistory ? JSON.parse(existingHistory) : [];
history.unshift(transaction);
// Keep only last 50 transactions
const trimmedHistory = history.slice(0, 50);
localStorage.setItem('nexus-transfer-transactions', JSON.stringify(trimmedHistory));
// Refresh balances after successful transfer
setTimeout(() => {
refreshBalances();
}, 5000);
return result;
} catch (error) {
console.error('Transfer transaction failed:', error);
throw error;
}
};
// Simulate transfer transaction for Part 2
const simulateTransfer = async (params: TransferParams) => {
if (!sdk) {
throw new Error('SDK not initialized');
}
try {
console.log('Simulating transfer transaction:', params);
const simulation = await sdk.simulateTransfer(params);
console.log('Transfer simulation result:', simulation);
return simulation;
} catch (error) {
console.error('Transfer simulation failed:', error);
throw error;
}
};
// Reset state when wallet disconnects
useEffect(() => {
if (!isConnected) {
setSdk(null);
setIsInitialized(false);
setBalances([]);
setError(null);
}
}, [isConnected]);
return (
<NexusContext.Provider
value={{
sdk,
isInitialized,
balances,
isLoading,
error,
refreshBalances,
transfer,
simulateTransfer
}}
>
{children}
</NexusContext.Provider>
);
}
export function useNexus() {
const context = useContext(NexusContext);
if (context === undefined) {
throw new Error('useNexus must be used within a NexusProvider');
}
return context;
}
What's New in the Provider
- Transfer Function: Handles transfer transactions and saves history to localStorage
- Simulate Transfer: Previews transfer costs and feasibility before execution
- Transaction History: Automatically tracks all transfer operations
- Auto-refresh: Updates balances after successful transfer operations
Step 2: Create the Transfer History Component
Create components/TransferHistory.tsx
:
'use client';
import { useState, useEffect } from 'react';
import { Clock, CheckCircle, XCircle, ExternalLink, History, Send } from 'lucide-react';
interface TransferTransaction {
id: string;
type: 'transfer';
token: string;
amount: string;
toChain: number;
recipient: string;
status: 'pending' | 'completed' | 'failed';
timestamp: Date;
hash?: string;
}
export function TransferHistory() {
const [transactions, setTransactions] = useState<TransferTransaction[]>([]);
const [showHistory, setShowHistory] = useState(false);
const chains = {
11155111: 'Sepolia',
84532: 'Base Sepolia',
80002: 'Polygon Amoy',
421614: 'Arbitrum Sepolia',
11155420: 'Optimism Sepolia'
};
useEffect(() => {
loadTransactionHistory();
}, []);
const loadTransactionHistory = () => {
try {
const savedTransactions = localStorage.getItem('nexus-transfer-transactions');
if (savedTransactions) {
const parsed = JSON.parse(savedTransactions);
setTransactions(parsed.map((tx: any) => ({
...tx,
timestamp: new Date(tx.timestamp)
})));
}
} catch (error) {
console.error('Error loading transfer history:', error);
}
};
const getStatusIcon = (status: TransferTransaction['status']) => {
switch (status) {
case 'completed':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'failed':
return <XCircle className="w-4 h-4 text-red-500" />;
case 'pending':
return <Clock className="w-4 h-4 text-yellow-500" />;
}
};
const getChainName = (chainId: number) => {
return chains[chainId as keyof typeof chains] || `Chain ${chainId}`;
};
const getExplorerUrl = (hash: string, chainId: number) => {
const explorers = {
11155111: 'https://sepolia.etherscan.io',
84532: 'https://sepolia.basescan.org',
80002: 'https://amoy.polygonscan.com',
421614: 'https://sepolia.arbiscan.io',
11155420: 'https://sepolia-optimism.etherscan.io'
};
const explorer = explorers[chainId as keyof typeof explorers];
return explorer ? `${explorer}/tx/${hash}` : `https://etherscan.io/tx/${hash}`;
};
const truncateAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
if (transactions.length === 0) {
return null;
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<History className="w-5 h-5 text-slate-600" />
<h3 className="text-lg font-semibold text-slate-900">Transfer History</h3>
</div>
<button
onClick={() => setShowHistory(!showHistory)}
className="text-purple-600 hover:text-purple-700 text-sm font-medium"
>
{showHistory ? 'Hide' : 'Show'} History ({transactions.length})
</button>
</div>
{showHistory && (
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="space-y-3">
{transactions.slice(0, 10).map((tx) => (
<div key={tx.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center space-x-3">
{getStatusIcon(tx.status)}
<div>
<p className="font-medium text-slate-900">
{tx.amount} {tx.token}
</p>
<p className="text-sm text-slate-500">
To: {truncateAddress(tx.recipient)} on {getChainName(tx.toChain)}
</p>
<p className="text-xs text-slate-400">
{tx.timestamp.toLocaleString()}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<span className={`
px-2 py-1 rounded-full text-xs font-medium
${tx.status === 'completed' ? 'bg-green-100 text-green-700' : ''}
${tx.status === 'failed' ? 'bg-red-100 text-red-700' : ''}
${tx.status === 'pending' ? 'bg-yellow-100 text-yellow-700' : ''}
`}>
{tx.status}
</span>
{tx.hash && (
<a
href={getExplorerUrl(tx.hash, tx.toChain)}
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-slate-600 transition-colors"
title="View on Explorer"
>
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
Transfer History Features
- Transaction Tracking: Stores and displays all transfer operations
- Status Indicators: Visual icons for pending/completed/failed states
- Explorer Links: Direct links to view transactions on blockchain explorers
- Recipient Display: Shows truncated recipient addresses
- Collapsible UI: Toggle show/hide with transaction count display
Step 3: Create the Transfer Form Component
Create components/TransferForm.tsx
:
'use client';
import { useState, useEffect } from 'react';
import { useNexus } from './NexusProvider';
import { Send, RefreshCw, ArrowRight, AlertCircle, Info, ChevronDown } from 'lucide-react';
export function TransferForm() {
const { sdk, isInitialized, balances, refreshBalances } = useNexus();
const [selectedToken, setSelectedToken] = useState('');
const [targetChain, setTargetChain] = useState('');
const [transferAmount, setTransferAmount] = useState('');
const [recipient, setRecipient] = useState('');
const [isTransferring, setIsTransferring] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSimulating, setIsSimulating] = useState(false);
const [simulation, setSimulation] = useState<any>(null);
// Chain mapping for testnet
const chains = {
11155111: { name: 'Sepolia', shortName: 'SEP', icon: 'π·' },
84532: { name: 'Base Sepolia', shortName: 'BASE-SEP', icon: 'π΅' },
80002: { name: 'Polygon Amoy', shortName: 'AMOY', icon: 'π£' },
421614: { name: 'Arbitrum Sepolia', shortName: 'ARB-SEP', icon: 'π΅' },
11155420: { name: 'Optimism Sepolia', shortName: 'OP-SEP', icon: 'π΄' }
};
// Get available tokens with non-zero balances
const availableTokens = balances.filter(token =>
token.breakdown && token.breakdown.some((item: any) => parseFloat(item.balance) > 0)
);
const selectedTokenData = availableTokens.find(token => token.symbol === selectedToken);
// Get total balance for selected token
const totalBalance = selectedTokenData ? parseFloat(selectedTokenData.balance) : 0;
// Get all possible target chains
const availableTargetChains = Object.entries(chains);
const canSubmit = selectedToken &&
targetChain &&
transferAmount &&
recipient &&
parseFloat(transferAmount) > 0 &&
parseFloat(transferAmount) <= totalBalance &&
/^0x[a-fA-F0-9]{40}$/.test(recipient);
// Reset dependent fields when selections change
useEffect(() => {
if (selectedToken) {
setTransferAmount('');
}
}, [selectedToken]);
useEffect(() => {
if (targetChain) {
setTransferAmount('');
}
}, [targetChain]);
// Simulate transfer transaction
useEffect(() => {
if (canSubmit && sdk) {
simulateTransfer();
} else {
setSimulation(null);
}
}, [selectedToken, targetChain, transferAmount, recipient, canSubmit]);
const simulateTransfer = async () => {
if (!sdk || !canSubmit) return;
try {
setIsSimulating(true);
const simulationResult = await sdk.simulateTransfer({
token: selectedToken,
amount: transferAmount,
chainId: parseInt(targetChain),
recipient: recipient
});
setSimulation(simulationResult);
} catch (error) {
console.error('Simulation failed:', error);
setSimulation(null);
} finally {
setIsSimulating(false);
}
};
const handleTransfer = async () => {
if (!canSubmit || !sdk) return;
try {
setIsTransferring(true);
setError(null);
const result = await sdk.transfer({
token: selectedToken,
amount: transferAmount,
chainId: parseInt(targetChain),
recipient: recipient
});
console.log('Transfer transaction result:', result);
// Reset form on success
setSelectedToken('');
setTargetChain('');
setTransferAmount('');
setRecipient('');
setSimulation(null);
// Refresh balances after a delay
setTimeout(() => {
refreshBalances();
}, 3000);
} catch (error) {
console.error('Transfer failed:', error);
setError(error instanceof Error ? error.message : 'Transfer transaction failed');
} finally {
setIsTransferring(false);
}
};
const setMaxAmount = () => {
if (selectedTokenData) {
setTransferAmount(selectedTokenData.balance);
}
};
const isValidAddress = (address: string) => {
return /^0x[a-fA-F0-9]{40}$/.test(address);
};
if (!isInitialized) {
return (
<div className="text-center py-8">
<p className="text-slate-500">Connect your wallet to start transferring</p>
</div>
);
}
if (availableTokens.length === 0) {
return (
<div className="text-center py-8">
<AlertCircle className="w-8 h-8 text-yellow-500 mx-auto mb-3" />
<p className="text-slate-600 mb-2">No tokens available for transfer</p>
<p className="text-sm text-slate-500">Make sure you have tokens on supported networks</p>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-slate-900">Transfer Assets</h2>
<p className="text-slate-600">Send tokens to any address across chains</p>
</div>
<button
onClick={refreshBalances}
className="flex items-center space-x-2 bg-slate-100 hover:bg-slate-200 text-slate-700 px-3 py-2 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
<span>Refresh</span>
</button>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-6 space-y-6">
{/* Token Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Select Token to Transfer
</label>
<div className="relative">
<select
value={selectedToken}
onChange={(e) => setSelectedToken(e.target.value)}
className="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Choose a token...</option>
{availableTokens.map((token, index) => (
<option key={index} value={token.symbol}>
{token.symbol} - {token.balance} total across chains
</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
</div>
</div>
{/* Target Chain Selection */}
{selectedToken && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Transfer To (Destination Chain)
</label>
<div className="relative">
<select
value={targetChain}
onChange={(e) => setTargetChain(e.target.value)}
className="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Choose destination chain...</option>
{availableTargetChains.map(([chainId, chain]) => (
<option key={chainId} value={chainId}>
{chain.icon} {chain.name}
</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
</div>
</div>
)}
{/* Recipient Address */}
{selectedToken && targetChain && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Recipient Address
</label>
<input
type="text"
placeholder="0x..."
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className={`w-full p-3 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
recipient && !isValidAddress(recipient) ? 'border-red-300' : 'border-slate-300'
}`}
/>
{recipient && !isValidAddress(recipient) && (
<p className="text-red-500 text-sm mt-1">Invalid Ethereum address</p>
)}
</div>
)}
{/* Amount Input */}
{selectedToken && targetChain && recipient && (
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-slate-700">
Amount to Transfer
</label>
{selectedTokenData && (
<button
onClick={setMaxAmount}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
Max: {parseFloat(selectedTokenData.balance).toFixed(4)} {selectedToken}
</button>
)}
</div>
<input
type="number"
step="any"
placeholder="0.0"
value={transferAmount}
onChange={(e) => setTransferAmount(e.target.value)}
className="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
)}
{/* Transfer Preview */}
{selectedToken && targetChain && recipient && transferAmount && (
<div className="bg-gradient-to-r from-purple-50 to-pink-50 rounded-lg p-4 border border-purple-200">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-slate-900">Transfer Summary</h4>
<ArrowRight className="w-5 h-5 text-purple-500" />
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-600">Amount:</span>
<span className="font-medium">{transferAmount} {selectedToken}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-600">To:</span>
<span className="font-medium">
{chains[parseInt(targetChain) as keyof typeof chains]?.icon} {chains[parseInt(targetChain) as keyof typeof chains]?.name}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Recipient:</span>
<span className="font-mono text-xs">{recipient.slice(0, 6)}...{recipient.slice(-4)}</span>
</div>
</div>
</div>
)}
{/* Simulation Results */}
{isSimulating && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-center space-x-2">
<div className="w-4 h-4 border-2 border-yellow-400 border-t-transparent rounded-full animate-spin"></div>
<span className="text-yellow-800">Simulating transaction...</span>
</div>
</div>
)}
{simulation && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start space-x-2">
<Info className="w-5 h-5 text-green-600 mt-0.5" />
<div>
<p className="text-green-800 font-medium">Transfer Ready</p>
<p className="text-green-700 text-sm">
Transaction simulated successfully. Ready to send your tokens!
</p>
</div>
</div>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start space-x-2">
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="text-red-800 font-medium">Transfer Failed</p>
<p className="text-red-700 text-sm">{error}</p>
</div>
</div>
</div>
)}
{/* Submit Button */}
<button
onClick={handleTransfer}
disabled={!canSubmit || isTransferring || isSimulating}
className={`
w-full py-3 px-4 rounded-lg font-medium transition-all duration-200
${canSubmit && !isTransferring && !isSimulating
? 'bg-purple-600 hover:bg-purple-700 text-white shadow-lg hover:shadow-xl'
: 'bg-slate-300 text-slate-500 cursor-not-allowed'
}
`}
>
{isTransferring ? (
<div className="flex items-center justify-center space-x-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
<span>Sending Transfer...</span>
</div>
) : (
<div className="flex items-center justify-center space-x-2">
<Send className="w-4 h-4" />
<span>Send Transfer</span>
</div>
)}
</button>
</div>
</div>
);
}
Transfer Form Features
- Smart Validation: Progressive form validation with real-time feedback
- Chain Selection: Destination chain picker with chain icons
- Address Validation: Real-time validation of recipient addresses
- Real-time Simulation: Preview transfer costs before execution
- Max Amount Button: One-click to use maximum available balance
- Visual Feedback: Clear transfer summary with chain icons
Step 4: Update the Main Page
Replace your main page (app/page.tsx
) with:
'use client';
import { WalletConnection } from '@/components/WalletConnection';
import { UnifiedBalances } from '@/components/UnifiedBalances';
import { TransferForm } from '@/components/TransferForm';
import { TransferHistory } from '@/components/TransferHistory';
import { useAccount } from 'wagmi';
import { useNexus } from '@/components/NexusProvider';
import { Globe, Zap, Shield, ArrowRight, Send } from 'lucide-react';
import { useState } from 'react';
export default function Home() {
const { isConnected } = useAccount();
const { isInitialized, isLoading } = useNexus();
const [activeTab, setActiveTab] = useState<'portfolio' | 'transfer'>('portfolio');
return (
<main className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center space-x-2 mb-4">
<Globe className="w-8 h-8 text-blue-600" />
<h1 className="text-4xl font-bold text-slate-900">
Nexus SDK Tutorial
</h1>
</div>
<div className="mb-4 flex justify-center gap-2">
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1 rounded-full">
Part 1: Portfolio View
</span>
<span className="bg-purple-100 text-purple-800 text-sm font-medium px-3 py-1 rounded-full">
Part 2: Cross-Chain Transfers
</span>
</div>
<p className="text-xl text-slate-600 max-w-2xl mx-auto">
Experience unified Web3 interactions with portfolio management and
seamless cross-chain transfers in one interface
</p>
</div>
{/* Features */}
<div className="grid md:grid-cols-3 gap-6 mb-12">
<div className="bg-white/60 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<Zap className="w-8 h-8 text-yellow-500 mb-3" />
<h3 className="font-semibold text-slate-900 mb-2">Lightning Fast</h3>
<p className="text-slate-600 text-sm">
Instant balance updates and transfer simulations across all supported chains
</p>
</div>
<div className="bg-white/60 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<Shield className="w-8 h-8 text-green-500 mb-3" />
<h3 className="font-semibold text-slate-900 mb-2">Secure by Design</h3>
<p className="text-slate-600 text-sm">
Built-in security with smart allowance management and transaction previews
</p>
</div>
<div className="bg-white/60 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<Globe className="w-8 h-8 text-blue-500 mb-3" />
<h3 className="font-semibold text-slate-900 mb-2">Testnet Ready</h3>
<p className="text-slate-600 text-sm">
Safe development environment using Sepolia, Base Sepolia, and more
</p>
</div>
</div>
{/* Wallet Connection */}
<div className="flex justify-center mb-8">
<WalletConnection />
</div>
{/* Main Content */}
{isConnected && isInitialized ? (
<div className="max-w-6xl mx-auto">
{/* Tab Navigation */}
<div className="flex justify-center mb-8">
<div className="bg-white rounded-lg p-1 shadow-sm border border-slate-200">
<button
onClick={() => setActiveTab('portfolio')}
className={`px-6 py-2 rounded-md font-medium transition-all duration-200 ${
activeTab === 'portfolio'
? 'bg-blue-500 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<div className="flex items-center space-x-2">
<Globe className="w-4 h-4" />
<span>Portfolio View</span>
</div>
</button>
<button
onClick={() => setActiveTab('transfer')}
className={`px-6 py-2 rounded-md font-medium transition-all duration-200 ${
activeTab === 'transfer'
? 'bg-purple-500 text-white shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<div className="flex items-center space-x-2">
<Send className="w-4 h-4" />
<span>Cross-Chain Transfer</span>
</div>
</button>
</div>
</div>
{/* Content Based on Active Tab */}
{activeTab === 'portfolio' ? (
<div className="space-y-8">
{/* Unified Balances */}
<UnifiedBalances />
{/* Quick Transfer CTA */}
<div className="bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl p-6 border border-purple-200">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-slate-900 mb-1">
Ready to Send Tokens?
</h3>
<p className="text-sm text-slate-600">
Send your assets directly to any address on any chain with one click
</p>
</div>
<button
onClick={() => setActiveTab('transfer')}
className="flex items-center space-x-2 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition-colors"
>
<Send className="w-4 h-4" />
<span>Send Tokens</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
) : (
<div className="space-y-8">
{/* Transfer History */}
<TransferHistory />
{/* Transfer Form */}
<TransferForm />
{/* Current Balances Reference */}
<div className="bg-blue-50 rounded-xl p-6 border border-blue-200">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-slate-900 mb-1">
Need to Check Your Balances?
</h3>
<p className="text-sm text-slate-600">
View your complete portfolio across all chains
</p>
</div>
<button
onClick={() => setActiveTab('portfolio')}
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors"
>
<Globe className="w-4 h-4" />
<span>View Portfolio</span>
</button>
</div>
</div>
</div>
)}
</div>
) : isConnected ? (
<div className="text-center py-12">
<div className="max-w-md mx-auto">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Setting Up Your Multi-Chain Experience
</h2>
<p className="text-slate-600 mb-8">
<strong>Testnet Tutorial:</strong> This demo safely uses testnet tokens
</p>
<ul className="text-sm text-slate-500 list-disc list-inside space-y-1">
<li>Sepolia (Ethereum testnet)</li>
<li>Base Sepolia (Base testnet)</li>
<li>Polygon Amoy, Arbitrum Sepolia, Optimism Sepolia</li>
</ul>
</div>
</div>
) : (
<div className="text-center py-12">
<div className="max-w-md mx-auto">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Ready to Experience Chain Abstraction?
</h2>
<p className="text-slate-600 mb-8">
Connect your wallet to see how the Nexus SDK unifies your Web3 experience
</p>
<div className="bg-white/60 backdrop-blur-sm rounded-xl p-6 border border-white/20">
<p className="text-sm text-slate-500 mb-2">
<strong>Testnet Tutorial:</strong> This demo safely uses testnet tokens
</p>
<ul className="text-sm text-slate-500 list-disc list-inside space-y-1">
<li>Sepolia (Ethereum testnet)</li>
<li>Base Sepolia (Base testnet)</li>
<li>Polygon Amoy, Arbitrum Sepolia, Optimism Sepolia</li>
</ul>
</div>
</div>
</div>
)}
{/* Loading State */}
{isConnected && isLoading && !isInitialized && (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 shadow-xl">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-600 border-t-transparent"></div>
<span className="text-slate-700">Initializing Nexus SDK...</span>
</div>
</div>
</div>
)}
{/* Tutorial Series Navigation */}
<div className="max-w-4xl mx-auto mt-16">
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-8 border border-white/20">
<h3 className="text-xl font-bold text-slate-900 mb-4">What's Next?</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center space-x-2 mb-2">
<span className="bg-green-500 text-white text-xs font-bold px-2 py-1 rounded">PART 3</span>
<span className="text-sm font-medium text-slate-900">Direct Transfers</span>
</div>
<p className="text-sm text-slate-600">
Send tokens directly across chains to any address with advanced routing
</p>
</div>
<div className="p-4 bg-orange-50 rounded-lg border border-orange-200">
<div className="flex items-center space-x-2 mb-2">
<span className="bg-orange-500 text-white text-xs font-bold px-2 py-1 rounded">PART 4</span>
<span className="text-sm font-medium text-slate-900">Production Ready</span>
</div>
<p className="text-sm text-slate-600">
Deploy to mainnet with advanced monitoring, error handling, and optimization
</p>
</div>
</div>
</div>
</div>
</div>
</main>
);
}
Step 5: Test Your Transfer Implementation
Now let's test everything to make sure it works perfectly.
1. Start the Development Server
npm run dev
2. Connect your wallet
Ensure you have testnet tokens or get testnet ETH from faucets for testing
3. Testing Checklist
Visit http://localhost:3000
and verify:
β Test the Portfolio Tab:
- [ ] Verify balances display correctly
- [ ] Check chain breakdown shows per-chain amounts
- [ ] Click "Send Tokens" CTA to switch tabs
β Test the Transfer Tab:
- [ ] Select a token with available balance
- [ ] Choose destination chain
- [ ] Enter a valid recipient address
- [ ] Enter amount and verify Max button works
- [ ] Check transfer preview shows correct details
- [ ] Watch for simulation success/failure
β Execute a Test Transfer:
- [ ] Start with a small amount (0.01 tokens)
- [ ] Confirm transaction in MetaMask
- [ ] Monitor transaction history
- [ ] Check explorer link works
- [ ] Verify balances update after completion
Common Issues & Solutions
Transfer Simulation Fails:
- Check you have sufficient balance
- Ensure transfer amount is greater than 0
- Verify recipient address is valid Ethereum address
Transaction History Not Showing:
- Check browser console for localStorage errors
- Try clearing localStorage and testing again
Balances Not Updating:
- Wait 30-60 seconds for blockchain confirmation
- Click the Refresh button manually
- Check if transaction was actually confirmed
Understanding the Transfer Flow
Step-by-Step Transfer Process
- Token Selection β User picks from available tokens with balance
- Destination Chain β All supported chains available
- Recipient Address β Real-time validation for Ethereum addresses
- Amount Input β With max amount helper and validation
- Simulation β Real-time preview of transfer cost and feasibility
- Execution β SDK handles transfer transaction automatically
- History Tracking β Transaction saved to localStorage
- Balance Update β Automatic refresh after successful transfer
How the Nexus SDK Simplifies Transfers
Traditional Cross-Chain Transfers:
- Find compatible bridge protocol
- Navigate to bridge website
- Connect wallet on both chains
- Bridge tokens to destination chain
- Send tokens to recipient
- Wait for multiple confirmations
With Nexus SDK:
- Select token and destination chain in one interface
- Automatic route optimization
- Single transaction for entire transfer
- Built-in confirmation tracking
- Unified balance updates
Security & Best Practices
Production Considerations
Approval Management:
// In production, implement proper approval modals
nexusSDK.setOnAllowanceHook(async ({ allow, deny, sources }) => {
// Show approval modal to user
const userApproval = await showApprovalModal(sources);
if (userApproval) {
allow(userApproval.allowances);
} else {
deny();
}
});
Transaction Previews:
// Show detailed transaction preview before execution
nexusSDK.setOnIntentHook(({ intent, allow, deny }) => {
// Display transaction details, costs, risks
const userConfirmed = await showTransactionPreview(intent);
if (userConfirmed) {
allow();
} else {
deny();
}
});
Error Handling:
- Always validate user inputs before simulation
- Handle network-specific errors gracefully
- Provide clear error messages for failed transactions
- Implement retry mechanisms for failed operations
What You've Accomplished
Congratulations! You've successfully built a complete cross-chain transfer interface.
Your app now includes:
- β Unified Portfolio View - See all your assets across multiple chains
- β Cross-Chain Transfers - Send assets directly to any address on any chain
- β Real-time Simulation - Preview transfer costs before execution
- β Transaction History - Track all transfer operations with status
- β Explorer Integration - Direct links to blockchain explorers
- β Seamless UX - Tab navigation and cross-references between features
Key Technical Achievements
- Chain Abstraction: Users don't need to understand complex bridge protocols
- Unified Interface: Portfolio and transfer functionality in one clean interface
- Smart Validation: Progressive form validation with real-time feedback
- Transaction Tracking: Persistent history with status monitoring
- Testnet Safety: Safe development environment with testnet tokens
Next Steps: Part 3 Preview
In Part 3: Advanced Transfer Features, you'll add:
- Batch Transfers - Send to multiple addresses in one transaction
- Address Book - Save and manage frequently used addresses
- Gas Optimization - Advanced routing for optimal transfer costs
- Scheduled Transfers - Set up future transfers with time delays
Stay tuned for Part 3 where we'll build the ultimate cross-chain transfer experience!
Advanced Customization Tips
Custom Transfer Themes
// Customize transfer UI colors
const transferTheme = {
primary: '#8b5cf6', // Purple
secondary: '#ec4899', // Pink
success: '#10b981', // Green
warning: '#f59e0b', // Yellow
error: '#ef4444' // Red
};
Add Transfer Analytics
// Track transfer usage for analytics
const trackTransferEvent = (eventName: string, properties: any) => {
console.log('Transfer Event:', eventName, properties);
// Add your analytics service here
};
// In transfer function:
trackTransferEvent('transfer_initiated', {
token: params.token,
amount: params.amount,
toChain: params.chainId,
recipient: params.recipient
});
Custom Chain Support
// Add new testnet chains
const customChains = {
999999: {
name: 'My Custom Testnet',
shortName: 'CUSTOM',
icon: 'β‘',
explorer: 'https://custom-explorer.com'
}
};
Additional Resources
- Nexus SDK API Documentation
- Testnet Faucets:
- Blockchain Explorers:
Troubleshooting Guide
Common Error Messages
"SDK not initialized"
Solution: Ensure wallet is connected before attempting transfer operations
"Insufficient balance"
Solution: Check you have enough tokens, including gas fees
"Invalid recipient address"
Solution: Verify recipient address is valid Ethereum format (0x...)
"Simulation failed"
Solution: Check transfer route exists for the selected token and chains
"Transaction reverted"
Solution: Check gas fees, token allowances, and network connectivity
Performance Optimization
Slow Balance Updates:
// Implement balance caching
const cachedBalances = useMemo(() => {
return balances.filter(balance => parseFloat(balance.balance) > 0);
}, [balances]);
Optimize Simulations:
// Debounce simulation calls
const debouncedSimulate = useMemo(
() => debounce(simulateTransfer, 500),
[simulateTransfer]
);
Debug Mode
Enable detailed logging for development:
// Add to NexusProvider
const [debugMode, setDebugMode] = useState(process.env.NODE_ENV === 'development');
if (debugMode) {
console.log('Transfer params:', params);
console.log('Simulation result:', simulation);
console.log('Transaction history:', transactions);
}
Learning Objectives Completed
By finishing Part 2, you've mastered:
Technical Skills
- β Cross-chain transfer integration with React
- β Real-time transaction simulation
- β Persistent storage with localStorage
- β Complex form validation and UX flows
- β Error handling for blockchain operations
Web3 Concepts
- β Chain abstraction principles
- β Transfer protocols and routing
- β Transaction lifecycle management
- β Multi-chain asset management
- β Gas optimization strategies
User Experience Design
- β Progressive disclosure in forms
- β Tab-based navigation patterns
- β Loading states and error feedback
- β Transaction status communication
- β Cross-component interaction design
π Bonus Challenges
Ready for more? Try these advanced features:
- Transfer Fee Calculator: Show estimated fees before transferring
- Slippage Protection: Add slippage tolerance settings
- Batch Transfers: Send to multiple addresses in one operation
- Transfer Scheduling: Schedule transfers for optimal gas prices
- Cross-Chain Swaps: Combine transfers with token swaps
π Congratulations! You've mastered cross-chain transfers with the Nexus SDK. Your users can now send assets seamlessly between blockchains with a professional, secure interface.
Ready for Part 3? We'll add advanced transfer features, letting users batch transfers, manage address books, and optimize gas costs across multiple chains!