SDK

Runtime Events

Observe transactions, slot ticks, account streaming, and runtime errors from inside a test. Rust exposes a channel; JS exposes a drain method.

The Surfnet runtime emits a stream of structured events covering transaction processing, slot ticks, account streaming, time travel, and errors. The SDK exposes this stream so tests can assert on runtime behavior without parsing logs.

Rust: Channel Receiver

Surfnet::events() returns a reference to the crossbeam_channel::Receiver<SimnetEvent> that the runtime publishes to. Drain it with try_iter() (non-blocking) or read individual events with recv() / recv_timeout().

use surfpool_sdk::{SimnetEvent, Surfnet};

let surfnet = Surfnet::start().await?;

// Drain whatever has accumulated so far.
for event in surfnet.events().try_iter() {
    match event {
        SimnetEvent::TransactionProcessed(_ts, metadata, error) => {
            println!("tx {} -> {:?}", metadata.signature, metadata.logs);
            if let Some(err) = error {
                eprintln!("  failed: {err:?}");
            }
        }
        SimnetEvent::ErrorLog(_ts, message) => {
            eprintln!("runtime error: {message}");
        }
        other => {
            println!("event: {other:?}");
        }
    }
}

Receiver is shared

If you spawn a background task to consume events, do not also call try_iter from the main thread. Events will be split unpredictably between the two consumers.

JS: Drain Method

The JS bindings buffer events internally and expose them through drainEvents. Each call returns whatever has accumulated since the previous call, then clears the buffer.

import { Surfnet } from "@solana/surfpool";

const surfnet = Surfnet.start();
try {
  // ... do test setup that triggers events ...

  const events = surfnet.drainEvents();
  for (const event of events) {
    switch (event.kind) {
      case "transactionProcessed":
        console.log("tx", event.transactionSignature, event.logs);
        break;
      case "errorLog":
        console.error("runtime error:", event.message);
        break;
      default:
        console.log(event.kind, event.message ?? "");
    }
  }
} finally {
  surfnet.stop();
}

Unbounded buffer

If you never call drainEvents, the buffer grows for the life of the instance. That's fine for short tests, but call it periodically for long-running fixtures so memory doesn't balloon.

SimnetEventValue Shape (JS)

The JS event type is a flat object with a kind discriminator and optional fields. Not every field is set on every variant — destructure based on kind.

interface SimnetEventValue {
  kind: string;                     // discriminator, e.g. "TransactionProcessed"
  message?: string;                 // human-readable message
  timestamp?: string;               // ISO timestamp

  // startup
  initialTransactionCount?: number;
  clock?: ClockValue;

  // slot / clock
  epochInfo?: EpochInfoValue;
  clockCommand?: string;
  slotIntervalMs?: number;

  // accounts
  accountPubkey?: string;

  // transactions
  transactionSignature?: string;
  logs?: string[];
  computeUnitsConsumed?: number;
  fee?: number;

  // errors
  errorMessage?: string;

  // misc
  tag?: string;
  profileKey?: string;
  profileSlot?: number;
  runbookId?: string;
  runbookErrors?: string[];
}

Event Kinds

Kind strings on the JS side are camelCase; the matching Rust variant uses PascalCase. Match on these exact strings — they're case-sensitive and stable across versions.

JS kindRust variantEmitted when
readySimnetEvent::ReadyThe runtime finishes booting; carries initialTransactionCount.
connectedSimnetEvent::ConnectedConnected to the upstream remote RPC; carries message (the URL).
abortedSimnetEvent::AbortedStartup was aborted; carries message (the reason).
shutdownSimnetEvent::ShutdownA graceful shutdown begins.
systemClockUpdatedSimnetEvent::SystemClockUpdatedThe system clock advanced; carries clock.
clockUpdateSimnetEvent::ClockUpdateA clock command ran (pause/resume/update interval); carries clockCommand and optionally slotIntervalMs.
epochInfoUpdateSimnetEvent::EpochInfoUpdateA new epoch info snapshot is available; carries epochInfo.
blockHashExpiredSimnetEvent::BlockHashExpiredA blockhash that was in use has expired.
transactionReceivedSimnetEvent::TransactionReceivedA transaction was queued; carries timestamp, transactionSignature.
transactionProcessedSimnetEvent::TransactionProcessedA transaction was executed; carries timestamp, transactionSignature, logs, computeUnitsConsumed, fee, and errorMessage on failure.
accountUpdateSimnetEvent::AccountUpdateAn account changed — covers streamed accounts and cheatcode-driven mutations; carries timestamp and accountPubkey.
infoLog / warnLog / errorLog / debugLogSimnetEvent::InfoLog / WarnLog / ErrorLog / DebugLogThe runtime emitted a log line; carries timestamp and message.
pluginLoadedSimnetEvent::PluginLoadedA Geyser plugin loaded; carries message (the plugin name).
taggedProfileSimnetEvent::TaggedProfileA profiler result was tagged; carries tag, profileKey, profileSlot, logs, computeUnitsConsumed.
runbookStarted / runbookCompletedSimnetEvent::RunbookStarted / RunbookCompletedA runbook started or finished; carries runbookId and (on completion) optional runbookErrors.

Assertion Patterns

Assert A Transaction Landed

import { Surfnet } from "@solana/surfpool";

const surfnet = Surfnet.start();
try {
  const sig = await sendTransaction(surfnet.rpcUrl /* ... */);
  // ... wait for confirmation via RPC ...

  const landed = surfnet
    .drainEvents()
    .some(e => e.kind === "transactionProcessed" && e.transactionSignature === sig);

  console.assert(landed, "expected transaction to land");
} finally {
  surfnet.stop();
}

Assert No Runtime Errors Occurred

use surfpool_sdk::{SimnetEvent, Surfnet};

let mut surfnet = Surfnet::start().await?;

// ... run the test ...

let errors: Vec<_> = surfnet
    .events()
    .try_iter()
    .filter(|e| matches!(e, SimnetEvent::ErrorLog(_, _)))
    .collect();

assert!(errors.is_empty(), "runtime errors: {errors:?}");

surfnet.stop()?;

Why Events Beat Log Parsing

Prefer the event stream over stdout

The runtime prints structured events through tracing, which can be tempting to capture and parse from stdout. Don't. The structured event stream:

  • Survives log-level changes — events are emitted regardless of RUST_LOG.
  • Comes with typed payloads — no string parsing, no format drift between versions.
  • Has stable kinds — log lines are formatted for humans and change freely; event kinds are part of the SDK contract.

On this page