@wireio/stake 2.2.1 → 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.
- package/README.md +185 -243
- package/lib/stake.browser.js +343 -202
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +104 -23
- package/lib/stake.js +511 -331
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +343 -202
- package/lib/stake.m.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +2 -2
- package/src/networks/ethereum/clients/receipt.client.ts +54 -69
- package/src/networks/ethereum/clients/validator.client.ts +61 -0
- package/src/networks/ethereum/ethereum.ts +21 -1
- package/src/networks/ethereum/types.ts +21 -17
- package/src/networks/solana/clients/convert.client.ts +339 -0
- package/src/networks/solana/solana.ts +35 -6
- package/src/networks/solana/types.ts +22 -0
- package/src/networks/solana/utils.ts +8 -1
- package/src/types.ts +40 -2
- package/src/networks/solana/clients/deposit.client.ts +0 -291
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
|
|
2
|
+
import {
|
|
3
|
+
SystemProgram,
|
|
4
|
+
TransactionInstruction,
|
|
5
|
+
StakeProgram,
|
|
6
|
+
SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
7
|
+
SYSVAR_CLOCK_PUBKEY,
|
|
8
|
+
SYSVAR_RENT_PUBKEY,
|
|
9
|
+
SYSVAR_STAKE_HISTORY_PUBKEY,
|
|
10
|
+
Connection,
|
|
11
|
+
PublicKey,
|
|
12
|
+
} from '@solana/web3.js';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
TOKEN_2022_PROGRAM_ID,
|
|
16
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
17
|
+
getAssociatedTokenAddressSync,
|
|
18
|
+
} from '@solana/spl-token';
|
|
19
|
+
|
|
20
|
+
import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';
|
|
21
|
+
import { GlobalAccount, ReceiptData, WalletLike } from '../types';
|
|
22
|
+
import { BalanceView, WithdrawReceipt } from '../../../types';
|
|
23
|
+
import { normalizeToBigInt } from '../utils';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ConvertClient (Solana):
|
|
27
|
+
* - deposit SOL -> liqSOL
|
|
28
|
+
* - request withdraw (liqSOL burn -> NFT receipt)
|
|
29
|
+
* - list withdrawal receipts owned by a user
|
|
30
|
+
* - build claim_withdraw instruction
|
|
31
|
+
*/
|
|
32
|
+
export class ConvertClient {
|
|
33
|
+
private program: Program<LiqsolCoreClientIdl>;
|
|
34
|
+
|
|
35
|
+
get connection(): Connection {
|
|
36
|
+
return this.provider.connection;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get wallet(): WalletLike {
|
|
40
|
+
return this.provider.wallet;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly provider: AnchorProvider,
|
|
45
|
+
private readonly pgs: SolanaProgramService,
|
|
46
|
+
) {
|
|
47
|
+
this.program = pgs.getProgram('liqsolCore');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a deposit instruction (SOL -> liqSOL).
|
|
52
|
+
*/
|
|
53
|
+
async buildDepositTx(
|
|
54
|
+
amount: bigint,
|
|
55
|
+
user = this.wallet.publicKey,
|
|
56
|
+
): Promise<TransactionInstruction> {
|
|
57
|
+
if (!user) throw new Error('ConvertClient.buildDepositTx: wallet not connected');
|
|
58
|
+
if (!amount || amount <= BigInt(0))
|
|
59
|
+
throw new Error('ConvertClient.buildDepositTx: amount must be greater than zero.');
|
|
60
|
+
|
|
61
|
+
const depositAuthority = this.pgs.deriveDepositAuthorityPda();
|
|
62
|
+
const liqsolMint = this.pgs.deriveLiqsolMintPda();
|
|
63
|
+
const liqsolMintAuthority = this.pgs.deriveLiqsolMintAuthorityPda();
|
|
64
|
+
const reservePool = this.pgs.deriveReservePoolPda();
|
|
65
|
+
const vault = this.pgs.deriveVaultPda();
|
|
66
|
+
const controllerState = this.pgs.deriveStakeControllerStatePda();
|
|
67
|
+
const payoutState = this.pgs.derivePayoutStatePda();
|
|
68
|
+
const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
|
|
69
|
+
const payRateHistory = this.pgs.derivePayRateHistoryPda();
|
|
70
|
+
const globalConfig = this.pgs.deriveGlobalConfigPda();
|
|
71
|
+
|
|
72
|
+
const userAta = getAssociatedTokenAddressSync(liqsolMint, user, true, TOKEN_2022_PROGRAM_ID);
|
|
73
|
+
const distributionState = this.pgs.deriveDistributionStatePda();
|
|
74
|
+
const userRecord = this.pgs.deriveUserRecordPda(userAta);
|
|
75
|
+
const bucketTokenAccount = getAssociatedTokenAddressSync(
|
|
76
|
+
liqsolMint,
|
|
77
|
+
bucketAuthority,
|
|
78
|
+
true,
|
|
79
|
+
TOKEN_2022_PROGRAM_ID,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const seed = Math.floor(Math.random() * 2 ** 32);
|
|
83
|
+
const ephemeralStake = await this.pgs.deriveEphemeralStakeAddress(user, seed);
|
|
84
|
+
|
|
85
|
+
return await this.program.methods
|
|
86
|
+
.deposit(new BN(amount.toString()), seed)
|
|
87
|
+
.accounts({
|
|
88
|
+
user,
|
|
89
|
+
depositAuthority,
|
|
90
|
+
systemProgram: SystemProgram.programId,
|
|
91
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
92
|
+
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
93
|
+
liqsolProgram: this.pgs.PROGRAM_IDS.LIQSOL_TOKEN,
|
|
94
|
+
stakeProgram: StakeProgram.programId,
|
|
95
|
+
liqsolMint,
|
|
96
|
+
userAta,
|
|
97
|
+
liqsolMintAuthority,
|
|
98
|
+
reservePool,
|
|
99
|
+
vault,
|
|
100
|
+
ephemeralStake,
|
|
101
|
+
controllerState,
|
|
102
|
+
payoutState,
|
|
103
|
+
bucketAuthority,
|
|
104
|
+
bucketTokenAccount,
|
|
105
|
+
userRecord,
|
|
106
|
+
distributionState,
|
|
107
|
+
payRateHistory,
|
|
108
|
+
instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
109
|
+
clock: SYSVAR_CLOCK_PUBKEY,
|
|
110
|
+
stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
|
|
111
|
+
rent: SYSVAR_RENT_PUBKEY,
|
|
112
|
+
globalConfig,
|
|
113
|
+
})
|
|
114
|
+
.instruction();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build a withdraw-request instruction (liqSOL burn -> NFT receipt).
|
|
119
|
+
*/
|
|
120
|
+
async buildWithdrawTx(
|
|
121
|
+
amount: bigint,
|
|
122
|
+
user = this.wallet.publicKey,
|
|
123
|
+
): Promise<TransactionInstruction> {
|
|
124
|
+
if (!user) throw new Error('ConvertClient.buildWithdrawTx: wallet not connected');
|
|
125
|
+
if (!amount || amount <= BigInt(0))
|
|
126
|
+
throw new Error('ConvertClient.buildWithdrawTx: amount must be greater than zero.');
|
|
127
|
+
|
|
128
|
+
const liqsolMint = this.pgs.deriveLiqsolMintPda();
|
|
129
|
+
const userAta = getAssociatedTokenAddressSync(liqsolMint, user, true, TOKEN_2022_PROGRAM_ID);
|
|
130
|
+
const userRecord = this.pgs.deriveUserRecordPda(userAta);
|
|
131
|
+
const distributionState = this.pgs.deriveDistributionStatePda();
|
|
132
|
+
|
|
133
|
+
const global = this.pgs.deriveWithdrawGlobalPda();
|
|
134
|
+
const reservePool = this.pgs.deriveReservePoolPda();
|
|
135
|
+
const stakeAllocationState = this.pgs.deriveStakeAllocationStatePda();
|
|
136
|
+
const stakeMetrics = this.pgs.deriveStakeMetricsPda();
|
|
137
|
+
const maintenanceLedger = this.pgs.deriveMaintenanceLedgerPda();
|
|
138
|
+
const globalConfig = this.pgs.deriveGlobalConfigPda();
|
|
139
|
+
|
|
140
|
+
const globalAcct: GlobalAccount = await this.program.account.global.fetch(global);
|
|
141
|
+
const rawId = globalAcct.nextReceiptId;
|
|
142
|
+
const receiptId = normalizeToBigInt(rawId);
|
|
143
|
+
|
|
144
|
+
const mintAuthority = this.pgs.deriveWithdrawMintAuthorityPda();
|
|
145
|
+
const metadata = this.pgs.deriveWithdrawMintMetadataPda();
|
|
146
|
+
const nftMint = this.pgs.deriveWithdrawNftMintPda(receiptId);
|
|
147
|
+
const receiptData = this.pgs.deriveLiqReceiptDataPda(nftMint);
|
|
148
|
+
const owner = user;
|
|
149
|
+
const nftAta = getAssociatedTokenAddressSync(nftMint, owner, true, TOKEN_2022_PROGRAM_ID);
|
|
150
|
+
|
|
151
|
+
const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
|
|
152
|
+
const bucketTokenAccount = getAssociatedTokenAddressSync(
|
|
153
|
+
liqsolMint,
|
|
154
|
+
bucketAuthority,
|
|
155
|
+
true,
|
|
156
|
+
TOKEN_2022_PROGRAM_ID,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return await this.program.methods
|
|
160
|
+
.requestWithdraw(new BN(amount.toString()))
|
|
161
|
+
.accounts({
|
|
162
|
+
user,
|
|
163
|
+
owner,
|
|
164
|
+
global,
|
|
165
|
+
liqsolMint,
|
|
166
|
+
userAta,
|
|
167
|
+
userRecord,
|
|
168
|
+
reservePool,
|
|
169
|
+
stakeAllocationState,
|
|
170
|
+
stakeMetrics,
|
|
171
|
+
maintenanceLedger,
|
|
172
|
+
clock: SYSVAR_CLOCK_PUBKEY,
|
|
173
|
+
mintAuthority,
|
|
174
|
+
receiptData,
|
|
175
|
+
metadata,
|
|
176
|
+
nftMint,
|
|
177
|
+
nftAta,
|
|
178
|
+
distributionState,
|
|
179
|
+
bucketTokenAccount,
|
|
180
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
181
|
+
tokenInterface: TOKEN_2022_PROGRAM_ID,
|
|
182
|
+
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
183
|
+
systemProgram: SystemProgram.programId,
|
|
184
|
+
rent: SYSVAR_RENT_PUBKEY,
|
|
185
|
+
globalConfig,
|
|
186
|
+
})
|
|
187
|
+
.instruction();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Enumerate withdrawal receipt NFTs owned by `owner`.
|
|
192
|
+
*/
|
|
193
|
+
async fetchWithdrawReceipts(owner: PublicKey): Promise<WithdrawReceipt[]> {
|
|
194
|
+
const globalPda = this.pgs.deriveWithdrawGlobalPda();
|
|
195
|
+
const globalAcct: GlobalAccount = await this.program.account.global.fetch(globalPda);
|
|
196
|
+
const nextId = normalizeToBigInt(globalAcct.nextReceiptId);
|
|
197
|
+
|
|
198
|
+
const mintToId = new Map<string, bigint>();
|
|
199
|
+
for (let i = BigInt(0); i < nextId; i++) {
|
|
200
|
+
const mint = this.pgs.deriveWithdrawNftMintPda(i);
|
|
201
|
+
mintToId.set(mint.toBase58(), i);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner(owner, {
|
|
205
|
+
programId: TOKEN_2022_PROGRAM_ID,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const receipts: WithdrawReceipt[] = [];
|
|
209
|
+
|
|
210
|
+
for (const { pubkey, account } of tokenAccounts.value) {
|
|
211
|
+
const info: any = account.data?.['parsed']?.info;
|
|
212
|
+
if (!info) continue;
|
|
213
|
+
const amount = info.tokenAmount;
|
|
214
|
+
const decimals = Number(amount?.decimals ?? 0);
|
|
215
|
+
const uiAmount = Number(amount?.uiAmount ?? 0);
|
|
216
|
+
if (decimals !== 0 || uiAmount !== 1) continue;
|
|
217
|
+
|
|
218
|
+
const mintStr: string | undefined = info.mint;
|
|
219
|
+
if (!mintStr) continue;
|
|
220
|
+
const receiptId = mintToId.get(mintStr);
|
|
221
|
+
if (receiptId === undefined) continue;
|
|
222
|
+
|
|
223
|
+
const mintKey = new PublicKey(mintStr);
|
|
224
|
+
const receiptDataPda = this.pgs.deriveLiqReceiptDataPda(mintKey);
|
|
225
|
+
|
|
226
|
+
let receiptData: ReceiptData;
|
|
227
|
+
try {
|
|
228
|
+
const raw = await this.program.account.liqReceiptData.fetch(receiptDataPda);
|
|
229
|
+
receiptData = {
|
|
230
|
+
receiptId: normalizeToBigInt(raw.receiptId),
|
|
231
|
+
liqports: normalizeToBigInt(raw.liqports),
|
|
232
|
+
epoch: normalizeToBigInt(raw.epoch),
|
|
233
|
+
fulfilled: Boolean(raw.fulfilled),
|
|
234
|
+
};
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(`ConvertClient: failed to fetch receipt data for mint ${mintStr}`, err);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { etaMs, readyAtMs } = await this.estimateEpochEta(receiptData.epoch);
|
|
241
|
+
const status = receiptData.fulfilled ? 'claimed' : etaMs <= 0 ? 'ready' : 'queued';
|
|
242
|
+
|
|
243
|
+
const amountView: BalanceView = {
|
|
244
|
+
amount: receiptData.liqports,
|
|
245
|
+
decimals: 9,
|
|
246
|
+
symbol: 'SOL',
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
receipts.push({
|
|
250
|
+
tokenId: receiptData.receiptId,
|
|
251
|
+
receipt: {
|
|
252
|
+
amount: amountView,
|
|
253
|
+
readyAt: readyAtMs,
|
|
254
|
+
chain: 'SOL',
|
|
255
|
+
epoch: receiptData.epoch,
|
|
256
|
+
status,
|
|
257
|
+
mint: mintKey.toBase58(),
|
|
258
|
+
ownerAta: pubkey.toBase58(),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return receipts;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Build the claim_withdraw instruction for a given receiptId.
|
|
268
|
+
*/
|
|
269
|
+
async buildClaimWithdrawTx(
|
|
270
|
+
receiptId: bigint,
|
|
271
|
+
user: PublicKey,
|
|
272
|
+
): Promise<TransactionInstruction> {
|
|
273
|
+
const mintAccount = this.pgs.deriveWithdrawNftMintPda(receiptId);
|
|
274
|
+
const receiptData = this.pgs.deriveLiqReceiptDataPda(mintAccount);
|
|
275
|
+
const ownerAta = getAssociatedTokenAddressSync(
|
|
276
|
+
mintAccount,
|
|
277
|
+
user,
|
|
278
|
+
true,
|
|
279
|
+
TOKEN_2022_PROGRAM_ID,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const accounts = {
|
|
283
|
+
user,
|
|
284
|
+
global: this.pgs.deriveWithdrawGlobalPda(),
|
|
285
|
+
mintAuthority: this.pgs.deriveWithdrawMintAuthorityPda(),
|
|
286
|
+
receiptData,
|
|
287
|
+
mintAccount,
|
|
288
|
+
ownerAta,
|
|
289
|
+
reservePool: this.pgs.deriveReservePoolPda(),
|
|
290
|
+
vault: this.pgs.deriveVaultPda(),
|
|
291
|
+
clock: SYSVAR_CLOCK_PUBKEY,
|
|
292
|
+
stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
|
|
293
|
+
globalConfig: this.pgs.deriveGlobalConfigPda(),
|
|
294
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
295
|
+
stakeProgram: StakeProgram.programId,
|
|
296
|
+
systemProgram: SystemProgram.programId,
|
|
297
|
+
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return this.program.methods.claimWithdraw().accounts(accounts).instruction();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Estimate ready time for target epoch using recent slot time.
|
|
305
|
+
*/
|
|
306
|
+
private async estimateEpochEta(targetEpoch: bigint): Promise<{ etaMs: number; readyAtMs: number }> {
|
|
307
|
+
const conn = this.connection;
|
|
308
|
+
const epochInfo = await conn.getEpochInfo();
|
|
309
|
+
const schedule = await conn.getEpochSchedule();
|
|
310
|
+
const currentEpoch = BigInt(epochInfo.epoch);
|
|
311
|
+
|
|
312
|
+
if (targetEpoch <= currentEpoch) {
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
return { etaMs: 0, readyAtMs: now };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let slotTimeSec = 0.4;
|
|
318
|
+
try {
|
|
319
|
+
const samples = await conn.getRecentPerformanceSamples(1);
|
|
320
|
+
if (samples?.length) {
|
|
321
|
+
const s = samples[0];
|
|
322
|
+
slotTimeSec = s.numSlots > 0 ? s.samplePeriodSecs / s.numSlots : slotTimeSec;
|
|
323
|
+
}
|
|
324
|
+
} catch (_) {
|
|
325
|
+
// ignore
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const slotsPerEpoch = BigInt(schedule.slotsPerEpoch);
|
|
329
|
+
const slotsRemainingInCurrent = slotsPerEpoch - BigInt(epochInfo.slotIndex);
|
|
330
|
+
const epochsRemaining = targetEpoch - currentEpoch - BigInt(1);
|
|
331
|
+
const slotsRemaining =
|
|
332
|
+
(epochsRemaining > 0 ? epochsRemaining * slotsPerEpoch : BigInt(0)) +
|
|
333
|
+
slotsRemainingInCurrent;
|
|
334
|
+
|
|
335
|
+
const etaSeconds = Number(slotsRemaining) * slotTimeSec;
|
|
336
|
+
const etaMs = Math.max(0, Math.round(etaSeconds * 1000));
|
|
337
|
+
return { etaMs, readyAtMs: Date.now() + etaMs };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
TrancheSnapshot,
|
|
43
43
|
} from '../../types';
|
|
44
44
|
|
|
45
|
-
import {
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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>;
|
|
@@ -70,6 +79,9 @@ export interface IStakingClient {
|
|
|
70
79
|
minBufferLamports?: bigint; // minimum gas buffer (default ~0.01 SOL)
|
|
71
80
|
balanceOverrideLamports?: bigint; // for tests/custom callers
|
|
72
81
|
}): Promise<bigint>
|
|
82
|
+
|
|
83
|
+
// Removed : ValidatorDeposit not required for Solana
|
|
84
|
+
// validatorDeposit(): Promise<string>
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
/**
|
|
@@ -298,9 +310,35 @@ export interface OPPAssertion {
|
|
|
298
310
|
raw: any;
|
|
299
311
|
}
|
|
300
312
|
|
|
301
|
-
|
|
302
|
-
|
|
303
313
|
export enum ReceiptNFTKind {
|
|
304
314
|
STAKE = 0,
|
|
305
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
|
+
};
|
|
306
344
|
}
|