@wireio/stake 0.0.6 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +260 -13
  2. package/lib/stake.browser.js +4861 -4218
  3. package/lib/stake.browser.js.map +1 -1
  4. package/lib/stake.d.ts +434 -6484
  5. package/lib/stake.js +5059 -4371
  6. package/lib/stake.js.map +1 -1
  7. package/lib/stake.m.js +4861 -4218
  8. package/lib/stake.m.js.map +1 -1
  9. package/package.json +2 -2
  10. package/src/assets/solana/idl/liqsol_core.json +4239 -0
  11. package/src/assets/solana/idl/liqsol_token.json +183 -0
  12. package/src/assets/solana/idl/validator_leaderboard.json +296 -250
  13. package/src/assets/solana/types/liqsol_core.ts +4245 -0
  14. package/src/assets/solana/types/liqsol_token.ts +189 -0
  15. package/src/assets/solana/types/validator_leaderboard.ts +296 -250
  16. package/src/index.ts +2 -5
  17. package/src/networks/ethereum/contract.ts +138 -36
  18. package/src/networks/ethereum/ethereum.ts +167 -38
  19. package/src/networks/ethereum/types.ts +32 -1
  20. package/src/networks/solana/clients/deposit.client.ts +92 -139
  21. package/src/networks/solana/clients/distribution.client.ts +302 -178
  22. package/src/networks/solana/clients/leaderboard.client.ts +40 -160
  23. package/src/networks/solana/constants.ts +238 -69
  24. package/src/networks/solana/program.ts +27 -93
  25. package/src/networks/solana/solana.ts +181 -36
  26. package/src/networks/solana/types.ts +47 -0
  27. package/src/networks/solana/utils.ts +522 -93
  28. package/src/scripts/fetch-artifacts.sh +24 -0
  29. package/src/scripts/tsconfig.json +17 -0
  30. package/src/staker/staker.ts +35 -30
  31. package/src/staker/types.ts +25 -22
  32. package/src/assets/solana/idl/deposit.json +0 -260
  33. package/src/assets/solana/idl/distribution.json +0 -736
  34. package/src/assets/solana/idl/liq_sol_token.json +0 -275
  35. package/src/assets/solana/idl/stake_controller.json +0 -1788
  36. package/src/assets/solana/idl/stake_registry.json +0 -435
  37. package/src/assets/solana/idl/treasury.json +0 -336
  38. package/src/assets/solana/idl/validator_registry.json +0 -418
  39. package/src/assets/solana/idl/yield_oracle.json +0 -32
  40. package/src/assets/solana/types/deposit.ts +0 -266
  41. package/src/assets/solana/types/distribution.ts +0 -742
  42. package/src/assets/solana/types/liq_sol_token.ts +0 -281
  43. package/src/assets/solana/types/stake_controller.ts +0 -1794
  44. package/src/assets/solana/types/stake_registry.ts +0 -441
  45. package/src/assets/solana/types/treasury.ts +0 -342
  46. package/src/assets/solana/types/validator_registry.ts +0 -424
  47. package/src/assets/solana/types/yield_oracle.ts +0 -38
  48. package/src/utils.ts +0 -9
