@soltracer/nft-staking 0.2.8 → 0.3.0

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/idl.json CHANGED
@@ -6193,7 +6193,7 @@
6193
6193
  {
6194
6194
  "name": "sync_stake_entry_secondary",
6195
6195
  "docs": [
6196
- "STAKE-FRESH-2026-05-11 (F-H4): re-sync one StakeEntry's contribution to",
6196
+ "re-sync one StakeEntry's contribution to",
6197
6197
  "`pool_secondary.rewards[secondary_index]`. Permissionless. Lets",
6198
6198
  "existing stakers begin accruing on a secondary that was added",
6199
6199
  "post-stake (or pick up a rate-update) without unstaking."
@@ -9025,7 +9025,7 @@
9025
9025
  {
9026
9026
  "name": "retired_at",
9027
9027
  "docs": [
9028
- "STAKE-FRESH-2026-05-11 (F-H3): unix timestamp at which the slot was",
9028
+ "unix timestamp at which the slot was",
9029
9029
  "retired. `0` while active. Used by `accrue_secondary` /",
9030
9030
  "`accrue_and_sub_secondary` to cap per-staker accrual at the moment of",
9031
9031
  "retirement so the time window between a staker's last interaction and",
@@ -9131,7 +9131,7 @@
9131
9131
  {
9132
9132
  "name": "early_unstake_penalty_bps",
9133
9133
  "docs": [
9134
- "STAKE2-M3 (audit 2026-05-05): early-unstake penalty (bps) captured at",
9134
+ "early-unstake penalty (bps) captured at",
9135
9135
  "stake time. Read at unstake instead of `pool.lock_configs[tier].early_unstake_penalty_bps`",
9136
9136
  "so the authority cannot retroactively raise the penalty after the",
9137
9137
  "staker has committed. Zero when the entry has no lock."
@@ -9144,7 +9144,7 @@
9144
9144
  {
9145
9145
  "name": "StakeEntrySecondarySynced",
9146
9146
  "docs": [
9147
- "STAKE-FRESH-2026-05-11 (F-H4): emitted when a StakeEntry's",
9147
+ "emitted when a StakeEntry's",
9148
9148
  "secondary_rate_contributions[index] is re-synced to the pool's current",
9149
9149
  "config (e.g. after an admin added a new secondary slot post-stake)."
9150
9150
  ],
@@ -9309,7 +9309,7 @@
9309
9309
  {
9310
9310
  "name": "total_effective_rate",
9311
9311
  "docs": [
9312
- "STAKE-FRESH-2026-05-11 (F-H1): aggregate of every active staker's",
9312
+ "aggregate of every active staker's",
9313
9313
  "`effective_rate`. Maintained at every stake/unstake/burn site.",
9314
9314
  "Used by [`pool_settle`] to advance obligation for stakers who have",
9315
9315
  "not yet visited the pool since `last_pool_accrual_ts`."
@@ -9319,7 +9319,7 @@
9319
9319
  {
9320
9320
  "name": "last_pool_accrual_ts",
9321
9321
  "docs": [
9322
- "STAKE-FRESH-2026-05-11 (F-H1): unix timestamp at which the pool's",
9322
+ "unix timestamp at which the pool's",
9323
9323
  "total obligation was last advanced. Bumped by every handler that",
9324
9324
  "touches the pool (via [`pool_settle`])."
9325
9325
  ],
package/dist/index.d.ts CHANGED
@@ -4,6 +4,6 @@ export { type NftStaking as NftStakingIDLType } from "./idl";
4
4
  export { default as NftStakingIDL } from "./idl.json";
5
5
  export * from "./errors";
6
6
  export * from "./helpers";
7
- export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate } from "./traitProof";
7
+ export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate, computeTraitBonusRate, type TraitBonusCatalogEntry, type NftTraitInput, type ComputeTraitBonusResult, } from "./traitProof";
8
8
  export { GATE_TYPE_NONE, GATE_TYPE_WALLET, GATE_TYPE_TOKEN_MINT, type GateType, type MerkleTree, hashLeaf, hashPair, buildMerkleTree, getMerkleProof, proofToAnchorArg, rootToAnchorArg, } from "./merkle";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,KAAK,UAAU,IAAI,iBAAiB,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACnG,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,KAAK,UAAU,IAAI,iBAAiB,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA"}
package/dist/index.js CHANGED
@@ -3,6 +3,6 @@ export { NFT_STAKING_PROGRAM_ID, getStakeConfigPda, getStakePoolPda, getStakeEnt
3
3
  export { default as NftStakingIDL } from "./idl.json";
4
4
  export * from "./errors";
5
5
  export * from "./helpers";
6
- export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate } from "./traitProof";
6
+ export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate, computeTraitBonusRate, } from "./traitProof";
7
7
  export { GATE_TYPE_NONE, GATE_TYPE_WALLET, GATE_TYPE_TOKEN_MINT, hashLeaf, hashPair, buildMerkleTree, getMerkleProof, proofToAnchorArg, rootToAnchorArg, } from "./merkle";
8
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GAKjB,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACnG,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EAGpB,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GAKjB,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,GAItB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EAGpB,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA"}
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Trait-bonus proof builder for NFT-staking `claim_rewards`.
3
3
  *
4
- * Audit 2026-05-11 F-H2: the on-chain Ed25519 message embeds
5
- * `staker.total_claimed` (strictly monotonic) instead of `last_update_at`,
6
- * eliminating same-second replay. Callers MUST read the live
7
- * `StakerAccount.totalClaimed` immediately before signing.
4
+ * The on-chain Ed25519 message embeds `staker.total_claimed` (a strictly
5
+ * monotonic nonce). Callers MUST read the live `StakerAccount.totalClaimed`
6
+ * immediately before signing.
8
7
  *
9
8
  * Message layout — 80 bytes, little-endian where applicable:
10
9
  *
@@ -64,35 +63,94 @@ export declare function buildTraitProofMessage(params: {
64
63
  * The on-chain `claim_rewards` instruction accepts a **single** `u64`
65
64
  * `traitBonusRate` per claim — the chain knows nothing about per-trait
66
65
  * granularity. Aggregation across N staked NFTs (each with M traits) is the
67
- * caller's responsibility, and the math depends on the pool's
68
- * `rewardConfig.traitBonusMode`:
66
+ * trait-authority server's responsibility.
69
67
  *
70
- * - `0` / `None` — proofs disabled. Any non-zero rate is rejected.
71
- * - `1` / `FixedExtra` — `traitBonusRate` is **absolute extra reward per
72
- * `rateInterval`** added to the staker's effective rate for the claim
73
- * accrual window. Sum per-trait fixed bonuses across every active staked
74
- * NFT (and every bonus-bearing trait on each).
75
- * - `2` / `BaseMultiplier` — `traitBonusRate` is **bonus BPS** applied to the
76
- * staker's CURRENT `effective_rate` (already includes lock-tier rates).
77
- * `10_000` = +100% (2x base for the window). Sum per-trait BPS bonuses the
78
- * same way — the bonus is a scalar over the whole staker rate.
68
+ * Intended backend flow:
79
69
  *
80
- * `bonusBps` and `fixedExtra` fields are mutually exclusive; mixing both in
81
- * one trait entry throws. Traits without bonuses can be omitted.
70
+ * 1. Client sends `{ wallet, poolId, mints: [..] }` to your API.
71
+ * 2. API validates `mints` are currently staked by `wallet` in `pool`
72
+ * (e.g. via `fetchStakeEntriesByOwner`) and DROPS any it cannot prove.
73
+ * 3. API resolves each surviving NFT's trait list from its own metadata
74
+ * source (off-chain JSON, on-chain metaplex data, indexer, etc.).
75
+ * 4. API looks up each `(traitType, value)` pair in the project's bonus
76
+ * catalog. Unmatched traits contribute nothing.
77
+ * 5. API sums the matched bonuses according to the pool's
78
+ * `rewardConfig.traitBonusMode` and signs the resulting `u64`.
79
+ *
80
+ * Modes:
81
+ *
82
+ * - `0` / `None` — proofs disabled. Always returns `0n`.
83
+ * - `1` / `FixedExtra` — bonuses are **absolute reward per `rateInterval`**.
84
+ * Summed and added on top of the staker's effective rate for the accrual
85
+ * window. Catalog entries MUST use `fixedExtra`.
86
+ * - `2` / `BaseMultiplier` — bonuses are **BPS over the staker's current
87
+ * `effective_rate`** (which already includes lock-tier rates). `10_000`
88
+ * means +100% (2x base). Summed BPS apply once to the staker rate.
89
+ * Catalog entries MUST use `bonusBps`.
90
+ *
91
+ * Catalog entries with both `bonusBps` and `fixedExtra` set, or with the wrong
92
+ * field for the active mode, are rejected. Mints can repeat the same trait
93
+ * type/value — each NFT contributes independently.
94
+ *
95
+ * @returns `{ traitBonusRate, perNft }` where `perNft` is the per-mint
96
+ * breakdown (useful for receipts, debugging, or surfacing a tooltip in the UI).
82
97
  *
83
98
  * @example
84
99
  * ```ts
85
- * // Pool is in BaseMultiplier mode. Each staked NFT exposes its trait list.
86
- * const traits = [
87
- * // NFT 1 "Gold" head (+5%), "Laser Eyes" (+10%)
88
- * { bonusBps: 500 }, { bonusBps: 1000 },
89
- * // NFT 2 "Mythic Background" (+25%)
90
- * { bonusBps: 2500 },
91
- * ]
92
- * const rate = aggregateTraitBonusRate(traits, 2) // → 4000 (i.e. +40% for the window)
93
- * const message = buildTraitProofMessage({ pool, wallet, traitBonusRate: rate, totalClaimed })
100
+ * // Backend after ownership validation:
101
+ * const result = computeTraitBonusRate({
102
+ * traitBonusMode: pool.rewardConfig.traitBonusMode as 0 | 1 | 2,
103
+ * catalog: [
104
+ * { traitType: "Background", value: "Mythic", bonusBps: 2500 },
105
+ * { traitType: "Eyes", value: "Laser", bonusBps: 1000 },
106
+ * { traitType: "Head", value: "Gold Crown", bonusBps: 500 },
107
+ * ],
108
+ * nfts: validatedStakedNfts, // [{ mint, traits: [{ traitType, value }] }, ..]
109
+ * })
110
+ *
111
+ * const message = buildTraitProofMessage({
112
+ * pool, wallet,
113
+ * traitBonusRate: result.traitBonusRate,
114
+ * totalClaimed: stakerAccount.totalClaimed,
115
+ * })
94
116
  * ```
95
117
  */
118
+ export type TraitBonusCatalogEntry = {
119
+ traitType: string;
120
+ value: string;
121
+ /** Bonus BPS — required when `traitBonusMode === 2` (BaseMultiplier). */
122
+ bonusBps?: number | bigint;
123
+ /** Extra reward per `rateInterval` — required when `traitBonusMode === 1` (FixedExtra). */
124
+ fixedExtra?: number | bigint;
125
+ };
126
+ export type NftTraitInput = {
127
+ mint: PkInput | string;
128
+ /** Ordered or unordered list of an NFT's traits, after ownership validation. */
129
+ traits: ReadonlyArray<{
130
+ traitType: string;
131
+ value: string;
132
+ }>;
133
+ };
134
+ export type ComputeTraitBonusResult = {
135
+ /** Aggregated u64 rate to sign and include in the claim. */
136
+ traitBonusRate: bigint;
137
+ /** Per-NFT breakdown (mint → total bonus contributed + matched-trait count). */
138
+ perNft: Array<{
139
+ mint: string;
140
+ bonus: bigint;
141
+ matchedTraits: number;
142
+ }>;
143
+ };
144
+ export declare function computeTraitBonusRate(params: {
145
+ traitBonusMode: 0 | 1 | 2;
146
+ catalog: ReadonlyArray<TraitBonusCatalogEntry>;
147
+ nfts: ReadonlyArray<NftTraitInput>;
148
+ }): ComputeTraitBonusResult;
149
+ /**
150
+ * @deprecated Use `computeTraitBonusRate` instead — it accepts the real per-NFT
151
+ * trait input and a bonus catalog, mirroring the backend flow. Retained for
152
+ * call sites that already pre-resolved a flat bonus array.
153
+ */
96
154
  export declare function aggregateTraitBonusRate(traits: ReadonlyArray<{
97
155
  bonusBps?: number | bigint;
98
156
  fixedExtra?: number | bigint;
@@ -1 +1 @@
1
- {"version":3,"file":"traitProof.d.ts","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAElE,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAcrC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,cAAc,EAAE,OAAO,CAAA;IACvB,8DAA8D;IAC9D,YAAY,EAAE,OAAO,CAAA;CACtB,GAAG,MAAM,CAOT;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,EACnF,cAAc,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GACxB,MAAM,CAqCR"}
1
+ {"version":3,"file":"traitProof.d.ts","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAElE,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAcrC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,cAAc,EAAE,OAAO,CAAA;IACvB,8DAA8D;IAC9D,YAAY,EAAE,OAAO,CAAA;CACtB,GAAG,MAAM,CAOT;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC1B,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAA;IACtB,gFAAgF;IAChF,MAAM,EAAE,aAAa,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5D,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,4DAA4D;IAC5D,cAAc,EAAE,MAAM,CAAA;IACtB,gFAAgF;IAChF,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACtE,CAAA;AAiCD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,cAAc,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACzB,OAAO,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAA;IAC9C,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;CACnC,GAAG,uBAAuB,CAuC1B;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,EACnF,cAAc,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GACxB,MAAM,CAmCR"}
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Trait-bonus proof builder for NFT-staking `claim_rewards`.
3
3
  *
4
- * Audit 2026-05-11 F-H2: the on-chain Ed25519 message embeds
5
- * `staker.total_claimed` (strictly monotonic) instead of `last_update_at`,
6
- * eliminating same-second replay. Callers MUST read the live
7
- * `StakerAccount.totalClaimed` immediately before signing.
4
+ * The on-chain Ed25519 message embeds `staker.total_claimed` (a strictly
5
+ * monotonic nonce). Callers MUST read the live `StakerAccount.totalClaimed`
6
+ * immediately before signing.
8
7
  *
9
8
  * Message layout — 80 bytes, little-endian where applicable:
10
9
  *
@@ -45,64 +44,67 @@ export function buildTraitProofMessage(params) {
45
44
  u64LE(params.totalClaimed).copy(msg, 72);
46
45
  return msg;
47
46
  }
47
+ function normalizeKey(s) {
48
+ return s.trim().toLowerCase();
49
+ }
50
+ function bonusFromCatalogEntry(entry, traitBonusMode) {
51
+ const bps = BigInt(entry.bonusBps ?? 0);
52
+ const extra = BigInt(entry.fixedExtra ?? 0);
53
+ if (bps !== 0n && extra !== 0n) {
54
+ throw new Error(`computeTraitBonusRate: catalog entry "${entry.traitType}=${entry.value}" sets both bonusBps and fixedExtra; pick one.`);
55
+ }
56
+ if (traitBonusMode === 1) {
57
+ if (bps !== 0n) {
58
+ throw new Error(`computeTraitBonusRate: traitBonusMode = FixedExtra but catalog entry "${entry.traitType}=${entry.value}" uses bonusBps. Use fixedExtra (raw reward per interval).`);
59
+ }
60
+ return extra;
61
+ }
62
+ if (extra !== 0n) {
63
+ throw new Error(`computeTraitBonusRate: traitBonusMode = BaseMultiplier but catalog entry "${entry.traitType}=${entry.value}" uses fixedExtra. Use bonusBps (10_000 = +100%).`);
64
+ }
65
+ return bps;
66
+ }
67
+ export function computeTraitBonusRate(params) {
68
+ if (params.traitBonusMode === 0) {
69
+ return { traitBonusRate: 0n, perNft: [] };
70
+ }
71
+ const mode = params.traitBonusMode;
72
+ // Build a lookup keyed by normalized "type::value".
73
+ // Validates every catalog entry up-front so misconfigured entries are
74
+ // surfaced even if no NFT references them.
75
+ const lookup = new Map();
76
+ for (const entry of params.catalog) {
77
+ const key = `${normalizeKey(entry.traitType)}::${normalizeKey(entry.value)}`;
78
+ if (lookup.has(key)) {
79
+ throw new Error(`computeTraitBonusRate: duplicate catalog entry for "${entry.traitType}=${entry.value}".`);
80
+ }
81
+ lookup.set(key, bonusFromCatalogEntry(entry, mode));
82
+ }
83
+ let total = 0n;
84
+ const perNft = [];
85
+ for (const nft of params.nfts) {
86
+ let nftBonus = 0n;
87
+ let matched = 0;
88
+ for (const trait of nft.traits) {
89
+ const hit = lookup.get(`${normalizeKey(trait.traitType)}::${normalizeKey(trait.value)}`);
90
+ if (hit === undefined || hit === 0n)
91
+ continue;
92
+ nftBonus += hit;
93
+ matched += 1;
94
+ }
95
+ total += nftBonus;
96
+ perNft.push({
97
+ mint: typeof nft.mint === "string" ? nft.mint : toPk(nft.mint).toBase58(),
98
+ bonus: nftBonus,
99
+ matchedTraits: matched,
100
+ });
101
+ }
102
+ return { traitBonusRate: total, perNft };
103
+ }
48
104
  /**
49
- * Example (off-chain trait-authority server):
50
- *
51
- * ```ts
52
- * import nacl from "tweetnacl"
53
- * import { Ed25519Program } from "@solana/web3.js"
54
- * import { buildTraitProofMessage } from "@soltracer/nft-staking"
55
- *
56
- * const message = buildTraitProofMessage({
57
- * pool,
58
- * wallet: staker,
59
- * traitBonusRate,
60
- * totalClaimed: stakerAccount.totalClaimed,
61
- * })
62
- * const signature = nacl.sign.detached(message, traitAuthority.secretKey)
63
- * const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({
64
- * publicKey: traitAuthority.publicKey.toBytes(),
65
- * message,
66
- * signature,
67
- * })
68
- * const claimIx = await client.claimRewards(poolId, { traitBonusRate })
69
- * await provider.sendAndConfirm(new Transaction().add(ed25519Ix, claimIx))
70
- * ```
71
- */
72
- /**
73
- * Mode-aware trait-bonus rate aggregator.
74
- *
75
- * The on-chain `claim_rewards` instruction accepts a **single** `u64`
76
- * `traitBonusRate` per claim — the chain knows nothing about per-trait
77
- * granularity. Aggregation across N staked NFTs (each with M traits) is the
78
- * caller's responsibility, and the math depends on the pool's
79
- * `rewardConfig.traitBonusMode`:
80
- *
81
- * - `0` / `None` — proofs disabled. Any non-zero rate is rejected.
82
- * - `1` / `FixedExtra` — `traitBonusRate` is **absolute extra reward per
83
- * `rateInterval`** added to the staker's effective rate for the claim
84
- * accrual window. Sum per-trait fixed bonuses across every active staked
85
- * NFT (and every bonus-bearing trait on each).
86
- * - `2` / `BaseMultiplier` — `traitBonusRate` is **bonus BPS** applied to the
87
- * staker's CURRENT `effective_rate` (already includes lock-tier rates).
88
- * `10_000` = +100% (2x base for the window). Sum per-trait BPS bonuses the
89
- * same way — the bonus is a scalar over the whole staker rate.
90
- *
91
- * `bonusBps` and `fixedExtra` fields are mutually exclusive; mixing both in
92
- * one trait entry throws. Traits without bonuses can be omitted.
93
- *
94
- * @example
95
- * ```ts
96
- * // Pool is in BaseMultiplier mode. Each staked NFT exposes its trait list.
97
- * const traits = [
98
- * // NFT 1 — "Gold" head (+5%), "Laser Eyes" (+10%)
99
- * { bonusBps: 500 }, { bonusBps: 1000 },
100
- * // NFT 2 — "Mythic Background" (+25%)
101
- * { bonusBps: 2500 },
102
- * ]
103
- * const rate = aggregateTraitBonusRate(traits, 2) // → 4000 (i.e. +40% for the window)
104
- * const message = buildTraitProofMessage({ pool, wallet, traitBonusRate: rate, totalClaimed })
105
- * ```
105
+ * @deprecated Use `computeTraitBonusRate` instead — it accepts the real per-NFT
106
+ * trait input and a bonus catalog, mirroring the backend flow. Retained for
107
+ * call sites that already pre-resolved a flat bonus array.
106
108
  */
107
109
  export function aggregateTraitBonusRate(traits, traitBonusMode) {
108
110
  if (traitBonusMode === 0) {
@@ -119,14 +121,12 @@ export function aggregateTraitBonusRate(traits, traitBonusMode) {
119
121
  throw new Error("aggregateTraitBonusRate: a single trait must set bonusBps OR fixedExtra, not both.");
120
122
  }
121
123
  if (traitBonusMode === 1) {
122
- // FixedExtra — sum absolute per-interval bonuses.
123
124
  if (bps !== 0n) {
124
125
  throw new Error("aggregateTraitBonusRate: traitBonusMode = FixedExtra expects fixedExtra (per-interval reward), not bonusBps.");
125
126
  }
126
127
  sum += extra;
127
128
  }
128
129
  else {
129
- // BaseMultiplier — sum BPS bonuses.
130
130
  if (extra !== 0n) {
131
131
  throw new Error("aggregateTraitBonusRate: traitBonusMode = BaseMultiplier expects bonusBps (10_000 = +100%), not fixedExtra.");
132
132
  }
@@ -1 +1 @@
1
- {"version":3,"file":"traitProof.js","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"traitProof.js","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soltracer/nft-staking",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",