From 5a207b043406e9a3531600758631af41c4191cec Mon Sep 17 00:00:00 2001 From: felix11 Date: Wed, 12 Aug 2026 18:22:01 +0400 Subject: [PATCH 1/4] feat(coinHelpers): grpcToken param, sent as x-token metadata --- src/core/coinHelpers.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/coinHelpers.ts b/src/core/coinHelpers.ts index 7c7dd21..333a75c 100644 --- a/src/core/coinHelpers.ts +++ b/src/core/coinHelpers.ts @@ -76,8 +76,9 @@ export async function getCoinObjectCounts( address: string, network: Network, grpcUrl?: string, + grpcToken?: string, ): Promise { - const client = grpcClient(network, grpcUrl); + const client = grpcClient(network, grpcUrl, grpcToken); const coinTypes = await listCoinTypes(client, address); const counts = await mapWithConcurrency( @@ -118,6 +119,7 @@ export async function buildMergeCoinsTransaction( address: string, network: Network, grpcUrl?: string, + grpcToken?: string, ): Promise { const blockchain = new Blockchain(network); const normalizedCoinType = normalizeStructTag(coinType); @@ -135,7 +137,7 @@ export async function buildMergeCoinsTransaction( addressBalance >= MIN_ADDRESS_BALANCE_FOR_GAS; const coins = await getCoinObjectsOfType( - grpcClient(network, grpcUrl), + grpcClient(network, grpcUrl, grpcToken), address, normalizedCoinType, ); @@ -298,11 +300,16 @@ async function getAddressBalance( return BigInt(response.data?.address?.balance?.addressBalance ?? 0); } -/** gRPC-web over fetch; works in browsers and Node. */ -function grpcClient(network: Network, grpcUrl?: string): SuiGrpcClient { +/** gRPC-web over fetch; works in browsers and Node. `grpcToken` is sent as `x-token` metadata. */ +function grpcClient( + network: Network, + grpcUrl?: string, + grpcToken?: string, +): SuiGrpcClient { return new SuiGrpcClient({ network, baseUrl: grpcUrl ?? GRPC_URL[network], + meta: grpcToken ? { "x-token": grpcToken } : undefined, }); } From 7b296e482db8331c9fa4f54ae673ad174e51c57f Mon Sep 17 00:00:00 2001 From: felix11 Date: Thu, 13 Aug 2026 14:28:26 +0400 Subject: [PATCH 2/4] feat: configurable SuiGrpcClient on Blockchain via AlphalendClientOptions grpcUrl/grpcToken --- src/core/client.ts | 11 +++++++++-- src/core/types.ts | 4 ++++ src/models/blockchain.ts | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/core/client.ts b/src/core/client.ts index 22eee18..1e894d0 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -95,7 +95,9 @@ export class AlphalendClient { * @param network One of the supported Sui networks. * @param graphqlUrl Optional GraphQL endpoint override. If omitted, a default * public endpoint for the given `network` is used. - * @param options Optional prebuilt coin metadata map for offline testing. + * @param options Optional prebuilt coin metadata map for offline testing, + * and/or a gRPC endpoint override + auth token for + * `blockchain.suiGrpcClient` (see AlphalendClientOptions). */ constructor( network: Network, @@ -106,7 +108,12 @@ export class AlphalendClient { this.constants = getConstants(network); this.lendingProtocol = new LendingProtocol(network, graphqlUrl); - this.blockchain = new Blockchain(network, graphqlUrl); + this.blockchain = new Blockchain( + network, + graphqlUrl, + options?.grpcUrl, + options?.grpcToken, + ); this.cetusSwap = new CetusSwap("mainnet"); // If a coin metadata map is provided, use it and mark as initialized diff --git a/src/core/types.ts b/src/core/types.ts index 379cf9a..6e63421 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -20,6 +20,10 @@ import { Decimal } from "decimal.js"; */ export interface AlphalendClientOptions { coinMetadataMap?: Map; + /** Optional Sui gRPC endpoint override for the client's `blockchain.suiGrpcClient`. */ + grpcUrl?: string; + /** Auth token for the gRPC endpoint, sent as `x-token` metadata (e.g. a BlockPI key). */ + grpcToken?: string; } /** diff --git a/src/models/blockchain.ts b/src/models/blockchain.ts index 5747f16..9460892 100644 --- a/src/models/blockchain.ts +++ b/src/models/blockchain.ts @@ -1,12 +1,15 @@ /** * Blockchain interface wrapper for Sui network operations using the SuiGraphQLClient. * - * All operations go through GraphQL, including transaction simulation via - * `gqlClient.core.simulateTransaction`, which builds the transaction, resolves - * gas, and simulates server-side. No JSON-RPC or gRPC client is used. + * All of the SDK's own operations go through GraphQL, including transaction + * simulation via `gqlClient.core.simulateTransaction`, which builds the + * transaction, resolves gas, and simulates server-side. No JSON-RPC is used. + * A `SuiGrpcClient` is also constructed (endpoint/token via the constructor) + * so consumers — and future SDK reads — can go over gRPC without re-plumbing. */ import { SuiGraphQLClient } from "@mysten/sui/graphql"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; import { graphql } from "@mysten/sui/graphql/schema"; import { Transaction, @@ -39,6 +42,12 @@ const GRAPHQL_URL: Record = { devnet: "https://graphql.devnet.sui.io/graphql", }; +const GRPC_URL: Record = { + mainnet: "https://fullnode.mainnet.sui.io:443", + testnet: "https://fullnode.testnet.sui.io:443", + devnet: "https://fullnode.devnet.sui.io:443", +}; + export interface GqlObject { address: string; contents?: T; @@ -52,17 +61,35 @@ export interface EarliestTxInfo { export class Blockchain { network: Network; gqlClient: SuiGraphQLClient; + /** + * gRPC (v2 Core) client. Not used by the SDK's own reads yet — exposed so + * consumers can share one configured client, and so future SDK reads can + * move to gRPC without a plumbing change. + */ + suiGrpcClient: SuiGrpcClient; constants: Constants; private initialSharedVersionCache: Map = new Map(); - constructor(network: Network, graphqlUrl?: string) { + constructor( + network: Network, + graphqlUrl?: string, + grpcUrl?: string, + grpcToken?: string, + ) { this.network = network; this.constants = getConstants(network); this.gqlClient = new SuiGraphQLClient({ url: graphqlUrl ?? GRAPHQL_URL[network], network, }); + this.suiGrpcClient = new SuiGrpcClient({ + network: network === "testnet" ? "testnet" : "mainnet", + baseUrl: grpcUrl ?? GRPC_URL[network], + // Direct property (not a conditional spread) so the option name is + // compiler-checked. Sent as x-token gRPC metadata (e.g. a BlockPI key). + meta: grpcToken ? { "x-token": grpcToken } : undefined, + }); } // -------------------------------------------------------------------------- From ff86d1543b07f6447244fa019c160262b3bb137f Mon Sep 17 00:00:00 2001 From: felix11 Date: Fri, 14 Aug 2026 11:13:16 +0400 Subject: [PATCH 3/4] fix: build the gRPC transport explicitly so x-token metadata is actually sent --- package-lock.json | 1 + package.json | 1 + src/core/coinHelpers.ts | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 435c0f7..d071ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@cetusprotocol/aggregator-sdk": "^1.5.5", "@naviprotocol/lending": "2.0.3", + "@protobuf-ts/grpcweb-transport": "^2.11.1", "@types/bn.js": "^5.2.0", "bn.js": "^5.2.5", "decimal.js": "^10.5.0", diff --git a/package.json b/package.json index d39d94c..5ca2e70 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "@cetusprotocol/aggregator-sdk": "^1.5.5", "@naviprotocol/lending": "2.0.3", + "@protobuf-ts/grpcweb-transport": "^2.11.1", "@types/bn.js": "^5.2.0", "bn.js": "^5.2.5", "decimal.js": "^10.5.0", diff --git a/src/core/coinHelpers.ts b/src/core/coinHelpers.ts index 333a75c..a936444 100644 --- a/src/core/coinHelpers.ts +++ b/src/core/coinHelpers.ts @@ -12,6 +12,7 @@ import { } from "@mysten/sui/transactions"; import { graphql } from "@mysten/sui/graphql/schema"; import { SuiGrpcClient } from "@mysten/sui/grpc"; +import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; import type { SuiClientTypes } from "@mysten/sui/client"; import { normalizeStructTag, SUI_TYPE_ARG } from "@mysten/sui/utils"; @@ -300,7 +301,13 @@ async function getAddressBalance( return BigInt(response.data?.address?.balance?.addressBalance ?? 0); } -/** gRPC-web over fetch; works in browsers and Node. `grpcToken` is sent as `x-token` metadata. */ +/** + * gRPC-web over fetch; works in browsers and Node. `grpcToken` is sent as + * `x-token` metadata. The transport is built explicitly because + * SuiGrpcClient's convenience constructor forwards only `baseUrl`/`fetchInit` + * to the transport it builds and silently drops `meta` — the token would + * never be sent. + */ function grpcClient( network: Network, grpcUrl?: string, @@ -308,8 +315,10 @@ function grpcClient( ): SuiGrpcClient { return new SuiGrpcClient({ network, - baseUrl: grpcUrl ?? GRPC_URL[network], - meta: grpcToken ? { "x-token": grpcToken } : undefined, + transport: new GrpcWebFetchTransport({ + baseUrl: grpcUrl ?? GRPC_URL[network], + ...(grpcToken ? { meta: { "x-token": grpcToken } } : {}), + }), }); } From 0af5ec89d664a3ea634e78d5c6f624bbb2a37e72 Mon Sep 17 00:00:00 2001 From: felix11 Date: Fri, 14 Aug 2026 11:14:37 +0400 Subject: [PATCH 4/4] fix: explicit gRPC transport so x-token is sent, pass network through unchanged --- __tests__/grpc-transport-auth.test.ts | 39 +++++++++++++++++++++++++++ src/models/blockchain.ts | 18 +++++++++---- 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 __tests__/grpc-transport-auth.test.ts diff --git a/__tests__/grpc-transport-auth.test.ts b/__tests__/grpc-transport-auth.test.ts new file mode 100644 index 0000000..ebc30b1 --- /dev/null +++ b/__tests__/grpc-transport-auth.test.ts @@ -0,0 +1,39 @@ +import { Blockchain } from "../src/models/blockchain"; +import { AlphalendClient } from "../src/core/client"; + +// SuiGrpcClient's convenience constructor drops `meta`, so the token must ride +// on an explicitly-built transport. These tests pin the property actually +// shipped: the transport's defaultOptions carry the x-token metadata. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const transportOf = (client: any) => client.ledgerService._transport; + +describe("gRPC transport auth", () => { + it("Blockchain attaches grpcToken as x-token metadata on the transport", () => { + const blockchain = new Blockchain( + "mainnet", + undefined, + "https://example.invalid:443", + "TEST_TOKEN", + ); + expect(transportOf(blockchain.suiGrpcClient).defaultOptions.meta).toEqual({ + "x-token": "TEST_TOKEN", + }); + }); + + it("Blockchain sends no metadata when grpcToken is absent", () => { + const blockchain = new Blockchain("mainnet"); + expect( + transportOf(blockchain.suiGrpcClient).defaultOptions.meta, + ).toBeUndefined(); + }); + + it("AlphalendClient threads options through to the transport", () => { + const client = new AlphalendClient("mainnet", undefined, { + grpcUrl: "https://example.invalid:443", + grpcToken: "TEST_TOKEN", + }); + expect( + transportOf(client.blockchain.suiGrpcClient).defaultOptions.meta, + ).toEqual({ "x-token": "TEST_TOKEN" }); + }); +}); diff --git a/src/models/blockchain.ts b/src/models/blockchain.ts index 9460892..b703af0 100644 --- a/src/models/blockchain.ts +++ b/src/models/blockchain.ts @@ -10,6 +10,7 @@ import { SuiGraphQLClient } from "@mysten/sui/graphql"; import { SuiGrpcClient } from "@mysten/sui/grpc"; +import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; import { graphql } from "@mysten/sui/graphql/schema"; import { Transaction, @@ -83,12 +84,19 @@ export class Blockchain { url: graphqlUrl ?? GRAPHQL_URL[network], network, }); + // SuiGrpcClient's convenience constructor forwards only `baseUrl`/`fetchInit` + // to the transport it builds and silently drops `meta`, so the token would + // never be sent. Build the transport explicitly — its `defaultOptions.meta` + // is merged into every call as gRPC metadata (the `x-token` auth header). + // `network` is passed through unchanged: GRPC_URL covers all three + // networks, and narrowing devnet to "mainnet" would mislabel the client + // for network-keyed resolution (e.g. MVR). this.suiGrpcClient = new SuiGrpcClient({ - network: network === "testnet" ? "testnet" : "mainnet", - baseUrl: grpcUrl ?? GRPC_URL[network], - // Direct property (not a conditional spread) so the option name is - // compiler-checked. Sent as x-token gRPC metadata (e.g. a BlockPI key). - meta: grpcToken ? { "x-token": grpcToken } : undefined, + network, + transport: new GrpcWebFetchTransport({ + baseUrl: grpcUrl ?? GRPC_URL[network], + ...(grpcToken ? { meta: { "x-token": grpcToken } } : {}), + }), }); }