Skip to content

feat(ingester): add CAP-46 NFT indexing with metadata caching#87

Merged
Miracle656 merged 1 commit into
Miracle656:mainfrom
Ipramking:feat/nft-indexing
May 30, 2026
Merged

feat(ingester): add CAP-46 NFT indexing with metadata caching#87
Miracle656 merged 1 commit into
Miracle656:mainfrom
Ipramking:feat/nft-indexing

Conversation

@Ipramking
Copy link
Copy Markdown
Contributor

@Ipramking Ipramking commented May 29, 2026

Summary

  • NFT auto-detection via ABI sniff: events with 4 topics [Symbol(transfer), Address(from), Address(to), ScVal(token_id)] are classified as CAP-46 NFT transfers; SEP-41 fungible transfers (3 topics + i128 value) continue on the existing path unchanged
  • New src/ingester/nft.ts: isNftTransferEvent, parseNftTransferEvent, parseNftEvents, fetchNftMetadata (lazy simulation via token_uri / name contract calls)
  • Two new Prisma models: NftTransfer + NftMetadata with appropriate indexes
  • New DB helpers: upsertNftTransfers, queryNftTransfers, getNftOwner, upsertNftMetadata, getNftMetadata
  • Indexer splits events by topic count, processes both paths, and lazy-loads metadata per unique (contractId, tokenId) on first sight
  • Two new REST endpoints:
    • GET /nfts/transfers?contract=…&token_id=…&address=…
    • GET /nfts/owners/:contract/:token_id — returns current owner + cached metadata
  • NFT_CONTRACT_IDS env var for explicit watch list (auto-detection works without it)
  • 12 unit tests in src/__tests__/nft.test.ts

Test plan

  • npm test passes — NFT parser unit tests green
  • Set NFT_CONTRACT_IDS=<testnet contract> and confirm NFT transfers populate nft_transfers table
  • GET /nfts/transfers?contract=<id> returns paginated results
  • GET /nfts/transfers?contract=<id>&token_id=<id> filters correctly
  • GET /nfts/owners/<contract>/<token_id> returns owner + metadata
  • GET /nfts/owners/<contract>/<unknown> returns 404
  • Existing /transfers/* endpoints unaffected

Closes #58

- Add src/ingester/nft.ts: isNftTransferEvent (4-topic ABI sniff),
  parseNftTransferEvent, parseNftEvents, fetchNftMetadata (simulated
  contract calls to token_uri + name for lazy metadata loading)
- Add NftTransfer and NftMetadata Prisma models with appropriate indexes
- Add upsertNftTransfers, queryNftTransfers, getNftOwner,
  upsertNftMetadata, getNftMetadata to db.ts
- Update indexer to classify events by topic count (3=fungible, 4=NFT),
  persist both, and lazy-load metadata per unique token
- Add NFT_CONTRACT_IDS env var; auto-detection also active for any
  contract emitting 4-topic transfer events
- Expose GET /nfts/transfers and GET /nfts/owners/:contract/:token_id
- Add comprehensive unit tests for NFT parser

Closes Miracle656#1
@Ipramking Ipramking force-pushed the feat/nft-indexing branch from 392f908 to 4682311 Compare May 30, 2026 07:23
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 30, 2026

@Ipramking Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Copy link
Copy Markdown
Owner

@Miracle656 Miracle656 left a comment

Choose a reason for hiding this comment

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

Clean CAP-46 NFT indexer. Highlights:

  • Detection heuristic (4 topics + Symbol("transfer") + first two scvAddress) is exactly what CAP-46 specifies. Fungible (3-topic + i128 value) keeps flowing through the existing path untouched — no risk of regression.
  • Token ID decoding handles u128 → decimal string, Uint8Array → hex, string passthrough, with an XDR base64 fallback so no event is ever lost.
  • Prisma indexes are right: contractId, tokenId, toAddress, fromAddress, plus the compound (contractId, tokenId) covers the owner-lookup path.
  • NftMetadata cache with @@unique([contractId, tokenId]) + lazy fetch via fetchNftMetadata calling token_uri / name is the standard memoization shape — won't hammer the RPC.
  • 12 unit tests cover happy paths and edge cases: non-transfer symbols, empty topics, wrong ScVal types, large u128 (2n**64n + 99n). Fixtures use nativeToScVal + Address.fromString().toScVal() correctly.
  • NFT_CONTRACT_IDS env as an explicit watch list (auto-detection still works without it) gives operators a per-deployment escape hatch.

Two new endpoints (GET /nfts/transfers and GET /nfts/owners/:contract/:token_id) follow the existing route patterns.

Merging. Closes #58.

@Miracle656 Miracle656 merged commit 61a6c03 into Miracle656:main May 30, 2026
1 check passed
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.

Add CAP-46 NFT (Soroban token) indexing

2 participants