Skip to main content

Sentrix Chain — gRPC API

Sentrix Chain ships a Tonic-based gRPC interface as a parallel transport to the JSON-RPC eth_* endpoints. Same backend, same state, different wire format.

When to use gRPC instead of JSON-RPC:

  • Binary protocol — smaller payloads, faster decode than JSON
  • Strongly-typed schema via Protocol Buffers — no runtime parsing surprises
  • HTTP/2 multiplexing — many in-flight calls over one connection
  • Native server-streaming (when StreamEvents lands in v0.3)

When JSON-RPC is still the right call:

  • MetaMask, ethers.js, hardhat, viem — all speak eth_* over JSON-RPC. Don't switch your dApp away from a working transport.
  • Quick curl exploration without code generation
  • Web pages that just need eth_blockNumber once on load

Endpoints

NetworkEndpointchain_id
Mainnetgrpc.sentrixchain.com:4437119
Testnetgrpc-testnet.sentrixchain.com:4437120

Both endpoints terminate TLS at the edge proxy and forward to the validator side-car over a private hop. Cloudflare proxy is enabled with the gRPC protocol toggle on, so HTTP/2 + te: trailers traverses the edge transparently.

Browser clients can hit the same hostnames over gRPC-Web (HTTP/1.1 or HTTP/2 with application/grpc-web content-type). Edge CORS is permissive (Access-Control-Allow-Origin: *) and exposes the grpc-status / grpc-message trailers so client libraries can read errors correctly.


Schema

Canonical .proto lives in the repository at:

crates/sentrix-grpc/proto/sentrix.proto

Service: sentrix.v1.Sentrix (note: Sentrix, not SentrixService or BlockchainService).

To generate clients, fetch the file directly from main and feed it to your codegen toolchain (protoc, tonic-build, grpc-tools, etc).

curl -O https://raw.githubusercontent.com/sentrix-labs/sentrix/main/crates/sentrix-grpc/proto/sentrix.proto

Reflection is not enabled on the side-car. grpcurl clients must be invoked with -import-path + -proto; grpcurl list will fail.


Methods (v0.2)

GetBlock(GetBlockRequest) → Block

Fetch a block by height, by hash, or via the latest / finalized selector. Returns NOT_FOUND if the block is outside the validator's in-memory chain window (currently 1000 blocks; older blocks need an indexer).

Request:

message GetBlockRequest {
oneof selector {
BlockHeight height = 1; // { value: <uint64> }
Hash hash = 2; // { value: <32 bytes> }
bool latest = 3;
bool finalized = 4;
}
}

Response (Block):

message Block {
uint64 index = 1;
Hash hash = 2;
Hash parent_hash = 3;
Hash state_root = 4;
uint64 timestamp = 5;
Address proposer = 6;
uint32 round = 7;
repeated Transaction transactions = 8; // empty in v0.2
bytes justification = 9; // bincoded BFT justification
}

v0.2 limitation: transactions is returned empty. Full marshalling lands in v0.3 alongside BroadcastTx. To fetch transactions today, use the JSON-RPC eth_getBlockByNumber endpoint.

GetBalance(GetBalanceRequest) → Account

