Skip to content

Commit

Permalink
refactor(experimental): add cluster level API for transports
Browse files Browse the repository at this point in the history
This change introduces cluster-level transports as well as cluster-level JSON RPCs
that infer the cluster from the provided transport.
  • Loading branch information
buffalojoec committed Feb 2, 2024
1 parent 61f7ba0 commit e58bb22
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 14 deletions.
48 changes: 48 additions & 0 deletions packages/rpc-transport/src/__typetests__/json-rpc-typetest.ts
Original file line number Diff line number Diff line change
@@ -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<MyApiMethods>();

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<MyApiMethods>;
//@ts-expect-error Should not be a devnet RPC
createJsonRpc({ api, transport: genericTransport }) satisfies RpcDevnet<MyApiMethods>;
//@ts-expect-error Should not be a testnet RPC
createJsonRpc({ api, transport: genericTransport }) satisfies RpcTestnet<MyApiMethods>;
//@ts-expect-error Should not be a mainnet RPC
createJsonRpc({ api, transport: genericTransport }) satisfies RpcMainnet<MyApiMethods>;

// When providing a devnet transport, the RPC should be typed as an RpcDevnet
createJsonRpc({ api, transport: devnetTransport }) satisfies RpcDevnet<MyApiMethods>;
//@ts-expect-error Should not be a testnet RPC
createJsonRpc({ api, transport: devnetTransport }) satisfies RpcTestnet<MyApiMethods>;
//@ts-expect-error Should not be a mainnet RPC
createJsonRpc({ api, transport: devnetTransport }) satisfies RpcMainnet<MyApiMethods>;

// When providing a testnet transport, the RPC should be typed as an RpcTestnet
createJsonRpc({ api, transport: testnetTransport }) satisfies RpcTestnet<MyApiMethods>;
//@ts-expect-error Should not be a devnet RPC
createJsonRpc({ api, transport: testnetTransport }) satisfies RpcDevnet<MyApiMethods>;
//@ts-expect-error Should not be a mainnet RPC
createJsonRpc({ api, transport: testnetTransport }) satisfies RpcMainnet<MyApiMethods>;

// When providing a mainnet transport, the RPC should be typed as an RpcMainnet
createJsonRpc({ api, transport: mainnetTransport }) satisfies RpcMainnet<MyApiMethods>;
//@ts-expect-error Should not be a devnet RPC
createJsonRpc({ api, transport: mainnetTransport }) satisfies RpcDevnet<MyApiMethods>;
//@ts-expect-error Should not be a testnet RPC
createJsonRpc({ api, transport: mainnetTransport }) satisfies RpcTestnet<MyApiMethods>;
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions packages/rpc-transport/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions packages/rpc-transport/src/json-rpc-config.ts
Original file line number Diff line number Diff line change
@@ -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<TRpcMethods> = Readonly<{
api: IRpcApi<TRpcMethods>;
transport: IRpcTransport;
transport: IRpcTransport | IRpcTransportWithCluster;
}>;

export type RpcSubscriptionConfig<TRpcMethods> = Readonly<{
Expand Down
23 changes: 23 additions & 0 deletions packages/rpc-transport/src/json-rpc-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Rpc } from '@solana/rpc-types';

import {
IRpcTransport,
IRpcTransportDevnet,
IRpcTransportMainnet,
IRpcTransportTestnet,
IRpcTransportWithCluster,
} from './transports/transport-types';

export type RpcDevnet<TRpcMethods> = Rpc<TRpcMethods> & { '~cluster': 'devnet' };
export type RpcTestnet<TRpcMethods> = Rpc<TRpcMethods> & { '~cluster': 'testnet' };
export type RpcMainnet<TRpcMethods> = Rpc<TRpcMethods> & { '~cluster': 'mainnet' };
export type RpcFromTransport<
TRpcMethods,
TRpcTransport extends IRpcTransport | IRpcTransportWithCluster,
> = TRpcTransport extends IRpcTransportDevnet
? RpcDevnet<TRpcMethods>
: TRpcTransport extends IRpcTransportTestnet
? RpcTestnet<TRpcMethods>
: TRpcTransport extends IRpcTransportMainnet
? RpcMainnet<TRpcMethods>
: Rpc<TRpcMethods>;
39 changes: 36 additions & 3 deletions packages/rpc-transport/src/json-rpc.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -54,6 +61,32 @@ function makeProxy<TRpcMethods>(rpcConfig: RpcConfig<TRpcMethods>): Rpc<TRpcMeth
}) as Rpc<TRpcMethods>;
}

