Skip to main content

WebSocket subscriptions

Real-time chain events at wss://rpc.sentrixchain.com/ws. Standard eth_subscribe JSON-RPC plus Sentrix-native channels. Built so dApps using ethers.js, viem, web3.js work without special-casing Sentrix.

Shipped 2026-04-28 in PR #398 (Phase 1) + PR #399 (Phase 2+3).

Endpoints

NetworkURL
Mainnetwss://rpc.sentrixchain.com/ws
Testnetwss://testnet-rpc.sentrixchain.com/ws

Channels — eth_subscribe namespace (EVM-compat)

ChannelPayloadTrigger
newHeadsblock header (number, hash, parentHash, timestamp, miner, stateRoot, transactionsRoot, gasLimit, gasUsed, difficulty, nonce, extraData, size)every consensus-finalized block
logsfilterable contract event (address, topics, data, blockNumber, blockHash, transactionHash, transactionIndex, logIndex, removed)per-tx event emission
newPendingTransactionstx hash stringevery successful mempool admission
syncingalways false (Sentrix has no syncing mode)once on subscribe

Channels — sentrix_subscribe namespace (Sentrix-native)

ChannelPayloadTrigger
sentrix_finalized{ height, hash, justificationSigners }every BFT-finalized block — distinct from newHeads because Sentrix has instant BFT finality
sentrix_validatorSet{ epoch, validators[] }every epoch-advance (active set rotation)
sentrix_tokenOps{ op, contract, from, to, amount, txid, blockHeight }every native TokenOp dispatched (Deploy, Transfer, Burn, Mint, Approve)
sentrix_stakingOps{ op, validator, delegator, amount, txid, blockHeight }every StakingOp dispatched (RegisterValidator, Delegate, Redelegate, Undelegate, ClaimRewards, Unjail, AddSelfStake, SubmitEvidence, JailEvidenceBundle)
sentrix_jail{ validator, epoch, missedBlocks, blockHeight }per-validator inside a JailEvidenceBundle dispatch (post-JAIL_CONSENSUS_HEIGHT)

Quick start

ethers.js v6

import { WebSocketProvider } from "ethers";

const provider = new WebSocketProvider("wss://rpc.sentrixchain.com/ws");

provider.on("block", (n) => console.log("new block:", n));

const filter = {
address: "0x4693b113e523A196d9579333c4ab8358e2656553", // WSRX
topics: [ethers.id("Transfer(address,address,uint256)")],
};
provider.on(filter, (log) => console.log("WSRX transfer:", log));

viem

import { createPublicClient, webSocket, parseAbiItem } from "viem";

const client = createPublicClient({
transport: webSocket("wss://rpc.sentrixchain.com/ws"),
});

const unwatch = client.watchBlocks({
onBlock: (block) => console.log("new block:", block.number),
});

const unwatchLogs = client.watchContractEvent({
address: "0x4693b113e523A196d9579333c4ab8358e2656553",
abi: [parseAbiItem("event Transfer(address indexed from, address indexed to, uint256 value)")],
onLogs: (logs) => console.log("WSRX transfers:", logs),
});

Raw JSON-RPC over WebSocket

const ws = new WebSocket("wss://rpc.sentrixchain.com/ws");

ws.onopen = () => {
// Subscribe to newHeads
ws.send(JSON.stringify({
jsonrpc: "2.0", id: 1, method: "eth_subscribe", params: ["newHeads"]
}));

// Subscribe to logs filtered by address + first topic
ws.send(JSON.stringify({
jsonrpc: "2.0", id: 2, method: "eth_subscribe",
params: ["logs", {
address: "0x4693b113e523A196d9579333c4ab8358e2656553",
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}]
}));
};

ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.method === "eth_subscription") {
console.log("event:", data.params.subscription, data.params.result);
} else {
console.log("response:", data);
}
};

Filters — eth_subscribe(logs)

Filter object follows eth_getLogs shape:

FieldTypeBehaviour
addressstring OR string[]match contract address(es). Empty = match any
topics(null | string | string[])[]positional topic match. null = wildcard at that position. [a, b] = match either at that position

Per-event match applied in the listener task — unsubscribed traffic never crosses the wire (saves bandwidth + reduces client parse cost).

Method parity — non-subscribe methods over WS

The same WebSocket connection serves all 20+ HTTP eth_* methods (eth_call, eth_blockNumber, eth_getBalance, etc) — saves dApps having to maintain two connections. Internally HTTP and WS share the same dispatcher; 100% method parity.

ws.send(JSON.stringify({
jsonrpc: "2.0", id: 99, method: "eth_call",
params: [{ to: "0x4693b113...", data: "0x06fdde03" }, "latest"]
}));

Lifecycle + edge cases

ScenarioBehaviour
eth_unsubscribe(<sub_id>)aborts the listener task; returns true first time, false on second call
connection dropsserver aborts every subscription task on that connection. client must reconnect + re-subscribe
client too slow → broadcast buffer fills (1024 events)server emits eth_subscription message with error: subscription lagged ({skipped} events skipped); reconnect to resume, then drops the subscription. client must reconnect
subscribing to same channel twiceboth succeed with distinct sub_ids

Limits

CapValueWhy
Concurrent WS connections per source IP10Defense — guards against fd exhaustion from a single client
Concurrent subscriptions per connection100Defense — guards against tokio task exhaustion via repeated subscribe
Broadcast channel capacity1024 eventsLagged consumers get error + drop instead of OOM

Over-limit IP → 503 at upgrade response (no socket established). Over-limit subscriptions → JSON-RPC error -32005.

Subscription ID format

0x + 16 hex characters. Mirrors geth/erigon — opaque token, monotonic per connection. Don't depend on the value being parseable as a number.

Smoke test from CLI

# Install wscat: npm install -g wscat
wscat -c wss://rpc.sentrixchain.com/ws

# After connect, paste:
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}

# Expected: subscription ID returned, then a stream of eth_subscription
# messages with block headers every ~1 second.

See also