Skip to content

feat(Rarity System): add automatic NFT Rarity rank and scoring based on traits distribution#1

Merged
vikiival merged 10 commits into
mainfrom
feat/pissue-26-rarity-rank-based
Feb 12, 2026
Merged

feat(Rarity System): add automatic NFT Rarity rank and scoring based on traits distribution#1
vikiival merged 10 commits into
mainfrom
feat/pissue-26-rarity-rank-based

Conversation

@hassnian
Copy link
Copy Markdown

@hassnian hassnian commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • NFT rarity fields added: score, rank, percentile and tier (Legendary, Epic, Rare, Uncommon, Common).
    • Rarity is recalculated automatically after mints, burns, and attribute updates and flushed during batch processing.
    • Background backfill is configurable with a toggle and per‑batch limit; DB migration adds indexes for the new rarity fields.
  • Tests

    • Added tests for scoring, ranking, percentile→tier mapping and edge cases.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a rarity system for NFTs by computing rarity scores/ranks/percentiles/tiers from trait distributions, persisting the results on NFTEntity, and triggering recalculation when collections change.

Changes:

  • Adds rarity fields to the GraphQL schema and generated NFTEntity model, plus a DB migration for new columns and indexes.
  • Introduces rarity computation + batch DB update utilities, with “dirty collection” tracking and flushing from the main mappings loop.
  • Hooks rarity dirty-marking into mint/burn/metadata/attribute handlers and adds Vitest coverage for determinism and edge cases.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/rarity.test.ts Adds unit tests for scoring, deterministic ranking, tier boundaries, and traitless edge cases.
src/model/generated/nftEntity.model.ts Extends the ORM model with nullable rarity fields and per-field indexes.
src/mappings/utils/rarity.ts Implements rarity scoring/ranking, dirty-collection tracking, and bulk SQL updates.
src/mappings/uniques/setMetadata.ts Marks collection rarity dirty on uniques metadata events.
src/mappings/uniques/setAttribute.ts Marks collection rarity dirty on uniques attribute changes.
src/mappings/uniques/mint.ts Marks collection rarity dirty on uniques mint.
src/mappings/uniques/burn.ts Marks collection rarity dirty on uniques burn.
src/mappings/nfts/setMetadata.ts Marks collection rarity dirty on nfts metadata events.
src/mappings/nfts/setAttribute.ts Marks collection rarity dirty on nfts attribute changes.
src/mappings/nfts/mint.ts Marks collection rarity dirty on nfts mint.
src/mappings/nfts/burn.ts Marks collection rarity dirty on nfts burn.
src/mappings/index.ts Flushes dirty rarity recomputation during batch processing.
schema.graphql Adds rarity fields to NFTEntity with @index.
db/migrations/1760160000000-Data.js Adds DB columns and composite indexes for collection-level rarity queries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mappings/uniques/setMetadata.ts
Comment thread db/migrations/1760160000000-Data.js
Comment thread src/mappings/index.ts Outdated
Comment thread src/mappings/nfts/setMetadata.ts
@hassnian
Copy link
Copy Markdown
Author

hassnian commented Feb 9, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Adds NFT rarity metrics: DB columns and indexes, GraphQL fields, event hooks marking collections dirty, a rarity utility that computes and persists scores/ranks/percentiles/tiers (with backfill and batching), and tests for the rarity logic.

Changes

Cohort / File(s) Summary
Database Migration & Schema
db/migrations/1760160000000-Data.js, schema.graphql
Adds four columns on nft_entity (rarity_score, rarity_rank, rarity_percentile, rarity_tier) and eight indexes; exposes four indexed fields in GraphQL.
Environment / Config
src/environment.ts
Adds asNonNegativeInteger helper and new exports RARITY_BACKFILL_ENABLED and RARITY_BACKFILL_PER_BATCH; includes them in runtime console output.
Event Handlers
src/mappings/nfts/mint.ts, src/mappings/nfts/burn.ts, src/mappings/nfts/setAttribute.ts, src/mappings/uniques/mint.ts, src/mappings/uniques/burn.ts, src/mappings/uniques/setAttribute.ts
Import and call markCollectionRarityDirty(...) after persisting token/collection changes to flag collections for rarity recalculation.
Rarity Utility
src/mappings/utils/rarity.ts
New module implementing rarity computation (score, rank, percentile, tier), helpers (asSn, attributeKey), dirty-collection cache, batch bulk-UPDATE to nft_entity, backfill via flushMissingCollectionRarity, and exports (calculateCollectionRarity, rarityTierFromPercentile, markCollectionRarityDirty, flushDirtyCollectionRarity, flushMissingCollectionRarity).
Integration
src/mappings/index.ts
Imports and awaits flushDirtyCollectionRarity and flushMissingCollectionRarity in mainFrame post block processing to persist/backfill rarity updates.
Tests
tests/rarity.test.ts
Adds tests for scoring/ranking determinism, percentile→tier boundaries, missing-trait handling, and traitless collection behavior.

Sequence Diagram

sequenceDiagram
    participant Event as Blockchain Event
    participant Handler as Event Handler
    participant Cache as Dirty Cache
    participant Frame as Main Frame
    participant Rarity as Rarity Service
    participant DB as Database

    Event->>Handler: emit (mint / burn / setAttribute)
    Handler->>Cache: markCollectionRarityDirty(collectionId)
    Cache-->>Handler: ack

    Note over Frame,Rarity: end of block processing
    Frame->>Rarity: flushDirtyCollectionRarity(store)
    activate Rarity
    Rarity->>DB: SELECT tokens + attributes for collection
    DB-->>Rarity: token rows
    Rarity->>Rarity: calculateCollectionRarity(tokens)
    Rarity->>DB: Bulk UPDATE nft_entity (score, rank, percentile, tier)
    DB-->>Rarity: update result
    Rarity-->>Frame: flush complete
    deactivate Rarity
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I counted traits by moon and sun,

