> For the complete documentation index, see [llms.txt](https://docs.switchboard.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.switchboard.xyz/docs-by-chain/sui/surge/surge-tutorial.md).

# Surge Tutorial

> **Example Code**: The complete working example for this tutorial is available at [sb-on-demand-examples/sui/surge/basic](https://github.com/switchboard-xyz/sb-on-demand-examples/tree/main/sui/surge/basic)

This tutorial shows you how to stream real-time price data via WebSocket using Switchboard Surge and submit updates to the Sui blockchain. This approach is ideal for applications requiring sub-second price updates.

> **Version source of truth:** [SDK Version Matrix](/tooling/sdk-version-matrix.md)

## What You'll Build

A TypeScript application that:

* Connects to Switchboard Surge for real-time price streaming via WebSocket
* Receives signed price updates with sub-second latency
* Submits price updates to the Sui blockchain
* Tracks latency statistics and oracle performance

## Prerequisites

* Sui CLI installed ([Installation Guide](https://docs.sui.io/guides/developer/getting-started/sui-install))
* Node.js 21+ and npm/pnpm
* A Sui keypair with SUI tokens (in your Sui keystore) for signing Sui transactions
* A Solana keypair with an active Surge subscription ([subscribe here](https://explorer.switchboardlabs.xyz/subscriptions))

Surge subscriptions are currently Solana-only; you cannot subscribe with a Sui keypair yet.

## Key Concepts

### Surge vs On-Demand Quotes

| Feature          | On-Demand Quotes      | Surge Streaming       |
| ---------------- | --------------------- | --------------------- |
| Update frequency | Request-based         | Continuous (\~100ms)  |
| Latency          | Higher (HTTP request) | Lower (WebSocket)     |
| Use case         | Occasional reads      | Real-time apps        |
| Authentication   | None required         | Subscription required |

### The emitSurgeQuote Function

Surge provides the `emitSurgeQuote()` function from `@switchboard-xyz/sui-sdk` that converts Surge updates into Sui transactions. This handles:

* Oracle signature formatting
* Transaction building
* Quote verification setup

### Oracle Mapping

Surge returns oracle public keys, but Sui needs oracle object IDs. The example fetches a mapping from Crossbar to convert between these formats.

### Transaction Queue Management

Since Sui transactions are sequential, the example implements a queue to:

* Buffer incoming price updates
* Process one transaction at a time
* Track processing latency

## The Streaming Client

Here's the complete mainnet streaming example:

```typescript
import * as sb from '@switchboard-xyz/on-demand';
import { SuiClient } from '@mysten/sui/client';
import {
  SwitchboardClient,
  emitSurgeQuote,
} from '@switchboard-xyz/sui-sdk';
import { fromB64 } from '@mysten/bcs';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Connection, Keypair as SolanaKeypair } from '@solana/web3.js';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { Transaction } from '@mysten/sui/transactions';

// Initialize Sui clients
const suiClient = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const switchboardClient = new SwitchboardClient(suiClient);
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');

// Oracle mapping cache
const oracleMapping = new Map<string, string>();
let lastOracleFetch = 0;
const ORACLE_CACHE_TTL = 1000 * 60 * 10; // 10 minutes

// Transaction queue management
let isTransactionProcessing = false;
const rawResponseQueue: Array<{
  rawResponse: any;
  timestamp: number;
}> = [];

// Process transaction queue - ensures only one transaction at a time
async function processTransactionQueue(): Promise<void> {
  if (isTransactionProcessing || rawResponseQueue.length === 0) {
    return;
  }

  isTransactionProcessing = true;

  try {
    const queueItem = rawResponseQueue.shift()!;
    const { rawResponse, timestamp } = queueItem;

    console.log(`Processing transaction (queue length: ${rawResponseQueue.length})`);

    const transaction = new Transaction();

    // Convert Surge update to Sui transaction
    await emitSurgeQuote(switchboardClient, transaction, rawResponse);

    const result = await suiClient.signAndExecuteTransaction({
      transaction: transaction,
      signer: suiKeypair!,
      options: {
        showEvents: true,
        showEffects: true,
      },
    });

    const processingTime = Date.now() - timestamp;
    console.log(`Transaction completed in ${processingTime}ms`);
    console.log('Transaction result:', result.digest);
  } catch (error) {
    console.error('Transaction failed:', error);
  } finally {
    isTransactionProcessing = false;

    // Process next transaction in queue if any
    if (rawResponseQueue.length > 0) {
      setImmediate(() => processTransactionQueue());
    }
  }
}

// Fetch oracle mappings from Crossbar
async function fetchOracleMappings(): Promise<Map<string, string>> {
  const now = Date.now();

  if (oracleMapping.size > 0 && now - lastOracleFetch < ORACLE_CACHE_TTL) {
    return oracleMapping;
  }

  try {
    const response = await fetch('https://crossbar.switchboard.xyz/oracles/sui');
    const oracles = (await response.json()) as Array<{
      oracle_id: string;
      oracle_key: string;
    }>;

    oracleMapping.clear();
    for (const oracle of oracles) {
      const cleanKey = oracle.oracle_key.startsWith('0x')
        ? oracle.oracle_key.slice(2)
        : oracle.oracle_key;
      oracleMapping.set(cleanKey, oracle.oracle_id);
    }

    lastOracleFetch = now;
    console.log(`Loaded ${oracleMapping.size} oracle mappings`);
    return oracleMapping;
  } catch (error) {
    console.error('Failed to fetch oracle mappings:', error);
    return oracleMapping;
  }
}

// Calculate latency statistics
function calculateStatistics(latencies: number[]) {
  const sorted = [...latencies].sort((a, b) => a - b);
  const sum = sorted.reduce((a, b) => a + b, 0);

  return {
    min: sorted[0],
    max: sorted[sorted.length - 1],
    median: sorted[Math.floor(sorted.length / 2)],
    mean: sum / sorted.length,
    count: sorted.length,
  };
}

// Load Sui keypair (for signing Sui transactions)
let suiKeypair: Ed25519Keypair | null = null;

try {
  const keystorePath = path.join(os.homedir(), '.sui', 'sui_config', 'sui.keystore');
  const keystore = JSON.parse(fs.readFileSync(keystorePath, 'utf-8'));
  const secretKey = fromB64(keystore[0]);
  suiKeypair = Ed25519Keypair.fromSecretKey(secretKey.slice(1));
} catch (error) {
  console.error('Error loading Sui keypair:', error);
}

// Load Solana keypair (subscription owner)
let solanaKeypair: SolanaKeypair | null = null;

try {
  const solanaKeypairPath =
    process.env.SOLANA_KEYPAIR_PATH ||
    path.join(os.homedir(), '.config', 'solana', 'id.json');
  const secretKey = Uint8Array.from(
    JSON.parse(fs.readFileSync(solanaKeypairPath, 'utf-8'))
  );
  solanaKeypair = SolanaKeypair.fromSecretKey(secretKey);
} catch (error) {
  console.error('Error loading Solana keypair:', error);
}

if (!suiKeypair) {
  throw new Error('Sui keypair not loaded');
}

if (!solanaKeypair) {
  throw new Error('Solana keypair not loaded');
}

// Main function
(async function main() {
  console.log('Starting Surge streaming...');
  console.log(`Using Sui keypair: ${suiKeypair!.toSuiAddress()}`);
  console.log(`Using Solana keypair: ${solanaKeypair!.publicKey.toBase58()}`);

  const latencies: number[] = [];

  // Initialize Surge with Solana keypair (subscription owner)
  const surge = new sb.Surge({
    connection: solanaConnection,
    keypair: solanaKeypair!,
    signatureScheme: 'ed25519',
  });
  // Auth note: the SDK signs with your Solana keypair to authenticate the session.
  // If the keypair has no active Surge subscription, connectAndSubscribe will fail.

  // Connect and subscribe to feeds
  await surge.connectAndSubscribe([{ symbol: 'BTC/USD' }]);

  // Pre-fetch oracle mappings
  await fetchOracleMappings();

  // Listen for price updates
  surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
    const currentLatency = Date.now() - response.data.source_ts_ms;
    latencies.push(currentLatency);

    const rawResponse = response.getRawResponse();
    const stats = calculateStatistics(latencies);
    const formattedPrices = response.getFormattedPrices();
    const currentPrice = Object.values(formattedPrices)[0] || 'N/A';

    console.log(
      `Update #${stats.count} | Price: ${currentPrice} | Latency: ${currentLatency}ms | Avg: ${stats.mean.toFixed(1)}ms`
    );

    // Queue the update for processing
    rawResponseQueue.push({
      rawResponse,
      timestamp: Date.now(),
    });

    // Trigger queue processing
    processTransactionQueue();
  });

  console.log('Listening for price updates...');
})();
```

### Code Walkthrough

#### Setup

```typescript
const suiClient = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const switchboardClient = new SwitchboardClient(suiClient);
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');
```

Initialize the Sui client you will submit transactions through, plus a Solana RPC connection for Surge authentication. For Sui testnet, use `https://fullnode.testnet.sui.io:443`.

#### Creating Surge Connection

```typescript
const surge = new sb.Surge({
  connection: solanaConnection,
  keypair: solanaKeypair!,
  signatureScheme: 'ed25519',
});

await surge.connectAndSubscribe([{ symbol: 'BTC/USD' }]);
```

* `connection`: Your Solana `Connection` instance
* `keypair`: Your Solana keypair (must have an active Surge subscription)
* `signatureScheme`: Use `'ed25519'` for Solana keypairs
* `connectAndSubscribe()`: Connects and subscribes to specified feeds

#### Handling Updates

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const rawResponse = response.getRawResponse();
  const formattedPrices = response.getFormattedPrices();
  // ...
});
```

The `signedPriceUpdate` event fires whenever new price data arrives. Key methods:

* `getRawResponse()`: Returns the raw signed data for transaction submission
* `getFormattedPrices()`: Returns human-readable prices

#### Submitting to Sui

```typescript
const transaction = new Transaction();
await emitSurgeQuote(switchboardClient, transaction, rawResponse);

