Skip to content

Commit f2bc962

Browse files
committed
js: replace bankrun with litesvm
1 parent 6159dba commit f2bc962

3 files changed

Lines changed: 204 additions & 256 deletions

File tree

clients/js-legacy/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,22 @@
3232
"./dist/types"
3333
],
3434
"devDependencies": {
35-
"@types/node": "^25.6.0",
3635
"@ava/typescript": "^6.0.0",
36+
"@types/node": "^25.6.0",
3737
"@typescript-eslint/eslint-plugin": "^8.58.2",
3838
"ava": "^7.0.0",
3939
"eslint": "^8.57.0",
40-
"solana-bankrun": "^0.4.0",
40+
"litesvm": "0.3.3",
4141
"tsup": "^8.5.1",
4242
"typescript": "^5.9.3"
4343
},
4444
"dependencies": {
45-
"@solana/web3.js": "^1.98.4",
4645
"@solana/addresses": "6.8.0",
4746
"@solana/prettier-config-solana": "^0.0.6",
4847
"@solana/rpc-api": "6.8.0",
4948
"@solana/rpc-spec": "6.8.0",
5049
"@solana/spl-single-pool": "workspace:*",
50+
"@solana/web3.js": "^1.98.4",
5151
"eslint-config-prettier": "^10.1.8",
5252
"eslint-plugin-prettier": "^5.5.5",
5353
"prettier": "^3.8.2"

clients/js-legacy/tests/transactions.test.ts

Lines changed: 119 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
/* eslint-disable */
2-
31
import test from 'ava';
4-
import { start, BanksClient, ProgramTestContext } from 'solana-bankrun';
2+
import path from 'node:path';
3+
import { LiteSVM, StakeHistoryEntry, FailedTransactionMetadata } from 'litesvm';
54
import {
65
Keypair,
76
PublicKey,
7+
SystemProgram,
88
Transaction,
99
Authorized,
1010
TransactionInstruction,
@@ -38,17 +38,46 @@ const voteAccount = {
3838
},
3939
};
4040

41-
const SLOTS_PER_EPOCH: bigint = 432000n;
4241
const LAMPORTS_PER_SOL: number = 1_000_000_000;
42+
const STAKE_HISTORY_ENTRY = new StakeHistoryEntry(
43+
BigInt(LAMPORTS_PER_SOL) * 1000n,
44+
BigInt(LAMPORTS_PER_SOL) * 10n,
45+
BigInt(LAMPORTS_PER_SOL) * 10n,
46+
);
47+
48+
type LiteContext = {
49+
svm: LiteSVM;
50+
payer: Keypair;
51+
banksClient: LiteBanksClient;
52+
advanceEpoch(): void;
53+
};
54+
55+
class LiteBanksClient {
56+
constructor(readonly svm: LiteSVM) {}
57+
58+
async getAccount(address: PublicKey) {
59+
const account = this.svm.getAccount(address);
60+
if (!account) return null;
61+
return { ...account, data: Buffer.from(account.data) };
62+
}
63+
64+
async processTransaction(tx: Transaction) {
65+
const res = this.svm.sendTransaction(tx);
66+
if (res instanceof FailedTransactionMetadata) {
67+
throw new Error(`${res.err().toString()}\n${res.meta().prettyLogs()}`);
68+
}
69+
return res;
70+
}
71+
}
4372

4473
class BanksConnection {
45-
constructor(client: BanksClient, payer: Keypair) {
74+
constructor(client: LiteBanksClient, payer: Keypair) {
4675
this.client = client;
4776
this.payer = payer;
4877
}
4978

5079
async getMinimumBalanceForRentExemption(dataLen: number): Promise<number> {
51-
const rent = await this.client.getRent();
80+
const rent = await this.client.svm.getRent();
5281
return Number(rent.minimumBalance(BigInt(dataLen)));
5382
}
5483

@@ -61,27 +90,27 @@ class BanksConnection {
6190
data: Buffer.from([13, 0, 0, 0]),
6291
}),
6392
);
64-
transaction.recentBlockhash = (await this.client.getLatestBlockhash())[0];
93+
transaction.recentBlockhash = this.client.svm.latestBlockhash();
6594
transaction.feePayer = this.payer.publicKey;
6695
transaction.sign(this.payer);
6796

68-
const res = await this.client.simulateTransaction(transaction);
69-
const data = Array.from(res.inner.meta.returnData.data);
97+
const res = await this.client.svm.simulateTransaction(transaction);
98+
const data = Array.from(res.meta().returnData().data());
7099
const minimumDelegation = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);
71100

