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.
Token Selection
This example demonstrates how to build React components for selecting source chains and tokens, including filtering tokens by balance.Overview
A bridge interface typically needs:- A chain selector dropdown
- A token selector with search and balance display
- State management to connect selections to the quote system
Chain Selection Component
This component displays available source chains with logos and names.import { useState, useEffect } from 'react';
import { useMina } from '@siphoyawe/mina-sdk/react';
import type { Chain } from '@siphoyawe/mina-sdk/react';
interface ChainSelectorProps {
selectedChainId: number | null;
onSelect: (chain: Chain) => void;
}
export function ChainSelector({ selectedChainId, onSelect }: ChainSelectorProps) {
const { mina, isReady } = useMina();
const [chains, setChains] = useState<Chain[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
// Fetch available chains on mount
useEffect(() => {
if (!mina || !isReady) return;
async function fetchChains() {
setIsLoading(true);
try {
// Get chains with valid routes to HyperEVM
const availableChains = await mina.getChainsByRoutes();
setChains(availableChains);
} catch (error) {
console.error('Failed to fetch chains:', error);
} finally {
setIsLoading(false);
}
}
fetchChains();
}, [mina, isReady]);
// Find the selected chain object
const selectedChain = chains.find((c) => c.id === selectedChainId);
if (isLoading) {
return (
<div className="chain-selector loading">
<span>Loading chains...</span>
</div>
);
}
return (
<div className="chain-selector">
{/* Selected Chain Display / Trigger Button */}
<button
className="chain-selector-trigger"
onClick={() => setIsOpen(!isOpen)}
type="button"
>
{selectedChain ? (
<>
<img
src={selectedChain.logoUrl}
alt={selectedChain.name}
className="chain-logo"
/>
<span>{selectedChain.name}</span>
</>
) : (
<span>Select a chain</span>
)}
<ChevronDownIcon />
</button>
{/* Dropdown List */}
{isOpen && (
<div className="chain-selector-dropdown">
{chains.map((chain) => (
<button
key={chain.id}
className={`chain-option ${chain.id === selectedChainId ? 'selected' : ''}`}
onClick={() => {
onSelect(chain);
setIsOpen(false);
}}
type="button"
>
<img
src={chain.logoUrl}
alt={chain.name}
className="chain-logo"
/>
<span className="chain-name">{chain.name}</span>
<span className="chain-id">ID: {chain.id}</span>
</button>
))}
</div>
)}
</div>
);
}
// Simple chevron icon component
function ChevronDownIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="2" fill="none" />
</svg>
);
}
Token Selection Component
This component displays available tokens for a selected chain with balance information.import { useState, useEffect, useMemo } from 'react';
import { useMina, useTokenBalance } from '@siphoyawe/mina-sdk/react';
import type { Token } from '@siphoyawe/mina-sdk/react';
interface TokenSelectorProps {
chainId: number | null;
walletAddress: string | null;
selectedToken: Token | null;
onSelect: (token: Token) => void;
showOnlyWithBalance?: boolean;
}
export function TokenSelector({
chainId,
walletAddress,
selectedToken,
onSelect,
showOnlyWithBalance = false,
}: TokenSelectorProps) {
const { mina, isReady } = useMina();
const [tokens, setTokens] = useState<Token[]>([]);
const [balances, setBalances] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// Fetch tokens when chain changes
useEffect(() => {
if (!mina || !isReady || !chainId) {
setTokens([]);
return;
}
async function fetchTokens() {
setIsLoading(true);
try {
// Get bridgeable tokens (only tokens that can bridge to HyperEVM)
const bridgeableTokens = await mina.getBridgeableTokens(chainId);
setTokens(bridgeableTokens);
} catch (error) {
console.error('Failed to fetch tokens:', error);
} finally {
setIsLoading(false);
}
}
fetchTokens();
}, [mina, isReady, chainId]);
// Fetch balances for all tokens
useEffect(() => {
if (!mina || !isReady || !chainId || !walletAddress || tokens.length === 0) {
return;
}
async function fetchBalances() {
const balanceMap: Record<string, string> = {};
// Fetch balances in parallel (batch of 5 at a time)
const batchSize = 5;
for (let i = 0; i < tokens.length; i += batchSize) {
const batch = tokens.slice(i, i + batchSize);
const results = await Promise.allSettled(
batch.map((token) =>
mina.getBalance(chainId, token.address, walletAddress)
)
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
balanceMap[batch[index].address] = result.value.formatted;
}
});
}
setBalances(balanceMap);
}
fetchBalances();
}, [mina, isReady, chainId, walletAddress, tokens]);
// Filter and sort tokens
const filteredTokens = useMemo(() => {
let filtered = tokens;
// Filter by search query
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(
(token) =>
token.symbol.toLowerCase().includes(query) ||
token.name.toLowerCase().includes(query)
);
}
// Filter to only tokens with balance
if (showOnlyWithBalance) {
filtered = filtered.filter((token) => {
const balance = balances[token.address];
return balance && parseFloat(balance) > 0;
});
}
// Sort by balance (tokens with balance first, then alphabetically)
return filtered.sort((a, b) => {
const balanceA = parseFloat(balances[a.address] || '0');
const balanceB = parseFloat(balances[b.address] || '0');
if (balanceA > 0 && balanceB === 0) return -1;
if (balanceA === 0 && balanceB > 0) return 1;
if (balanceA !== balanceB) return balanceB - balanceA;
return a.symbol.localeCompare(b.symbol);
});
}, [tokens, searchQuery, showOnlyWithBalance, balances]);
if (!chainId) {
return (
<div className="token-selector disabled">
<span>Select a chain first</span>
</div>
);
}
return (
<div className="token-selector">
{/* Selected Token Display */}
<button
className="token-selector-trigger"
onClick={() => setIsOpen(!isOpen)}
type="button"
disabled={isLoading}
>
{selectedToken ? (
<>
<img
src={selectedToken.logoUrl}
alt={selectedToken.symbol}
className="token-logo"
/>
<div className="token-info">
<span className="token-symbol">{selectedToken.symbol}</span>
{balances[selectedToken.address] && (
<span className="token-balance">
{balances[selectedToken.address]}
</span>
)}
</div>
</>
) : (
<span>{isLoading ? 'Loading tokens...' : 'Select token'}</span>
)}
</button>
{/* Dropdown with Search */}
{isOpen && (
<div className="token-selector-dropdown">
{/* Search Input */}
<div className="token-search">
<input
type="text"
placeholder="Search tokens..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
autoFocus
/>
</div>
{/* Token List */}
<div className="token-list">
{filteredTokens.length === 0 ? (
<div className="no-tokens">
{searchQuery ? 'No tokens match your search' : 'No tokens available'}
</div>
) : (
filteredTokens.map((token) => (
<TokenOption
key={token.address}
token={token}
balance={balances[token.address]}
isSelected={selectedToken?.address === token.address}
onSelect={() => {
onSelect(token);
setIsOpen(false);
setSearchQuery('');
}}
/>
))
)}
</div>
</div>
)}
</div>
);
}
// Individual token option component
interface TokenOptionProps {
token: Token;
balance?: string;
isSelected: boolean;
onSelect: () => void;
}
function TokenOption({ token, balance, isSelected, onSelect }: TokenOptionProps) {
const hasBalance = balance && parseFloat(balance) > 0;
return (
<button
className={`token-option ${isSelected ? 'selected' : ''} ${hasBalance ? 'has-balance' : ''}`}
onClick={onSelect}
type="button"
>
<img src={token.logoUrl} alt={token.symbol} className="token-logo" />
<div className="token-details">
<span className="token-symbol">{token.symbol}</span>
<span className="token-name">{token.name}</span>
</div>
<div className="token-balance-info">
{balance ? (
<>
<span className="balance-amount">{formatBalance(balance)}</span>
{token.priceUsd && (
<span className="balance-usd">
${(parseFloat(balance) * token.priceUsd).toFixed(2)}
</span>
)}
</>
) : (
<span className="balance-loading">...</span>
)}
</div>
</button>
);
}
// Helper to format balance display
function formatBalance(balance: string): string {
const num = parseFloat(balance);
if (num === 0) return '0';
if (num < 0.0001) return '<0.0001';
if (num < 1) return num.toFixed(4);
if (num < 1000) return num.toFixed(2);
if (num < 1000000) return `${(num / 1000).toFixed(2)}K`;
return `${(num / 1000000).toFixed(2)}M`;
}
Using with State Management
Here’s how to combine these components with React state:BridgeForm.tsx
import { useState } from 'react';
import { MinaProvider } from '@siphoyawe/mina-sdk/react';
import type { Chain, Token } from '@siphoyawe/mina-sdk/react';
import { ChainSelector } from './ChainSelector';
import { TokenSelector } from './TokenSelector';
import { useAccount } from 'wagmi'; // Or your wallet library
function BridgeForm() {
const { address: walletAddress } = useAccount();
// State for selections
const [sourceChain, setSourceChain] = useState<Chain | null>(null);
const [sourceToken, setSourceToken] = useState<Token | null>(null);
const [amount, setAmount] = useState('');
// Reset token when chain changes
const handleChainChange = (chain: Chain) => {
setSourceChain(chain);
setSourceToken(null); // Clear token selection
setAmount('');
};
return (
<div className="bridge-form">
<div className="form-section">
<label>From Chain</label>
<ChainSelector
selectedChainId={sourceChain?.id ?? null}
onSelect={handleChainChange}
/>
</div>
<div className="form-section">
<label>Token</label>
<TokenSelector
chainId={sourceChain?.id ?? null}
walletAddress={walletAddress ?? null}
selectedToken={sourceToken}
onSelect={setSourceToken}
showOnlyWithBalance={false}
/>
</div>
<div className="form-section">
<label>Amount</label>
<input
type="number"
placeholder="0.00"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={!sourceToken}
/>
</div>
{/* Display selected values */}
{sourceChain && sourceToken && amount && (
<div className="selection-summary">
Bridging {amount} {sourceToken.symbol} from {sourceChain.name} to HyperEVM
</div>
)}
</div>
);
}
// Wrap with MinaProvider at app level
export function App() {
return (
<MinaProvider config={{ integrator: 'my-app' }}>
<BridgeForm />
</MinaProvider>
);
}
Using the useTokenBalance Hook
For individual token balance display, use theuseTokenBalance hook:
SingleTokenBalance.tsx
import { useTokenBalance } from '@siphoyawe/mina-sdk/react';
interface BalanceDisplayProps {
chainId: number;
tokenAddress: string;
walletAddress: string;
}
export function BalanceDisplay({ chainId, tokenAddress, walletAddress }: BalanceDisplayProps) {
const {
formattedBalance,
symbol,
balanceUsd,
isLoading,
error,
refetch,
} = useTokenBalance({
chainId,
tokenAddress,
walletAddress,
refetchInterval: 15000, // Refresh every 15 seconds
});
if (isLoading && !formattedBalance) {
return <span className="balance">Loading...</span>;
}
if (error) {
return (
<span className="balance error">
Error loading balance
<button onClick={refetch}>Retry</button>
</span>
);
}
return (
<div className="balance-display">
<span className="balance">
{formattedBalance ?? '0'} {symbol}
</span>
{balanceUsd !== null && (
<span className="balance-usd">${balanceUsd.toFixed(2)}</span>
)}
</div>
);
}
Best Practices
When building token selectors, consider these UX best practices:
- Show tokens with balances at the top of the list
- Include token logos for visual recognition
- Display both symbol and name for clarity
- Show USD values when available
- Add search functionality for chains with many tokens
- Disable the token selector until a chain is selected
Next Steps
Quote Display
Show quote details and fees
Full Integration
Complete bridge widget
