@wireio/stake 0.2.3 → 0.2.5

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.
@@ -2,32 +2,65 @@ import {
2
2
  Commitment,
3
3
  Connection,
4
4
  ConnectionConfig,
5
+ LAMPORTS_PER_SOL,
5
6
  PublicKey as SolPubKey,
6
7
  Transaction,
7
8
  TransactionInstruction,
8
9
  TransactionSignature,
9
10
  } from '@solana/web3.js';
10
- import { AnchorProvider, BN } from '@coral-xyz/anchor';
11
+ import { AnchorProvider } from '@coral-xyz/anchor';
11
12
  import { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base';
12
- import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
13
+ import {
14
+ ASSOCIATED_TOKEN_PROGRAM_ID,
15
+ getAssociatedTokenAddressSync,
16
+ TOKEN_2022_PROGRAM_ID,
17
+ } from '@solana/spl-token';
13
18
 
14
- import { ChainID, ExternalNetwork, KeyType, PublicKey } from '@wireio/core';
15
- import { IStakingClient, Portfolio, PurchaseAsset, PurchaseQuote, StakerConfig, TrancheSnapshot } from '../../types';
19
+ import {
20
+ ChainID,
21
+ ExternalNetwork,
22
+ KeyType,
23
+ PublicKey,
24
+ SolChainID,
25
+ } from '@wireio/core';
26
+
27
+ import {
28
+ IStakingClient,
29
+ Portfolio,
30
+ PurchaseAsset,
31
+ PurchaseQuote,
32
+ StakerConfig,
33
+ TrancheSnapshot,
34
+ } from '../../types';
16
35
 
17
36
  import { DepositClient } from './clients/deposit.client';
18
37
  import { DistributionClient } from './clients/distribution.client';
38
+ import { LeaderboardClient } from './clients/leaderboard.client';
39
+ import { OutpostClient } from './clients/outpost.client';
40
+ import { TokenClient } from './clients/token.client';
41
+
19
42
  import {
20
43
  deriveLiqsolMintPda,
21
44
  deriveReservePoolPda,
22
45
  deriveVaultPda,
23
46
  } from './constants';
47
+
48
+ import { buildSolanaTrancheSnapshot } from './utils';
24
49
  import { SolanaTransaction } from './types';
25
- import { LeaderboardClient } from './clients/leaderboard.client';
26
- import { OutpostClient } from './clients/outpost.client';
27
- import { TokenClient } from './clients/token.client';
28
50
 
29
51
  const commitment: Commitment = 'confirmed';
30
52
 
53
+ /**
54
+ * Solana implementation of IStakingClient.
55
+ *
56
+ * Responsibilities:
57
+ * - Wire together liqSOL deposit/withdraw
58
+ * - Outpost stake/unstake
59
+ * - Prelaunch WIRE (pretokens) buy flows
60
+ * - Unified portfolio + tranche snapshot + buy quotes
61
+ *
62
+ * This class composes lower-level clients; it does not know about UI.
63
+ */
31
64
  export class SolanaStakingClient implements IStakingClient {
32
65
  public pubKey: PublicKey;
33
66
  public connection: Connection;
@@ -39,8 +72,13 @@ export class SolanaStakingClient implements IStakingClient {
39
72
  private outpostClient: OutpostClient;
40
73
  private tokenClient: TokenClient;
41
74
 
42
- get solPubKey(): SolPubKey { return new SolPubKey(this.pubKey.data.array); }
43
- get network() { return this.config.network; }
75
+ get solPubKey(): SolPubKey {
76
+ return new SolPubKey(this.pubKey.data.array);
77
+ }
78
+
79
+ get network(): ExternalNetwork {
80
+ return this.config.network;
81
+ }
44
82
 
45
83
  constructor(private config: StakerConfig) {
46
84
  const adapter = config.provider as BaseSignerWalletAdapter;
@@ -54,7 +92,10 @@ export class SolanaStakingClient implements IStakingClient {
54
92
  }
55
93
 
56
94
  const opts: ConnectionConfig = { commitment };
57
- if (config.network.rpcUrls.length > 1 && config.network.rpcUrls[1].startsWith('ws')) {
95
+ if (
96
+ config.network.rpcUrls.length > 1 &&
97
+ config.network.rpcUrls[1].startsWith('ws')
98
+ ) {
58
99
  opts.wsEndpoint = config.network.rpcUrls[1];
59
100
  }
60
101
 
@@ -63,7 +104,9 @@ export class SolanaStakingClient implements IStakingClient {
63
104
  async signTransaction<T extends SolanaTransaction>(tx: T): Promise<T> {
64
105
  return adapter.signTransaction(tx);
65
106
  },
66
- async signAllTransactions<T extends SolanaTransaction>(txs: T[]): Promise<T[]> {
107
+ async signAllTransactions<T extends SolanaTransaction>(
108
+ txs: T[],
109
+ ): Promise<T[]> {
67
110
  return Promise.all(txs.map((tx) => adapter.signTransaction(tx)));
68
111
  },
69
112
  };
@@ -77,77 +120,89 @@ export class SolanaStakingClient implements IStakingClient {
77
120
  this.leaderboardClient = new LeaderboardClient(this.anchor);
78
121
  this.outpostClient = new OutpostClient(this.anchor);
79
122
  this.tokenClient = new TokenClient(this.anchor);
123
+ this.tokenClient = new TokenClient(this.anchor);
80
124
  }
81
125
 
82
126
  // ---------------------------------------------------------------------
83
- // Public IStakingClient Interface Methods
127
+ // IStakingClient core methods
84
128
  // ---------------------------------------------------------------------
85
129
 
86
130
  /**
87
- * Deposit SOL into liqSOL protocol (liqsol_core deposit flow).
88
- * @param amountLamports Amount of SOL to deposit (smallest unit)
89
- * @return Transaction signature
131
+ * Deposit native SOL into liqSOL (liqsol_core::deposit).
132
+ * Handles tx build, sign, send, and confirmation.
90
133
  */
91
134
  async deposit(amountLamports: bigint): Promise<string> {
92
- if (amountLamports <= BigInt(0)) throw new Error('Deposit amount must be greater than zero.');
93
- // const amount = new BN(amountLamports.toString());
135
+ if (amountLamports <= BigInt(0)) {
136
+ throw new Error('Deposit amount must be greater than zero.');
137
+ }
138
+
94
139
  const tx = await this.depositClient.buildDepositTx(amountLamports);
95
- const { tx: prepared, blockhash, lastValidBlockHeight } = await this.prepareTx(tx);
140
+ const { tx: prepared, blockhash, lastValidBlockHeight } = await this.prepareTx(
141
+ tx,
142
+ );
96
143
  const signed = await this.signTransaction(prepared);
97
- const result = await this.sendAndConfirmHttp(signed, { blockhash, lastValidBlockHeight });
98
- return result.signature;
144
+ return await this.sendAndConfirmHttp(signed, {
145
+ blockhash,
146
+ lastValidBlockHeight,
147
+ });
99
148
  }
100
149
 
101
150
  /**
102
151
  * Withdraw SOL from liqSOL protocol.
103
- * (Wire up once you have DepositClient.buildWithdrawTx or equivalent.)
152
+ * NOTE: placeholder until a withdraw flow is implemented in DepositClient.
104
153
  */
105
- async withdraw(amountLamports: bigint): Promise<string> {
154
+ async withdraw(_amountLamports: bigint): Promise<string> {
106
155
  throw new Error('Withdraw method not yet implemented.');
107
156
  }
108
157
 
109
158
  /**
110
159
  * Stake liqSOL into Outpost (liqSOL -> pool).
111
- * Matches deposit flow: build -> prepare -> sign -> send/confirm.
112
- * @param amountLamports Amount of liqSOL to stake (smallest unit)
160
+ * Ensures user ATA exists, then stakes via outpostClient.
113
161
  */
114
162
  async stake(amountLamports: bigint): Promise<string> {
115
- if (amountLamports <= BigInt(0)) throw new Error('Stake amount must be greater than zero.');
116
- // const amount = new BN(amountLamports.toString());
117
- const preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(this.solPubKey);
163
+ if (amountLamports <= BigInt(0)) {
164
+ throw new Error('Stake amount must be greater than zero.');
165
+ }
166
+
167
+ const preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(
168
+ this.solPubKey,
169
+ );
118
170
  const stakeIx = await this.outpostClient.buildStakeLiqsolIx(amountLamports);
119
171
  const tx = new Transaction().add(...preIxs, stakeIx);
172
+
120
173
  const prepared = await this.prepareTx(tx);
121
174
  const signed = await this.signTransaction(prepared.tx);
122
- const result = await this.sendAndConfirmHttp(signed, prepared);
123
- return result.signature;
175
+ return await this.sendAndConfirmHttp(signed, prepared);
124
176
  }
125
177
 
126
178
  /**
127
179
  * Unstake liqSOL from Outpost (pool -> liqSOL).
128
- * Matches deposit flow: build -> prepare -> sign -> send/confirm.
129
- * @param amountLamports Amount of liqSOL principal to unstake (smallest unit)
180
+ * Mirrors stake() but calls withdrawStake.
130
181
  */
131
182
  async unstake(amountLamports: bigint): Promise<string> {
132
- if (amountLamports <= BigInt(0)) throw new Error('Unstake amount must be greater than zero.');
183
+ if (amountLamports <= BigInt(0)) {
184
+ throw new Error('Unstake amount must be greater than zero.');
185
+ }
186
+
133
187
  const user = this.solPubKey;
134
- // const amount = new BN(amountLamports.toString());
135
188
  const preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(user);
136
- const withdrawIx = await this.outpostClient.buildWithdrawStakeIx(amountLamports);
189
+ const withdrawIx =
190
+ await this.outpostClient.buildWithdrawStakeIx(amountLamports);
137
191
  const tx = new Transaction().add(...preIxs, withdrawIx);
192
+
138
193
  const prepared = await this.prepareTx(tx);
139
194
  const signed = await this.signTransaction(prepared.tx);
140
- const result = await this.sendAndConfirmHttp(signed, prepared);
141
- return result.signature;
195
+ return await this.sendAndConfirmHttp(signed, prepared);
142
196
  }
143
197
 
144
-
145
- /** Buy pretoken (warrants) using specified asset.
146
- * @param amount Amount in smallest units (lamports / wei / token units).
147
- * Required for SOL / LIQSOL / ETH / LIQETH.
148
- * Ignored for YIELD (the program uses tracked yield).
149
- * @param purchaseAsset Asset used to buy pretoken.
150
- * @returns Transaction signature
198
+ /**
199
+ * Buy prelaunch WIRE “pretokens” using a supported asset.
200
+ *
201
+ * - SOL: uses purchase_with_sol
202
+ * - LIQSOL: uses purchase_with_liqsol
203
+ * - YIELD: uses purchase_warrants_from_yield
204
+ *
205
+ * ETH / LIQETH are not valid on Solana.
151
206
  */
152
207
  async buy(amountLamports: bigint, purchaseAsset: PurchaseAsset): Promise<string> {
153
208
  const user = this.solPubKey;
@@ -156,49 +211,63 @@ export class SolanaStakingClient implements IStakingClient {
156
211
 
157
212
  switch (purchaseAsset) {
158
213
  case PurchaseAsset.SOL: {
159
- if (!amountLamports || amountLamports <= BigInt(0))
160
- throw new Error("SOL pretoken purchase requires a positive amount.");
161
-
162
- ix = await this.tokenClient.buildPurchaseWithSolIx(amountLamports, user);
214
+ if (!amountLamports || amountLamports <= BigInt(0)) {
215
+ throw new Error('SOL pretoken purchase requires a positive amount.');
216
+ }
217
+ ix = await this.tokenClient.buildPurchaseWithSolIx(
218
+ amountLamports,
219
+ user,
220
+ );
163
221
  break;
164
222
  }
165
223
 
166
224
  case PurchaseAsset.LIQSOL: {
167
- if (!amountLamports || amountLamports <= BigInt(0))
168
- throw new Error("liqSOL pretoken purchase requires a positive amount.");
169
-
225
+ if (!amountLamports || amountLamports <= BigInt(0)) {
226
+ throw new Error(
227
+ 'liqSOL pretoken purchase requires a positive amount.',
228
+ );
229
+ }
170
230
  preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(user);
171
- ix = await this.tokenClient.buildPurchaseWithLiqsolIx(amountLamports, user);
231
+ ix = await this.tokenClient.buildPurchaseWithLiqsolIx(
232
+ amountLamports,
233
+ user,
234
+ );
172
235
  break;
173
236
  }
174
237
 
175
238
  case PurchaseAsset.YIELD: {
239
+ // Amount is ignored by the on-chain program; it consumes tracked yield.
176
240
  ix = await this.tokenClient.buildPurchaseFromYieldIx(user);
177
241
  break;
178
242
  }
179
243
 
180
244
  case PurchaseAsset.ETH:
181
245
  case PurchaseAsset.LIQETH: {
182
- throw new Error("ETH / LIQETH pretoken purchases are not supported on Solana.");
246
+ throw new Error(
247
+ 'ETH / LIQETH pretoken purchases are not supported on Solana.',
248
+ );
183
249
  }
184
250
 
185
251
  default:
186
- throw new Error(`Unsupported pretoken purchase asset: ${String(purchaseAsset)}`);
252
+ throw new Error(`Unsupported pretoken purchase asset: ${String(
253
+ purchaseAsset,
254
+ )}`);
187
255
  }
188
256
 
189
257
  const tx = new Transaction().add(...preIxs, ix);
190
258
  const prepared = await this.prepareTx(tx);
191
259
  const signed = await this.signTransaction(prepared.tx);
192
- const res = await this.sendAndConfirmHttp(signed, prepared);
193
- return res.signature;
260
+ return await this.sendAndConfirmHttp(signed, prepared);
194
261
  }
195
262
 
196
263
  /**
197
- * native = SOL in wallet
198
- * liq = liqSOL token balance (from Token-2022 ATA)
199
- * staked = Outpost staked liqSOL principal (from wireReceipt.stakedLiqsol)
200
- * tracked = liqSOL tracked balance (from Distribution.userRecord)
201
- * wire = prelaunch WIRE “shares” (UserWarrantRecord.totalWarrantsPurchased, 1e8)
264
+ * Aggregate view of the user’s balances on Solana:
265
+ * - native: SOL wallet balance
266
+ * - liq: liqSOL token balance (Token-2022 ATA)
267
+ * - staked: Outpost-staked liqSOL principal
268
+ * - tracked: distribution program trackedBalance (liqSOL)
269
+ * - wire: total prelaunch WIRE shares (warrants/pretokens, 1e8)
270
+ * - extras: useful internal addresses and raw state for debugging/UX
202
271
  */
203
272
  async getPortfolio(): Promise<Portfolio> {
204
273
  const user = this.solPubKey;
@@ -215,35 +284,36 @@ export class SolanaStakingClient implements IStakingClient {
215
284
  ASSOCIATED_TOKEN_PROGRAM_ID,
216
285
  );
217
286
 
218
- // IMPORTANT: use the SAME read primitive the outpost tests rely on
219
- const [nativeLamports, actualBalResp, userRecord, snapshot] = await Promise.all([
220
- this.connection.getBalance(user, "confirmed"),
221
- this.connection.getTokenAccountBalance(userLiqsolAta, "confirmed").catch(() => null),
287
+ const [
288
+ nativeLamports,
289
+ actualBalResp,
290
+ userRecord,
291
+ snapshot,
292
+ ] = await Promise.all([
293
+ this.connection.getBalance(user, 'confirmed'),
294
+ this.connection
295
+ .getTokenAccountBalance(userLiqsolAta, 'confirmed')
296
+ .catch(() => null),
222
297
  this.distributionClient.getUserRecord(user).catch(() => null),
223
298
  this.outpostClient.getWireStateSnapshot(user).catch(() => null),
224
299
  ]);
225
300
 
226
301
  const LIQSOL_DECIMALS = 9;
227
302
 
228
- const actualAmountStr = actualBalResp?.value?.amount ?? "0";
229
-
303
+ const actualAmountStr = actualBalResp?.value?.amount ?? '0';
230
304
  const trackedAmountStr =
231
- userRecord?.trackedBalance ? userRecord.trackedBalance.toString() : "0";
305
+ userRecord?.trackedBalance?.toString() ?? '0';
232
306
 
233
- // Snapshot is canonical; receipt may be null if user never staked/purchased
234
307
  const wireReceipt = snapshot?.wireReceipt ?? null;
235
308
  const userWarrantRecord = snapshot?.userWarrantRecord ?? null;
236
309
  const trancheState = snapshot?.trancheState ?? null;
237
310
  const globalState = snapshot?.globalState ?? null;
238
311
 
239
312
  const stakedAmountStr =
240
- wireReceipt?.stakedLiqsol ? wireReceipt.stakedLiqsol.toString() : "0";
313
+ wireReceipt?.stakedLiqsol?.toString() ?? '0';
241
314
 
242
- // Prelaunch WIRE "shares" = total warrants purchased by this user (1e8 precision)
243
315
  const wireSharesStr =
244
- userWarrantRecord?.totalWarrantsPurchased
245
- ? userWarrantRecord.totalWarrantsPurchased.toString()
246
- : "0";
316
+ userWarrantRecord?.totalWarrantsPurchased?.toString() ?? '0';
247
317
 
248
318
  return {
249
319
  native: {
@@ -279,7 +349,6 @@ export class SolanaStakingClient implements IStakingClient {
279
349
  vaultPDA: vaultPDA.toBase58(),
280
350
  wireReceipt,
281
351
  userWarrantRecord,
282
- // global pretoken context (handy for UI + quoting)
283
352
  globalIndex: globalState?.currentIndex?.toString(),
284
353
  totalShares: globalState?.totalShares?.toString(),
285
354
  currentTrancheNumber: trancheState?.currentTrancheNumber?.toString(),
@@ -290,165 +359,190 @@ export class SolanaStakingClient implements IStakingClient {
290
359
  }
291
360
 
292
361
  /**
293
- * Program-level prelaunch WIRE / tranche snapshot for Solana.
294
- * Uses the same OutpostWireStateSnapshot primitive as getPortfolio().
362
+ * Unified, chain-agnostic tranche snapshot for Solana.
363
+ *
364
+ * Uses:
365
+ * - liqsol_core.globalState (currentIndex, totalShares, etc.)
366
+ * - liqsol_core.trancheState (price, supply, total sold, etc.)
367
+ * - Chainlink/PriceHistory for SOL/USD (via TokenClient.getSolPriceUsdSafe)
368
+ *
369
+ * windowBefore/windowAfter control how many ladder rows we precompute
370
+ * around the current tranche for UI, but you can pass nothing if you
371
+ * only need current tranche info.
295
372
  */
296
- async getTrancheSnapshot(): Promise<TrancheSnapshot | null> {
297
- const snapshot = await this.outpostClient.getWireStateSnapshot(this.solPubKey);
298
- const global = snapshot.globalState;
299
- const tranche = snapshot.trancheState;
373
+ async getTrancheSnapshot(options?: {
374
+ chainID?: ChainID;
375
+ windowBefore?: number;
376
+ windowAfter?: number;
377
+ }): Promise<TrancheSnapshot> {
378
+ const {
379
+ chainID = SolChainID.WireTestnet,
380
+ windowBefore,
381
+ windowAfter,
382
+ } = options ?? {};
383
+
384
+ // Canonical program state
385
+ const [globalState, trancheState] = await Promise.all([
386
+ this.tokenClient.fetchGlobalState(),
387
+ this.tokenClient.fetchTrancheState(),
388
+ ]);
300
389
 
301
- if (!global || !tranche) return null;
302
-
303
- return {
304
- chainID: this.network.chainId,
305
- totalShares: BigInt(global.totalShares.toString()),
306
- currentIndex: BigInt(global.currentIndex.toString()),
307
- currentTrancheNumber: BigInt(tranche.currentTrancheNumber.toString()),
308
- currentTrancheSupply: BigInt(tranche.currentTrancheSupply.toString()),
309
- totalWarrantsSold: BigInt(tranche.totalWarrantsSold.toString()),
310
- currentTranchePriceUsd: BigInt(tranche.currentTranchePriceUsd.toString()),
311
- };
390
+ // Latest SOL/USD price (1e8) + timestamp from PriceHistory
391
+ const { price: solPriceUsd, timestamp } =
392
+ await this.tokenClient.getSolPriceUsdSafe();
393
+
394
+ return buildSolanaTrancheSnapshot({
395
+ chainID,
396
+ globalState,
397
+ trancheState,
398
+ solPriceUsd,
399
+ nativePriceTimestamp: timestamp,
400
+ ladderWindowBefore: windowBefore,
401
+ ladderWindowAfter: windowAfter,
402
+ });
312
403
  }
313
404
 
314
- // -------------------------------------------------------------
315
- // Prelaunch WIRE quote (pretokens / warrants) Solana side
316
- // -------------------------------------------------------------
317
- async getBuyQuote(amount: bigint, purchaseAsset: PurchaseAsset): Promise<PurchaseQuote> {
318
- if (amount <= BigInt(0) && purchaseAsset !== PurchaseAsset.YIELD)
319
- throw new Error("Buy amount must be greater than zero for non-YIELD purchases.");
405
+ /**
406
+ * Approximate prelaunch WIRE quote for a given amount & asset.
407
+ *
408
+ * Uses TrancheSnapshot + SOL/USD price for:
409
+ * - SOL: amount is lamports
410
+ * - LIQSOL: amount is liqSOL base units (decimals = 9)
411
+ * - YIELD: amount is treated as SOL lamports-equivalent of yield
412
+ *
413
+ * NOTE: On-chain rounding may differ slightly (this is UI-only).
414
+ */
415
+ async getBuyQuote(
416
+ amount: bigint,
417
+ asset: PurchaseAsset,
418
+ opts?: { chainID?: ChainID },
419
+ ): Promise<PurchaseQuote> {
420
+ // For non-YIELD purchases we require a positive amount.
421
+ if (asset !== PurchaseAsset.YIELD && amount <= BigInt(0)) {
422
+ throw new Error('amount must be > 0 for non-YIELD purchases');
423
+ }
424
+
425
+ const snapshot = await this.getTrancheSnapshot({
426
+ chainID: opts?.chainID,
427
+ });
320
428
 
321
- const user = this.solPubKey;
322
- const snapshot = await this.outpostClient.getWireStateSnapshot(user);
323
- const tranche = snapshot.trancheState;
429
+ const wirePriceUsd = snapshot.currentPriceUsd; // 1e8
430
+ const solPriceUsd = snapshot.nativePriceUsd; // 1e8
324
431
 
325
- if (!tranche) {
326
- throw new Error("TrancheState not initialized; cannot quote WIRE purchase.");
432
+ if (!wirePriceUsd || wirePriceUsd <= BigInt(0)) {
433
+ throw new Error('Invalid WIRE price in tranche snapshot');
434
+ }
435
+ if (!solPriceUsd || solPriceUsd <= BigInt(0)) {
436
+ throw new Error('No SOL/USD price available');
327
437
  }
328
438
 
329
- // WIRE pretoken price in USD (1e8 precision) from liqsol_core.TrancheState
330
- const wirePriceUsd = tranche.currentTranchePriceUsd; // BN
331
- const wireDecimals = 8;
439
+ const ONE_E9 = BigInt(LAMPORTS_PER_SOL); // 1e9
440
+ const ONE_E8 = BigInt(100_000_000); // 1e8
332
441
 
333
- let notionalUsd: BN; // 1e8 USD
334
- let wireSharesBn: BN; // 1e8 WIRE shares
442
+ let notionalUsd: bigint;
335
443
 
336
- switch (purchaseAsset) {
444
+ switch (asset) {
337
445
  case PurchaseAsset.SOL: {
338
- // SOL price in USD (1e8) – you wire this up in TokenClient using Chainlink / PriceHistory
339
- const solPriceUsd = await this.tokenClient.getSolPriceUsd();
340
-
341
- // amount is lamports (1e9). Convert to USD notional:
342
- // usdValue = lamports * solPriceUsd / 1e9
343
- notionalUsd = new BN(amount)
344
- .mul(solPriceUsd)
345
- .div(new BN(1_000_000_000)); // 10^9
346
-
347
- wireSharesBn = this.calculateExpectedWarrants(notionalUsd, wirePriceUsd);
446
+ // lamports * solPriceUsd / 1e9 → 1e8 USD
447
+ notionalUsd = (amount * solPriceUsd) / ONE_E9;
348
448
  break;
349
449
  }
350
450
 
351
451
  case PurchaseAsset.LIQSOL: {
352
- // liqSOL price in USD (1e8). In many setups this == SOL price, but you keep it explicit.
353
- const liqsolPriceUsd = await this.tokenClient.getSolPriceUsd();
354
-
355
- // liqSOL has 9 decimals as well; treat `amount` as raw token units.
356
- notionalUsd = new BN(amount)
357
- .mul(liqsolPriceUsd)
358
- .div(new BN(1_000_000_000)); // 10^9
359
-
360
- wireSharesBn = this.calculateExpectedWarrants(notionalUsd, wirePriceUsd);
452
+ // liqSOL also uses 9 decimals; use same conversion.
453
+ notionalUsd = (amount * solPriceUsd) / ONE_E9;
361
454
  break;
362
455
  }
363
456
 
364
457
  case PurchaseAsset.YIELD: {
365
- // For purchase_warrants_from_yield, the on-chain logic effectively consumes
366
- // tracked yield (claimBalance / availableBalance) at face value.
367
- // You have two options for UI semantics:
368
- // - treat `amount` as a virtual SOL amount and reuse the SOL path, or
369
- // - ignore `amount` and quote based on the user's actual claimBalance.
370
- //
371
- // Here we keep it simple: `amount` is lamports-equivalent of SOL yield.
372
- const solPriceUsd = await this.tokenClient.getSolPriceUsd();
373
-
374
- notionalUsd = new BN(amount)
375
- .mul(solPriceUsd)
376
- .div(new BN(1_000_000_000));
377
-
378
- wireSharesBn = this.calculateExpectedWarrants(notionalUsd, wirePriceUsd);
458
+ // Treat amount as lamports-equivalent of SOL yield (UI convention).
459
+ notionalUsd = (amount * solPriceUsd) / ONE_E9;
379
460
  break;
380
461
  }
381
462
 
382
463
  case PurchaseAsset.ETH:
383
464
  case PurchaseAsset.LIQETH:
384
- throw new Error("getBuyQuote for ETH/LIQETH is not supported on Solana.");
465
+ throw new Error('getBuyQuote for ETH/LIQETH is not supported on Solana');
385
466
 
386
467
  default:
387
- // TS safety should never hit for your enum
388
- throw new Error(`Unsupported purchase asset: ${String(purchaseAsset)}`);
468
+ throw new Error(`Unsupported purchase asset: ${String(asset)}`);
389
469
  }
390
470
 
471
+ // WIRE shares (1e8) = (notionalUsd * 1e8) / wirePriceUsd
472
+ // Add a small bias to avoid truncating to 0 on tiny buys.
473
+ const numerator = notionalUsd * ONE_E8;
474
+ const wireShares =
475
+ numerator === BigInt(0)
476
+ ? BigInt(0)
477
+ : (numerator + wirePriceUsd - BigInt(1)) / wirePriceUsd;
478
+
391
479
  return {
392
- purchaseAsset,
480
+ purchaseAsset: asset,
393
481
  amountIn: amount,
394
- wireShares: BigInt(wireSharesBn.toString()),
395
- wireDecimals,
396
- wirePriceUsd: BigInt(wirePriceUsd.toString()), // 1e8
397
- notionalUsd: BigInt(notionalUsd.toString()), // 1e8
482
+ wireShares,
483
+ wireDecimals: 8,
484
+ wirePriceUsd,
485
+ notionalUsd,
398
486
  };
399
487
  }
400
488
 
401
- // ---------------------------------------------------------------------
402
- // SOL-only extras
403
- // ---------------------------------------------------------------------
404
-
405
489
  /**
406
- * Exact warrant math reused from your old utils, just phrased in terms
407
- * of already-computed USD notional:
408
- *
409
- * expectedWarrants = (notionalUsd * 1e8) / wirePriceUsd
410
- *
411
- * where:
412
- * - notionalUsd, wirePriceUsd are 1e8-scaled USD
413
- * - result is 1e8-scaled WIRE "shares"
490
+ * Convenience helper to fetch the distribution userRecord for the current user.
491
+ * Used by balance-correction flows and debugging.
414
492
  */
415
- private calculateExpectedWarrants(notionalUsd: BN, wirePriceUsd: BN): BN {
416
- const SCALE = new BN("100000000"); // 1e8
417
- return notionalUsd.mul(SCALE).div(wirePriceUsd);
418
- }
419
-
420
493
  async getUserRecord() {
421
494
  return this.distributionClient.getUserRecord(this.solPubKey);
422
495
  }
423
496
 
497
+ /**
498
+ * Run the "correct & register" flow on Solana:
499
+ * - builds the minimal transaction (maybe multi-user) to reconcile liqSOL
500
+ * - signs and sends the transaction if it can succeed
501
+ */
424
502
  async correctBalance(amount?: bigint): Promise<string> {
425
503
  const build = await this.distributionClient.buildCorrectRegisterTx({ amount });
426
504
  if (!build.canSucceed || !build.transaction) {
427
505
  throw new Error(build.reason ?? 'Unable to build Correct&Register transaction');
428
506
  }
429
507
 
430
- const { tx, blockhash, lastValidBlockHeight } = await this.prepareTx(build.transaction);
508
+ const { tx, blockhash, lastValidBlockHeight } = await this.prepareTx(
509
+ build.transaction,
510
+ );
431
511
  const signed = await this.signTransaction(tx);
432
- const result = await this.sendAndConfirmHttp(signed, { blockhash, lastValidBlockHeight });
433
- return result.signature;
512
+ const signature = await this.sendAndConfirmHttp(signed, {
513
+ blockhash,
514
+ lastValidBlockHeight,
515
+ });
516
+ return signature;
434
517
  }
435
518
 
436
519
  // ---------------------------------------------------------------------
437
520
  // Tx helpers
438
521
  // ---------------------------------------------------------------------
439
522
 
523
+ /**
524
+ * Send a signed transaction over HTTP RPC and wait for confirmation.
525
+ * Throws if the transaction fails.
526
+ */
440
527
  private async sendAndConfirmHttp(
441
528
  signed: SolanaTransaction,
442
529
  ctx: { blockhash: string; lastValidBlockHeight: number },
443
- ): Promise<TxResult> {
444
- const signature = await this.connection.sendRawTransaction(signed.serialize(), {
445
- skipPreflight: false,
446
- preflightCommitment: commitment,
447
- maxRetries: 3,
448
- });
530
+ ): Promise<string> {
531
+ const signature = await this.connection.sendRawTransaction(
532
+ signed.serialize(),
533
+ {
534
+ skipPreflight: false,
535
+ preflightCommitment: commitment,
536
+ maxRetries: 3,
537
+ },
538
+ );
449
539
 
450
540
  const conf = await this.connection.confirmTransaction(
451
- { signature, blockhash: ctx.blockhash, lastValidBlockHeight: ctx.lastValidBlockHeight },
541
+ {
542
+ signature,
543
+ blockhash: ctx.blockhash,
544
+ lastValidBlockHeight: ctx.lastValidBlockHeight,
545
+ },
452
546
  commitment,
453
547
  );
454
548
 
@@ -456,29 +550,35 @@ export class SolanaStakingClient implements IStakingClient {
456
550
  throw new Error(`Transaction failed: ${JSON.stringify(conf.value.err)}`);
457
551
  }
458
552
 
459
- return { signature, slot: conf.context.slot, confirmed: true };
553
+ return signature;
460
554
  }
461
555
 
556
+ /**
557
+ * Sign a single Solana transaction using the connected wallet adapter.
558
+ */
462
559
  async signTransaction(tx: SolanaTransaction): Promise<SolanaTransaction> {
463
560
  return this.anchor.wallet.signTransaction(tx);
464
561
  }
465
562
 
563
+ /**
564
+ * Generic "fire and forget" send helper if the caller already
565
+ * prepared and signed the transaction.
566
+ */
466
567
  async sendTransaction(signed: SolanaTransaction): Promise<TransactionSignature> {
467
568
  return this.anchor.sendAndConfirm(signed);
468
569
  }
469
570
 
571
+ /**
572
+ * Attach recent blockhash + fee payer to a transaction.
573
+ * Required before signing and sending.
574
+ */
470
575
  async prepareTx(
471
576
  tx: Transaction,
472
577
  ): Promise<{ tx: Transaction; blockhash: string; lastValidBlockHeight: number }> {
473
- const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash('confirmed');
578
+ const { blockhash, lastValidBlockHeight } =
579
+ await this.connection.getLatestBlockhash('confirmed');
474
580
  tx.recentBlockhash = blockhash;
475
581
  tx.feePayer = this.solPubKey;
476
582
  return { tx, blockhash, lastValidBlockHeight };
477
583
  }
478
- }
479
-
480
- export interface TxResult {
481
- signature: string;
482
- slot: number;
483
- confirmed: boolean;
484
584
  }