@soltracer/nft-staking 0.2.0 → 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/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, getAta, decodeAccount, getFeeConfigPda, getTreasuryPda, PROJECT_MANAGEMENT_PROGRAM_ID, ADMIN_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, MPL_CORE_PROGRAM_ID, } from "@soltracer/core";
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. */
@@ -32,13 +50,59 @@ function base58HashToArray(hash) {
32
50
  function getTreeConfigPda(merkleTree) {
33
51
  return PublicKey.findProgramAddressSync([merkleTree.toBytes()], BUBBLEGUM_PROGRAM_ID);
34
52
  }
53
+ function toPk(v) {
54
+ return typeof v === "string" ? new PublicKey(v) : v;
55
+ }
56
+ function toBN(v) {
57
+ return typeof v === "number" ? new BN(v) : v;
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
+ }
35
99
  export class NftStakingClient {
36
100
  program;
37
101
  provider;
38
102
  tokenProgramCache = new Map();
39
103
  mintDecimalsCache = new Map();
40
104
  poolCache = new Map();
41
- static POOL_CACHE_TTL = 30_000; // 30s
105
+ static POOL_CACHE_TTL = 30_000;
42
106
  /** Project ID bound to this client. Set via `create()` options or `setProjectId()`. */
43
107
  projectId;
44
108
  constructor(program, provider, projectId) {
@@ -79,6 +143,13 @@ export class NftStakingClient {
79
143
  this.poolCache.set(key, { data: pool, fetchedAt: Date.now() });
80
144
  return pool;
81
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
+ }
82
153
  /** Invalidate cached pool data (call after pool mutations). */
83
154
  invalidatePoolCache(poolId) {
84
155
  if (poolId !== undefined) {
@@ -98,10 +169,30 @@ export class NftStakingClient {
98
169
  if (cached)
99
170
  return cached;
100
171
  const acct = await this.provider.connection.getAccountInfo(mint);
101
- const result = acct?.owner.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
172
+ const result = acct?.owner.equals(TOKEN_2022_PROGRAM_ID)
173
+ ? TOKEN_2022_PROGRAM_ID
174
+ : TOKEN_PROGRAM_ID;
102
175
  this.tokenProgramCache.set(key, result);
103
176
  return result;
104
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
+ }
105
196
  /** Fetch the decimal precision of a token mint (cached). */
106
197
  async getMintDecimals(mint) {
107
198
  const key = mint.toBase58();
@@ -130,28 +221,17 @@ export class NftStakingClient {
130
221
  })),
131
222
  };
132
223
  }
133
- /** Hardcoded Pyth SOL/USD push oracle address (PriceUpdateV2). */
134
- static PYTH_SOL_USD_FEED = new PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
135
- /** Resolve fee PDA accounts for fee CPI. */
136
- resolveFeeAccounts(fee) {
137
- const programId = this.program.programId;
138
- const [feeConfig] = getFeeConfigPda(programId);
139
- const [treasury] = getTreasuryPda();
140
- let referralAccount = ADMIN_PROGRAM_ID;
141
- if (fee?.referralAccount) {
142
- try {
143
- referralAccount = new PublicKey(fee.referralAccount);
144
- }
145
- catch {
146
- }
147
- }
148
- return {
149
- feeConfig,
150
- treasury,
151
- referralAccount,
152
- solUsdPriceFeed: NftStakingClient.PYTH_SOL_USD_FEED,
153
- adminProgram: ADMIN_PROGRAM_ID,
154
- };
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
+ });
155
235
  }
