@wireio/stake 0.5.0 → 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.
@@ -16,6 +16,8 @@ import { PretokenClient } from './clients/pretoken.client';
16
16
  import { OPPClient } from './clients/opp.client';
17
17
  import { ReceiptClient } from './clients/receipt.client';
18
18
 
19
+ export const INITIAL_TRANCHE_SUPPLY = 35000;
20
+
19
21
  export class EthereumStakingClient implements IStakingClient {
20
22
  private readonly provider: ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider;
21
23
  public readonly pubKey?: WirePubKey;
@@ -154,8 +156,8 @@ export class EthereumStakingClient implements IStakingClient {
154
156
  * actual = liqETH token balance (ERC-20)
155
157
  * tracked = liqETH tracked balance (protocol/accounting view)
156
158
  */
157
- async getPortfolio(): Promise<Portfolio> {
158
- this.ensureUser();
159
+ async getPortfolio(): Promise<Portfolio | null> {
160
+ if (!this.signer) return Promise.resolve(null);
159
161
 
160
162
  const walletAddress = await this.signer!.getAddress();
161
163
 
@@ -199,7 +201,7 @@ export class EthereumStakingClient implements IStakingClient {
199
201
  currentIndex = BigInt(indexBn.toString());
200
202
  totalShares = BigInt(totalSharesBn.toString());
201
203
  userShares = BigInt(userSharesBn.toString());
202
- } catch {}
204
+ } catch { }
203
205
 
204
206
  // sharesToTokens(userShares, currentIndex) = userShares * currentIndex / indexScale
205
207
  let estimatedClaim = BigInt(0);
@@ -287,10 +289,18 @@ export class EthereumStakingClient implements IStakingClient {
287
289
  // READ-ONLY Public Methods
288
290
  // ---------------------------------------------------------------------
289
291
 
290
- // Estimated total APY for staking yeild
292
+ // Protocol-wide ETH staking APY in percent, e.g. 3.0 => "3.00%"
291
293
  async getSystemAPY(): Promise<number> {
294
+ // NOTE: despite the name, this value is effectively *annual* BPS on-chain.
295
+ // e.g. 300 => 3% APY
292
296
  const annualBpsBn = await this.contract.DepositManager.dailyRateBPS();
293
- return annualBpsBn.toNumber() / 10000; // 0.04 for 4%
297
+ const annualBps = annualBpsBn.toNumber(); // e.g. 300 for 3%
298
+
299
+ // Convert basis points (1/100 of 1%) to percent:
300
+ // 10,000 bps = 100% ⇒ 100 bps = 1% ⇒ divide by 100.
301
+ const apyPercent = annualBps / 100;
302
+
303
+ return apyPercent; // 3 => "3.00%"
294
304
  }
295
305
 
296
306
  // Protocol fee charged for deposit from Native to LIQ
@@ -339,7 +349,7 @@ export class EthereumStakingClient implements IStakingClient {
339
349
 
340
350
 
341
351
  // Fetch all required contract data
342
- const [totalSharesBn, indexBn, trancheNumberBn, trancheSupplyBn, tranchePriceWadBn, totalSupplyBn, supplyGrowthBps, priceIncrementUsd, minPriceUsd, maxPriceUsd] = await Promise.all([
352
+ const [totalSharesBn, indexBn, trancheNumberBn, trancheSupplyBn, tranchePriceWadBn, totalSupplyBn, supplyGrowthBps, priceGrowthCents, minPriceUsd, maxPriceUsd] = await Promise.all([
343
353
  this.contract.Depositor.totalShares(blockTag),
344
354
  this.contract.Depositor.index(blockTag),
345
355
  this.contract.Pretoken.trancheNumber(blockTag),
@@ -362,11 +372,7 @@ export class EthereumStakingClient implements IStakingClient {
362
372
  let ethPriceUsd: bigint = BigInt(answer.toString());
363
373
  let nativePriceTimestamp: number = Number(updatedAt);
364
374
 
365
- // ! Placeholder from hoodi deployment - don't think this can be fetched dynamically
366
- const initialTrancheSupply = BigInt(70000) * BigInt(1e8);
367
-
368
- console.log('priceIncrementUsd',priceIncrementUsd);
369
-
375
+ const initialTrancheSupply = BigInt(INITIAL_TRANCHE_SUPPLY) * BigInt(1e8);
370
376
 
371
377
  return buildEthereumTrancheSnapshot({
372
378
  chainID,
@@ -378,7 +384,7 @@ export class EthereumStakingClient implements IStakingClient {
378
384
  totalTrancheSupply,
379
385
  initialTrancheSupply,
380
386
  supplyGrowthBps,
381
- priceIncrementUsd,
387
+ priceGrowthCents,
382
388
  minPriceUsd,
383
389
  maxPriceUsd,
384
390
 
@@ -390,11 +396,81 @@ export class EthereumStakingClient implements IStakingClient {
390
396
  }
391
397
  catch (err: any) {
392
398
  console.log(err);
393
-
399
+
394
400
  throw new Error(`Error fetching Ethereum tranche snapshot: ${err?.message || err}`);
395
401
  }
396
402
  }
397
403
 
404
+ /**
405
+ * Estimate a conservative native ETH buffer (in wei) to leave in the wallet
406
+ * so the user can pay gas for the current deposit and at least one more tx.
407
+ *
408
+ * Typical usage in UI:
409
+ * const buffer = await client.estimateGasBuffer();
410
+ * const maxSpendable = balanceWei > buffer ? balanceWei - buffer : 0n;
411
+ *
412
+ * @param options.txCount How many transactions to cover (default 2: deposit + 1 more)
413
+ * @param options.safetyMultiplier Additional safety multiplier on top of txCount (default 1.5x)
414
+ * @param options.minBufferWei Optional override minimum buffer (defaults ~0.002 ETH)
415
+ */
416
+ async estimateGasBuffer(options?: {
417
+ txCount?: number;
418
+ safetyMultiplier?: number;
419
+ minBufferWei?: bigint;
420
+ }): Promise<bigint> {
421
+ this.ensureUser();
422
+
423
+ const walletAddress = await this.signer!.getAddress();
424
+
425
+ // 1) Estimate a baseline gas usage using a simple self-transfer.
426
+ // This is cheap and doesn't depend on your contract ABI at all.
427
+ const baseGas = await this.provider.estimateGas({
428
+ from: walletAddress,
429
+ to: walletAddress,
430
+ value: ethers.constants.Zero,
431
+ });
432
+
433
+ // 2) Fetch current gas price / max fee per gas.
434
+ const feeData = await this.provider.getFeeData();
435
+ let gasPrice =
436
+ feeData.maxFeePerGas ??
437
+ feeData.gasPrice ??
438
+ ethers.utils.parseUnits('20', 'gwei'); // conservative fallback
439
+
440
+ // 3) How many txs do we want to cover?
441
+ // Default: 2 (deposit + one extra action such as stake or small follow-up).
442
+ const txCount = options?.txCount ?? 2;
443
+
444
+ // We also assume that contract interactions are more expensive than a simple transfer.
445
+ // Use a multiplier (e.g., 5x) on baseGas to approximate a more complex tx.
446
+ const COMPLEX_TX_MULTIPLIER = 5; // tuning knob
447
+ const totalGasUnits = baseGas
448
+ .mul(COMPLEX_TX_MULTIPLIER)
449
+ .mul(txCount);
450
+
451
+ const baseCost = totalGasUnits.mul(gasPrice);
452
+
453
+ // 4) Safety multiplier on top of that (e.g. 1.5x).
454
+ const safetyMultiplier = options?.safetyMultiplier ?? 1.5;
455
+ const safetyScaled = Math.round(safetyMultiplier * 100); // e.g. 150
456
+
457
+ const bufferedCost = baseCost
458
+ .mul(safetyScaled)
459
+ .div(100); // apply safety factor
460
+
461
+ let bufferWei = bufferedCost.toBigInt();
462
+
463
+ // 5) Enforce a minimum floor (e.g. ~0.002 ETH).
464
+ const defaultMinBufferWei = BigInt(2_000_000_000_000_000); // 0.002 ETH
465
+ const minBufferWei = options?.minBufferWei ?? defaultMinBufferWei;
466
+
467
+ if (bufferWei < minBufferWei) {
468
+ bufferWei = minBufferWei;
469
+ }
470
+
471
+ return bufferWei;
472
+ }
473
+
398
474
  // ---------------------------------------------------------------------
399
475
  // Internal ETH Staking client helper functions
400
476
  // ---------------------------------------------------------------------
@@ -157,7 +157,7 @@ export function buildEthereumTrancheLadder(options: {
157
157
  currentTrancheSupply: bigint;
158
158
  currentPriceUsd: bigint;
159
159
  supplyGrowthBps: number;
160
- priceIncrementUsd: number;
160
+ priceGrowthCents: number;
161
161
  windowBefore?: number;
162
162
  windowAfter?: number;
163
163
  }): TrancheLadderItem[] {
@@ -167,7 +167,7 @@ export function buildEthereumTrancheLadder(options: {
167
167
  currentTrancheSupply,
168
168
  currentPriceUsd,
169
169
  supplyGrowthBps,
170
- priceIncrementUsd,
170
+ priceGrowthCents,
171
171
  windowBefore = 5,
172
172
  windowAfter = 5,
173
173
  } = options;
@@ -190,7 +190,7 @@ export function buildEthereumTrancheLadder(options: {
190
190
  const prevCap = capacity.get(id - 1)!;
191
191
  const prevPrice = price.get(id - 1)!;
192
192
  capacity.set(id, growOnce(prevCap, supplyGrowthBps));
193
- price.set(id, growOnce(prevPrice, priceIncrementUsd));
193
+ price.set(id, growOnce(prevPrice, priceGrowthCents));
194
194
  }
195
195
 
196
196
  // Backward (past tranches)
@@ -198,7 +198,7 @@ export function buildEthereumTrancheLadder(options: {
198
198
  const nextCap = capacity.get(id + 1)!;
199
199
  const nextPrice = price.get(id + 1)!;
200
200
  capacity.set(id, shrinkOnce(nextCap, supplyGrowthBps));
201
- price.set(id, shrinkOnce(nextPrice, priceIncrementUsd));
201
+ price.set(id, shrinkOnce(nextPrice, priceGrowthCents));
202
202
  }
203
203
 
204
204
  const ladder: TrancheLadderItem[] = [];
@@ -240,7 +240,7 @@ export async function buildEthereumTrancheSnapshot(options: {
240
240
  totalTrancheSupply;
241
241
  initialTrancheSupply;
242
242
  supplyGrowthBps;
243
- priceIncrementUsd;
243
+ priceGrowthCents;
244
244
  minPriceUsd;
245
245
  maxPriceUsd;
246
246
 
@@ -264,7 +264,7 @@ export async function buildEthereumTrancheSnapshot(options: {
264
264
  totalTrancheSupply,
265
265
  initialTrancheSupply,
266
266
  supplyGrowthBps,
267
- priceIncrementUsd,
267
+ priceGrowthCents,
268
268
  minPriceUsd,
269
269
  maxPriceUsd,
270
270
  } = options;
@@ -284,7 +284,7 @@ export async function buildEthereumTrancheSnapshot(options: {
284
284
  currentTrancheSupply,
285
285
  currentPriceUsd,
286
286
  supplyGrowthBps,
287
- priceIncrementUsd,
287
+ priceGrowthCents,
288
288
  windowBefore: ladderWindowBefore,
289
289
  windowAfter: ladderWindowAfter,
290
290
  });
@@ -297,7 +297,7 @@ export async function buildEthereumTrancheSnapshot(options: {
297
297
  currentTranche,
298
298
  currentPriceUsd,
299
299
  supplyGrowthBps,
300
- priceIncrementUsd,
300
+ priceGrowthCents,
301
301
  currentTrancheSupply,
302
302
  initialTrancheSupply,
303
303
  totalPretokensSold: totalTrancheSupply,
@@ -42,7 +42,7 @@ import {
42
42
  deriveLiqReceiptDataPda,
43
43
  deriveGlobalConfigPda,
44
44
  } from '../constants';
45
- import { WalletLike } from '../types';
45
+ import { GlobalAccount, WalletLike } from '../types';
46
46
 
47
47
  export class DepositClient {
48
48
  private program: Program<LiqsolCore>;
@@ -192,23 +192,41 @@ export class DepositClient {
192
192
  );
193
193
 
194
194
  // Distribution / balance-tracking
195
- // user_record is keyed by the user’s liqSOL ATA (same convention as deposit/purchase)
195
+ // user_record is keyed by the user’s liqSOL ATA (same convention
196
+ // as deposit/purchase).
196
197
  const userRecord = deriveUserRecordPda(userAta);
197
198
  const distributionState = deriveDistributionStatePda();
198
199
 
199
200
  // Reserve + stake controller PDAs
200
- const global = deriveWithdrawGlobalPda();
201
+ const global = deriveWithdrawGlobalPda(); // withdraw operator state
201
202
  const reservePool = deriveReservePoolPda();
202
203
  const stakeAllocationState = deriveStakeAllocationStatePda();
203
204
  const stakeMetrics = deriveStakeMetricsPda();
204
205
  const maintenanceLedger = deriveMaintenanceLedgerPda();
205
- const globalConfig = deriveGlobalConfigPda();
206
+ const globalConfig = deriveGlobalConfigPda(); // liqSOL config / roles
206
207
 
207
208
  // -------------------------------------------------------------
208
209
  // Need nextReceiptId from withdraw global state
209
210
  // -------------------------------------------------------------
210
- const globalState = await this.program.account.global.fetch(global);
211
- const receiptId = (globalState.nextReceiptId as BN).toBigInt();
211
+ const globalAcct : GlobalAccount = await this.program.account.global.fetch(global);
212
+
213
+ const rawId = globalAcct.nextReceiptId;
214
+ let receiptId: bigint;
215
+
216
+ if (typeof rawId === 'bigint') {
217
+ // New-style IDL / accounts returning bigint directly
218
+ receiptId = rawId;
219
+ } else if (rawId != null && typeof rawId === 'object' && 'toString' in rawId) {
220
+ // Anchor BN / bn.js or similar – normalize through string
221
+ receiptId = BigInt(rawId.toString());
222
+ } else if (typeof rawId === 'number') {
223
+ // Just in case someone typed it as a JS number in tests
224
+ receiptId = BigInt(rawId);
225
+ } else {
226
+ throw new Error(
227
+ `DepositClient.buildWithdrawTx: unexpected nextReceiptId type (${typeof rawId})`,
228
+ );
229
+ }
212
230
 
213
231
  // -------------------------------------------------------------
214
232
  // NFT receipt PDAs (mint, metadata, data, ATA)
@@ -265,7 +283,7 @@ export class DepositClient {
265
283
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
266
284
  systemProgram: SystemProgram.programId,
267
285
  rent: SYSVAR_RENT_PUBKEY,
268
- globalConfig
286
+ globalConfig,
269
287
  })
270
288
  .instruction();
271
289
 
@@ -10,8 +10,9 @@ import {
10
10
  derivePayRateHistoryPda,
11
11
  deriveUserRecordPda,
12
12
  } from '../constants';
13
- import type { DistributionState, DistributionUserRecord, GlobalConfig, PayRateHistory } from '../types';
13
+ import type { DistributionState, DistributionUserRecord, GlobalConfig, PayRateEntry, PayRateHistory } from '../types';
14
14
  import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
15
+ import { ceilDiv } from '../utils';
15
16
 
16
17
  /**
17
18
  * Distribution client – wraps the distribution portion of the liqsol_core
@@ -165,4 +166,86 @@ export class DistributionClient {
165
166
 
166
167
  return { shares: userShares, totalShares, ratio };
167
168
  }
169
+
170
+ /**
171
+ * Compute an average scaled pay rate over the most recent `windowSize`
172
+ * valid entries in the pay-rate history circular buffer.
173
+ *
174
+ * Returns a BN scaled by 1e12 (same as on-chain).
175
+ */
176
+ async getAverageScaledPayRate(windowSize = 5): Promise<BN> {
177
+ const history = await this.getPayRateHistory();
178
+ if (!history) {
179
+ return new BN(0);
180
+ }
181
+
182
+ const entries: PayRateEntry[] = history.entries ?? [];
183
+ if (!entries.length) {
184
+ return new BN(0);
185
+ }
186
+
187
+ const maxEntries: number =
188
+ typeof history.maxEntries === 'number'
189
+ ? history.maxEntries
190
+ : entries.length;
191
+
192
+ const rawTotalAdded: any = history.totalEntriesAdded ?? 0;
193
+ const totalAddedBn = new BN(rawTotalAdded.toString());
194
+
195
+ // No valid entries written yet
196
+ if (totalAddedBn.isZero()) {
197
+ return new BN(0);
198
+ }
199
+
200
+ const totalAdded = Math.min(
201
+ totalAddedBn.toNumber(),
202
+ maxEntries,
203
+ entries.length,
204
+ );
205
+
206
+ if (!totalAdded) {
207
+ return new BN(0);
208
+ }
209
+
210
+ const COUNT = Math.max(1, Math.min(windowSize, totalAdded));
211
+
212
+ const currentIndexNum = Number(history.currentIndex ?? 0);
213
+
214
+ // Most recently written entry is at currentIndex - 1 (mod maxEntries)
215
+ let idx =
216
+ currentIndexNum === 0
217
+ ? maxEntries - 1
218
+ : currentIndexNum - 1;
219
+
220
+ let sum = new BN(0);
221
+ let valid = 0;
222
+ const zero = new BN(0);
223
+
224
+ for (let i = 0; i < COUNT; i++) {
225
+ const entry: any = entries[idx];
226
+ if (entry) {
227
+ // Support both camelCase and snake_case (for safety)
228
+ const rawScaled =
229
+ entry.scaledRate ??
230
+ entry.scaled_rate ??
231
+ 0;
232
+
233
+ const rate = new BN(rawScaled.toString());
234
+ if (rate.gt(zero)) {
235
+ sum = sum.add(rate);
236
+ valid += 1;
237
+ }
238
+ }
239
+
240
+ // Walk backwards through the circular buffer
241
+ idx = idx === 0 ? maxEntries - 1 : idx - 1;
242
+ }
243
+
244
+ if (!valid) {
245
+ return new BN(0);
246
+ }
247
+
248
+ // Same behavior as the dashboard: use a ceiling-like average
249
+ return ceilDiv(sum, new BN(valid));
250
+ }
168
251
  }
@@ -100,34 +100,40 @@ export class OutpostClient {
100
100
 
101
101
  const pdas = await this.buildAccounts(userPk);
102
102
 
103
- const [
104
- globalState,
105
- outpostAccount,
106
- distributionState,
107
- userPretokenRecord,
108
- trancheState,
109
- ] = await Promise.all([
110
- this.program.account.globalState.fetch(pdas.globalState),
111
- this.program.account.outpostAccount.fetchNullable(pdas.outpostAccount),
112
- this.program.account.distributionState.fetchNullable(pdas.distributionState),
113
- this.program.account.userPretokenRecord.fetchNullable(pdas.userPretokenRecord),
114
- this.program.account.trancheState.fetchNullable(pdas.trancheState),
115
- ]);
116
-
117
- const [liqsolPoolBalance, userLiqsolBalance] = await Promise.all([
118
- this.getTokenBalance(pdas.liqsolPoolAta),
119
- this.getTokenBalance(pdas.userAta),
120
- ]);
121
-
122
- return {
123
- globalState,
124
- outpostAccount,
125
- distributionState,
126
- trancheState,
127
- userPretokenRecord,
128
- liqsolPoolBalance,
129
- userLiqsolBalance,
130
- };
103
+ try {
104
+ const [
105
+ globalState,
106
+ outpostAccount,
107
+ distributionState,
108
+ userPretokenRecord,
109
+ trancheState,
110
+ ] = await Promise.all([
111
+ this.program.account.globalState.fetch(pdas.globalState),
112
+ this.program.account.outpostAccount.fetchNullable(pdas.outpostAccount),
113
+ this.program.account.distributionState.fetchNullable(pdas.distributionState),
114
+ this.program.account.userPretokenRecord.fetchNullable(pdas.userPretokenRecord),
115
+ this.program.account.trancheState.fetchNullable(pdas.trancheState),
116
+ ]);
117
+
118
+ const [liqsolPoolBalance, userLiqsolBalance] = await Promise.all([
119
+ this.getTokenBalance(pdas.liqsolPoolAta),
120
+ this.getTokenBalance(pdas.userAta),
121
+ ]);
122
+
123
+ return {
124
+ globalState,
125
+ outpostAccount,
126
+ distributionState,
127
+ trancheState,
128
+ userPretokenRecord,
129
+ liqsolPoolBalance,
130
+ userLiqsolBalance,
131
+ };
132
+ }
133
+ catch (err) {
134
+ console.error('Error fetching Outpost wire state:', err);
135
+ throw err;
136
+ }
131
137
  }
132
138
 
133
139
  // -------------------------------------------------------------------------
@@ -89,9 +89,6 @@ export const PDA_SEEDS = {
89
89
  MINT_METADATA: 'mint_metadata',
90
90
  LIQ_RECEIPT_DATA: 'liq_receipt_data',
91
91
  WITHDRAW_MINT: 'mint',
92
-
93
-
94
-
95
92
  } as const;
96
93
 
97
94
  // Global Config PDA