Full Bridge Widget
This example combines chain selection, token selection, amount input, quote display, execution, and status tracking into a complete bridge widget.Overview
The full integration includes:- Chain and token selection with balance display
- Amount input with max button
- Real-time quote fetching with debouncing
- Execution with wallet integration
- Transaction status tracking
- Comprehensive error handling
Complete Widget Component
Copy
'use client';
import { useState, useCallback, useMemo } from 'react';
import {
MinaProvider,
useMina,
useQuote,
useTokenBalance,
useTransactionStatus,
} from '@siphoyawe/mina-sdk/react';
import type {
Chain,
Token,
Quote,
ExecutionResult,
StepStatusPayload,
TransactionStatusPayload,
} from '@siphoyawe/mina-sdk/react';
import { useAccount, useWalletClient } from 'wagmi';
// Main widget component
export function BridgeWidget() {
return (
<MinaProvider config={{ integrator: 'my-bridge-app', autoDeposit: true }}>
<BridgeWidgetInner />
</MinaProvider>
);
}
function BridgeWidgetInner() {
const { mina, isReady } = useMina();
const { address, isConnected } = useAccount();
const { data: walletClient } = useWalletClient();
// Form state
const [sourceChain, setSourceChain] = useState<Chain | null>(null);
const [sourceToken, setSourceToken] = useState<Token | null>(null);
const [amount, setAmount] = useState('');
const [slippage, setSlippage] = useState(0.5);
// Execution state
const [isExecuting, setIsExecuting] = useState(false);
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null);
const [currentStep, setCurrentStep] = useState<StepStatusPayload | null>(null);
const [txHash, setTxHash] = useState<string | null>(null);
// Convert amount to smallest unit
const amountInSmallestUnit = useMemo(() => {
if (!amount || !sourceToken) return '';
try {
const parsed = parseFloat(amount);
if (isNaN(parsed) || parsed <= 0) return '';
return (BigInt(Math.floor(parsed * 10 ** sourceToken.decimals))).toString();
} catch {
return '';
}
}, [amount, sourceToken]);
// Fetch quote with useQuote hook
const { quote, isLoading: isQuoteLoading, error: quoteError, refetch: refetchQuote } = useQuote({
fromChain: sourceChain?.id,
toChain: 999, // HyperEVM
fromToken: sourceToken?.address,
toToken: '0xb88339cb7199b77e23db6e890353e22632ba630f', // USDC on HyperEVM
amount: amountInSmallestUnit,
fromAddress: address,
slippageTolerance: slippage,
enabled: Boolean(sourceChain && sourceToken && amountInSmallestUnit && address),
});
// Fetch source token balance
const {
formattedBalance,
balance: rawBalance,
isLoading: isBalanceLoading,
refetch: refetchBalance,
} = useTokenBalance({
chainId: sourceChain?.id,
tokenAddress: sourceToken?.address,
walletAddress: address ?? undefined,
refetchInterval: 15000,
});
// Track transaction status
const { status: txStatus } = useTransactionStatus(txHash);
// Create signer from wallet client
const signer = useMemo(() => {
if (!walletClient || !sourceChain) return null;
return {
sendTransaction: async (request: {
to: string;
data: string;
value: string;
chainId: number;
}) => {
const hash = await walletClient.sendTransaction({
to: request.to as `0x${string}`,
data: request.data as `0x${string}`,
value: BigInt(request.value),
chain: {
id: request.chainId,
name: sourceChain.name,
nativeCurrency: {
name: sourceChain.nativeToken.name,
symbol: sourceChain.nativeToken.symbol,
decimals: sourceChain.nativeToken.decimals,
},
rpcUrls: { default: { http: [] } },
},
});
return hash;
},
getAddress: async () => walletClient.account.address,
getChainId: async () => sourceChain.id,
};
}, [walletClient, sourceChain]);
// Handle max button
const handleMaxClick = useCallback(() => {
if (formattedBalance && sourceToken) {
setAmount(formattedBalance);
}
}, [formattedBalance, sourceToken]);
// Handle execution
const handleExecute = useCallback(async () => {
if (!mina || !quote || !signer) return;
setIsExecuting(true);
setExecutionResult(null);
setCurrentStep(null);
try {
const result = await mina.execute({
quote,
signer,
onStepChange: (step: StepStatusPayload) => {
setCurrentStep(step);
if (step.txHash) {
setTxHash(step.txHash);
}
},
onStatusChange: (status: TransactionStatusPayload) => {
console.log('Status update:', status);
},
});
setExecutionResult(result);
if (result.status === 'completed') {
// Reset form on success
setAmount('');
refetchBalance();
}
} catch (error) {
console.error('Execution failed:', error);
setExecutionResult({
executionId: 'error',
status: 'failed',
steps: [],
error: error instanceof Error ? error : new Error('Unknown error'),
});
} finally {
setIsExecuting(false);
}
}, [mina, quote, signer, refetchBalance]);
// Reset when chain changes
const handleChainChange = useCallback((chain: Chain) => {
setSourceChain(chain);
setSourceToken(null);
setAmount('');
setExecutionResult(null);
setTxHash(null);
}, []);
// Validation
const hasInsufficientBalance = useMemo(() => {
if (!rawBalance || !amountInSmallestUnit) return false;
return BigInt(amountInSmallestUnit) > BigInt(rawBalance);
}, [rawBalance, amountInSmallestUnit]);
const canExecute = useMemo(() => {
return (
isConnected &&
quote &&
!isQuoteLoading &&
!hasInsufficientBalance &&
!isExecuting &&
signer
);
}, [isConnected, quote, isQuoteLoading, hasInsufficientBalance, isExecuting, signer]);
if (!isReady) {
return <div className="bridge-widget loading">Initializing SDK...</div>;
}
return (
<div className="bridge-widget">
<div className="widget-header">
<h2>Bridge to Hyperliquid</h2>
<SlippageSelector value={slippage} onChange={setSlippage} />
</div>
{/* Source Chain Selection */}
<div className="form-section">
<label>From Chain</label>
<ChainSelector
selectedChainId={sourceChain?.id ?? null}
onSelect={handleChainChange}
/>
</div>
{/* Source Token Selection */}
<div className="form-section">
<label>Token</label>
<TokenSelector
chainId={sourceChain?.id ?? null}
walletAddress={address ?? null}
selectedToken={sourceToken}
onSelect={setSourceToken}
/>
</div>
{/* Amount Input */}
<div className="form-section">
<div className="amount-header">
<label>Amount</label>
{formattedBalance && (
<span className="balance">
Balance: {formattedBalance} {sourceToken?.symbol}
<button onClick={handleMaxClick} className="max-button">
MAX
</button>
</span>
)}
</div>
<input
type="number"
placeholder="0.00"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={!sourceToken}
className={hasInsufficientBalance ? 'error' : ''}
/>
{hasInsufficientBalance && (
<span className="error-text">Insufficient balance</span>
)}
</div>
{/* Destination Display */}
<div className="destination-section">
<div className="destination-chain">
<span className="label">To</span>
<div className="chain-display">
<span className="chain-name">HyperEVM</span>
<span className="token-name">USDC</span>
</div>
</div>
</div>
{/* Quote Display */}
{(isQuoteLoading || quote || quoteError) && (
<QuoteSection
quote={quote}
isLoading={isQuoteLoading}
error={quoteError}
onRefresh={refetchQuote}
/>
)}
{/* Execution Status */}
{(isExecuting || executionResult || currentStep) && (
<ExecutionStatus
isExecuting={isExecuting}
currentStep={currentStep}
result={executionResult}
txStatus={txStatus}
/>
)}
{/* Execute Button */}
<button
className="execute-button"
onClick={handleExecute}
disabled={!canExecute}
>
{!isConnected
? 'Connect Wallet'
: isExecuting
? 'Bridging...'
: hasInsufficientBalance
? 'Insufficient Balance'
: !quote
? 'Enter Amount'
: 'Bridge'}
</button>
</div>
);
}
Styling
BridgeWidget.css
Copy
.bridge-widget {
max-width: 440px;
margin: 0 auto;
padding: 24px;
background: #0d0d1a;
border: 1px solid #2d2d44;
border-radius: 20px;
color: #ffffff;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.widget-header h2 {
margin: 0;
font-size: 20px;
}
.form-section {
margin-bottom: 16px;
}
.form-section label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #888;
}
.amount-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.amount-header .balance {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #888;
}
.max-button {
padding: 2px 8px;
background: #7dd3fc;
border: none;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
color: #0d0d1a;
cursor: pointer;
}
input[type="number"] {
width: 100%;
padding: 16px;
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 12px;
font-size: 24px;
color: #ffffff;
}
input[type="number"]:focus {
outline: none;
border-color: #7dd3fc;
}
input[type="number"].error {
border-color: #ef4444;
}
.error-text {
display: block;
margin-top: 4px;
font-size: 12px;
color: #ef4444;
}
.selector {
position: relative;
}
.selector-trigger {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px 16px;
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 12px;
color: #ffffff;
cursor: pointer;
text-align: left;
}
.selector-trigger .logo {
width: 28px;
height: 28px;
border-radius: 50%;
}
.selector-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 12px;
max-height: 300px;
overflow-y: auto;
z-index: 100;
}
.search-input {
width: calc(100% - 24px);
margin: 12px;
padding: 10px 12px;
background: #0d0d1a;
border: 1px solid #2d2d44;
border-radius: 8px;
color: #ffffff;
}
.option {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px 16px;
background: transparent;
border: none;
color: #ffffff;
cursor: pointer;
text-align: left;
}
.option:hover {
background: #2d2d44;
}
.option.selected {
background: rgba(125, 211, 252, 0.2);
}
.destination-section {
padding: 16px;
background: #1a1a2e;
border-radius: 12px;
margin-bottom: 16px;
}
.quote-section {
padding: 16px;
background: #1a1a2e;
border-radius: 12px;
margin-bottom: 16px;
}
.quote-section.loading {
display: flex;
align-items: center;
gap: 12px;
}
.quote-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #2d2d44;
}
.quote-row:last-child {
border-bottom: none;
}
.quote-row .label {
color: #888;
}
.warning {
margin-top: 12px;
padding: 12px;
background: rgba(239, 68, 68, 0.1);
border-radius: 8px;
color: #ef4444;
font-size: 12px;
}
.badge {
display: inline-block;
margin-top: 12px;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
}
.badge.success {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.execution-status {
display: flex;
gap: 16px;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
}
.execution-status.success {
background: rgba(34, 197, 94, 0.1);
}
.execution-status.error {
background: rgba(239, 68, 68, 0.1);
}
.execution-status.pending {
background: rgba(125, 211, 252, 0.1);
}
.execute-button {
width: 100%;
padding: 16px;
background: #7dd3fc;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
color: #0d0d1a;
cursor: pointer;
transition: opacity 0.2s;
}
.execute-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.execute-button:not(:disabled):hover {
opacity: 0.9;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #2d2d44;
border-top-color: #7dd3fc;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.slippage-selector {
display: flex;
align-items: center;
gap: 8px;
}
.slippage-selector .label {
font-size: 12px;
color: #888;
}
.slippage-selector .presets {
display: flex;
gap: 4px;
}
.slippage-selector .preset {
padding: 4px 8px;
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 6px;
font-size: 12px;
color: #888;
cursor: pointer;
}
.slippage-selector .preset.active {
background: #7dd3fc;
border-color: #7dd3fc;
color: #0d0d1a;
}
Integration with Wagmi
The widget uses wagmi for wallet connection. Wrap your app:App.tsx
Copy
import { WagmiProvider, createConfig, http } from 'wagmi';
import { arbitrum, optimism, base, mainnet } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BridgeWidget } from './BridgeWidget';
const config = createConfig({
chains: [mainnet, arbitrum, optimism, base],
transports: {
[mainnet.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
[base.id]: http(),
},
});
const queryClient = new QueryClient();
export function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<BridgeWidget />
</QueryClientProvider>
</WagmiProvider>
);
}
Next Steps
Error Handling
Handle all error scenarios
Event Tracking
Real-time status updates
