@wireio/stake 0.5.0 → 0.5.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.
@@ -4,6 +4,7 @@ import {
4
4
  Connection,
5
5
  ConnectionConfig,
6
6
  PublicKey as SolPubKey,
7
+ SystemProgram,
7
8
  Transaction,
8
9
  TransactionSignature,
9
10
  } from '@solana/web3.js';
@@ -43,11 +44,14 @@ import {
43
44
  INDEX_SCALE,
44
45
  } from './constants';
45
46
 
46
- import { buildSolanaTrancheSnapshot } from './utils';
47
- import { PayRateEntry, SolanaTransaction } from './types';
47
+ import { buildSolanaTrancheSnapshot, ceilDiv } from './utils';
48
+ import { GlobalConfig, PayRateEntry, SolanaTransaction } from './types';
49
+ import { SolanaProgramService } from './program';
48
50
 
49
51
  const commitment: Commitment = 'confirmed';
50
52
 
53
+ export const SCALE = new BN('1000000000000');
54
+
51
55
  /**
52
56
  * Solana implementation of IStakingClient.
53
57
  *
@@ -473,10 +477,25 @@ export class SolanaStakingClient implements IStakingClient {
473
477
  // READ-ONLY Public Methods
474
478
  // ---------------------------------------------------------------------
475
479
 
476
- // Estimated total APY for staking yeild
477
- getSystemAPY(): Promise<number> {
478
- // TODO
479
- return Promise.resolve(0);
480
+ // Estimated total APY for staking yield (percent, e.g. 7.23)
481
+ async getSystemAPY(): Promise<number> {
482
+ // Reuse same window as the dashboard (5 most recent valid entries)
483
+ const avgPayRate = await this.distributionClient.getAverageScaledPayRate(5);
484
+
485
+ if (avgPayRate.isZero()) {
486
+ return 0;
487
+ }
488
+
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
492
+
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
497
+
498
+ return apyPercent;
480
499
  }
481
500
 
482
501
  // ---------------------------------------------
@@ -501,87 +520,17 @@ export class SolanaStakingClient implements IStakingClient {
501
520
  return BigInt(0);
502
521
  }
503
522
 
504
- const [history, globalConfig] = await Promise.all([
505
- this.distributionClient.getPayRateHistory(),
523
+ const [avgPayRate, globalConfig] : [BN, GlobalConfig] = await Promise.all([
524
+ this.distributionClient.getAverageScaledPayRate(windowSize),
506
525
  this.distributionClient.getGlobalConfig(),
507
526
  ]);
508
527
 
509
- if (!history || !globalConfig) {
510
- return BigInt(0);
511
- }
512
-
513
- const entries = history.entries ?? [];
514
- if (!entries.length) {
515
- return BigInt(0);
516
- }
517
-
518
- const maxEntries =
519
- typeof history.maxEntries === 'number'
520
- ? history.maxEntries
521
- : entries.length;
522
-
523
- const totalEntriesAdded = new BN(
524
- history.totalEntriesAdded.toString(),
525
- );
526
-
527
- const COUNT = Math.max(1, Math.min(windowSize, maxEntries, entries.length));
528
-
529
- // ---------------------------------------------
530
- // Walk the circular buffer from "most recent"
531
- // back through COUNT entries, using the same
532
- // logic as your coworker script.
533
- // ---------------------------------------------
534
-
535
- let avgPayRate = new BN(0);
536
-
537
- if (COUNT > 0) {
538
- let idx: number;
539
-
540
- if (totalEntriesAdded.isZero()) {
541
- // "Not wrapped yet" case
542
- idx = 0;
543
- } else if (history.currentIndex === 0) {
544
- idx = maxEntries - 1;
545
- } else {
546
- idx = history.currentIndex - 1;
547
- }
548
-
549
- let sum = new BN(0);
550
- let valid = 0;
551
- const zero = new BN(0);
552
-
553
- for (let i = 0; i < COUNT; i++) {
554
- const entry: PayRateEntry | undefined = entries[idx];
555
- if (entry) {
556
- const rate = new BN(entry.scaledRate.toString());
557
- if (rate.gt(zero)) {
558
- sum = sum.add(rate);
559
- valid += 1;
560
- }
561
- }
562
-
563
- // Same wrap logic as the coworker’s script:
564
- if (totalEntriesAdded.isZero()) {
565
- // simple forward iteration if never wrapped
566
- idx = (idx + 1) % maxEntries;
567
- } else {
568
- // walk backwards through the ring buffer
569
- idx = idx === 0 ? maxEntries - 1 : idx - 1;
570
- }
571
- }
572
-
573
- if (valid > 0) {
574
- avgPayRate = this.ceilDiv(sum, new BN(valid));
575
- }
576
- }
577
-
578
- // If no valid pay-rate entries, no fee
579
- if (avgPayRate.isZero()) {
528
+ if (!globalConfig || avgPayRate.isZero()) {
580
529
  return BigInt(0);
581
530
  }
582
531
 
583
532
  // depositFeeMultiplier may be BN or number depending on your type
584
- const rawMultiplier: any = (globalConfig as any).depositFeeMultiplier;
533
+ const rawMultiplier = globalConfig.depositFeeMultiplier;
585
534
  const multiplier = new BN(
586
535
  rawMultiplier?.toString?.() ?? rawMultiplier ?? 0,
587
536
  );
@@ -592,9 +541,8 @@ export class SolanaStakingClient implements IStakingClient {
592
541
  const amountBn = new BN(amountLamports.toString());
593
542
 
594
543
  // 10^12 scale (matches scaledRate / index scale)
595
- const SCALE = new BN('1000000000000');
596
544
 
597
- const feeBn = this.ceilDiv(
545
+ const feeBn = ceilDiv(
598
546
  avgPayRate.mul(multiplier).mul(amountBn),
599
547
  SCALE,
600
548
  );
@@ -642,6 +590,75 @@ export class SolanaStakingClient implements IStakingClient {
642
590
  });
643
591
  }
644
592
 
593
+ /**
594
+ * Estimate a conservative SOL buffer (lamports) to leave in the wallet
595
+ * so the user can pay fees for the current deposit and at least one
596
+ * more transaction, plus a bit extra for future interactions.
597
+ *
598
+ * Intended usage in UI:
599
+ * const bufferLamports = await client.estimateGasBuffer();
600
+ * const maxSpendable = balanceLamports > bufferLamports
601
+ * ? balanceLamports - bufferLamports
602
+ * : 0n;
603
+ *
604
+ * @param options.txCount How many transactions to cover (default 2)
605
+ * @param options.safetyMultiplier Extra safety multiplier (default 3x)
606
+ * @param options.minBufferLamports Optional override minimum buffer (default ~0.01 SOL)
607
+ */
608
+ async estimateGasBuffer(options?: {
609
+ txCount?: number;
610
+ safetyMultiplier?: number;
611
+ minBufferLamports?: bigint;
612
+ }): Promise<bigint> {
613
+ this.ensureUser();
614
+
615
+ const payer = this.solPubKey;
616
+
617
+ // 1) Build a small dummy transaction (self-transfer of 0 lamports).
618
+ const dummyIx = SystemProgram.transfer({
619
+ fromPubkey: payer,
620
+ toPubkey: payer,
621
+ lamports: 0,
622
+ });
623
+
624
+ const tx = new Transaction().add(dummyIx);
625
+ const { blockhash } = await this.connection.getLatestBlockhash(commitment);
626
+ tx.recentBlockhash = blockhash;
627
+ tx.feePayer = payer;
628
+
629
+ // 2) Ask the cluster what it would charge in lamports for this tx.
630
+ const message = tx.compileMessage();
631
+ const feeInfo = await this.connection.getFeeForMessage(message, commitment);
632
+
633
+ // Fallback to a conservative default if RPC doesn't give a value.
634
+ const singleTxFeeLamports = BigInt(feeInfo.value ?? 5000);
635
+
636
+ // 3) How many txs do we want to cover?
637
+ // Default: 2 (deposit + one extra operation).
638
+ const txCount = BigInt(options?.txCount ?? 2);
639
+
640
+ // 4) Apply a safety multiplier (default 3x to be very safe).
641
+ const safetyMultiplier = options?.safetyMultiplier ?? 3;
642
+ const safetyScaled = BigInt(Math.round(safetyMultiplier * 100)); // e.g. 300
643
+
644
+ let bufferLamports =
645
+ (singleTxFeeLamports *
646
+ txCount *
647
+ safetyScaled) /
648
+ BigInt(100);
649
+
650
+ // 5) Enforce a minimum buffer (default ~0.01 SOL).
651
+ const defaultMinBufferLamports = BigInt(10_000_000); // 0.01 SOL (1e9 / 100)
652
+ const minBufferLamports =
653
+ options?.minBufferLamports ?? defaultMinBufferLamports;
654
+
655
+ if (bufferLamports < minBufferLamports) {
656
+ bufferLamports = minBufferLamports;
657
+ }
658
+
659
+ return bufferLamports;
660
+ }
661
+
645
662
  // ---------------------------------------------------------------------
