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
ethCallswhose 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