Skip to content

Commit 789cdc7

Browse files
committed
feat: add transaction rpc helper and prep utilities
1 parent c02ca1e commit 789cdc7

18 files changed

+778
-32
lines changed

examples/react-hooks/src/App.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SolanaClientConfig, WalletConnector } from '@solana/client-core';
1+
import { createSolanaRpcClient, type SolanaClientConfig, type WalletConnector } from '@solana/client-core';
22
import { SolanaClientProvider, useConnectWallet, useWallet, useWalletStandardConnectors } from '@solana/react-hooks';
33
import { useEffect, useMemo, useRef } from 'react';
44

@@ -18,13 +18,23 @@ const DEFAULT_CLIENT_CONFIG: SolanaClientConfig = {
1818

1919
export default function App() {
2020
const walletConnectors = useWalletStandardConnectors();
21+
const rpcClient = useMemo(
22+
() =>
23+
createSolanaRpcClient({
24+
commitment: DEFAULT_CLIENT_CONFIG.commitment,
25+
endpoint: DEFAULT_CLIENT_CONFIG.endpoint,
26+
websocketEndpoint: DEFAULT_CLIENT_CONFIG.websocketEndpoint,
27+
}),
28+
[],
29+
);
2130

2231
const clientConfig = useMemo<SolanaClientConfig>(
2332
() => ({
2433
...DEFAULT_CLIENT_CONFIG,
34+
rpcClient,
2535
walletConnectors,
2636
}),
27-
[walletConnectors],
37+
[rpcClient, walletConnectors],
2838
);
2939

3040
return (

packages/client-core/src/client/actions.test.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import type { ClientActions, SolanaClientRuntime, WalletConnector, WalletRegistr
66
import { createActions } from './actions';
77
import { createDefaultClientStore } from './createClientStore';
88

9-
const createSolanaRpcMock = vi.hoisted(() => vi.fn());
10-
const createSolanaRpcSubscriptionsMock = vi.hoisted(() => vi.fn());
119
const getBase64EncodedWireTransactionMock = vi.hoisted(() => vi.fn((tx: unknown) => `wire:${String(tx)}`));
1210
const airdropFactoryMock = vi.hoisted(() => vi.fn());
1311
const createBlockHeightExceedencePromiseFactoryMock = vi.hoisted(() => vi.fn());
@@ -20,13 +18,27 @@ const nowMock = vi.hoisted(() => {
2018
return vi.fn(() => ++current);
2119
});
2220

21+
const createSolanaRpcClientMock = vi.hoisted(() =>
22+
vi.fn(() => ({
23+
commitment: 'confirmed',
24+
endpoint: 'https://rpc.test',
25+
rpc: {} as SolanaClientRuntime['rpc'],
26+
rpcSubscriptions: {} as SolanaClientRuntime['rpcSubscriptions'],
27+
sendAndConfirmTransaction: vi.fn(),
28+
simulateTransaction: vi.fn(),
29+
websocketEndpoint: 'wss://rpc.test',
30+
})),
31+
);
32+
2333
vi.mock('@solana/kit', () => ({
24-
createSolanaRpc: createSolanaRpcMock,
25-
createSolanaRpcSubscriptions: createSolanaRpcSubscriptionsMock,
2634
getBase64EncodedWireTransaction: getBase64EncodedWireTransactionMock,
2735
airdropFactory: airdropFactoryMock,
2836
}));
2937

38+
vi.mock('../rpc/createSolanaRpcClient', () => ({
39+
createSolanaRpcClient: createSolanaRpcClientMock,
40+
}));
41+
3042
vi.mock('@solana/transaction-confirmation', () => ({
3143
createBlockHeightExceedencePromiseFactory: createBlockHeightExceedencePromiseFactoryMock,
3244
createRecentSignatureConfirmationPromiseFactory: createRecentSignatureConfirmationPromiseFactoryMock,
@@ -92,14 +104,21 @@ describe('client actions', () => {
92104
signatureNotifications: vi.fn(),
93105
} as unknown as SolanaClientRuntime['rpcSubscriptions'];
94106

95-
createSolanaRpcMock.mockReturnValue(rpc);
96-
createSolanaRpcSubscriptionsMock.mockReturnValue(rpcSubscriptions);
97-
98107
runtime = {
99108
rpc: rpc as unknown as SolanaClientRuntime['rpc'],
100109
rpcSubscriptions,
101110
};
102111

112+
createSolanaRpcClientMock.mockImplementation(({ commitment, endpoint, websocketEndpoint }) => ({
113+
commitment: commitment ?? 'confirmed',
114+
endpoint,
115+
rpc: { endpoint } as SolanaClientRuntime['rpc'],
116+
rpcSubscriptions: { endpoint: websocketEndpoint ?? endpoint } as SolanaClientRuntime['rpcSubscriptions'],
117+
sendAndConfirmTransaction: vi.fn(),
118+
simulateTransaction: vi.fn(),
119+
websocketEndpoint: websocketEndpoint ?? endpoint,
120+
}));
121+
103122
walletConnector = {
104123
id: 'wallet-1',
105124
name: 'Wallet 1',
@@ -134,8 +153,11 @@ describe('client actions', () => {
134153
const state = store.getState();
135154
expect(state.cluster.endpoint).toBe('https://new.rpc');
136155
expect(state.cluster.status).toMatchObject({ status: 'ready' });
137-
expect(createSolanaRpcMock).toHaveBeenCalledWith('https://new.rpc');
138-
expect(createSolanaRpcSubscriptionsMock).toHaveBeenCalledWith('wss://new');
156+
expect(createSolanaRpcClientMock).toHaveBeenCalledWith({
157+
commitment: 'processed',
158+
endpoint: 'https://new.rpc',
159+
websocketEndpoint: 'wss://new',
160+
});
139161
});
140162

141163
it('connects and disconnects a wallet, handling errors', async () => {

packages/client-core/src/client/actions.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ import type {
77
Signature,
88
Transaction,
99
} from '@solana/kit';
10-
import {
11-
airdropFactory,
12-
createSolanaRpc,
13-
createSolanaRpcSubscriptions,
14-
getBase64EncodedWireTransaction,
15-
} from '@solana/kit';
10+
import { airdropFactory, getBase64EncodedWireTransaction } from '@solana/kit';
1611
import type { TransactionWithLastValidBlockHeight } from '@solana/transaction-confirmation';
1712
import {
1813
createBlockHeightExceedencePromiseFactory,
@@ -21,6 +16,7 @@ import {
2116
} from '@solana/transaction-confirmation';
2217

2318
import { createLogger, formatError } from '../logging/logger';
19+
import { createSolanaRpcClient } from '../rpc/createSolanaRpcClient';
2420
import type { ClientActions, ClientState, ClientStore, SolanaClientRuntime, WalletRegistry } from '../types';
2521
import { now } from '../utils';
2622

@@ -113,8 +109,13 @@ export function createActions({ connectors, logger: inputLogger, runtime, store
113109
lastUpdatedAt: now(),
114110
}));
115111
try {
116-
runtime.rpc = createSolanaRpc(endpoint);
117-
runtime.rpcSubscriptions = createSolanaRpcSubscriptions(websocketEndpoint);
112+
const newRpcClient = createSolanaRpcClient({
113+
commitment: nextCommitment,
114+
endpoint,
115+
websocketEndpoint,
116+
});
117+
runtime.rpc = newRpcClient.rpc;
118+
runtime.rpcSubscriptions = newRpcClient.rpcSubscriptions;
118119
const latencyMs = await warmupCluster(endpoint, nextCommitment);
119120
store.setState((state) => ({
120121
...state,

packages/client-core/src/client/createClient.test.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest';
22

3+
import type { SolanaRpcClient } from '../rpc/createSolanaRpcClient';
34
import type { SolanaClientConfig } from '../types';
45
import { createClient } from './createClient';
56

@@ -9,8 +10,6 @@ type Logger = ReturnType<ReturnType<typeof createLoggerMock>>;
910

1011
type Helpers = ReturnType<typeof createClientHelpersMock>;
1112

12-
const createSolanaRpcMock = vi.hoisted(() => vi.fn(() => ({ rpc: true })));
13-
const createSolanaRpcSubscriptionsMock = vi.hoisted(() => vi.fn(() => ({ sub: true })));
1413
const createActionsMock = vi.hoisted(() =>
1514
vi.fn(() => ({
1615
setCluster: vi.fn().mockResolvedValue(undefined),
@@ -37,10 +36,23 @@ const createWalletRegistryMock = vi.hoisted(() =>
3736
const createLoggerMock = vi.hoisted(() => vi.fn(() => vi.fn()));
3837
const formatErrorMock = vi.hoisted(() => vi.fn((error: unknown) => ({ formatted: error })));
3938
const nowMock = vi.hoisted(() => vi.fn(() => 111));
39+
const createSolanaRpcClientMock = vi.hoisted(() =>
40+
vi.fn(
41+
() =>
42+
({
43+
commitment: 'confirmed',
44+
endpoint: 'https://rpc.example',
45+
rpc: { tag: 'rpc' },
46+
rpcSubscriptions: { tag: 'sub' },
47+
sendAndConfirmTransaction: vi.fn(),
48+
simulateTransaction: vi.fn(),
49+
websocketEndpoint: 'wss://rpc.example',
50+
}) satisfies SolanaRpcClient,
51+
),
52+
);
4053

41-
vi.mock('@solana/kit', () => ({
42-
createSolanaRpc: createSolanaRpcMock,
43-
createSolanaRpcSubscriptions: createSolanaRpcSubscriptionsMock,
54+
vi.mock('../rpc/createSolanaRpcClient', () => ({
55+
createSolanaRpcClient: createSolanaRpcClientMock,
4456
}));
4557

4658
vi.mock('../logging/logger', () => ({
@@ -87,8 +99,11 @@ describe('createClient', () => {
8799

88100
it('instantiates the client with runtime wiring and helpers', async () => {
89101
const client = createClient(config);
90-
expect(createSolanaRpcMock).toHaveBeenCalledWith('https://rpc.example');
91-
expect(createSolanaRpcSubscriptionsMock).toHaveBeenCalledWith('https://rpc.example');
102+
expect(createSolanaRpcClientMock).toHaveBeenCalledWith({
103+
commitment: 'finalized',
104+
endpoint: 'https://rpc.example',
105+
websocketEndpoint: 'https://rpc.example',
106+
});
92107
expect(createWalletRegistryMock).toHaveBeenCalledWith(config.walletConnectors);
93108
expect(createActionsMock).toHaveBeenCalled();
94109
expect(createWatchersMock).toHaveBeenCalled();
@@ -110,6 +125,24 @@ describe('createClient', () => {
110125
expect(client.store.getState().cluster.status).toEqual({ status: 'idle' });
111126
});
112127

128+
it('respects a provided rpcClient instance', () => {
129+
const rpcClient = {
130+
commitment: 'processed',
131+
endpoint: 'https://rpc.example',
132+
rpc: { tag: 'external-rpc' },
133+
rpcSubscriptions: { tag: 'external-sub' },
134+
sendAndConfirmTransaction: vi.fn(),
135+
simulateTransaction: vi.fn(),
136+
websocketEndpoint: 'wss://rpc.example',
137+
} satisfies SolanaRpcClient;
138+
createClient({
139+
...config,
140+
commitment: 'processed',
141+
rpcClient,
142+
});
143+
expect(createSolanaRpcClientMock).not.toHaveBeenCalled();
144+
});
145+
113146
it('logs errors when initial cluster setup fails', async () => {
114147
const logger = vi.fn();
115148
createLoggerMock.mockReturnValueOnce(logger as Logger);

packages/client-core/src/client/createClient.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit';
2-
31
import { createLogger, formatError } from '../logging/logger';
2+
import { createSolanaRpcClient } from '../rpc/createSolanaRpcClient';
43
import type { ClientStore, SolanaClient, SolanaClientConfig, SolanaClientRuntime } from '../types';
54
import { now } from '../utils';
65
import { createWalletRegistry } from '../wallet/registry';
@@ -24,9 +23,16 @@ export function createClient(config: SolanaClientConfig): SolanaClient {
2423
websocketEndpoint,
2524
});
2625
const store: ClientStore = config.createStore ? config.createStore(initialState) : createClientStore(initialState);
26+
const rpcClient =
27+
config.rpcClient ??
28+
createSolanaRpcClient({
29+
commitment,
30+
endpoint: config.endpoint,
31+
websocketEndpoint,
32+
});
2733
const runtime: SolanaClientRuntime = {
28-
rpc: createSolanaRpc(config.endpoint),
29-
rpcSubscriptions: createSolanaRpcSubscriptions(websocketEndpoint),
34+
rpc: rpcClient.rpc,
35+
rpcSubscriptions: rpcClient.rpcSubscriptions,
3036
};
3137
const connectors = createWalletRegistry(config.walletConnectors ?? []);
3238
const logger = createLogger(config.logger);
@@ -78,6 +84,7 @@ export function createClient(config: SolanaClientConfig): SolanaClient {
7884
get transaction() {
7985
return helpers.transaction;
8086
},
87+
prepareTransaction: helpers.prepareTransaction,
8188
watchers,
8289
};
8390
}

packages/client-core/src/client/createClientHelpers.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
33

44
import type { SolTransferPrepareConfig } from '../features/sol';
55
import type { SplTokenHelperConfig } from '../features/spl';
6+
import type { PrepareTransactionMessage } from '../transactions/prepareTransaction';
67
import { createClientHelpers } from './createClientHelpers';
78
import { createDefaultClientStore } from './createClientStore';
89

@@ -33,6 +34,8 @@ const createTransactionHelperMock = vi.hoisted(() =>
3334
})),
3435
);
3536

37+
const prepareTransactionMock = vi.hoisted(() => vi.fn());
38+
3639
vi.mock('../features/sol', () => ({
3740
createSolTransferHelper: createSolTransferHelperMock,
3841
}));
@@ -45,6 +48,10 @@ vi.mock('../features/transactions', () => ({
4548
createTransactionHelper: createTransactionHelperMock,
4649
}));
4750

51+
vi.mock('../transactions/prepareTransaction', () => ({
52+
prepareTransaction: prepareTransactionMock,
53+
}));
54+
4855
describe('client helpers', () => {
4956
const runtime = {
5057
rpc: {} as unknown,
@@ -61,6 +68,7 @@ describe('client helpers', () => {
6168
createSolTransferHelperMock.mockClear();
6269
createSplTokenHelperMock.mockClear();
6370
createTransactionHelperMock.mockClear();
71+
prepareTransactionMock.mockClear();
6472
});
6573

6674
it('lazily creates sol transfer helper and injects default commitment', async () => {
@@ -108,4 +116,13 @@ describe('client helpers', () => {
108116
const splDifferent = helpers.splToken({ ...splDifferentConfig });
109117
expect(splDifferent).not.toBe(splA);
110118
});
119+
120+
it('prepares transactions using the runtime RPC', async () => {
121+
const store = createDefaultClientStore(config);
122+
const rpc = { tag: 'rpc' };
123+
const helpers = createClientHelpers({ ...runtime, rpc } as never, store);
124+
const transaction = { tag: 'message' } as unknown as PrepareTransactionMessage;
125+
await helpers.prepareTransaction({ transaction });
126+
expect(prepareTransactionMock).toHaveBeenCalledWith({ transaction, rpc });
127+
});
111128
});

packages/client-core/src/client/createClientHelpers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { Commitment } from '@solana/kit';
33
import { createSolTransferHelper, type SolTransferHelper } from '../features/sol';
44
import { createSplTokenHelper, type SplTokenHelper, type SplTokenHelperConfig } from '../features/spl';
55
import { createTransactionHelper, type TransactionHelper } from '../features/transactions';
6+
import {
7+
type PrepareTransactionMessage,
8+
type PrepareTransactionOptions,
9+
prepareTransaction as prepareTransactionUtility,
10+
} from '../transactions/prepareTransaction';
611
import type { ClientHelpers, ClientStore, SolanaClientRuntime } from '../types';
712

813
type SplTokenCacheEntry = Readonly<{
@@ -108,6 +113,14 @@ export function createClientHelpers(runtime: SolanaClientRuntime, store: ClientS
108113
return scoped;
109114
}
110115

116+
const prepareTransactionWithRuntime = <TMessage extends PrepareTransactionMessage>(
117+
options: PrepareTransactionOptions<TMessage>,
118+
) =>
119+
prepareTransactionUtility({
120+
...options,
121+
rpc: runtime.rpc as Parameters<typeof prepareTransactionUtility>[0]['rpc'],
122+
});
123+
111124
return Object.freeze({
112125
get solTransfer() {
113126
return getSolTransfer();
@@ -116,5 +129,6 @@ export function createClientHelpers(runtime: SolanaClientRuntime, store: ClientS
116129
get transaction() {
117130
return getTransaction();
118131
},
132+
prepareTransaction: prepareTransactionWithRuntime,
119133
});
120134
}

packages/client-core/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,25 @@ export {
4141
toBigint,
4242
} from './numeric/math';
4343
export { type ApplyRatioOptions, applyRatio, createRatio, type Ratio, type RoundingMode } from './numeric/rational';
44+
export {
45+
type CreateSolanaRpcClientConfig,
46+
createSolanaRpcClient,
47+
type SendAndConfirmTransactionOptions,
48+
type SimulateTransactionOptions,
49+
type SolanaRpcClient,
50+
} from './rpc/createSolanaRpcClient';
4451
export { bigintFromJson, bigintToJson, lamportsFromJson, lamportsToJson } from './serialization/json';
52+
export {
53+
transactionToBase64,
54+
transactionToBase64WithSigners,
55+
} from './transactions/base64';
56+
export {
57+
type PrepareTransactionConfig,
58+
type PrepareTransactionMessage,
59+
type PrepareTransactionOptions,
60+
prepareTransaction,
61+
} from './transactions/prepareTransaction';
62+
export { insertReferenceKey, insertReferenceKeys } from './transactions/referenceKeys';
4563
export type {
4664
AccountCache,
4765
AccountCacheEntry,

0 commit comments

Comments
 (0)