@wireio/stake 1.5.69 → 1.6.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wireio/stake",
3
- "version": "1.5.69",
3
+ "version": "1.6.69",
4
4
  "description": "LIQ Staking Module for Wire Network",
5
5
  "homepage": "https://gitea.gitgo.app/Wire/sdk-stake",
6
6
  "license": "FSL-1.1-Apache-2.0",
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Staker utilities
2
2
  export * from './staker';
3
3
  export * from './types';
4
+ export * from './status';
4
5
 
5
6
  // NETWORKS
6
7
  export * from './networks/ethereum/ethereum';
@@ -112,8 +112,26 @@ export class OutpostClient {
112
112
  * Internal helper: get raw token balance (BN) for a given ATA.
113
113
  */
114
114
  private async getTokenBalance(ata: PublicKey): Promise<BN> {
115
- const bal = await this.connection.getTokenAccountBalance(ata);
116
- return new BN(bal.value.amount);
115
+ try {
116
+ const bal = await this.connection.getTokenAccountBalance(ata);
117
+ return new BN(bal.value.amount);
118
+ } catch (error) {
119
+ if (this.isMissingTokenAccountError(error)) {
120
+ return new BN(0);
121
+ }
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ private isMissingTokenAccountError(error: unknown): boolean {
127
+ const message = String(
128
+ (error as { message?: string } | null)?.message ?? '',
129
+ ).toLowerCase();
130
+
131
+ return (
132
+ message.includes('failed to get token account balance') &&
133
+ message.includes('could not find account')
134
+ );
117
135
  }
118
136
 
119
137
  /**
@@ -37,7 +37,7 @@ const DEVNET_PROGRAM_IDS: SolanaProgramIds = {
37
37
  LIQSOL_TOKEN: new PublicKey(devnetLiqSolTokenIDL.address),
38
38
  VALIDATOR_LEADERBOARD: new PublicKey(devnetValidatorLeaderboardIDL.address),
39
39
  TRANSFER_HOOK: new PublicKey(devnetTransferHookIDL.address),
40
- ALT: new PublicKey("3dm6p83nqBTLnbJBFEfbHJ988y6cfKtrGSoKJbGD3hqp")
40
+ ALT: new PublicKey("EG5pouZneDQxfw5coaWkkv5qeoJkRsgyMiq4r6bw7K2F")
41
41
  };
42
42
 
43
43
  export const PROGRAM_IDS_BY_CHAIN: Record<SupportedSolChainID, SolanaProgramIds> = {
@@ -323,6 +323,85 @@ export class SolanaStakingClient implements IStakingClient {
323
323
  }
324
324
  }
325
325
 
326
+ getLiqsolDestinationOwner(owner?: SolPubKey): SolPubKey {
327
+ return owner ?? this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
328
+ }
329
+
330
+ getLiqsolDestinationAta(owner?: SolPubKey): SolPubKey {
331
+ const destinationOwner = this.getLiqsolDestinationOwner(owner);
332
+ const mint = this.program.deriveLiqsolMintPda();
333
+ return getAssociatedTokenAddressSync(
334
+ mint,
335
+ destinationOwner,
336
+ true,
337
+ TOKEN_2022_PROGRAM_ID,
338
+ ASSOCIATED_TOKEN_PROGRAM_ID,
339
+ );
340
+ }
341
+
342
+ async getLiqsolDestinationAtaState(owner?: SolPubKey): Promise<{
343
+ owner: SolPubKey;
344
+ mint: SolPubKey;
345
+ ata: SolPubKey;
346
+ exists: boolean;
347
+ }> {
348
+ const destinationOwner = this.getLiqsolDestinationOwner(owner);
349
+ const mint = this.program.deriveLiqsolMintPda();
350
+ const ata = this.getLiqsolDestinationAta(destinationOwner);
351
+ const accountInfo = await this.connection.getAccountInfo(ata, 'confirmed');
352
+
353
+ return {
354
+ owner: destinationOwner,
355
+ mint,
356
+ ata,
357
+ exists: !!accountInfo,
358
+ };
359
+ }
360
+
361
+ async maybeBuildCreateLiqsolDestinationAtaIx(
362
+ owner?: SolPubKey,
363
+ ): Promise<TransactionInstruction | null> {
364
+ const state = await this.getLiqsolDestinationAtaState(owner);
365
+ if (state.exists) {
366
+ return null;
367
+ }
368
+
369
+ return createAssociatedTokenAccountInstruction(
370
+ this.feePayer,
371
+ state.ata,
372
+ state.owner,
373
+ state.mint,
374
+ TOKEN_2022_PROGRAM_ID,
375
+ ASSOCIATED_TOKEN_PROGRAM_ID,
376
+ );
377
+ }
378
+
379
+ async ensureLiqsolDestinationAta(owner?: SolPubKey): Promise<string | null> {
380
+ this.ensureUser();
381
+ const createAtaIx = await this.maybeBuildCreateLiqsolDestinationAtaIx(owner);
382
+ if (!createAtaIx) {
383
+ return null;
384
+ }
385
+
386
+ try {
387
+ return await this.buildAndSendIx(createAtaIx);
388
+ } catch (error) {
389
+ if (!this.isAtaAlreadyCreatedError(error)) {
390
+ throw error;
391
+ }
392
+ return null;
393
+ }
394
+ }
395
+
396
+ private async prependLiqsolDestinationAtaIx(
397
+ ix: TransactionInstruction | TransactionInstruction[],
398
+ owner?: SolPubKey,
399
+ ): Promise<TransactionInstruction[]> {
400
+ const ixs = Array.isArray(ix) ? [...ix] : [ix];
401
+ const createAtaIx = await this.maybeBuildCreateLiqsolDestinationAtaIx(owner);
402
+ return createAtaIx ? [createAtaIx, ...ixs] : ixs;
403
+ }
404
+
326
405
  /**
327
406
  * Claim accrued liqSOL distribution rewards (liqsol_core::claim_rewards).
328
407
  */
@@ -332,9 +411,10 @@ export class SolanaStakingClient implements IStakingClient {
332
411
 
333
412
  try {
334
413
  const ix = await this.distributionClient.buildClaimRewardsIx(owner);
414
+ const ixs = await this.prependLiqsolDestinationAtaIx(ix, owner);
335
415
  return !!this.squadsX
336
- ? await this.sendSquadsIxs(ix)
337
- : await this.buildAndSendIx(ix);
416
+ ? await this.sendSquadsIxs(ixs)
417
+ : await this.buildAndSendIx(ixs);
338
418
  } catch (err) {
339
419
  console.log(`Failed to claim liqSOL rewards on Solana: ${err}`);
340
420
  throw err;
@@ -374,10 +454,12 @@ export class SolanaStakingClient implements IStakingClient {
374
454
  throw new Error('Unstake amount must be greater than zero.');
375
455
 
376
456
  try {
457
+ const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
377
458
  const ix = await this.outpostClient.buildUnstakeIx(amountLamports, this.squadsVaultPDA)
459
+ const ixs = await this.prependLiqsolDestinationAtaIx(ix, owner);
378
460
  return !!this.squadsX
379
- ? await this.sendSquadsIxs(ix)
380
- : await this.buildAndSendIx(ix)
461
+ ? await this.sendSquadsIxs(ixs)
462
+ : await this.buildAndSendIx(ixs)
381
463
  }
382
464
  catch (err) {
383
465
  console.log(`Failed to unstake Solana: ${err}`);
@@ -462,10 +544,11 @@ export class SolanaStakingClient implements IStakingClient {
462
544
  const user = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
463
545
  const unstakeIx = await this.outpostClient.buildUnstakeIx(amountLamports, user);
464
546
  const withdrawIx = await this.convertClient.buildWithdrawTx(amountLamports, user);
547
+ const ixs = await this.prependLiqsolDestinationAtaIx([unstakeIx, withdrawIx], user);
465
548
 
466
549
  return !!this.squadsX
467
- ? await this.sendSquadsIxs([unstakeIx, withdrawIx])
468
- : await this.buildAndSendIx([unstakeIx, withdrawIx]);
550
+ ? await this.sendSquadsIxs(ixs)
551
+ : await this.buildAndSendIx(ixs);
469
552
  } catch (err) {
470
553
  console.log(`Failed to unstake and withdraw: ${err}`);
471
554
  throw err;
@@ -897,6 +980,13 @@ export class SolanaStakingClient implements IStakingClient {
897
980
  || normalized.includes('custom program error: 0x1784');
898
981
  }
899
982
 
983
+ private isAtaAlreadyCreatedError(error: unknown): boolean {
984
+ const message = error instanceof Error ? error.message : String(error ?? '');
985
+ const normalized = message.toLowerCase();
986
+ return normalized.includes('already in use')
987
+ || normalized.includes('already exists');
988
+ }
989
+
900
990
  // ---------------------------------------------------------------------
901
991
  // READ-ONLY Public Methods
902
992
  // ---------------------------------------------------------------------
package/src/status.ts ADDED
@@ -0,0 +1,301 @@
1
+ import { Connection } from '@solana/web3.js';
2
+ import { BigNumber, ethers } from 'ethers';
3
+
4
+ import { EthereumStakingClient } from './networks/ethereum/ethereum';
5
+ import { formatContractErrors, resolveContractWriteOverrides } from './networks/ethereum/utils';
6
+ import { SolanaStakingClient } from './networks/solana/solana';
7
+
8
+ export type StakingTxChain = 'eth' | 'sol';
9
+ export type StakingTxState = 'pending' | 'confirmed' | 'failed' | 'not_found';
10
+
11
+ export interface StakingTransactionStatus {
12
+ chain: StakingTxChain;
13
+ txId: string;
14
+ state: StakingTxState;
15
+ timestampMs: number | null;
16
+ blockNumber?: number;
17
+ slot?: number;
18
+ confirmations?: number | null;
19
+ errorMessage?: string;
20
+ }
21
+
22
+ export interface EthereumSubmitStepResult {
23
+ stepId: 'deposit' | 'approve' | 'source';
24
+ label: string;
25
+ txId: string;
26
+ timestampMs: number | null;
27
+ }
28
+
29
+ export interface EthereumStakeSubmission {
30
+ finalTxId: string;
31
+ steps: EthereumSubmitStepResult[];
32
+ }
33
+
34
+ type EthereumLikeProvider =
35
+ | ethers.providers.Web3Provider
36
+ | ethers.providers.JsonRpcProvider;
37
+
38
+ export async function getEthereumTransactionStatus(
39
+ source: EthereumStakingClient | EthereumLikeProvider,
40
+ txId: string,
41
+ ): Promise<StakingTransactionStatus> {
42
+ const provider = isEthereumStakingClient(source)
43
+ ? getEthereumProvider(source)
44
+ : source;
45
+ const tx = await provider.getTransaction(txId).catch(() => null);
46
+ const receipt = await provider.getTransactionReceipt(txId).catch(() => null);
47
+
48
+ if (!tx && !receipt) {
49
+ return {
50
+ chain: 'eth',
51
+ txId,
52
+ state: 'not_found',
53
+ timestampMs: null,
54
+ };
55
+ }
56
+
57
+ if (!receipt) {
58
+ return {
59
+ chain: 'eth',
60
+ txId,
61
+ state: 'pending',
62
+ timestampMs: null,
63
+ confirmations: tx?.confirmations ?? null,
64
+ };
65
+ }
66
+
67
+ const block = receipt.blockNumber != null
68
+ ? await provider.getBlock(receipt.blockNumber).catch(() => null)
69
+ : null;
70
+
71
+ return {
72
+ chain: 'eth',
73
+ txId,
74
+ state: receipt.status === 0 ? 'failed' : 'confirmed',
75
+ timestampMs: block?.timestamp ? block.timestamp * 1000 : null,
76
+ blockNumber: receipt.blockNumber,
77
+ confirmations: tx?.confirmations ?? null,
78
+ };
79
+ }
80
+
81
+ export async function getSolanaTransactionStatus(
82
+ source: SolanaStakingClient | Connection,
83
+ txId: string,
84
+ ): Promise<StakingTransactionStatus> {
85
+ const connection = isSolanaStakingClient(source)
86
+ ? source.connection
87
+ : source;
88
+
89
+ const statusResponse = await connection.getSignatureStatuses(
90
+ [txId],
91
+ { searchTransactionHistory: true },
92
+ ).catch(() => ({ value: [null] }));
93
+ const status = statusResponse.value[0];
94
+ const transaction = await connection.getTransaction(txId, {
95
+ commitment: 'confirmed',
96
+ maxSupportedTransactionVersion: 0,
97
+ }).catch(() => null);
98
+
99
+ if (!status && !transaction) {
100
+ return {
101
+ chain: 'sol',
102
+ txId,
103
+ state: 'not_found',
104
+ timestampMs: null,
105
+ };
106
+ }
107
+
108
+ const errorMessage = status?.err ? JSON.stringify(status.err) : undefined;
109
+ const isConfirmed = !!transaction || status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized';
110
+
111
+ return {
112
+ chain: 'sol',
113
+ txId,
114
+ state: status?.err ? 'failed' : isConfirmed ? 'confirmed' : 'pending',
115
+ timestampMs: transaction?.blockTime ? transaction.blockTime * 1000 : null,
116
+ slot: transaction?.slot ?? status?.slot,
117
+ confirmations: status?.confirmations ?? null,
118
+ errorMessage,
119
+ };
120
+ }
121
+
122
+ export async function getStakingClientTransactionStatus(
123
+ client: EthereumStakingClient | SolanaStakingClient,
124
+ txId: string,
125
+ ): Promise<StakingTransactionStatus> {
126
+ if (isEthereumStakingClient(client)) {
127
+ return getEthereumTransactionStatus(client, txId);
128
+ }
129
+ return getSolanaTransactionStatus(client, txId);
130
+ }
131
+
132
+ export async function submitEthereumStakeToWireFlow(
133
+ client: EthereumStakingClient,
134
+ amount: bigint | string | number | BigNumber,
135
+ wireAccount: string,
136
+ ): Promise<EthereumStakeSubmission> {
137
+ ensureEthereumUser(client);
138
+
139
+ const amountWei = BigNumber.isBigNumber(amount)
140
+ ? amount
141
+ : BigNumber.from(amount);
142
+ const signerAddress = await client.address;
143
+ if (!signerAddress) {
144
+ throw new Error('Ethereum signer address is unavailable.');
145
+ }
146
+
147
+ const contractService = getEthereumContractService(client);
148
+ const depositor = client.contract.Depositor.address;
149
+ const liqRead = client.contract.LiqEthToken;
150
+ const liqWrite = contractService.getWrite('LiqEthToken');
151
+ const steps: EthereumSubmitStepResult[] = [];
152
+
153
+ const [balance, allowance, paused] = await Promise.all([
154
+ liqRead.balanceOf(signerAddress),
155
+ liqRead.allowance(signerAddress, depositor),
156
+ client.contract.Depositor.paused(),
157
+ ]);
158
+
159
+ if (paused) {
160
+ throw new Error('StakeClient.performStakeToWire: Depositor is in a paused state');
161
+ }
162
+ if (!wireAccount?.trim()) {
163
+ throw new Error('StakeClient.performStakeToWire: wireAccount is required');
164
+ }
165
+ if (balance.lt(amountWei)) {
166
+ throw new Error('StakeClient.performStakeToWire: Insufficient LiqETH balance');
167
+ }
168
+
169
+ if (allowance.lt(amountWei)) {
170
+ const approveTx = await liqWrite.approve(depositor, amountWei);
171
+ const approveReceipt = await approveTx.wait(1);
172
+ const updatedAllowance = await liqRead.allowance(signerAddress, depositor);
173
+ if (updatedAllowance.lt(amountWei)) {
174
+ throw new Error('StakeClient.performStakeToWire: Liq allowance approval failed or allowance still insufficient after approve');
175
+ }
176
+ steps.push({
177
+ stepId: 'approve',
178
+ label: 'Approve LIQETH spend',
179
+ txId: approveTx.hash,
180
+ timestampMs: approveReceipt?.blockNumber != null
181
+ ? await getEthereumBlockTimestampMs(getEthereumProvider(client), approveReceipt.blockNumber)
182
+ : null,
183
+ });
184
+ }
185
+
186
+ try {
187
+ await client.contract.Depositor.callStatic.stakeLiqETHToWire(amountWei, wireAccount);
188
+ } catch (error: any) {
189
+ const formatted = formatContractErrors(error);
190
+ throw new Error(`StakeClient.performStakeToWire: ${formatted.name ?? formatted.raw}`);
191
+ }
192
+
193
+ const txOverrides = await resolveContractWriteOverrides(
194
+ client.contract.Depositor,
195
+ 'stakeLiqETHToWire',
196
+ [amountWei, wireAccount],
197
+ );
198
+ const stakeTx = await client.contract.Depositor.stakeLiqETHToWire(
199
+ amountWei,
200
+ wireAccount,
201
+ txOverrides,
202
+ );
203
+ const stakeReceipt = await stakeTx.wait(1);
204
+ steps.push({
205
+ stepId: 'source',
206
+ label: 'Stake LIQETH to Wire',
207
+ txId: stakeTx.hash,
208
+ timestampMs: stakeReceipt?.blockNumber != null
209
+ ? await getEthereumBlockTimestampMs(getEthereumProvider(client), stakeReceipt.blockNumber)
210
+ : null,
211
+ });
212
+
213
+ return {
214
+ finalTxId: stakeTx.hash,
215
+ steps,
216
+ };
217
+ }
218
+
219
+ export async function submitEthereumDepositAndStakeToWireFlow(
220
+ client: EthereumStakingClient,
221
+ amount: bigint | string | number | BigNumber,
222
+ wireAccount: string,
223
+ ): Promise<EthereumStakeSubmission> {
224
+ ensureEthereumUser(client);
225
+
226
+ const amountWei = BigNumber.isBigNumber(amount)
227
+ ? amount
228
+ : BigNumber.from(amount);
229
+ if (amountWei.lte(0)) {
230
+ throw new Error('Amount must be greater than zero.');
231
+ }
232
+
233
+ const convertClient = getEthereumConvertClient(client);
234
+ const depositResult = await convertClient.performDeposit(amountWei);
235
+ const steps: EthereumSubmitStepResult[] = [{
236
+ stepId: 'deposit',
237
+ label: 'Deposit native ETH',
238
+ txId: depositResult.txHash,
239
+ timestampMs: depositResult.receipt?.blockNumber != null
240
+ ? await getEthereumBlockTimestampMs(getEthereumProvider(client), depositResult.receipt.blockNumber)
241
+ : null,
242
+ }];
243
+
244
+ const stake = await submitEthereumStakeToWireFlow(client, amountWei, wireAccount);
245
+ return {
246
+ finalTxId: stake.finalTxId,
247
+ steps: [...steps, ...stake.steps],
248
+ };
249
+ }
250
+
251
+ function isEthereumStakingClient(value: unknown): value is EthereumStakingClient {
252
+ return value instanceof EthereumStakingClient;
253
+ }
254
+
255
+ function isSolanaStakingClient(value: unknown): value is SolanaStakingClient {
256
+ return value instanceof SolanaStakingClient;
257
+ }
258
+
259
+ function getEthereumProvider(client: EthereumStakingClient): EthereumLikeProvider {
260
+ const provider = (client as unknown as { provider?: EthereumLikeProvider }).provider;
261
+ if (!provider) {
262
+ throw new Error('Ethereum provider is unavailable.');
263
+ }
264
+ return provider;
265
+ }
266
+
267
+ function getEthereumContractService(client: EthereumStakingClient): {
268
+ getWrite: (name: string) => any;
269
+ } {
270
+ const contractService = (client as unknown as { contractService?: { getWrite: (name: string) => any } }).contractService;
271
+ if (!contractService) {
272
+ throw new Error('Ethereum contract service is unavailable.');
273
+ }
274
+ return contractService;
275
+ }
276
+
277
+ function getEthereumConvertClient(client: EthereumStakingClient): {
278
+ performDeposit: (amountWei: BigNumber) => Promise<{
279
+ txHash: string;
280
+ receipt?: { blockNumber?: number };
281
+ }>;
282
+ } {
283
+ const convertClient = (client as unknown as { convertClient?: any }).convertClient;
284
+ if (!convertClient?.performDeposit) {
285
+ throw new Error('Ethereum convert client is unavailable.');
286
+ }
287
+ return convertClient;
288
+ }
289
+
290
+ function ensureEthereumUser(client: EthereumStakingClient): void {
291
+ const ensureUser = (client as unknown as { ensureUser?: () => void }).ensureUser;
292
+ ensureUser?.call(client);
293
+ }
294
+
295
+ async function getEthereumBlockTimestampMs(
296
+ provider: EthereumLikeProvider,
297
+ blockNumber: number,
298
+ ): Promise<number | null> {
299
+ const block = await provider.getBlock(blockNumber).catch(() => null);
300
+ return block?.timestamp ? block.timestamp * 1000 : null;
301
+ }