@soltracer/nft-staking 0.2.1 → 0.2.4

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