Skip to main content

Event Tracking

This example demonstrates how to subscribe to SDK events to build real-time UI updates, progress indicators, and transaction link displays.

Overview

The Mina SDK provides an event system that emits updates throughout the bridge execution process:
  • Quote updates
  • Execution started
  • Step changes (approval, swap, bridge, deposit)
  • Transaction sent/confirmed
  • Deposit started/completed
  • Execution completed/failed
  • Overall status changes

Available Events

EventPayloadDescription
quoteUpdated{ quoteId, timestamp }Quote has been refreshed
executionStarted{ executionId, quoteId, timestamp }Bridge execution has begun
stepChangedStepStatusPayloadIndividual step status changed
approvalRequired{ tokenAddress, amount, spender }Token approval is needed
transactionSent{ txHash, chainId, stepType }Transaction submitted to chain
transactionConfirmed{ txHash, chainId, stepType }Transaction confirmed on chain
depositStarted{ amount, walletAddress }L1 deposit process started
depositCompleted{ txHash, amount }L1 deposit completed
executionCompleted{ executionId, txHash, receivedAmount }Full execution successful
executionFailed{ executionId, error, step }Execution failed at a step
statusChangedTransactionStatusPayloadOverall status updated

Subscribing to Events

import { Mina, SDK_EVENTS } from '@siphoyawe/mina-sdk';
import type {
  StepStatusPayload,
  TransactionStatusPayload,
} from '@siphoyawe/mina-sdk';

const mina = new Mina({ integrator: 'my-app' });

// Subscribe to step changes
mina.on(SDK_EVENTS.STEP_CHANGED, (step: StepStatusPayload) => {
  console.log(`Step: ${step.step}`);
  console.log(`Status: ${step.status}`);
  console.log(`TxHash: ${step.txHash}`);
});

// Subscribe to overall status changes
mina.on(SDK_EVENTS.STATUS_CHANGED, (status: TransactionStatusPayload) => {
  console.log(`Overall status: ${status.status}`);
  console.log(`Progress: ${status.progress}%`);
  console.log(`Current step: ${status.currentStep}/${status.totalSteps}`);
});

// Subscribe to execution completion
mina.on(SDK_EVENTS.EXECUTION_COMPLETED, (result) => {
  console.log('Bridge completed!');
  console.log(`TxHash: ${result.txHash}`);
  console.log(`Received: ${result.receivedAmount}`);
});

// Subscribe to execution failure
mina.on(SDK_EVENTS.EXECUTION_FAILED, (failure) => {
  console.error(`Bridge failed at step: ${failure.step}`);
  console.error(`Error: ${failure.error.message}`);
});

// Subscribe once (auto-unsubscribes after first event)
mina.once(SDK_EVENTS.TRANSACTION_SENT, (tx) => {
  console.log(`First transaction sent: ${tx.txHash}`);
});

// Unsubscribe from events
const stepHandler = (step: StepStatusPayload) => console.log(step);
mina.on(SDK_EVENTS.STEP_CHANGED, stepHandler);
// Later...
mina.off(SDK_EVENTS.STEP_CHANGED, stepHandler);

React Hook for Event Tracking

import { useEffect, useCallback, useState, useRef } from 'react';
import { useMina } from '@siphoyawe/mina-sdk/react';
import { SDK_EVENTS } from '@siphoyawe/mina-sdk';
import type {
  StepStatusPayload,
  TransactionStatusPayload,
} from '@siphoyawe/mina-sdk';

interface BridgeStep {
  id: string;
  type: string;
  status: 'pending' | 'active' | 'completed' | 'failed';
  txHash: string | null;
  error: string | null;
  timestamp: number;
}

interface BridgeProgress {
  status: 'idle' | 'pending' | 'in_progress' | 'completed' | 'failed';
  progress: number;
  currentStep: number;
  totalSteps: number;
  estimatedTime: number;
  steps: BridgeStep[];
  txHash: string | null;
  receivingTxHash: string | null;
  error: string | null;
}

const initialProgress: BridgeProgress = {
  status: 'idle',
  progress: 0,
  currentStep: 0,
  totalSteps: 0,
  estimatedTime: 0,
  steps: [],
  txHash: null,
  receivingTxHash: null,
  error: null,
};

