@wireio/stake 2.2.2 → 2.3.0

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.
@@ -42,7 +42,7 @@ import {
42
42
  TrancheSnapshot,
43
43
  } from '../../types';
44
44
 
45
- import { DepositClient } from './clients/deposit.client';
45
+ import { ConvertClient } from './clients/convert.client';
46
46
  import { DistributionClient } from './clients/distribution.client';
47
47
  import { LeaderboardClient } from './clients/leaderboard.client';
48
48
  import { OutpostClient } from './clients/outpost.client';
@@ -56,6 +56,7 @@ import {
56
56
 
57
57
  import { buildSolanaTrancheSnapshot, ceilDiv } from './utils';
58
58
  import { GlobalConfig, PayRateEntry, SolanaTransaction } from './types';
59
+ import { WithdrawReceipt } from '../../types';
59
60
  import { SolanaProgramService } from './program';
60
61
  import bs58 from 'bs58';
61
62
 
@@ -79,7 +80,7 @@ export class SolanaStakingClient implements IStakingClient {
79
80
  public connection: Connection;
80
81
  public anchor: AnchorProvider;
81
82
 
82
- public depositClient: DepositClient;
83
+ public convertClient: ConvertClient;
83
84
  public distributionClient: DistributionClient;
84
85
  public leaderboardClient: LeaderboardClient;
85
86
  public outpostClient: OutpostClient;
@@ -210,7 +211,7 @@ export class SolanaStakingClient implements IStakingClient {
210
211
 
211
212
  this.program = new SolanaProgramService(this.anchor, config.network.chainId as SolChainID);
212
213
 
213
- this.depositClient = new DepositClient(this.anchor, this.program);
214
+ this.convertClient = new ConvertClient(this.anchor, this.program);
214
215
  this.distributionClient = new DistributionClient(this.anchor, this.program);
215
216
  this.leaderboardClient = new LeaderboardClient(this.anchor, this.program);
216
217
  this.outpostClient = new OutpostClient(this.anchor, this.program);
@@ -231,7 +232,7 @@ export class SolanaStakingClient implements IStakingClient {
231
232
  throw new Error('Deposit amount must be greater than zero.');
232
233
 
233
234
  try {
234
- const ix = await this.depositClient.buildDepositTx(amountLamports, this.squadsVaultPDA)
235
+ const ix = await this.convertClient.buildDepositTx(amountLamports, this.squadsVaultPDA)
235
236
  return !!this.squadsX
236
237
  ? await this.sendSquadsIxs(ix)
237
238
  : await this.buildAndSendIx(ix)
@@ -258,7 +259,7 @@ export class SolanaStakingClient implements IStakingClient {
258
259
  throw new Error('Withdraw amount must be greater than zero.');
259
260
 
260
261
  try {
261
- const ix = await this.depositClient.buildWithdrawTx(amountLamports, this.squadsVaultPDA)
262
+ const ix = await this.convertClient.buildWithdrawTx(amountLamports, this.squadsVaultPDA)
262
263
  return !!this.squadsX
263
264
  ? await this.sendSquadsIxs(ix)
264
265
  : await this.buildAndSendIx(ix)
@@ -269,6 +270,34 @@ export class SolanaStakingClient implements IStakingClient {
269
270
  }
270
271
  }
271
272
 
273
+ /**
274
+ * Enumerate withdrawal receipt NFTs held by the user (queued/ready/claimed).
275
+ * Mirrors the ETH getPendingWithdraws helper for UI parity.
276
+ */
277
+ async getPendingWithdraws(): Promise<WithdrawReceipt[]> {
278
+ this.ensureUser();
279
+ const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
280
+ return await this.convertClient.fetchWithdrawReceipts(owner);
281
+ }
282
+
283
+ /**
284
+ * Claim a withdrawal receipt (burn NFT + receive SOL) via claim_withdraw.
285
+ */
286
+ async claimWithdraw(tokenId: bigint): Promise<string> {
287
+ this.ensureUser();
288
+ const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
289
+
290
+ try {
291
+ const ix = await this.convertClient.buildClaimWithdrawTx(tokenId, owner);
292
+ return !!this.squadsX
293
+ ? await this.sendSquadsIxs(ix)
294
+ : await this.buildAndSendIx(ix);
295
+ } catch (err) {
296
+ console.log(`Failed to claim withdraw on Solana: ${err}`);
297
+ throw err;
298
+ }
299
+ }
300
+
272
301
  /**
273
302
  * Stake liqSOL into Outpost (liqSOL → pool) via liqsol_core::synd.
274
303
  */
@@ -1103,4 +1132,4 @@ export class SolanaStakingClient implements IStakingClient {
1103
1132
  }
1104
1133
 
1105
1134
 
1106
- }
1135
+ }
@@ -728,4 +728,26 @@ export type ValidatorRecord = {
728
728
  * PDA bump seed for validatorRecord.
729
729
  */
730
730
  bump: number;
731
+ };
732
+
733
+ /**
734
+ * IDL: `receiptData`
735
+ *
736
+ * Withdrawal receipt data structure tracking pending withdrawals.
737
+ * Each receipt represents a user's request to withdraw liqSOL,
738
+ * with the withdrawal contingent on sufficient encumbered funds
739
+ * being available at the serviceable epoch.
740
+ */
741
+ export type ReceiptData = {
742
+ /** Unique receipt identifier (monotonically increasing, u64) */
743
+ receiptId: bigint;
744
+
745
+ /** Amount of liqSOL requested for withdrawal (Token-2022 raw amount, u64) */
746
+ liqports: bigint;
747
+
748
+ /** Epoch at which this receipt becomes claimable (u64) */
749
+ epoch: bigint;
750
+
751
+ /** Flag indicating whether this receipt has been fulfilled/claimed (bool) */
752
+ fulfilled: boolean;
731
753
  };
@@ -548,4 +548,11 @@ export function ceilDiv(n: BN, d: BN): BN {
548
548
  throw new Error('Division by zero in ceilDiv');
549
549
  }
550
550
  return n.add(d.subn(1)).div(d);
551
- }
551
+ }
552
+
553
+ export function normalizeToBigInt(x: any): bigint {
554
+ if (typeof x === 'bigint') return x;
555
+ if (x != null && typeof x === 'object' && 'toString' in x) return BigInt(x.toString());
556
+ if (typeof x === 'number') return BigInt(x);
557
+ throw new Error(`normalizeToBigInt: unsupported type ${typeof x}`);
558
+ }
package/src/types.ts CHANGED
@@ -26,6 +26,9 @@ export enum SupportedSolChainID {
26
26
  Devnet = SolChainID.Devnet,
27
27
  }
28
28
 
29
+ export type ChainSymbol = 'ETH' | 'SOL';
30
+ export type WithdrawStatus = 'queued' | 'ready' | 'claimed';
31
+
29
32
  export interface IStakingClient {
30
33
  pubKey?: PublicKey;
31
34
  network: ExternalNetwork;
@@ -36,6 +39,12 @@ export interface IStakingClient {
36
39
  stake(amount: bigint): Promise<string>;
37
40
  unstake(amount: bigint): Promise<string>;
38
41
  buy(amount: bigint): Promise<string>;
42
+
43
+ /** Claim a withdrawal receipt (burn NFT + receive ETH/SOL) via claim_withdraw. */
44
+ claimWithdraw(tokenId: bigint): Promise<string>
45
+
46
+ /** Enumerate withdrawal receipt NFTs held by the user (queued/ready/claimed). */
47
+ getPendingWithdraws(): Promise<WithdrawReceipt[]>
39
48
 
40
49
  /** Fetch the complete user portfolio */
41
50
  getPortfolio(): Promise<Portfolio | null>;
@@ -71,7 +80,8 @@ export interface IStakingClient {
71
80
  balanceOverrideLamports?: bigint; // for tests/custom callers
72
81
  }): Promise<bigint>
73
82
 
74
- validatorDeposit(): Promise<string>
83
+ // Removed : ValidatorDeposit not required for Solana
84
+ // validatorDeposit(): Promise<string>
75
85
  }
76
86
 
77
87
  /**
@@ -300,9 +310,35 @@ export interface OPPAssertion {
300
310
  raw: any;
301
311
  }
302
312
 
303
-
304
-
305
313
  export enum ReceiptNFTKind {
306
314
  STAKE = 0,
307
315
  PRETOKEN_PURCHASE = 1,
316
+ }
317
+
318
+ /**
319
+ * Unified cross-chain withdraw receipt (ETH + SOL).
320
+ *
321
+ * tokenId – NFT id (ERC721 on ETH, PDA-seeded mint on SOL)
322
+ * receipt – chain-specific data normalized for UI
323
+ */
324
+ export interface WithdrawReceipt {
325
+ tokenId: bigint;
326
+ receipt: {
327
+ /** Display balance (wei or lamports) with symbol/decimals. */
328
+ amount: BalanceView;
329
+ /** Claimable time in ms since epoch (readyAt on ETH; ETA from epoch on SOL). */
330
+ readyAt: number;
331
+ /** Chain discriminator. */
332
+ chain: ChainSymbol;
333
+ /** Solana-only: epoch when claimable. */
334
+ epoch?: bigint;
335
+ /** queued | ready | claimed */
336
+ status?: WithdrawStatus;
337
+ /** NFT mint (SOL) or ERC721 contract address (ETH queue address). */
338
+ mint?: string;
339
+ /** Owner token account (SOL) for the NFT. */
340
+ ownerAta?: string;
341
+ /** Optional explicit contract address for ETH queue NFT. */
342
+ contractAddress?: string;
343
+ };
308
344
  }
@@ -1,291 +0,0 @@
1
- import { AnchorProvider, BN, Program, Wallet } from '@coral-xyz/anchor';
2
- import {
3
- SystemProgram,
4
- Transaction,
5
- TransactionInstruction,
6
- StakeProgram,
7
- SYSVAR_INSTRUCTIONS_PUBKEY,
8
- SYSVAR_CLOCK_PUBKEY,
9
- SYSVAR_RENT_PUBKEY,
10
- SYSVAR_STAKE_HISTORY_PUBKEY,
11
- Connection,
12
- PublicKey,
13
- TransactionMessage,
14
- LAMPORTS_PER_SOL,
15
- Signer,
16
- } from '@solana/web3.js';
17
-
18
- import {
19
- TOKEN_2022_PROGRAM_ID,
20
- ASSOCIATED_TOKEN_PROGRAM_ID,
21
- getAssociatedTokenAddressSync,
22
- } from '@solana/spl-token';
23
- import * as multisig from "@sqds/multisig";
24
-
25
- import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';
26
-
27
- import { GlobalAccount, WalletLike } from '../types';
28
-
29
- export class DepositClient {
30
- private program: Program<LiqsolCoreClientIdl>;
31
-
32
- get connection() {
33
- return this.provider.connection;
34
- }
35
-
36
- get wallet(): WalletLike {
37
- return this.provider.wallet;
38
- }
39
-
40
- constructor(
41
- private readonly provider: AnchorProvider,
42
- private readonly pgs: SolanaProgramService
43
- ) {
44
- this.program = pgs.getProgram('liqsolCore');
45
- }
46
-
47
- /**
48
- * Build a deposit transaction:
49
- * SOL -> liqSOL via liqsol_core::deposit.
50
- */
51
- async buildDepositTx(
52
- amount: bigint,
53
- user = this.wallet.publicKey,
54
- ): Promise<TransactionInstruction> {
55
- if (!user) {
56
- throw new Error(
57
- 'DepositClient.buildDepositTx: wallet not connected',
58
- );
59
- }
60
- if (!amount || amount <= BigInt(0)) {
61
- throw new Error(
62
- 'DepositClient.buildDepositTx: amount must be greater than zero.',
63
- );
64
- }
65
-
66
- // -------------------------------------------------------------
67
- // PDAs
68
- // -------------------------------------------------------------
69
- const depositAuthority = this.pgs.deriveDepositAuthorityPda();
70
- const liqsolMint = this.pgs.deriveLiqsolMintPda();
71
- const liqsolMintAuthority = this.pgs.deriveLiqsolMintAuthorityPda();
72
- const reservePool = this.pgs.deriveReservePoolPda();
73
- const vault = this.pgs.deriveVaultPda();
74
- const controllerState = this.pgs.deriveStakeControllerStatePda();
75
- const payoutState = this.pgs.derivePayoutStatePda();
76
- const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
77
- const payRateHistory = this.pgs.derivePayRateHistoryPda();
78
- const globalConfig = this.pgs.deriveGlobalConfigPda();
79
-
80
- // -------------------------------------------------------------
81
- // Token-2022 ATAs
82
- // -------------------------------------------------------------
83
- const userAta = getAssociatedTokenAddressSync(
84
- liqsolMint,
85
- user,
86
- true,
87
- TOKEN_2022_PROGRAM_ID,
88
- );
89
-
90
- // -------------------------------------------------------------
91
- // Distribution state + user_record (KEYED BY TOKEN ACCOUNT)
92
- // -------------------------------------------------------------
93
- const distributionState = this.pgs.deriveDistributionStatePda();
94
- const userRecord = this.pgs.deriveUserRecordPda(userAta);
95
-
96
- const bucketTokenAccount = getAssociatedTokenAddressSync(
97
- liqsolMint,
98
- bucketAuthority,
99
- true,
100
- TOKEN_2022_PROGRAM_ID,
101
- );
102
-
103
- // -------------------------------------------------------------
104
- // Ephemeral stake
105
- // -------------------------------------------------------------
106
- const seed = Math.floor(Math.random() * 2 ** 32);
107
- const ephemeralStake = await this.pgs.deriveEphemeralStakeAddress(user, seed);
108
-
109
- // -------------------------------------------------------------
110
- // BUILD IX (MUST MATCH IDL)
111
- // -------------------------------------------------------------
112
- return await this.program.methods
113
- .deposit(new BN(amount.toString()), seed)
114
- .accounts({
115
- user,
116
- depositAuthority,
117
- systemProgram: SystemProgram.programId,
118
- tokenProgram: TOKEN_2022_PROGRAM_ID,
119
- associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
120
- liqsolProgram: this.pgs.PROGRAM_IDS.LIQSOL_TOKEN,
121
- stakeProgram: StakeProgram.programId,
122
- liqsolMint,
123
- userAta,
124
- liqsolMintAuthority,
125
- reservePool,
126
- vault,
127
- ephemeralStake,
128
- controllerState,
129
- payoutState,
130
- bucketAuthority,
131
- bucketTokenAccount,
132
- userRecord,
133
- distributionState,
134
- payRateHistory,
135
- instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
136
- clock: SYSVAR_CLOCK_PUBKEY,
137
- stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
138
- rent: SYSVAR_RENT_PUBKEY,
139
- globalConfig
140
- })
141
- .instruction();
142
- }
143
-
144
- /**
145
- * Build a withdraw-request transaction:
146
- * liqSOL -> SOL via liqsol_core::requestWithdraw.
147
- *
148
- * This:
149
- * - burns liqSOL from the user
150
- * - increments totalEncumberedFunds in global state
151
- * - mints an NFT receipt (liqReceiptData + NFT ATA for owner)
152
- */
153
- async buildWithdrawTx(
154
- amount: bigint,
155
- user = this.wallet.publicKey,
156
- ): Promise<TransactionInstruction> {
157
- if (!user) {
158
- throw new Error(
159
- 'DepositClient.buildWithdrawTx: wallet not connected',
160
- );
161
- }
162
- if (!amount || amount <= BigInt(0)) {
163
- throw new Error(
164
- 'DepositClient.buildWithdrawTx: amount must be greater than zero.',
165
- );
166
- }
167
-
168
- // -------------------------------------------------------------
169
- // Core program + liqSOL mint / user ATA
170
- // -------------------------------------------------------------
171
- const liqsolMint = this.pgs.deriveLiqsolMintPda();
172
-
173
- const userAta = getAssociatedTokenAddressSync(
174
- liqsolMint,
175
- user,
176
- true,
177
- TOKEN_2022_PROGRAM_ID,
178
- );
179
-
180
- // Distribution / balance-tracking
181
- // user_record is keyed by the user’s liqSOL ATA (same convention
182
- // as deposit/purchase).
183
- const userRecord = this.pgs.deriveUserRecordPda(userAta);
184
- const distributionState = this.pgs.deriveDistributionStatePda();
185
-
186
- // Reserve + stake controller PDAs
187
- const global = this.pgs.deriveWithdrawGlobalPda(); // withdraw operator state
188
- const reservePool = this.pgs.deriveReservePoolPda();
189
- const stakeAllocationState = this.pgs.deriveStakeAllocationStatePda();
190
- const stakeMetrics = this.pgs.deriveStakeMetricsPda();
191
- const maintenanceLedger = this.pgs.deriveMaintenanceLedgerPda();
192
- const globalConfig = this.pgs.deriveGlobalConfigPda(); // liqSOL config / roles
193
-
194
- // -------------------------------------------------------------
195
- // Need nextReceiptId from withdraw global state
196
- // -------------------------------------------------------------
197
- const globalAcct: GlobalAccount = await this.program.account.global.fetch(global);
198
-
199
- const rawId = globalAcct.nextReceiptId;
200
- let receiptId: bigint;
201
-
202
- if (typeof rawId === 'bigint') {
203
- // New-style IDL / accounts returning bigint directly
204
- receiptId = rawId;
205
- } else if (rawId != null && typeof rawId === 'object' && 'toString' in rawId) {
206
- // Anchor BN / bn.js or similar – normalize through string
207
- receiptId = BigInt(rawId.toString());
208
- } else if (typeof rawId === 'number') {
209
- // Just in case someone typed it as a JS number in tests
210
- receiptId = BigInt(rawId);
211
- } else {
212
- throw new Error(
213
- `DepositClient.buildWithdrawTx: unexpected nextReceiptId type (${typeof rawId})`,
214
- );
215
- }
216
-
217
- // -------------------------------------------------------------
218
- // NFT receipt PDAs (mint, metadata, data, ATA)
219
- // -------------------------------------------------------------
220
- const mintAuthority = this.pgs.deriveWithdrawMintAuthorityPda();
221
- const metadata = this.pgs.deriveWithdrawMintMetadataPda();
222
-
223
- const nftMint = this.pgs.deriveWithdrawNftMintPda(receiptId);
224
- const receiptData = this.pgs.deriveLiqReceiptDataPda(nftMint);
225
-
226
- const owner = user;
227
- const nftAta = getAssociatedTokenAddressSync(
228
- nftMint,
229
- owner,
230
- true,
231
- TOKEN_2022_PROGRAM_ID,
232
- );
233
-
234
- // Bucket token account (same bucket used by deposit/distribution)
235
- const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
236
- const bucketTokenAccount = getAssociatedTokenAddressSync(
237
- liqsolMint,
238
- bucketAuthority,
239
- true,
240
- TOKEN_2022_PROGRAM_ID,
241
- );
242
-
243
- // -------------------------------------------------------------
244
- // BUILD IX (MUST MATCH requestWithdraw IDL)
245
- // -------------------------------------------------------------
246
- return await this.program.methods
247
- .requestWithdraw(new BN(amount.toString()))
248
- .accounts({
249
- user,
250
- owner,
251
- global,
252
- liqsolMint,
253
- userAta,
254
- userRecord,
255
- reservePool,
256
- stakeAllocationState,
257
- stakeMetrics,
258
- maintenanceLedger,
259
- clock: SYSVAR_CLOCK_PUBKEY,
260
- mintAuthority,
261
- receiptData,
262
- metadata,
263
- nftMint,
264
- nftAta,
265
- distributionState,
266
- bucketTokenAccount,
267
- tokenProgram: TOKEN_2022_PROGRAM_ID,
268
- tokenInterface: TOKEN_2022_PROGRAM_ID,
269
- associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
270
- systemProgram: SystemProgram.programId,
271
- rent: SYSVAR_RENT_PUBKEY,
272
- globalConfig,
273
- })
274
- .instruction();
275
-
276
- }
277
- }
278
-
279
- // A “wallet-adapter-like” shape (AnchorProvider.wallet matches this)
280
- type WalletLikeSigner = {
281
- publicKey: PublicKey;
282
- signTransaction: (tx: Transaction) => Promise<Transaction>;
283
- };
284
-
285
- function isKeypairSigner(x: any): x is { publicKey: PublicKey; secretKey: Uint8Array } {
286
- return !!x && x.publicKey instanceof PublicKey && x.secretKey instanceof Uint8Array;
287
- }
288
-
289
- function isWalletLikeSigner(x: any): x is WalletLikeSigner {
290
- return !!x && x.publicKey instanceof PublicKey && typeof x.signTransaction === "function";
291
- }