Single round-trip for eth_getBalance + eth_getTransactionCount. Includes mempool-pending nonce (matches the chain's pending-aware nonce behaviour).

Request:

message GetBalanceRequest {
Address address = 1; // { value: <20 bytes> }
optional BlockHeight at_height = 2; // historical reads — see limitation
}

Response (Account):

message Account {
Address address = 1;
Amount balance = 2; // { sentri: <uint64> }
uint64 nonce = 3;
Hash storage_root = 4; // not populated in v0.2
Hash code_hash = 5; // not populated in v0.2
}

v0.2 limitation: at_height historical reads return FAILED_PRECONDITION. Snapshot-isolated reads need an MDBX-snapshot refactor; tracked for v0.3.

BroadcastTx(BroadcastTxRequest) → BroadcastTxResponse

Submit a signed transaction to the local mempool. Returns UNIMPLEMENTED in v0.2. Use JSON-RPC eth_sendRawTransaction for now.

StreamEvents(StreamEventsRequest) → stream ChainEvent

Server-streaming subscription replacing N separate eth_subscribe calls. Live since v2.1.71 — yields ChainEvent::BlockFinalized per block as it lands, plus ChainEvent::Lagged sentinel on backpressure (consumer behind 1024+ events).

Subscribes to the same EventBus broadcast channel that powers the WebSocket eth_subscribe handlers — single source of truth for event ordering. A gRPC subscriber and a WS subscriber see the same sequence at the broadcast::Sender boundary.

# tail blocks in real time (no polling)
grpcurl -import-path . -proto sentrix.proto \
-d '{}' grpc.sentrixchain.com:443 sentrix.v1.Sentrix/StreamEvents

Filter / from_sequence / additional event variants (PendingTx, ValidatorSetChange, LogEmitted) deferred to v0.4. Current impl always subscribes to all BlockFinalized from "now". Reconnect with exponential backoff is the client's responsibility — see the TypeScript example in Quickstart below for a reference reconnect loop.


Quickstart

grpcurl (CLI)

# fetch the proto
curl -O https://raw.githubusercontent.com/sentrix-labs/sentrix/main/crates/sentrix-grpc/proto/sentrix.proto

# latest block on mainnet
grpcurl -import-path . -proto sentrix.proto \
-d '{"latest":true}' \
grpc.sentrixchain.com:443 sentrix.v1.Sentrix/GetBlock

# specific height
grpcurl -import-path . -proto sentrix.proto \
-d '{"height":{"value":1440000}}' \
grpc.sentrixchain.com:443 sentrix.v1.Sentrix/GetBlock

# balance — Address.value is base64-encoded 20 bytes
grpcurl -import-path . -proto sentrix.proto \
-d '{"address":{"value":"<base64-of-20-byte-address>"}}' \
grpc.sentrixchain.com:443 sentrix.v1.Sentrix/GetBalance

Rust (Tonic)

# Cargo.toml
[dependencies]
tonic = "0.12"
prost = "0.13"
tokio = { version = "1", features = ["full"] }

[build-dependencies]
tonic-build = "0.12"
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/sentrix.proto")?;
Ok(())
}

// src/main.rs
pub mod sentrix_proto { tonic::include_proto!("sentrix.v1"); }

use sentrix_proto::sentrix_client::SentrixClient;
use sentrix_proto::GetBlockRequest;
use sentrix_proto::get_block_request::Selector;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = SentrixClient::connect("https://grpc.sentrixchain.com").await?;
let resp = client.get_block(GetBlockRequest {
selector: Some(Selector::Latest(true)),
}).await?;
println!("latest block index = {}", resp.into_inner().index);
Ok(())
}

TypeScript / Node (@grpc/grpc-js)

npm install @grpc/grpc-js @grpc/proto-loader
import { credentials, loadPackageDefinition } from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";

const pkgDef = loadSync("./sentrix.proto", { keepCase: true, longs: String });
const proto = loadPackageDefinition(pkgDef) as any;

const client = new proto.sentrix.v1.Sentrix(
"grpc.sentrixchain.com:443",
credentials.createSsl(),
);

client.GetBlock({ latest: true }, (err: any, block: any) => {
if (err) return console.error(err);
console.log("latest block index =", block.index);
});

Streaming with auto-reconnect (recommended for indexers):

function subscribeBlocks() {
const call = client.StreamEvents({});
let backoffMs = 500;

call.on("data", (msg: any) => {
backoffMs = 500; // reset on first frame after reconnect
if (msg.block_finalized?.block) {
const b = msg.block_finalized.block;
console.log("block", b.index);
} else if (msg.lagged) {
console.warn("stream lagged, skipped:", msg.lagged.skipped_count);
// resync: fetch last N blocks via JSON-RPC eth_getBlockByNumber
}
});

const reconnect = () => {
setTimeout(subscribeBlocks, backoffMs);
backoffMs = Math.min(backoffMs * 2, 8000);
};
call.on("error", reconnect);
call.on("end", reconnect);
}