72101
return { value: minimumDelegation };
73102
}
74103

75-
async getAccountInfo(address: PublicKey, commitment?: string): Promise<AccountInfo<Buffer>> {
76-
const account = await this.client.getAccount(address, commitment);
104+
async getAccountInfo(address: PublicKey): Promise<AccountInfo<Buffer>> {
105+
const account = await this.client.getAccount(address);
77106
if (account) {
78107
account.data = Buffer.from(account.data);
79108
}
80109
return account;
81110
}
82111
}
83112

84-
async function startWithContext(authorizedWithdrawer?: PublicKey) {
113+
async function startWithContext(authorizedWithdrawer?: PublicKey): Promise<LiteContext> {
85114
const voteAccountData = Uint8Array.from(atob(voteAccount.account.data[0]), (c) =>
86115
c.charCodeAt(0),
87116
);
@@ -90,48 +119,74 @@ async function startWithContext(authorizedWithdrawer?: PublicKey) {
90119
voteAccountData.set(authorizedWithdrawer.toBytes(), 36);
91120
}
92121

93-
return await start(
94-
[
95-
{
96-
name: Buffer.from('spl_single_pool').toString('utf-8'),
97-
programId: SinglePoolProgram.programId,
98-
},
99-
{
100-
name: Buffer.from('mpl_token_metadata').toString('utf-8'),
101-
programId: MPL_METADATA_PROGRAM_ID,
102-
},
103-
],
104-
[
105-
{
106-
address: new PublicKey(voteAccount.pubkey),
107-
info: {
108-
lamports: voteAccount.account.lamports,
109-
data: voteAccountData,
110-
owner: VoteProgram.programId,
111-
executable: false,
112-
},
113-
},
114-
],
115-
undefined,
116-
undefined,
117-
// stake_raise_minimum_delegation_to_1_sol::id()
118-
[new PublicKey('9onWzzvCzNC2jfhxxeqRgs5q7nFAAKpCUvkj6T6GJK9i')],
122+
const svm = new LiteSVM();
123+
const payer = Keypair.generate();
124+
125+
svm.airdrop(payer.publicKey, 10_000n * BigInt(LAMPORTS_PER_SOL));
126+
127+
svm.addProgramFromFile(
128+
SinglePoolProgram.programId,
129+
path.resolve(process.cwd(), 'tests', 'fixtures', 'spl_single_pool.so'),
119130
);
131+
svm.addProgramFromFile(
132+
MPL_METADATA_PROGRAM_ID,
133+
path.resolve(process.cwd(), 'tests', 'fixtures', 'mpl_token_metadata.so'),
134+
);
135+
136+
svm.setAccount(new PublicKey(voteAccount.pubkey), {
137+
lamports: voteAccount.account.lamports,
138+
data: voteAccountData,
139+
owner: VoteProgram.programId,
140+
executable: false,
141+
rentEpoch: 0,
142+
});
143+
144+
const schedule = svm.getEpochSchedule();
145+
const clock = svm.getClock();
146+
const history = svm.getStakeHistory();
147+
148+
clock.slot = schedule.firstNormalSlot + 1n;
149+
clock.epoch = schedule.firstNormalEpoch;
150+
clock.leaderScheduleEpoch = schedule.firstNormalEpoch;
151+
152+
for (let epoch = 0n; epoch < schedule.firstNormalEpoch; epoch += 1n) {
153+
history.add(epoch, STAKE_HISTORY_ENTRY);
154+
}
155+
156+
svm.setClock(clock);
157+
svm.setStakeHistory(history);
158+
159+
const banksClient = new LiteBanksClient(svm);
160+
161+
return {
162+
svm,
163+
payer,
164+
banksClient,
165+
advanceEpoch() {
166+
const schedule = svm.getEpochSchedule();
167+
const clock = svm.getClock();
168+
const history = svm.getStakeHistory();
169+
170+
history.add(clock.epoch, STAKE_HISTORY_ENTRY);
171+
clock.slot += schedule.slotsPerEpoch;
172+
clock.epoch += 1n;
173+
clock.leaderScheduleEpoch = clock.epoch;
174+
175+
svm.setClock(clock);
176+
svm.setStakeHistory(history);
177+
},
178+
};
120179
}
121180

122-
async function processTransaction(
123-
context: ProgramTestContext,
124-
transaction: Transaction,
125-
signers = [],
126-
) {
127-
transaction.recentBlockhash = context.lastBlockhash;
181+
async function processTransaction(context: LiteContext, transaction: Transaction, signers = []) {
182+
transaction.recentBlockhash = context.svm.latestBlockhash();
128183
transaction.feePayer = context.payer.publicKey;
129184
transaction.sign(...[context.payer].concat(signers));
130185
return context.banksClient.processTransaction(transaction);
131186
}
132187

133188
async function createAndDelegateStakeAccount(
134-
context: ProgramTestContext,
189+
context: LiteContext,
135190
voteAccountAddress: PublicKey,
136191
): Promise<PublicKey> {
137192
const connection = new BanksConnection(context.banksClient, context.payer);
@@ -193,6 +248,9 @@ test('replenish pool', async (t) => {
193248
const connection = new BanksConnection(client, payer);
194249

195250
const voteAccountAddress = new PublicKey(voteAccount.pubkey);
251+
const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress);
252+
const poolStakeAddress = await findPoolStakeAddress(SinglePoolProgram.programId, poolAddress);
253+
const poolOnrampAddress = await findPoolOnRampAddress(SinglePoolProgram.programId, poolAddress);
196254

197255
// initialize pool
198256
let transaction = await SinglePoolProgram.initialize(
@@ -201,17 +259,24 @@ test('replenish pool', async (t) => {
201259
payer.publicKey,
202260
);
203261
await processTransaction(context, transaction);
204-
205-
const slot = await client.getSlot();
206-
context.warpToSlot(slot + SLOTS_PER_EPOCH);
262+
context.advanceEpoch();
263+
264+
transaction = new Transaction().add(
265+
SystemProgram.transfer({
266+
fromPubkey: payer.publicKey,
267+
toPubkey: poolStakeAddress,
268+
lamports: LAMPORTS_PER_SOL,
269+
}),
270+
);
271+
await processTransaction(context, transaction);
207272

208273
// replenish pool
209274
transaction = await SinglePoolProgram.replenishPool(voteAccountAddress);
275+
await processTransaction(context, transaction);
210276

211-
// NOTE we cannot test executing this because bankrun latest is on 1.18
212-
// maybe someday
213-
//await processTransaction(context, transaction);
214-
t.true(true);
277+
const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
278+
const poolOnrampAccount = await client.getAccount(poolOnrampAddress);
279+
t.is(poolOnrampAccount.lamports, LAMPORTS_PER_SOL + stakeRent, 'lamports have been replenished');
215280
});
216281

217282
test('deposit', async (t) => {
@@ -232,12 +297,9 @@ test('deposit', async (t) => {
232297
payer.publicKey,
233298
);
234299
await processTransaction(context, transaction);
235-
236-
const slot = await client.getSlot();
237-
context.warpToSlot(slot + SLOTS_PER_EPOCH);
300+
context.advanceEpoch();
238301

239302
// deposit
240-
/* bankrun is still on 1.18 so this fails. update later
241303
transaction = await SinglePoolProgram.deposit({
242304
connection,
243305
pool: poolAddress,
@@ -254,9 +316,6 @@ test('deposit', async (t) => {
254316
LAMPORTS_PER_SOL + minimumDelegation + stakeRent,
255317
'stake has been deposited',
256318
);
257-
*/
258-
259-
t.true(true);
260319
});
261320

262321
test('withdraw', async (t) => {
@@ -277,9 +336,7 @@ test('withdraw', async (t) => {
277336
payer.publicKey,
278337
);
279338
await processTransaction(context, transaction);
280-
281-
const slot = await client.getSlot();
282-
context.warpToSlot(slot + SLOTS_PER_EPOCH);
339+
context.advanceEpoch();
283340

284341
// deposit
285342
transaction = await SinglePoolProgram.deposit({
@@ -288,7 +345,6 @@ test('withdraw', async (t) => {
288345
userWallet: payer.publicKey,
289346
userStakeAccount: depositAccount,
290347
});
291-
/* bankrun is still on 1.18 so this fails. update later
292348
await processTransaction(context, transaction);
293349

294350
const minimumDelegation = (await connection.getStakeMinimumDelegation()).value;
@@ -310,9 +366,6 @@ test('withdraw', async (t) => {
310366
const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
311367
const userStakeAccount = await client.getAccount(withdrawAccount.publicKey);
312368
t.is(userStakeAccount.lamports, minimumDelegation + stakeRent, 'stake has been withdrawn');
313-
*/
314-
315-
t.true(true);
316369
});
317370

318371
test('create metadata', async (t) => {

0 commit comments

Comments
 (0)