156
236
  async fetchStakeConfig(projectId) {
157
237
  const pid = this.resolveProjectId(projectId);
@@ -171,15 +251,17 @@ export class NftStakingClient {
171
251
  const decimals = await this.getMintDecimals(new PublicKey(pool.rewardConfig.rewardMint));
172
252
  return this.applyPoolDecimals(pool, decimals);
173
253
  }
174
- async fetchStakeEntry(poolId, nftMint) {
175
- const [pda] = getStakeEntryPda(poolId, nftMint);
254
+ async fetchStakeEntry(poolId, nftMint, projectId) {
255
+ const pid = this.resolveProjectId(projectId);
256
+ const [pda] = getStakeEntryPda(pid, poolId, toPk(nftMint));
176
257
  const raw = await this.program.account.stakeEntry.fetchNullable(pda);
177
258
  if (!raw)
178
259
  return null;
179
260
  return decodeAccount(raw);
180
261
  }
181
- async fetchStakerAccount(poolId, wallet, rewardDecimals) {
182
- const [pda] = getStakerAccountPda(poolId, wallet);
262
+ async fetchStakerAccount(poolId, wallet, rewardDecimals, projectId) {
263
+ const pid = this.resolveProjectId(projectId);
264
+ const [pda] = getStakerAccountPda(pid, poolId, toPk(wallet));
183
265
  const raw = await this.program.account.stakerAccount.fetchNullable(pda);
184
266
  if (!raw)
185
267
  return null;
@@ -210,22 +292,224 @@ export class NftStakingClient {
210
292
  return pools;
211
293
  }
212
294
  /** Fetch all active stake entries for a wallet in a specific pool via gPA. */
213
- async fetchStakeEntriesByOwner(_poolId, owner) {
295
+ async fetchStakeEntriesByOwner(poolId, owner, projectId) {
296
+ const pid = this.resolveProjectId(projectId);
297
+ const _owner = toPk(owner);
298
+ const [poolPda] = getStakePoolPda(pid, poolId);
214
299
  const entries = await this.program.account.stakeEntry.all([
215
- { memcmp: { offset: 72, bytes: owner.toBase58() } },
300
+ { memcmp: { offset: 72, bytes: _owner.toBase58() } },
216
301
  ]);
217
302
  return entries
218
303
  .map(({ account }) => decodeAccount(account))
219
- .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
+ };
220
503
  }
221
504
  /**
222
505
  * Fetch active stake entries for specific NFT mints by deriving their PDAs.
223
506
  * More reliable than gPA in browser environments.
224
507
  */
225
- async fetchStakeEntriesForMints(poolId, nftMints) {
508
+ async fetchStakeEntriesForMints(poolId, nftMints, projectId) {
226
509
  if (nftMints.length === 0)
227
510
  return [];
228
- const pdas = nftMints.map((mint) => getStakeEntryPda(poolId, mint)[0]);
511
+ const pid = this.resolveProjectId(projectId);
512
+ const pdas = nftMints.map((mint) => getStakeEntryPda(pid, poolId, toPk(mint))[0]);
229
513
  const accounts = await this.program.account.stakeEntry.fetchMultiple(pdas);
230
514
  return accounts
231
515
  .filter((a) => a !== null)
@@ -245,13 +529,14 @@ export class NftStakingClient {
245
529
  * Fetch active stake entries for given mints across multiple pools.
246
530
  * Uses PDA derivation + fetchMultiple (reliable in browser).
247
531
  */
248
- async fetchStakeEntriesAcrossPools(poolIds, nftMints) {
532
+ async fetchStakeEntriesAcrossPools(poolIds, nftMints, projectId) {
249
533
  if (nftMints.length === 0 || poolIds.length === 0)
250
534
  return [];
535
+ const pid = this.resolveProjectId(projectId);
251
536
  const pdas = [];
252
537
  for (const poolId of poolIds) {
253
538
  for (const mint of nftMints) {
254
- pdas.push(getStakeEntryPda(poolId, mint)[0]);
539
+ pdas.push(getStakeEntryPda(pid, poolId, mint)[0]);
255
540
  }
256
541
  }
257
542
  const accounts = await this.program.account.stakeEntry.fetchMultiple(pdas);
@@ -270,11 +555,16 @@ export class NftStakingClient {
270
555
  return stakers.map(({ account }) => decodeAccount(account));
271
556
  }
272
557
  async closeLegacyCollection(collectionMint, projectId) {
558
+ const _collMint = toPk(collectionMint);
273
559
  const pid = this.resolveProjectId(projectId);
274
- const [collection] = PublicKey.findProgramAddressSync([Buffer.from("project_collection"), new BN(pid).toArrayLike(Buffer, "le", 8), collectionMint.toBuffer()], this.program.programId);
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);
275
565
  const [project] = getProjectPda(pid);
276
566
  return this.program.methods
277
- .closeLegacyCollection(new BN(pid), collectionMint)
567
+ .closeLegacyCollection(new BN(pid), _collMint)
278
568
  .accountsStrict({
279
569
  collection,
280
570
  authority: this.provider.wallet.publicKey,
@@ -299,44 +589,50 @@ export class NftStakingClient {
299
589
  .instruction();
300
590
  }
301
591
  async createStakePool(stakingMode, rewardConfig, lockConfigs, collectionMint, opts) {
592
+ const _collMint = toPk(collectionMint);
593
+ const _rewardMint = toPk(rewardConfig.rewardMint);
302
594
  const pid = this.resolveProjectId(opts?.projectId);
303
595
  const rewardEndAt = opts?.rewardEndAt ?? 0;
304
596
  const maxStaked = opts?.maxStaked ?? 0;
597
+ const allowUnlockedStaking = opts?.allowUnlockedStaking ?? true;
305
598
  const config = await this.fetchStakeConfig(pid);
306
599
  const nextPoolId = config ? config.totalPools : 0;
307
- const tokenProgram = await this.resolveTokenProgram(rewardConfig.rewardMint);
308
- const decimals = await this.getMintDecimals(rewardConfig.rewardMint);
600
+ const tokenProgram = await this.resolveTokenProgram(_rewardMint);
601
+ const decimals = await this.getMintDecimals(_rewardMint);
309
602
  const multiplier = 10 ** decimals;
310
603
  const rawRewardConfig = {
311
604
  rewardType: rewardConfig.rewardType,
312
- rewardMint: rewardConfig.rewardMint,
605
+ rewardMint: _rewardMint,
313
606
  baseRate: new BN(Math.round(rewardConfig.baseRate * multiplier)),
314
607
  rateInterval: new BN(rewardConfig.rateInterval),
315
608
  traitBonusMode: rewardConfig.traitBonusMode,
316
- quantityThresholds: rewardConfig.quantityThresholds,
609
+ quantityThresholds: rewardConfig.quantityThresholds.map(normalizeQuantityBonus),
317
610
  };
318
611
  const rawLockConfigs = lockConfigs.map((lc) => ({
319
612
  lockDuration: new BN(lc.lockDuration),
320
613
  rewardRate: new BN(Math.round(lc.rewardRate * multiplier)),
321
614
  earlyUnstakePenaltyBps: lc.earlyUnstakePenaltyBps,
615
+ claimOnlyAtEnd: lc.claimOnlyAtEnd,
322
616
  }));
323
617
  const [configPda] = getStakeConfigPda(pid);
324
618
  const [poolPda] = getStakePoolPda(pid, nextPoolId);
325
- const [collectionConfigPda] = getCollectionPda(pid, collectionMint);
326
- const [poolAuthority] = getPoolAuthorityPda(nextPoolId);
327
- const rewardVault = getAta(poolAuthority, rewardConfig.rewardMint, tokenProgram);
619
+ const [collectionConfigPda] = getCollectionPda(pid, _collMint);
620
+ const [poolAuthority] = getPoolAuthorityPda(pid, nextPoolId);
621
+ const rewardVault = getAta(poolAuthority, _rewardMint, tokenProgram);
622
+ const [project] = getProjectPda(pid);
328
623
  const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
329
624
  return this.program.methods
330
- .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)
331
626
  .accountsStrict({
332
627
  config: configPda,
333
628
  pool: poolPda,
334
629
  collectionConfig: collectionConfigPda,
335
630
  poolAuthority,
336
- collectionMint,
337
- rewardMint: rewardConfig.rewardMint,
631
+ collectionMint: _collMint,
632
+ rewardMint: _rewardMint,
338
633
  rewardVault,
339
634
  utilityConfig,
635
+ project,
340
636
  projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
341
637
  authority: this.provider.wallet.publicKey,
342
638
  tokenProgram,
@@ -350,33 +646,36 @@ export class NftStakingClient {
350
646
  const rewardConfig = updates.rewardConfig ?? null;
351
647
  const lockConfigs = updates.lockConfigs ?? null;
352
648
  const isActive = updates.isActive ?? null;
353
- const traitAuthority = updates.traitAuthority ?? null;
649
+ const traitAuthority = updates.traitAuthority != null ? toPk(updates.traitAuthority) : null;
354
650
  const canBurn = updates.canBurn ?? null;
355
651
  const merkleRoot = updates.merkleRoot ?? null;
356
652
  const gateType = updates.gateType ?? null;
357
653
  const rewardEndAt = updates.rewardEndAt != null ? new BN(updates.rewardEndAt) : null;
358
654
  const maxStaked = updates.maxStaked != null ? new BN(updates.maxStaked) : null;
655
+ const allowUnlockedStaking = updates.allowUnlockedStaking ?? null;
359
656
  let rawRewardConfig = null;
360
657
  let rawLockConfigs = null;
361
658
  if (rewardConfig) {
362
- const decimals = await this.getMintDecimals(rewardConfig.rewardMint);
659
+ const _rMint = toPk(rewardConfig.rewardMint);
660
+ const decimals = await this.getMintDecimals(_rMint);
363
661
  const multiplier = 10 ** decimals;
364
662
  rawRewardConfig = {
365
663
  rewardType: rewardConfig.rewardType,
366
- rewardMint: rewardConfig.rewardMint,
664
+ rewardMint: _rMint,
367
665
  baseRate: new BN(Math.round(rewardConfig.baseRate * multiplier)),
368
666
  rateInterval: new BN(rewardConfig.rateInterval),
369
667
  traitBonusMode: rewardConfig.traitBonusMode,
370
- quantityThresholds: rewardConfig.quantityThresholds,
668
+ quantityThresholds: rewardConfig.quantityThresholds.map(normalizeQuantityBonus),
371
669
  };
372
670
  }
373
671
  if (lockConfigs && rewardConfig) {
374
- const decimals = await this.getMintDecimals(rewardConfig.rewardMint);
672
+ const decimals = await this.getMintDecimals(toPk(rewardConfig.rewardMint));
375
673
  const multiplier = 10 ** decimals;
376
674
  rawLockConfigs = lockConfigs.map((lc) => ({
377
675
  lockDuration: new BN(lc.lockDuration),
378
676
  rewardRate: new BN(Math.round(lc.rewardRate * multiplier)),
379
677
  earlyUnstakePenaltyBps: lc.earlyUnstakePenaltyBps,
678
+ claimOnlyAtEnd: lc.claimOnlyAtEnd,
380
679
  }));
381
680
  }
382
681
  else if (lockConfigs) {
@@ -387,13 +686,17 @@ export class NftStakingClient {
387
686
  lockDuration: new BN(lc.lockDuration),
388
687
  rewardRate: new BN(Math.round(lc.rewardRate * multiplier)),
389
688
  earlyUnstakePenaltyBps: lc.earlyUnstakePenaltyBps,
689
+ claimOnlyAtEnd: lc.claimOnlyAtEnd,
390
690
  }));
391
691
  }
392
692
  const [poolPda] = getStakePoolPda(pid, poolId);
693
+ const [project] = getProjectPda(pid);
393
694
  return this.program.methods
394
- .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)
395
696
  .accountsStrict({
396
697
  pool: poolPda,
698
+ project,
699
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
397
700
  authority: this.provider.wallet.publicKey,
398
701
  })
399
702
  .instruction();
@@ -402,14 +705,18 @@ export class NftStakingClient {
402
705
  async fundRewardVault(poolId, amount, opts) {
403
706
  const pid = this.resolveProjectId(opts?.projectId);
404
707
  const pool = await this.getPoolData(pid, poolId);
405
- const rewardMint = opts?.rewardMint ?? new PublicKey(pool.rewardConfig.rewardMint);
708
+ const rewardMint = opts?.rewardMint
709
+ ? toPk(opts.rewardMint)
710
+ : new PublicKey(pool.rewardConfig.rewardMint);
406
711
  const tokenProgram = await this.resolveTokenProgram(rewardMint);
407
712
  const [poolPda] = getStakePoolPda(pid, poolId);
408
- const [poolAuthority] = getPoolAuthorityPda(poolId);
713
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
409
714
  const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
410
- const funderAta = opts?.funderTokenAccount ?? getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
715
+ const funderAta = opts?.funderTokenAccount
716
+ ? toPk(opts.funderTokenAccount)
717
+ : getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
411
718
  return this.program.methods
412
- .fundRewardVault(new BN(pid), new BN(poolId), amount)
719
+ .fundRewardVault(new BN(pid), new BN(poolId), toBN(amount))
413
720
  .accountsStrict({
414
721
  pool: poolPda,
415
722
  poolAuthority,
@@ -425,16 +732,23 @@ export class NftStakingClient {
425
732
  async withdrawRewardVault(poolId, amount, opts) {
426
733
  const pid = this.resolveProjectId(opts?.projectId);
427
734
  const pool = await this.getPoolData(pid, poolId);
428
- const rewardMint = opts?.rewardMint ?? new PublicKey(pool.rewardConfig.rewardMint);
735
+ const rewardMint = opts?.rewardMint
736
+ ? toPk(opts.rewardMint)
737
+ : new PublicKey(pool.rewardConfig.rewardMint);
429
738
  const tokenProgram = await this.resolveTokenProgram(rewardMint);
430
739
  const [poolPda] = getStakePoolPda(pid, poolId);
431
- const [poolAuthority] = getPoolAuthorityPda(poolId);
740
+ const [project] = getProjectPda(pid);
741
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
432
742
  const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
433
- const destAta = opts?.destinationTokenAccount ?? getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
743
+ const destAta = opts?.destinationTokenAccount
744
+ ? toPk(opts.destinationTokenAccount)
745
+ : getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
434
746
  return this.program.methods
435
- .withdrawRewardVault(new BN(pid), new BN(poolId), amount)
747
+ .withdrawRewardVault(new BN(pid), new BN(poolId), toBN(amount))
436
748
  .accountsStrict({
437
749
  pool: poolPda,
750
+ project,
751
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
438
752
  poolAuthority,
439
753
  rewardVault,
440
754
  destinationTokenAccount: destAta,
@@ -447,58 +761,70 @@ export class NftStakingClient {
447
761
  async closeStakePool(poolId, projectId) {
448
762
  const pid = this.resolveProjectId(projectId);
449
763
  const [poolPda] = getStakePoolPda(pid, poolId);
764
+ const [project] = getProjectPda(pid);
450
765
  return this.program.methods
451
766
  .closeStakePool(new BN(pid), new BN(poolId))
452
767
  .accountsStrict({
453
768
  pool: poolPda,
769
+ project,
770
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
771
+ rewardVault: null,
772
+ tokenProgram: null,
454
773
  authority: this.provider.wallet.publicKey,
455
774
  })
456
775
  .instruction();
457
776
  }
458
777
  /** Stake a legacy/pNFT. Auto-resolves stakingMode from pool. */
459
778
  async stakeNft(poolId, nftMint, opts) {
779
+ const _nftMint = toPk(nftMint);
460
780
  const pid = this.resolveProjectId(opts?.projectId);
461
781
  const pool = await this.getPoolData(pid, poolId);
462
782
  const stakingMode = pool.stakingMode;
463
783
  const lockTierIndex = opts?.lockTierIndex ?? null;
464
784
  const gateProof = opts?.gateProof ?? [];
465
785
  const [poolPda] = getStakePoolPda(pid, poolId);
466
- const [stakeEntry] = getStakeEntryPda(poolId, nftMint);
786
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftMint);
787
+ const [stakeLock] = getNftStakeLockPda(_nftMint);
467
788
  const staker = this.provider.wallet.publicKey;
468
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
469
- const [poolAuthority] = getPoolAuthorityPda(poolId);
470
- const nftTokenProgram = await this.resolveTokenProgram(nftMint);
471
- const stakerNftAccount = getAta(staker, nftMint, nftTokenProgram);
789
+ const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
790
+ const [userProfile] = getUserProfilePda(staker);
791
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
792
+ const nftTokenProgram = await this.resolveTokenProgram(_nftMint);
793
+ const stakerNftAccount = getAta(staker, _nftMint, nftTokenProgram);
472
794
  const isEscrow = stakingMode !== 1;
473
- const escrowNftAccount = isEscrow
474
- ? getAta(poolAuthority, nftMint, nftTokenProgram)
475
- : null;
476
- const nftMetadata = getMetadataPda(nftMint);
477
- const nftEdition = getEditionPda(nftMint);
478
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
795
+ const escrowNftAccount = isEscrow ? getAta(poolAuthority, _nftMint, nftTokenProgram) : null;
796
+ const nftMetadata = getMetadataPda(_nftMint);
797
+ const nftEdition = getEditionPda(_nftMint);
798
+ const { tokenRecord, destinationTokenRecord } = await this.resolvePNftTokenRecords(_nftMint, stakerNftAccount, escrowNftAccount);
799
+ const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
800
+ const [project] = getProjectPda(pid);
479
801
  const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
480
802
  return this.program.methods
481
- .stakeNft(new BN(pid), new BN(poolId), nftMint, lockTierIndex, gateProof)
803
+ .stakeNft(new BN(pid), new BN(poolId), _nftMint, lockTierIndex, gateProof)
482
804
  .accountsStrict({
483
805
  pool: poolPda,
484
806
  stakeEntry,
807
+ stakeLock,
485
808
  stakerAccount,
486
809
  poolAuthority,
487
- nftMint,
810
+ nftMint: _nftMint,
488
811
  stakerNftAccount,
489
812
  escrowNftAccount,
490
813
  nftMetadata,
491
814
  nftEdition,
492
815
  mplTokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
493
816
  sysvarInstructions: SYSVAR_INSTRUCTIONS_ID,
494
- tokenRecord: null,
495
- destinationTokenRecord: null,
817
+ tokenRecord,
818
+ destinationTokenRecord,
819
+ userProfile,
496
820
  utilityConfig,
497
821
  feeConfig: feeAccts.feeConfig,
822
+ programRegistry: feeAccts.programRegistry,
498
823
  treasury: feeAccts.treasury,
499
824
  referralAccount: feeAccts.referralAccount,
500
825
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
501
826
  adminProgram: feeAccts.adminProgram,
827
+ project,
502
828
  staker,
503
829
  tokenProgram: nftTokenProgram,
504
830
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
@@ -506,37 +832,99 @@ export class NftStakingClient {
506
832
  })
507
833
  .instruction();
508
834
  }
509
- /** Unstake a legacy/pNFT. Auto-resolves rewardMint and stakingMode from pool. */
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
+ */
510
877
  async unstakeNft(poolId, nftMint, opts) {
878
+ const _nftMint = toPk(nftMint);
511
879
  const pid = this.resolveProjectId(opts?.projectId);
512
880
  const pool = await this.getPoolData(pid, poolId);
513
881
  const rewardMint = new PublicKey(pool.rewardConfig.rewardMint);
514
882
  const stakingMode = pool.stakingMode;
515
883
  const rewardTokenProgram = await this.resolveTokenProgram(rewardMint);
516
- const nftTokenProgram = await this.resolveTokenProgram(nftMint);
884
+ const nftTokenProgram = await this.resolveTokenProgram(_nftMint);
517
885
  const [poolPda] = getStakePoolPda(pid, poolId);
518
- const [stakeEntry] = getStakeEntryPda(poolId, nftMint);
886
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftMint);
887
+ const [stakeLock] = getNftStakeLockPda(_nftMint);
519
888
  const staker = this.provider.wallet.publicKey;
520
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
521
- const [poolAuthority] = getPoolAuthorityPda(poolId);
522
- const stakerNftAccount = getAta(staker, nftMint, nftTokenProgram);
889
+ const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
890
+ const [userProfile] = getUserProfilePda(staker);
891
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
892
+ const stakerNftAccount = getAta(staker, _nftMint, nftTokenProgram);
523
893
  const isEscrow = stakingMode !== 1;
524
- const escrowNftAccount = isEscrow
525
- ? getAta(poolAuthority, nftMint, nftTokenProgram)
526
- : null;
894
+ const escrowNftAccount = isEscrow ? getAta(poolAuthority, _nftMint, nftTokenProgram) : null;
527
895
  const rewardVault = getAta(poolAuthority, rewardMint, rewardTokenProgram);
528
896
  const stakerRewardAccount = getAta(staker, rewardMint, rewardTokenProgram);
529
- const nftMetadata = getMetadataPda(nftMint);
530
- const nftEdition = getEditionPda(nftMint);
531
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
897
+ const nftMetadata = getMetadataPda(_nftMint);
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
+ }
532
919
  return this.program.methods
533
- .unstakeNft(new BN(pid), new BN(poolId), nftMint)
920
+ .unstakeNft(new BN(pid), new BN(poolId), _nftMint)
534
921
  .accountsStrict({
535
922
  pool: poolPda,
536
923
  stakeEntry,
924
+ stakeLock,
537
925
  stakerAccount,
538
926
  poolAuthority,
539
- nftMint,
927
+ nftMint: _nftMint,
540
928
  stakerNftAccount,
541
929
  escrowNftAccount,
542
930
  rewardVault,
@@ -546,19 +934,16 @@ export class NftStakingClient {
546
934
  nftEdition,
547
935
  mplTokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
548
936
  sysvarInstructions: SYSVAR_INSTRUCTIONS_ID,
549
- tokenRecord: null,
550
- destinationTokenRecord: null,
551
- feeConfig: feeAccts.feeConfig,
552
- treasury: feeAccts.treasury,
553
- referralAccount: feeAccts.referralAccount,
554
- solUsdPriceFeed: feeAccts.solUsdPriceFeed,
555
- adminProgram: feeAccts.adminProgram,
937
+ tokenRecord,
938
+ destinationTokenRecord,
556
939
  staker,
940
+ userProfile,
557
941
  tokenProgram: nftTokenProgram,
558
942
  rewardTokenProgram,
559
943
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
560
944
  systemProgram: SystemProgram.programId,
561
945
  })
946
+ .remainingAccounts(remainingAccounts)
562
947
  .instruction();
563
948
  }
564
949
  /** Claim accrued rewards. Auto-resolves rewardMint from pool. */
@@ -570,12 +955,14 @@ export class NftStakingClient {
570
955
  const tokenProgram = await this.resolveTokenProgram(rewardMint);
571
956
  const [poolPda] = getStakePoolPda(pid, poolId);
572
957
  const staker = this.provider.wallet.publicKey;
573
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
574
- const [poolAuthority] = getPoolAuthorityPda(poolId);
958
+ const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
959
+ const [userProfile] = getUserProfilePda(staker);
960
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
575
961
  const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
576
962
  const stakerRewardAccount = getAta(staker, rewardMint, tokenProgram);
577
963
  const [stakeConfig] = getStakeConfigPda(pid);
578
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
964
+ const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
965
+ const [project] = getProjectPda(pid);
579
966
  return this.program.methods
580
967
  .claimRewards(new BN(pid), new BN(poolId), traitBonusRate)
581
968
  .accountsStrict({
@@ -588,11 +975,14 @@ export class NftStakingClient {
588
975
  stakeConfig,
589
976
  instructionsSysvar: new PublicKey("Sysvar1nstructions1111111111111111111111111"),
590
977
  feeConfig: feeAccts.feeConfig,
978
+ programRegistry: feeAccts.programRegistry,
591
979
  treasury: feeAccts.treasury,
592
980
  referralAccount: feeAccts.referralAccount,
593
981
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
594
982
  adminProgram: feeAccts.adminProgram,
983
+ project,
595
984
  staker,
985
+ userProfile,
596
986
  tokenProgram,
597
987
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
598
988
  systemProgram: SystemProgram.programId,
@@ -601,84 +991,107 @@ export class NftStakingClient {
601
991
  }
602
992
  /** Stake a Core NFT. Auto-resolves collection from pool. */
603
993
  async stakeCoreNft(poolId, nftAsset, opts) {
994
+ const _nftAsset = toPk(nftAsset);
604
995
  const pid = this.resolveProjectId(opts?.projectId);
605
996
  const pool = await this.getPoolData(pid, poolId);
606
997
  const collection = new PublicKey(pool.collectionMint);
607
998
  const lockTierIndex = opts?.lockTierIndex ?? null;
608
999
  const gateProof = opts?.gateProof ?? [];
609
1000
  const [poolPda] = getStakePoolPda(pid, poolId);
610
- const [stakeEntry] = getStakeEntryPda(poolId, nftAsset);
1001
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftAsset);
1002
+ const [stakeLock] = getNftStakeLockPda(_nftAsset);
611
1003
  const staker = this.provider.wallet.publicKey;
612
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
613
- const [poolAuthority] = getPoolAuthorityPda(poolId);
614
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
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);
1008
+ const [project] = getProjectPda(pid);
615
1009
  const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
616
1010
  return this.program.methods
617
- .stakeCoreNft(new BN(pid), new BN(poolId), nftAsset, lockTierIndex, gateProof)
1011
+ .stakeCoreNft(new BN(pid), new BN(poolId), _nftAsset, lockTierIndex, gateProof)
618
1012
  .accountsStrict({
619
1013
  pool: poolPda,
620
1014
  stakeEntry,
1015
+ stakeLock,
621
1016
  stakerAccount,
622
1017
  poolAuthority,
623
- nftAsset,
1018
+ nftAsset: _nftAsset,
624
1019
  collection,
625
1020
  mplCoreProgram: MPL_CORE_PROGRAM_ID,
626
1021
  utilityConfig,
627
1022
  feeConfig: feeAccts.feeConfig,
1023
+ programRegistry: feeAccts.programRegistry,
628
1024
  treasury: feeAccts.treasury,
629
1025
  referralAccount: feeAccts.referralAccount,
630
1026
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
631
1027
  adminProgram: feeAccts.adminProgram,
1028
+ project,
632
1029
  staker,
1030
+ userProfile,
633
1031
  systemProgram: SystemProgram.programId,
634
1032
  })
635
1033
  .instruction();
636
1034
  }
637
1035
  /** Unstake a Core NFT. Auto-resolves collection and rewardMint from pool. */
638
1036
  async unstakeCoreNft(poolId, nftAsset, opts) {
1037
+ const _nftAsset = toPk(nftAsset);
639
1038
  const pid = this.resolveProjectId(opts?.projectId);
640
1039
  const pool = await this.getPoolData(pid, poolId);
641
1040
  const collection = new PublicKey(pool.collectionMint);
642
1041
  const rewardMint = new PublicKey(pool.rewardConfig.rewardMint);
643
1042
  const rewardTokenProgram = await this.resolveTokenProgram(rewardMint);
644
1043
  const [poolPda] = getStakePoolPda(pid, poolId);
645
- const [stakeEntry] = getStakeEntryPda(poolId, nftAsset);
1044
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftAsset);
1045
+ const [stakeLock] = getNftStakeLockPda(_nftAsset);
646
1046
  const staker = this.provider.wallet.publicKey;
647
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
648
- const [poolAuthority] = getPoolAuthorityPda(poolId);
1047
+ const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
1048
+ const [userProfile] = getUserProfilePda(staker);
1049
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
649
1050
  const rewardVault = getAta(poolAuthority, rewardMint, rewardTokenProgram);
650
1051
  const stakerRewardAccount = getAta(staker, rewardMint, rewardTokenProgram);
651
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
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
+ }
652
1067
  return this.program.methods
653
- .unstakeCoreNft(new BN(pid), new BN(poolId), nftAsset)
1068
+ .unstakeCoreNft(new BN(pid), new BN(poolId), _nftAsset)
654
1069
  .accountsStrict({
655
1070
  pool: poolPda,
656
1071
  stakeEntry,
1072
+ stakeLock,
657
1073
  stakerAccount,
658
1074
  poolAuthority,
659
- nftAsset,
1075
+ nftAsset: _nftAsset,
660
1076
  collection,
661
1077
  mplCoreProgram: MPL_CORE_PROGRAM_ID,
662
1078
  rewardVault,
663
1079
  stakerRewardAccount,
664
1080
  rewardMint,
665
- feeConfig: feeAccts.feeConfig,
666
- treasury: feeAccts.treasury,
667
- referralAccount: feeAccts.referralAccount,
668
- solUsdPriceFeed: feeAccts.solUsdPriceFeed,
669
- adminProgram: feeAccts.adminProgram,
670
1081
  staker,
1082
+ userProfile,
671
1083
  rewardTokenProgram,
672
1084
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
673
1085
  systemProgram: SystemProgram.programId,
674
1086
  })
1087
+ .remainingAccounts(remainingAccounts)
675
1088
  .instruction();
676
1089
  }
677
1090
  /** Stake a compressed NFT (cNFT). All address/hash params accepted as strings. */
678
1091
  async stakeCnft(poolId, cnftParams, opts) {
679
1092
  const pid = this.resolveProjectId(opts?.projectId);
680
1093
  const gateProof = opts?.gateProof ?? [];
681
- const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes } = cnftParams;
1094
+ const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes, } = cnftParams;
682
1095
  const nftAssetIdPk = new PublicKey(nftAssetId);
683
1096
  const merkleTreePk = new PublicKey(merkleTree);
684
1097
  const [treeConfigPk] = getTreeConfigPda(merkleTreePk);
@@ -686,11 +1099,14 @@ export class NftStakingClient {
686
1099
  const cnftDataHashArr = base58HashToArray(cnftDataHash);
687
1100
  const cnftCreatorHashArr = base58HashToArray(cnftCreatorHash);
688
1101
  const [poolPda] = getStakePoolPda(pid, poolId);
689
- const [stakeEntry] = getStakeEntryPda(poolId, nftAssetIdPk);
1102
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, nftAssetIdPk);
1103
+ const [stakeLock] = getNftStakeLockPda(nftAssetIdPk);
690
1104
  const staker = this.provider.wallet.publicKey;
691
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
692
- const [poolAuthority] = getPoolAuthorityPda(poolId);
693
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
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);
1109
+ const [project] = getProjectPda(pid);
694
1110
  const [utilityConfig] = getUtilityConfigPda(pid, this.program.programId);
695
1111
  const remainingAccounts = proofNodes.map((node) => ({
696
1112
  pubkey: new PublicKey(node),
@@ -702,6 +1118,7 @@ export class NftStakingClient {
702
1118
  .accountsStrict({
703
1119
  pool: poolPda,
704
1120
  stakeEntry,
1121
+ stakeLock,
705
1122
  stakerAccount,
706
1123
  poolAuthority,
707
1124
  treeConfig: treeConfigPk,
@@ -711,11 +1128,14 @@ export class NftStakingClient {
711
1128
  bubblegumProgram: BUBBLEGUM_PROGRAM_ID,
712
1129
  utilityConfig,
713
1130
  feeConfig: feeAccts.feeConfig,
1131
+ programRegistry: feeAccts.programRegistry,
714
1132
  treasury: feeAccts.treasury,
715
1133
  referralAccount: feeAccts.referralAccount,
716
1134
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
717
1135
  adminProgram: feeAccts.adminProgram,
1136
+ project,
718
1137
  staker,
1138
+ userProfile,
719
1139
  systemProgram: SystemProgram.programId,
720
1140
  })
721
1141
  .remainingAccounts(remainingAccounts)
@@ -725,7 +1145,7 @@ export class NftStakingClient {
725
1145
  async unstakeCnft(poolId, cnftParams, opts) {
726
1146
  const pid = this.resolveProjectId(opts?.projectId);
727
1147
  const pool = await this.getPoolData(pid, poolId);
728
- const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes } = cnftParams;
1148
+ const { nftAssetId, merkleTree, cnftRoot, cnftDataHash, cnftCreatorHash, cnftNonce, cnftIndex, proofNodes, } = cnftParams;
729
1149
  const nftAssetIdPk = new PublicKey(nftAssetId);
730
1150
  const merkleTreePk = new PublicKey(merkleTree);
731
1151
  const [treeConfigPk] = getTreeConfigPda(merkleTreePk);
@@ -734,23 +1154,45 @@ export class NftStakingClient {
734
1154
  const cnftDataHashArr = base58HashToArray(cnftDataHash);
735
1155
  const cnftCreatorHashArr = base58HashToArray(cnftCreatorHash);
736
1156
  const [poolPda] = getStakePoolPda(pid, poolId);
737
- const [stakeEntry] = getStakeEntryPda(poolId, nftAssetIdPk);
1157
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, nftAssetIdPk);
1158
+ const [stakeLock] = getNftStakeLockPda(nftAssetIdPk);
738
1159
  const staker = this.provider.wallet.publicKey;
739
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
740
- const [poolAuthority] = getPoolAuthorityPda(poolId);
741
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
1160
+ const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
1161
+ const [userProfile] = getUserProfilePda(staker);
1162
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
742
1163
  const rewardVaultPk = getAta(poolAuthority, rewardMintPk);
743
1164
  const stakerRewardAccountPk = getAta(staker, rewardMintPk);
744
- const remainingAccounts = proofNodes.map((node) => ({
745
- pubkey: new PublicKey(node),
746
- isSigner: false,
747
- isWritable: false,
748
- }));
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
+ }
749
1190
  return this.program.methods
750
1191
  .unstakeCnft(new BN(pid), new BN(poolId), nftAssetIdPk, cnftRootArr, cnftDataHashArr, cnftCreatorHashArr, new BN(cnftNonce), cnftIndex)
751
1192
  .accountsStrict({
752
1193
  pool: poolPda,
753
1194
  stakeEntry,
1195
+ stakeLock,
754
1196
  stakerAccount,
755
1197
  poolAuthority,
756
1198
  treeConfig: treeConfigPk,
@@ -761,12 +1203,8 @@ export class NftStakingClient {
761
1203
  rewardVault: rewardVaultPk,
762
1204
  stakerRewardAccount: stakerRewardAccountPk,
763
1205
  rewardMint: rewardMintPk,
764
- feeConfig: feeAccts.feeConfig,
765
- treasury: feeAccts.treasury,
766
- referralAccount: feeAccts.referralAccount,
767
- solUsdPriceFeed: feeAccts.solUsdPriceFeed,
768
- adminProgram: feeAccts.adminProgram,
769
1206
  staker,
1207
+ userProfile,
770
1208
  tokenProgram: TOKEN_PROGRAM_ID,
771
1209
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
772
1210
  systemProgram: SystemProgram.programId,
@@ -774,31 +1212,57 @@ export class NftStakingClient {
774
1212
  .remainingAccounts(remainingAccounts)
775
1213
  .instruction();
776
1214
  }
777
- /** Burn a permanently-locked legacy/pNFT. Auto-resolves stakingMode from pool. */
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
+ */
778
1222
  async burnStakedNft(poolId, nftMint, opts) {
1223
+ const _nftMint = toPk(nftMint);
779
1224
  const pid = this.resolveProjectId(opts?.projectId);
780
1225
  const pool = await this.getPoolData(pid, poolId);
781
1226
  const stakingMode = pool.stakingMode;
782
- const nftTokenProgram = await this.resolveTokenProgram(nftMint);
1227
+ const nftTokenProgram = await this.resolveTokenProgram(_nftMint);
783
1228
  const [poolPda] = getStakePoolPda(pid, poolId);
784
- const [stakeEntry] = getStakeEntryPda(poolId, nftMint);
1229
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftMint);
1230
+ const [stakeLock] = getNftStakeLockPda(_nftMint);
785
1231
  const staker = this.provider.wallet.publicKey;
786
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
787
- const [poolAuthority] = getPoolAuthorityPda(poolId);
788
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
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);
1236
+ const [project] = getProjectPda(pid);
789
1237
  const isWalletLock = stakingMode === 1;
790
- const escrowNftAccount = isWalletLock ? null : getAta(poolAuthority, nftMint, nftTokenProgram);
791
- const stakerNftAccount = isWalletLock ? getAta(staker, nftMint, nftTokenProgram) : null;
792
- const nftMetadata = isWalletLock ? getMetadataPda(nftMint) : null;
793
- const nftEdition = isWalletLock ? getEditionPda(nftMint) : null;
1238
+ const escrowNftAccount = isWalletLock ? null : getAta(poolAuthority, _nftMint, nftTokenProgram);
1239
+ const stakerNftAccount = isWalletLock ? getAta(staker, _nftMint, nftTokenProgram) : null;
1240
+ const nftMetadata = isWalletLock ? getMetadataPda(_nftMint) : null;
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
+ }
794
1257
  return this.program.methods
795
- .burnStakedNft(new BN(pid), new BN(poolId), nftMint)
1258
+ .burnStakedNft(new BN(pid), new BN(poolId), _nftMint)
796
1259
  .accountsStrict({
797
1260
  pool: poolPda,
798
1261
  stakeEntry,
1262
+ stakeLock,
799
1263
  stakerAccount,
800
1264
  poolAuthority,
801
- nftMint,
1265
+ nftMint: _nftMint,
802
1266
  escrowNftAccount,
803
1267
  stakerNftAccount,
804
1268
  nftMetadata,
@@ -808,45 +1272,72 @@ export class NftStakingClient {
808
1272
  tokenRecord: null,
809
1273
  collectionMetadata: null,
810
1274
  feeConfig: feeAccts.feeConfig,
1275
+ programRegistry: feeAccts.programRegistry,
811
1276
  treasury: feeAccts.treasury,
812
1277
  referralAccount: feeAccts.referralAccount,
813
1278
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
814
1279
  adminProgram: feeAccts.adminProgram,
1280
+ project,
815
1281
  staker,
1282
+ userProfile,
816
1283
  tokenProgram: nftTokenProgram,
817
1284
  systemProgram: SystemProgram.programId,
818
1285
  })
1286
+ .remainingAccounts(remainingAccounts)
819
1287
  .instruction();
820
1288
  }
821
1289
  /** Burn a permanently-locked Core NFT. Auto-resolves collection from pool. */
822
1290
  async burnStakedCoreNft(poolId, nftAsset, opts) {
1291
+ const _nftAsset = toPk(nftAsset);
823
1292
  const pid = this.resolveProjectId(opts?.projectId);
824
1293
  const pool = await this.getPoolData(pid, poolId);
825
1294
  const collection = new PublicKey(pool.collectionMint);
826
1295
  const [poolPda] = getStakePoolPda(pid, poolId);
827
- const [stakeEntry] = getStakeEntryPda(poolId, nftAsset);
1296
+ const [stakeEntry] = getStakeEntryPda(pid, poolId, _nftAsset);
1297
+ const [stakeLock] = getNftStakeLockPda(_nftAsset);
828
1298
  const staker = this.provider.wallet.publicKey;
829
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
830
- const [poolAuthority] = getPoolAuthorityPda(poolId);
831
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
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);
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
+ }
832
1318
  return this.program.methods
833
- .burnStakedCoreNft(new BN(pid), new BN(poolId), nftAsset)
1319
+ .burnStakedCoreNft(new BN(pid), new BN(poolId), _nftAsset)
834
1320
  .accountsStrict({
835
1321
  pool: poolPda,
836
1322
  stakeEntry,
1323
+ stakeLock,
837
1324
  stakerAccount,
838
1325
  poolAuthority,
839
- nftAsset,
1326
+ nftAsset: _nftAsset,
840
1327
  collection,
841
1328
  mplCoreProgram: MPL_CORE_PROGRAM_ID,
842
1329
  feeConfig: feeAccts.feeConfig,
1330
+ programRegistry: feeAccts.programRegistry,
843
1331
  treasury: feeAccts.treasury,
844
1332
  referralAccount: feeAccts.referralAccount,
845
1333
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
846
1334
  adminProgram: feeAccts.adminProgram,
1335
+ project,
847
1336
  staker,
1337
+ userProfile,
848
1338
  systemProgram: SystemProgram.programId,
849
1339
  })
1340
+ .remainingAccounts(remainingAccounts)
850
1341
  .instruction();
851
1342
  }
852
1343
  /** Spend accrued points from a Points reward type pool. */
@@ -854,32 +1345,39 @@ export class NftStakingClient {
854
1345
  const pid = this.resolveProjectId(opts?.projectId);
855
1346
  const [poolPda] = getStakePoolPda(pid, poolId);
856
1347
  const staker = this.provider.wallet.publicKey;
857
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
858
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
1348
+ const [stakerAccount] = getStakerAccountPda(pid, poolId, staker);
1349
+ const [userProfile] = getUserProfilePda(staker);
1350
+ const feeAccts = await this.resolveFeeAccounts(pid, opts?.fee);
1351
+ const [project] = getProjectPda(pid);
859
1352
  return this.program.methods
860
- .spendPoints(new BN(pid), new BN(poolId), amount)
1353
+ .spendPoints(new BN(pid), new BN(poolId), toBN(amount))
861
1354
  .accountsStrict({
862
1355
  pool: poolPda,
863
1356
  stakerAccount,
864
1357
  feeConfig: feeAccts.feeConfig,
1358
+ programRegistry: feeAccts.programRegistry,
865
1359
  treasury: feeAccts.treasury,
866
1360
  referralAccount: feeAccts.referralAccount,
867
1361
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
868
1362
  adminProgram: feeAccts.adminProgram,
1363
+ project,
869
1364
  staker,
1365
+ userProfile,
870
1366
  systemProgram: SystemProgram.programId,
871
1367
  })
872
1368
  .instruction();
873
1369
  }
874
- async fetchPoolSecondaryRewards(poolId) {
875
- const [pda] = getPoolSecondaryRewardsPda(poolId);
1370
+ async fetchPoolSecondaryRewards(poolId, projectId) {
1371
+ const pid = this.resolveProjectId(projectId);
1372
+ const [pda] = getPoolSecondaryRewardsPda(pid, poolId);
876
1373
  const raw = await this.program.account.poolSecondaryRewards.fetchNullable(pda);
877
1374
  if (!raw)
878
1375
  return null;
879
1376
  return decodeAccount(raw);
880
1377
  }
881
- async fetchStakerSecondaryRewards(poolId, wallet) {
882
- const [pda] = getStakerSecondaryRewardsPda(poolId, wallet);
1378
+ async fetchStakerSecondaryRewards(poolId, wallet, projectId) {
1379
+ const pid = this.resolveProjectId(projectId);
1380
+ const [pda] = getStakerSecondaryRewardsPda(pid, poolId, toPk(wallet));
883
1381
  const raw = await this.program.account.stakerSecondaryRewards.fetchNullable(pda);
884
1382
  if (!raw)
885
1383
  return null;
@@ -887,18 +1385,22 @@ export class NftStakingClient {
887
1385
  }
888
1386
  async addPoolSecondaryReward(poolId, rewardMint, baseRate, lockTierRates, projectId) {
889
1387
  const pid = this.resolveProjectId(projectId);
1388
+ const _rewardMint = toPk(rewardMint);
890
1389
  const [poolPda] = getStakePoolPda(pid, poolId);
891
- const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
892
- const [poolAuthority] = getPoolAuthorityPda(poolId);
893
- const tokenProgram = await this.resolveTokenProgram(rewardMint);
894
- const vault = getAta(poolAuthority, rewardMint, tokenProgram);
1390
+ const [project] = getProjectPda(pid);
1391
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
1392
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
1393
+ const tokenProgram = await this.resolveTokenProgram(_rewardMint);
1394
+ const vault = getAta(poolAuthority, _rewardMint, tokenProgram);
895
1395
  return this.program.methods
896
- .addPoolSecondaryReward(new BN(pid), new BN(poolId), baseRate, lockTierRates)
1396
+ .addPoolSecondaryReward(new BN(pid), new BN(poolId), toBN(baseRate), lockTierRates.map(toBN))
897
1397
  .accountsStrict({
898
1398
  pool: poolPda,
1399
+ project,
1400
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
899
1401
  poolSecondary,
900
1402
  poolAuthority,
901
- secondaryRewardMint: rewardMint,
1403
+ secondaryRewardMint: _rewardMint,
902
1404
  secondaryVault: vault,
903
1405
  authority: this.provider.wallet.publicKey,
904
1406
  tokenProgram,
@@ -910,11 +1412,14 @@ export class NftStakingClient {
910
1412
  async removePoolSecondaryReward(poolId, rewardIndex, projectId) {
911
1413
  const pid = this.resolveProjectId(projectId);
912
1414
  const [poolPda] = getStakePoolPda(pid, poolId);
913
- const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
1415
+ const [project] = getProjectPda(pid);
1416
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
914
1417
  return this.program.methods
915
1418
  .removePoolSecondaryReward(new BN(pid), new BN(poolId), rewardIndex)
916
1419
  .accountsStrict({
917
1420
  pool: poolPda,
1421
+ project,
1422
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
918
1423
  poolSecondary,
919
1424
  authority: this.provider.wallet.publicKey,
920
1425
  })
@@ -922,21 +1427,24 @@ export class NftStakingClient {
922
1427
  }
923
1428
  async fundSecondaryVault(poolId, rewardIndex, amount, rewardMint, opts) {
924
1429
  const pid = this.resolveProjectId(opts?.projectId);
925
- const tokenProgram = await this.resolveTokenProgram(rewardMint);
1430
+ const _rewardMint = toPk(rewardMint);
1431
+ const tokenProgram = await this.resolveTokenProgram(_rewardMint);
926
1432
  const [poolPda] = getStakePoolPda(pid, poolId);
927
- const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
928
- const [poolAuthority] = getPoolAuthorityPda(poolId);
929
- const vault = getAta(poolAuthority, rewardMint, tokenProgram);
930
- const funderAta = opts?.funderTokenAccount ?? getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
1433
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
1434
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
1435
+ const vault = getAta(poolAuthority, _rewardMint, tokenProgram);
1436
+ const funderAta = opts?.funderTokenAccount
1437
+ ? toPk(opts.funderTokenAccount)
1438
+ : getAta(this.provider.wallet.publicKey, _rewardMint, tokenProgram);
931
1439
  return this.program.methods
932
- .fundSecondaryVault(new BN(pid), new BN(poolId), rewardIndex, amount)
1440
+ .fundSecondaryVault(new BN(pid), new BN(poolId), rewardIndex, toBN(amount))
933
1441
  .accountsStrict({
934
1442
  pool: poolPda,
935
1443
  poolSecondary,
936
1444
  poolAuthority,
937
1445
  secondaryVault: vault,
938
1446
  funderTokenAccount: funderAta,
939
- secondaryRewardMint: rewardMint,
1447
+ secondaryRewardMint: _rewardMint,
940
1448
  authority: this.provider.wallet.publicKey,
941
1449
  tokenProgram,
942
1450
  })
@@ -944,21 +1452,27 @@ export class NftStakingClient {
944
1452
  }
945
1453
  async withdrawSecondaryVault(poolId, rewardIndex, amount, rewardMint, opts) {
946
1454
  const pid = this.resolveProjectId(opts?.projectId);
947
- const tokenProgram = await this.resolveTokenProgram(rewardMint);
1455
+ const _rewardMint = toPk(rewardMint);
1456
+ const tokenProgram = await this.resolveTokenProgram(_rewardMint);
948
1457
  const [poolPda] = getStakePoolPda(pid, poolId);
949
- const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
950
- const [poolAuthority] = getPoolAuthorityPda(poolId);
951
- const vault = getAta(poolAuthority, rewardMint, tokenProgram);
952
- const destAta = opts?.destinationTokenAccount ?? getAta(this.provider.wallet.publicKey, rewardMint, tokenProgram);
1458
+ const [project] = getProjectPda(pid);
1459
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
1460
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
1461
+ const vault = getAta(poolAuthority, _rewardMint, tokenProgram);
1462
+ const destAta = opts?.destinationTokenAccount
1463
+ ? toPk(opts.destinationTokenAccount)
1464
+ : getAta(this.provider.wallet.publicKey, _rewardMint, tokenProgram);
953
1465
  return this.program.methods
954
- .withdrawSecondaryVault(new BN(pid), new BN(poolId), rewardIndex, amount)
1466
+ .withdrawSecondaryVault(new BN(pid), new BN(poolId), rewardIndex, toBN(amount))
955
1467
  .accountsStrict({
956
1468
  pool: poolPda,
1469
+ project,
1470
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
957
1471
  poolSecondary,
958
1472
  poolAuthority,
959
1473
  secondaryVault: vault,
960
1474
  destinationTokenAccount: destAta,
961
- secondaryRewardMint: rewardMint,
1475
+ secondaryRewardMint: _rewardMint,
962
1476
  authority: this.provider.wallet.publicKey,
963
1477
  tokenProgram,
964
1478
  })
@@ -967,9 +1481,9 @@ export class NftStakingClient {
967
1481
  async initStakerSecondary(poolId, projectId) {
968
1482
  const pid = this.resolveProjectId(projectId);
969
1483
  const [poolPda] = getStakePoolPda(pid, poolId);
970
- const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
1484
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
971
1485
  const staker = this.provider.wallet.publicKey;
972
- const [stakerSecondary] = getStakerSecondaryRewardsPda(poolId, staker);
1486
+ const [stakerSecondary] = getStakerSecondaryRewardsPda(pid, poolId, staker);
973
1487
  return this.program.methods
974
1488
  .initStakerSecondary(new BN(pid), new BN(poolId))
975
1489
  .accountsStrict({
@@ -984,18 +1498,26 @@ export class NftStakingClient {
984
1498
  async claimSecondaryRewards(poolId, secondaryRewards, opts) {
985
1499
  const pid = this.resolveProjectId(opts?.projectId);
986
1500
  const [poolPda] = getStakePoolPda(pid, poolId);
987
- const [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
1501
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
988
1502
  const staker = this.provider.wallet.publicKey;
989
- const [stakerAccount] = getStakerAccountPda(poolId, staker);
990
- const [stakerSecondary] = getStakerSecondaryRewardsPda(poolId, staker);
991
- const [poolAuthority] = getPoolAuthorityPda(poolId);
992
- const feeAccts = this.resolveFeeAccounts(opts?.fee);
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);
1508
+ const [project] = getProjectPda(pid);
993
1509
  const remainingAccounts = [];
1510
+ let sharedTokenProgram = null;
994
1511
  for (const sr of secondaryRewards) {
995
- const tokenProgram = await this.resolveTokenProgram(sr.mint);
996
- const vault = getAta(poolAuthority, sr.mint, tokenProgram);
997
- const stakerAta = getAta(staker, sr.mint, tokenProgram);
998
- remainingAccounts.push({ pubkey: vault, isSigner: false, isWritable: true }, { pubkey: stakerAta, isSigner: false, isWritable: true }, { pubkey: sr.mint, isSigner: false, isWritable: false }, { pubkey: tokenProgram, isSigner: false, isWritable: false });
1512
+ const _srMint = toPk(sr.mint);
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;
1518
+ const vault = getAta(poolAuthority, _srMint, tokenProgram);
1519
+ const stakerAta = getAta(staker, _srMint, tokenProgram);
1520
+ remainingAccounts.push({ pubkey: vault, isSigner: false, isWritable: true }, { pubkey: stakerAta, isSigner: false, isWritable: true }, { pubkey: _srMint, isSigner: false, isWritable: false });
999
1521
  }
1000
1522
  return this.program.methods
1001
1523
  .claimSecondaryRewards(new BN(pid), new BN(poolId))
@@ -1006,17 +1528,36 @@ export class NftStakingClient {
1006
1528
  stakerSecondary,
1007
1529
  poolAuthority,
1008
1530
  feeConfig: feeAccts.feeConfig,
1531
+ programRegistry: feeAccts.programRegistry,
1009
1532
  treasury: feeAccts.treasury,
1010
1533
  referralAccount: feeAccts.referralAccount,
1011
1534
  solUsdPriceFeed: feeAccts.solUsdPriceFeed,
1012
1535
  adminProgram: feeAccts.adminProgram,
1536
+ project,
1013
1537
  staker,
1014
- tokenProgram: TOKEN_PROGRAM_ID,
1538
+ userProfile,
1539
+ tokenProgram: sharedTokenProgram ?? TOKEN_PROGRAM_ID,
1015
1540
  associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
1016
1541
  systemProgram: SystemProgram.programId,
1017
1542
  })
1018
1543
  .remainingAccounts(remainingAccounts)
1019
1544
  .instruction();
1020
1545
  }
1546
+ async updatePoolSecondaryReward(poolId, rewardIndex, baseRate, lockTierRates, projectId) {
1547
+ const pid = this.resolveProjectId(projectId);
1548
+ const [poolPda] = getStakePoolPda(pid, poolId);
1549
+ const [project] = getProjectPda(pid);
1550
+ const [poolSecondary] = getPoolSecondaryRewardsPda(pid, poolId);
1551
+ return this.program.methods
1552
+ .updatePoolSecondaryReward(new BN(pid), new BN(poolId), rewardIndex, toBN(baseRate), lockTierRates.map(toBN))
1553
+ .accountsStrict({
1554
+ pool: poolPda,
1555
+ project,
1556
+ projectManagementProgram: PROJECT_MANAGEMENT_PROGRAM_ID,
1557
+ poolSecondary,
1558
+ authority: this.provider.wallet.publicKey,
1559
+ })
1560
+ .instruction();
1561
+ }
1021
1562
  }
1022
1563
  //# sourceMappingURL=client.js.map