@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/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, getProgramRegistryPda, 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. */
@@ -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; // 30s
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) ? 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;
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
- /** Hardcoded Pyth SOL/USD push oracle address (PriceUpdateV2). */
140
- static PYTH_SOL_USD_FEED = new PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
141
- /** Resolve fee PDA accounts for fee CPI. */
142
- resolveFeeAccounts(fee) {
143
- const programId = this.program.programId;
144
- const [feeConfig] = getFeeConfigPda(programId);
145
- const [programRegistry] = getProgramRegistryPda(programId);
146
- const [treasury] = getTreasuryPda();
147
- let referralAccount = ADMIN_PROGRAM_ID;
148
- if (fee?.referralAccount) {
149
- try {
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 [pda] = getStakeEntryPda(poolId, toPk(nftMint));
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 [pda] = getStakerAccountPda(poolId, toPk(wallet));
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(_poolId, owner) {
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 pdas = nftMints.map((mint) => getStakeEntryPda(poolId, toPk(mint))[0]);
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([Buffer.from("project_collection"), new BN(pid).toArrayLike(Buffer, "le", 8), _collMint.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);
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 ? toPk(opts.rewardMint) : new PublicKey(pool.rewardConfig.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 ? toPk(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);
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 ? toPk(opts.rewardMint) : new PublicKey(pool.rewardConfig.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 [poolAuthority] = getPoolAuthorityPda(poolId);
740
+ const [project] = getProjectPda(pid);
741
+ const [poolAuthority] = getPoolAuthorityPda(pid, poolId);
445
742
  const rewardVault = getAta(poolAuthority, rewardMint, tokenProgram);
446
- const destAta = opts?.destinationTokenAccount ? toPk(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);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
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 feeAccts = this.resolveFeeAccounts(opts?.fee);
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: null,
512
- destinationTokenRecord: null,
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
- /** 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
+ */
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 [poolAuthority] = getPoolAuthorityPda(poolId);
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: null,
569
- destinationTokenRecord: null,
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 [poolAuthority] = getPoolAuthorityPda(poolId);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
632
- 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);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
709
- 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);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
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
- const remainingAccounts = proofNodes.map((node) => ({
763
- pubkey: new PublicKey(node),
764
- isSigner: false,
765
- isWritable: false,
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
- /** 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
+ */
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 [poolAuthority] = getPoolAuthorityPda(poolId);
802
- 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);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
849
- 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);
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 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);
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 [pda] = getPoolSecondaryRewardsPda(poolId);
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 [pda] = getStakerSecondaryRewardsPda(poolId, toPk(wallet));
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 [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
917
- const [poolAuthority] = getPoolAuthorityPda(poolId);
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 [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
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 ? toPk(opts.funderTokenAccount) : getAta(this.provider.wallet.publicKey, _rewardMint, tokenProgram);
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 [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
977
- const [poolAuthority] = getPoolAuthorityPda(poolId);
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 ? toPk(opts.destinationTokenAccount) : getAta(this.provider.wallet.publicKey, _rewardMint, tokenProgram);
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 [poolAuthority] = getPoolAuthorityPda(poolId);
1019
- 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);
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 }, { pubkey: tokenProgram, 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
- tokenProgram: TOKEN_PROGRAM_ID,
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 [poolSecondary] = getPoolSecondaryRewardsPda(poolId);
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
  })