@wireio/stake 0.5.1 → 0.5.2

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": "0.5.1",
3
+ "version": "0.5.2",
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",
@@ -3,6 +3,7 @@ import {
3
3
  ComputeBudgetProgram,
4
4
  Connection,
5
5
  ConnectionConfig,
6
+ PerfSample,
6
7
  PublicKey as SolPubKey,
7
8
  SystemProgram,
8
9
  Transaction,
@@ -40,8 +41,10 @@ import { TokenClient } from './clients/token.client';
40
41
  import {
41
42
  deriveLiqsolMintPda,
42
43
  deriveReservePoolPda,
44
+ deriveStakeMetricsPda,
43
45
  deriveVaultPda,
44
46
  INDEX_SCALE,
47
+ PAY_RATE_SCALE_FACTOR,
45
48
  } from './constants';
46
49
 
47
50
  import { buildSolanaTrancheSnapshot, ceilDiv } from './utils';
@@ -73,6 +76,7 @@ export class SolanaStakingClient implements IStakingClient {
73
76
  public leaderboardClient: LeaderboardClient;
74
77
  public outpostClient: OutpostClient;
75
78
  public tokenClient: TokenClient;
79
+ public program: SolanaProgramService
76
80
 
77
81
  get solPubKey(): SolPubKey {
78
82
  if (!this.pubKey) throw new Error('pubKey is undefined');
@@ -186,6 +190,7 @@ export class SolanaStakingClient implements IStakingClient {
186
190
  this.leaderboardClient = new LeaderboardClient(this.anchor);
187
191
  this.outpostClient = new OutpostClient(this.anchor);
188
192
  this.tokenClient = new TokenClient(this.anchor);
193
+ this.program = new SolanaProgramService(this.anchor);
189
194
  }
190
195
 
191
196
  // ---------------------------------------------------------------------
@@ -477,27 +482,114 @@ export class SolanaStakingClient implements IStakingClient {
477
482
  // READ-ONLY Public Methods
478
483
  // ---------------------------------------------------------------------
479
484
 
480
- // Estimated total APY for staking yield (percent, e.g. 7.23)
485
+ /**
486
+ * Returns the system APY (percent) for Solana,
487
+ * using compound interest per epoch and a
488
+ * cluster-derived epochs-per-year.
489
+ */
481
490
  async getSystemAPY(): Promise<number> {
482
- // Reuse same window as the dashboard (5 most recent valid entries)
483
- const avgPayRate = await this.distributionClient.getAverageScaledPayRate(5);
491
+ // 1) Per-epoch rate (decimal) from on-chain stakeMetrics
492
+ const ratePerEpoch = await this.getEpochRateDecimalFromProgram();
493
+ console.log('epochRateDecimal', ratePerEpoch);
484
494
 
485
- if (avgPayRate.isZero()) {
486
- return 0;
487
- }
495
+ // 2) Live epochs-per-year estimate from cluster
496
+ const epochsPerYear = await this.getEpochsPerYearFromCluster();
497
+ console.log('epochsPerYear', epochsPerYear);
488
498
 
489
- // 10^12, same scale used on-chain and in the dashboard
490
- const SCALE = new BN('1000000000000');
491
- const EPOCHS_PER_YEAR = 365; // matches DEFAULT_PAY_RATE semantics
499
+ // 3) Compound: (1 + r)^N - 1
500
+ const apyDecimal = Math.pow(1 + ratePerEpoch, epochsPerYear) - 1;
501
+ console.log('apyDecimal', apyDecimal);
492
502
 
493
- // Safe: pay rate is well below 1e12, so .toNumber() won't overflow
494
- const ratePerPeriod = avgPayRate.toNumber() / SCALE.toNumber(); // e.g. 0.0001917…
495
- const apyDecimal = ratePerPeriod * EPOCHS_PER_YEAR; // e.g. ~0.07
496
- const apyPercent = apyDecimal * 100; // e.g. 7
503
+ // 4) Convert to percent
504
+ const apyPercent = apyDecimal * 100;
505
+ console.log('apyPercent', apyPercent);
497
506
 
498
507
  return apyPercent;
499
508
  }
500
509
 
510
+ /**
511
+ * Reads the liqsol_core stakeMetrics account and returns the
512
+ * Solana per-epoch system rate as a **decimal** (not BPS),
513
+ * de-scaled using PAY_RATE_SCALE_FACTOR (1e12).
514
+ */
515
+ private async getEpochRateDecimalFromProgram(): Promise<number> {
516
+ const liqSolCoreProgram = this.program.getProgram('liqsolCore');
517
+ const stakeMetricsPda = deriveStakeMetricsPda();
518
+ const stakeMetrics =
519
+ await liqSolCoreProgram.account.stakeMetrics.fetch(stakeMetricsPda);
520
+
521
+ // solSystemPayRate is stored on-chain with PAY_RATE_SCALE_FACTOR (1e12)
522
+ const raw = BigInt(stakeMetrics.solSystemPayRate.toString());
523
+
524
+ // Convert to JS number in **decimal per epoch** units
525
+ const rateDecimal = Number(raw) / Number(PAY_RATE_SCALE_FACTOR);
526
+
527
+ console.log('solSystemPayRate(raw)', raw.toString());
528
+ console.log('epochRateDecimal(computed)', rateDecimal);
529
+
530
+ return rateDecimal;
531
+ }
532
+
533
+ // Simple cache so we don’t hammer RPC
534
+ private epochsPerYearCache?: { value: number; fetchedAt: number };
535
+ private static readonly EPOCHS_PER_YEAR_TTL_MS = 10 * 60 * 1000; // 10 minutes
536
+
537
+ /**
538
+ * Derive "epochs per year" from the live Solana cluster.
539
+ *
540
+ * Uses:
541
+ * - getRecentPerformanceSamples() to estimate slots/second
542
+ * - getEpochInfo() to read slotsInEpoch
543
+ */
544
+ private async getEpochsPerYearFromCluster(): Promise<number> {
545
+ const now = Date.now();
546
+
547
+ if (
548
+ this.epochsPerYearCache &&
549
+ now - this.epochsPerYearCache.fetchedAt <
550
+ SolanaStakingClient.EPOCHS_PER_YEAR_TTL_MS
551
+ ) {
552
+ return this.epochsPerYearCache.value;
553
+ }
554
+
555
+ const connection = this.anchor.connection;
556
+
557
+ // 1) Estimate slots/second from recent performance samples
558
+ const samples: PerfSample[] = await connection.getRecentPerformanceSamples(
559
+ 60,
560
+ );
561
+ if (!samples.length) {
562
+ throw new Error('No performance samples available from cluster');
563
+ }
564
+
565
+ const totalSlots = samples.reduce((acc, s) => acc + s.numSlots, 0);
566
+ const totalSecs = samples.reduce((acc, s) => acc + s.samplePeriodSecs, 0);
567
+
568
+ if (totalSecs === 0) {
569
+ throw new Error(
570
+ 'Cluster returned zero samplePeriodSecs in performance samples',
571
+ );
572
+ }
573
+
574
+ const slotsPerSecond = totalSlots / totalSecs;
575
+
576
+ // 2) Slots per epoch from cluster
577
+ const epochInfo = await connection.getEpochInfo(); // finalized commitment by default
578
+ const slotsPerEpoch = epochInfo.slotsInEpoch;
579
+
580
+ const secondsPerEpoch = slotsPerEpoch / slotsPerSecond;
581
+ const secondsPerYear = 365 * 24 * 60 * 60;
582
+
583
+ const epochsPerYear = secondsPerYear / secondsPerEpoch;
584
+
585
+ this.epochsPerYearCache = {
586
+ value: epochsPerYear,
587
+ fetchedAt: now,
588
+ };
589
+
590
+ return epochsPerYear;
591
+ }
592
+
501
593
  // ---------------------------------------------
502
594
  // Deposit fee calculation (SOL -> liqSOL)
503
595
  // ---------------------------------------------
@@ -520,7 +612,7 @@ export class SolanaStakingClient implements IStakingClient {
520
612
  return BigInt(0);
521
613
  }
522
614
 
523
- const [avgPayRate, globalConfig] : [BN, GlobalConfig] = await Promise.all([
615
+ const [avgPayRate, globalConfig]: [BN, GlobalConfig] = await Promise.all([
524
616
  this.distributionClient.getAverageScaledPayRate(windowSize),
525
617
  this.distributionClient.getGlobalConfig(),
526
618
  ]);
@@ -756,4 +848,5 @@ export class SolanaStakingClient implements IStakingClient {
756
848
  );
757
849
  }
758
850
  }
851
+
759
852
  }
package/src/staker.ts CHANGED
@@ -36,7 +36,7 @@ export class Staker {
36
36
 
37
37
  config.forEach((cfg) => {
38
38
  switch (cfg.network.chainId) {
39
- case SolChainID.WireTestnet:
39
+ case SolChainID.Devnet:
40
40
  this.clients.set(cfg.network.chainId, new SolanaStakingClient(cfg));
41
41
  break;
42
42