export function useBridgeEvents() {
  const { mina, isReady } = useMina();
  const [progress, setProgress] = useState<BridgeProgress>(initialProgress);
  const stepsRef = useRef<BridgeStep[]>([]);

  // Reset progress
  const reset = useCallback(() => {
    stepsRef.current = [];
    setProgress(initialProgress);
  }, []);

  // Event handlers
  useEffect(() => {
    if (!mina || !isReady) return;

    // Execution started
    const handleExecutionStarted = () => {
      stepsRef.current = [];
      setProgress({
        ...initialProgress,
        status: 'pending',
      });
    };

    // Step changed
    const handleStepChanged = (step: StepStatusPayload) => {
      const existingIndex = stepsRef.current.findIndex((s) => s.id === step.stepId);

      const newStep: BridgeStep = {
        id: step.stepId,
        type: step.step,
        status: step.status,
        txHash: step.txHash,
        error: step.error?.message ?? null,
        timestamp: step.timestamp,
      };

      if (existingIndex >= 0) {
        stepsRef.current[existingIndex] = newStep;
      } else {
        stepsRef.current.push(newStep);
      }

      setProgress((prev) => ({
        ...prev,
        steps: [...stepsRef.current],
      }));
    };

    // Status changed
    const handleStatusChanged = (status: TransactionStatusPayload) => {
      setProgress((prev) => ({
        ...prev,
        status: status.status,
        progress: status.progress,
        currentStep: status.currentStep,
        totalSteps: status.totalSteps,
        estimatedTime: status.estimatedTime,
        txHash: status.txHash,
        receivingTxHash: status.receivingTxHash,
      }));
    };

    // Transaction sent
    const handleTransactionSent = (tx: { txHash: string; stepType: string }) => {
      setProgress((prev) => ({
        ...prev,
        txHash: tx.txHash,
      }));
    };

    // Execution completed
    const handleCompleted = (result: { txHash: string; receivedAmount: string | null }) => {
      setProgress((prev) => ({
        ...prev,
        status: 'completed',
        progress: 100,
        txHash: result.txHash,
      }));
    };

    // Execution failed
    const handleFailed = (failure: { error: Error; step: string | null }) => {
      setProgress((prev) => ({
        ...prev,
        status: 'failed',
        error: failure.error.message,
      }));
    };

    // Subscribe to events
    mina.on(SDK_EVENTS.EXECUTION_STARTED, handleExecutionStarted);
    mina.on(SDK_EVENTS.STEP_CHANGED, handleStepChanged);
    mina.on(SDK_EVENTS.STATUS_CHANGED, handleStatusChanged);
    mina.on(SDK_EVENTS.TRANSACTION_SENT, handleTransactionSent);
    mina.on(SDK_EVENTS.EXECUTION_COMPLETED, handleCompleted);
    mina.on(SDK_EVENTS.EXECUTION_FAILED, handleFailed);

    // Cleanup on unmount
    return () => {
      mina.off(SDK_EVENTS.EXECUTION_STARTED, handleExecutionStarted);
      mina.off(SDK_EVENTS.STEP_CHANGED, handleStepChanged);
      mina.off(SDK_EVENTS.STATUS_CHANGED, handleStatusChanged);
      mina.off(SDK_EVENTS.TRANSACTION_SENT, handleTransactionSent);
      mina.off(SDK_EVENTS.EXECUTION_COMPLETED, handleCompleted);
      mina.off(SDK_EVENTS.EXECUTION_FAILED, handleFailed);
    };
  }, [mina, isReady]);

  return { progress, reset };
}

Progress Tracker Component

import type { ReactNode } from 'react';

interface BridgeStep {
  id: string;
  type: string;
  status: 'pending' | 'active' | 'completed' | 'failed';
  txHash: string | null;
  error: string | null;
  timestamp: number;
}

interface BridgeProgress {
  status: 'idle' | 'pending' | 'in_progress' | 'completed' | 'failed';
  progress: number;
  currentStep: number;
  totalSteps: number;
  estimatedTime: number;
  steps: BridgeStep[];
  txHash: string | null;
  receivingTxHash: string | null;
  error: string | null;
}

