From 28cb3a0413d5ff541419711d0d94f8474a434eb0 Mon Sep 17 00:00:00 2001 From: satyakwok <119509589+satyakwok@users.noreply.github.com> Date: Sun, 10 May 2026 23:36:29 +0200 Subject: [PATCH] feat(grpc): Node-side @sentrix/chain/grpc client + bundled .proto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the SDK surface coverage. Existing doors: evm (viem), native (REST), bft (WebSocket subs), wallet (signing). New: grpc — typed wrapper around the chain's sentrix.v1.Sentrix service over @grpc/grpc-js. Node-only; browser consumers use the gRPC-Web transport in sentrix-explorer-v2 or wire grpc-web npm directly. Why a thin wrapper instead of asking callers to load the proto themselves: bundle the .proto with the npm package version-locked to the SDK so a chain proto bump can't silently mismatch your client; centralise the endpoint URL via the same network spec the rest of the SDK uses; hide the proto-loader → service-stub plumbing so consumers don't reach into @grpc/grpc-js internals. Available calls (chain v0.4+): - getLatestBlock() / getBlockByHeight(h) - getBalance(address) — 20-byte hex or Buffer - getValidatorSet({ atHeight? }) — full active set + flags - getSupply({ atHeight? }) — minted/burned/circ snapshot - getMempool({ limit }) — pending-tx size + headers - streamEvents([filters]) — server-stream ChainEvent Older chain hosts (v0.2 / v0.3) return Status::unimplemented for the newer methods; the SDK forwards the error verbatim so callers can fall back to JSON-RPC / REST. Build script copies the .proto from src/grpc-proto/ into dist/grpc-proto/ so the published package resolves correctly at runtime. proto-loader path is computed relative to the compiled JS location via fileURLToPath(import.meta.url). Verified end-to-end against live grpc.sentrixchain.com:443 — getLatestBlock() returns block 1678214. --- package.json | 11 +- pnpm-lock.yaml | 259 +++++++++++++++++++++++++++++++++++ src/grpc-proto/sentrix.proto | 257 ++++++++++++++++++++++++++++++++++ src/grpc/index.ts | 189 +++++++++++++++++++++++++ 4 files changed, 714 insertions(+), 2 deletions(-) create mode 100644 src/grpc-proto/sentrix.proto create mode 100644 src/grpc/index.ts diff --git a/package.json b/package.json index 7bceb55..9c4b184 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,20 @@ "./wallet": { "types": "./dist/wallet/index.d.ts", "import": "./dist/wallet/index.js" + }, + "./grpc": { + "types": "./dist/grpc/index.d.ts", + "import": "./dist/grpc/index.js" } }, "files": [ "dist", + "src/grpc-proto", "README.md", "LICENSE" ], "scripts": { - "build": "tsc -p tsconfig.json", + "build": "tsc -p tsconfig.json && mkdir -p dist/grpc-proto && cp src/grpc-proto/*.proto dist/grpc-proto/", "dev": "tsc -p tsconfig.json --watch", "lint": "eslint src", "typecheck": "tsc --noEmit", @@ -63,7 +68,9 @@ "dependencies": { "ws": "^8.18.0", "@noble/secp256k1": "^2.2.0", - "@noble/hashes": "^1.6.0" + "@noble/hashes": "^1.6.0", + "@grpc/grpc-js": "^1.12.0", + "@grpc/proto-loader": "^0.7.13" }, "devDependencies": { "@types/node": "^22.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 934c203..2a95d21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@grpc/grpc-js': + specifier: ^1.12.0 + version: 1.14.3 + '@grpc/proto-loader': + specifier: ^0.7.13 + version: 0.7.15 '@noble/hashes': specifier: ^1.6.0 version: 1.8.0 @@ -177,9 +183,26 @@ packages: cpu: [x64] os: [win32] + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -195,6 +218,36 @@ packages: '@noble/secp256k1@2.3.0': resolution: {integrity: sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@rollup/rollup-android-arm-eabi@4.60.2': resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] @@ -229,66 +282,79 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} @@ -378,6 +444,14 @@ packages: zod: optional: true + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -394,6 +468,17 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -407,6 +492,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -415,6 +503,10 @@ packages: engines: {node: '>=12'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -430,11 +522,25 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + isows@1.0.7: resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: ws: '*' + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -471,6 +577,14 @@ packages: resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} + protobufjs@7.5.7: + resolution: {integrity: sha512-NGnrxS/nLKUo5nkbVQxlC71sB4hdfImdYIbFeSCidxtwATx0AHRPcANSLd0q5Bb2BkoSWo2iisQhGg5/r+ihbA==} + engines: {node: '>=12.0.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + rollup@4.60.2: resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -489,6 +603,14 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -589,6 +711,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -613,6 +739,18 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -686,8 +824,29 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.7 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.7 + yargs: 17.7.2 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@js-sdsl/ordered-map@4.4.2': {} + '@noble/ciphers@1.3.0': {} '@noble/curves@1.9.1': @@ -698,6 +857,29 @@ snapshots: '@noble/secp256k1@2.3.0': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.1 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.1': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true @@ -840,6 +1022,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + assertion-error@2.0.1: {} cac@6.7.14: {} @@ -854,12 +1042,26 @@ snapshots: check-error@2.1.3: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + debug@4.4.3: dependencies: ms: 2.1.3 deep-eql@5.0.2: {} + emoji-regex@8.0.0: {} + es-module-lexer@1.7.0: {} esbuild@0.21.5: @@ -888,6 +1090,8 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + escalade@3.2.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -899,10 +1103,18 @@ snapshots: fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + + is-fullwidth-code-point@3.0.0: {} + isows@1.0.7(ws@8.18.3): dependencies: ws: 8.18.3 + lodash.camelcase@4.3.0: {} + + long@5.3.2: {} + loupe@3.2.1: {} magic-string@0.30.21: @@ -940,6 +1152,23 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + protobufjs@7.5.7: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.1 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 22.19.17 + long: 5.3.2 + + require-directory@2.1.1: {} + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 @@ -979,6 +1208,16 @@ snapshots: std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1077,6 +1316,26 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.18.3: {} ws@8.20.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/src/grpc-proto/sentrix.proto b/src/grpc-proto/sentrix.proto new file mode 100644 index 0000000..f6c33d4 --- /dev/null +++ b/src/grpc-proto/sentrix.proto @@ -0,0 +1,257 @@ +syntax = "proto3"; + +// Sentrix gRPC service v0.1 — package sentrix.v1. +// +// Parallel transport to JSON-RPC at port 8545. Same backend, same state, +// different wire format. Methods here mirror the most common JSON-RPC paths +// plus add a server-streaming variant for events that JSON-RPC + WebSocket +// can express but at a higher per-subscriber cost. +// +// Versioning: "sentrix.v1" is a hard contract. Breaking changes go to +// "sentrix.v2" with v1 retained for ≥ 2 minor releases overlap. +// +// Status: v0.1 skeleton. Handlers in crates/sentrix-grpc/src/lib.rs return +// tonic::Status::unimplemented until main.rs integration lands. Reference +// design doc: founder-private/audits/2026-05-05-grpc-service-proto-draft.md. + +package sentrix.v1; + +// 20-byte EVM-compatible address. Mirror of sentrix-primitives::Address. +// Wire = bytes (NOT hex string) for binary efficiency. +message Address { + bytes value = 1; // exactly 20 bytes +} + +// 32-byte hash — block hash, tx hash, state root. +message Hash { + bytes value = 1; // exactly 32 bytes +} + +// Native amount — sentri (10^-8 SRX). u64 wire to match Sentrix internal. +message Amount { + uint64 sentri = 1; +} + +// Block height — u64 starting at 0 (genesis). +message BlockHeight { + uint64 value = 1; +} + +// Mirror of sentrix-primitives::Transaction. Identical wire format to chain +// MDBX serialisation (modulo prost-encoding). Field numbers match the +// canonical struct order; do NOT renumber on changes. +message Transaction { + Hash txid = 1; + Address from_address = 2; + Address to_address = 3; + Amount amount = 4; + Amount fee = 5; + uint64 nonce = 6; + uint64 timestamp = 7; + bytes signature = 8; // 64-byte ed25519 OR 65-byte secp256k1 + bytes payload = 9; // contract calldata (EVM) or staking-op blob + uint32 chain_id = 10; + uint32 tx_type = 11; // 0 = transfer, 1 = contract, 2 = staking-op +} + +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; + // BFT justification carried from validator-side; included for audit. + // Empty for blocks at h<100_000 (pre state-root-fork). + bytes justification = 9; +} + +message Account { + Address address = 1; + Amount balance = 2; + uint64 nonce = 3; + // For contract accounts: 32-byte storage root + non-empty code_hash. + Hash storage_root = 4; + Hash code_hash = 5; +} + +// Server-streaming event types. +message ChainEvent { + oneof event { + BlockFinalized block_finalized = 1; + PendingTx pending_tx = 2; + ValidatorSetChange validator_set_change = 3; + LogEmitted log = 4; + // Synthetic Lagged sentinel — emitted when the per-subscriber broadcast + // channel drops oldest events (slow consumer). Mirrors tokio broadcast + // RecvError::Lagged semantics. Consumer should resync state from RPC. + StreamLagged lagged = 5; + } + uint64 sequence = 100; // monotonic across all events on this stream + uint64 timestamp = 101; // server-wall-clock unix seconds +} + +message BlockFinalized { Block block = 1; } +message PendingTx { Transaction tx = 1; } + +message ValidatorSetChange { + uint32 epoch = 1; + repeated Address active = 2; + repeated Address jailed = 3; +} + +message LogEmitted { + Address contract = 1; + repeated Hash topics = 2; // 0..4 topics per EVM convention + bytes data = 3; + Hash tx_hash = 4; + uint64 block_height = 5; + uint32 log_index = 6; +} + +message StreamLagged { + uint64 skipped_count = 1; // events dropped since last delivery +} + +// ──────────────────────────────────────────────────────────── +// Service definition +// ──────────────────────────────────────────────────────────── +service Sentrix { + // Submit a signed transaction to the local mempool. Same semantics as + // JSON-RPC eth_sendRawTransaction for EVM txs, with native fields exposed + // for staking-ops and other native variants. + rpc BroadcastTx(BroadcastTxRequest) returns (BroadcastTxResponse); + + // Fetch a block by height OR by hash. Returns NOT_FOUND if outside the + // local chain window (currently 1000 blocks; older blocks need indexer). + rpc GetBlock(GetBlockRequest) returns (Block); + + // Fetch account balance + nonce. Mirrors eth_getBalance + + // eth_getTransactionCount in a single round-trip. Includes mempool-pending + // nonce so wallets build correct next-tx without an extra call (matches + // the pending-aware nonce behaviour from chain v2.1.57). + rpc GetBalance(GetBalanceRequest) returns (Account); + + // v0.4 read-only state queries — drop the REST `/sentrix_status_extended` + // bridge from the explorer's stats hot path. All three are pure reads + // off `state.read()` snapshots; same lock contention profile as + // GetBlock/GetBalance. + // + // Active validator set + per-validator stake/active/jailed flags. + rpc GetValidatorSet(GetValidatorSetRequest) returns (ValidatorSet); + // Native-token supply snapshot (minted, burned, circulating). + rpc GetSupply(GetSupplyRequest) returns (Supply); + // Pending-tx count + a capped header window for UI display. + rpc GetMempool(GetMempoolRequest) returns (Mempool); + + // Server-streaming chain events. Subscribe once, receive every event type + // until cancel or server restart. Replaces N separate eth_subscribe calls. + // Backpressure: server bounded channel per stream (capacity 4096 — same as + // event_tx in chain). On slow client, server drops OLDEST events and emits + // a StreamLagged sentinel. Client should resync state from RPC. + rpc StreamEvents(StreamEventsRequest) returns (stream ChainEvent); +} + +message BroadcastTxRequest { Transaction tx = 1; } + +message BroadcastTxResponse { + Hash txid = 1; + uint64 mempool_position = 2; // 0-indexed; FIFO ordering UX +} + +message GetBlockRequest { + oneof selector { + BlockHeight height = 1; + Hash hash = 2; + bool latest = 3; // selector == "latest" shorthand + bool finalized = 4; // selector == "finalized" (BFT-finalized head) + } +} + +message GetBalanceRequest { + Address address = 1; + // Snapshot height — read state as-of this block. Default = latest. + // Returns FAILED_PRECONDITION if outside chain window. + optional BlockHeight at_height = 2; +} + +message StreamEventsRequest { + // Filter set — empty = subscribe to all event types. Backend filters + // server-side so the wire only carries matched events. + repeated EventFilter filters = 1; + // Resume from a sequence number (0 = current head). Backend keeps last + // 4096 events in a ring buffer — beyond that, sequence is not resumable + // and the server returns FAILED_PRECONDITION. + uint64 from_sequence = 2; +} + +enum EventFilter { + EVENT_FILTER_UNSPECIFIED = 0; + EVENT_FILTER_BLOCK_FINALIZED = 1; + EVENT_FILTER_PENDING_TX = 2; + EVENT_FILTER_VALIDATOR_SET = 3; + EVENT_FILTER_LOG = 4; +} + +// ──────────────────────────────────────────────────────────── +// v0.4 read-only state-query messages +// ──────────────────────────────────────────────────────────── + +message GetValidatorSetRequest { + // Reserved for at_height historical reads — gated on MDBX snapshot + // isolation (Refactor 5). v0.4 returns the latest finalized set. + optional BlockHeight at_height = 1; +} + +message ValidatorSet { + uint32 epoch = 1; + uint32 active_count = 2; + uint32 total_count = 3; + // Total stake across the active set, in sentri (10^-8 SRX). + uint64 total_active_stake_sentri = 4; + repeated ValidatorEntry validators = 5; +} + +message ValidatorEntry { + Address address = 1; + uint64 stake_sentri = 2; + bool active = 3; + bool jailed = 4; +} + +message GetSupplyRequest { + optional BlockHeight at_height = 1; +} + +message Supply { + // sentri = 10^-8 SRX. Cap is fork-gated (210M pre-fork, 315M post- + // tokenomics-v2 at h=640800); leave conversion to display layer. + uint64 minted_sentri = 1; + uint64 burned_sentri = 2; + uint64 circulating_sentri = 3; // = minted − burned +} + +message GetMempoolRequest { + // Cap returned tx headers; default 100, max 500. Larger windows need + // pagination (deferred). 0 = use server default (100). + uint32 limit = 1; +} + +message Mempool { + // Authoritative pending-tx count. `entries` is capped by `limit`; + // size always reflects the full mempool depth. + uint32 size = 1; + repeated MempoolEntry entries = 2; +} + +message MempoolEntry { + Hash txid = 1; + Address from_address = 2; + Address to_address = 3; + Amount amount = 4; + Amount fee = 5; + uint64 nonce = 6; + uint32 tx_type = 7; +} diff --git a/src/grpc/index.ts b/src/grpc/index.ts new file mode 100644 index 0000000..4cd7a1f --- /dev/null +++ b/src/grpc/index.ts @@ -0,0 +1,189 @@ +// gRPC door — typed wrapper around the chain's `sentrix.v1.Sentrix` +// service. Node-only (uses @grpc/grpc-js + @grpc/proto-loader). For +// browser consumers the chain exposes the same methods over gRPC-Web +// at the same endpoint; use a separate gRPC-Web client (eg the +// sentrix-explorer-v2 wrapper, or wire your own via grpc-web npm). +// +// Why a thin wrapper instead of having callers `loadProto` themselves: +// 1. Bundle the .proto with the npm package — version-locked to the +// SDK so a chain proto bump can't silently mismatch your client. +// 2. Centralise the endpoint URL (mainnet / testnet) via the same +// network spec the rest of the SDK uses. +// 3. Hide the proto-loader → service-stub plumbing so consumers +// don't need to reach into @grpc/grpc-js internals. +// +// Available calls on the v0.4+ chain side: +// - getBlock { latest } / { height } → Block +// - getBalance { address } → Account +// - getValidatorSet { atHeight } → ValidatorSet +// - getSupply { atHeight } → Supply +// - getMempool { limit } → Mempool +// - streamEvents { filters, from } → server-stream of ChainEvent +// +// Older (v0.2/0.3) chain hosts return Status::unimplemented for the +// newer methods. The SDK forwards the error so callers can fall back +// to the JSON-RPC / REST surface or skip the feature. + +import { fileURLToPath } from "node:url"; +import { dirname, resolve as pathResolve } from "node:path"; +import { credentials, type ChannelCredentials, type ClientReadableStream } from "@grpc/grpc-js"; +import { loadSync, type Options as ProtoOptions } from "@grpc/proto-loader"; +import { loadPackageDefinition } from "@grpc/grpc-js"; +import { getSpec, type SentrixNetwork } from "../network.js"; + +// Resolve the bundled proto file relative to THIS source file. Works +// for both ESM (import.meta.url) and after esbuild/tsc compile (the +// proto sits next to dist/grpc/index.js because tsc copies static +// assets via tsconfig "files" + the package.json "files" array). +const HERE = dirname(fileURLToPath(import.meta.url)); +const PROTO_PATH = pathResolve(HERE, "..", "grpc-proto", "sentrix.proto"); + +const PROTO_OPTS: ProtoOptions = { + // Match Sentrix's prost generation: long fields → string (so u64 + // block heights survive JSON round-trip) + bytes as Buffer. + longs: String, + bytes: Buffer, + enums: String, + defaults: true, + oneofs: true, +}; + +// Lazy-loaded so callers that never touch /grpc don't pay the +// proto-load cost on import. +let cachedDef: GrpcServiceShape | null = null; +function getServiceDef(): GrpcServiceShape { + if (cachedDef) return cachedDef; + const pkg = loadPackageDefinition(loadSync(PROTO_PATH, PROTO_OPTS)); + // pkg shape: { sentrix: { v1: { Sentrix: } } } + const ctor = (pkg as unknown as { sentrix: { v1: { Sentrix: new (...args: unknown[]) => GrpcServiceClient } } }) + .sentrix.v1.Sentrix; + if (typeof ctor !== "function") { + throw new Error("@sentrix/chain/grpc: failed to load sentrix.v1.Sentrix service from bundled proto"); + } + cachedDef = { ctor }; + return cachedDef; +} + +interface GrpcServiceShape { + ctor: new (target: string, creds: ChannelCredentials) => GrpcServiceClient; +} + +// Method shape — proto-loader generates per-method functions. Typing +// stays loose intentionally: the .proto is the source of truth, and +// hand-mirroring every message type would drift the moment chain v0.5 +// adds a field. Callers cast at the use site if they need a strict +// shape. +interface GrpcServiceClient { + getBlock(req: object, cb: GrpcUnaryCallback): void; + getBalance(req: object, cb: GrpcUnaryCallback): void; + getValidatorSet(req: object, cb: GrpcUnaryCallback): void; + getSupply(req: object, cb: GrpcUnaryCallback): void; + getMempool(req: object, cb: GrpcUnaryCallback): void; + streamEvents(req: object): ClientReadableStream; + close(): void; +} +type GrpcUnaryCallback = (err: Error | null, resp: unknown) => void; + +export interface GrpcClientOptions { + /** Override the gRPC endpoint host:port. Defaults to the network's + * public endpoint (`grpc.sentrixchain.com:443` for mainnet, + * `grpc-testnet.sentrixchain.com:443` for testnet). */ + endpoint?: string; + /** Use insecure (plaintext) credentials instead of TLS. Local + * sidecar dev only — NEVER on a public endpoint. */ + insecure?: boolean; +} + +export class GrpcClient { + private readonly inner: GrpcServiceClient; + + constructor(network: SentrixNetwork, opts: GrpcClientOptions = {}) { + const endpoint = opts.endpoint ?? defaultEndpoint(network); + const creds = opts.insecure ? credentials.createInsecure() : credentials.createSsl(); + const { ctor } = getServiceDef(); + this.inner = new ctor(endpoint, creds); + } + + /** GetBlock { latest: true } — latest finalized block. */ + async getLatestBlock(): Promise { + return this.unary("getBlock", { latest: true }); + } + + /** GetBlock { height } — block at a specific height. Throws + * tonic::Status equivalent if the height has been pruned. */ + async getBlockByHeight(height: bigint | number): Promise { + return this.unary("getBlock", { height: { value: height.toString() } }); + } + + /** GetBalance — current native + EVM balance for a 20-byte address. + * `address` accepts hex string (with or without 0x) or raw Buffer. */ + async getBalance(address: string | Buffer): Promise { + const bytes = typeof address === "string" ? hexToBytes(address) : address; + return this.unary("getBalance", { address: { value: bytes } }); + } + + /** v0.4+ only — full validator set with active/jail flags. */ + async getValidatorSet(atHeight?: bigint | number): Promise { + const req: Record = {}; + if (atHeight !== undefined) req.atHeight = { value: atHeight.toString() }; + return this.unary("getValidatorSet", req); + } + + /** v0.4+ only — minted/burned/circulating snapshot. */ + async getSupply(atHeight?: bigint | number): Promise { + const req: Record = {}; + if (atHeight !== undefined) req.atHeight = { value: atHeight.toString() }; + return this.unary("getSupply", req); + } + + /** v0.4+ only — pending-tx size + capped header window. */ + async getMempool(limit = 100): Promise { + return this.unary("getMempool", { limit }); + } + + /** Server-stream of ChainEvent. Returns a Node Readable-like stream. + * Drain with `for await (const ev of stream) { … }` or + * `stream.on("data", ev => …)`. Filters list is sent verbatim; + * empty array = subscribe-all. */ + streamEvents(filters: number[] = []): ClientReadableStream { + return this.inner.streamEvents({ filters, fromSequence: 0 }); + } + + /** Close the underlying channel. Outstanding RPCs cancel. */ + close(): void { + this.inner.close(); + } + + // ── internals ──────────────────────────────────────────────── + private unary(method: keyof GrpcServiceClient, req: object): Promise { + return new Promise((resolve, reject) => { + const fn = this.inner[method] as ( + r: object, + cb: GrpcUnaryCallback, + ) => void; + fn.call(this.inner, req, (err, resp) => { + if (err) reject(err); + else resolve(resp); + }); + }); + } +} + +function defaultEndpoint(network: SentrixNetwork): string { + // Public gRPC endpoint mirrors the JSON-RPC LB; same chain, same + // backend. See network.ts for the full URL inventory. + void getSpec; // referenced for future per-spec endpoint fields + return network === "mainnet" + ? "grpc.sentrixchain.com:443" + : "grpc-testnet.sentrixchain.com:443"; +} + +function hexToBytes(hex: string): Buffer { + const stripped = hex.startsWith("0x") ? hex.slice(2) : hex; + if (stripped.length !== 40) { + throw new Error( + `@sentrix/chain/grpc: address must be 20 bytes / 40 hex chars (got ${stripped.length})`, + ); + } + return Buffer.from(stripped, "hex"); +}