Skip to main content
This guide demonstrates how to build user-friendly progress UIs that track the status of bridge, swap, and execute operations in real-time.

Overview

The Nexus SDK emits detailed step-by-step events during operations, allowing you to:
  • Show real-time progress to users
  • Display current step and remaining steps
  • Link to blockchain explorers for transparency
  • Handle errors gracefully with context
  • Build custom progress visualizations

Event System

All main operations emit two key events:

STEPS_LIST

Emitted once at the start with all expected steps

STEP_COMPLETE

Emitted each time a step completes

Basic Progress Tracker

1

Set up state

Create state to track steps and completion:
import { NEXUS_EVENTS, type BridgeStepType } from '@avail-project/nexus-core';

// State for tracking progress
let allSteps: BridgeStepType[] = [];
let completedSteps: Set<string> = new Set();
let currentStepIndex = 0;

function getProgress() {
  return {
    total: allSteps.length,
    completed: completedSteps.size,
    percentage: Math.round((completedSteps.size / allSteps.length) * 100),
  };
}
2

Handle events

Listen to events and update state:
const result = await sdk.bridge(params, {
  onEvent: (event) => {
    if (event.name === NEXUS_EVENTS.STEPS_LIST) {
      // Initialize with all expected steps
      allSteps = event.args;
      console.log(`Starting operation with ${allSteps.length} steps`);
      renderProgressUI();
    }

    if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {
      // Mark step as complete
      const step = event.args;
      completedSteps.add(step.typeID);
      currentStepIndex = completedSteps.size - 1;

      console.log(`Completed: ${step.type}`);
      renderProgressUI();

      // Handle specific steps
      if (step.data?.explorerURL) {
        console.log(`View on explorer: ${step.data.explorerURL}`);
      }
    }
  },
});
3

Render progress

Display progress to users:
function renderProgressUI() {
  const progress = getProgress();

  console.log(`Progress: ${progress.percentage}%`);
  console.log(`Steps: ${progress.completed}/${progress.total}`);

  allSteps.forEach((step, index) => {
    const isComplete = completedSteps.has(step.typeID);
    const isCurrent = index === currentStepIndex + 1;

    const status = isComplete ? '✓' : isCurrent ? '●' : '○';
    console.log(`${status} ${step.type}`);
  });
}

React Progress Component

A complete React example with visual progress:
import { useState, useEffect } from 'react';
import { NEXUS_EVENTS, type BridgeStepType } from '@avail-project/nexus-core';

interface ProgressTrackerProps {
  steps: BridgeStepType[];
  completedSteps: Set<string>;
}

