@wireio/stake 2.3.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wireio/stake",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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",
@@ -32,9 +32,9 @@ export class EthereumStakingClient implements IStakingClient {
32
32
  private receiptClient: ReceiptClient;
33
33
  private validatorClient: ValidatorClient;
34
34
 
35
-
36
35
  get contract() { return this.contractService.contract; }
37
36
  get network() { return this.config.network; }
37
+ get address() { return this.signer?.getAddress(); }
38
38
 
39
39
  constructor(private config: StakerConfig) {
40
40
  try {
@@ -95,7 +95,7 @@ export class EthereumStakingClient implements IStakingClient {
95
95
  async withdraw(amount: bigint): Promise<string> {
96
96
  this.ensureUser();
97
97
 
98
- const address = await this.signer!.getAddress();
98
+ const address = await this.address!;
99
99
  const amountWei = BigNumber.from(amount);
100
100
 
101
101
  const result = await this.convertClient.performWithdraw(address, amountWei)
@@ -109,7 +109,7 @@ export class EthereumStakingClient implements IStakingClient {
109
109
  */
110
110
  async getPendingWithdraws(): Promise<WithdrawReceipt[]> {
111
111
  this.ensureUser();
112
- const address = await this.signer!.getAddress();
112
+ const address = await this.address!;
113
113
 
114
114
  return await this.receiptClient.fetchWithdrawReceipts(address);
115
115
  }
@@ -136,7 +136,7 @@ export class EthereumStakingClient implements IStakingClient {
136
136
  async stake(amount: bigint): Promise<string> {
137
137
  this.ensureUser();
138
138
 
139
- const walletAddress = await this.signer!.getAddress();
139
+ const walletAddress = await this.address!;
140
140
  const amountWei = BigNumber.from(amount);
141
141
 
142
142
  const result = await this.stakeClient.performStake(amountWei, walletAddress);
@@ -168,7 +168,7 @@ export class EthereumStakingClient implements IStakingClient {
168
168
  async buy(amount: bigint): Promise<string> {
169
169
  this.ensureUser();
170
170
 
171
- const buyer = await this.signer!.getAddress();
171
+ const buyer = await this.address!;
172
172
 
173
173
  // ! Hoodi only - check if the mock aggregator price is stale, and if so, update it before submitting the buy request
174
174
  // const network = await this.provider.getNetwork();
@@ -208,7 +208,7 @@ export class EthereumStakingClient implements IStakingClient {
208
208
  try {
209
209
  if (!this.signer) return Promise.resolve(null);
210
210
 
211
- const walletAddress = await this.signer!.getAddress();
211
+ const walletAddress = await this.address!;
212
212
 
213
213
  // 1) Native ETH balance
214
214
  const nativeBalance = await this.provider.getBalance(walletAddress);
@@ -314,7 +314,7 @@ export class EthereumStakingClient implements IStakingClient {
314
314
  async fetchPrelaunchReceipts(address?: string): Promise<preLaunchReceipt[]> {
315
315
  this.ensureUser();
316
316
 
317
- if (address === undefined) address = await this.signer!.getAddress();
317
+ if (address === undefined) address = await this.address!;
318
318
 
319
319
  //default to stake receipts
320
320
  return await this.receiptClient.stakeReceipts(address);
@@ -323,7 +323,7 @@ export class EthereumStakingClient implements IStakingClient {
323
323
  async getOPPMessages(address?: string): Promise<OPPAssertion[]> {
324
324
  this.ensureUser();
325
325
 
326
- if (!address) address = await this.signer!.getAddress();
326
+ if (!address) address = await this.address!;
327
327
 
328
328
  return await this.oppClient.getMessages(address);
329
329
  }
@@ -460,7 +460,7 @@ export class EthereumStakingClient implements IStakingClient {
460
460
  }): Promise<bigint> {
461
461
  this.ensureUser();
462
462
 
463
- const walletAddress = await this.signer!.getAddress();
463
+ const walletAddress = await this.address!;
464
464
 
465
465
  // 1) Estimate a baseline gas usage using a simple self-transfer.
466
466
  // This is cheap and doesn't depend on your contract ABI at all.
@@ -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
+ }
@@ -298,6 +298,24 @@ export class SolanaStakingClient implements IStakingClient {
298
298
  }
299
299
  }
300
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
+
301
319
  /**
302
320
  * Stake liqSOL into Outpost (liqSOL → pool) via liqsol_core::synd.
303
321
  */
@@ -391,12 +409,13 @@ export class SolanaStakingClient implements IStakingClient {
391
409
  // - nativeLamports: wallet SOL
392
410
  // - actualBalResp: liqSOL balance in user ATA
393
411
  // - snapshot: Outpost + pretokens + global index/shares
394
- const [nativeLamports, actualBalResp, snapshot] = await Promise.all([
412
+ const [nativeLamports, actualBalResp, snapshot, claimableLamports] = await Promise.all([
395
413
  this.connection.getBalance(user, 'confirmed'),
396
414
  this.connection
397
415
  .getTokenAccountBalance(userLiqsolAta, 'confirmed')
398
416
  .catch(() => null),
399
417
  this.outpostClient.fetchWireState(user).catch(() => null),
418
+ this.distributionClient.getClaimableLiqsol(user).catch(() => new BN(0)),
400
419
  ]);
401
420
 
402
421
  const LIQSOL_DECIMALS = 9;
@@ -472,6 +491,12 @@ export class SolanaStakingClient implements IStakingClient {
472
491
  decimals: LIQSOL_DECIMALS,
473
492
  ata: userLiqsolAta,
474
493
  },
494
+ claimable: {
495
+ amount: BigInt(claimableLamports.toString()),
496
+ symbol: 'LiqSOL',
497
+ decimals: LIQSOL_DECIMALS,
498
+ ata: userLiqsolAta,
499
+ },
475
500
  staked: {
476
501
  // liqSOL staked in Outpost via `synd`
477
502
  amount: stakedLiqsol,
package/src/types.ts CHANGED
@@ -42,6 +42,9 @@ export interface IStakingClient {
42
42
 
43
43
  /** Claim a withdrawal receipt (burn NFT + receive ETH/SOL) via claim_withdraw. */
44
44
  claimWithdraw(tokenId: bigint): Promise<string>
45
+
46
+ /** Solana only: claim accrued liqSOL distribution rewards. */
47
+ claimLiqsolRewards?(): Promise<string>
45
48
 
46
49
  /** Enumerate withdrawal receipt NFTs held by the user (queued/ready/claimed). */
47
50
  getPendingWithdraws(): Promise<WithdrawReceipt[]>
@@ -105,6 +108,12 @@ export interface Portfolio {
105
108
  /** Liquid staking token balance (LiqETH, LiqSOL, etc.). */
106
109
  liq: BalanceView;
107
110
 
111
+ /**
112
+ * Solana-only liqSOL rewards currently claimable from the distribution bucket.
113
+ * Not populated on Ethereum.
114
+ */
115
+ claimable?: BalanceView;
116
+
108
117
  /**
109
118
  * Outpost-staked balance:
110
119
  * - On Solana: liqSOL staked via `synd` (stakedLiqsol, in lamports).
@@ -341,4 +350,4 @@ export interface WithdrawReceipt {
341
350
  /** Optional explicit contract address for ETH queue NFT. */
342
351
  contractAddress?: string;
343
352
  };
344
- }
353
+ }