646
663
  // Tx helpers
647
664
  // ---------------------------------------------------------------------
@@ -739,12 +756,4 @@ export class SolanaStakingClient implements IStakingClient {
739
756
  );
740
757
  }
741
758
  }
742
-
743
- private ceilDiv(n: BN, d: BN): BN {
744
- if (d.isZero()) {
745
- throw new Error('Division by zero in ceilDiv');
746
- }
747
- return n.add(d.subn(1)).div(d);
748
- }
749
-
750
759
  }
@@ -177,7 +177,7 @@ export interface SharesPreview {
177
177
  * - NFT withdrawal receipts
178
178
  * - Encumbered funds for pending withdrawals
179
179
  */
180
- export type Global = {
180
+ export type GlobalAccount = {
181
181
  /** PDA bump */
182
182
  bump: number;
183
183
 
@@ -210,9 +210,6 @@ export type Global = {
210
210
  * - `wireState` lifecycle (preLaunch/postLaunch/refund)
211
211
  */
212
212
  export type GlobalState = {
213
- /** Admin authority for pretokens/outpost config */
214
- admin: PublicKey;
215
-
216
213
  /** Deployment timestamp (Unix, seconds) */
217
214
  deployedAt: BN;
218
215
 
@@ -495,9 +492,6 @@ export type UserPretokenRecord = {
495
492
  * All price/supply fields use 8-decimal precision (SCALE = 1e8).
496
493
  */
497
494
  export type TrancheState = {
498
- /** Admin authority for tranche parameters */
499
- admin: PublicKey;
500
-
501
495
  /** Current tranche number */
502
496
  currentTrancheNumber: BN;
503
497
 
@@ -516,8 +510,8 @@ export type TrancheState = {
516
510
  /** Supply growth in basis points per tranche */
517
511
  supplyGrowthBps: number;
518
512
 
519
- /** Price growth in basis points per tranche */
520
- priceGrowthBps: number;
513
+ /** Price growth in cents per tranche (0.01 USD units) */
514
+ priceGrowthCents: number;
521
515
 
522
516
  /** Minimum valid SOL/USD price for Chainlink validation (8-decimal i128) */
523
517
  minPriceUsd: BN;
@@ -114,7 +114,7 @@ export function buildSolanaTrancheLadder(options: {
114
114
  currentTrancheSupply: bigint;
115
115
  currentPriceUsd: bigint;
116
116
  supplyGrowthBps: number;
117
- priceGrowthBps: number;
117
+ priceGrowthCents: number;
118
118
  windowBefore?: number;
119
119
  windowAfter?: number;
120
120
  }): TrancheLadderItem[] {
@@ -124,7 +124,7 @@ export function buildSolanaTrancheLadder(options: {
124
124
  currentTrancheSupply,
125
125
  currentPriceUsd,
126
126
  supplyGrowthBps,
127
- priceGrowthBps,
127
+ priceGrowthCents,
128
128
  windowBefore = 5,
129
129
  windowAfter = 5,
130
130
  } = options;
@@ -144,7 +144,7 @@ export function buildSolanaTrancheLadder(options: {
144
144
  const prevCap = capacity.get(id - 1)!;
145
145
  const prevPrice = price.get(id - 1)!;
146
146
  capacity.set(id, growOnce(prevCap, supplyGrowthBps));
147
- price.set(id, growOnce(prevPrice, priceGrowthBps));
147
+ price.set(id, growOnce(prevPrice, priceGrowthCents));
148
148
  }
149
149
 
150
150
  // Backward (past tranches)
@@ -152,7 +152,7 @@ export function buildSolanaTrancheLadder(options: {
152
152
  const nextCap = capacity.get(id + 1)!;
153
153
  const nextPrice = price.get(id + 1)!;
154
154
  capacity.set(id, shrinkOnce(nextCap, supplyGrowthBps));
155
- price.set(id, shrinkOnce(nextPrice, priceGrowthBps));
155
+ price.set(id, shrinkOnce(nextPrice, priceGrowthCents));
156
156
  }
157
157
 
158
158
  const ladder: TrancheLadderItem[] = [];
@@ -210,7 +210,7 @@ export function buildSolanaTrancheSnapshot(options: {
210
210
  const totalPretokensSold = toBigint(trancheState.totalPretokensSold);
211
211
  const currentPriceUsd = toBigint(trancheState.currentTranchePriceUsd);
212
212
  const supplyGrowthBps = trancheState.supplyGrowthBps;
213
- const priceGrowthBps = trancheState.priceGrowthBps;
213
+ const priceGrowthCents = trancheState.priceGrowthCents;
214
214
 
215
215
  const ladder = buildSolanaTrancheLadder({
216
216
  currentTranche,
@@ -218,7 +218,7 @@ export function buildSolanaTrancheSnapshot(options: {
218
218
  currentTrancheSupply,
219
219
  currentPriceUsd,
220
220
  supplyGrowthBps,
221
- priceGrowthBps,
221
+ priceGrowthCents,
222
222
  windowBefore: ladderWindowBefore,
223
223
  windowAfter: ladderWindowAfter,
224
224
  });
@@ -230,7 +230,7 @@ export function buildSolanaTrancheSnapshot(options: {
230
230
  currentTranche,
231
231
  currentPriceUsd,
232
232
  supplyGrowthBps,
233
- priceGrowthBps,
233
+ priceGrowthCents,
234
234
  totalPretokensSold,
235
235
  currentTrancheSupply,
236
236
  initialTrancheSupply,
@@ -798,4 +798,11 @@ export async function waitUntilSafeToExecuteFunction(
798
798
  export interface ScheduleConfig {
799
799
  early?: number; // fraction of epoch, default 0.10
800
800
  late?: number; // fraction of epoch, default 0.90
801
+ }
802
+
803
+ export function ceilDiv(n: BN, d: BN): BN {
804
+ if (d.isZero()) {
805
+ throw new Error('Division by zero in ceilDiv');
806
+ }
807
+ return n.add(d.subn(1)).div(d);
801
808
  }
package/src/types.ts CHANGED
@@ -21,7 +21,7 @@ export interface IStakingClient {
21
21
  buy(amount: bigint): Promise<string>;
22
22
 
23
23
  /** Fetch the complete user portfolio */
24
- getPortfolio(): Promise<Portfolio>;
24
+ getPortfolio(): Promise<Portfolio | null>;
25
25
 
26
26
  // Estimated total APY for staking yeild
27
27
  getSystemAPY(): Promise<number>;
@@ -45,12 +45,17 @@ export interface IStakingClient {
45
45
  windowBefore?: number;
46
46
  windowAfter?: number;
47
47
  }): Promise<TrancheSnapshot | null>;
48
-
49
- // Estimated total APY for staking yeild
50
- getSystemAPY(): Promise<number>;
51
48
 
52
- // Protocol fee charged for deposit from Native to LIQ
53
- getDepositFee(amount: bigint): Promise<bigint>;
49
+ /**
50
+ * Estimate a conservative ETH(wei) / SOL(lamports) buffer to leave in the wallet
51
+ * so the user can pay fees for the current deposit and at least one
52
+ * more transaction, plus a bit extra for future interactions.
53
+ */
54
+ estimateGasBuffer(options?: {
55
+ txCount?: number;
56
+ safetyMultiplier?: number;
57
+ minBuffer?: bigint;
58
+ }): Promise<bigint>
54
59
  }
55
60
 
56
61
  /**
@@ -211,10 +216,9 @@ export interface TrancheSnapshot {
211
216
  /** Current tranche price in USD (1e8 scale) */
212
217
  currentPriceUsd: bigint;
213
218
 
214
- /** Tranche curve config (per-chain) */
215
- // TODO make a constant?
216
- supplyGrowthBps: number; // e.g. 100 = +1% per tranche
217
- priceIncrementUsd: number; // e.g. 200 = +2% per tranche
219
+ supplyGrowthBps: number; // 2.5% growth per tranche
220
+
221
+ priceGrowthCents: number; // $0.02 USD growth per tranche
218
222
 
219
223
  totalPretokensSold: bigint; // total pretokens sold across all tranches (1e8 scale)
220
224
 
@@ -1,62 +0,0 @@
1
- import { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base';
2
- import { PublicKey as SolPubKey } from '@solana/web3.js';
3
- import { ChainID, ExternalNetwork, PublicKey } from '@wireio/core';
4
- import { ethers } from 'ethers';
5
-
6
- export interface IStakingClient {
7
- pubKey: PublicKey;
8
- network: ExternalNetwork;
9
-
10
- /** Amount is in the chain's smallest unit (lamports/wei, etc.) */
11
- deposit(amount: bigint): Promise<string>;
12
- withdraw(amount: bigint): Promise<string>;
13
- stake(amount: bigint): Promise<string>;
14
- unstake(amount: bigint): Promise<string>;
15
-
16
- buy?(amount: bigint, purchaseAsset: PurchaseAsset): Promise<string>;
17
-
18
- // REMOVED from shared client, SOLANA ONLY
19
- /** Register any untracked LIQ staked tokens */
20
- // register(): Promise<string>;
21
-
22
- /** Fetch the portfolio for the LIQ stake user */
23
- getPortfolio(): Promise<Portfolio>;
24
- }
25
-
26
- // Enum describing which asset is being used to buy pretoken
27
- export enum PurchaseAsset {
28
- SOL = "SOL",
29
- LIQSOL = "LIQSOL",
30
- ETH = "ETH",
31
- LIQETH = "LIQETH",
32
- YIELD = "YIELD",
33
- }
34
-
35
- export type StakerConfig = {
36
- network: ExternalNetwork;
37
- provider: BaseSignerWalletAdapter | ethers.providers.Web3Provider;
38
- pubKey: PublicKey;
39
- }
40
-
41
- export interface Portfolio {
42
- /** Native balance on chain: ETH, SOL */
43
- native: BalanceView;
44
- /** Actual Liquid balance of LiqETH, LiqSOL*/
45
- liq: BalanceView;
46
- /** Outpost Staked balance */
47
- staked: BalanceView
48
- /** SOL ONLY!
49
- * Tracked liqSOL balance from distribution program */
50
- tracked?: BalanceView;
51
- /** Extra PDAs and account addresses */
52
- extras?: Record<string, any>;
53
- /** Chain ID of the network for which this portfolio is from */
54
- chainID: ChainID;
55
- }
56
-
57
- export type BalanceView = {
58
- amount: bigint; // raw on-chain integer value
59
- decimals: number; // number of decimal places
60
- symbol?: string; // optional token symbol identifier
61
- ata?: SolPubKey; // associated token account address
62
- };