export function ProgressTracker({ steps, completedSteps }: ProgressTrackerProps) {
  const progress = Math.round((completedSteps.size / steps.length) * 100);

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

      {/* Progress Text */}
      <p className="progress-text">
        {completedSteps.size} of {steps.length} steps complete ({progress}%)
      </p>

      {/* Step List */}
      <div className="steps-list">
        {steps.map((step, index) => {
          const isComplete = completedSteps.has(step.typeID);
          const isCurrent =
            !isComplete && completedSteps.size === index;

          return (
            <div
              key={step.typeID}
              className={`step ${
                isComplete ? 'complete' : isCurrent ? 'current' : 'pending'
              }`}
            >
              <div className="step-icon">
                {isComplete ? '✓' : isCurrent ? '●' : '○'}
              </div>
              <div className="step-content">
                <h4>{formatStepName(step.type)}</h4>
                {step.data?.chainName && (
                  <p className="step-chain">{step.data.chainName}</p>
                )}
                {step.data?.explorerURL && isComplete && (
                  <a
                    href={step.data.explorerURL}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="step-explorer"
                  >
                    View on Explorer →
                  </a>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function formatStepName(type: string): string {
  return type
    .split('_')
    .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
    .join(' ');
}

Bridge Step Types

Understand the steps emitted during bridge operations:
Step TypeDescriptionData Available
INTENT_ACCEPTEDIntent created and acceptedintentID
INTENT_HASH_SIGNEDUser signed the intent hash-
INTENT_SUBMITTEDIntent submitted to networkexplorerURL, txHash
INTENT_FULFILLEDIntent fulfilled by solver-
ALLOWANCE_USER_APPROVALWaiting for allowance approvalchainID, chainName
ALLOWANCE_APPROVAL_MINEDAllowance transaction minedchainID, explorerURL
ALLOWANCE_ALL_DONEAll allowances approved-
INTENT_DEPOSITDeposit on source chainchainID, amount, explorerURL
INTENT_DEPOSITS_CONFIRMEDAll deposits confirmed-
INTENT_COLLECTIONCollecting from sourcechainID
INTENT_COLLECTION_COMPLETEAll funds collected-
APPROVALToken approval for executionexplorerURL
TRANSACTION_SENTExecute transaction senttxHash, explorerURL
TRANSACTION_CONFIRMEDExecute transaction confirmedexplorerURL

Swap Step Types

Steps emitted during swap operations:
Step TypeDescription
SWAP_STARTSwap operation started
DETERMINING_SWAPCalculating optimal swap route
CREATE_PERMIT_EOA_TO_EPHEMERALCreating permit for ephemeral wallet
CREATE_PERMIT_FOR_SOURCE_SWAPCreating permit for source swap
SOURCE_SWAP_BATCH_TXExecuting source chain swaps
SOURCE_SWAP_HASHSource swap transaction hash
BRIDGE_DEPOSITBridge deposit for cross-chain swap
RFF_IDRequest for funds ID
DESTINATION_SWAP_BATCH_TXExecuting destination swaps
DESTINATION_SWAP_HASHDestination swap transaction hash
SWAP_COMPLETESwap completed successfully
SWAP_SKIPPEDSwap skipped (balance sufficient)

Advanced: Multi-Operation Tracker

Track multiple operations simultaneously:
import { NEXUS_EVENTS } from '@avail-project/nexus-core';

class OperationTracker {
  private operations = new Map<string, {
    steps: any[];
    completed: Set<string>;
    startTime: number;
  }>();

  startOperation(id: string) {
    this.operations.set(id, {
      steps: [],
      completed: new Set(),
      startTime: Date.now(),
    });
  }

  getEventHandler(operationId: string) {
    return (event: any) => {
      const op = this.operations.get(operationId);
      if (!op) return;

      if (event.name === NEXUS_EVENTS.STEPS_LIST) {
        op.steps = event.args;
        this.render();
      }

      if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {
        op.completed.add(event.args.typeID);
        this.render();
      }
    };
  }

  getProgress(operationId: string) {
    const op = this.operations.get(operationId);
    if (!op) return null;

    return {
      total: op.steps.length,
      completed: op.completed.size,
      percentage: Math.round((op.completed.size / op.steps.length) * 100),
      duration: Date.now() - op.startTime,
    };
  }

  render() {
    console.clear();
    console.log('Active Operations:');
    console.log('==================\n');

    for (const [id, op] of this.operations.entries()) {
      const progress = this.getProgress(id)!;
      console.log(`${id}: ${progress.percentage}% (${progress.completed}/${progress.total})`);
      console.log(`Duration: ${(progress.duration / 1000).toFixed(1)}s\n`);
    }
  }

  complete(operationId: string) {
    this.operations.delete(operationId);
    this.render();
  }
}

// Usage
const tracker = new OperationTracker();

async function executeBridge(params: any) {
  const operationId = `bridge-${Date.now()}`;
  tracker.startOperation(operationId);

  try {
    await sdk.bridge(params, {
      onEvent: tracker.getEventHandler(operationId),
    });
  } finally {
    tracker.complete(operationId);
  }
}

Vue.js Example

For Vue developers:
<template>
  <div class="progress-container" v-if="isLoading">
    <div class="progress-bar">
      <div class="progress-fill" :style="{ width: `${progress}%` }" />
    </div>

    <p class="progress-text">
      {{ completedSteps.size }} of {{ steps.length }} steps complete ({{ progress }}%)
    </p>

    <div class="steps-list">
      <div
        v-for="(step, index) in steps"
        :key="step.typeID"
        :class="[
          'step',
          completedSteps.has(step.typeID) ? 'complete' : '',
          !completedSteps.has(step.typeID) && completedSteps.size === index ? 'current' : '',
        ]"
      >
        <div class="step-icon">
          {{ completedSteps.has(step.typeID) ? '✓' : '●' }}
        </div>
        <div class="step-content">
          <h4>{{ formatStepName(step.type) }}</h4>
          <p v-if="step.data?.chainName" class="step-chain">{{ step.data.chainName }}</p>
          <a
            v-if="step.data?.explorerURL && completedSteps.has(step.typeID)"
            :href="step.data.explorerURL"
            target="_blank"
            class="step-explorer"
          >
            View on Explorer →
          </a>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { NEXUS_EVENTS, type BridgeStepType } from '@avail-project/nexus-core';

const steps = ref<BridgeStepType[]>([]);
const completedSteps = ref(new Set<string>());
const isLoading = ref(false);

const progress = computed(() =>
  Math.round((completedSteps.value.size / steps.value.length) * 100)
);

function formatStepName(type: string): string {
  return type
    .split('_')
    .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
    .join(' ');
}

async function executeBridge(sdk: any, params: any) {
  isLoading.value = true;

  try {
    await sdk.bridge(params, {
      onEvent: (event: any) => {
        if (event.name === NEXUS_EVENTS.STEPS_LIST) {
          steps.value = event.args;
        }
        if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {
          completedSteps.value.add(event.args.typeID);
        }
      },
    });
  } finally {
    isLoading.value = false;
  }
}
</script>

Best Practices

The STEPS_LIST event tells you all expected steps upfront. Use it to initialize your UI and show users what to expect.
Each step has a unique typeID. Use it (not the type name) to track completion, as some steps may repeat.
If an operation fails, display the last completed step and error context to help users understand what happened.
Track startTime and show elapsed time. Most operations complete in 30-120 seconds.

Next Steps

Basic Bridge

Learn bridge operations with events

Swap Examples

Implement swap progress tracking

Error Handling

Handle errors in progress UIs

Events Reference

Complete events API documentation

Build docs developers (and LLMs) love