Complete Simple Bridge Example
This is a complete, production-ready example of a Simple Bridge implementation using SilentSwap React hooks.
Full Component
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useAccount, useWalletClient } from 'wagmi';
import { useQuote, useTransaction } from '@silentswap/react';
import { parseUnits, formatUnits } from 'viem';
import { getAllAssetsArray, type AssetInfo } from '@silentswap/sdk';
// Helper to parse CAIP-19 to chain/address
function parseCaip19(caip19: string): { chainId: number; address: string } | null {
const match = caip19.match(/^eip155:(\d+)\/(erc20:(0x[a-fA-F0-9]+)|slip44:\d+)$/);
if (!match) return null;
const chainId = parseInt(match[1]);
const address = match[3] || '0x0000000000000000000000000000000000000000';
return { chainId, address };
}
// Helper to get asset info from CAIP-19
function getAssetFromCaip19(caip19: string): AssetInfo | null {
const allAssets = getAllAssetsArray();
return allAssets.find(asset => asset.caip19 === caip19) || null;
}
export default function SimpleBridgePage() {
const { address, isConnected, connector } = useAccount();
const { data: walletClient } = useWalletClient();
// Form state
const [sourceAsset, setSourceAsset] = useState<string>('eip155:1/slip44:60'); // ETH on Ethereum
const [destinationAsset, setDestinationAsset] = useState<string>('eip155:43114/erc20:0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E'); // USDC on Avalanche
const [sourceAmount, setSourceAmount] = useState('');
const [destinationAddress, setDestinationAddress] = useState('');
// Quote state
const [quote, setQuote] = useState<any>(null);
const [bridgeStatus, setBridgeStatus] = useState<any>(null);
// Get current asset info
const sourceTokenInfo = useMemo(() => getAssetFromCaip19(sourceAsset), [sourceAsset]);
const destinationTokenInfo = useMemo(() => getAssetFromCaip19(destinationAsset), [destinationAsset]);
// Bridge quote hook
const {
getQuote,
isLoading: quoteLoading,
error: quoteError,
} = useQuote({
address: address || undefined,
maxImpactPercent: 5,
});
// Bridge execution hook
const {
executeTransaction,
getStatus,
isLoading: bridgeLoading,
currentStep,
error: bridgeError,
} = useTransaction({
address: address!,
walletClient: walletClient as any,
connector: connector,
});
// Set default destination address
useEffect(() => {
if (address && !destinationAddress) {
setDestinationAddress(address);
}
}, [address, destinationAddress]);
// Auto-fetch quote when form changes
useEffect(() => {
const fetchQuote = async () => {
if (!sourceAmount || !address || parseFloat(sourceAmount) <= 0 || !sourceTokenInfo || !destinationTokenInfo) {
setQuote(null);
return;
}
try {
const sourceParsed = parseCaip19(sourceAsset);
const destParsed = parseCaip19(destinationAsset);
if (!sourceParsed || !destParsed) {
console.error('Failed to parse CAIP-19 identifiers');
return;
}
// Parse amount to token units
const amountInUnits = parseUnits(sourceAmount, sourceTokenInfo.decimals).toString();
// Get quote
const result = await getQuote(
sourceParsed.chainId,
sourceParsed.address,
amountInUnits,
destParsed.chainId,
destParsed.address
);
setQuote(result);
} catch (error) {
console.error('Failed to get quote:', error);
setQuote(null);
}
};
// Debounce quote fetching
const timeout = setTimeout(fetchQuote, 800);
return () => clearTimeout(timeout);
}, [sourceAsset, destinationAsset, sourceAmount, address, sourceTokenInfo, destinationTokenInfo, getQuote]);
// Execute bridge transaction
const handleExecuteBridge = useCallback(async () => {
if (!quote || !walletClient || !sourceTokenInfo) {
alert('Quote or wallet not ready');
return;
}
try {
const sourceParsed = parseCaip19(sourceAsset);
if (!sourceParsed) {
alert('Invalid source asset');
return;
}
// Convert quote result to BridgeQuote format
const bridgeQuote = {
provider: quote.provider,
estimatedTime: quote.estimatedTime,
fee: {
amount: '0',
token: sourceTokenInfo.symbol,
usdValue: quote.feeUsd,
},
slippage: quote.slippage,
route: quote.rawResponse,
txs: extractTransactions(quote.rawResponse, quote.provider, sourceParsed.chainId),
};
const result = await executeTransaction(bridgeQuote);
if (result) {
setBridgeStatus(result);
console.log('Bridge completed:', result);
// Check status if we have a request ID
if (result.requestId) {
setTimeout(async () => {
const status = await getStatus(result.requestId!, quote.provider);
if (status) {
setBridgeStatus(status);
}
}, 5000);
}
}
} catch (error) {
console.error('Bridge execution failed:', error);
alert(`Bridge failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [quote, walletClient, sourceAsset, sourceTokenInfo, executeTransaction, getStatus]);
// Extract transactions from raw response
const extractTransactions = (rawResponse: any, provider: string, chainId: number) => {
if (provider === 'relay') {
return rawResponse.steps?.flatMap((step: any) =>
step.items?.map((item: any) => ({
to: item.data?.to,
value: item.data?.value || '0',
data: item.data?.data || '0x',
gasLimit: item.data?.gas,
chainId: item.data?.chainId || chainId,
})) || []
) || [];
} else if (provider === 'debridge') {
return rawResponse.tx ? [{
to: rawResponse.tx.to,
value: rawResponse.tx.value || '0',
data: rawResponse.tx.data || '0x',
chainId: chainId,
}] : [];
}
return [];
};
// Form validation
const isFormValid = useMemo(() => {
return (
isConnected &&
sourceAmount &&
parseFloat(sourceAmount) > 0 &&
destinationAddress &&
!quoteLoading &&
quote
);
}, [isConnected, sourceAmount, destinationAddress, quoteLoading, quote]);
if (!isConnected) {
return <div>Please connect your wallet</div>;
}
return (
<div className="bridge-container">
<h1>Simple Bridge</h1>
{/* Error Display */}
{(quoteError || bridgeError) && (
<div className="error">
{quoteError && <div>Quote Error: {quoteError.message}</div>}
{bridgeError && <div>Bridge Error: {bridgeError.message}</div>}
</div>
)}
{/* Bridge Status */}
{(bridgeLoading || currentStep) && (
<div className="status">
<p>{currentStep || 'Processing...'}</p>
<progress value={0.5} />
</div>
)}
{/* Source Token Selection */}
<div className="form-section">
<label>From</label>
<button onClick={() => {/* Open token selector */}}>
{sourceTokenInfo ? sourceTokenInfo.symbol : 'Select Token'}
</button>
<input
type="text"
placeholder="0.0"
value={sourceAmount}
onChange={(e) => {
const value = e.target.value;
if (/^\d*\.?\d*$/.test(value) || value === '') {
setSourceAmount(value);
}
}}
/>
{sourceTokenInfo && (
<div className="token-info">
{sourceTokenInfo.name} ({sourceTokenInfo.symbol})
</div>
)}
</div>
{/* Destination Token Selection */}
<div className="form-section">
<label>To</label>
<button onClick={() => {/* Open token selector */}}>
{destinationTokenInfo ? destinationTokenInfo.symbol : 'Select Token'}
</button>
{quote && destinationTokenInfo && (
<div className="output-amount">
<div className="amount">
{formatUnits(BigInt(quote.outputAmount), destinationTokenInfo.decimals)}
</div>
<div className="token-info">
{destinationTokenInfo.name} ({destinationTokenInfo.symbol})
</div>
</div>
)}
<input
type="text"
placeholder="Recipient address"
value={destinationAddress}
onChange={(e) => setDestinationAddress(e.target.value)}
/>
</div>
{/* Quote Details */}
{quote && (
<div className="quote-details">
<h3>Quote Details</h3>
<div className="detail-row">
<span>Provider:</span>
<span>{quote.provider}</span>
</div>
<div className="detail-row">
<span>Fee:</span>
<span>${quote.feeUsd.toFixed(2)}</span>
</div>
<div className="detail-row">
<span>Slippage:</span>
<span>{quote.slippage.toFixed(2)}%</span>
</div>
<div className="detail-row">
<span>Estimated Time:</span>
<span>{Math.floor(quote.estimatedTime / 60)} min</span>
</div>
<div className="detail-row">
<span>Retention Rate:</span>
<span>{(quote.retentionRate * 100).toFixed(2)}%</span>
</div>
</div>
)}
{/* Execute Button */}
<button
onClick={handleExecuteBridge}
disabled={!isFormValid || bridgeLoading}
>
{bridgeLoading ? currentStep || 'Executing Bridge...' : 'Execute Bridge'}
</button>
{/* Bridge Status Result */}
{bridgeStatus && (
<div className="bridge-status">
<h3>Bridge Status</h3>
<div className="detail-row">
<span>Status:</span>
<span>{bridgeStatus.status}</span>
</div>
{bridgeStatus.requestId && (
<div className="detail-row">
<span>Request ID:</span>
<code>{bridgeStatus.requestId}</code>
</div>
)}
{bridgeStatus.txHashes && bridgeStatus.txHashes.length > 0 && (
<div>
<span>Transaction Hashes:</span>
{bridgeStatus.txHashes.map((hash: string, idx: number) => (
<code key={idx}>{hash}</code>
))}
</div>
)}
</div>
)}
</div>
);
}Key Features
- Auto-fetch Quotes - Automatically fetches quotes when amount changes (with debouncing)
- Real-time Status - Shows transaction progress with
currentStep - Error Handling - Displays errors from both quote and transaction operations
- Form Validation - Validates form before allowing execution
- Status Monitoring - Polls for bridge status after execution
Next Steps
- Learn about useQuote
- Understand useTransaction
- Explore Silent Swap for private swaps