const result = await suiClient.signAndExecuteTransaction({
  transaction,
  signer: suiKeypair,
});
```

The `emitSurgeQuote()` function handles converting the Surge response into a valid Sui transaction.

## Mainnet vs Testnet

The mainnet and testnet examples are nearly identical with these differences:

| Setting        | Mainnet                               | Testnet                               |
| -------------- | ------------------------------------- | ------------------------------------- |
| RPC URL        | `https://fullnode.mainnet.sui.io:443` | `https://fullnode.testnet.sui.io:443` |
| Oracle Mapping | `/oracles/sui`                        | `/oracles/sui/testnet`                |

### Testnet Configuration

```typescript
// Testnet setup
const suiClient = new SuiClient({ url: 'https://fullnode.testnet.sui.io:443' });
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');

const surge = new sb.Surge({
  connection: solanaConnection,
  keypair: solanaKeypair!,
  signatureScheme: 'ed25519',
});

// Testnet oracle mapping endpoint
const response = await fetch('https://crossbar.switchboard.xyz/oracles/sui/testnet');
```

## Running the Examples

### 1. Clone the Repository

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples
cd sb-on-demand-examples/sui/surge/basic
```

### 2. Install Dependencies

```bash
npm install
```

### 3. Ensure Active Subscription

Your Solana keypair (default `~/.config/solana/id.json` or `SOLANA_KEYPAIR_PATH`) must have an active Surge subscription. The Sui keypair only signs Sui transactions. Subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions).

### 4. Run the Examples

```bash
# Mainnet streaming
npm run stream

