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
- Connect Wallet - User connects wallet
- Authenticate - Auto-authenticate with SIWE
- Generate Wallet - Generate facilitator wallet from entropy
- Get Quote - Fetch quote with facilitator public keys
- Execute Swap - Execute complete swap flow
- Monitor Status - Track swap progress
Next Steps
- Learn about client initialization
- Understand authentication
- Explore wallet generation
- See quote and execution