Skip to content

Commit 2012d5e

Browse files
committed
Improve typing of client
1 parent 0617877 commit 2012d5e

File tree

1 file changed

+57
-18
lines changed

1 file changed

+57
-18
lines changed

examples/helper-lib/src/example.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,32 @@
1010
import { createLogger } from '@solana/example-utils/createLogger.js';
1111
import pressAnyKeyPrompt from '@solana/example-utils/pressAnyKeyPrompt.js';
1212
import {
13+
address,
1314
Address,
1415
airdropFactory,
1516
appendTransactionMessageInstruction,
1617
appendTransactionMessageInstructions,
1718
assertIsTransactionWithBlockhashLifetime,
1819
BlockhashLifetimeConstraint,
20+
ClusterUrl,
1921
createSolanaRpc,
2022
createSolanaRpcSubscriptions,
2123
createTransactionMessage,
2224
createTransactionPlanExecutor,
2325
createTransactionPlanner,
26+
devnet,
2427
generateKeyPairSigner,
2528
getSignatureFromTransaction,
2629
Instruction,
2730
InstructionPlan,
2831
lamports,
2932
Lamports,
33+
mainnet,
34+
MainnetUrl,
3035
parallelInstructionPlan,
3136
pipe,
37+
Rpc,
38+
RpcSubscriptions,
3239
sendAndConfirmTransactionFactory,
3340
sequentialInstructionPlan,
3441
setTransactionMessageFeePayer,
@@ -38,6 +45,7 @@ import {
3845
singleTransactionPlan,
3946
SolanaError,
4047
SolanaRpcApi,
48+
SolanaRpcApiFromClusterUrl,
4149
SolanaRpcSubscriptionsApi,
4250
TransactionMessage,
4351
TransactionMessageWithBlockhashLifetime,
@@ -118,20 +126,28 @@ function summarizeTransactionPlanResult(result: TransactionPlanResult): CompactS
118126
return transactionResults;
119127
}
120128

121-
type Client<TRpcApi = SolanaRpcApi, TRpcSubscriptionsApi = SolanaRpcSubscriptionsApi> = {
122-
rpc: TRpcApi;
123-
rpcSubscriptions: TRpcSubscriptionsApi;
124-
airdrop(recipientAddress: Address, amount: number | bigint | Lamports): Promise<Signature>;
129+
type HideFromMainnet<TClusterUrl extends ClusterUrl, T> = TClusterUrl extends MainnetUrl ? never : T;
130+
131+
type SolanaClient<
132+
TClusterUrl extends ClusterUrl,
133+
> = {
134+
rpc: Rpc<SolanaRpcApiFromClusterUrl<TClusterUrl>>;
135+
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
136+
airdrop: HideFromMainnet<TClusterUrl, (recipientAddress: Address, amount: number | bigint | Lamports) => Promise<Signature>>;
125137
transaction(config: TransactionConfig): TransactionBuilder;
126138
transactionPlan(config: TransactionConfig): TransactionPlanBuilder;
127139
sendAndConfirm(transaction: SendableTransactionMessage | TransactionPlan, abortSignal?: AbortSignal): Promise<CompactSingleTransactionSummary[]>;
128140
}
129141

130-
function createSolanaClient(endpoint: string): Client<ReturnType<typeof createSolanaRpc>, ReturnType<typeof createSolanaRpcSubscriptions>> {
142+
function createSolanaClient<TClusterUrl extends ClusterUrl>(endpoint: TClusterUrl): SolanaClient<TClusterUrl> {
131143
const rpc = createSolanaRpc(endpoint);
144+
// Typescript doesn't know the cluster URL, so internally we cast to the base SolanaRpcApi
145+
// We return `rpc` which is cluster-aware though
146+
const internalRpc = rpc as Rpc<SolanaRpcApi>;
132147
const rpcSubscriptions = createSolanaRpcSubscriptions(endpoint.replace('http', 'ws').replace('8899', '8900'));
133148

134-
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
149+
// We hide this from mainnet in the `SolanaClient` type
150+
const airdrop = airdropFactory({ rpc: internalRpc, rpcSubscriptions });
135151

136152
async function createBaseTransactionMessage(config: TransactionConfig, abortSignal?: AbortSignal): Promise<SendableTransactionMessage> {
137153
return pipe(
@@ -148,7 +164,7 @@ function createSolanaClient(endpoint: string): Client<ReturnType<typeof createSo
148164
if (config.blockhash) {
149165
return setTransactionMessageLifetimeUsingBlockhash(config.blockhash, tx);
150166
} else {
151-
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send({ abortSignal });
167+
const { value: latestBlockhash } = await internalRpc.getLatestBlockhash({ commitment: 'confirmed' }).send({ abortSignal });
152168
return setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx);
153169
}
154170
}
@@ -189,8 +205,8 @@ function createSolanaClient(endpoint: string): Client<ReturnType<typeof createSo
189205
}
190206
}
191207

192-
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
193-
const estimateCULimit = estimateComputeUnitLimitFactory({ rpc });
208+
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc: internalRpc, rpcSubscriptions });
209+
const estimateCULimit = estimateComputeUnitLimitFactory({ rpc: internalRpc });
194210
async function estimateWithMultiplier(...args: Parameters<typeof estimateCULimit>): Promise<number> {
195211
const estimate = await estimateCULimit(...args);
196212
return Math.ceil(estimate * 1.1);
@@ -208,17 +224,19 @@ function createSolanaClient(endpoint: string): Client<ReturnType<typeof createSo
208224
}
209225
})
210226