@@ -1,122 +1,551 @@
1
- // src/solana/utils.ts
2
- import { AnchorProvider, Program } from '@coral-xyz/anchor'
3
- import { PublicKey } from '@solana/web3.js'
4
- import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
1
+ // src/networks/solana/utils.ts
2
+
3
+ import { Program, BN, AnchorProvider } from '@coral-xyz/anchor';
4
+
5
5
  import {
6
- DEPOSIT_PROGRAM_ID,
7
- DISTRIBUTION_PROGRAM_ID,
8
- STAKE_CONTROLLER_PROGRAM_ID,
9
- VALIDATOR_LEADERBOARD_PROGRAM_ID,
10
- LIQSOL_MINT_ADDRESS,
11
- DepositIDL,
12
- Deposit
13
- } from './constants'
14
-
15
- // -- Program factories ------------------------------------------------------
6
+ Connection,
7
+ Keypair,
8
+ PublicKey,
9
+ SystemProgram,
10
+ StakeProgram,
11
+ } from '@solana/web3.js';
12
+ import {
13
+ TOKEN_2022_PROGRAM_ID,
14
+ ASSOCIATED_TOKEN_PROGRAM_ID,
15
+ getAssociatedTokenAddress,
16
+ } from '@solana/spl-token';
17
+
18
+ import {
19
+ LIQSOL_CORE,
20
+ LIQSOL_TOKEN,
21
+ PAY_RATE_SCALE_FACTOR,
22
+ DEFAULT_AVERAGE_PAY_RATE,
23
+ EPHEMERAL_RENT_EXEMPTION,
24
+ LAMPORTS_PER_SOL,
25
+ lamportsToSol,
26
+ solToLamports,
27
+ deriveDepositAuthorityPda,
28
+ deriveLiqsolMintPda,
29
+ deriveLiqsolMintAuthorityPda,
30
+ deriveVaultPda,
31
+ deriveReservePoolPda,
32
+ deriveStakeControllerStatePda,
33
+ derivePayoutStatePda,
34
+ deriveBucketAuthorityPda,
35
+ deriveDistributionStatePda,
36
+ deriveUserRecordPda,
37
+ derivePayRateHistoryPda,
38
+ deriveStakeControllerVaultPda,
39
+ deriveEphemeralStakeAddress,
40
+ DEFAULT_PAY_RATE_LOOKBACK,
41
+ } from './constants';
42
+
43
+ import liqsolCoreIDL from '../../assets/solana/idl/liqsol_core.json';
44
+
45
+ // -----------------------------------------------------------------------------
46
+ // Read-only liqsol_core Program helper
47
+ // -----------------------------------------------------------------------------
48
+
49
+ let _liqsolCoreProgram: Program | null = null;
50
+
51
+ export function getLiqsolCoreProgram(
52
+ connection: Connection,
53
+ ): Program {
54
+ if (_liqsolCoreProgram && _liqsolCoreProgram.provider.connection === connection) {
55
+ return _liqsolCoreProgram;
56
+ }
57
+
58
+ // Dummy wallet – we're only doing read-only account fetches here.
59
+ const tmpKeypair = Keypair.generate();
60
+ const wallet : any = { publicKey: tmpKeypair.publicKey, signAllTransactions: async () => [], signTransaction: async () => tmpKeypair };
61
+
62
+ const provider = new AnchorProvider(connection, wallet, {
63
+ commitment: 'confirmed',
64
+ });
65
+
66
+ const program = new Program(
67
+ liqsolCoreIDL,
68
+ provider,
69
+ ) as Program;
70
+
71
+ _liqsolCoreProgram = program;
72
+ return program;
73
+ }
74
+
75
+ // -----------------------------------------------------------------------------
76
+ // Deposit accounts bundle
77
+ // -----------------------------------------------------------------------------
78
+
79
+ export interface DepositAccounts {
80
+ user: PublicKey;
81
+ depositAuthority: PublicKey;
82
+ liqsolMint: PublicKey;
83
+ liqsolMintAuthority: PublicKey;
84
+ userAta: PublicKey;
85
+ stakeControllerVault: PublicKey;
86
+ stakeControllerReservePool: PublicKey;
87
+ stakeControllerState: PublicKey;
88
+ payoutState: PublicKey;
89
+ bucketAuthority: PublicKey;
90
+ bucketTokenAccount: PublicKey;
91
+ distributionState: PublicKey;
92
+ userRecord: PublicKey;
93
+ payRateHistory: PublicKey;
94
+ ephemeralStake: PublicKey;
95
+ ephemeralSeed: string;
96
+ }
97
+
16
98
  /**
17
- * Create an Anchor Program client for the Deposit program
99
+ * Build a complete DepositAccounts set for a given user, matching the
100
+ * on-chain PDAs used by the liqSOL core program.
101
+ *
102
+ * The optional `seed` lets you make deposit flows replayable/deterministic.
103
+ * If omitted, a random u32 seed is generated.
18
104
  */
