@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.
- package/README.md +185 -243
- package/lib/stake.browser.js +302 -202
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +93 -24
- package/lib/stake.js +364 -250
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +302 -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/ethereum.ts +1 -1
- package/src/networks/ethereum/types.ts +14 -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 +39 -3
- package/src/networks/solana/clients/deposit.client.ts +0 -291
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wireio/stake",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@types/node": "^18.19.0",
|
|
65
65
|
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
|
66
66
|
"@typescript-eslint/parser": "^5.60.0",
|
|
67
|
-
"@wireio/core": "^0.3.
|
|
67
|
+
"@wireio/core": "^0.3.1",
|
|
68
68
|
"assert": "^2.0.0",
|
|
69
69
|
"chai": "^4.3.6",
|
|
70
70
|
"esbuild": "^0.25.8",
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,8 @@ export * as SOL from './networks/solana/types';
|
|
|
13
13
|
export * from './networks/solana/utils';
|
|
14
14
|
|
|
15
15
|
// CLIENTS
|
|
16
|
-
export * from './networks/solana/clients/
|
|
16
|
+
export * from './networks/solana/clients/convert.client';
|
|
17
17
|
export * from './networks/solana/clients/distribution.client';
|
|
18
18
|
export * from './networks/solana/clients/leaderboard.client';
|
|
19
19
|
export * from './networks/solana/clients/outpost.client';
|
|
20
|
-
export * from './networks/solana/clients/token.client';
|
|
20
|
+
export * from './networks/solana/clients/token.client';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BigNumber } from "ethers";
|
|
2
|
-
import { preLaunchReceipt
|
|
2
|
+
import { preLaunchReceipt } from "../types";
|
|
3
|
+
import { WithdrawReceipt, ReceiptNFTKind } from "../../../types";
|
|
3
4
|
import { EthereumContractService } from "../contract";
|
|
4
|
-
import { ReceiptNFTKind } from "../../../types";
|
|
5
5
|
|
|
6
6
|
export class ReceiptClient {
|
|
7
7
|
|
|
@@ -13,8 +13,7 @@ export class ReceiptClient {
|
|
|
13
13
|
this.contractService = contract;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// NOTE: Stake/pretoken receipts intentionally ignored for withdraw view.
|
|
18
17
|
async allReceipts(address: string): Promise<preLaunchReceipt[]> {
|
|
19
18
|
return this.fetchPreLaunchReceipts(address);
|
|
20
19
|
}
|
|
@@ -23,9 +22,7 @@ export class ReceiptClient {
|
|
|
23
22
|
try {
|
|
24
23
|
const receipts = await this.fetchPreLaunchReceipts(address, ReceiptNFTKind.STAKE);
|
|
25
24
|
return receipts;
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
// console.log('Error fetching stake receipts:', err);
|
|
25
|
+
} catch (err) {
|
|
29
26
|
return [];
|
|
30
27
|
}
|
|
31
28
|
}
|
|
@@ -34,28 +31,20 @@ export class ReceiptClient {
|
|
|
34
31
|
return this.fetchPreLaunchReceipts(address, ReceiptNFTKind.PRETOKEN_PURCHASE);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
34
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param address (string) to fetch receipts for
|
|
42
|
-
* @returns preLaunchReceipt[]
|
|
35
|
+
* Fetch ReceiptNFT data (stake/pretoken) for an address, optionally filtered by kind.
|
|
43
36
|
*/
|
|
44
37
|
async fetchPreLaunchReceipts(address: string, type?: ReceiptNFTKind): Promise<preLaunchReceipt[]> {
|
|
45
38
|
const receiptContract = this.contract.ReceiptNFT;
|
|
46
39
|
|
|
47
|
-
// first figure out which tokenIds this address owns, from events
|
|
48
40
|
const tokenIds = await this.getOwnedReceiptNFTsFor(address);
|
|
49
|
-
|
|
50
41
|
const results: preLaunchReceipt[] = [];
|
|
51
42
|
|
|
52
|
-
// next fetch on-chain receipt data just for those ids
|
|
53
43
|
for (const idBN of tokenIds) {
|
|
54
44
|
try {
|
|
55
45
|
const receiptData = await receiptContract.getReceipt(idBN);
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
if(type !== undefined && receiptData.kind !== type) continue;
|
|
47
|
+
if (type !== undefined && receiptData.kind !== type) continue;
|
|
59
48
|
|
|
60
49
|
results.push({
|
|
61
50
|
tokenId: idBN.toBigInt(),
|
|
@@ -78,7 +67,6 @@ export class ReceiptClient {
|
|
|
78
67
|
}
|
|
79
68
|
});
|
|
80
69
|
} catch (err) {
|
|
81
|
-
// in case of any mismatch or race, just skip this id
|
|
82
70
|
console.warn(`Failed to load receipt for tokenId=${idBN.toString()}`, err);
|
|
83
71
|
continue;
|
|
84
72
|
}
|
|
@@ -88,50 +76,6 @@ export class ReceiptClient {
|
|
|
88
76
|
}
|
|
89
77
|
|
|
90
78
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
private async getOwnedReceiptNFTsFor(
|
|
94
|
-
owner: string,
|
|
95
|
-
fromBlock = 0,
|
|
96
|
-
toBlock: number | string = "latest"
|
|
97
|
-
): Promise<BigNumber[]> {
|
|
98
|
-
const receiptContract = this.contract.ReceiptNFT;
|
|
99
|
-
|
|
100
|
-
// Logs where address received tokens
|
|
101
|
-
const toLogs = await receiptContract.queryFilter(
|
|
102
|
-
receiptContract.filters.Transfer(null, owner),
|
|
103
|
-
fromBlock,
|
|
104
|
-
toBlock
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// Logs where address sent tokens (including burns from owner → 0)
|
|
108
|
-
const fromLogs = await receiptContract.queryFilter(
|
|
109
|
-
receiptContract.filters.Transfer(owner, null),
|
|
110
|
-
fromBlock,
|
|
111
|
-
toBlock
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const owned = new Set<string>();
|
|
115
|
-
|
|
116
|
-
// Add all received tokenIds
|
|
117
|
-
for (const e of toLogs) {
|
|
118
|
-
const tokenId = e.args?.tokenId;
|
|
119
|
-
if (!tokenId) continue;
|
|
120
|
-
owned.add(tokenId.toString());
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Remove all sent tokenIds
|
|
124
|
-
for (const e of fromLogs) {
|
|
125
|
-
const tokenId = e.args?.tokenId;
|
|
126
|
-
if (!tokenId) continue;
|
|
127
|
-
owned.delete(tokenId.toString());
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Convert to BigNumbers
|
|
131
|
-
return Array.from(owned).map((id) => BigNumber.from(id));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
79
|
/**
|
|
136
80
|
*
|
|
137
81
|
* @param address (string) to fetch receipts for
|
|
@@ -147,16 +91,21 @@ export class ReceiptClient {
|
|
|
147
91
|
try {
|
|
148
92
|
const receiptData = await this.contract.WithdrawalQueue.info(idBN);
|
|
149
93
|
|
|
150
|
-
|
|
151
|
-
|
|
94
|
+
// status: ready if readyAt <= now; otherwise queued
|
|
95
|
+
const readyAtMs = Number(receiptData.readyAt) * 1000;
|
|
96
|
+
const status = readyAtMs <= Date.now() ? 'ready' : 'queued';
|
|
97
|
+
|
|
98
|
+
results.push({
|
|
99
|
+
tokenId: idBN.toBigInt(),
|
|
152
100
|
receipt: {
|
|
153
|
-
|
|
154
|
-
ethBalance: {
|
|
101
|
+
amount: {
|
|
155
102
|
amount: receiptData.ethAmount.toBigInt(),
|
|
156
103
|
decimals: 18,
|
|
157
|
-
symbol: "ETH"
|
|
104
|
+
symbol: "ETH",
|
|
158
105
|
},
|
|
159
|
-
readyAt:
|
|
106
|
+
readyAt: readyAtMs,
|
|
107
|
+
chain: 'ETH',
|
|
108
|
+
status,
|
|
160
109
|
}
|
|
161
110
|
});
|
|
162
111
|
} catch (err) {
|
|
@@ -211,4 +160,40 @@ export class ReceiptClient {
|
|
|
211
160
|
// Convert to BigNumbers
|
|
212
161
|
return Array.from(owned).map((id) => BigNumber.from(id));
|
|
213
162
|
}
|
|
214
|
-
|
|
163
|
+
|
|
164
|
+
private async getOwnedReceiptNFTsFor(
|
|
165
|
+
owner: string,
|
|
166
|
+
fromBlock = 0,
|
|
167
|
+
toBlock: number | string = "latest"
|
|
168
|
+
): Promise<BigNumber[]> {
|
|
169
|
+
const receiptContract = this.contract.ReceiptNFT;
|
|
170
|
+
|
|
171
|
+
const toLogs = await receiptContract.queryFilter(
|
|
172
|
+
receiptContract.filters.Transfer(null, owner),
|
|
173
|
+
fromBlock,
|
|
174
|
+
toBlock
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const fromLogs = await receiptContract.queryFilter(
|
|
178
|
+
receiptContract.filters.Transfer(owner, null),
|
|
179
|
+
fromBlock,
|
|
180
|
+
toBlock
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const owned = new Set<string>();
|
|
184
|
+
|
|
185
|
+
for (const e of toLogs) {
|
|
186
|
+
const tokenId = e.args?.tokenId;
|
|
187
|
+
if (!tokenId) continue;
|
|
188
|
+
owned.add(tokenId.toString());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const e of fromLogs) {
|
|
192
|
+
const tokenId = e.args?.tokenId;
|
|
193
|
+
if (!tokenId) continue;
|
|
194
|
+
owned.delete(tokenId.toString());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return Array.from(owned).map((id) => BigNumber.from(id));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -107,7 +107,7 @@ export class EthereumStakingClient implements IStakingClient {
|
|
|
107
107
|
* @param amount Amount in wei (or something convertible to BigNumber).
|
|
108
108
|
* @returns transaction hash
|
|
109
109
|
*/
|
|
110
|
-
async
|
|
110
|
+
async getPendingWithdraws(): Promise<WithdrawReceipt[]> {
|
|
111
111
|
this.ensureUser();
|
|
112
112
|
const address = await this.signer!.getAddress();
|
|
113
113
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BigNumber, ethers } from 'ethers';
|
|
2
|
-
import { BalanceView } from '../../types';
|
|
2
|
+
import { BalanceView, ChainSymbol, WithdrawReceipt as UnifiedWithdrawReceipt } from '../../types';
|
|
3
3
|
|
|
4
4
|
export const CONTRACT_NAMES = [
|
|
5
5
|
// LiqETH contracts
|
|
@@ -99,17 +99,20 @@ export interface SharesBurnedEvent {
|
|
|
99
99
|
tokenValue: BigNumber;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Legacy stake/pretoken receipt (kept for compatibility; not used in withdraw UI).
|
|
104
|
+
*/
|
|
102
105
|
export interface preLaunchReceipt {
|
|
103
106
|
tokenId: bigint;
|
|
104
107
|
receipt: {
|
|
105
|
-
account: string
|
|
106
|
-
currency: number
|
|
107
|
-
kind: number
|
|
108
|
-
indexAtMint: BalanceView
|
|
109
|
-
principal: BalanceView
|
|
110
|
-
shares: BalanceView
|
|
111
|
-
timestamp: string
|
|
112
|
-
}
|
|
108
|
+
account: string;
|
|
109
|
+
currency: number;
|
|
110
|
+
kind: number;
|
|
111
|
+
indexAtMint: BalanceView;
|
|
112
|
+
principal: BalanceView;
|
|
113
|
+
shares: BalanceView;
|
|
114
|
+
timestamp: string;
|
|
115
|
+
};
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
export interface ClaimedEvent {
|
|
@@ -117,14 +120,8 @@ export interface ClaimedEvent {
|
|
|
117
120
|
amount: BigNumber;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
receipt: {
|
|
123
|
-
ethAmount: BigNumber;
|
|
124
|
-
ethBalance: BalanceView;
|
|
125
|
-
readyAt: number;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
123
|
+
// Unified withdraw receipt type is defined centrally in src/types.ts
|
|
124
|
+
export type WithdrawReceipt = UnifiedWithdrawReceipt;
|
|
128
125
|
|
|
129
126
|
|
|
130
127
|
|
|
@@ -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
|
+
}
|