interface ProgressTrackerProps {
  progress: BridgeProgress;
}

export function ProgressTracker({ progress }: ProgressTrackerProps) {
  if (progress.status === 'idle') return null;

  return (
    <div className="progress-tracker">
      {/* Progress Bar */}
      <div className="progress-bar-container">
        <div
          className={`progress-bar ${progress.status}`}
          style={{ width: `${progress.progress}%` }}
        />
      </div>

      {/* Progress Text */}
      <div className="progress-info">
        <span className="progress-percent">{progress.progress}%</span>
        {progress.estimatedTime > 0 && (
          <span className="time-remaining">
            ~{formatTime(progress.estimatedTime)} remaining
          </span>
        )}
      </div>

      {/* Steps Timeline */}
      <div className="steps-timeline">
        {progress.steps.map((step, index) => (
          <StepItem
            key={step.id}
            step={step}
            isLast={index === progress.steps.length - 1}
          />
        ))}
      </div>

      {/* Transaction Links */}
      {progress.txHash && (
        <TransactionLinks
          bridgeTxHash={progress.txHash}
          receivingTxHash={progress.receivingTxHash}
        />
      )}

      {/* Error Display */}
      {progress.error && (
        <div className="progress-error">
          <span className="error-icon">!</span>
          <span>{progress.error}</span>
        </div>
      )}

      {/* Completion Message */}
      {progress.status === 'completed' && (
        <div className="completion-message">
          <span className="success-icon">OK</span>
          <span>Bridge completed successfully!</span>
        </div>
      )}
    </div>
  );
}

interface StepItemProps {
  step: BridgeStep;
  isLast: boolean;
}

function StepItem({ step, isLast }: StepItemProps) {
  const stepLabels: Record<string, string> = {
    approval: 'Approve Token',
    swap: 'Swap Tokens',
    bridge: 'Bridge to HyperEVM',
    deposit: 'Deposit to L1',
  };

  return (
    <div className={`step-item ${step.status}`}>
      <div className="step-indicator">
        <StepIcon status={step.status} />
        {!isLast && <div className="step-line" />}
      </div>

      <div className="step-content">
        <span className="step-label">{stepLabels[step.type] ?? step.type}</span>
        <span className="step-status">{getStatusText(step.status)}</span>

        {step.txHash && (
          <a
            href={getExplorerUrl(step.txHash, step.type)}
            target="_blank"
            rel="noopener noreferrer"
            className="step-tx-link"
          >
            View transaction
          </a>
        )}

        {step.error && (
          <span className="step-error">{step.error}</span>
        )}
      </div>
    </div>
  );
}

function StepIcon({ status }: { status: BridgeStep['status'] }) {
  switch (status) {
    case 'completed':
      return (
        <div className="step-icon completed">
          <svg viewBox="0 0 16 16" fill="currentColor">
            <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" />
          </svg>
        </div>
      );
    case 'active':
      return (
        <div className="step-icon active">
          <div className="spinner" />
        </div>
      );
    case 'failed':
      return (
        <div className="step-icon failed">
          <svg viewBox="0 0 16 16" fill="currentColor">
            <path d="M4.47 4.47a.75.75 0 011.06 0L8 6.94l2.47-2.47a.75.75 0 111.06 1.06L9.06 8l2.47 2.47a.75.75 0 11-1.06 1.06L8 9.06l-2.47 2.47a.75.75 0 01-1.06-1.06L6.94 8 4.47 5.53a.75.75 0 010-1.06z" />
          </svg>
        </div>
      );
    default:
      return <div className="step-icon pending" />;
  }
}

interface TransactionLinksProps {
  bridgeTxHash: string;
  receivingTxHash: string | null;
}

function TransactionLinks({ bridgeTxHash, receivingTxHash }: TransactionLinksProps) {
  return (
    <div className="transaction-links">
      <a
        href={`https://arbiscan.io/tx/${bridgeTxHash}`}
        target="_blank"
        rel="noopener noreferrer"
        className="tx-link"
      >
        <span>Source Transaction</span>
        <ExternalLinkIcon />
      </a>

      {receivingTxHash && (
        <a
          href={`https://hyperevmscan.io/tx/${receivingTxHash}`}
          target="_blank"
          rel="noopener noreferrer"
          className="tx-link"
        >
          <span>Destination Transaction</span>
          <ExternalLinkIcon />
        </a>
      )}
    </div>
  );
}