19
- export function getDepositProgram(provider: AnchorProvider): Program<Deposit> {
20
- // Ensure the IDL has the correct program address
21
- const idlWithAddress = {
22
- ...JSON.parse(JSON.stringify(DepositIDL)),
23
- address: DEPOSIT_PROGRAM_ID.toString(),
105
+ export async function buildDepositAccounts(
106
+ connection: Connection,
107
+ user: PublicKey,
108
+ ): Promise<DepositAccounts> {
109
+ const depositAuthority = deriveDepositAuthorityPda();
110
+ const liqsolMint = deriveLiqsolMintPda();
111
+ const liqsolMintAuthority = deriveLiqsolMintAuthorityPda();
112
+ const stakeControllerVault = deriveStakeControllerVaultPda();
113
+ const stakeControllerReservePool = deriveReservePoolPda();
114
+ const stakeControllerState = deriveStakeControllerStatePda();
115
+ const payoutState = derivePayoutStatePda();
116
+ const bucketAuthority = deriveBucketAuthorityPda();
117
+ const distributionState = deriveDistributionStatePda();
118
+ const userRecord = deriveUserRecordPda(user);
119
+ const payRateHistory = derivePayRateHistoryPda();
120
+
121
+ const userAta = await getAssociatedTokenAddress(
122
+ liqsolMint,
123
+ user,
124
+ false,
125
+ TOKEN_2022_PROGRAM_ID,
126
+ );
127
+
128
+ const bucketTokenAccount = await getAssociatedTokenAddress(
129
+ liqsolMint,
130
+ bucketAuthority,
131
+ true,
132
+ TOKEN_2022_PROGRAM_ID,
133
+ );
134
+
135
+ // -------------------------------------------------------------
136
+ // Ephemeral stake
137
+ // -------------------------------------------------------------
138
+ const seed = Math.floor(Math.random() * 2 ** 32);
139
+ const ephemeralSeed = `ephemeral_${seed}`;
140
+ const ephemeralStake = await deriveEphemeralStakeAddress(user, ephemeralSeed);
141
+
142
+ // `connection` is currently unused, but we keep it in the signature
143
+ // so this helper can evolve to preflight/validate accounts if needed.
144
+ void connection;
145
+
146
+ return {
147
+ user,
148
+ depositAuthority,
149
+ liqsolMint,
150
+ liqsolMintAuthority,
151
+ userAta,
152
+ stakeControllerVault,
153
+ stakeControllerReservePool,
154
+ stakeControllerState,
155
+ payoutState,
156
+ bucketAuthority,
157
+ bucketTokenAccount,
158
+ distributionState,
159
+ userRecord,
160
+ payRateHistory,
161
+ ephemeralStake,
162
+ ephemeralSeed,
163
+ };
164
+ }
165
+
166
+ // -----------------------------------------------------------------------------
167
+ // Balance + state getters (UI-friendly)
168
+ // -----------------------------------------------------------------------------
169
+
170
+ export async function getUserLiqSolBalance(
171
+ connection: Connection,
172
+ user: PublicKey,
173
+ ): Promise<number> {
174
+ const liqsolMint = deriveLiqsolMintPda();
175
+ const ata = await getAssociatedTokenAddress(
176
+ liqsolMint,
177
+ user,
178
+ false,
179
+ TOKEN_2022_PROGRAM_ID,
180
+ );
181
+
182
+ try {
183
+ const bal = await connection.getTokenAccountBalance(ata);
184
+ return Number(bal.value.amount); // raw lamports, not decimals-adjusted
185
+ } catch {
186
+ return 0;
24
187
  }
25
- return new Program(idlWithAddress as any, provider) as Program<Deposit>
26
188
  }
27
189
 
28
- // (Other program factories can go here if needed)
190
+ export async function getBucketLiqSolBalance(
191
+ connection: Connection,
192
+ ): Promise<number> {
193
+ const liqsolMint = deriveLiqsolMintPda();
194
+ const bucketAuthority = deriveBucketAuthorityPda();
195
+ const ata = await getAssociatedTokenAddress(
196
+ liqsolMint,
197
+ bucketAuthority,
198
+ true,
199
+ TOKEN_2022_PROGRAM_ID,
200
+ );
29
201
 
30
- // -- PDA derivation helpers ------------------------------------------------
31
- export function deriveDepositAuthorityPDA(
32
- programId: PublicKey = DEPOSIT_PROGRAM_ID
33
- ): [PublicKey, number] {
34
- return PublicKey.findProgramAddressSync(
35
- [Buffer.from('program_authority')],
36
- programId
37
- )
202
+ try {
203
+ const bal = await connection.getTokenAccountBalance(ata);
204
+ return Number(bal.value.amount);
205
+ } catch {
206
+ return 0;
207
+ }
38
208
  }
39
209
 
40
- export function deriveLiqsolMintAuthorityPDA(
41
- programId: PublicKey = LIQSOL_MINT_ADDRESS
42
- ): [PublicKey, number] {
43
- return PublicKey.findProgramAddressSync(
44
- [Buffer.from('mint_authority')],
45
- programId
46
- )
210
+ export async function getReservePoolBalance(
211
+ connection: Connection,
212
+ ): Promise<number> {
213
+ const reservePool = deriveReservePoolPda();
214
+ try {
215
+ const lamports = await connection.getBalance(reservePool);
216
+ return lamports;
217
+ } catch {
218
+ return 0;
219
+ }
47
220
  }
48
221
 
49
- export function deriveStakeControllerVaultPDA(): [PublicKey, number] {
50
- return PublicKey.findProgramAddressSync(
51
- [Buffer.from('vault')],
52
- STAKE_CONTROLLER_PROGRAM_ID
53
- )
222
+ /**
223
+ * Raw account info for the stake_controller state (Anchor decode is left
224
+ * to the caller so the SDK can stay IDL-agnostic at this layer).
225
+ */
226
+ export async function getStakeControllerStateRaw(
227
+ connection: Connection,
228
+ ): Promise<Uint8Array | null> {
229
+ const pda = deriveStakeControllerStatePda();
230
+ const info = await connection.getAccountInfo(pda);
231
+ return info?.data ?? null;
54
232
  }
55
233
 
56
- export function deriveStakeControllerReservePoolPDA(): [PublicKey, number] {
57
- return PublicKey.findProgramAddressSync(
58
- [Buffer.from('reserve_pool')],
59
- STAKE_CONTROLLER_PROGRAM_ID
60
- )
234
+ /**
235
+ * Raw account info for the payout-state account.
236
+ */
237
+ export async function getPayoutStateRaw(
238
+ connection: Connection,
239
+ ): Promise<Uint8Array | null> {
240
+ const pda = derivePayoutStatePda();
241
+ const info = await connection.getAccountInfo(pda);
242
+ return info?.data ?? null;
61
243
  }
62
244
 
63
- export function deriveStakeControllerStatePDA(): [PublicKey, number] {
64
- return PublicKey.findProgramAddressSync(
65
- [Buffer.from('stake_controller')],
66
- STAKE_CONTROLLER_PROGRAM_ID
67
- )
245
+ /**
246
+ * Raw account info for a user's distribution user_record.
247
+ */
248
+ export async function getUserRecordRaw(
249
+ connection: Connection,
250
+ user: PublicKey,
251
+ ): Promise<Uint8Array | null> {
252
+ const pda = deriveUserRecordPda(user);
253
+ const info = await connection.getAccountInfo(pda);
254
+ return info?.data ?? null;
68
255
  }
69
256
 
70
- export function deriveDistributionStatePDA(): [PublicKey, number] {
71
- return PublicKey.findProgramAddressSync(
72
- [Buffer.from('distribution_state')],
73
- DISTRIBUTION_PROGRAM_ID
74
- )
257
+ // -----------------------------------------------------------------------------
258
+ // Pay-rate & fee utilities (on-chain compatible)
259
+ // -----------------------------------------------------------------------------
260
+
261
+ export async function getAveragePayRate(
262
+ connection: Connection,
263
+ lookback: number = DEFAULT_PAY_RATE_LOOKBACK,
264
+ ): Promise<bigint> {
265
+ const program = getLiqsolCoreProgram(connection);
266
+ const payRateHistoryPda = derivePayRateHistoryPda();
267
+
268
+ try {
269
+ const anyProgram = program as any;
270
+ const payRateHistoryAccount = await anyProgram.account.payRateHistory.fetch(
271
+ payRateHistoryPda,
272
+ );
273
+
274
+ const entries: any[] = payRateHistoryAccount.entries ?? [];
275
+ const totalEntriesAdded = Number(
276
+ payRateHistoryAccount.totalEntriesAdded ?? 0,
277
+ );
278
+ const currentIndex: number = payRateHistoryAccount.currentIndex ?? 0;
279
+ const maxEntries: number =
280
+ payRateHistoryAccount.maxEntries ?? entries.length;
281
+
282
+ if (!entries.length) {
283
+ return DEFAULT_AVERAGE_PAY_RATE;
284
+ }
285
+
286
+ const entriesToFetch = Math.min(lookback, maxEntries, entries.length);
287
+
288
+ let idx: number;
289
+ if (totalEntriesAdded === 0) {
290
+ idx = 0;
291
+ } else if (currentIndex === 0) {
292
+ idx = maxEntries - 1;
293
+ } else {
294
+ idx = currentIndex - 1;
295
+ }
296
+
297
+ let sum = BigInt(0);
298
+ let validCount = BigInt(0);
299
+
300
+ for (let i = 0; i < entriesToFetch; i++) {
301
+ const entry = entries[idx];
302
+ if (entry && typeof entry.scaledRate !== 'undefined') {
303
+ const rate = BigInt(entry.scaledRate.toString());
304
+ if (rate > BigInt(0)) {
305
+ sum += rate;
306
+ validCount += BigInt(1);
307
+ }
308
+ }
309
+
310
+ if (totalEntriesAdded === 0) {
311
+ idx = (idx + 1) % maxEntries;
312
+ } else {
313
+ idx = idx === 0 ? maxEntries - 1 : idx - 1;
314
+ }
315
+ }
316
+
317
+ if (validCount === BigInt(0)) {
318
+ return DEFAULT_AVERAGE_PAY_RATE;
319
+ }
320
+
321
+ return sum / validCount;
322
+ } catch (err) {
323
+ // Fallback to default when history missing/unavailable
324
+ return DEFAULT_AVERAGE_PAY_RATE;
325
+ }
75
326
  }
327
+ /**
328
+ * On-chain fee formula:
329
+ * fee = (average_pay_rate * 4 * deposit_amount_lamports) / 10^12
330
+ */
331
+ export function calculateExpectedFee(
332
+ depositAmountLamports: BN,
333
+ averagePayRate: BN,
334
+ ): BN {
335
+ return averagePayRate
336
+ .mul(new BN(4))
337
+ .mul(depositAmountLamports)
338
+ .div(new BN(1_000_000_000_000)); // 10^12
339
+ }
340
+
341
+ /**
342
+ * Convenience helper to preview how a deposit splits between user + bucket
343
+ * and how much goes into the reserve, assuming the simple model:
344
+ *
345
+ * - userLiqSol = amount - fee + EPHEMERAL_RENT_EXEMPTION
346
+ * - bucketLiqSol = fee
347
+ * - reserveLamports = amount + EPHEMERAL_RENT_EXEMPTION
348
+ */
349
+ export function previewDepositEffects(params: {
350
+ depositAmountLamports: BN;
351
+ averagePayRate: BN;
352
+ rentExemptionLamports?: number;
353
+ }): {
354
+ feeLamports: BN;
355
+ userLiqSolLamports: BN;
356
+ bucketLiqSolLamports: BN;
357
+ reserveIncreaseLamports: BN;
358
+ } {
359
+ const { depositAmountLamports, averagePayRate } = params;
360
+ const rent = new BN(
361
+ params.rentExemptionLamports ?? EPHEMERAL_RENT_EXEMPTION,
362
+ );
363
+
364
+ const fee = calculateExpectedFee(depositAmountLamports, averagePayRate);
365
+ const userLiqSol = depositAmountLamports.sub(fee).add(rent);
366
+ const bucketLiqSol = fee;
367
+ const reserveIncrease = depositAmountLamports.add(rent);
76
368
 
77
- export function deriveUserRecordPDA(
78
- user: PublicKey
79
- ): [PublicKey, number] {
80
- return PublicKey.findProgramAddressSync(
81
- [Buffer.from('user_record'), user.toBuffer()],
82
- DISTRIBUTION_PROGRAM_ID
83
- )
369
+ return {
370
+ feeLamports: fee,
371
+ userLiqSolLamports: userLiqSol,
372
+ bucketLiqSolLamports: bucketLiqSol,
373
+ reserveIncreaseLamports: reserveIncrease,
374
+ };
84
375
  }
85
376
 
86
- export function deriveLeaderboardHeadPDA(): [PublicKey, number] {
87
- return PublicKey.findProgramAddressSync(
88
- [Buffer.from('leaderboard_head')],
89
- VALIDATOR_LEADERBOARD_PROGRAM_ID
90
- )
377
+ // -----------------------------------------------------------------------------
378
+ // Epoch / scheduling helpers
379
+ // -----------------------------------------------------------------------------
380
+
381
+ export type EpochSnapshot = {
382
+ epoch: number;
383
+ slot: number;
384
+ firstSlot: number;
385
+ slotsInEpoch: number;
386
+ slotMs: number;
387
+ };
388
+
389
+ export async function getEpochSnapshot(
390
+ connection: Connection,
391
+ ): Promise<EpochSnapshot> {
392
+ const info = await connection.getEpochInfo();
393
+
394
+ // Fallback slot time
395
+ let slotTimeMs = Number(process.env.SLOT_TIME_MS_FALLBACK ?? 400);
396
+
397
+ try {
398
+ const samples = await connection.getRecentPerformanceSamples(16);
399
+ if (samples.length) {
400
+ const avgMs =
401
+ samples.reduce(
402
+ (acc, s) => acc + (s.samplePeriodSecs * 1000) / s.numSlots,
403
+ 0,
404
+ ) / samples.length;
405
+ if (isFinite(avgMs) && avgMs > 0) {
406
+ slotTimeMs = avgMs;
407
+ }
408
+ }
409
+ } catch {
410
+ // ignore; keep fallback
411
+ }
412
+
413
+ return {
414
+ epoch: info.epoch,
415
+ slot: info.slotIndex,
416
+ firstSlot: info.absoluteSlot - info.slotIndex,
417
+ slotsInEpoch: info.slotsInEpoch,
418
+ slotMs: slotTimeMs,
419
+ };
91
420
  }
92
421
 
93
- export function deriveValidatorRecordPDA(
94
- voteAccount: PublicKey
95
- ): [PublicKey, number] {
96
- return PublicKey.findProgramAddressSync(
97
- [Buffer.from('validator_record'), voteAccount.toBuffer()],
98
- VALIDATOR_LEADERBOARD_PROGRAM_ID
99
- )
422
+ export function msToEpochEnd(snapshot: EpochSnapshot): number {
423
+ const remainingSlots = snapshot.slotsInEpoch - snapshot.slot;
424
+ return remainingSlots * snapshot.slotMs;
100
425
  }
101
426
 
102
- export function deriveTop10CachePDA(): [PublicKey, number] {
103
- return PublicKey.findProgramAddressSync(
104
- [Buffer.from('top_10_cache')],
105
- VALIDATOR_LEADERBOARD_PROGRAM_ID
106
- )
427
+ /**
428
+ * Generic "execute around epoch boundaries" helper:
429
+ *
430
+ * - If current progress is within [early, late], run immediately.
431
+ * - If too early, sleep until `early` percentage into the epoch.
432
+ * - If too late, sleep until `early` percentage into the *next* epoch.
433
+ *
434
+ * This is generic; you can wrap any instruction builder in here (including
435
+ * deposit flows) without baking in program-specific logic.
436
+ */
437
+ export async function scheduledInstruction<T>(
438
+ connection: Connection,
439
+ config: ScheduleConfig,
440
+ instruction: () => Promise<T>,
441
+ ): Promise<T> {
442
+ const early = config.early ?? 0.10;
443
+ const late = config.late ?? 0.90;
444
+
445
+ const snapshot = await getEpochSnapshot(connection);
446
+ const progress = snapshot.slot / snapshot.slotsInEpoch;
447
+
448
+ // Case 1: Already in safe zone
449
+ if (progress >= early && progress <= late) {
450
+ return instruction();
451
+ }
452
+
453
+ // Case 2: Early in epoch → wait until `early`
454
+ if (progress < early) {
455
+ const targetSlot = snapshot.slotsInEpoch * early;
456
+ const slotsRemaining = targetSlot - snapshot.slot;
457
+ const msToWait = slotsRemaining * snapshot.slotMs;
458
+
459
+ console.log(
460
+ `Epoch early (${(progress * 100).toFixed(
461
+ 2,
462
+ )}%). Sleeping ${(msToWait / 1000).toFixed(1)}s until ${early * 100
463
+ }%...`,
464
+ );
465
+
466
+ await sleep(Math.max(1000, msToWait));
467
+ return instruction();
468
+ }
469
+
470
+ // Case 3: Late in epoch → wait for next epoch + early window
471
+ const msToNextEpoch = msToEpochEnd(snapshot);
472
+ const earlyWaitTime = snapshot.slotsInEpoch * early * snapshot.slotMs;
473
+ const totalWaitTime = msToNextEpoch + earlyWaitTime + 1000;
474
+
475
+ console.log(
476
+ `Epoch late (${(progress * 100).toFixed(
477
+ 2,
478
+ )}%). Sleeping ${(totalWaitTime / 1000).toFixed(
479
+ 1,
480
+ )}s until next epoch + ${early * 100}%...`,
481
+ );
482
+
483
+ await sleep(totalWaitTime);
484
+ return instruction();
107
485
  }
108
486
 
109
- // -- SPL Token helper -------------------------------------------------------
110
- export function getUserLiqsolATA(
111
- user: PublicKey
112
- ): PublicKey {
113
- return getAssociatedTokenAddressSync(
114
- LIQSOL_MINT_ADDRESS,
115
- user,
116
- false,
117
- TOKEN_2022_PROGRAM_ID,
118
- ASSOCIATED_TOKEN_PROGRAM_ID
119
- )
487
+
488
+ // -----------------------------------------------------------------------------
489
+ // Generic helpers (used in tests + can be useful in apps)
490
+ // -----------------------------------------------------------------------------
491
+
492
+ export function getErrorMessage(error: any): string {
493
+ return (
494
+ error?.error?.errorCode?.code ||
495
+ error?.error?.errorMessage ||
496
+ error?.message ||
497
+ ''
498
+ );
499
+ }
500
+
501
+ export function generateRandomDepositAmount(
502
+ minSol = 2,
503
+ maxSol = 100,
504
+ ): BN {
505
+ const randomSol = Math.random() * (maxSol - minSol) + minSol;
506
+ return new BN(solToLamports(randomSol));
507
+ }
508
+
509
+ export function generateTestKeypair(): Keypair {
510
+ return Keypair.generate();
511
+ }
512
+
513
+ export async function airdropSol(
514
+ connection: Connection,
515
+ publicKey: PublicKey,
516
+ amountSol: number,
517
+ ): Promise<void> {
518
+ const lamports = solToLamports(amountSol);
519
+ const sig = await connection.requestAirdrop(publicKey, lamports);
520
+ await connection.confirmTransaction(sig, 'confirmed');
521
+ }
522
+
523
+ export async function waitForConfirmation(
524
+ connection: Connection,
525
+ signature: string,
526
+ ): Promise<void> {
527
+ await connection.confirmTransaction(signature, 'confirmed');
528
+ }
529
+
530
+ export function sleep(ms: number): Promise<void> {
531
+ return new Promise((resolve) => setTimeout(resolve, ms));
532
+ }
533
+
534
+ /**
535
+ * Simple helper used in tests: wait until safe zone, no-op.
536
+ */
537
+ export async function waitUntilSafeToExecuteFunction(
538
+ connection: Connection,
539
+ config: ScheduleConfig = {},
540
+ ): Promise<void> {
541
+ await scheduledInstruction(connection, config, async () => {
542
+ // no-op
543
+ return;
544
+ });
545
+ }
546
+
547
+ export interface ScheduleConfig {
548
+ early?: number; // fraction of epoch, default 0.10
549
+ late?: number; // fraction of epoch, default 0.90
120
550
  }
121
551
 
122
- // seedToPID(seed: string): PublicKey
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Define variables
5
+ IDL_ZIP_URL="https://bucket.gitgo.app/solana/idl.zip"
6
+ TYPES_ZIP_URL="https://bucket.gitgo.app/solana/types.zip"
7
+ DEST_DIR="$(dirname "$0")/../assets/solana"
8
+ TMP_DIR="$(mktemp -d)"
9
+
10
+ # Download the zip files
11
+ curl -L "$IDL_ZIP_URL" -o "$TMP_DIR/idl.zip"
12
+ curl -L "$TYPES_ZIP_URL" -o "$TMP_DIR/types.zip"
13
+
14
+ # Create destination directory if it doesn't exist
15
+ mkdir -p "$DEST_DIR"
16
+
17
+ # Unzip into the destination directory
18
+ unzip -o "$TMP_DIR/idl.zip" -d "$DEST_DIR"
19
+ unzip -o "$TMP_DIR/types.zip" -d "$DEST_DIR"
20
+
21
+ # Clean up
22
+ rm -rf "$TMP_DIR"
23
+
24
+ echo "IDL and types files fetched and extracted to $DEST_DIR"
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "target": "es2019",
6
+ "moduleResolution": "node",
7
+ "types": [
8
+ "node"
9
+ ],
10
+ "isolatedModules": false
11
+ },
12
+ "include": [
13
+ "./**/*",
14
+ "../networks/**/*",
15
+ "../assets/**/*"
16
+ ]
17
+ }