# Testnet streaming
npm run stream:testnet
```

### Expected Output

```
Starting Surge streaming...
Using Sui keypair: 0x...
Using Solana keypair: 9k...
Loaded 15 oracle mappings
Listening for price updates...
Update #1 | Price: 97234.50 | Latency: 85ms | Avg: 85.0ms
Processing transaction (queue length: 0)
Transaction completed in 1234ms
Transaction result: 8Js7NsQ7...
Update #2 | Price: 97235.10 | Latency: 92ms | Avg: 88.5ms
...
```

## Adding to Your Project

### Dependencies

```bash
npm install @switchboard-xyz/on-demand@3.10.3 @switchboard-xyz/sui-sdk@0.1.16 @mysten/sui@1.38.0 @solana/web3.js@1.98.4
```

### Minimal Integration

This example assumes you already loaded a `solanaKeypair` (with an active Surge subscription) and a `suiKeypair` (for signing Sui transactions).

```typescript
import * as sb from '@switchboard-xyz/on-demand';
import { SuiClient } from '@mysten/sui/client';
import { SwitchboardClient, emitSurgeQuote } from '@switchboard-xyz/sui-sdk';
import { Transaction } from '@mysten/sui/transactions';
import { Connection } from '@solana/web3.js';

const suiClient = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const switchboardClient = new SwitchboardClient(suiClient);
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');

