From e58bb221ba177c81de2444324b3a0fc2c25615fc Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 2 Feb 2024 10:38:30 -0600 Subject: [PATCH] refactor(experimental): add cluster level API for transports This change introduces cluster-level transports as well as cluster-level JSON RPCs that infer the cluster from the provided transport. --- .../src/__typetests__/json-rpc-typetest.ts | 48 +++++++++++++++++++ .../__typetests__/methods-api-typetest.ts | 2 +- .../subscriptions-api-typetest.ts | 2 +- packages/rpc-transport/src/index.ts | 4 +- packages/rpc-transport/src/json-rpc-config.ts | 4 +- packages/rpc-transport/src/json-rpc-types.ts | 23 +++++++++ packages/rpc-transport/src/json-rpc.ts | 39 +++++++++++++-- .../__typetests__/http-transport-typetest.ts | 39 +++++++++++++++ .../src/transports/http/http-transport.ts | 14 ++++-- .../src/transports/transport-types.ts | 13 +++++ 10 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 packages/rpc-transport/src/__typetests__/json-rpc-typetest.ts rename packages/rpc-transport/src/{ => apis}/__typetests__/methods-api-typetest.ts (90%) rename packages/rpc-transport/src/{ => apis}/__typetests__/subscriptions-api-typetest.ts (88%) create mode 100644 packages/rpc-transport/src/json-rpc-types.ts create mode 100644 packages/rpc-transport/src/transports/http/__typetests__/http-transport-typetest.ts diff --git a/packages/rpc-transport/src/__typetests__/json-rpc-typetest.ts b/packages/rpc-transport/src/__typetests__/json-rpc-typetest.ts new file mode 100644 index 000000000000..04be2f3efe2a --- /dev/null +++ b/packages/rpc-transport/src/__typetests__/json-rpc-typetest.ts @@ -0,0 +1,48 @@ +import { devnet, IRpcApiMethods, mainnet, Rpc, testnet } from '@solana/rpc-types'; + +import { createJsonRpcApi } from '../apis/methods/methods-api'; +import { createJsonRpc } from '../json-rpc'; +import { RpcDevnet, RpcMainnet, RpcTestnet } from '../json-rpc-types'; +import { createHttpTransport } from '../transports/http/http-transport'; + +interface MyApiMethods extends IRpcApiMethods { + foo(): number; + bar(): string; +} + +const api = createJsonRpcApi(); + +const genericTransport = createHttpTransport({ url: 'http://localhost:8899' }); +const devnetTransport = createHttpTransport({ url: devnet('https://api.devnet.solana.com') }); +const testnetTransport = createHttpTransport({ url: testnet('https://api.testnet.solana.com') }); +const mainnetTransport = createHttpTransport({ url: mainnet('https://api.mainnet-beta.solana.com') }); + +// When providing a generic transport, the RPC should be typed as an Rpc +createJsonRpc({ api, transport: genericTransport }) satisfies Rpc; +//@ts-expect-error Should not be a devnet RPC +createJsonRpc({ api, transport: genericTransport }) satisfies RpcDevnet; +//@ts-expect-error Should not be a testnet RPC +createJsonRpc({ api, transport: genericTransport }) satisfies RpcTestnet; +//@ts-expect-error Should not be a mainnet RPC +createJsonRpc({ api, transport: genericTransport }) satisfies RpcMainnet; + +// When providing a devnet transport, the RPC should be typed as an RpcDevnet +createJsonRpc({ api, transport: devnetTransport }) satisfies RpcDevnet; +//@ts-expect-error Should not be a testnet RPC +createJsonRpc({ api, transport: devnetTransport }) satisfies RpcTestnet; +//@ts-expect-error Should not be a mainnet RPC +createJsonRpc({ api, transport: devnetTransport }) satisfies RpcMainnet; + +// When providing a testnet transport, the RPC should be typed as an RpcTestnet +createJsonRpc({ api, transport: testnetTransport }) satisfies RpcTestnet; +//@ts-expect-error Should not be a devnet RPC +createJsonRpc({ api, transport: testnetTransport }) satisfies RpcDevnet; +//@ts-expect-error Should not be a mainnet RPC +createJsonRpc({ api, transport: testnetTransport }) satisfies RpcMainnet; + +// When providing a mainnet transport, the RPC should be typed as an RpcMainnet +createJsonRpc({ api, transport: mainnetTransport }) satisfies RpcMainnet; +//@ts-expect-error Should not be a devnet RPC +createJsonRpc({ api, transport: mainnetTransport }) satisfies RpcDevnet; +//@ts-expect-error Should not be a testnet RPC +createJsonRpc({ api, transport: mainnetTransport }) satisfies RpcTestnet; diff --git a/packages/rpc-transport/src/__typetests__/methods-api-typetest.ts b/packages/rpc-transport/src/apis/__typetests__/methods-api-typetest.ts similarity index 90% rename from packages/rpc-transport/src/__typetests__/methods-api-typetest.ts rename to packages/rpc-transport/src/apis/__typetests__/methods-api-typetest.ts index 7adc1e99d29b..42c1ad05258f 100644 --- a/packages/rpc-transport/src/__typetests__/methods-api-typetest.ts +++ b/packages/rpc-transport/src/apis/__typetests__/methods-api-typetest.ts @@ -1,6 +1,6 @@ import { IRpcApi, IRpcApiMethods } from '@solana/rpc-types'; -import { createJsonRpcApi } from '../apis/methods/methods-api'; +import { createJsonRpcApi } from '../methods/methods-api'; type NftCollectionDetailsApiResponse = Readonly<{ address: string; diff --git a/packages/rpc-transport/src/__typetests__/subscriptions-api-typetest.ts b/packages/rpc-transport/src/apis/__typetests__/subscriptions-api-typetest.ts similarity index 88% rename from packages/rpc-transport/src/__typetests__/subscriptions-api-typetest.ts rename to packages/rpc-transport/src/apis/__typetests__/subscriptions-api-typetest.ts index eb19902e1e37..99aba5161560 100644 --- a/packages/rpc-transport/src/__typetests__/subscriptions-api-typetest.ts +++ b/packages/rpc-transport/src/apis/__typetests__/subscriptions-api-typetest.ts @@ -1,6 +1,6 @@ import { IRpcApiMethods, IRpcSubscriptionsApi } from '@solana/rpc-types'; -import { createJsonRpcSubscriptionsApi } from '../apis/subscriptions/subscriptions-api'; +import { createJsonRpcSubscriptionsApi } from '../subscriptions/subscriptions-api'; type NftCollectionDetailsApiResponse = Readonly<{ address: string; diff --git a/packages/rpc-transport/src/index.ts b/packages/rpc-transport/src/index.ts index 260c413f4404..3a72dd1341ce 100644 --- a/packages/rpc-transport/src/index.ts +++ b/packages/rpc-transport/src/index.ts @@ -4,7 +4,7 @@ export * from './apis/subscriptions/subscriptions-api'; export * from './json-rpc'; export type { SolanaJsonRpcErrorCode } from './json-rpc-errors'; export * from './json-rpc-subscription'; - +export * from './json-rpc-types'; export * from './transports/http/http-transport'; -export type { IRpcTransport, IRpcWebSocketTransport } from './transports/transport-types'; +export * from './transports/transport-types'; export * from './transports/websocket/websocket-transport'; diff --git a/packages/rpc-transport/src/json-rpc-config.ts b/packages/rpc-transport/src/json-rpc-config.ts index 6708414779db..e913b0674f8e 100644 --- a/packages/rpc-transport/src/json-rpc-config.ts +++ b/packages/rpc-transport/src/json-rpc-config.ts @@ -1,10 +1,10 @@ import { IRpcApi, IRpcSubscriptionsApi } from '@solana/rpc-types'; -import { IRpcTransport, IRpcWebSocketTransport } from './transports/transport-types'; +import { IRpcTransport, IRpcTransportWithCluster, IRpcWebSocketTransport } from './transports/transport-types'; export type RpcConfig = Readonly<{ api: IRpcApi; - transport: IRpcTransport; + transport: IRpcTransport | IRpcTransportWithCluster; }>; export type RpcSubscriptionConfig = Readonly<{ diff --git a/packages/rpc-transport/src/json-rpc-types.ts b/packages/rpc-transport/src/json-rpc-types.ts new file mode 100644 index 000000000000..320c02a21db4 --- /dev/null +++ b/packages/rpc-transport/src/json-rpc-types.ts @@ -0,0 +1,23 @@ +import { Rpc } from '@solana/rpc-types'; + +import { + IRpcTransport, + IRpcTransportDevnet, + IRpcTransportMainnet, + IRpcTransportTestnet, + IRpcTransportWithCluster, +} from './transports/transport-types'; + +export type RpcDevnet = Rpc & { '~cluster': 'devnet' }; +export type RpcTestnet = Rpc & { '~cluster': 'testnet' }; +export type RpcMainnet = Rpc & { '~cluster': 'mainnet' }; +export type RpcFromTransport< + TRpcMethods, + TRpcTransport extends IRpcTransport | IRpcTransportWithCluster, +> = TRpcTransport extends IRpcTransportDevnet + ? RpcDevnet + : TRpcTransport extends IRpcTransportTestnet + ? RpcTestnet + : TRpcTransport extends IRpcTransportMainnet + ? RpcMainnet + : Rpc; diff --git a/packages/rpc-transport/src/json-rpc.ts b/packages/rpc-transport/src/json-rpc.ts index 128bf39c2c76..2307c12240b1 100644 --- a/packages/rpc-transport/src/json-rpc.ts +++ b/packages/rpc-transport/src/json-rpc.ts @@ -1,8 +1,15 @@ -import { PendingRpcRequest, Rpc, RpcRequest, SendOptions } from '@solana/rpc-types'; +import { IRpcApi, PendingRpcRequest, Rpc, RpcRequest, SendOptions } from '@solana/rpc-types'; import { RpcConfig } from './json-rpc-config'; import { SolanaJsonRpcError } from './json-rpc-errors'; import { createJsonRpcMessage } from './json-rpc-message'; +import { RpcDevnet, RpcFromTransport, RpcMainnet, RpcTestnet } from './json-rpc-types'; +import { + IRpcTransport, + IRpcTransportDevnet, + IRpcTransportMainnet, + IRpcTransportTestnet, +} from './transports/transport-types'; interface IHasIdentifier { readonly id: number; @@ -54,6 +61,32 @@ function makeProxy(rpcConfig: RpcConfig): Rpc; } -export function createJsonRpc(rpcConfig: RpcConfig): Rpc { - return makeProxy(rpcConfig); +export function createJsonRpc( + rpcConfig: Readonly<{ + api: IRpcApi; + transport: IRpcTransportDevnet; + }>, +): RpcDevnet; +export function createJsonRpc( + rpcConfig: Readonly<{ + api: IRpcApi; + transport: IRpcTransportTestnet; + }>, +): RpcTestnet; +export function createJsonRpc( + rpcConfig: Readonly<{ + api: IRpcApi; + transport: IRpcTransportMainnet; + }>, +): RpcMainnet; +export function createJsonRpc( + rpcConfig: Readonly<{ + api: IRpcApi; + transport: IRpcTransport; + }>, +): Rpc; +export function createJsonRpc>( + rpcConfig: TConfig, +): RpcFromTransport { + return makeProxy(rpcConfig) as RpcFromTransport; } diff --git a/packages/rpc-transport/src/transports/http/__typetests__/http-transport-typetest.ts b/packages/rpc-transport/src/transports/http/__typetests__/http-transport-typetest.ts new file mode 100644 index 000000000000..95d1314e776f --- /dev/null +++ b/packages/rpc-transport/src/transports/http/__typetests__/http-transport-typetest.ts @@ -0,0 +1,39 @@ +import { devnet, mainnet, testnet } from '@solana/rpc-types'; + +import { IRpcTransport, IRpcTransportDevnet, IRpcTransportMainnet, IRpcTransportTestnet } from '../../transport-types'; +import { createHttpTransport } from '../http-transport'; + +const genericUrl = 'http://localhost:8899'; +const devnetUrl = devnet('https://api.devnet.solana.com'); +const testnetUrl = testnet('https://api.testnet.solana.com'); +const mainnetUrl = mainnet('https://api.mainnet-beta.solana.com'); + +// When providing a generic URL, the transport should be typed as an IRpcTransport +createHttpTransport({ url: genericUrl }) satisfies IRpcTransport; +//@ts-expect-error Should not be a devnet transport +createHttpTransport({ url: genericUrl }) satisfies IRpcTransportDevnet; +//@ts-expect-error Should not be a testnet transport +createHttpTransport({ url: genericUrl }) satisfies IRpcTransportTestnet; +//@ts-expect-error Should not be a mainnet transport +createHttpTransport({ url: genericUrl }) satisfies IRpcTransportMainnet; + +// When providing a devnet URL, the transport should be typed as an IRpcTransportDevnet +createHttpTransport({ url: devnetUrl }) satisfies IRpcTransportDevnet; +//@ts-expect-error Should not be a testnet transport +createHttpTransport({ url: devnetUrl }) satisfies IRpcTransportTestnet; +//@ts-expect-error Should not be a mainnet transport +createHttpTransport({ url: devnetUrl }) satisfies IRpcTransportMainnet; + +// When providing a testnet URL, the transport should be typed as an IRpcTransportTestnet +createHttpTransport({ url: testnetUrl }) satisfies IRpcTransportTestnet; +//@ts-expect-error Should not be a devnet transport +createHttpTransport({ url: testnetUrl }) satisfies IRpcTransportDevnet; +//@ts-expect-error Should not be a mainnet transport +createHttpTransport({ url: testnetUrl }) satisfies IRpcTransportMainnet; + +// When providing a mainnet URL, the transport should be typed as an IRpcTransportMainnet +createHttpTransport({ url: mainnetUrl }) satisfies IRpcTransportMainnet; +//@ts-expect-error Should not be a devnet transport +createHttpTransport({ url: mainnetUrl }) satisfies IRpcTransportDevnet; +//@ts-expect-error Should not be a testnet transport +createHttpTransport({ url: mainnetUrl }) satisfies IRpcTransportTestnet; diff --git a/packages/rpc-transport/src/transports/http/http-transport.ts b/packages/rpc-transport/src/transports/http/http-transport.ts index d62c45922973..7f6efd6322c0 100644 --- a/packages/rpc-transport/src/transports/http/http-transport.ts +++ b/packages/rpc-transport/src/transports/http/http-transport.ts @@ -1,6 +1,7 @@ +import { ClusterUrl } from '@solana/rpc-types'; import fetchImpl from 'fetch-impl'; -import { IRpcTransport } from '../transport-types'; +import { IRpcTransport, IRpcTransportFromClusterUrl } from '../transport-types'; import { SolanaHttpError } from './http-transport-errors'; import { AllowedHttpRequestHeaders, @@ -8,12 +9,15 @@ import { normalizeHeaders, } from './http-transport-headers'; -type Config = Readonly<{ +type Config = Readonly<{ headers?: AllowedHttpRequestHeaders; - url: string; + url: TClusterUrl; }>; -export function createHttpTransport({ headers, url }: Config): IRpcTransport { +export function createHttpTransport({ + headers, + url, +}: Config): IRpcTransportFromClusterUrl { if (__DEV__ && headers) { assertIsAllowedHttpRequestHeaders(headers); } @@ -43,5 +47,5 @@ export function createHttpTransport({ headers, url }: Config): IRpcTransport { }); } return (await response.json()) as TResponse; - }; + } as IRpcTransportFromClusterUrl; } diff --git a/packages/rpc-transport/src/transports/transport-types.ts b/packages/rpc-transport/src/transports/transport-types.ts index 4dc25bdd49e8..ad4687a75383 100644 --- a/packages/rpc-transport/src/transports/transport-types.ts +++ b/packages/rpc-transport/src/transports/transport-types.ts @@ -1,3 +1,5 @@ +import { ClusterUrl, DevnetUrl, MainnetUrl, TestnetUrl } from '@solana/rpc-types'; + import { RpcWebSocketConnection } from './websocket/websocket-connection'; type RpcTransportConfig = Readonly<{ @@ -8,6 +10,17 @@ type RpcTransportConfig = Readonly<{ export interface IRpcTransport { (config: RpcTransportConfig): Promise; } +export type IRpcTransportDevnet = IRpcTransport & { '~cluster': 'devnet' }; +export type IRpcTransportTestnet = IRpcTransport & { '~cluster': 'testnet' }; +export type IRpcTransportMainnet = IRpcTransport & { '~cluster': 'mainnet' }; +export type IRpcTransportWithCluster = IRpcTransportDevnet | IRpcTransportTestnet | IRpcTransportMainnet; +export type IRpcTransportFromClusterUrl = TClusterUrl extends DevnetUrl + ? IRpcTransportDevnet + : TClusterUrl extends TestnetUrl + ? IRpcTransportTestnet + : TClusterUrl extends MainnetUrl + ? IRpcTransportMainnet + : IRpcTransport; type RpcWebSocketTransportConfig = Readonly<{ payload: unknown;