Somnia Reactivity / Tutorials

Wildcard Off-Chain Reactivity Tutorial

Subscribe to all new logs over WebSocket and receive optional read-only call results with each event

This tutorial shows how to create an off-chain Reactivity subscription from TypeScript using the @somnia-chain/reactivity SDK. A wildcard subscription omits both address and topic filters, so the node pushes every new log it sees on the WebSocket connection.

Wildcard subscriptions are useful for quick testing and exploratory scripts. Production applications should usually add filters, as shown in the filtered subscriptions tutorial.

Off-chain subscriptions:

  • live only for the WebSocket connection that created them
  • are not stored on-chain
  • do not require a wallet client or gas
  • can include ethCalls whose raw return data is delivered with each event

For the full protocol reference, see Off-chain Reactivity.

Prerequisites

You'll need Node.js 20+.

Install the SDK and Viem:

npm install @somnia-chain/reactivity viem
npm install --save-dev tsx typescript @types/node

Step 1: Define the Chain

This example uses Somnia Testnet. You can switch to mainnet by changing the chain ID, native currency and RPC URLs to the mainnet values from Network Info.

import { defineChain } from 'viem';

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
      webSocket: ['wss://api.infra.testnet.somnia.network/ws'],
    },
  },
});

Step 2: Create the SDK

Off-chain reactivity requires a public client with a WebSocket transport. You do not need a wallet client because no transaction is signed.

import { SDK } from '@somnia-chain/reactivity';
import { createPublicClient, webSocket } from 'viem';

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: webSocket(somniaTestnet.rpcUrls.default.webSocket[0]),
});

const sdk = new SDK({ public: publicClient });

Step 3: Subscribe to All Logs

Pass ethCalls: [] when you only want log notifications. Omitting eventContractSources and topicOverrides makes the subscription wildcard.

const subscription = await sdk.subscribe({
  ethCalls: [],
  onData: (data) => {
    const event = data.result;

    console.log('Emitter:', event.address);
    console.log('Topics:', event.topics);
    console.log('Data:', event.data);
    console.log('Simulation results:', event.simulationResults);
  },
  onError: (error) => {
    console.error('Subscription error:', error);
  },
});

if (subscription instanceof Error) {
  throw subscription;
}

Each notification's event payload contains:

Field Meaning
address Contract address that emitted the log.
topics Event topics, with the event signature at topics[0].
data ABI-encoded non-indexed event data.
simulationResults Raw return data from configured ethCalls, in order.

Step 4: Decode a Known Event

A wildcard subscription receives many different event types. Decode only logs whose signature matches the ABI you are using. This example recognises ERC-20 Transfer logs.

import {
  decodeEventLog,
  erc20Abi,
  toEventSelector,
} from 'viem';

const transferTopic = toEventSelector('Transfer(address,address,uint256)');

function handleEvent(event: {
  address: `0x${string}`;
  topics: `0x${string}`[];
  data: `0x${string}`;
  simulationResults: `0x${string}`[];
}) {
  if (event.topics[0] !== transferTopic) {
    return;
  }

  const decoded = decodeEventLog({
    abi: erc20Abi,
    topics: event.topics,
    data: event.data,
  });

  console.log('ERC-20 Transfer:', decoded.args);
}

Then call handleEvent(data.result) inside onData.

Full Script

Save this as main.ts:

import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  decodeEventLog,
  defineChain,
  erc20Abi,
  toEventSelector,
  webSocket,
} from 'viem';

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
      webSocket: ['wss://api.infra.testnet.somnia.network/ws'],
    },
  },
});

const transferTopic = toEventSelector('Transfer(address,address,uint256)');

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: webSocket(somniaTestnet.rpcUrls.default.webSocket[0]),
});

const sdk = new SDK({ public: publicClient });

async function main() {
  const subscription = await sdk.subscribe({
    ethCalls: [],
    onData: (data) => {
      const event = data.result;

      console.log('Raw event:', event);

      if (event.topics[0] !== transferTopic) {
        return;
      }

      const decoded = decodeEventLog({
        abi: erc20Abi,
        topics: event.topics,
        data: event.data,
      });

      console.log('Decoded ERC-20 Transfer:', decoded.args);
    },
    onError: (error) => {
      console.error('Subscription error:', error);
    },
  });

  if (subscription instanceof Error) {
    throw subscription;
  }

  console.log('Subscribed:', subscription.subscriptionId);

  process.on('SIGINT', async () => {
    await subscription.unsubscribe();
    process.exit(0);
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Run it:

npx tsx main.ts

Next Steps

Add eventContractSources and topicOverrides once you know which contracts and events you want. Add ethCalls when you want the node to attach read-only simulation results to each pushed event.

Copied from docs.somnia.network (somnia-main-docs) by scripts/import-gitbook.mts, until it finds a home in a product's docs.Edit this page