Skip to main content

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