subscribeBlocks();

Browser / Next.js (@grpc/grpc-web or @protobuf-ts/grpcweb-transport)

The gRPC-Web codec is enabled on the same host — browsers can call directly without a separate proxy.

npm install @protobuf-ts/grpcweb-transport @protobuf-ts/runtime-rpc
# (plus your codegen pipeline of choice — e.g. ts-proto, protobuf-ts plugin for protoc)
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { SentrixClient } from "./generated/sentrix.client";

const transport = new GrpcWebFetchTransport({
baseUrl: "https://grpc.sentrixchain.com",
});

const client = new SentrixClient(transport);

const { response } = await client.getBlock({ selector: { oneofKind: "latest", latest: true } });
console.log("latest block index =", response.index);

Python (grpcio)

pip install grpcio grpcio-tools
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. sentrix.proto
import grpc
import sentrix_pb2 as pb
import sentrix_pb2_grpc as svc

channel = grpc.secure_channel("grpc.sentrixchain.com:443", grpc.ssl_channel_credentials())
client = svc.SentrixStub(channel)

resp = client.GetBlock(pb.GetBlockRequest(latest=True))
print("latest block index =", resp.index)

Go (google.golang.org/grpc)

go get google.golang.org/grpc google.golang.org/grpc/credentials
protoc --go_out=. --go-grpc_out=. sentrix.proto
package main

import (
"context"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
pb "yourmodule/proto"
)

func main() {
creds := credentials.NewClientTLSFromCert(nil, "")
conn, err := grpc.Dial("grpc.sentrixchain.com:443", grpc.WithTransportCredentials(creds))
if err != nil { log.Fatal(err) }
defer conn.Close()

client := pb.NewSentrixClient(conn)
block, err := client.GetBlock(context.Background(), &pb.GetBlockRequest{
Selector: &pb.GetBlockRequest_Latest{Latest: true},
})
if err != nil { log.Fatal(err) }
log.Printf("latest block index = %d", block.Index)
}

CORS (browser clients)

The edge proxy adds the following headers on every gRPC-Web response:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Grpc-Web, X-User-Agent, Grpc-Timeout
Access-Control-Expose-Headers: Grpc-Status, Grpc-Message, Grpc-Encoding, Grpc-Accept-Encoding
Access-Control-Max-Age: 86400

Preflight OPTIONS requests are answered with 204 No Content. Standard gRPC-Web client libraries work without any custom configuration.


Limitations

  • Chain window: Validators serve blocks within their last ~1000-block in-memory window. Older blocks need to come from an indexer (none operated by Sentrix Labs at this time — community indexers welcome).
  • No reflection: grpcurl list won't work. Use the .proto file.
  • No history reads: at_height on GetBalance returns FAILED_PRECONDITION.
  • Write path still on JSON-RPC: BroadcastTx returns UNIMPLEMENTED until v0.4 (proto Transaction marshalling). Use eth_sendRawTransaction for writes.
  • StreamEvents emits BlockFinalized only: other variants (PendingTx, ValidatorSetChange, LogEmitted) deferred to v0.4. Use WebSocket eth_subscribe for those today.
  • Single validator per network: The published endpoint forwards to a single validator side-car. If that validator restarts, expect a brief connection reset; clients should implement standard gRPC retry with exponential backoff.

Roadmap

  • v0.3 — ✅ shipped 2026-05-04 (v2.1.71). StreamEvents server-streaming subscription on the EventBus broadcast bus; RecvError::Lagged mapped to ChainEvent::Lagged sentinel.
  • v0.4BroadcastTx proto Transaction marshalling, StreamEvents filter + from_sequence support, additional event variants (PendingTx, ValidatorSetChange, LogEmitted), MDBX snapshot reads for at_height historical queries.
  • v0.5 — multi-validator load balancing at the edge, optional gRPC compression negotiation, server reflection toggle for tooling.

Track progress at the canonical design doc in the repo: crates/sentrix-grpc/proto/sentrix.proto is updated as methods come online.