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
39 changes: 39 additions & 0 deletions __tests__/grpc-transport-auth.test.ts
Original file line number Diff line number Diff line change
@@ -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" });
});
});
1 change: 1 addition & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 9 additions & 2 deletions src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,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,
Expand All @@ -107,7 +109,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
Expand Down
26 changes: 21 additions & 5 deletions src/core/coinHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -76,8 +77,9 @@ export async function getCoinObjectCounts(
address: string,
network: Network,
grpcUrl?: string,
grpcToken?: string,
): Promise<CoinTypeCount[]> {
const client = grpcClient(network, grpcUrl);
const client = grpcClient(network, grpcUrl, grpcToken);
const coinTypes = await listCoinTypes(client, address);

const counts = await mapWithConcurrency(
Expand Down Expand Up @@ -118,6 +120,7 @@ export async function buildMergeCoinsTransaction(
address: string,
network: Network,
grpcUrl?: string,
grpcToken?: string,
): Promise<Transaction> {
const blockchain = new Blockchain(network);
const normalizedCoinType = normalizeStructTag(coinType);
Expand All @@ -135,7 +138,7 @@ export async function buildMergeCoinsTransaction(
addressBalance >= MIN_ADDRESS_BALANCE_FOR_GAS;

const coins = await getCoinObjectsOfType(
grpcClient(network, grpcUrl),
grpcClient(network, grpcUrl, grpcToken),
address,
normalizedCoinType,
);
Expand Down Expand Up @@ -298,11 +301,24 @@ 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. 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,
grpcToken?: string,
): SuiGrpcClient {
return new SuiGrpcClient({
network,
baseUrl: grpcUrl ?? GRPC_URL[network],
transport: new GrpcWebFetchTransport({
baseUrl: grpcUrl ?? GRPC_URL[network],
...(grpcToken ? { meta: { "x-token": grpcToken } } : {}),
}),
});
}

Expand Down
4 changes: 4 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { Decimal } from "decimal.js";
*/
export interface AlphalendClientOptions {
coinMetadataMap?: Map<string, CoinMetadata>;
/** 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;
}

/**
Expand Down
43 changes: 39 additions & 4 deletions src/models/blockchain.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/**
* 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 { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { graphql } from "@mysten/sui/graphql/schema";
import {
Transaction,
Expand Down Expand Up @@ -39,6 +43,12 @@ const GRAPHQL_URL: Record<Network, string> = {
devnet: "https://graphql.devnet.sui.io/graphql",
};

const GRPC_URL: Record<Network, string> = {
mainnet: "https://fullnode.mainnet.sui.io:443",
testnet: "https://fullnode.testnet.sui.io:443",
devnet: "https://fullnode.devnet.sui.io:443",
};

export interface GqlObject<T> {
address: string;
contents?: T;
Expand All @@ -52,17 +62,42 @@ 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<string, string> = 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,
});
// 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,
transport: new GrpcWebFetchTransport({
baseUrl: grpcUrl ?? GRPC_URL[network],
...(grpcToken ? { meta: { "x-token": grpcToken } } : {}),
}),
});
}

// --------------------------------------------------------------------------
Expand Down
Loading