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