scored each token, one by one,
marked the dirty, queued the tide,
flushed the ranks from far and wide,
now rarity dances, all done.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: introducing a rarity system that automatically ranks and scores NFTs based on trait distribution across collections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pissue-26-rarity-rank-based

No actionable comments were generated in the recent review. 🎉

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/environment.ts`:
- Around line 11-12: The RARITY_BACKFILL_PER_BATCH environment value can parse
to NaN and get passed into SQL LIMIT; update the initialization of
RARITY_BACKFILL_PER_BATCH to validate the parsed value (e.g., parse with
Number.parseInt, then check Number.isFinite/Number.isNaN or Number.isInteger and
ensure > 0) and fall back to a safe default (like 10) or clamp to a minimum (1)
when the value is invalid or <= 0; ensure consumers such as
flushMissingCollectionRarity receive a guaranteed positive integer.

In `@src/mappings/utils/rarity.ts`:
- Around line 238-250: The code clears dirtyCollectionIds before processing, so
if updateCollectionRarity(store, collectionId) throws, remaining dirty IDs are
lost; instead preserve pending IDs by not clearing the set up-front — iterate
over a snapshot (collectionIds = Array.from(dirtyCollectionIds)) but only remove
each collectionId from dirtyCollectionIds after a successful update (or on
success call dirtyCollectionIds.delete(collectionId)), and on error re-throw
without removing the failed and remaining IDs so they remain scheduled for
retry; use the existing logger.error and updateCollectionRarity references when
implementing the change.
🧹 Nitpick comments (2)
db/migrations/1760160000000-Data.js (1)

10-16: Consider adding composite index for (collection_id, rarity_score).

You have composite indexes for (collection_id, rarity_rank), (collection_id, rarity_tier), and (collection_id, rarity_percentile), but not for (collection_id, rarity_score). If you expect queries that filter or sort by rarity_score within a collection, you should add the corresponding composite index for consistency.

Proposed fix
         await db.query(`CREATE INDEX "IDX_nft_entity_collection_rarity_rank" ON "nft_entity" ("collection_id", "rarity_rank") `)
         await db.query(`CREATE INDEX "IDX_nft_entity_collection_rarity_tier" ON "nft_entity" ("collection_id", "rarity_tier") `)
         await db.query(`CREATE INDEX "IDX_nft_entity_collection_rarity_percentile" ON "nft_entity" ("collection_id", "rarity_percentile") `)
+        await db.query(`CREATE INDEX "IDX_nft_entity_collection_rarity_score" ON "nft_entity" ("collection_id", "rarity_score") `)

And in down():

     async down(db) {
+        await db.query(`DROP INDEX "public"."IDX_nft_entity_collection_rarity_score"`)
         await db.query(`DROP INDEX "public"."IDX_nft_entity_collection_rarity_percentile"`)
src/mappings/uniques/setAttribute.ts (1)

40-41: Consider skipping rarity marking for collection-level attribute changes.

When event.sn === undefined, the attribute is being set on the collection itself, not on an NFT. Collection-level attributes typically don't affect NFT rarity calculations (which are based on NFT trait distribution within the collection). Marking rarity dirty in this case triggers an unnecessary recalculation.

Proposed fix
   await context.store.save(final)
-  markCollectionRarityDirty(event.collectionId)
+  // Only mark rarity dirty for NFT attribute changes, not collection-level attributes
+  if (event.sn !== undefined) {
+    markCollectionRarityDirty(event.collectionId)
+  }

Comment thread src/environment.ts Outdated
Comment thread src/mappings/utils/rarity.ts
@hassnian
Copy link
Copy Markdown
Author

hassnian commented Feb 9, 2026

@coderabbitai review, and recheck comment you left

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 9, 2026

@hassnian: I'll review the changes in this PR and verify all my previous comments to ensure they're still relevant and accurate.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@vikiival vikiival left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that once in indexer crashes, you will lose the items in the set, and additionally, you are forced to recompute it almost every time.what I would do is to have it as a handler on the mint/setAttribute and once collection is done, I would recompute the whole rarity system

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mappings/nfts/setAttribute.ts Outdated
@hassnian
Copy link
Copy Markdown
Author

The problem is that once in indexer crashes, you will lose the items in the set, and additionally, you are forced to recompute it almost every time

from what i understand/checked is that the indexer processes batches transactionally and replays from last committed state after restart, so the in-memory dirty set is rebuilt during replay rather than permanently lost. let me know if im missing something

what I would do is to have it as a handler on the mint/setAttribute and once collection is done, I would recompute the whole rarity system

I agree with the recompute concern but also I can see some issues with “recompute only when collection is done”

  • “done” is not always a stable signal (for example collections without a max supply), and even minted collections can still change due to later item attribute updates or burns.
  • deferring all recompute until collection is minted would also leave rarity stale or null during the active minting phase, so during this period rarity sorting/filtering wont be available/not accurate.

The current batch dirty-recompute approach keeps data fresher while still controlling recompute frequency.


  • fix: we now mark rarity dirty only for item level Nfts.setAttribute, not collection-level attributes, so that avoids unnecessary full-collection recomputes.

@vikiival vikiival merged commit 845d0b6 into main Feb 12, 2026
1 check passed
@vikiival
Copy link
Copy Markdown

Fair thanks for the explanation!

Feel free to deploy imo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants