Documentation Index
Fetch the complete documentation index at: https://docs.usemina.co/llms.txt
Use this file to discover all available pages before exploring further.
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
'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
.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
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
