@soltracer/nft-staking 0.2.1 → 0.2.4
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/INTEGRATION.md +213 -74
- package/dist/client.d.ts +157 -31
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +624 -132
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +60 -0
- package/dist/errors.js.map +1 -0
- package/dist/helpers.d.ts +50 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +88 -0
- package/dist/helpers.js.map +1 -0
- package/dist/idl.d.ts +3252 -1457
- package/dist/idl.d.ts.map +1 -1
- package/dist/idl.json +3252 -1457
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/merkle.d.ts +57 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +110 -0
- package/dist/merkle.js.map +1 -0
- package/dist/traitProof.d.ts +61 -0
- package/dist/traitProof.d.ts.map +1 -0
- package/dist/traitProof.js +72 -0
- package/dist/traitProof.js.map +1 -0
- package/package.json +4 -2
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BN, Program } from "@coral-xyz/anchor";
|
|
2
2
|
import { PublicKey, SystemProgram } from "@solana/web3.js";
|
|
3
|
-
import { getStakeConfigPda, getStakePoolPda, getStakeEntryPda, getStakerAccountPda, getCollectionPda, getPoolAuthorityPda, getPoolSecondaryRewardsPda, getStakerSecondaryRewardsPda, getProjectPda, getUtilityConfigPda,
|
|
3
|
+
import { getStakeConfigPda, getStakePoolPda, getStakeEntryPda, getNftStakeLockPda, getStakerAccountPda, getUserProfilePda, getCollectionPda, getPoolAuthorityPda, getPoolSecondaryRewardsPda, getStakerSecondaryRewardsPda, getProjectPda, getUtilityConfigPda, getAta, decodeAccount, PROJECT_MANAGEMENT_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, MPL_CORE_PROGRAM_ID, toPk, toBN, } from "@soltracer/core";
|
|
4
|
+
import { ProjectReferralCache, resolveFeeAccounts, } from "@soltracer/cpi-accounts";
|
|
4
5
|
import NftStakingIDL from "./idl.json";
|
|
5
6
|
/** Well-known program IDs for cNFT operations. */
|
|
6
7
|
const BUBBLEGUM_PROGRAM_ID = new PublicKey("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY");
|
|
@@ -22,6 +23,23 @@ function getEditionPda(mint) {
|
|
|
22
23
|
], TOKEN_METADATA_PROGRAM_ID);
|
|
23
24
|
return pda;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Derive the Token Record PDA for a programmable NFT (pNFT). The token
|
|
28
|
+
* record exists per (mint, tokenAccount) pair and is required as a
|
|
29
|
+
* mutable account for pNFT lock/unlock CPIs.
|
|
30
|
+
*
|
|
31
|
+
* seeds = ["metadata", MPL_TOKEN_METADATA, mint, "token_record", tokenAccount]
|
|
32
|
+
*/
|
|
33
|
+
function getTokenRecordPda(mint, tokenAccount) {
|
|
34
|
+
const [pda] = PublicKey.findProgramAddressSync([
|
|
35
|
+
new TextEncoder().encode("metadata"),
|
|
36
|
+
TOKEN_METADATA_PROGRAM_ID.toBytes(),
|
|
37
|
+
mint.toBytes(),
|
|
38
|
+
new TextEncoder().encode("token_record"),
|
|
39
|
+
tokenAccount.toBytes(),
|
|
40
|
+
], TOKEN_METADATA_PROGRAM_ID);
|
|
41
|
+
return pda;
|
|
42
|
+
}
|
|
25
43
|
/** Sysvar Instructions address. */
|
|
26
44
|
const SYSVAR_INSTRUCTIONS_ID = new PublicKey("Sysvar1nstructions1111111111111111111111111");
|
|
27
45
|
/** Decode a base58-encoded 32-byte hash into a number array. */
|
|
@@ -32,11 +50,45 @@ function base58HashToArray(hash) {
|
|
|
32
50
|
function getTreeConfigPda(merkleTree) {
|
|
33
51
|
return PublicKey.findProgramAddressSync([merkleTree.toBytes()], BUBBLEGUM_PROGRAM_ID);
|
|
34
52
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
53
|
+
function normalizeQuantityBonus(input) {
|
|
54
|
+
const bonusType = input.bonusType ?? 0;
|
|
55
|
+
return {
|
|
56
|
+
minCount: input.minCount,
|
|
57
|
+
bonusType,
|
|
58
|
+
target: input.target ?? 0,
|
|
59
|
+
bonusBps: input.bonusBps ?? 0,
|
|
60
|
+
fixedAmount: toBN(input.fixedAmount ?? 0),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function amountWithDecimals(raw, decimals) {
|
|
64
|
+
const divisor = 10 ** decimals;
|
|
65
|
+
return { raw, ui: decimals > 0 ? raw / divisor : raw, decimals };
|
|
37
66
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
67
|
+
function applyQuantityBonusRaw(reward, thresholds, stakedCount, target) {
|
|
68
|
+
if (reward <= 0)
|
|
69
|
+
return 0;
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const threshold of thresholds) {
|
|
72
|
+
if ((threshold.target ?? 0) !== target || stakedCount < threshold.minCount)
|
|
73
|
+
continue;
|
|
74
|
+
if (!best) {
|
|
75
|
+
best = threshold;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const bestFixed = Number(best.fixedAmount ?? 0);
|
|
79
|
+
const nextFixed = Number(threshold.fixedAmount ?? 0);
|
|
80
|
+
if (threshold.minCount > best.minCount ||
|
|
81
|
+
(threshold.minCount === best.minCount &&
|
|
82
|
+
nextFixed >= bestFixed &&
|
|
83
|
+
(threshold.bonusBps ?? 0) >= (best.bonusBps ?? 0))) {
|
|
84
|
+
best = threshold;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!best)
|
|
88
|
+
return reward;
|
|
89
|
+
if ((best.bonusType ?? 0) === 1)
|
|
90
|
+
return reward + Number(best.fixedAmount ?? 0);
|
|
91
|
+
return Math.floor((reward * (10_000 + (best.bonusBps ?? 0))) / 10_000);
|
|
40
92
|
}
|
|
41
93
|
export class NftStakingClient {
|
|
42
94
|
program;
|
|
@@ -44,7 +96,7 @@ export class NftStakingClient {
|
|
|
44
96
|
tokenProgramCache = new Map();
|
|
45
97
|
mintDecimalsCache = new Map();
|
|
46
98
|
poolCache = new Map();
|
|
47
|
-
static POOL_CACHE_TTL = 30_000;
|
|
99
|
+
static POOL_CACHE_TTL = 30_000;
|
|
48
100
|
/** Project ID bound to this client. Set via `create()` options or `setProjectId()`. */
|
|
49
101
|
projectId;
|
|
50
102
|
constructor(program, provider, projectId) {
|
|
@@ -85,6 +137,13 @@ export class NftStakingClient {
|
|
|
85
137
|
this.poolCache.set(key, { data: pool, fetchedAt: Date.now() });
|
|
86
138
|
return pool;
|
|
87
139
|
}
|
|
140
|
+
async fetchRawStakePool(projectId, poolId) {
|
|
141
|
+
const [pda] = getStakePoolPda(projectId, poolId);
|
|
142
|
+
const raw = await this.program.account.stakePool.fetchNullable(pda);
|
|
143
|
+
if (!raw)
|
|
144
|
+
return null;
|
|
145
|
+
return decodeAccount(raw);
|
|
146
|
+
}
|
|
88
147
|
/** Invalidate cached pool data (call after pool mutations). */
|
|
89
148
|
invalidatePoolCache(poolId) {
|
|
90
149
|
if (poolId !== undefined) {
|
|
@@ -104,17 +163,38 @@ export class NftStakingClient {
|
|
|
104
163
|
if (cached)
|
|
105
164
|
return cached;
|
|
106
165
|
const acct = await this.provider.connection.getAccountInfo(mint);
|
|
107
|
-
const result = acct?.owner.equals(TOKEN_2022_PROGRAM_ID)
|
|
166
|
+
const result = acct?.owner.equals(TOKEN_2022_PROGRAM_ID)
|
|
167
|
+
? TOKEN_2022_PROGRAM_ID
|
|
168
|
+
: TOKEN_PROGRAM_ID;
|
|
108
169
|
this.tokenProgramCache.set(key, result);
|
|
109
170
|
return result;
|
|
110
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Resolve programmable-NFT token-record PDAs for a (mint, source, dest)
|
|
174
|
+
* triple. Detection: derive the SOURCE token record PDA and probe the
|
|
175
|
+
* account on-chain. A token-record account only exists for pNFTs, so its
|
|
176
|
+
* presence is a reliable, version-agnostic detector (and is what the
|
|
177
|
+
* mpl-token-metadata clients use). Returns `{ tokenRecord: null,
|
|
178
|
+
* destinationTokenRecord: null }` for legacy NFTs, otherwise the pair of
|
|
179
|
+
* PDAs. `destination` may be null for transfer-mode stakes (no escrow).
|
|
180
|
+
*/
|
|
181
|
+
async resolvePNftTokenRecords(mint, source, destination) {
|
|
182
|
+
const sourceRecord = getTokenRecordPda(mint, source);
|
|
183
|
+
const info = await this.provider.connection.getAccountInfo(sourceRecord);
|
|
184
|
+
if (!info) {
|
|
185
|
+
return { tokenRecord: null, destinationTokenRecord: null };
|
|
186
|
+
}
|
|
187
|
+
const destinationRecord = destination ? getTokenRecordPda(mint, destination) : null;
|
|
188
|
+
return { tokenRecord: sourceRecord, destinationTokenRecord: destinationRecord };
|
|
189
|
+
}
|
|
111
190
|
/** Fetch the decimal precision of a token mint (cached). */
|
|
112
191
|
async getMintDecimals(mint) {
|
|
113
|
-
const
|
|
192
|
+
const _mint = toPk(mint);
|
|
193
|
+
const key = _mint.toBase58();
|
|
114
194
|
const cached = this.mintDecimalsCache.get(key);
|
|
115
195
|
if (cached !== undefined)
|
|
116
196
|
return cached;
|
|
117
|
-
const acct = await this.provider.connection.getAccountInfo(
|
|
197
|
+
const acct = await this.provider.connection.getAccountInfo(_mint);
|
|
118
198
|
const decimals = acct ? (acct.data[44] ?? 0) : 0;
|
|
119
199
|
this.mintDecimalsCache.set(key, decimals);
|
|
120
200
|
return decimals;
|
|
@@ -136,30 +216,17 @@ export class NftStakingClient {
|
|
|
136
216
|
})),
|
|
137
217
|
};
|
|
138
218
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
/** Resolve fee PDA accounts for fee CPI. */
|
|
142
|
-
resolveFeeAccounts(fee) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
referralAccount = new PublicKey(fee.referralAccount);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
feeConfig,
|
|
157
|
-
programRegistry,
|
|
158
|
-
treasury,
|
|
159
|
-
referralAccount,
|
|
160
|
-
solUsdPriceFeed: NftStakingClient.PYTH_SOL_USD_FEED,
|
|
161
|
-
adminProgram: ADMIN_PROGRAM_ID,
|
|
162
|
-
};
|
|
219
|
+
/** Cache of project_id -> referral pubkey (or null = no referral). */
|
|
220
|
+
projectReferralCache = new ProjectReferralCache();
|
|
221
|
+
/** Resolve fee PDA accounts for fee CPI. Auto-supplies referral from project if not provided. */
|
|
222
|
+
async resolveFeeAccounts(pid, fee) {
|
|
223
|
+
return resolveFeeAccounts({
|
|
224
|
+
connection: this.provider.connection,
|
|
225
|
+
utilityProgramId: this.program.programId,
|
|
226
|
+
projectId: pid,
|
|
227
|
+
fee,
|
|
228
|
+
referralCache: this.projectReferralCache,
|
|
229
|
+
});
|
|
163
230
|
}
|
|
164
231
|
async fetchStakeConfig(projectId) {
|
|
165
232
|
const pid = this.resolveProjectId(projectId);
|
|
@@ -179,15 +246,17 @@ export class NftStakingClient {
|
|
|
179
246
|
const decimals = await this.getMintDecimals(new PublicKey(pool.rewardConfig.rewardMint));
|
|
180
247
|
return this.applyPoolDecimals(pool, decimals);
|
|
181
248
|
}
|
|
182
|
-
async fetchStakeEntry(poolId, nftMint) {
|
|
183
|
-
const
|
|
249
|
+
async fetchStakeEntry(poolId, nftMint, projectId) {
|
|
250
|
+
const pid = this.resolveProjectId(projectId);
|
|
251
|
+
const [pda] = getStakeEntryPda(pid, poolId, toPk(nftMint));
|
|
184
252
|
const raw = await this.program.account.stakeEntry.fetchNullable(pda);
|
|
185
253
|
if (!raw)
|
|
186
254
|
return null;
|
|
187
255
|
return decodeAccount(raw);
|
|
188
256
|
}
|
|
189
|
-
async fetchStakerAccount(poolId, wallet, rewardDecimals) {
|
|
190
|
-
const
|
|
257
|
+
async fetchStakerAccount(poolId, wallet, rewardDecimals, projectId) {
|
|
258
|
+
const pid = this.resolveProjectId(projectId);
|
|
259
|
+
const [pda] = getStakerAccountPda(pid, poolId, toPk(wallet));
|
|
191
260
|
const raw = await this.program.account.stakerAccount.fetchNullable(pda);
|
|
192
261
|
if (!raw)
|
|
193
262
|
return null;
|
|
@@ -218,23 +287,224 @@ export class NftStakingClient {
|
|
|
218
287
|
return pools;
|
|
219
288
|
}
|
|
220
289
|
/** Fetch all active stake entries for a wallet in a specific pool via gPA. */
|
|
221
|
-
async fetchStakeEntriesByOwner(
|
|
290
|
+
async fetchStakeEntriesByOwner(poolId, owner, projectId) {
|
|
291
|
+
const pid = this.resolveProjectId(projectId);
|
|
222
292
|
const _owner = toPk(owner);
|
|
293
|
+
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
223
294
|
const entries = await this.program.account.stakeEntry.all([
|
|
224
295
|
{ memcmp: { offset: 72, bytes: _owner.toBase58() } },
|
|
225
296
|
]);
|
|
226
297
|
return entries
|
|
227
298
|
.map(({ account }) => decodeAccount(account))
|
|
228
|
-
.filter((e) => e.isActive);
|
|
299
|
+
.filter((e) => e.isActive && new PublicKey(e.pool).equals(poolPda));
|
|
300
|
+
}
|
|
301
|
+
async fetchTokenAccountAmount(account) {
|
|
302
|
+
try {
|
|
303
|
+
const balance = await this.provider.connection.getTokenAccountBalance(account);
|
|
304
|
+
return Number(balance.value.amount);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
isEntryClaimDeferred(pool, entry, now) {
|
|
311
|
+
if (entry.lockTierIndex === 255 || entry.lockExpiresAt <= 0 || now >= entry.lockExpiresAt) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return Boolean(pool.lockConfigs[entry.lockTierIndex]?.claimOnlyAtEnd);
|
|
315
|
+
}
|
|
316
|
+
splitAccruedByRates(accrued, claimableRate, totalRate) {
|
|
317
|
+
if (accrued <= 0)
|
|
318
|
+
return [0, 0];
|
|
319
|
+
if (totalRate <= 0)
|
|
320
|
+
return [accrued, 0];
|
|
321
|
+
const claimable = Math.floor((accrued * claimableRate) / totalRate);
|
|
322
|
+
return [claimable, accrued - claimable];
|
|
323
|
+
}
|
|
324
|
+
pendingFromRate(rate, interval, rewardEndAt, lastUpdateAt, now) {
|
|
325
|
+
if (rate <= 0 || interval <= 0)
|
|
326
|
+
return 0;
|
|
327
|
+
const effectiveNow = rewardEndAt > 0 && now > rewardEndAt ? rewardEndAt : now;
|
|
328
|
+
const elapsed = Math.max(0, effectiveNow - lastUpdateAt);
|
|
329
|
+
return Math.floor((elapsed * rate) / interval);
|
|
330
|
+
}
|
|
331
|
+
resolveSecondaryDecimals(mint, index, overrides) {
|
|
332
|
+
if (!overrides)
|
|
333
|
+
return undefined;
|
|
334
|
+
if (Array.isArray(overrides))
|
|
335
|
+
return overrides[index];
|
|
336
|
+
return overrides[mint];
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Build a display-ready reward summary for a staker in one pool.
|
|
340
|
+
*
|
|
341
|
+
* Current on-chain primary and secondary claims are still blocked while
|
|
342
|
+
* `claimLockedUntil` is active. This helper partitions estimated accruals
|
|
343
|
+
* into claimable versus claim-at-end buckets from active StakeEntry rates so
|
|
344
|
+
* UIs can show what is available now and what remains locked.
|
|
345
|
+
*/
|
|
346
|
+
async fetchStakerRewardSummary(poolId, wallet, opts = {}) {
|
|
347
|
+
const pid = this.resolveProjectId(opts.projectId);
|
|
348
|
+
const _wallet = toPk(wallet);
|
|
349
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
350
|
+
const pool = await this.fetchRawStakePool(pid, poolId);
|
|
351
|
+
if (!pool)
|
|
352
|
+
return null;
|
|
353
|
+
const rewardMint = new PublicKey(pool.rewardConfig.rewardMint);
|
|
354
|
+
const rewardDecimals = opts.rewardDecimals ?? (await this.getMintDecimals(rewardMint));
|
|
355
|
+
const staker = await this.fetchStakerAccount(poolId, _wallet, undefined, pid);
|
|
356
|
+
const entries = opts.entries ?? (await this.fetchStakeEntriesByOwner(poolId, _wallet, pid));
|
|
357
|
+
const activeEntries = entries.filter((entry) => entry.isActive);
|
|
358
|
+
const claimableEntries = activeEntries.filter((entry) => !this.isEntryClaimDeferred(pool, entry, now));
|
|
359
|
+
const unclaimableEntries = activeEntries.filter((entry) => this.isEntryClaimDeferred(pool, entry, now));
|
|
360
|
+
const claimableRate = claimableEntries.reduce((sum, entry) => sum + entry.rateContribution, 0);
|
|
361
|
+
const unclaimableRate = unclaimableEntries.reduce((sum, entry) => sum + entry.rateContribution, 0);
|
|
362
|
+
const totalRate = claimableRate + unclaimableRate;
|
|
363
|
+
const stakerRate = staker?.effectiveRate ?? 0;
|
|
364
|
+
const settledPrimary = staker?.accruedRewards ?? 0;
|
|
365
|
+
const [claimableSettledPrimary, unclaimableSettledPrimary] = this.splitAccruedByRates(settledPrimary, claimableRate, totalRate || stakerRate);
|
|
366
|
+
const pendingClaimablePrimary = staker
|
|
367
|
+
? this.pendingFromRate(claimableRate, pool.rewardConfig.rateInterval, pool.rewardEndAt, staker.lastUpdateAt, now)
|
|
368
|
+
: 0;
|
|
369
|
+
const pendingUnclaimablePrimary = staker
|
|
370
|
+
? this.pendingFromRate(unclaimableRate, pool.rewardConfig.rateInterval, pool.rewardEndAt, staker.lastUpdateAt, now)
|
|
371
|
+
: 0;
|
|
372
|
+
const claimableBasePrimary = claimableSettledPrimary + pendingClaimablePrimary;
|
|
373
|
+
const unclaimableBasePrimary = unclaimableSettledPrimary + pendingUnclaimablePrimary;
|
|
374
|
+
const quantityThresholds = pool.rewardConfig.quantityThresholds;
|
|
375
|
+
const stakedCount = staker?.stakedCount ?? activeEntries.length;
|
|
376
|
+
const claimablePayoutPrimary = applyQuantityBonusRaw(claimableBasePrimary, quantityThresholds, stakedCount, 0);
|
|
377
|
+
const unclaimablePayoutPrimary = applyQuantityBonusRaw(unclaimableBasePrimary, quantityThresholds, stakedCount, 0);
|
|
378
|
+
let primaryVaultBalance;
|
|
379
|
+
if (opts.includeVaultBalances) {
|
|
380
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
381
|
+
const tokenProgram = await this.resolveTokenProgram(rewardMint);
|
|
382
|
+
const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
|
|
383
|
+
const rawBalance = await this.fetchTokenAccountAmount(rewardVault);
|
|
384
|
+
primaryVaultBalance =
|
|
385
|
+
rawBalance == null ? null : amountWithDecimals(rawBalance, rewardDecimals);
|
|
386
|
+
}
|
|
387
|
+
const secondary = [];
|
|
388
|
+
const poolSecondary = await this.fetchPoolSecondaryRewards(poolId, pid);
|
|
389
|
+
const stakerSecondary = await this.fetchStakerSecondaryRewards(poolId, _wallet, pid);
|
|
390
|
+
if (poolSecondary && stakerSecondary) {
|
|
391
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
392
|
+
for (let index = 0; index < poolSecondary.rewards.length; index++) {
|
|
393
|
+
const reward = poolSecondary.rewards[index];
|
|
394
|
+
if (!reward)
|
|
395
|
+
continue;
|
|
396
|
+
const mint = reward.rewardMint;
|
|
397
|
+
const decimals = this.resolveSecondaryDecimals(mint, index, opts.secondaryRewardDecimals) ??
|
|
398
|
+
(await this.getMintDecimals(new PublicKey(mint)));
|
|
399
|
+
const secondaryClaimableRate = claimableEntries.reduce((sum, entry) => sum + (entry.secondaryRateContributions[index] ?? 0), 0);
|
|
400
|
+
const secondaryUnclaimableRate = unclaimableEntries.reduce((sum, entry) => sum + (entry.secondaryRateContributions[index] ?? 0), 0);
|
|
401
|
+
const secondaryTotalRate = secondaryClaimableRate + secondaryUnclaimableRate;
|
|
402
|
+
const settledSecondary = stakerSecondary.accruedRewards[index] ?? 0;
|
|
403
|
+
const [claimableSettledSecondary, unclaimableSettledSecondary] = this.splitAccruedByRates(settledSecondary, secondaryClaimableRate, secondaryTotalRate || stakerSecondary.effectiveRate[index] || 0);
|
|
404
|
+
const isRetired = Boolean(reward.isRetired);
|
|
405
|
+
const retiredAtRaw = reward.retiredAt;
|
|
406
|
+
const retiredAt = typeof retiredAtRaw === "number"
|
|
407
|
+
? retiredAtRaw
|
|
408
|
+
: retiredAtRaw &&
|
|
409
|
+
typeof retiredAtRaw.toNumber === "function"
|
|
410
|
+
? retiredAtRaw.toNumber()
|
|
411
|
+
: Number(retiredAtRaw ?? 0);
|
|
412
|
+
// STAKE-FRESH-2026-05-11 (F-H3): for retired slots, pending accrues
|
|
413
|
+
// only up to `retiredAt`. Pre-fix the SDK zeroed pending entirely,
|
|
414
|
+
// hiding the legitimately-owed pre-retire window from the UI; now we
|
|
415
|
+
// mirror the on-chain cap so the displayed claimable matches what
|
|
416
|
+
// `claim_secondary_rewards` will actually pay out.
|
|
417
|
+
const cappedNow = isRetired && retiredAt > 0 && retiredAt < now ? retiredAt : now;
|
|
418
|
+
const pendingClaimableSecondary = staker
|
|
419
|
+
? this.pendingFromRate(secondaryClaimableRate, pool.rewardConfig.rateInterval, pool.rewardEndAt, staker.lastUpdateAt, cappedNow)
|
|
420
|
+
: 0;
|
|
421
|
+
const pendingUnclaimableSecondary = staker
|
|
422
|
+
? this.pendingFromRate(secondaryUnclaimableRate, pool.rewardConfig.rateInterval, pool.rewardEndAt, staker.lastUpdateAt, cappedNow)
|
|
423
|
+
: 0;
|
|
424
|
+
const claimableBaseSecondary = claimableSettledSecondary + pendingClaimableSecondary;
|
|
425
|
+
const unclaimableBaseSecondary = unclaimableSettledSecondary + pendingUnclaimableSecondary;
|
|
426
|
+
const claimablePayoutSecondary = applyQuantityBonusRaw(claimableBaseSecondary, quantityThresholds, stakedCount, 1);
|
|
427
|
+
const unclaimablePayoutSecondary = applyQuantityBonusRaw(unclaimableBaseSecondary, quantityThresholds, stakedCount, 1);
|
|
428
|
+
// STAKE-FRESH-2026-05-11 (F-H3): a retired slot is "tombstoned" once
|
|
429
|
+
// there is nothing left to claim AND nothing left to accrue (true by
|
|
430
|
+
// definition while retired). Hide from default output so the UI
|
|
431
|
+
// "deprecated reward disappears after claim" naturally.
|
|
432
|
+
const tombstoned = isRetired &&
|
|
433
|
+
claimablePayoutSecondary === 0 &&
|
|
434
|
+
unclaimablePayoutSecondary === 0 &&
|
|
435
|
+
(stakerSecondary.accruedRewards[index] ?? 0) === 0;
|
|
436
|
+
if (tombstoned && !opts.includeTombstoned) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
let secondaryVaultBalance;
|
|
440
|
+
if (opts.includeVaultBalances) {
|
|
441
|
+
const mintPk = new PublicKey(mint);
|
|
442
|
+
const tokenProgram = await this.resolveTokenProgram(mintPk);
|
|
443
|
+
const vault = getAta(poolAuthority, mintPk, tokenProgram);
|
|
444
|
+
const rawBalance = await this.fetchTokenAccountAmount(vault);
|
|
445
|
+
secondaryVaultBalance =
|
|
446
|
+
rawBalance == null ? null : amountWithDecimals(rawBalance, decimals);
|
|
447
|
+
}
|
|
448
|
+
secondary.push({
|
|
449
|
+
index,
|
|
450
|
+
rewardMint: mint,
|
|
451
|
+
rewardDecimals: decimals,
|
|
452
|
+
isRetired,
|
|
453
|
+
retiredAt,
|
|
454
|
+
tombstoned,
|
|
455
|
+
totalAccrued: amountWithDecimals(claimablePayoutSecondary + unclaimablePayoutSecondary, decimals),
|
|
456
|
+
claimable: amountWithDecimals(claimablePayoutSecondary, decimals),
|
|
457
|
+
unclaimable: amountWithDecimals(unclaimablePayoutSecondary, decimals),
|
|
458
|
+
claimableBase: amountWithDecimals(claimableBaseSecondary, decimals),
|
|
459
|
+
unclaimableBase: amountWithDecimals(unclaimableBaseSecondary, decimals),
|
|
460
|
+
claimableRate: secondaryClaimableRate,
|
|
461
|
+
unclaimableRate: secondaryUnclaimableRate,
|
|
462
|
+
totalRate: secondaryTotalRate,
|
|
463
|
+
vaultBalance: secondaryVaultBalance,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
poolId,
|
|
469
|
+
wallet: _wallet.toBase58(),
|
|
470
|
+
generatedAt: now,
|
|
471
|
+
rewardMint: rewardMint.toBase58(),
|
|
472
|
+
rewardDecimals,
|
|
473
|
+
primary: {
|
|
474
|
+
totalAccrued: amountWithDecimals(claimablePayoutPrimary + unclaimablePayoutPrimary, rewardDecimals),
|
|
475
|
+
claimable: amountWithDecimals(claimablePayoutPrimary, rewardDecimals),
|
|
476
|
+
unclaimable: amountWithDecimals(unclaimablePayoutPrimary, rewardDecimals),
|
|
477
|
+
claimableBase: amountWithDecimals(claimableBasePrimary, rewardDecimals),
|
|
478
|
+
unclaimableBase: amountWithDecimals(unclaimableBasePrimary, rewardDecimals),
|
|
479
|
+
claimableRate,
|
|
480
|
+
unclaimableRate,
|
|
481
|
+
totalRate,
|
|
482
|
+
claimLockedUntil: staker?.claimLockedUntil ?? 0,
|
|
483
|
+
claimTransactionBlocked: Boolean(staker?.claimLockedUntil && now < staker.claimLockedUntil),
|
|
484
|
+
vaultBalance: primaryVaultBalance,
|
|
485
|
+
},
|
|
486
|
+
secondary,
|
|
487
|
+
entries: {
|
|
488
|
+
totalActive: activeEntries.length,
|
|
489
|
+
claimableActive: claimableEntries.length,
|
|
490
|
+
unclaimableActive: unclaimableEntries.length,
|
|
491
|
+
},
|
|
492
|
+
notes: [
|
|
493
|
+
"Raw amounts are base units; ui amounts are divided by rewardDecimals.",
|
|
494
|
+
"claimable/unclaimable splits are estimated from active StakeEntry rate contributions.",
|
|
495
|
+
"Current on-chain primary and secondary claim instructions are still blocked while claimLockedUntil is active.",
|
|
496
|
+
],
|
|
497
|
+
};
|
|
229
498
|
}
|
|
230
499
|
/**
|
|
231
500
|
* Fetch active stake entries for specific NFT mints by deriving their PDAs.
|
|
232
501
|
* More reliable than gPA in browser environments.
|
|
233
502
|
*/
|
|
234
|
-
async fetchStakeEntriesForMints(poolId, nftMints) {
|
|
503
|
+
async fetchStakeEntriesForMints(poolId, nftMints, projectId) {
|
|
235
504
|
if (nftMints.length === 0)
|
|
236
505
|
return [];
|
|
237
|
-
const
|
|
506
|
+
const pid = this.resolveProjectId(projectId);
|
|
507
|
+
const pdas = nftMints.map((mint) => getStakeEntryPda(pid, poolId, toPk(mint))[0]);
|
|
238
508
|
const accounts = await this.program.account.stakeEntry.fetchMultiple(pdas);
|
|
239
509
|
return accounts
|
|
240
510
|
.filter((a) => a !== null)
|
|
@@ -254,13 +524,14 @@ export class NftStakingClient {
|
|
|
254
524
|
* Fetch active stake entries for given mints across multiple pools.
|
|
255
525
|
* Uses PDA derivation + fetchMultiple (reliable in browser).
|
|
256
526
|
*/
|
|
257
|
-
async fetchStakeEntriesAcrossPools(poolIds, nftMints) {
|
|
527
|
+
async fetchStakeEntriesAcrossPools(poolIds, nftMints, projectId) {
|
|
258
528
|
if (nftMints.length === 0 || poolIds.length === 0)
|
|
259
529
|
return [];
|
|
530
|
+
const pid = this.resolveProjectId(projectId);
|
|
260
531
|
const pdas = [];
|
|
261
532
|
for (const poolId of poolIds) {
|
|
262
533
|
for (const mint of nftMints) {
|
|
263
|
-
pdas.push(getStakeEntryPda(poolId, mint)[0]);
|
|
534
|
+
pdas.push(getStakeEntryPda(pid, poolId, mint)[0]);
|
|
264
535
|
}
|
|
265
536
|
}
|
|
266
537
|
const accounts = await this.program.account.stakeEntry.fetchMultiple(pdas);
|
|
@@ -281,7 +552,11 @@ export class NftStakingClient {
|
|
|
281
552
|
async closeLegacyCollection(collectionMint, projectId) {
|
|
282
553
|
const _collMint = toPk(collectionMint);
|
|
283
554
|
const pid = this.resolveProjectId(projectId);
|
|
284
|
-
const [collection] = PublicKey.findProgramAddressSync([
|
|
555
|
+
const [collection] = PublicKey.findProgramAddressSync([
|
|
556
|
+
Buffer.from("project_collection"),
|
|
557
|
+
new BN(pid).toArrayLike(Buffer, "le", 8),
|
|
558
|
+
_collMint.toBuffer(),
|
|
559
|
+
], this.program.programId);
|
|
285
560
|
const [project] = getProjectPda(pid);
|
|
286
561
|
return this.program.methods
|
|
287
562
|
.closeLegacyCollection(new BN(pid), _collMint)
|
|
@@ -314,6 +589,7 @@ export class NftStakingClient {
|
|
|
314
589
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
315
590
|
const rewardEndAt = opts?.rewardEndAt ?? 0;
|
|
316
591
|
const maxStaked = opts?.maxStaked ?? 0;
|
|
592
|
+
const allowUnlockedStaking = opts?.allowUnlockedStaking ?? true;
|
|
317
593
|
const config = await this.fetchStakeConfig(pid);
|
|
318
594
|
const nextPoolId = config ? config.totalPools : 0;
|
|
319
595
|
const tokenProgram = await this.resolveTokenProgram(_rewardMint);
|
|
@@ -325,21 +601,23 @@ export class NftStakingClient {
|
|
|
325
601
|
baseRate: new BN(Math.round(rewardConfig.baseRate * multiplier)),
|
|
326
602
|
rateInterval: new BN(rewardConfig.rateInterval),
|
|
327
603
|
traitBonusMode: rewardConfig.traitBonusMode,
|
|
328
|
-
quantityThresholds: rewardConfig.quantityThresholds,
|
|
604
|
+
quantityThresholds: rewardConfig.quantityThresholds.map(normalizeQuantityBonus),
|
|
329
605
|
};
|
|
330
606
|
const rawLockConfigs = lockConfigs.map((lc) => ({
|
|
331
607
|
lockDuration: new BN(lc.lockDuration),
|
|
332
608
|
rewardRate: new BN(Math.round(lc.rewardRate * multiplier)),
|
|
333
609
|
earlyUnstakePenaltyBps: lc.earlyUnstakePenaltyBps,
|
|
610
|
+
claimOnlyAtEnd: lc.claimOnlyAtEnd,
|
|
334
611
|
}));
|
|
335
612
|
const [configPda] = getStakeConfigPda(pid);
|
|
336
613
|
const [poolPda] = getStakePoolPda(pid, nextPoolId);
|
|
337
614
|
const [collectionConfigPda] = getCollectionPda(pid, _collMint);
|
|
338
|
-
const [poolAuthority] = getPoolAuthorityPda(nextPoolId);
|
|
615
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, nextPoolId);
|
|
339
616
|
const rewardVault = getAta(poolAuthority, _rewardMint, tokenProgram);
|
|
617
|
+
const [project] = getProjectPda(pid);
|
|
340
618
|
const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
|
|
341
619
|
return this.program.methods
|
|
342
|
-
.createStakePool(new BN(pid), stakingMode, rawRewardConfig, rawLockConfigs, new BN(rewardEndAt), new BN(maxStaked))
|
|
620
|
+
.createStakePool(new BN(pid), stakingMode, rawRewardConfig, rawLockConfigs, new BN(rewardEndAt), new BN(maxStaked), allowUnlockedStaking)
|
|
343
621
|
.accountsStrict({
|
|
344
622
|
config: configPda,
|
|
345
623
|
pool: poolPda,
|
|
@@ -349,6 +627,7 @@ export class NftStakingClient {
|
|
|
349
627
|
rewardMint: _rewardMint,
|
|
350
628
|
rewardVault,
|
|
351
629
|
utilityConfig,
|
|
630
|
+
project,
|
|
352
631
|
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
353
632
|
authority: this.provider.wallet.publicKey,
|
|
354
633
|
tokenProgram,
|
|
@@ -368,6 +647,7 @@ export class NftStakingClient {
|
|
|
368
647
|
const gateType = updates.gateType ?? null;
|
|
369
648
|
const rewardEndAt = updates.rewardEndAt != null ? new BN(updates.rewardEndAt) : null;
|
|
370
649
|
const maxStaked = updates.maxStaked != null ? new BN(updates.maxStaked) : null;
|
|
650
|
+
const allowUnlockedStaking = updates.allowUnlockedStaking ?? null;
|
|
371
651
|
let rawRewardConfig = null;
|
|
372
652
|
let rawLockConfigs = null;
|
|
373
653
|
if (rewardConfig) {
|
|
@@ -380,7 +660,7 @@ export class NftStakingClient {
|
|
|
380
660
|
baseRate: new BN(Math.round(rewardConfig.baseRate * multiplier)),
|
|
381
661
|
rateInterval: new BN(rewardConfig.rateInterval),
|
|
382
662
|
traitBonusMode: rewardConfig.traitBonusMode,
|
|
383
|
-
quantityThresholds: rewardConfig.quantityThresholds,
|
|
663
|
+
quantityThresholds: rewardConfig.quantityThresholds.map(normalizeQuantityBonus),
|
|
384
664
|
};
|
|
385
665
|
}
|
|
386
666
|
if (lockConfigs && rewardConfig) {
|
|
@@ -390,6 +670,7 @@ export class NftStakingClient {
|
|
|
390
670
|
lockDuration: new BN(lc.lockDuration),
|
|
391
671
|
rewardRate: new BN(Math.round(lc.rewardRate * multiplier)),
|
|
392
672
|
earlyUnstakePenaltyBps: lc.earlyUnstakePenaltyBps,
|
|
673
|
+
claimOnlyAtEnd: lc.claimOnlyAtEnd,
|
|
393
674
|
}));
|
|
394
675
|
}
|
|
395
676
|
else if (lockConfigs) {
|
|
@@ -400,13 +681,17 @@ export class NftStakingClient {
|
|
|
400
681
|
lockDuration: new BN(lc.lockDuration),
|
|
401
682
|
rewardRate: new BN(Math.round(lc.rewardRate * multiplier)),
|
|
402
683
|
earlyUnstakePenaltyBps: lc.earlyUnstakePenaltyBps,
|
|
684
|
+
claimOnlyAtEnd: lc.claimOnlyAtEnd,
|
|
403
685
|
}));
|
|
404
686
|
}
|
|
405
687
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
688
|
+
const [project] = getProjectPda(pid);
|
|
406
689
|
return this.program.methods
|
|
407
|
-
.updateStakePool(new BN(pid), new BN(poolId), rawRewardConfig, rawLockConfigs, isActive, traitAuthority, canBurn, merkleRoot, gateType, rewardEndAt, maxStaked)
|
|
690
|
+
.updateStakePool(new BN(pid), new BN(poolId), rawRewardConfig, rawLockConfigs, isActive, traitAuthority, canBurn, merkleRoot, gateType, rewardEndAt, maxStaked, allowUnlockedStaking)
|
|
408
691
|
.accountsStrict({
|
|
409
692
|
pool: poolPda,
|
|
693
|
+
project,
|
|
694
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
410
695
|
authority: this.provider.wallet.publicKey,
|
|
411
696
|
})
|
|
412
697
|
.instruction();
|
|
@@ -415,12 +700,16 @@ export class NftStakingClient {
|
|
|
415
700
|
async fundRewardVault(poolId, amount, opts) {
|
|
416
701
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
417
702
|
const pool = await this.getPoolData(pid, poolId);
|
|
418
|
-
const rewardMint = opts?.rewardMint
|
|
703
|
+
const rewardMint = opts?.rewardMint
|
|
704
|
+
? toPk(opts.rewardMint)
|
|
705
|
+
: new PublicKey(pool.rewardConfig.rewardMint);
|
|
419
706
|
const tokenProgram = await this.resolveTokenProgram(rewardMint);
|
|
420
707
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
421
|
-
const [poolAuthority] = getPoolAuthorityPda(poolId);
|
|
708
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
422
709
|
const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
|
|
423
|
-
const funderAta = opts?.funderTokenAccount
|
|
710
|
+
const funderAta = opts?.funderTokenAccount
|
|
711
|
+
? toPk(opts.funderTokenAccount)
|
|
712
|
+
: getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
|
|
424
713
|
return this.program.methods
|
|
425
714
|
.fundRewardVault(new BN(pid), new BN(poolId), toBN(amount))
|
|
426
715
|
.accountsStrict({
|
|
@@ -438,16 +727,23 @@ export class NftStakingClient {
|
|
|
438
727
|
async withdrawRewardVault(poolId, amount, opts) {
|
|
439
728
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
440
729
|
const pool = await this.getPoolData(pid, poolId);
|
|
441
|
-
const rewardMint = opts?.rewardMint
|
|
730
|
+
const rewardMint = opts?.rewardMint
|
|
731
|
+
? toPk(opts.rewardMint)
|
|
732
|
+
: new PublicKey(pool.rewardConfig.rewardMint);
|
|
442
733
|
const tokenProgram = await this.resolveTokenProgram(rewardMint);
|
|
443
734
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
444
|
-
const [
|
|
735
|
+
const [project] = getProjectPda(pid);
|
|
736
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
445
737
|
const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
|
|
446
|
-
const destAta = opts?.destinationTokenAccount
|
|
738
|
+
const destAta = opts?.destinationTokenAccount
|
|
739
|
+
? toPk(opts.destinationTokenAccount)
|
|
740
|
+
: getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
|
|
447
741
|
return this.program.methods
|
|
448
742
|
.withdrawRewardVault(new BN(pid), new BN(poolId), toBN(amount))
|
|
449
743
|
.accountsStrict({
|
|
450
744
|
pool: poolPda,
|
|
745
|
+
project,
|
|
746
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
451
747
|
poolAuthority,
|
|
452
748
|
rewardVault,
|
|
453
749
|
destinationTokenAccount: destAta,
|
|
@@ -460,10 +756,13 @@ export class NftStakingClient {
|
|
|
460
756
|
async closeStakePool(poolId, projectId) {
|
|
461
757
|
const pid = this.resolveProjectId(projectId);
|
|
462
758
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
759
|
+
const [project] = getProjectPda(pid);
|
|
463
760
|
return this.program.methods
|
|
464
761
|
.closeStakePool(new BN(pid), new BN(poolId))
|
|
465
762
|
.accountsStrict({
|
|
466
763
|
pool: poolPda,
|
|
764
|
+
project,
|
|
765
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
467
766
|
rewardVault: null,
|
|
468
767
|
tokenProgram: null,
|
|
469
768
|
authority: this.provider.wallet.publicKey,
|
|
@@ -479,19 +778,20 @@ export class NftStakingClient {
|
|
|
479
778
|
const lockTierIndex = opts?.lockTierIndex ?? null;
|
|
480
779
|
const gateProof = opts?.gateProof ?? [];
|
|
481
780
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
482
|
-
const [stakeEntry] = getStakeEntryPda(poolId, _nftMint);
|
|
781
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftMint);
|
|
782
|
+
const [stakeLock] = getNftStakeLockPda(_nftMint);
|
|
483
783
|
const staker = this.provider.wallet.publicKey;
|
|
484
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
485
|
-
const [
|
|
784
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
785
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
786
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
486
787
|
const nftTokenProgram = await this.resolveTokenProgram(_nftMint);
|
|
487
788
|
const stakerNftAccount = getAta(staker, _nftMint, nftTokenProgram);
|
|
488
789
|
const isEscrow = stakingMode !== 1;
|
|
489
|
-
const escrowNftAccount = isEscrow
|
|
490
|
-
? getAta(poolAuthority, _nftMint, nftTokenProgram)
|
|
491
|
-
: null;
|
|
790
|
+
const escrowNftAccount = isEscrow ? getAta(poolAuthority, _nftMint, nftTokenProgram) : null;
|
|
492
791
|
const nftMetadata = getMetadataPda(_nftMint);
|
|
493
792
|
const nftEdition = getEditionPda(_nftMint);
|
|
494
|
-
const
|
|
793
|
+
const { tokenRecord, destinationTokenRecord } = await this.resolvePNftTokenRecords(_nftMint, stakerNftAccount, escrowNftAccount);
|
|
794
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
495
795
|
const [project] = getProjectPda(pid);
|
|
496
796
|
const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
|
|
497
797
|
return this.program.methods
|
|
@@ -499,6 +799,7 @@ export class NftStakingClient {
|
|
|
499
799
|
.accountsStrict({
|
|
500
800
|
pool: poolPda,
|
|
501
801
|
stakeEntry,
|
|
802
|
+
stakeLock,
|
|
502
803
|
stakerAccount,
|
|
503
804
|
poolAuthority,
|
|
504
805
|
nftMint: _nftMint,
|
|
@@ -508,8 +809,9 @@ export class NftStakingClient {
|
|
|
508
809
|
nftEdition,
|
|
509
810
|
mplTokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
|
|
510
811
|
sysvarInstructions: SYSVAR_INSTRUCTIONS_ID,
|
|
511
|
-
tokenRecord
|
|
512
|
-
destinationTokenRecord
|
|
812
|
+
tokenRecord,
|
|
813
|
+
destinationTokenRecord,
|
|
814
|
+
userProfile,
|
|
513
815
|
utilityConfig,
|
|
514
816
|
feeConfig: feeAccts.feeConfig,
|
|
515
817
|
programRegistry: feeAccts.programRegistry,
|
|
@@ -525,7 +827,48 @@ export class NftStakingClient {
|
|
|
525
827
|
})
|
|
526
828
|
.instruction();
|
|
527
829
|
}
|
|
528
|
-
/**
|
|
830
|
+
/**
|
|
831
|
+
* Resolve the staker's remaining StakeEntry PDAs (excluding the entry being
|
|
832
|
+
* closed) for the on-chain claim-lock recompute. Used by `unstakeNft`,
|
|
833
|
+
* `unstakeCoreNft`, `unstakeCnft`, `burnStakedNft`, `burnStakedCoreNft`.
|
|
834
|
+
*
|
|
835
|
+
* - `undefined`: auto-discover via `fetchAllStakeEntriesByPool` (one RPC).
|
|
836
|
+
* - `null`: skip recompute (existing `claim_locked_until` is left as-is).
|
|
837
|
+
* - `PublicKey[]`: use exactly this list (filtered to exclude the closed entry).
|
|
838
|
+
*/
|
|
839
|
+
async resolveRemainingStakeEntries(poolId, pid, staker, excludeStakeEntry, explicit) {
|
|
840
|
+
if (explicit === null)
|
|
841
|
+
return null;
|
|
842
|
+
if (explicit !== undefined) {
|
|
843
|
+
return explicit.filter((pk) => !pk.equals(excludeStakeEntry));
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
const all = await this.fetchAllStakeEntriesByPool(poolId, pid);
|
|
847
|
+
return all
|
|
848
|
+
.filter((e) => e.isActive && new PublicKey(e.owner).equals(staker))
|
|
849
|
+
.map((e) => getStakeEntryPda(pid, poolId, new PublicKey(e.nftMint))[0])
|
|
850
|
+
.filter((pk) => !pk.equals(excludeStakeEntry));
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Unstake a previously staked NFT.
|
|
858
|
+
*
|
|
859
|
+
* Pass `opts.remainingStakeEntries` (the staker's other active StakeEntry
|
|
860
|
+
* PDAs in this pool, NOT including the one being unstaked) so the on-chain
|
|
861
|
+
* handler can recompute `claim_locked_until` from scratch. If omitted, the
|
|
862
|
+
* existing `claim_locked_until` is left unchanged on the staker account —
|
|
863
|
+
* the staker stays behind their lock until either it expires naturally or
|
|
864
|
+
* they retry the call with the full set of remaining entries. This default
|
|
865
|
+
* is intentionally conservative; client UIs should always supply the list
|
|
866
|
+
* to avoid stranding stakers behind a phantom lock.
|
|
867
|
+
*
|
|
868
|
+
* If `opts.remainingStakeEntries === undefined`, the SDK will attempt to
|
|
869
|
+
* auto-discover them via `fetchAllStakeEntriesByPool` (one extra RPC
|
|
870
|
+
* call). Pass `null` to opt out of discovery entirely.
|
|
871
|
+
*/
|
|
529
872
|
async unstakeNft(poolId, nftMint, opts) {
|
|
530
873
|
const _nftMint = toPk(nftMint);
|
|
531
874
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
@@ -535,24 +878,45 @@ export class NftStakingClient {
|
|
|
535
878
|
const rewardTokenProgram = await this.resolveTokenProgram(rewardMint);
|
|
536
879
|
const nftTokenProgram = await this.resolveTokenProgram(_nftMint);
|
|
537
880
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
538
|
-
const [stakeEntry] = getStakeEntryPda(poolId, _nftMint);
|
|
881
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftMint);
|
|
882
|
+
const [stakeLock] = getNftStakeLockPda(_nftMint);
|
|
539
883
|
const staker = this.provider.wallet.publicKey;
|
|
540
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
541
|
-
const [
|
|
884
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
885
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
886
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
542
887
|
const stakerNftAccount = getAta(staker, _nftMint, nftTokenProgram);
|
|
543
888
|
const isEscrow = stakingMode !== 1;
|
|
544
|
-
const escrowNftAccount = isEscrow
|
|
545
|
-
? getAta(poolAuthority, _nftMint, nftTokenProgram)
|
|
546
|
-
: null;
|
|
889
|
+
const escrowNftAccount = isEscrow ? getAta(poolAuthority, _nftMint, nftTokenProgram) : null;
|
|
547
890
|
const rewardVault = getAta(poolAuthority, rewardMint, rewardTokenProgram);
|
|
548
891
|
const stakerRewardAccount = getAta(staker, rewardMint, rewardTokenProgram);
|
|
549
892
|
const nftMetadata = getMetadataPda(_nftMint);
|
|
550
893
|
const nftEdition = getEditionPda(_nftMint);
|
|
894
|
+
const { tokenRecord, destinationTokenRecord } = await this.resolvePNftTokenRecords(_nftMint,
|
|
895
|
+
// For unstake, the *source* of the NFT is the escrow when escrowed,
|
|
896
|
+
// otherwise the staker's wallet ATA (with the staker_account PDA as
|
|
897
|
+
// delegate).
|
|
898
|
+
escrowNftAccount ?? stakerNftAccount, escrowNftAccount ? stakerNftAccount : null);
|
|
899
|
+
// entry has secondary rates), then the remaining StakeEntry PDAs for
|
|
900
|
+
// the claim-lock recompute.
|
|
901
|
+
const entry = await this.fetchStakeEntry(poolId, _nftMint, pid);
|
|
902
|
+
const hasSecondary = entry?.secondaryRateContributions?.some((r) => r > 0n) ?? false;
|
|
903
|
+
const remainingAccounts = [];
|
|
904
|
+
if (hasSecondary) {
|
|
905
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
906
|
+
remainingAccounts.push({ pubkey: stakerSecondary, isSigner: false, isWritable: true });
|
|
907
|
+
}
|
|
908
|
+
let stakeEntryList = await this.resolveRemainingStakeEntries(poolId, pid, staker, stakeEntry, opts?.remainingStakeEntries);
|
|
909
|
+
if (stakeEntryList) {
|
|
910
|
+
for (const pk of stakeEntryList) {
|
|
911
|
+
remainingAccounts.push({ pubkey: pk, isSigner: false, isWritable: false });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
551
914
|
return this.program.methods
|
|
552
915
|
.unstakeNft(new BN(pid), new BN(poolId), _nftMint)
|
|
553
916
|
.accountsStrict({
|
|
554
917
|
pool: poolPda,
|
|
555
918
|
stakeEntry,
|
|
919
|
+
stakeLock,
|
|
556
920
|
stakerAccount,
|
|
557
921
|
poolAuthority,
|
|
558
922
|
nftMint: _nftMint,
|
|
@@ -565,14 +929,16 @@ export class NftStakingClient {
|
|
|
565
929
|
nftEdition,
|
|
566
930
|
mplTokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
|
|
567
931
|
sysvarInstructions: SYSVAR_INSTRUCTIONS_ID,
|
|
568
|
-
tokenRecord
|
|
569
|
-
destinationTokenRecord
|
|
932
|
+
tokenRecord,
|
|
933
|
+
destinationTokenRecord,
|
|
570
934
|
staker,
|
|
935
|
+
userProfile,
|
|
571
936
|
tokenProgram: nftTokenProgram,
|
|
572
937
|
rewardTokenProgram,
|
|
573
938
|
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
574
939
|
systemProgram: SystemProgram.programId,
|
|
575
940
|
})
|
|
941
|
+
.remainingAccounts(remainingAccounts)
|
|
576
942
|
.instruction();
|
|
577
943
|
}
|
|
578
944
|
/** Claim accrued rewards. Auto-resolves rewardMint from pool. */
|
|
@@ -580,16 +946,17 @@ export class NftStakingClient {
|
|
|
580
946
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
581
947
|
const pool = await this.getPoolData(pid, poolId);
|
|
582
948
|
const rewardMint = new PublicKey(pool.rewardConfig.rewardMint);
|
|
583
|
-
const traitBonusRate = opts?.traitBonusRate
|
|
949
|
+
const traitBonusRate = opts?.traitBonusRate == null ? null : toBN(opts.traitBonusRate);
|
|
584
950
|
const tokenProgram = await this.resolveTokenProgram(rewardMint);
|
|
585
951
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
586
952
|
const staker = this.provider.wallet.publicKey;
|
|
587
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
588
|
-
const [
|
|
953
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
954
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
955
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
589
956
|
const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
|
|
590
957
|
const stakerRewardAccount = getAta(staker, rewardMint, tokenProgram);
|
|
591
958
|
const [stakeConfig] = getStakeConfigPda(pid);
|
|
592
|
-
const feeAccts = this.resolveFeeAccounts(opts?.fee);
|
|
959
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
593
960
|
const [project] = getProjectPda(pid);
|
|
594
961
|
return this.program.methods
|
|
595
962
|
.claimRewards(new BN(pid), new BN(poolId), traitBonusRate)
|
|
@@ -610,6 +977,7 @@ export class NftStakingClient {
|
|
|
610
977
|
adminProgram: feeAccts.adminProgram,
|
|
611
978
|
project,
|
|
612
979
|
staker,
|
|
980
|
+
userProfile,
|
|
613
981
|
tokenProgram,
|
|
614
982
|
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
615
983
|
systemProgram: SystemProgram.programId,
|
|
@@ -625,11 +993,13 @@ export class NftStakingClient {
|
|
|
625
993
|
const lockTierIndex = opts?.lockTierIndex ?? null;
|
|
626
994
|
const gateProof = opts?.gateProof ?? [];
|
|
627
995
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
628
|
-
const [stakeEntry] = getStakeEntryPda(poolId, _nftAsset);
|
|
996
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftAsset);
|
|
997
|
+
const [stakeLock] = getNftStakeLockPda(_nftAsset);
|
|
629
998
|
const staker = this.provider.wallet.publicKey;
|
|
630
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
631
|
-
const [
|
|
632
|
-
const
|
|
999
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1000
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1001
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
1002
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
633
1003
|
const [project] = getProjectPda(pid);
|
|
634
1004
|
const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
|
|
635
1005
|
return this.program.methods
|
|
@@ -637,6 +1007,7 @@ export class NftStakingClient {
|
|
|
637
1007
|
.accountsStrict({
|
|
638
1008
|
pool: poolPda,
|
|
639
1009
|
stakeEntry,
|
|
1010
|
+
stakeLock,
|
|
640
1011
|
stakerAccount,
|
|
641
1012
|
poolAuthority,
|
|
642
1013
|
nftAsset: _nftAsset,
|
|
@@ -651,6 +1022,7 @@ export class NftStakingClient {
|
|
|
651
1022
|
adminProgram: feeAccts.adminProgram,
|
|
652
1023
|
project,
|
|
653
1024
|
staker,
|
|
1025
|
+
userProfile,
|
|
654
1026
|
systemProgram: SystemProgram.programId,
|
|
655
1027
|
})
|
|
656
1028
|
.instruction();
|
|
@@ -664,17 +1036,35 @@ export class NftStakingClient {
|
|
|
664
1036
|
const rewardMint = new PublicKey(pool.rewardConfig.rewardMint);
|
|
665
1037
|
const rewardTokenProgram = await this.resolveTokenProgram(rewardMint);
|
|
666
1038
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
667
|
-
const [stakeEntry] = getStakeEntryPda(poolId, _nftAsset);
|
|
1039
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftAsset);
|
|
1040
|
+
const [stakeLock] = getNftStakeLockPda(_nftAsset);
|
|
668
1041
|
const staker = this.provider.wallet.publicKey;
|
|
669
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
670
|
-
const [
|
|
1042
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1043
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1044
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
671
1045
|
const rewardVault = getAta(poolAuthority, rewardMint, rewardTokenProgram);
|
|
672
1046
|
const stakerRewardAccount = getAta(staker, rewardMint, rewardTokenProgram);
|
|
1047
|
+
// Build remaining_accounts (optional secondary + staker's other active
|
|
1048
|
+
// StakeEntry PDAs).
|
|
1049
|
+
const entry = await this.fetchStakeEntry(poolId, _nftAsset, pid);
|
|
1050
|
+
const hasSecondary = entry?.secondaryRateContributions?.some((r) => r > 0n) ?? false;
|
|
1051
|
+
const remainingAccounts = [];
|
|
1052
|
+
if (hasSecondary) {
|
|
1053
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
1054
|
+
remainingAccounts.push({ pubkey: stakerSecondary, isSigner: false, isWritable: true });
|
|
1055
|
+
}
|
|
1056
|
+
const stakeEntryList = await this.resolveRemainingStakeEntries(poolId, pid, staker, stakeEntry, opts?.remainingStakeEntries);
|
|
1057
|
+
if (stakeEntryList) {
|
|
1058
|
+
for (const pk of stakeEntryList) {
|
|
1059
|
+
remainingAccounts.push({ pubkey: pk, isSigner: false, isWritable: false });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
673
1062
|
return this.program.methods
|
|
674
1063
|
.unstakeCoreNft(new BN(pid), new BN(poolId), _nftAsset)
|
|
675
1064
|
.accountsStrict({
|
|
676
1065
|
pool: poolPda,
|
|
677
1066
|
stakeEntry,
|
|
1067
|
+
stakeLock,
|
|
678
1068
|
stakerAccount,
|
|
679
1069
|
poolAuthority,
|
|
680
1070
|
nftAsset: _nftAsset,
|
|
@@ -684,17 +1074,19 @@ export class NftStakingClient {
|
|
|
684
1074
|
stakerRewardAccount,
|
|
685
1075
|
rewardMint,
|
|
686
1076
|
staker,
|
|
1077
|
+
userProfile,
|
|
687
1078
|
rewardTokenProgram,
|
|
688
1079
|
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
689
1080
|
systemProgram: SystemProgram.programId,
|
|
690
1081
|
})
|
|
1082
|
+
.remainingAccounts(remainingAccounts)
|
|
691
1083
|
.instruction();
|
|
692
1084
|
}
|
|
693
1085
|
/** Stake a compressed NFT (cNFT). All address/hash params accepted as strings. */
|
|
694
1086
|
async stakeCnft(poolId, cnftParams, opts) {
|
|
695
1087
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
696
1088
|
const gateProof = opts?.gateProof ?? [];
|
|
697
|
-
const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes } = cnftParams;
|
|
1089
|
+
const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes, } = cnftParams;
|
|
698
1090
|
const nftAssetIdPk = new PublicKey(nftAssetId);
|
|
699
1091
|
const merkleTreePk = new PublicKey(merkleTree);
|
|
700
1092
|
const [treeConfigPk] = getTreeConfigPda(merkleTreePk);
|
|
@@ -702,11 +1094,13 @@ export class NftStakingClient {
|
|
|
702
1094
|
const cnftDataHashArr = base58HashToArray(cnftDataHash);
|
|
703
1095
|
const cnftCreatorHashArr = base58HashToArray(cnftCreatorHash);
|
|
704
1096
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
705
|
-
const [stakeEntry] = getStakeEntryPda(poolId, nftAssetIdPk);
|
|
1097
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, nftAssetIdPk);
|
|
1098
|
+
const [stakeLock] = getNftStakeLockPda(nftAssetIdPk);
|
|
706
1099
|
const staker = this.provider.wallet.publicKey;
|
|
707
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
708
|
-
const [
|
|
709
|
-
const
|
|
1100
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1101
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1102
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
1103
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
710
1104
|
const [project] = getProjectPda(pid);
|
|
711
1105
|
const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
|
|
712
1106
|
const remainingAccounts = proofNodes.map((node) => ({
|
|
@@ -719,6 +1113,7 @@ export class NftStakingClient {
|
|
|
719
1113
|
.accountsStrict({
|
|
720
1114
|
pool: poolPda,
|
|
721
1115
|
stakeEntry,
|
|
1116
|
+
stakeLock,
|
|
722
1117
|
stakerAccount,
|
|
723
1118
|
poolAuthority,
|
|
724
1119
|
treeConfig: treeConfigPk,
|
|
@@ -735,6 +1130,7 @@ export class NftStakingClient {
|
|
|
735
1130
|
adminProgram: feeAccts.adminProgram,
|
|
736
1131
|
project,
|
|
737
1132
|
staker,
|
|
1133
|
+
userProfile,
|
|
738
1134
|
systemProgram: SystemProgram.programId,
|
|
739
1135
|
})
|
|
740
1136
|
.remainingAccounts(remainingAccounts)
|
|
@@ -744,7 +1140,7 @@ export class NftStakingClient {
|
|
|
744
1140
|
async unstakeCnft(poolId, cnftParams, opts) {
|
|
745
1141
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
746
1142
|
const pool = await this.getPoolData(pid, poolId);
|
|
747
|
-
const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes } = cnftParams;
|
|
1143
|
+
const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes, } = cnftParams;
|
|
748
1144
|
const nftAssetIdPk = new PublicKey(nftAssetId);
|
|
749
1145
|
const merkleTreePk = new PublicKey(merkleTree);
|
|
750
1146
|
const [treeConfigPk] = getTreeConfigPda(merkleTreePk);
|
|
@@ -753,22 +1149,45 @@ export class NftStakingClient {
|
|
|
753
1149
|
const cnftDataHashArr = base58HashToArray(cnftDataHash);
|
|
754
1150
|
const cnftCreatorHashArr = base58HashToArray(cnftCreatorHash);
|
|
755
1151
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
756
|
-
const [stakeEntry] = getStakeEntryPda(poolId, nftAssetIdPk);
|
|
1152
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, nftAssetIdPk);
|
|
1153
|
+
const [stakeLock] = getNftStakeLockPda(nftAssetIdPk);
|
|
757
1154
|
const staker = this.provider.wallet.publicKey;
|
|
758
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
759
|
-
const [
|
|
1155
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1156
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1157
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
760
1158
|
const rewardVaultPk = getAta(poolAuthority, rewardMintPk);
|
|
761
1159
|
const stakerRewardAccountPk = getAta(staker, rewardMintPk);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1160
|
+
// cNFT layout for remaining_accounts is
|
|
1161
|
+
// [<staker_secondary if has_secondary>, <proof nodes...>, <trailing
|
|
1162
|
+
// StakeEntry PDAs for claim-lock recompute>]
|
|
1163
|
+
// The on-chain handler tail-walks for program-owned accounts to find
|
|
1164
|
+
// stake entries.
|
|
1165
|
+
const entry = await this.fetchStakeEntry(poolId, nftAssetIdPk, pid);
|
|
1166
|
+
const hasSecondary = entry?.secondaryRateContributions?.some((r) => r > 0n) ?? false;
|
|
1167
|
+
const remainingAccounts = [];
|
|
1168
|
+
if (hasSecondary) {
|
|
1169
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
1170
|
+
remainingAccounts.push({ pubkey: stakerSecondary, isSigner: false, isWritable: true });
|
|
1171
|
+
}
|
|
1172
|
+
for (const node of proofNodes) {
|
|
1173
|
+
remainingAccounts.push({
|
|
1174
|
+
pubkey: new PublicKey(node),
|
|
1175
|
+
isSigner: false,
|
|
1176
|
+
isWritable: false,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
const stakeEntryList = await this.resolveRemainingStakeEntries(poolId, pid, staker, stakeEntry, opts?.remainingStakeEntries);
|
|
1180
|
+
if (stakeEntryList) {
|
|
1181
|
+
for (const pk of stakeEntryList) {
|
|
1182
|
+
remainingAccounts.push({ pubkey: pk, isSigner: false, isWritable: false });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
767
1185
|
return this.program.methods
|
|
768
1186
|
.unstakeCnft(new BN(pid), new BN(poolId), nftAssetIdPk, cnftRootArr, cnftDataHashArr, cnftCreatorHashArr, new BN(cnftNonce), cnftIndex)
|
|
769
1187
|
.accountsStrict({
|
|
770
1188
|
pool: poolPda,
|
|
771
1189
|
stakeEntry,
|
|
1190
|
+
stakeLock,
|
|
772
1191
|
stakerAccount,
|
|
773
1192
|
poolAuthority,
|
|
774
1193
|
treeConfig: treeConfigPk,
|
|
@@ -780,6 +1199,7 @@ export class NftStakingClient {
|
|
|
780
1199
|
stakerRewardAccount: stakerRewardAccountPk,
|
|
781
1200
|
rewardMint: rewardMintPk,
|
|
782
1201
|
staker,
|
|
1202
|
+
userProfile,
|
|
783
1203
|
tokenProgram: TOKEN_PROGRAM_ID,
|
|
784
1204
|
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
785
1205
|
systemProgram: SystemProgram.programId,
|
|
@@ -787,7 +1207,13 @@ export class NftStakingClient {
|
|
|
787
1207
|
.remainingAccounts(remainingAccounts)
|
|
788
1208
|
.instruction();
|
|
789
1209
|
}
|
|
790
|
-
/**
|
|
1210
|
+
/**
|
|
1211
|
+
* Burn a permanently-locked legacy/pNFT. Auto-resolves stakingMode from pool.
|
|
1212
|
+
*
|
|
1213
|
+
* Pass `opts.remainingStakeEntries` so the on-chain handler can recompute
|
|
1214
|
+
* `claim_locked_until`. See `unstakeNft` for full semantics. Default: SDK
|
|
1215
|
+
* auto-discovers remaining entries via gPA.
|
|
1216
|
+
*/
|
|
791
1217
|
async burnStakedNft(poolId, nftMint, opts) {
|
|
792
1218
|
const _nftMint = toPk(nftMint);
|
|
793
1219
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
@@ -795,22 +1221,40 @@ export class NftStakingClient {
|
|
|
795
1221
|
const stakingMode = pool.stakingMode;
|
|
796
1222
|
const nftTokenProgram = await this.resolveTokenProgram(_nftMint);
|
|
797
1223
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
798
|
-
const [stakeEntry] = getStakeEntryPda(poolId, _nftMint);
|
|
1224
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftMint);
|
|
1225
|
+
const [stakeLock] = getNftStakeLockPda(_nftMint);
|
|
799
1226
|
const staker = this.provider.wallet.publicKey;
|
|
800
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
801
|
-
const [
|
|
802
|
-
const
|
|
1227
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1228
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1229
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
1230
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
803
1231
|
const [project] = getProjectPda(pid);
|
|
804
1232
|
const isWalletLock = stakingMode === 1;
|
|
805
1233
|
const escrowNftAccount = isWalletLock ? null : getAta(poolAuthority, _nftMint, nftTokenProgram);
|
|
806
1234
|
const stakerNftAccount = isWalletLock ? getAta(staker, _nftMint, nftTokenProgram) : null;
|
|
807
1235
|
const nftMetadata = isWalletLock ? getMetadataPda(_nftMint) : null;
|
|
808
1236
|
const nftEdition = isWalletLock ? getEditionPda(_nftMint) : null;
|
|
1237
|
+
// Assemble remaining_accounts (secondary slot first when applicable,
|
|
1238
|
+
// then remaining StakeEntry PDAs).
|
|
1239
|
+
const entry = await this.fetchStakeEntry(poolId, _nftMint, pid);
|
|
1240
|
+
const hasSecondary = entry?.secondaryRateContributions?.some((r) => r > 0n) ?? false;
|
|
1241
|
+
const remainingAccounts = [];
|
|
1242
|
+
if (hasSecondary) {
|
|
1243
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
1244
|
+
remainingAccounts.push({ pubkey: stakerSecondary, isSigner: false, isWritable: true });
|
|
1245
|
+
}
|
|
1246
|
+
let stakeEntryList = await this.resolveRemainingStakeEntries(poolId, pid, staker, stakeEntry, opts?.remainingStakeEntries);
|
|
1247
|
+
if (stakeEntryList) {
|
|
1248
|
+
for (const pk of stakeEntryList) {
|
|
1249
|
+
remainingAccounts.push({ pubkey: pk, isSigner: false, isWritable: false });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
809
1252
|
return this.program.methods
|
|
810
1253
|
.burnStakedNft(new BN(pid), new BN(poolId), _nftMint)
|
|
811
1254
|
.accountsStrict({
|
|
812
1255
|
pool: poolPda,
|
|
813
1256
|
stakeEntry,
|
|
1257
|
+
stakeLock,
|
|
814
1258
|
stakerAccount,
|
|
815
1259
|
poolAuthority,
|
|
816
1260
|
nftMint: _nftMint,
|
|
@@ -830,9 +1274,11 @@ export class NftStakingClient {
|
|
|
830
1274
|
adminProgram: feeAccts.adminProgram,
|
|
831
1275
|
project,
|
|
832
1276
|
staker,
|
|
1277
|
+
userProfile,
|
|
833
1278
|
tokenProgram: nftTokenProgram,
|
|
834
1279
|
systemProgram: SystemProgram.programId,
|
|
835
1280
|
})
|
|
1281
|
+
.remainingAccounts(remainingAccounts)
|
|
836
1282
|
.instruction();
|
|
837
1283
|
}
|
|
838
1284
|
/** Burn a permanently-locked Core NFT. Auto-resolves collection from pool. */
|
|
@@ -842,17 +1288,34 @@ export class NftStakingClient {
|
|
|
842
1288
|
const pool = await this.getPoolData(pid, poolId);
|
|
843
1289
|
const collection = new PublicKey(pool.collectionMint);
|
|
844
1290
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
845
|
-
const [stakeEntry] = getStakeEntryPda(poolId, _nftAsset);
|
|
1291
|
+
const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftAsset);
|
|
1292
|
+
const [stakeLock] = getNftStakeLockPda(_nftAsset);
|
|
846
1293
|
const staker = this.provider.wallet.publicKey;
|
|
847
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
848
|
-
const [
|
|
849
|
-
const
|
|
1294
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1295
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1296
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
1297
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
850
1298
|
const [project] = getProjectPda(pid);
|
|
1299
|
+
// Build remaining_accounts (secondary + stake entries).
|
|
1300
|
+
const entry = await this.fetchStakeEntry(poolId, _nftAsset, pid);
|
|
1301
|
+
const hasSecondary = entry?.secondaryRateContributions?.some((r) => r > 0n) ?? false;
|
|
1302
|
+
const remainingAccounts = [];
|
|
1303
|
+
if (hasSecondary) {
|
|
1304
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
1305
|
+
remainingAccounts.push({ pubkey: stakerSecondary, isSigner: false, isWritable: true });
|
|
1306
|
+
}
|
|
1307
|
+
const stakeEntryList = await this.resolveRemainingStakeEntries(poolId, pid, staker, stakeEntry, opts?.remainingStakeEntries);
|
|
1308
|
+
if (stakeEntryList) {
|
|
1309
|
+
for (const pk of stakeEntryList) {
|
|
1310
|
+
remainingAccounts.push({ pubkey: pk, isSigner: false, isWritable: false });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
851
1313
|
return this.program.methods
|
|
852
1314
|
.burnStakedCoreNft(new BN(pid), new BN(poolId), _nftAsset)
|
|
853
1315
|
.accountsStrict({
|
|
854
1316
|
pool: poolPda,
|
|
855
1317
|
stakeEntry,
|
|
1318
|
+
stakeLock,
|
|
856
1319
|
stakerAccount,
|
|
857
1320
|
poolAuthority,
|
|
858
1321
|
nftAsset: _nftAsset,
|
|
@@ -866,8 +1329,10 @@ export class NftStakingClient {
|
|
|
866
1329
|
adminProgram: feeAccts.adminProgram,
|
|
867
1330
|
project,
|
|
868
1331
|
staker,
|
|
1332
|
+
userProfile,
|
|
869
1333
|
systemProgram: SystemProgram.programId,
|
|
870
1334
|
})
|
|
1335
|
+
.remainingAccounts(remainingAccounts)
|
|
871
1336
|
.instruction();
|
|
872
1337
|
}
|
|
873
1338
|
/** Spend accrued points from a Points reward type pool. */
|
|
@@ -875,8 +1340,9 @@ export class NftStakingClient {
|
|
|
875
1340
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
876
1341
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
877
1342
|
const staker = this.provider.wallet.publicKey;
|
|
878
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
879
|
-
const
|
|
1343
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1344
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1345
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
880
1346
|
const [project] = getProjectPda(pid);
|
|
881
1347
|
return this.program.methods
|
|
882
1348
|
.spendPoints(new BN(pid), new BN(poolId), toBN(amount))
|
|
@@ -891,19 +1357,22 @@ export class NftStakingClient {
|
|
|
891
1357
|
adminProgram: feeAccts.adminProgram,
|
|
892
1358
|
project,
|
|
893
1359
|
staker,
|
|
1360
|
+
userProfile,
|
|
894
1361
|
systemProgram: SystemProgram.programId,
|
|
895
1362
|
})
|
|
896
1363
|
.instruction();
|
|
897
1364
|
}
|
|
898
|
-
async fetchPoolSecondaryRewards(poolId) {
|
|
899
|
-
const
|
|
1365
|
+
async fetchPoolSecondaryRewards(poolId, projectId) {
|
|
1366
|
+
const pid = this.resolveProjectId(projectId);
|
|
1367
|
+
const [pda] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
900
1368
|
const raw = await this.program.account.poolSecondaryRewards.fetchNullable(pda);
|
|
901
1369
|
if (!raw)
|
|
902
1370
|
return null;
|
|
903
1371
|
return decodeAccount(raw);
|
|
904
1372
|
}
|
|
905
|
-
async fetchStakerSecondaryRewards(poolId, wallet) {
|
|
906
|
-
const
|
|
1373
|
+
async fetchStakerSecondaryRewards(poolId, wallet, projectId) {
|
|
1374
|
+
const pid = this.resolveProjectId(projectId);
|
|
1375
|
+
const [pda] = getStakerSecondaryRewardsPda(pid, poolId, toPk(wallet));
|
|
907
1376
|
const raw = await this.program.account.stakerSecondaryRewards.fetchNullable(pda);
|
|
908
1377
|
if (!raw)
|
|
909
1378
|
return null;
|
|
@@ -913,14 +1382,17 @@ export class NftStakingClient {
|
|
|
913
1382
|
const pid = this.resolveProjectId(projectId);
|
|
914
1383
|
const _rewardMint = toPk(rewardMint);
|
|
915
1384
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
916
|
-
const [
|
|
917
|
-
const [
|
|
1385
|
+
const [project] = getProjectPda(pid);
|
|
1386
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
1387
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
918
1388
|
const tokenProgram = await this.resolveTokenProgram(_rewardMint);
|
|
919
1389
|
const vault = getAta(poolAuthority, _rewardMint, tokenProgram);
|
|
920
1390
|
return this.program.methods
|
|
921
1391
|
.addPoolSecondaryReward(new BN(pid), new BN(poolId), toBN(baseRate), lockTierRates.map(toBN))
|
|
922
1392
|
.accountsStrict({
|
|
923
1393
|
pool: poolPda,
|
|
1394
|
+
project,
|
|
1395
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
924
1396
|
poolSecondary,
|
|
925
1397
|
poolAuthority,
|
|
926
1398
|
secondaryRewardMint: _rewardMint,
|
|
@@ -935,11 +1407,14 @@ export class NftStakingClient {
|
|
|
935
1407
|
async removePoolSecondaryReward(poolId, rewardIndex, projectId) {
|
|
936
1408
|
const pid = this.resolveProjectId(projectId);
|
|
937
1409
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
938
|
-
const [
|
|
1410
|
+
const [project] = getProjectPda(pid);
|
|
1411
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
939
1412
|
return this.program.methods
|
|
940
1413
|
.removePoolSecondaryReward(new BN(pid), new BN(poolId), rewardIndex)
|
|
941
1414
|
.accountsStrict({
|
|
942
1415
|
pool: poolPda,
|
|
1416
|
+
project,
|
|
1417
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
943
1418
|
poolSecondary,
|
|
944
1419
|
authority: this.provider.wallet.publicKey,
|
|
945
1420
|
})
|
|
@@ -950,10 +1425,12 @@ export class NftStakingClient {
|
|
|
950
1425
|
const _rewardMint = toPk(rewardMint);
|
|
951
1426
|
const tokenProgram = await this.resolveTokenProgram(_rewardMint);
|
|
952
1427
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
953
|
-
const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
|
|
954
|
-
const [poolAuthority] = getPoolAuthorityPda(poolId);
|
|
1428
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
1429
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
955
1430
|
const vault = getAta(poolAuthority, _rewardMint, tokenProgram);
|
|
956
|
-
const funderAta = opts?.funderTokenAccount
|
|
1431
|
+
const funderAta = opts?.funderTokenAccount
|
|
1432
|
+
? toPk(opts.funderTokenAccount)
|
|
1433
|
+
: getAta(this.provider.wallet.publicKey, _rewardMint, tokenProgram);
|
|
957
1434
|
return this.program.methods
|
|
958
1435
|
.fundSecondaryVault(new BN(pid), new BN(poolId), rewardIndex, toBN(amount))
|
|
959
1436
|
.accountsStrict({
|
|
@@ -973,14 +1450,19 @@ export class NftStakingClient {
|
|
|
973
1450
|
const _rewardMint = toPk(rewardMint);
|
|
974
1451
|
const tokenProgram = await this.resolveTokenProgram(_rewardMint);
|
|
975
1452
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
976
|
-
const [
|
|
977
|
-
const [
|
|
1453
|
+
const [project] = getProjectPda(pid);
|
|
1454
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
1455
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
978
1456
|
const vault = getAta(poolAuthority, _rewardMint, tokenProgram);
|
|
979
|
-
const destAta = opts?.destinationTokenAccount
|
|
1457
|
+
const destAta = opts?.destinationTokenAccount
|
|
1458
|
+
? toPk(opts.destinationTokenAccount)
|
|
1459
|
+
: getAta(this.provider.wallet.publicKey, _rewardMint, tokenProgram);
|
|
980
1460
|
return this.program.methods
|
|
981
1461
|
.withdrawSecondaryVault(new BN(pid), new BN(poolId), rewardIndex, toBN(amount))
|
|
982
1462
|
.accountsStrict({
|
|
983
1463
|
pool: poolPda,
|
|
1464
|
+
project,
|
|
1465
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
984
1466
|
poolSecondary,
|
|
985
1467
|
poolAuthority,
|
|
986
1468
|
secondaryVault: vault,
|
|
@@ -994,9 +1476,9 @@ export class NftStakingClient {
|
|
|
994
1476
|
async initStakerSecondary(poolId, projectId) {
|
|
995
1477
|
const pid = this.resolveProjectId(projectId);
|
|
996
1478
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
997
|
-
const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
|
|
1479
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
998
1480
|
const staker = this.provider.wallet.publicKey;
|
|
999
|
-
const [stakerSecondary] = getStakerSecondaryRewardsPda(poolId, staker);
|
|
1481
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
1000
1482
|
return this.program.methods
|
|
1001
1483
|
.initStakerSecondary(new BN(pid), new BN(poolId))
|
|
1002
1484
|
.accountsStrict({
|
|
@@ -1011,20 +1493,26 @@ export class NftStakingClient {
|
|
|
1011
1493
|
async claimSecondaryRewards(poolId, secondaryRewards, opts) {
|
|
1012
1494
|
const pid = this.resolveProjectId(opts?.projectId);
|
|
1013
1495
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
1014
|
-
const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
|
|
1496
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
1015
1497
|
const staker = this.provider.wallet.publicKey;
|
|
1016
|
-
const [stakerAccount] = getStakerAccountPda(poolId, staker);
|
|
1017
|
-
const [stakerSecondary] = getStakerSecondaryRewardsPda(poolId, staker);
|
|
1018
|
-
const [
|
|
1019
|
-
const
|
|
1498
|
+
const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
|
|
1499
|
+
const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
|
|
1500
|
+
const [userProfile] = getUserProfilePda(staker);
|
|
1501
|
+
const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
|
|
1502
|
+
const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
|
|
1020
1503
|
const [project] = getProjectPda(pid);
|
|
1021
1504
|
const remainingAccounts = [];
|
|
1505
|
+
let sharedTokenProgram = null;
|
|
1022
1506
|
for (const sr of secondaryRewards) {
|
|
1023
1507
|
const _srMint = toPk(sr.mint);
|
|
1024
1508
|
const tokenProgram = await this.resolveTokenProgram(_srMint);
|
|
1509
|
+
if (sharedTokenProgram && !sharedTokenProgram.equals(tokenProgram)) {
|
|
1510
|
+
throw new Error("claimSecondaryRewards requires all secondary mints in one transaction to use the same token program");
|
|
1511
|
+
}
|
|
1512
|
+
sharedTokenProgram = tokenProgram;
|
|
1025
1513
|
const vault = getAta(poolAuthority, _srMint, tokenProgram);
|
|
1026
1514
|
const stakerAta = getAta(staker, _srMint, tokenProgram);
|
|
1027
|
-
remainingAccounts.push({ pubkey: vault, isSigner: false, isWritable: true }, { pubkey: stakerAta, isSigner: false, isWritable: true }, { pubkey: _srMint, isSigner: false, isWritable: false }
|
|
1515
|
+
remainingAccounts.push({ pubkey: vault, isSigner: false, isWritable: true }, { pubkey: stakerAta, isSigner: false, isWritable: true }, { pubkey: _srMint, isSigner: false, isWritable: false });
|
|
1028
1516
|
}
|
|
1029
1517
|
return this.program.methods
|
|
1030
1518
|
.claimSecondaryRewards(new BN(pid), new BN(poolId))
|
|
@@ -1042,7 +1530,8 @@ export class NftStakingClient {
|
|
|
1042
1530
|
adminProgram: feeAccts.adminProgram,
|
|
1043
1531
|
project,
|
|
1044
1532
|
staker,
|
|
1045
|
-
|
|
1533
|
+
userProfile,
|
|
1534
|
+
tokenProgram: sharedTokenProgram ?? TOKEN_PROGRAM_ID,
|
|
1046
1535
|
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
1047
1536
|
systemProgram: SystemProgram.programId,
|
|
1048
1537
|
})
|
|
@@ -1052,11 +1541,14 @@ export class NftStakingClient {
|
|
|
1052
1541
|
async updatePoolSecondaryReward(poolId, rewardIndex, baseRate, lockTierRates, projectId) {
|
|
1053
1542
|
const pid = this.resolveProjectId(projectId);
|
|
1054
1543
|
const [poolPda] = getStakePoolPda(pid, poolId);
|
|
1055
|
-
const [
|
|
1544
|
+
const [project] = getProjectPda(pid);
|
|
1545
|
+
const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
|
|
1056
1546
|
return this.program.methods
|
|
1057
1547
|
.updatePoolSecondaryReward(new BN(pid), new BN(poolId), rewardIndex, toBN(baseRate), lockTierRates.map(toBN))
|
|
1058
1548
|
.accountsStrict({
|
|
1059
1549
|
pool: poolPda,
|
|
1550
|
+
project,
|
|
1551
|
+
projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
|
|
1060
1552
|
poolSecondary,
|
|
1061
1553
|
authority: this.provider.wallet.publicKey,
|
|
1062
1554
|
})
|