const surge = new sb.Surge({
  connection: solanaConnection,
  keypair: solanaKeypair, // Solana keypair with active Surge subscription
  signatureScheme: 'ed25519',
});

await surge.connectAndSubscribe([{ symbol: 'BTC/USD' }]);

surge.on('signedPriceUpdate', async (response) => {
  const tx = new Transaction();
  await emitSurgeQuote(switchboardClient, tx, response.getRawResponse());

  // Sign and send transaction
  await suiClient.signAndExecuteTransaction({
    transaction: tx,
    signer: suiKeypair,
  });
});
```

### Multiple Feeds

```typescript
await surge.connectAndSubscribe([
  { symbol: 'BTC/USD' },
  { symbol: 'ETH/USD' },
  { symbol: 'SOL/USD' },
]);
```

### Error Handling

```typescript
surge.on('error', (error) => {
  console.error('Surge error:', error);
});

surge.on('close', () => {
  console.log('Connection closed, attempting reconnect...');
  // Implement reconnection logic
});
```

## Performance Considerations

### Transaction Queue

The example uses a queue because:

* Sui transactions are sequential per sender
* Surge updates arrive faster than transactions complete
* Queuing prevents transaction conflicts

### Latency Optimization

* Keep your Sui node geographically close
* Use dedicated RPC endpoints for production
* Consider batching updates if latency isn't critical

### Oracle Mapping Cache

The oracle mapping is cached for 10 minutes to avoid repeated API calls. Adjust `ORACLE_CACHE_TTL` based on your needs.

## Troubleshooting

### "Keypair not loaded"

* Ensure you have a valid Sui keypair in `~/.sui/sui_config/sui.keystore`
* Run `sui client new-address ed25519` to create one
* Ensure you have a valid Solana keypair at `~/.config/solana/id.json` or `SOLANA_KEYPAIR_PATH`
* Run `solana-keygen new` to create one

### "Subscription not found" or connection rejected

* Ensure your Solana keypair has an active Surge subscription
* Subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions)

### "Oracle ID not found for key"

* The oracle mapping might be stale
* Force refresh by clearing `oracleMapping` and calling `fetchOracleMappings()`
* Check you're using the correct network (mainnet vs testnet)

### Transaction Failures

* Ensure your wallet has sufficient SUI for gas
* Check network connectivity
* Verify you're on the correct network

### High Latency

* Check your network connection
* Consider using a dedicated RPC endpoint
* Reduce logging if running in production

## Next Steps

* **Quote Verifier Pattern**: See the [Price Feeds](/docs-by-chain/sui/price-feeds.md) tutorial for verified on-chain price storage
* **Multiple Feeds**: Subscribe to multiple feeds for portfolio tracking
* **Custom Integration**: Use the price data to trigger your own Move contract logic