function ExternalLinkIcon() {
  return (
    <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
      <path d="M3.5 3a.5.5 0 01.5-.5h5a.5.5 0 01.5.5v5a.5.5 0 01-1 0V4.207L3.854 8.854a.5.5 0 01-.708-.708L7.793 3.5H4a.5.5 0 01-.5-.5z" />
    </svg>
  );
}

function formatTime(seconds: number): string {
  if (seconds < 60) return `${seconds}s`;
  if (seconds < 3600) return `${Math.ceil(seconds / 60)}m`;
  return `${Math.ceil(seconds / 3600)}h`;
}

function getStatusText(status: BridgeStep['status']): string {
  const texts: Record<string, string> = {
    pending: 'Waiting',
    active: 'Processing',
    completed: 'Complete',
    failed: 'Failed',
  };
  return texts[status] ?? status;
}

function getExplorerUrl(txHash: string, stepType: string): string {
  // Return appropriate explorer based on step type
  if (stepType === 'deposit') {
    return `https://hyperevmscan.io/tx/${txHash}`;
  }
  // Default to source chain explorer (e.g., Arbiscan)
  return `https://arbiscan.io/tx/${txHash}`;
}

Cleanup on Unmount

Always clean up event listeners when components unmount to prevent memory leaks:
CleanupExample.tsx
import { useEffect } from 'react';
import { useMina } from '@siphoyawe/mina-sdk/react';
import { SDK_EVENTS } from '@siphoyawe/mina-sdk';

function BridgeMonitor() {
  const { mina, isReady } = useMina();

  useEffect(() => {
    if (!mina || !isReady) return;

    // Define handlers
    const handleStepChange = (step) => {
      console.log('Step changed:', step);
    };

    const handleComplete = (result) => {
      console.log('Completed:', result);
    };

    // Subscribe
    mina.on(SDK_EVENTS.STEP_CHANGED, handleStepChange);
    mina.on(SDK_EVENTS.EXECUTION_COMPLETED, handleComplete);

    // Cleanup function - IMPORTANT!
    return () => {
      mina.off(SDK_EVENTS.STEP_CHANGED, handleStepChange);
      mina.off(SDK_EVENTS.EXECUTION_COMPLETED, handleComplete);
    };
  }, [mina, isReady]);

  return <div>Monitoring bridge events...</div>;
}

Using useTransactionStatus Hook

For simpler status tracking, use the built-in hook:
SimpleStatusTracking.tsx
import { useState } from 'react';
import { useMina, useTransactionStatus } from '@siphoyawe/mina-sdk/react';

function SimpleBridgeStatus() {
  const { mina } = useMina();
  const [txHash, setTxHash] = useState<string | null>(null);

  // Hook automatically polls for status updates
  const { status, isLoading, error } = useTransactionStatus(txHash);

  const handleExecute = async () => {
    const result = await mina.execute({ quote, signer });
    if (result.txHash) {
      setTxHash(result.txHash);
    }
  };

  return (
    <div>
      <button onClick={handleExecute}>Bridge</button>

      {isLoading && <p>Loading status...</p>}
      {error && <p>Error: {error.message}</p>}

      {status && (
        <div>
          <p>Status: {status.status}</p>
          <p>Steps: {status.steps.length}</p>
          {status.steps.map((step, i) => (
            <div key={i}>
              {step.stepId}: {step.status}
              {step.txHash && (
                <a href={`https://arbiscan.io/tx/${step.txHash}`}>
                  View
                </a>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Best Practices

When working with SDK events:
  1. Always unsubscribe from events on component unmount
  2. Use refs to track mutable state in event handlers
  3. Avoid subscribing/unsubscribing on every render
  4. Handle all terminal states (completed, failed)
  5. Show transaction links as soon as available
  6. Provide clear progress feedback to users

Next Steps

Error Handling

Handle execution errors

Full Integration

Complete bridge widget