Skip to content

Commit ae3f1f0

Browse files
authored
refactor(experimental): add generic createJsonRpcSubscriptionsApi for custom APIs
Continuing the work from #1781, this PR adds a generic function for creating an RPC Subscriptions API, and drives this function from the main `@solana/web3.js`.
1 parent 15440e6 commit ae3f1f0

File tree

8 files changed

+106
-56
lines changed

8 files changed

+106
-56
lines changed

packages/rpc-core/src/rpc-subscriptions/index.ts

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IRpcSubscriptionsApi, RpcSubscription } from '@solana/rpc-transport';
1+
import { createJsonRpcSubscriptionsApi, IRpcSubscriptionsApi } from '@solana/rpc-transport';
22

33
import { patchParamsForSolanaLabsRpc } from '../params-patcher';
44
import { patchResponseForSolanaLabsRpcSubscriptions } from '../response-patcher';
@@ -25,63 +25,38 @@ export type SolanaRpcSubscriptions = AccountNotificationsApi &
2525
SlotNotificationsApi;
2626
export type SolanaRpcSubscriptionsUnstable = SlotsUpdatesNotificationsApi & VoteNotificationsApi;
2727

28-
export function createSolanaRpcSubscriptionsApi(
28+
export function createSolanaRpcSubscriptionsApi_INTERNAL(
2929
config?: Config,
3030
): IRpcSubscriptionsApi<SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable> {
31-
return new Proxy({} as IRpcSubscriptionsApi<SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable>, {
32-
defineProperty() {
33-
return false;
34-
},
35-
deleteProperty() {
36-
return false;
37-
},
38-
get<
39-
TNotificationName extends keyof IRpcSubscriptionsApi<
40-
SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable
41-
>,
42-
>(
43-
...args: Parameters<
44-
NonNullable<
45-
ProxyHandler<IRpcSubscriptionsApi<SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable>>['get']
46-
>
47-
>
48-
) {
49-
const [_, p] = args;
50-
const notificationName = p.toString() as keyof (SolanaRpcSubscriptions &
51-
SolanaRpcSubscriptionsUnstable) as string;
52-
return function (
53-
...rawParams: Parameters<
54-
(SolanaRpcSubscriptions &
55-
SolanaRpcSubscriptionsUnstable)[TNotificationName] extends CallableFunction
56-
? (SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable)[TNotificationName]
57-
: never
58-
>
59-
): RpcSubscription<
60-
ReturnType<(SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable)[TNotificationName]>
61-
> {
62-
const handleIntegerOverflow = config?.onIntegerOverflow;
63-
const params = patchParamsForSolanaLabsRpc(
64-
rawParams,
65-
handleIntegerOverflow
66-
? (keyPath, value) => handleIntegerOverflow(notificationName, keyPath, value)
67-
: undefined,
68-
);
69-
return {
70-
params,
71-
responseTransformer: rawResponse =>
72-
patchResponseForSolanaLabsRpcSubscriptions(rawResponse, notificationName),
73-
subscribeMethodName: notificationName.replace(/Notifications$/, 'Subscribe'),
74-
unsubscribeMethodName: notificationName.replace(/Notifications$/, 'Unsubscribe'),
75-
};
76-
};
77-
},
31+
const handleIntegerOverflow = config?.onIntegerOverflow;
32+
return createJsonRpcSubscriptionsApi<SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable>({
33+
parametersTransformer: <T>(rawParams: T, methodName: string) =>
34+
patchParamsForSolanaLabsRpc(
35+
rawParams,
36+
handleIntegerOverflow
37+
? (keyPath, value) => handleIntegerOverflow(methodName, keyPath, value)
38+
: undefined,
39+
) as unknown[],
40+
responseTransformer: <T>(rawResponse: unknown, methodName: string): T =>
41+
patchResponseForSolanaLabsRpcSubscriptions(
42+
rawResponse,
43+
methodName as keyof (SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable),
44+
),
45+
subscribeNotificationNameTransformer: (notificationName: string) =>
46+
notificationName.replace(/Notifications$/, 'Subscribe'),
47+
unsubscribeNotificationNameTransformer: (notificationName: string) =>
48+
notificationName.replace(/Notifications$/, 'Unsubscribe'),
7849
});
7950
}
8051

52+
export function createSolanaRpcSubscriptionsApi(config?: Config): IRpcSubscriptionsApi<SolanaRpcSubscriptions> {
53+
return createSolanaRpcSubscriptionsApi_INTERNAL(config) as IRpcSubscriptionsApi<SolanaRpcSubscriptions>;
54+
}
55+
8156
export function createSolanaRpcSubscriptionsApi_UNSTABLE(
8257
config?: Config,
8358
): IRpcSubscriptionsApi<SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable> {
84-
return createSolanaRpcSubscriptionsApi(config) as IRpcSubscriptionsApi<
59+
return createSolanaRpcSubscriptionsApi_INTERNAL(config) as IRpcSubscriptionsApi<
8560
SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable
8661
>;
8762
}

packages/rpc-transport/src/__tests__/json-rpc-subscription-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ describe('JSON-RPC 2.0 Subscriptions', () => {
328328
.thingNotifications()
329329
.subscribe({ abortSignal: new AbortController().signal });
330330
await thingNotifications[Symbol.asyncIterator]().next();
331-
expect(responseTransformer).toHaveBeenCalledWith(123);
331+
expect(responseTransformer).toHaveBeenCalledWith(123, 'thingSubscribe');
332332
});
333333
it('returns the processed response', async () => {
334334
expect.assertions(1);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { IRpcSubscriptionsApi } from '../../json-rpc-types';
2+
import { IRpcApiMethods } from '../api-types';
3+
import { createJsonRpcSubscriptionsApi } from '../subscriptions/subscriptions-api';
4+
5+
type NftCollectionDetailsApiResponse = Readonly<{
6+
address: string;
7+
circulatingSupply: number;
8+
description: string;
9+
erc721: boolean;
10+
erc1155: boolean;
11+
genesisBlock: string;
12+
genesisTransaction: string;
13+
name: string;
14+
totalSupply: number;
15+
}>;
16+
17+
interface NftCollectionDetailsApi extends IRpcApiMethods {
18+
qn_fetchNFTCollectionDetails(args: { contracts: string[] }): NftCollectionDetailsApiResponse;
19+
}
20+
21+
type QuickNodeRpcMethods = NftCollectionDetailsApi;
22+
23+
createJsonRpcSubscriptionsApi<QuickNodeRpcMethods>() satisfies IRpcSubscriptionsApi<QuickNodeRpcMethods>;

packages/rpc-transport/src/apis/api-types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ export interface IRpcApiMethods {
1313

1414
// RPC Subscription Methods
1515
export type RpcSubscriptionsApiConfig = Readonly<{
16-
parametersTransformer?: <T>(params: T, methodName: string) => unknown[];
17-
responseTransformer?: <T>(response: unknown, methodName: string) => T;
16+
parametersTransformer?: <T>(params: T, notificationName: string) => unknown[];
17+
responseTransformer?: <T>(response: unknown, notificationName: string) => T;
18+
subscribeNotificationNameTransformer?: (notificationName: string) => string;
19+
unsubscribeNotificationNameTransformer?: (notificationName: string) => string;
1820
}>;
1921

2022
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2123
type RpcSubscription = (...args: any) => any;
2224

2325
export interface IRpcApiSubscriptions {
24-
[methodName: string]: RpcSubscription;
26+
[notificationName: string]: RpcSubscription;
2527
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { IRpcSubscriptionsApi, RpcSubscription } from '../../json-rpc-types';
2+
import { IRpcApiSubscriptions, RpcSubscriptionsApiConfig } from '../api-types';
3+
4+
export function createJsonRpcSubscriptionsApi<TRpcSubscriptions extends IRpcApiSubscriptions>(
5+
config?: RpcSubscriptionsApiConfig,
6+
): IRpcSubscriptionsApi<TRpcSubscriptions> {
7+
return new Proxy({} as IRpcSubscriptionsApi<TRpcSubscriptions>, {
8+
defineProperty() {
9+
return false;
10+
},
11+
deleteProperty() {
12+
return false;
13+
},
14+
get<TNotificationName extends keyof IRpcSubscriptionsApi<TRpcSubscriptions>>(
15+
...args: Parameters<NonNullable<ProxyHandler<IRpcSubscriptionsApi<TRpcSubscriptions>>['get']>>
16+
) {
17+
const [_, p] = args;
18+
const notificationName = p.toString() as keyof TRpcSubscriptions as string;
19+
return function (
20+
...rawParams: Parameters<
21+
TRpcSubscriptions[TNotificationName] extends CallableFunction
22+
? TRpcSubscriptions[TNotificationName]
23+
: never
24+
>
25+
): RpcSubscription<ReturnType<TRpcSubscriptions[TNotificationName]>> {
26+
const params = config?.parametersTransformer
27+
? config?.parametersTransformer(rawParams, notificationName)
28+
: rawParams;
29+
const responseTransformer = config?.responseTransformer
30+
? config?.responseTransformer<ReturnType<TRpcSubscriptions[TNotificationName]>>
31+
: (rawResponse: unknown) => rawResponse as ReturnType<TRpcSubscriptions[TNotificationName]>;
32+
const subscribeMethodName = config?.subscribeNotificationNameTransformer
33+
? config?.subscribeNotificationNameTransformer(notificationName)
34+
: notificationName;
35+
const unsubscribeMethodName = config?.unsubscribeNotificationNameTransformer
36+
? config?.unsubscribeNotificationNameTransformer(notificationName)
37+
: notificationName;
38+
return {
39+
params,
40+
responseTransformer,
41+
subscribeMethodName,
42+
unsubscribeMethodName,
43+
};
44+
};
45+
},
46+
});
47+
}

packages/rpc-transport/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './apis/api-types';
22
export * from './apis/methods/methods-api';
3+
export * from './apis/subscriptions/subscriptions-api';
34
export * from './json-rpc';
45
export type { SolanaJsonRpcErrorCode } from './json-rpc-errors';
56
export * from './json-rpc-subscription';

packages/rpc-transport/src/json-rpc-subscription.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ function createPendingRpcSubscription<TRpcSubscriptionMethods, TNotification>(
9494
continue;
9595
}
9696
const notification = message.params.result as TNotification;
97-
yield responseTransformer ? responseTransformer(notification) : notification;
97+
yield responseTransformer
98+
? responseTransformer(notification, subscribeMethodName)
99+
: notification;
98100
}
99101
},
100102
};

packages/rpc-transport/src/json-rpc-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type RpcRequest<TResponse> = {
3030
};
3131
export type RpcSubscription<TResponse> = {
3232
params: unknown[];
33-
responseTransformer?: (response: unknown) => TResponse;
33+
responseTransformer?: (response: unknown, notificationName: string) => TResponse;
3434
subscribeMethodName: string;
3535
unsubscribeMethodName: string;
3636
};

0 commit comments

Comments
 (0)