Skip to content

Complete Silent Swap Example

This is a complete, production-ready example of a Silent Swap implementation with multi-output support using SilentSwap React hooks.

Full Component

import { useState, useEffect, useCallback, useMemo } from 'react';
import { useAccount, useWalletClient } from 'wagmi';
import {
  useSilentClient,
  useAuth,
  useWallet,
  useSilentQuote,
  BridgeProvider,
} from '@silentswap/react';
import {
  DeliveryMethod,
  FacilitatorKeyType,
  ENVIRONMENT,
  caip19FungibleEvmToken,
  PublicKeyArgGroups,
  S0X_ADDR_USDC_AVALANCHE,
} from '@silentswap/sdk';
import { BigNumber } from 'bignumber.js';
 
interface Destination {
  asset: string;
  contact: string;
  amount: string;
}
 
export default function SilentSwapPage() {
  const { address, isConnected, connector } = useAccount();
  const { data: walletClient } = useWalletClient();
 
  // Initialize SilentSwap client
  const { client } = useSilentClient({
    config: {
      environment: ENVIRONMENT.MAINNET,
      baseUrl: 'https://api.silentswap.com',
    },
  });
 
  // Authentication hook
  const {
    auth,
    signIn,
    isAuthenticated,
    isLoading: authLoading,
    error: authError,
  } = useAuth({
    client,
    address: address!,
    walletClient: walletClient as any,
    domain: window.location.hostname,
    autoAuthenticate: true,
  });
 
  // Wallet management hook
  const {
    wallet,
    generateWallet,
    isLoading: walletLoading,
    error: walletError,
  } = useWallet({
    address: address!,
    auth: auth || undefined,
    walletClient: walletClient as any,
    connector: connector,
  });
 
  // Form state
  const [sourceAsset, setSourceAsset] = useState('eip155:43114/erc20:0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E'); // USDC on Avalanche
  const [sourceAmount, setSourceAmount] = useState('');
  const [destinations, setDestinations] = useState<Destination[]>([
    { asset: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', contact: '', amount: '' }, // USDC on Ethereum
  ]);
  const [splits, setSplits] = useState<number[]>([1]); // Cumulative splits
  const [facilitatorGroup, setFacilitatorGroup] = useState<any>(null);
  const [loadingAmounts, setLoadingAmounts] = useState(false);
 
  // Swap execution hook
  const {
    getQuote,
    executeSwap,
    isLoading: swapLoading,
    currentStep,
    error: swapError,
    quote,
  } = useSilentQuote({
    client,
    address: address!,
    walletClient: walletClient as any,
    facilitatorGroup,
    connector: connector,
    bridgeProvider: 'relay' as BridgeProvider,
    onStatus: (status) => console.log('Swap status:', status),
  });
 
  // Generate wallet when authenticated
  useEffect(() => {
    if (auth && walletClient && !wallet && !walletLoading) {
      generateWallet();
    }
  }, [auth, walletClient, wallet, walletLoading, generateWallet]);
 
  // Resolve facilitator group when wallet is available
  useEffect(() => {
    if (wallet?.accounts[0] && !facilitatorGroup) {
      wallet.accounts[0].group().then(setFacilitatorGroup);
    }
  }, [wallet, facilitatorGroup]);
 
  // Set default destination address when wallet connects
  useEffect(() => {
    if (address && destinations[0] && !destinations[0].contact) {
      setDestinations(prev => prev.map((dest, idx) => 
        idx === 0 ? { ...dest, contact: address } : dest
      ));
    }
  }, [address, destinations]);
 
  // Fetch quote when form changes
  const handleGetQuote = useCallback(async () => {
    if (!wallet?.accounts[0] || !sourceAmount || parseFloat(sourceAmount) <= 0) {
      return;
    }
 
    try {
      setLoadingAmounts(true);
      
      // Generate facilitator public keys
      const facilitatorGroup = await wallet.accounts[0].group();
      const viewer = await facilitatorGroup.viewer();
      const { publicKeyBytes: viewerPk } = viewer.exportPublicKey('*', FacilitatorKeyType.SECP256K1);
 
      const groupPks = await facilitatorGroup.exportPublicKeys(destinations.length, PublicKeyArgGroups.GENERIC);
 
      // Parse source asset
      const sourceMatch = sourceAsset.match(/eip155:(\d+)\/erc20:(0x[a-fA-F0-9]+)/);
      if (!sourceMatch) {
        throw new Error('Invalid source asset format');
      }
 
      // Build outputs array
      const outputs = destinations.map((dest, idx) => {
        const destMatch = dest.asset.match(/eip155:(\d+)\/erc20:(0x[a-fA-F0-9]+)/);
        if (!destMatch) {
          throw new Error(`Invalid destination asset format at index ${idx}`);
        }
 
        const destChainId = parseInt(destMatch[1]);
        const destTokenAddress = destMatch[2] as `0x${string}`;
        
        // Calculate split amount
        const splitAmount = idx === 0 
          ? splits[0] 
          : splits[idx] - splits[idx - 1];
        
        const sourceAmountBN = BigNumber(sourceAmount);
        const splitAmountBN = sourceAmountBN.times(splitAmount);
 
        return {
          method: DeliveryMethod.SNIP,
          asset: caip19FungibleEvmToken(destChainId, destTokenAddress),
          value: splitAmountBN.toFixed(0) as `${bigint}`,
          recipient: dest.contact as `0x${string}`,
          facilitatorPublicKeys: groupPks[idx],
        };
      });
 
      const quoteResult = await getQuote({
        signer: address!,
        viewer: viewerPk,
        outputs,
      });
 
      if (quoteResult) {
        console.log('Quote received:', quoteResult);
      }
    } catch (error) {
      console.error('Quote failed:', error);
    } finally {
      setLoadingAmounts(false);
    }
  }, [wallet, sourceAsset, sourceAmount, destinations, splits, address, getQuote]);
 
  // Execute swap
  const handleExecuteSwap = useCallback(async () => {
    if (!facilitatorGroup || !quote) {
      alert('Facilitator group or quote not ready');
      return;
    }
 
    try {
      // Determine bridge provider
      const isSourceUsdcAvalanche = sourceAsset === `eip155:43114/erc20:${S0X_ADDR_USDC_AVALANCHE}`;
      const bridgeProvider = isSourceUsdcAvalanche ? 'none' : 'relay';
 
      const result = await executeSwap({
        quote: quote,
        sourceAsset,
        sourceAmount: BigNumber(sourceAmount).toFixed(0),
        senderContactId: `caip10:eip155:*:${address}`,
        bridgeProvider: bridgeProvider as any,
      });
 
      if (result) {
        console.log('Swap completed:', result);
        alert(`Swap completed! Order ID: ${result.orderId}`);
      }
    } catch (error) {
      console.error('Swap failed:', error);
      alert(`Swap failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }, [facilitatorGroup, quote, sourceAsset, sourceAmount, address, executeSwap]);
 
  // Validate form
  const isFormValid = useMemo(() => {
    return (
      isConnected &&
      isAuthenticated() &&
      wallet &&
      sourceAmount &&
      parseFloat(sourceAmount) > 0 &&
      destinations.every(dest => dest.asset && dest.contact)
    );
  }, [isConnected, isAuthenticated, wallet, sourceAmount, destinations]);
 
  if (!isConnected) {
    return <div>Please connect your wallet</div>;
  }
 
  if (!isAuthenticated()) {
    return (
      <div>
        <p>Please sign in to authenticate with SilentSwap</p>
        <button onClick={signIn} disabled={authLoading}>
          {authLoading ? 'Signing In...' : 'Sign In with Ethereum'}
        </button>
      </div>
    );
  }
 
  return (
    <div className="swap-container">
      <h1>SilentSwap - Cross-Chain Swap</h1>
 
      {/* Error Display */}
      {(authError || walletError || swapError) && (
        <div className="error">
          {authError && <div>Authentication Error: {authError.message}</div>}
          {walletError && <div>Wallet Error: {walletError.message}</div>}
          {swapError && <div>Swap Error: {swapError.message}</div>}
        </div>
      )}
 
      {/* Source Asset */}
      <div className="form-section">
        <label>From</label>
        <button onClick={() => {/* Open token selector */}}>
          {sourceAsset.split('/').pop()?.split(':').pop()?.slice(0, 6) || '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);
            }
          }}
        />
      </div>
 
      {/* Destination Assets */}
      {destinations.map((dest, idx) => (
        <div key={idx} className="form-section">
          <label>Output {idx + 1}</label>
          <button onClick={() => {/* Open token selector */}}>
            {dest.asset.split('/').pop()?.split(':').pop()?.slice(0, 6) || 'Select Token'}
          </button>
          <input
            type="text"
            placeholder="Recipient address"
            value={dest.contact}
            onChange={(e) => {
              setDestinations(prev => prev.map((d, i) => 
                i === idx ? { ...d, contact: e.target.value } : d
              ));
            }}
          />
          <input
            type="range"
            min="0"
            max="1"
            step="0.01"
            value={idx === 0 ? splits[0] : splits[idx] - splits[idx - 1]}
            onChange={(e) => {
              const value = parseFloat(e.target.value);
              const cumulativeValue = idx === 0 
                ? value 
                : splits[idx - 1] + value;
              // Update splits logic here
            }}
          />
        </div>
      ))}
 
      {/* Action Buttons */}
      <div className="actions">
        <button
          onClick={handleGetQuote}
          disabled={swapLoading || !sourceAmount || parseFloat(sourceAmount) <= 0}
        >
          {loadingAmounts ? 'Getting Quote...' : 'Get Quote'}
        </button>
 
        <button
          onClick={handleExecuteSwap}
          disabled={swapLoading || !isFormValid || !quote}
        >
          {swapLoading ? currentStep || 'Executing Swap...' : 'Execute Swap'}
        </button>
      </div>
 
      {/* Swap Status */}
      {swapLoading && currentStep && (
        <div className="status">
          <p>Status: {currentStep}</p>
        </div>
      )}
 
      {/* Quote Display */}
      {quote && (
        <div className="quote-details">
          <h3>Quote Details</h3>
          <div className="detail-row">
            <span>Quote ID:</span>
            <code>{quote.quoteId}</code>
          </div>
          <div className="detail-row">
            <span>Authorizations:</span>
            <span>{quote.authorizations.length}</span>
          </div>
          <div className="detail-row">
            <span>Facilitators:</span>
            <span>{quote.facilitators.length}</span>
          </div>
        </div>
      )}
    </div>
  );
}

Flow Summary

  1. Connect Wallet - User connects wallet
  2. Authenticate - Auto-authenticate with SIWE
  3. Generate Wallet - Generate facilitator wallet from entropy
  4. Get Quote - Fetch quote with facilitator public keys
  5. Execute Swap - Execute complete swap flow
  6. Monitor Status - Track swap progress

Next Steps