export function createJsonRpc<TRpcMethods>(rpcConfig: RpcConfig<TRpcMethods>): Rpc<TRpcMethods> {
return makeProxy(rpcConfig);
export function createJsonRpc<TRpcMethods>(
rpcConfig: Readonly<{
api: IRpcApi<TRpcMethods>;
transport: IRpcTransportDevnet;
}>,
): RpcDevnet<TRpcMethods>;
export function createJsonRpc<TRpcMethods>(
rpcConfig: Readonly<{
api: IRpcApi<TRpcMethods>;
transport: IRpcTransportTestnet;
}>,
): RpcTestnet<TRpcMethods>;
export function createJsonRpc<TRpcMethods>(
rpcConfig: Readonly<{
api: IRpcApi<TRpcMethods>;
transport: IRpcTransportMainnet;
}>,
): RpcMainnet<TRpcMethods>;
export function createJsonRpc<TRpcMethods>(
rpcConfig: Readonly<{
api: IRpcApi<TRpcMethods>;
transport: IRpcTransport;
}>,
): Rpc<TRpcMethods>;
export function createJsonRpc<TRpcMethods, TConfig extends RpcConfig<TRpcMethods>>(
rpcConfig: TConfig,
): RpcFromTransport<TRpcMethods, TConfig['transport']> {
return makeProxy(rpcConfig) as RpcFromTransport<TRpcMethods, TConfig['transport']>;
}
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 9 additions & 5 deletions packages/rpc-transport/src/transports/http/http-transport.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
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,
assertIsAllowedHttpRequestHeaders,
normalizeHeaders,
} from './http-transport-headers';

type Config = Readonly<{
type Config<TClusterUrl extends ClusterUrl> = Readonly<{
headers?: AllowedHttpRequestHeaders;
url: string;
url: TClusterUrl;
}>;

export function createHttpTransport({ headers, url }: Config): IRpcTransport {
export function createHttpTransport<TClusterUrl extends ClusterUrl>({
headers,
url,
}: Config<TClusterUrl>): IRpcTransportFromClusterUrl<TClusterUrl> {
if (__DEV__ && headers) {
assertIsAllowedHttpRequestHeaders(headers);
}
Expand Down Expand Up @@ -43,5 +47,5 @@ export function createHttpTransport({ headers, url }: Config): IRpcTransport {
});
}
return (await response.json()) as TResponse;
};
} as IRpcTransportFromClusterUrl<TClusterUrl>;
}
13 changes: 13 additions & 0 deletions packages/rpc-transport/src/transports/transport-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ClusterUrl, DevnetUrl, MainnetUrl, TestnetUrl } from '@solana/rpc-types';

import { RpcWebSocketConnection } from './websocket/websocket-connection';

type RpcTransportConfig = Readonly<{
Expand All @@ -8,6 +10,17 @@ type RpcTransportConfig = Readonly<{
export interface IRpcTransport {
<TResponse>(config: RpcTransportConfig): Promise<TResponse>;
}
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 ClusterUrl> = TClusterUrl extends DevnetUrl
? IRpcTransportDevnet
: TClusterUrl extends TestnetUrl
? IRpcTransportTestnet
: TClusterUrl extends MainnetUrl
? IRpcTransportMainnet
: IRpcTransport;

type RpcWebSocketTransportConfig = Readonly<{
payload: unknown;
Expand Down

0 comments on commit e58bb22

Please sign in to comment.