Skip to content

Commit 26abb3f

Browse files
authored
Migrate JS tests to a Kit client and LiteSVM (#158)
1 parent 7ed1aa8 commit 26abb3f

18 files changed

Lines changed: 653 additions & 1151 deletions

Makefile

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,25 +76,17 @@ build-doc-%:
7676
test-doc-%:
7777
cargo $(nightly) test --doc --all-features --manifest-path $(call make-path,$*)/Cargo.toml $(ARGS)
7878

79-
test-%:
80-
SBF_OUT_DIR=$(PWD)/target/deploy cargo $(nightly) test --manifest-path $(call make-path,$*)/Cargo.toml $(ARGS)
81-
8279
format-check-js-%:
8380
cd $(call make-path,$*) && pnpm install && pnpm format $(ARGS)
8481

8582
lint-js-%:
8683
cd $(call make-path,$*) && pnpm install && pnpm lint $(ARGS)
8784

8885
test-js-%:
89-
make restart-test-validator
9086
cd $(call make-path,$*) && pnpm install && pnpm build && pnpm test $(ARGS)
91-
make stop-test-validator
9287

93-
restart-test-validator:
94-
./scripts/restart-test-validator.sh
95-
96-
stop-test-validator:
97-
pkill -f solana-test-validator
88+
test-%:
89+
SBF_OUT_DIR=$(PWD)/target/deploy cargo $(nightly) test --manifest-path $(call make-path,$*)/Cargo.toml $(ARGS)
9890

9991
generate-fixtures:
10092
mkdir -p ./target/fixtures && RUST_LOG=error EJECT_FUZZ_FIXTURES=../target/fixtures cargo test-sbf --features mollusk-svm/fuzz --manifest-path program/Cargo.toml

clients/js/README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,21 @@ A generated JavaScript library for the Token program.
44

55
## Getting started
66

7-
To build and test your JavaScript client from the root of the repository, you may use the following command.
7+
The JS client tests use [LiteSVM](https://github.com/LiteSVM/litesvm) in-process, so no local validator is needed. To build and test your JavaScript client from the root of the repository, you may use the following command.
88

99
```sh
10-
pnpm clients:js:test
10+
make test-js-clients-js
1111
```
1212

13-
This will start a new local validator, if one is not already running, and run the tests for your JavaScript client.
13+
This installs dependencies, builds the client, and runs the test suite.
1414

1515
## Available client scripts.
1616

1717
Alternatively, you can go into the client directory and run the tests directly.
1818

1919
```sh
20-
# Build your programs and start the validator.
21-
pnpm programs:build
22-
pnpm validator:restart
20+
# Build the program `.so` that LiteSVM loads.
21+
make build-sbf-pinocchio-program
2322

2423
# Go into the client directory and run the tests.
2524
cd clients/js

clients/js/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"@solana/eslint-config-solana": "^3.0.3",
5454
"@solana/kit": "^6.5.0",
5555
"@solana/kit-client-rpc": "^0.9.0",
56+
"@solana/kit-plugin-litesvm": "^0.10.0",
57+
"@solana/kit-plugin-signer": "^0.10.0",
5658
"@types/node": "^24",
5759
"@typescript-eslint/eslint-plugin": "^7.16.1",
5860
"@typescript-eslint/parser": "^7.16.1",

clients/js/pnpm-lock.yaml

Lines changed: 259 additions & 132 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/js/test/_setup.ts

Lines changed: 44 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -1,260 +1,94 @@
1-
import { getCreateAccountInstruction } from '@solana-program/system';
2-
import {
3-
Address,
4-
TransactionMessage,
5-
Commitment,
6-
Rpc,
7-
RpcSubscriptions,
8-
SolanaRpcApi,
9-
SolanaRpcSubscriptionsApi,
10-
TransactionMessageWithBlockhashLifetime,
11-
TransactionMessageWithFeePayer,
12-
TransactionPlan,
13-
TransactionPlanResult,
14-
TransactionPlanner,
15-
TransactionSigner,
16-
airdropFactory,
17-
appendTransactionMessageInstructions,
18-
assertIsSendableTransaction,
19-
assertIsTransactionWithBlockhashLifetime,
20-
createSolanaRpc,
21-
createSolanaRpcSubscriptions,
22-
createTransactionMessage,
23-
createTransactionPlanExecutor,
24-
createTransactionPlanner,
25-
generateKeyPairSigner,
26-
getSignatureFromTransaction,
27-
lamports,
28-
pipe,
29-
sendAndConfirmTransactionFactory,
30-
setTransactionMessageFeePayerSigner,
31-
setTransactionMessageLifetimeUsingBlockhash,
32-
signTransactionMessageWithSigners,
33-
} from '@solana/kit';
1+
import path from 'node:path';
2+
3+
import { systemProgram } from '@solana-program/system';
4+
import { Address, TransactionSigner, createClient, generateKeyPairSigner, lamports } from '@solana/kit';
5+
import { litesvm } from '@solana/kit-plugin-litesvm';
6+
import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer';
7+
348
import {
359
TOKEN_PROGRAM_ADDRESS,
10+
associatedTokenProgram,
3611
findAssociatedTokenPda,
37-
getInitializeAccountInstruction,
38-
getInitializeMintInstruction,
39-
getMintSize,
40-
getMintToATAInstructionPlan,
41-
getMintToInstruction,
4212
getTokenSize,
13+
tokenProgram,
4314
} from '../src';
4415

45-
type Client = {
46-
rpc: Rpc<SolanaRpcApi>;
47-
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
48-
sendTransactionPlan: (transactionPlan: TransactionPlan) => Promise<TransactionPlanResult>;
49-
};
50-
51-
export const createDefaultSolanaClient = (): Client => {
52-
const rpc = createSolanaRpc('http://127.0.0.1:8899');
53-
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
54-
55-
const sendAndConfirm = sendAndConfirmTransactionFactory({
56-
rpc,
57-
rpcSubscriptions,
58-
});
59-
const transactionPlanExecutor = createTransactionPlanExecutor({
60-
executeTransactionMessage: async (context, transactionMessage) => {
61-
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
62-
context.transaction = signedTransaction;
63-
assertIsSendableTransaction(signedTransaction);
64-
assertIsTransactionWithBlockhashLifetime(signedTransaction);
65-
await sendAndConfirm(signedTransaction, { commitment: 'confirmed' });
66-
return signedTransaction;
67-
},
68-
});
69-
70-
const sendTransactionPlan = async (transactionPlan: TransactionPlan) => {
71-
return transactionPlanExecutor(transactionPlan);
72-
};
73-
74-
return { rpc, rpcSubscriptions, sendTransactionPlan };
16+
const TOKEN_BINARY_PATH = path.resolve(__dirname, '..', '..', '..', 'target', 'deploy', 'pinocchio_token_program.so');
17+
18+
export const createTestClient = () => {
19+
return createClient()
20+
.use(generatedSigner())
21+
.use(litesvm())
22+
.use(airdropSigner(lamports(1_000_000_000n)))
23+
.use(client => {
24+
// Load the token program into the LiteSVM instance from its compiled
25+
// `.so` file. This must run after the `litesvm()` plugin so that
26+
// `client.svm` is available. The system and associated-token
27+
// programs are LiteSVM builtins and need no loading.
28+
client.svm.addProgramFromFile(TOKEN_PROGRAM_ADDRESS, TOKEN_BINARY_PATH);
29+
return client;
30+
})
31+
.use(systemProgram())
32+
.use(tokenProgram())
33+
.use(associatedTokenProgram());
7534
};
7635

77-
export const generateKeyPairSignerWithSol = async (client: Client, putativeLamports: bigint = 1_000_000_000n) => {
78-
const signer = await generateKeyPairSigner();
79-
await airdropFactory(client)({
80-
recipientAddress: signer.address,
81-
lamports: lamports(putativeLamports),
82-
commitment: 'confirmed',
83-
});
84-
return signer;
85-
};
36+
export type TestClient = Awaited<ReturnType<typeof createTestClient>>;
8637

87-
export const createDefaultTransaction = async (client: Client, feePayer: TransactionSigner) => {
88-
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
89-
return pipe(
90-
createTransactionMessage({ version: 0 }),
91-
tx => setTransactionMessageFeePayerSigner(feePayer, tx),
92-
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
93-
);
94-
};
95-
96-
export const signAndSendTransaction = async (
97-
client: Client,
98-
transactionMessage: TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime,
99-
commitment: Commitment = 'confirmed',
100-
) => {
101-
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
102-
const signature = getSignatureFromTransaction(signedTransaction);
103-
assertIsSendableTransaction(signedTransaction);
104-
assertIsTransactionWithBlockhashLifetime(signedTransaction);
105-
await sendAndConfirmTransactionFactory(client)(signedTransaction, {
106-
commitment,
107-
});
108-
return signature;
109-
};
110-
111-
export const createDefaultTransactionPlanner = (client: Client, feePayer: TransactionSigner): TransactionPlanner => {
112-
return createTransactionPlanner({
113-
createTransactionMessage: async () => {
114-
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
115-
116-
return pipe(
117-
createTransactionMessage({ version: 0 }),
118-
tx => setTransactionMessageFeePayerSigner(feePayer, tx),
119-
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
120-
);
121-
},
122-
});
123-
};
124-
125-
export const getBalance = async (client: Client, address: Address) =>
126-
(await client.rpc.getBalance(address, { commitment: 'confirmed' }).send()).value;
127-
128-
export const createMint = async (
129-
client: Client,
130-
payer: TransactionSigner,
131-
mintAuthority: Address,
132-
decimals: number = 0,
133-
): Promise<Address> => {
134-
const space = BigInt(getMintSize());
135-
const [transactionMessage, rent, mint] = await Promise.all([
136-
createDefaultTransaction(client, payer),
137-
client.rpc.getMinimumBalanceForRentExemption(space).send(),
138-
generateKeyPairSigner(),
139-
]);
140-
const instructions = [
141-
getCreateAccountInstruction({
142-
payer,
143-
newAccount: mint,
144-
lamports: rent,
145-
space,
146-
programAddress: TOKEN_PROGRAM_ADDRESS,
147-
}),
148-
getInitializeMintInstruction({
149-
mint: mint.address,
150-
decimals,
151-
mintAuthority,
152-
}),
153-
];
154-
await pipe(
155-
transactionMessage,
156-
tx => appendTransactionMessageInstructions(instructions, tx),
157-
tx => signAndSendTransaction(client, tx),
158-
);
159-
160-
return mint.address;
161-
};
162-
163-
export const createToken = async (
164-
client: Client,
165-
payer: TransactionSigner,
166-
mint: Address,
167-
owner: Address,
168-
): Promise<Address> => {
38+
export const createToken = async (client: TestClient, mint: Address, owner: Address): Promise<Address> => {
16939
const space = BigInt(getTokenSize());
170-
const [transactionMessage, rent, token] = await Promise.all([
171-
createDefaultTransaction(client, payer),
40+
const [rent, token] = await Promise.all([
17241
client.rpc.getMinimumBalanceForRentExemption(space).send(),
17342
generateKeyPairSigner(),
17443
]);
175-
const instructions = [
176-
getCreateAccountInstruction({
177-
payer,
44+
await client.sendTransaction([
45+
client.system.instructions.createAccount({
17846
newAccount: token,
17947
lamports: rent,
18048
space,
18149
programAddress: TOKEN_PROGRAM_ADDRESS,
18250
}),
183-
getInitializeAccountInstruction({ account: token.address, mint, owner }),
184-
];
185-
await pipe(
186-
transactionMessage,
187-
tx => appendTransactionMessageInstructions(instructions, tx),
188-
tx => signAndSendTransaction(client, tx),
189-
);
51+
client.token.instructions.initializeAccount({ account: token.address, mint, owner }),
52+
]);
19053

19154
return token.address;
19255
};
19356

19457
export const createTokenWithAmount = async (
195-
client: Client,
196-
payer: TransactionSigner,
58+
client: TestClient,
19759
mintAuthority: TransactionSigner,
19860
mint: Address,
19961
owner: Address,
20062
amount: bigint,
20163
): Promise<Address> => {
20264
const space = BigInt(getTokenSize());
203-
const [transactionMessage, rent, token] = await Promise.all([
204-
createDefaultTransaction(client, payer),
65+
const [rent, token] = await Promise.all([
20566
client.rpc.getMinimumBalanceForRentExemption(space).send(),
20667
generateKeyPairSigner(),
20768
]);
208-
const instructions = [
209-
getCreateAccountInstruction({
210-
payer,
69+
await client.sendTransaction([
70+
client.system.instructions.createAccount({
21171
newAccount: token,
21272
lamports: rent,
21373
space,
21474
programAddress: TOKEN_PROGRAM_ADDRESS,
21575
}),
216-
getInitializeAccountInstruction({ account: token.address, mint, owner }),
217-
getMintToInstruction({ mint, token: token.address, mintAuthority, amount }),
218-
];
219-
await pipe(
220-
transactionMessage,
221-
tx => appendTransactionMessageInstructions(instructions, tx),
222-
tx => signAndSendTransaction(client, tx),
223-
);
76+
client.token.instructions.initializeAccount({ account: token.address, mint, owner }),
77+
client.token.instructions.mintTo({ mint, token: token.address, mintAuthority, amount }),
78+
]);
22479

22580
return token.address;
22681
};
22782

22883
export const createTokenPdaWithAmount = async (
229-
client: Client,
230-
payer: TransactionSigner,
84+
client: TestClient,
23185
mintAuthority: TransactionSigner,
23286
mint: Address,
23387
owner: Address,
23488
amount: bigint,
23589
decimals: number,
23690
): Promise<Address> => {
237-
const [token] = await findAssociatedTokenPda({
238-
owner,
239-
mint,
240-
tokenProgram: TOKEN_PROGRAM_ADDRESS,
241-
});
242-
243-
const transactionPlan = await createDefaultTransactionPlanner(
244-
client,
245-
payer,
246-
)(
247-
getMintToATAInstructionPlan({
248-
payer,
249-
ata: token,
250-
owner,
251-
mint,
252-
mintAuthority,
253-
amount,
254-
decimals,
255-
}),
256-
);
257-
258-
await client.sendTransactionPlan(transactionPlan);
91+
await client.token.instructions.mintToATA({ owner, mint, mintAuthority, amount, decimals }).sendTransaction();
92+
const [token] = await findAssociatedTokenPda({ owner, mint, tokenProgram: TOKEN_PROGRAM_ADDRESS });
25993
return token;
26094
};

0 commit comments

Comments
 (0)