@wireio/stake 2.2.2 → 2.3.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wireio/stake",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "LIQ Staking Module for Wire Network",
5
5
  "homepage": "https://gitea.gitgo.app/Wire/sdk-stake",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -64,7 +64,7 @@
64
64
  "@types/node": "^18.19.0",
65
65
  "@typescript-eslint/eslint-plugin": "^5.60.0",
66
66
  "@typescript-eslint/parser": "^5.60.0",
67
- "@wireio/core": "^0.3.0",
67
+ "@wireio/core": "^0.3.1",
68
68
  "assert": "^2.0.0",
69
69
  "chai": "^4.3.6",
70
70
  "esbuild": "^0.25.8",
package/src/index.ts CHANGED
@@ -13,8 +13,8 @@ export * as SOL from './networks/solana/types';
13
13
  export * from './networks/solana/utils';
14
14
 
15
15
  // CLIENTS
16
- export * from './networks/solana/clients/deposit.client';
16
+ export * from './networks/solana/clients/convert.client';
17
17
  export * from './networks/solana/clients/distribution.client';
18
18
  export * from './networks/solana/clients/leaderboard.client';
19
19
  export * from './networks/solana/clients/outpost.client';
20
- export * from './networks/solana/clients/token.client';
20
+ export * from './networks/solana/clients/token.client';
@@ -1,7 +1,7 @@
1
1
  import { BigNumber } from "ethers";
2
- import { preLaunchReceipt, WithdrawReceipt } from "../types";
2
+ import { preLaunchReceipt } from "../types";
3
+ import { WithdrawReceipt, ReceiptNFTKind } from "../../../types";
3
4
  import { EthereumContractService } from "../contract";
4
- import { ReceiptNFTKind } from "../../../types";
5
5
 
6
6
  export class ReceiptClient {
7
7
 
@@ -13,8 +13,7 @@ export class ReceiptClient {
13
13
  this.contractService = contract;
14
14
  }
15
15
 
16
-
17
-
16
+ // NOTE: Stake/pretoken receipts intentionally ignored for withdraw view.
18
17
  async allReceipts(address: string): Promise<preLaunchReceipt[]> {
19
18
  return this.fetchPreLaunchReceipts(address);
20
19
  }
@@ -23,9 +22,7 @@ export class ReceiptClient {
23
22
  try {
24
23
  const receipts = await this.fetchPreLaunchReceipts(address, ReceiptNFTKind.STAKE);
25
24
  return receipts;
26
- }
27
- catch (err) {
28
- // console.log('Error fetching stake receipts:', err);
25
+ } catch (err) {
29
26
  return [];
30
27
  }
31
28
  }
@@ -34,28 +31,20 @@ export class ReceiptClient {
34
31
  return this.fetchPreLaunchReceipts(address, ReceiptNFTKind.PRETOKEN_PURCHASE);
35
32
  }
36
33
 
37
-
38
-
39
34
  /**
40
- *
41
- * @param address (string) to fetch receipts for
42
- * @returns preLaunchReceipt[]
35
+ * Fetch ReceiptNFT data (stake/pretoken) for an address, optionally filtered by kind.
43
36
  */
44
37
  async fetchPreLaunchReceipts(address: string, type?: ReceiptNFTKind): Promise<preLaunchReceipt[]> {
45
38
  const receiptContract = this.contract.ReceiptNFT;
46
39
 
47
- // first figure out which tokenIds this address owns, from events
48
40
  const tokenIds = await this.getOwnedReceiptNFTsFor(address);
49
-
50
41
  const results: preLaunchReceipt[] = [];
51
42
 
52
- // next fetch on-chain receipt data just for those ids
53
43
  for (const idBN of tokenIds) {
54
44
  try {
55
45
  const receiptData = await receiptContract.getReceipt(idBN);
56
46
 
57
- //skip any receipt not of the requested type
58
- if(type !== undefined && receiptData.kind !== type) continue;
47
+ if (type !== undefined && receiptData.kind !== type) continue;
59
48
 
60
49
  results.push({
61
50
  tokenId: idBN.toBigInt(),
@@ -78,7 +67,6 @@ export class ReceiptClient {
78
67
  }
79
68
  });
80
69
  } catch (err) {
81
- // in case of any mismatch or race, just skip this id
82
70
  console.warn(`Failed to load receipt for tokenId=${idBN.toString()}`, err);
83
71
  continue;
84
72
  }
@@ -88,50 +76,6 @@ export class ReceiptClient {
88
76
  }
89
77
 
90
78
 
91
-
92
-
93
- private async getOwnedReceiptNFTsFor(
94
- owner: string,
95
- fromBlock = 0,
96
- toBlock: number | string = "latest"
97
- ): Promise<BigNumber[]> {
98
- const receiptContract = this.contract.ReceiptNFT;
99
-
100
- // Logs where address received tokens
101
- const toLogs = await receiptContract.queryFilter(
102
- receiptContract.filters.Transfer(null, owner),
103
- fromBlock,
104
- toBlock
105
- );
106
-
107
- // Logs where address sent tokens (including burns from owner → 0)
108
- const fromLogs = await receiptContract.queryFilter(
109
- receiptContract.filters.Transfer(owner, null),
110
- fromBlock,
111
- toBlock
112
- );
113
-
114
- const owned = new Set<string>();
115
-
116
- // Add all received tokenIds
117
- for (const e of toLogs) {
118
- const tokenId = e.args?.tokenId;
119
- if (!tokenId) continue;
120
- owned.add(tokenId.toString());
121
- }
122
-
123
- // Remove all sent tokenIds
124
- for (const e of fromLogs) {
125
- const tokenId = e.args?.tokenId;
126
- if (!tokenId) continue;
127
- owned.delete(tokenId.toString());
128
- }
129
-
130
- // Convert to BigNumbers
131
- return Array.from(owned).map((id) => BigNumber.from(id));
132
- }
133
-
134
-
135
79
  /**
136
80
  *
137
81
  * @param address (string) to fetch receipts for
@@ -147,16 +91,21 @@ export class ReceiptClient {
147
91
  try {
148
92
  const receiptData = await this.contract.WithdrawalQueue.info(idBN);
149
93
 
150
- results.push({
151
- tokenId: idBN.toBigInt(),
94
+ // status: ready if readyAt <= now; otherwise queued
95
+ const readyAtMs = Number(receiptData.readyAt) * 1000;
96
+ const status = readyAtMs <= Date.now() ? 'ready' : 'queued';
97
+
98
+ results.push({
99
+ tokenId: idBN.toBigInt(),
152
100
  receipt: {
153
- ethAmount: receiptData.ethAmount,
154
- ethBalance: {
101
+ amount: {
155
102
  amount: receiptData.ethAmount.toBigInt(),
156
103
  decimals: 18,
157
- symbol: "ETH"
104
+ symbol: "ETH",
158
105
  },
159
- readyAt: new Date(Number(receiptData.readyAt.toString()) * 1000).valueOf(),
106
+ readyAt: readyAtMs,
107
+ chain: 'ETH',
108
+ status,
160
109
  }
161
110
  });
162
111
  } catch (err) {
@@ -211,4 +160,40 @@ export class ReceiptClient {
211
160
  // Convert to BigNumbers
212
161
  return Array.from(owned).map((id) => BigNumber.from(id));
213
162
  }
214
- }
163
+
164
+ private async getOwnedReceiptNFTsFor(
165
+ owner: string,
166
+ fromBlock = 0,
167
+ toBlock: number | string = "latest"
168
+ ): Promise<BigNumber[]> {
169
+ const receiptContract = this.contract.ReceiptNFT;
170
+
171
+ const toLogs = await receiptContract.queryFilter(
172
+ receiptContract.filters.Transfer(null, owner),
173
+ fromBlock,
174
+ toBlock
175
+ );
176
+
177
+ const fromLogs = await receiptContract.queryFilter(
178
+ receiptContract.filters.Transfer(owner, null),
179
+ fromBlock,
180
+ toBlock
181
+ );
182
+
183
+ const owned = new Set<string>();
184
+
185
+ for (const e of toLogs) {
186
+ const tokenId = e.args?.tokenId;
187
+ if (!tokenId) continue;
188
+ owned.add(tokenId.toString());
189
+ }
190
+
191
+ for (const e of fromLogs) {
192
+ const tokenId = e.args?.tokenId;
193
+ if (!tokenId) continue;
194
+ owned.delete(tokenId.toString());
195
+ }
196
+
197
+ return Array.from(owned).map((id) => BigNumber.from(id));
198
+ }
199
+ }
@@ -107,7 +107,7 @@ export class EthereumStakingClient implements IStakingClient {
107
107
  * @param amount Amount in wei (or something convertible to BigNumber).
108
108
  * @returns transaction hash
109
109
  */
110
- async loadPendingWithdraws(): Promise<WithdrawReceipt[]> {
110
+ async getPendingWithdraws(): Promise<WithdrawReceipt[]> {
111
111
  this.ensureUser();
112
112
  const address = await this.signer!.getAddress();
113
113
 
@@ -1,5 +1,5 @@
1
1
  import { BigNumber, ethers } from 'ethers';
2
- import { BalanceView } from '../../types';
2
+ import { BalanceView, ChainSymbol, WithdrawReceipt as UnifiedWithdrawReceipt } from '../../types';
3
3
 
4
4
  export const CONTRACT_NAMES = [
5
5
  // LiqETH contracts
@@ -99,17 +99,20 @@ export interface SharesBurnedEvent {
99
99
  tokenValue: BigNumber;
100
100
  }
101
101
 
102
+ /**
103
+ * Legacy stake/pretoken receipt (kept for compatibility; not used in withdraw UI).
104
+ */
102
105
  export interface preLaunchReceipt {
103
106
  tokenId: bigint;
104
107
  receipt: {
105
- account: string,
106
- currency: number,
107
- kind: number,
108
- indexAtMint: BalanceView,
109
- principal: BalanceView,
110
- shares: BalanceView,
111
- timestamp: string,
112
- }
108
+ account: string;
109
+ currency: number;
110
+ kind: number;
111
+ indexAtMint: BalanceView;
112
+ principal: BalanceView;
113
+ shares: BalanceView;
114
+ timestamp: string;
115
+ };
113
116
  }
114
117
 
115
118
  export interface ClaimedEvent {
@@ -117,14 +120,8 @@ export interface ClaimedEvent {
117
120
  amount: BigNumber;
118
121
  }
119
122
 
120
- export interface WithdrawReceipt {
121
- tokenId: bigint;
122
- receipt: {
123
- ethAmount: BigNumber;
124
- ethBalance: BalanceView;
125
- readyAt: number;
126
- }
127
- }
123
+ // Unified withdraw receipt type is defined centrally in src/types.ts
124
+ export type WithdrawReceipt = UnifiedWithdrawReceipt;
128
125
 
129
126
 
130
127
 
@@ -0,0 +1,339 @@
1
+ import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
2
+ import {
3
+ SystemProgram,
4
+ TransactionInstruction,
5
+ StakeProgram,
6
+ SYSVAR_INSTRUCTIONS_PUBKEY,
7
+ SYSVAR_CLOCK_PUBKEY,
8
+ SYSVAR_RENT_PUBKEY,
9
+ SYSVAR_STAKE_HISTORY_PUBKEY,
10
+ Connection,
11
+ PublicKey,
12
+ } from '@solana/web3.js';
13
+
14
+ import {
15
+ TOKEN_2022_PROGRAM_ID,
16
+ ASSOCIATED_TOKEN_PROGRAM_ID,
17
+ getAssociatedTokenAddressSync,
18
+ } from '@solana/spl-token';
19
+
20
+ import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';
21
+ import { GlobalAccount, ReceiptData, WalletLike } from '../types';
22
+ import { BalanceView, WithdrawReceipt } from '../../../types';
23
+ import { normalizeToBigInt } from '../utils';
24
+
25
+ /**
26
+ * ConvertClient (Solana):
27
+ * - deposit SOL -> liqSOL
28
+ * - request withdraw (liqSOL burn -> NFT receipt)
29
+ * - list withdrawal receipts owned by a user
30
+ * - build claim_withdraw instruction
31
+ */
32
+ export class ConvertClient {
33
+ private program: Program<LiqsolCoreClientIdl>;
34
+
35
+ get connection(): Connection {
36
+ return this.provider.connection;
37
+ }
38
+
39
+ get wallet(): WalletLike {
40
+ return this.provider.wallet;
41
+ }
42
+
43
+ constructor(
44
+ private readonly provider: AnchorProvider,
45
+ private readonly pgs: SolanaProgramService,
46
+ ) {
47
+ this.program = pgs.getProgram('liqsolCore');
48
+ }
49
+
50
+ /**
51
+ * Build a deposit instruction (SOL -> liqSOL).
52
+ */
53
+ async buildDepositTx(
54
+ amount: bigint,
55
+ user = this.wallet.publicKey,
56
+ ): Promise<TransactionInstruction> {
57
+ if (!user) throw new Error('ConvertClient.buildDepositTx: wallet not connected');
58
+ if (!amount || amount <= BigInt(0))
59
+ throw new Error('ConvertClient.buildDepositTx: amount must be greater than zero.');
60
+
61
+ const depositAuthority = this.pgs.deriveDepositAuthorityPda();
62
+ const liqsolMint = this.pgs.deriveLiqsolMintPda();
63
+ const liqsolMintAuthority = this.pgs.deriveLiqsolMintAuthorityPda();
64
+ const reservePool = this.pgs.deriveReservePoolPda();
65
+ const vault = this.pgs.deriveVaultPda();
66
+ const controllerState = this.pgs.deriveStakeControllerStatePda();
67
+ const payoutState = this.pgs.derivePayoutStatePda();
68
+ const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
69
+ const payRateHistory = this.pgs.derivePayRateHistoryPda();
70
+ const globalConfig = this.pgs.deriveGlobalConfigPda();
71
+
72
+ const userAta = getAssociatedTokenAddressSync(liqsolMint, user, true, TOKEN_2022_PROGRAM_ID);
73
+ const distributionState = this.pgs.deriveDistributionStatePda();
74
+ const userRecord = this.pgs.deriveUserRecordPda(userAta);
75
+ const bucketTokenAccount = getAssociatedTokenAddressSync(
76
+ liqsolMint,
77
+ bucketAuthority,
78
+ true,
79
+ TOKEN_2022_PROGRAM_ID,
80
+ );
81
+
82
+ const seed = Math.floor(Math.random() * 2 ** 32);
83
+ const ephemeralStake = await this.pgs.deriveEphemeralStakeAddress(user, seed);
84
+
85
+ return await this.program.methods
86
+ .deposit(new BN(amount.toString()), seed)
87
+ .accounts({
88
+ user,
89
+ depositAuthority,
90
+ systemProgram: SystemProgram.programId,
91
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
92
+ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
93
+ liqsolProgram: this.pgs.PROGRAM_IDS.LIQSOL_TOKEN,
94
+ stakeProgram: StakeProgram.programId,
95
+ liqsolMint,
96
+ userAta,
97
+ liqsolMintAuthority,
98
+ reservePool,
99
+ vault,
100
+ ephemeralStake,
101
+ controllerState,
102
+ payoutState,
103
+ bucketAuthority,
104
+ bucketTokenAccount,
105
+ userRecord,
106
+ distributionState,
107
+ payRateHistory,
108
+ instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
109
+ clock: SYSVAR_CLOCK_PUBKEY,
110
+ stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
111
+ rent: SYSVAR_RENT_PUBKEY,
112
+ globalConfig,
113
+ })
114
+ .instruction();
115
+ }
116
+
117
+ /**
118
+ * Build a withdraw-request instruction (liqSOL burn -> NFT receipt).
119
+ */
120
+ async buildWithdrawTx(
121
+ amount: bigint,
122
+ user = this.wallet.publicKey,
123
+ ): Promise<TransactionInstruction> {
124
+ if (!user) throw new Error('ConvertClient.buildWithdrawTx: wallet not connected');
125
+ if (!amount || amount <= BigInt(0))
126
+ throw new Error('ConvertClient.buildWithdrawTx: amount must be greater than zero.');
127
+
128
+ const liqsolMint = this.pgs.deriveLiqsolMintPda();
129
+ const userAta = getAssociatedTokenAddressSync(liqsolMint, user, true, TOKEN_2022_PROGRAM_ID);
130
+ const userRecord = this.pgs.deriveUserRecordPda(userAta);
131
+ const distributionState = this.pgs.deriveDistributionStatePda();
132
+
133
+ const global = this.pgs.deriveWithdrawGlobalPda();
134
+ const reservePool = this.pgs.deriveReservePoolPda();
135
+ const stakeAllocationState = this.pgs.deriveStakeAllocationStatePda();
136
+ const stakeMetrics = this.pgs.deriveStakeMetricsPda();
137
+ const maintenanceLedger = this.pgs.deriveMaintenanceLedgerPda();
138
+ const globalConfig = this.pgs.deriveGlobalConfigPda();
139
+
140
+ const globalAcct: GlobalAccount = await this.program.account.global.fetch(global);
141
+ const rawId = globalAcct.nextReceiptId;
142
+ const receiptId = normalizeToBigInt(rawId);
143
+
144
+ const mintAuthority = this.pgs.deriveWithdrawMintAuthorityPda();
145
+ const metadata = this.pgs.deriveWithdrawMintMetadataPda();
146
+ const nftMint = this.pgs.deriveWithdrawNftMintPda(receiptId);
147
+ const receiptData = this.pgs.deriveLiqReceiptDataPda(nftMint);
148
+ const owner = user;
149
+ const nftAta = getAssociatedTokenAddressSync(nftMint, owner, true, TOKEN_2022_PROGRAM_ID);
150
+
151
+ const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
152
+ const bucketTokenAccount = getAssociatedTokenAddressSync(
153
+ liqsolMint,
154
+ bucketAuthority,
155
+ true,
156
+ TOKEN_2022_PROGRAM_ID,
157
+ );
158
+
159
+ return await this.program.methods
160
+ .requestWithdraw(new BN(amount.toString()))
161
+ .accounts({
162
+ user,
163
+ owner,
164
+ global,
165
+ liqsolMint,
166
+ userAta,
167
+ userRecord,
168
+ reservePool,
169
+ stakeAllocationState,
170
+ stakeMetrics,
171
+ maintenanceLedger,
172
+ clock: SYSVAR_CLOCK_PUBKEY,
173
+ mintAuthority,
174
+ receiptData,
175
+ metadata,
176
+ nftMint,
177
+ nftAta,
178
+ distributionState,
179
+ bucketTokenAccount,
180
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
181
+ tokenInterface: TOKEN_2022_PROGRAM_ID,
182
+ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
183
+ systemProgram: SystemProgram.programId,
184
+ rent: SYSVAR_RENT_PUBKEY,
185
+ globalConfig,
186
+ })
187
+ .instruction();
188
+ }
189
+
190
+ /**
191
+ * Enumerate withdrawal receipt NFTs owned by `owner`.
192
+ */
193
+ async fetchWithdrawReceipts(owner: PublicKey): Promise<WithdrawReceipt[]> {
194
+ const globalPda = this.pgs.deriveWithdrawGlobalPda();
195
+ const globalAcct: GlobalAccount = await this.program.account.global.fetch(globalPda);
196
+ const nextId = normalizeToBigInt(globalAcct.nextReceiptId);
197
+
198
+ const mintToId = new Map<string, bigint>();
199
+ for (let i = BigInt(0); i < nextId; i++) {
200
+ const mint = this.pgs.deriveWithdrawNftMintPda(i);
201
+ mintToId.set(mint.toBase58(), i);
202
+ }
203
+
204
+ const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner(owner, {
205
+ programId: TOKEN_2022_PROGRAM_ID,
206
+ });
207
+
208
+ const receipts: WithdrawReceipt[] = [];
209
+
210
+ for (const { pubkey, account } of tokenAccounts.value) {
211
+ const info: any = account.data?.['parsed']?.info;
212
+ if (!info) continue;
213
+ const amount = info.tokenAmount;
214
+ const decimals = Number(amount?.decimals ?? 0);
215
+ const uiAmount = Number(amount?.uiAmount ?? 0);
216
+ if (decimals !== 0 || uiAmount !== 1) continue;
217
+
218
+ const mintStr: string | undefined = info.mint;
219
+ if (!mintStr) continue;
220
+ const receiptId = mintToId.get(mintStr);
221
+ if (receiptId === undefined) continue;
222
+
223
+ const mintKey = new PublicKey(mintStr);
224
+ const receiptDataPda = this.pgs.deriveLiqReceiptDataPda(mintKey);
225
+
226
+ let receiptData: ReceiptData;
227
+ try {
228
+ const raw = await this.program.account.liqReceiptData.fetch(receiptDataPda);
229
+ receiptData = {
230
+ receiptId: normalizeToBigInt(raw.receiptId),
231
+ liqports: normalizeToBigInt(raw.liqports),
232
+ epoch: normalizeToBigInt(raw.epoch),
233
+ fulfilled: Boolean(raw.fulfilled),
234
+ };
235
+ } catch (err) {
236
+ console.warn(`ConvertClient: failed to fetch receipt data for mint ${mintStr}`, err);
237
+ continue;
238
+ }
239
+
240
+ const { etaMs, readyAtMs } = await this.estimateEpochEta(receiptData.epoch);
241
+ const status = receiptData.fulfilled ? 'claimed' : etaMs <= 0 ? 'ready' : 'queued';
242
+
243
+ const amountView: BalanceView = {
244
+ amount: receiptData.liqports,
245
+ decimals: 9,
246
+ symbol: 'SOL',
247
+ };
248
+
249
+ receipts.push({
250
+ tokenId: receiptData.receiptId,
251
+ receipt: {
252
+ amount: amountView,
253
+ readyAt: readyAtMs,
254
+ chain: 'SOL',
255
+ epoch: receiptData.epoch,
256
+ status,
257
+ mint: mintKey.toBase58(),
258
+ ownerAta: pubkey.toBase58(),
259
+ },
260
+ });
261
+ }
262
+
263
+ return receipts;
264
+ }
265
+
266
+ /**
267
+ * Build the claim_withdraw instruction for a given receiptId.
268
+ */
269
+ async buildClaimWithdrawTx(
270
+ receiptId: bigint,
271
+ user: PublicKey,
272
+ ): Promise<TransactionInstruction> {
273
+ const mintAccount = this.pgs.deriveWithdrawNftMintPda(receiptId);
274
+ const receiptData = this.pgs.deriveLiqReceiptDataPda(mintAccount);
275
+ const ownerAta = getAssociatedTokenAddressSync(
276
+ mintAccount,
277
+ user,
278
+ true,
279
+ TOKEN_2022_PROGRAM_ID,
280
+ );
281
+
282
+ const accounts = {
283
+ user,
284
+ global: this.pgs.deriveWithdrawGlobalPda(),
285
+ mintAuthority: this.pgs.deriveWithdrawMintAuthorityPda(),
286
+ receiptData,
287
+ mintAccount,
288
+ ownerAta,
289
+ reservePool: this.pgs.deriveReservePoolPda(),
290
+ vault: this.pgs.deriveVaultPda(),
291
+ clock: SYSVAR_CLOCK_PUBKEY,
292
+ stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
293
+ globalConfig: this.pgs.deriveGlobalConfigPda(),
294
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
295
+ stakeProgram: StakeProgram.programId,
296
+ systemProgram: SystemProgram.programId,
297
+ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
298
+ };
299
+
300
+ return this.program.methods.claimWithdraw().accounts(accounts).instruction();
301
+ }
302
+
303
+ /**
304
+ * Estimate ready time for target epoch using recent slot time.
305
+ */
306
+ private async estimateEpochEta(targetEpoch: bigint): Promise<{ etaMs: number; readyAtMs: number }> {
307
+ const conn = this.connection;
308
+ const epochInfo = await conn.getEpochInfo();
309
+ const schedule = await conn.getEpochSchedule();
310
+ const currentEpoch = BigInt(epochInfo.epoch);
311
+
312
+ if (targetEpoch <= currentEpoch) {
313
+ const now = Date.now();
314
+ return { etaMs: 0, readyAtMs: now };
315
+ }
316
+
317
+ let slotTimeSec = 0.4;
318
+ try {
319
+ const samples = await conn.getRecentPerformanceSamples(1);
320
+ if (samples?.length) {
321
+ const s = samples[0];
322
+ slotTimeSec = s.numSlots > 0 ? s.samplePeriodSecs / s.numSlots : slotTimeSec;
323
+ }
324
+ } catch (_) {
325
+ // ignore
326
+ }
327
+
328
+ const slotsPerEpoch = BigInt(schedule.slotsPerEpoch);
329
+ const slotsRemainingInCurrent = slotsPerEpoch - BigInt(epochInfo.slotIndex);
330
+ const epochsRemaining = targetEpoch - currentEpoch - BigInt(1);
331
+ const slotsRemaining =
332
+ (epochsRemaining > 0 ? epochsRemaining * slotsPerEpoch : BigInt(0)) +
333
+ slotsRemainingInCurrent;
334
+
335
+ const etaSeconds = Number(slotsRemaining) * slotTimeSec;
336
+ const etaMs = Math.max(0, Math.round(etaSeconds * 1000));
337
+ return { etaMs, readyAtMs: Date.now() + etaMs };
338
+ }
339
+ }