Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Official TypeScript SDK for **Sentrix Chain** (chain ID `7119` mainnet, `7120` t
- **`@sentrix/chain/bft`** — WebSocket subscription manager for all 9 channels (newHeads, logs, sentrix_finalized, sentrix_jail, …) with keepalive ping + automatic reconnect + typed payloads
- **`@sentrix/chain/wallet`** — secp256k1 keypair + Sentrix-native tx signing (same address as your MetaMask key — Sentrix derives addresses identically to Ethereum)
- **`@sentrix/chain/grpc`** — Node-side gRPC client over `@grpc/grpc-js` for the chain's `sentrix.v1.Sentrix` service (getBlock, getBalance, getValidatorSet, getSupply, getMempool, streamEvents). Bundled `.proto` so version drift can't bite you.
- **`@sentrix/chain/grpc-web`** — browser-side equivalent over `@protobuf-ts/grpcweb-transport`. Same surface, same chain endpoint (`grpc.sentrixchain.com` — Caddy transcodes gRPC-Web ↔ native gRPC).

## Install

Expand Down Expand Up @@ -132,7 +133,22 @@ c.close();

The chain's `sentrix.v1.Sentrix` service mirrors the JSON-RPC + REST shape. v0.4+ adds `getValidatorSet` / `getSupply` / `getMempool` and a server-streaming `streamEvents` for push-style consumption without the WebSocket overhead. Older chain hosts return `Status::unimplemented` for the newer methods; the SDK forwards the error verbatim so callers can fall back to JSON-RPC / REST.

> Browser consumers: `/grpc` is **Node-only** because `@grpc/grpc-js` needs raw HTTP/2. For browser dApps that want gRPC, the [sentrix-explorer-v2](https://github.com/Sentriscloud/sentrix-explorer-v2) repo has the working Rust + WASM + tonic-web pattern; or wire `grpc-web` npm directly against the same `grpc.sentrixchain.com:443` endpoint (Caddy transcodes between gRPC-Web ↔ gRPC).
> Browser consumers: use `@sentrix/chain/grpc-web` instead. Same surface, same chain endpoint — only the transport changes (`@protobuf-ts/grpcweb-transport` instead of `@grpc/grpc-js`). The `/grpc` subpath stays Node-only because `@grpc/grpc-js` needs raw HTTP/2 sockets browsers don't expose.

### Read via gRPC-Web (browser)

```ts
import { GrpcWebClient } from "@sentrix/chain/grpc-web";

const c = new GrpcWebClient("mainnet");
const block = await c.getLatestBlock();

for await (const ev of c.streamEvents([])) {
console.log(ev);
}
```

Same chain endpoint as the Node `/grpc` subpath (`grpc.sentrixchain.com`) — Caddy at the edge transcodes gRPC-Web ↔ native gRPC via `tonic-web` so server code is identical.

## Network identity helpers

Expand Down
19 changes: 14 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@sentrix/chain",
"version": "0.3.0-rc.0",
"description": "Official TypeScript SDK for Sentrix Chain \u2014 typed wrappers around EVM JSON-RPC + native REST + WebSocket subscriptions.",
"description": "Official TypeScript SDK for Sentrix Chain typed wrappers around EVM JSON-RPC + native REST + WebSocket subscriptions.",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
Expand Down Expand Up @@ -30,6 +30,10 @@
"./grpc": {
"types": "./dist/grpc/index.d.ts",
"import": "./dist/grpc/index.js"
},
"./grpc-web": {
"types": "./dist/grpc-web/index.d.ts",
"import": "./dist/grpc-web/index.js"
}
},
"files": [
Expand Down Expand Up @@ -66,13 +70,18 @@
"viem": "^2.48.8"
},
"dependencies": {
"ws": "^8.18.0",
"@noble/secp256k1": "^2.2.0",
"@noble/hashes": "^1.6.0",
"@grpc/grpc-js": "^1.12.0",
"@grpc/proto-loader": "^0.7.13"
"@grpc/proto-loader": "^0.7.13",
"@noble/hashes": "^1.6.0",
"@noble/secp256k1": "^2.2.0",
"@protobuf-ts/grpcweb-transport": "^2.11.1",
"@protobuf-ts/runtime": "^2.11.1",
"@protobuf-ts/runtime-rpc": "^2.11.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@protobuf-ts/plugin": "^2.11.1",
"@protobuf-ts/protoc": "^2.11.1",
"@types/node": "^22.10.0",
"@types/ws": "^8.5.13",
"typescript": "^5.7.2",
Expand Down
98 changes: 98 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

153 changes: 153 additions & 0 deletions src/grpc-web/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// gRPC-Web door — browser-side client for the chain's `sentrix.v1.Sentrix`
// service. Mirror of the Node-side `@sentrix/chain/grpc` API but over
// `@protobuf-ts/grpcweb-transport` instead of `@grpc/grpc-js`, so it
// works in the browser bundle without HTTP/2 raw socket access.
//
// The chain accepts gRPC-Web at the same endpoint as native gRPC —
// Caddy at the edge runs `tonic-web` to transcode between the two. So
// browser dApps connect to the same `grpc.sentrixchain.com:443`
// hostname; only the transport changes.
//
// Why a thin wrapper instead of asking callers to wire the transport
// themselves: hide endpoint resolution, package the generated stubs
// behind a stable surface, and keep the API shape identical to the
// Node `/grpc` subpath so dApp code that reads from both rails (eg an
// Electron app or a desktop CLI sharing a UI lib) doesn't need to
// branch per environment.

import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { SentrixClient as InnerClient } from "./sentrix.client.js";
import {
GetBlockRequest,
GetBalanceRequest,
GetValidatorSetRequest,
GetSupplyRequest,
GetMempoolRequest,
StreamEventsRequest,
} from "./sentrix.js";
import type {
Block,
Account,
ValidatorSet,
Supply,
Mempool,
ChainEvent,
EventFilter,
} from "./sentrix.js";
import type { SentrixNetwork } from "../network.js";

export interface GrpcWebClientOptions {
/** Override the gRPC-Web endpoint host. Defaults to the network's
* public endpoint (`https://grpc.sentrixchain.com` mainnet,
* `https://grpc-testnet.sentrixchain.com` testnet). Must include the
* scheme — gRPC-Web speaks plain HTTP framing on top of TLS, so the
* URL is `https://...` not `grpc://...`. */
endpoint?: string;
/** Pass through to fetch — useful for credentials / cookies in
* authenticated proxy setups. */
fetchInit?: Partial<RequestInit>;
}

/** Browser-friendly gRPC-Web client. Same surface as the Node-side
* `@sentrix/chain/grpc` client; pick the subpath that fits your
* runtime. */
export class GrpcWebClient {
private readonly inner: InnerClient;

constructor(network: SentrixNetwork, opts: GrpcWebClientOptions = {}) {
const endpoint = opts.endpoint ?? defaultEndpoint(network);
const transport = new GrpcWebFetchTransport({
baseUrl: endpoint,
fetchInit: opts.fetchInit,
});
this.inner = new InnerClient(transport);
}

/** GetBlock { latest: true } — latest finalised block. */
async getLatestBlock(): Promise<Block> {
const req = GetBlockRequest.create({ selector: { oneofKind: "latest", latest: true } });
return (await this.inner.getBlock(req)).response;
}

/** GetBlock { height } — block at a specific height. Throws if the
* chain pruned it. */
async getBlockByHeight(height: bigint | number): Promise<Block> {
const req = GetBlockRequest.create({
selector: {
oneofKind: "height",
height: { value: BigInt(height) },
},
});
return (await this.inner.getBlock(req)).response;
}

/** GetBalance — current balance for a 20-byte address. */
async getBalance(address: string | Uint8Array): Promise<Account> {
const bytes =
typeof address === "string" ? hexToBytes(address) : address;
if (bytes.length !== 20) {
throw new Error(
`@sentrix/chain/grpc-web: address must be 20 bytes (got ${bytes.length})`,
);
}
const req = GetBalanceRequest.create({ address: { value: bytes } });
return (await this.inner.getBalance(req)).response;
}

/** v0.4+ — ValidatorSet snapshot. */
async getValidatorSet(atHeight?: bigint | number): Promise<ValidatorSet> {
const req = GetValidatorSetRequest.create(
atHeight !== undefined
? { atHeight: { value: BigInt(atHeight) } }
: {},
);
return (await this.inner.getValidatorSet(req)).response;
}

/** v0.4+ — Supply snapshot. */
async getSupply(atHeight?: bigint | number): Promise<Supply> {
const req = GetSupplyRequest.create(
atHeight !== undefined
? { atHeight: { value: BigInt(atHeight) } }
: {},
);
return (await this.inner.getSupply(req)).response;
}

/** v0.4+ — Mempool snapshot. `limit = 0` ⇒ server default (100). */
async getMempool(limit = 100): Promise<Mempool> {
const req = GetMempoolRequest.create({ limit });
return (await this.inner.getMempool(req)).response;
}

/** v0.4+ — server-streaming chain events. Drain with for-await:
*
* for await (const ev of client.streamEvents([])) { … }
*
* Empty filter list = subscribe-all. */
async *streamEvents(filters: EventFilter[] = []): AsyncIterable<ChainEvent> {
const req = StreamEventsRequest.create({ filters, fromSequence: 0n });
const call = this.inner.streamEvents(req);
for await (const ev of call.responses) {
yield ev;
}
}
}

function defaultEndpoint(network: SentrixNetwork): string {
return network === "mainnet"
? "https://grpc.sentrixchain.com"
: "https://grpc-testnet.sentrixchain.com";
}

function hexToBytes(hex: string): Uint8Array {
const stripped = hex.startsWith("0x") ? hex.slice(2) : hex;
if (stripped.length % 2 !== 0) {
throw new Error(`@sentrix/chain/grpc-web: hex address has odd length: ${stripped.length}`);
}
const out = new Uint8Array(stripped.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);
}
return out;
}
Loading
Loading