# 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.9.0 @switchboard-xyz/sui-sdk@0.1.14 @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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.switchboard.xyz/docs-by-chain/sui/surge/surge-tutorial.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
