feat(ingester): add CAP-46 NFT indexing with metadata caching#87
Merged
Conversation
- 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
392f908 to
4682311
Compare
|
@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! 🚀 |
Miracle656
approved these changes
May 30, 2026
Owner
Miracle656
left a comment
There was a problem hiding this comment.
Clean CAP-46 NFT indexer. Highlights:
- Detection heuristic (4 topics +
Symbol("transfer")+ first twoscvAddress) 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. NftMetadatacache with@@unique([contractId, tokenId])+ lazy fetch viafetchNftMetadatacallingtoken_uri/nameis 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 usenativeToScVal+Address.fromString().toScVal()correctly. NFT_CONTRACT_IDSenv 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
[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 unchangedsrc/ingester/nft.ts:isNftTransferEvent,parseNftTransferEvent,parseNftEvents,fetchNftMetadata(lazy simulation viatoken_uri/namecontract calls)NftTransfer+NftMetadatawith appropriate indexesupsertNftTransfers,queryNftTransfers,getNftOwner,upsertNftMetadata,getNftMetadata(contractId, tokenId)on first sightGET /nfts/transfers?contract=…&token_id=…&address=…GET /nfts/owners/:contract/:token_id— returns current owner + cached metadataNFT_CONTRACT_IDSenv var for explicit watch list (auto-detection works without it)src/__tests__/nft.test.tsTest plan
npm testpasses — NFT parser unit tests greenNFT_CONTRACT_IDS=<testnet contract>and confirm NFT transfers populatenft_transferstableGET /nfts/transfers?contract=<id>returns paginated resultsGET /nfts/transfers?contract=<id>&token_id=<id>filters correctlyGET /nfts/owners/<contract>/<token_id>returns owner + metadataGET /nfts/owners/<contract>/<unknown>returns 404/transfers/*endpoints unaffectedCloses #58