Skip to content

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

  1. Auto-fetch Quotes - Automatically fetches quotes when amount changes (with debouncing)
  2. Real-time Status - Shows transaction progress with currentStep
  3. Error Handling - Displays errors from both quote and transaction operations
  4. Form Validation - Validates form before allowing execution
  5. Status Monitoring - Polls for bridge status after execution

Next Steps