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
StreamEventslands 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
curlexploration without code generation - Web pages that just need
eth_blockNumberonce on load
Endpoints
| Network | Endpoint | chain_id |
|---|---|---|
| Mainnet | grpc.sentrixchain.com:443 | 7119 |
| Testnet | grpc-testnet.sentrixchain.com:443 | 7120 |
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:
transactionsis returned empty. Full marshalling lands in v0.3 alongsideBroadcastTx. To fetch transactions today, use the JSON-RPCeth_getBlockByNumberendpoint.
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_heighthistorical reads returnFAILED_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 listwon't work. Use the.protofile. - No history reads:
at_heightonGetBalancereturnsFAILED_PRECONDITION. - Write path still on JSON-RPC:
BroadcastTxreturnsUNIMPLEMENTEDuntil v0.4 (proto Transaction marshalling). Useeth_sendRawTransactionfor writes. - StreamEvents emits BlockFinalized only: other variants (PendingTx, ValidatorSetChange, LogEmitted) deferred to v0.4. Use WebSocket
eth_subscribefor 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).
StreamEventsserver-streaming subscription on the EventBus broadcast bus;RecvError::Laggedmapped toChainEvent::Laggedsentinel. - v0.4 —
BroadcastTxproto Transaction marshalling,StreamEventsfilter + from_sequence support, additional event variants (PendingTx,ValidatorSetChange,LogEmitted), MDBX snapshot reads forat_heighthistorical 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.