@wireio/stake 2.2.2 → 2.3.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.
@@ -1,5 +1,5 @@
1
1
  import { AnchorProvider, Program, BN } from '@coral-xyz/anchor';
2
- import { PublicKey } from '@solana/web3.js';
2
+ import { PublicKey, TransactionInstruction } from '@solana/web3.js';
3
3
 
4
4
  import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';
5
5
  import type { LiqsolCore } from '../../../assets/solana/devnet/types/liqsol_core';
@@ -7,6 +7,8 @@ import type { DistributionState, DistributionUserRecord, GlobalConfig, PayRateEn
7
7
  import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
8
8
  import { ceilDiv } from '../utils';
9
9
 
10
+ const INDEX_SCALE_BN = new BN('1000000000000');
11
+
10
12
  /**
11
13
  * Distribution client – wraps the distribution portion of the liqsol_core
12
14
  * program in the *new* shares-only model.
@@ -39,6 +41,15 @@ export class DistributionClient {
39
41
  return this.provider.connection;
40
42
  }
41
43
 
44
+ private async getTokenBalance(ata: PublicKey): Promise<BN> {
45
+ try {
46
+ const bal = await this.connection.getTokenAccountBalance(ata);
47
+ return new BN(bal.value.amount);
48
+ } catch {
49
+ return new BN(0);
50
+ }
51
+ }
52
+
42
53
  /**
43
54
  * Fetch the global distribution state account.
44
55
  *
@@ -162,6 +173,107 @@ export class DistributionClient {
162
173
  return { shares: userShares, totalShares, ratio };
163
174
  }
164
175
 
176
+ /**
177
+ * Compute claimable liqSOL for a wallet:
178
+ * claimable = max(0, entitled - actual)
179
+ * entitled = floor(shares * syncedIndex / 1e12)
180
+ *
181
+ * `syncedIndex` mirrors on-chain `sync_index` behavior by incorporating any
182
+ * unsynced bucket delta (bucket_balance - last_bucket_balance).
183
+ */
184
+ async getClaimableLiqsol(user: PublicKey): Promise<BN> {
185
+ const liqsolMint = this.pgs.deriveLiqsolMintPda();
186
+ const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
187
+
188
+ const userAta = getAssociatedTokenAddressSync(
189
+ liqsolMint,
190
+ user,
191
+ true,
192
+ TOKEN_2022_PROGRAM_ID,
193
+ );
194
+ const bucketTokenAccount = getAssociatedTokenAddressSync(
195
+ liqsolMint,
196
+ bucketAuthority,
197
+ true,
198
+ TOKEN_2022_PROGRAM_ID,
199
+ );
200
+
201
+ const [distributionState, userRecord, actualBalance, bucketBalance] = await Promise.all([
202
+ this.getDistributionState(),
203
+ this.getUserRecord(user),
204
+ this.getTokenBalance(userAta),
205
+ this.getTokenBalance(bucketTokenAccount),
206
+ ]);
207
+
208
+ if (!distributionState || !userRecord) {
209
+ return new BN(0);
210
+ }
211
+
212
+ let syncedIndex = new BN(distributionState.currentIndex.toString());
213
+ const totalShares = new BN(distributionState.totalShares.toString());
214
+ const lastBucketBalance = new BN(distributionState.lastBucketBalance.toString());
215
+
216
+ if (totalShares.gt(new BN(0)) && bucketBalance.gt(lastBucketBalance)) {
217
+ const delta = bucketBalance.sub(lastBucketBalance);
218
+ const indexDelta = delta.mul(INDEX_SCALE_BN).div(totalShares);
219
+ if (indexDelta.gt(new BN(0))) {
220
+ syncedIndex = syncedIndex.add(indexDelta);
221
+ }
222
+ }
223
+
224
+ const shares = new BN(userRecord.shares.toString());
225
+ const entitled = shares.mul(syncedIndex).div(INDEX_SCALE_BN);
226
+
227
+ if (entitled.lte(actualBalance)) {
228
+ return new BN(0);
229
+ }
230
+ return entitled.sub(actualBalance);
231
+ }
232
+
233
+ /**
234
+ * Build claim_rewards instruction for a wallet.
235
+ */
236
+ async buildClaimRewardsIx(user: PublicKey): Promise<TransactionInstruction> {
237
+ const liqsolMint = this.pgs.deriveLiqsolMintPda();
238
+ const distributionState = this.pgs.deriveDistributionStatePda();
239
+ const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
240
+
241
+ const userAta = getAssociatedTokenAddressSync(
242
+ liqsolMint,
243
+ user,
244
+ true,
245
+ TOKEN_2022_PROGRAM_ID,
246
+ );
247
+ const bucketTokenAccount = getAssociatedTokenAddressSync(
248
+ liqsolMint,
249
+ bucketAuthority,
250
+ true,
251
+ TOKEN_2022_PROGRAM_ID,
252
+ );
253
+
254
+ const userRecord = this.pgs.deriveUserRecordPda(userAta);
255
+ const bucketUserRecord = this.pgs.deriveUserRecordPda(bucketTokenAccount);
256
+ const extraAccountMetaList = this.pgs.deriveExtraAccountMetaListPda(liqsolMint);
257
+
258
+ return this.program.methods
259
+ .claimRewards()
260
+ .accounts({
261
+ user,
262
+ userAta,
263
+ userRecord,
264
+ bucketUserRecord,
265
+ distributionState,
266
+ extraAccountMetaList,
267
+ liqsolCoreProgram: this.pgs.PROGRAM_IDS.LIQSOL_CORE,
268
+ transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
269
+ liqsolMint,
270
+ bucketAuthority,
271
+ bucketTokenAccount,
272
+ tokenProgram: TOKEN_2022_PROGRAM_ID,
273
+ })
274
+ .instruction();
275
+ }
276
+
165
277
  /**
166
278
  * Compute an average scaled pay rate over the most recent `windowSize`
167
279
  * valid entries in the pay-rate history circular buffer.
@@ -243,4 +355,4 @@ export class DistributionClient {
243
355
  // Same behavior as the dashboard: use a ceiling-like average
244
356
  return ceilDiv(sum, new BN(valid));
245
357
  }
246
- }
358
+ }
@@ -42,7 +42,7 @@ import {
42
42
  TrancheSnapshot,
43
43
  } from '../../types';
44
44
 
45
- import { DepositClient } from './clients/deposit.client';
45
+ import { ConvertClient } from './clients/convert.client';
46
46
  import { DistributionClient } from './clients/distribution.client';
47
47
  import { LeaderboardClient } from './clients/leaderboard.client';
48
48
  import { OutpostClient } from './clients/outpost.client';
@@ -56,6 +56,7 @@ import {
56
56
 
57
57
  import { buildSolanaTrancheSnapshot, ceilDiv } from './utils';
58
58
  import { GlobalConfig, PayRateEntry, SolanaTransaction } from './types';
59
+ import { WithdrawReceipt } from '../../types';
59
60
  import { SolanaProgramService } from './program';
60
61
  import bs58 from 'bs58';
61
62
 
@@ -79,7 +80,7 @@ export class SolanaStakingClient implements IStakingClient {
79
80
  public connection: Connection;
80
81
  public anchor: AnchorProvider;
81
82
 
82
- public depositClient: DepositClient;
83
+ public convertClient: ConvertClient;
83
84
  public distributionClient: DistributionClient;
84
85
  public leaderboardClient: LeaderboardClient;
85
86
  public outpostClient: OutpostClient;
@@ -210,7 +211,7 @@ export class SolanaStakingClient implements IStakingClient {
210
211
 
211
212
  this.program = new SolanaProgramService(this.anchor, config.network.chainId as SolChainID);
212
213
 
213
- this.depositClient = new DepositClient(this.anchor, this.program);
214
+ this.convertClient = new ConvertClient(this.anchor, this.program);
214
215
  this.distributionClient = new DistributionClient(this.anchor, this.program);
215
216
  this.leaderboardClient = new LeaderboardClient(this.anchor, this.program);
216
217
  this.outpostClient = new OutpostClient(this.anchor, this.program);
@@ -231,7 +232,7 @@ export class SolanaStakingClient implements IStakingClient {
231
232
  throw new Error('Deposit amount must be greater than zero.');
232
233
 
233
234
  try {
234
- const ix = await this.depositClient.buildDepositTx(amountLamports, this.squadsVaultPDA)
235
+ const ix = await this.convertClient.buildDepositTx(amountLamports, this.squadsVaultPDA)
235
236
  return !!this.squadsX
236
237
  ? await this.sendSquadsIxs(ix)
237
238
  : await this.buildAndSendIx(ix)
@@ -258,7 +259,7 @@ export class SolanaStakingClient implements IStakingClient {
258
259
  throw new Error('Withdraw amount must be greater than zero.');
259
260
 
260
261
  try {
261
- const ix = await this.depositClient.buildWithdrawTx(amountLamports, this.squadsVaultPDA)
262
+ const ix = await this.convertClient.buildWithdrawTx(amountLamports, this.squadsVaultPDA)
262
263
  return !!this.squadsX
263
264
  ? await this.sendSquadsIxs(ix)
264
265
  : await this.buildAndSendIx(ix)
@@ -269,6 +270,52 @@ export class SolanaStakingClient implements IStakingClient {
269
270
  }
270
271
  }
271
272
 
273
+ /**
274
+ * Enumerate withdrawal receipt NFTs held by the user (queued/ready/claimed).
275
+ * Mirrors the ETH getPendingWithdraws helper for UI parity.
276
+ */
277
+ async getPendingWithdraws(): Promise<WithdrawReceipt[]> {
278
+ this.ensureUser();
279
+ const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
280
+ return await this.convertClient.fetchWithdrawReceipts(owner);
281
+ }
282
+
283
+ /**
284
+ * Claim a withdrawal receipt (burn NFT + receive SOL) via claim_withdraw.
285
+ */
286
+ async claimWithdraw(tokenId: bigint): Promise<string> {
287
+ this.ensureUser();
288
+ const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
289
+
290
+ try {
291
+ const ix = await this.convertClient.buildClaimWithdrawTx(tokenId, owner);
292
+ return !!this.squadsX
293
+ ? await this.sendSquadsIxs(ix)
294
+ : await this.buildAndSendIx(ix);
295
+ } catch (err) {
296
+ console.log(`Failed to claim withdraw on Solana: ${err}`);
297
+ throw err;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Claim accrued liqSOL distribution rewards (liqsol_core::claim_rewards).
303
+ */
304
+ async claimLiqsolRewards(): Promise<string> {
305
+ this.ensureUser();
306
+ const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
307
+
308
+ try {
309
+ const ix = await this.distributionClient.buildClaimRewardsIx(owner);
310
+ return !!this.squadsX
311
+ ? await this.sendSquadsIxs(ix)
312
+ : await this.buildAndSendIx(ix);
313
+ } catch (err) {
314
+ console.log(`Failed to claim liqSOL rewards on Solana: ${err}`);
315
+ throw err;
316
+ }
317
+ }
318
+
272
319
  /**
273
320
  * Stake liqSOL into Outpost (liqSOL → pool) via liqsol_core::synd.
274
321
  */
@@ -362,12 +409,13 @@ export class SolanaStakingClient implements IStakingClient {
362
409
  // - nativeLamports: wallet SOL
363
410
  // - actualBalResp: liqSOL balance in user ATA
364
411
  // - snapshot: Outpost + pretokens + global index/shares
365
- const [nativeLamports, actualBalResp, snapshot] = await Promise.all([
412
+ const [nativeLamports, actualBalResp, snapshot, claimableLamports] = await Promise.all([
366
413
  this.connection.getBalance(user, 'confirmed'),
367
414
  this.connection
368
415
  .getTokenAccountBalance(userLiqsolAta, 'confirmed')
369
416
  .catch(() => null),
370
417
  this.outpostClient.fetchWireState(user).catch(() => null),
418
+ this.distributionClient.getClaimableLiqsol(user).catch(() => new BN(0)),
371
419
  ]);
372
420
 
373
421
  const LIQSOL_DECIMALS = 9;
@@ -443,6 +491,12 @@ export class SolanaStakingClient implements IStakingClient {
443
491
  decimals: LIQSOL_DECIMALS,
444
492
  ata: userLiqsolAta,
445
493
  },
494
+ claimable: {
495
+ amount: BigInt(claimableLamports.toString()),
496
+ symbol: 'LiqSOL',
497
+ decimals: LIQSOL_DECIMALS,
498
+ ata: userLiqsolAta,
499
+ },
446
500
  staked: {
447
501
  // liqSOL staked in Outpost via `synd`
448
502
  amount: stakedLiqsol,
@@ -1103,4 +1157,4 @@ export class SolanaStakingClient implements IStakingClient {
1103
1157
  }
1104
1158
 
1105
1159
 
1106
- }
1160
+ }
@@ -728,4 +728,26 @@ export type ValidatorRecord = {
728
728
  * PDA bump seed for validatorRecord.
729
729
  */
730
730
  bump: number;
731
+ };
732
+
733
+ /**
734
+ * IDL: `receiptData`
735
+ *
736
+ * Withdrawal receipt data structure tracking pending withdrawals.
737
+ * Each receipt represents a user's request to withdraw liqSOL,
738
+ * with the withdrawal contingent on sufficient encumbered funds
739
+ * being available at the serviceable epoch.
740
+ */
741
+ export type ReceiptData = {
742
+ /** Unique receipt identifier (monotonically increasing, u64) */
743
+ receiptId: bigint;
744
+
745
+ /** Amount of liqSOL requested for withdrawal (Token-2022 raw amount, u64) */
746
+ liqports: bigint;
747
+
748
+ /** Epoch at which this receipt becomes claimable (u64) */
749
+ epoch: bigint;
750
+
751
+ /** Flag indicating whether this receipt has been fulfilled/claimed (bool) */
752
+ fulfilled: boolean;
731
753
  };
@@ -548,4 +548,11 @@ export function ceilDiv(n: BN, d: BN): BN {
548
548
  throw new Error('Division by zero in ceilDiv');
549
549
  }
550
550
  return n.add(d.subn(1)).div(d);
551
- }
551
+ }
552
+
553
+ export function normalizeToBigInt(x: any): bigint {
554
+ if (typeof x === 'bigint') return x;
555
+ if (x != null && typeof x === 'object' && 'toString' in x) return BigInt(x.toString());
556
+ if (typeof x === 'number') return BigInt(x);
557
+ throw new Error(`normalizeToBigInt: unsupported type ${typeof x}`);
558
+ }
package/src/types.ts CHANGED
@@ -26,6 +26,9 @@ export enum SupportedSolChainID {
26
26
  Devnet = SolChainID.Devnet,
27
27
  }
28
28
 
29
+ export type ChainSymbol = 'ETH' | 'SOL';
30
+ export type WithdrawStatus = 'queued' | 'ready' | 'claimed';
31
+
29
32
  export interface IStakingClient {
30
33
  pubKey?: PublicKey;
31
34
  network: ExternalNetwork;
@@ -36,6 +39,15 @@ export interface IStakingClient {
36
39
  stake(amount: bigint): Promise<string>;
37
40
  unstake(amount: bigint): Promise<string>;
38
41
  buy(amount: bigint): Promise<string>;
42
+
43
+ /** Claim a withdrawal receipt (burn NFT + receive ETH/SOL) via claim_withdraw. */
44
+ claimWithdraw(tokenId: bigint): Promise<string>
45
+
46
+ /** Solana only: claim accrued liqSOL distribution rewards. */
47
+ claimLiqsolRewards?(): Promise<string>
48
+
49
+ /** Enumerate withdrawal receipt NFTs held by the user (queued/ready/claimed). */
50
+ getPendingWithdraws(): Promise<WithdrawReceipt[]>
39
51
 
40
52
  /** Fetch the complete user portfolio */
41
53
  getPortfolio(): Promise<Portfolio | null>;
@@ -71,7 +83,8 @@ export interface IStakingClient {
71
83
  balanceOverrideLamports?: bigint; // for tests/custom callers
72
84
  }): Promise<bigint>
73
85
 
74
- validatorDeposit(): Promise<string>
86
+ // Removed : ValidatorDeposit not required for Solana
87
+ // validatorDeposit(): Promise<string>
75
88
  }
76
89
 
77
90
  /**
@@ -95,6 +108,12 @@ export interface Portfolio {
95
108
  /** Liquid staking token balance (LiqETH, LiqSOL, etc.). */
96
109
  liq: BalanceView;
97
110
 
111
+ /**
112
+ * Solana-only liqSOL rewards currently claimable from the distribution bucket.
113
+ * Not populated on Ethereum.
114
+ */
115
+ claimable?: BalanceView;
116
+
98
117
  /**
99
118
  * Outpost-staked balance:
100
119
  * - On Solana: liqSOL staked via `synd` (stakedLiqsol, in lamports).
@@ -300,9 +319,35 @@ export interface OPPAssertion {
300
319
  raw: any;
301
320
  }
302
321
 
303
-
304
-
305
322
  export enum ReceiptNFTKind {
306
323
  STAKE = 0,
307
324
  PRETOKEN_PURCHASE = 1,
308
- }
325
+ }
326
+
327
+ /**
328
+ * Unified cross-chain withdraw receipt (ETH + SOL).
329
+ *
330
+ * tokenId – NFT id (ERC721 on ETH, PDA-seeded mint on SOL)
331
+ * receipt – chain-specific data normalized for UI
332
+ */
333
+ export interface WithdrawReceipt {
334
+ tokenId: bigint;
335
+ receipt: {
336
+ /** Display balance (wei or lamports) with symbol/decimals. */
337
+ amount: BalanceView;
338
+ /** Claimable time in ms since epoch (readyAt on ETH; ETA from epoch on SOL). */
339
+ readyAt: number;
340
+ /** Chain discriminator. */
341
+ chain: ChainSymbol;
342
+ /** Solana-only: epoch when claimable. */
343
+ epoch?: bigint;
344
+ /** queued | ready | claimed */
345
+ status?: WithdrawStatus;
346
+ /** NFT mint (SOL) or ERC721 contract address (ETH queue address). */
347
+ mint?: string;
348
+ /** Owner token account (SOL) for the NFT. */
349
+ ownerAta?: string;
350
+ /** Optional explicit contract address for ETH queue NFT. */
351
+ contractAddress?: string;
352
+ };
353
+ }