227+
async function airdropHelper(recipientAddress: Address, amount: number | bigint | Lamports): Promise<Signature> {
228+
const lamportsAmount = typeof amount === 'number' ? lamports(BigInt(amount)) : typeof amount === 'bigint' ? lamports(amount) : amount;
229+
return await airdrop({
230+
commitment: 'confirmed',
231+
recipientAddress,
232+
lamports: lamportsAmount,
233+
});
234+
}
235+
211236
return {
212237
rpc,
213238
rpcSubscriptions,
214-
async airdrop(recipientAddress: Address, amount: number | bigint | Lamports): Promise<Signature> {
215-
const lamportsAmount = typeof amount === 'number' ? lamports(BigInt(amount)) : typeof amount === 'bigint' ? lamports(amount) : amount;
216-
return await airdrop({
217-
commitment: 'confirmed',
218-
recipientAddress,
219-
lamports: lamportsAmount,
220-
});
221-
},
239+
airdrop: airdropHelper as HideFromMainnet<TClusterUrl, typeof airdropHelper>,
222240
transaction(config: TransactionConfig): TransactionBuilder {
223241
return transactionBuilder(config);
224242
},
@@ -253,7 +271,6 @@ function displayAmount(amount: bigint, decimals: number): string {
253271
const client = createSolanaClient('http://127.0.0.1:8899');
254272

255273
// Example: Airdrop SOL to a new signer
256-
257274
const signer = await generateKeyPairSigner();
258275
await client.airdrop(signer.address, sol(1.5));
259276
log.info('Airdropped 1.5 SOL to the new signer address');
@@ -315,6 +332,7 @@ async function createMintExample() {
315332
}
316333
await createMintExample();
317334

335+
// Example: Airdrop tokens to multiple recipients
318336
async function tokenAirdropExample() {
319337
const tokenMint = await generateKeyPairSigner();
320338

@@ -383,3 +401,24 @@ async function tokenAirdropExample() {
383401
await pressAnyKeyPrompt('Press any key to quit');
384402
}
385403
await tokenAirdropExample();
404+
405+
// Additional typetests
406+
async () => {
407+
const client = createSolanaClient('');
408+
await client.rpc.requestAirdrop(address('a'), lamports(1n)).send();
409+
await client.airdrop(address('a'), 1);
410+
const customSendConfirm = sendAndConfirmTransactionFactory({ rpc: client.rpc, rpcSubscriptions: client.rpcSubscriptions });
411+
412+
const devnetClient = createSolanaClient(devnet(''));
413+
await devnetClient.rpc.requestAirdrop(address('a'), lamports(1n)).send();
414+
await devnetClient.airdrop(address('a'), 1);
415+
const devnetCustomSendConfirm = sendAndConfirmTransactionFactory({ rpc: devnetClient.rpc, rpcSubscriptions: devnetClient.rpcSubscriptions });
416+
417+
const mainnetClient = createSolanaClient(mainnet(''));
418+
await mainnetClient.rpc.getBalance(address('a')).send();
419+
// @ts-expect-error requestAirdrop should be unavailable on mainnet rpc
420+
await mainnetClient.rpc.requestAirdrop(address('a'), lamports(1n)).send();
421+
// @ts-expect-error airdrop should be unavailable on mainnet client
422+
await mainnetClient.airdrop(address('a'), 1); // should error
423+
const mainnetCustomSendConfirm = sendAndConfirmTransactionFactory({ rpc: mainnetClient.rpc, rpcSubscriptions: mainnetClient.rpcSubscriptions });
424+
}

0 commit comments

Comments
 (0)