feat(nfts): collections - #329
Open
piggydoughnut wants to merge 10 commits into
Open
Conversation
…ollections only returns claimable
piggydoughnut
marked this pull request as draft
August 26, 2026 12:15
piggydoughnut
marked this pull request as ready for review
August 28, 2026 08:47
piggydoughnut
marked this pull request as draft
August 28, 2026 10:31
📦 Bundle size impactComparing
Thresholds — 🟡 ≥10% or ≥5.0 KB · 🟠 ≥20% or ≥15.0 KB (bundled). Percentage only applies once the baseline is ≥ 10 KB. Informational — this check never blocks merge. |
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.
Part of #318.
Adds
@parity/product-sdk-nfts: catalogue reads of theScarcitypallet on Asset Hub. Every value comes from storage, pinned to one finalized block per call.Pagination
DEFAULT_PAGE_LIMIT(100) and caps atMAX_PAGE_LIMIT(1000) - both exported.limit,fromIdidCeiling,nextIdPagination walks the sequential
u32index space each read enumerates, which is sound because the runtime guarantees the shape: ids and indices come from counters that only move forward, and bothdelete_collectionanddelete_itemdocument that identifiers are never reused.Shared options
Every read takes these.
getCollectionItemsaddsattributes.limitnumber1001000; a larger request is capped by max limit rather than fail, andnextIdstill reports where the page stopped.fromIdnumber0nextId.atFinalizedSnapshotsignalAbortSignalatis what makes a walk coherent. Without it every call pins its own finalized block, right for unrelated questions, wrong for one question asked in pages, since a walk over its own snapshots is not a walk of any single chain state. Pass another result'satstraight back in. It also makes two reads agree: the registry and the full listing, or a listing and a catalogue, at one block.API functions
Each pins its block first through
raw.assetHub.getFinalizedBlock()(unless givenat), then addresses storage at that hash, so all values in one result come from one block. Each returns aResult, per the SDK-wide error model.getCollections(chain, options?)→
Result<CollectionsResult, ProductNftsError>Every collection on chain, ascending by id, a page at a time.
selection: nullmeans the collection exists but accepts no claims.raw.assetHub.getFinalizedBlock()atwas givenquery.Scarcity.NextCollectionId.getValue(at)query.Scarcity.Collections.getValues(window)query.NftClaims.CollectionMinters.getValues(page ids)selectionfor the pagequery.Scarcity.CollectionMetadata.getValues(page ids × "name")getClaimableCollections(chain, options?)→
Result<ClaimableCollectionsResult, ProductNftsError>The subset
getCollectionsfilters: collections registered to accept claims, ascending by id.raw.assetHub.getFinalizedBlock()atwas givenquery.Scarcity.NextCollectionId.getValue(at)query.NftClaims.CollectionMinters.getValues(window)query.Scarcity.Collections.getValues(page ids)query.Scarcity.CollectionMetadata.getValues(page ids × "name")getCollectionItems(chain, id, options?)→
Result<CollectionItemsResult, ProductNftsError>One page of a collection's item catalogue, items ascending by index. Applies no registry filter, so it reads a collection that accepts no claims just as well. A collection nobody created is not an error; it rides the
okchannel asNotFound.idCeilingcounts every item ever defined here (indices are never reused);collection.itemCountcounts the ones still alive. They diverge permanently once anything is deleted.Extra option:
attributesbooleanfalseraw.assetHub.getFinalizedBlock()atwas givenquery.Scarcity.Collections.getValue(id, at)NotFoundtestquery.Scarcity.CollectionMetadata.getEntries(id, at)query.Scarcity.ItemDefs.getValues(window)query.Scarcity.ItemMetadata.getValues(page × 3 named keys)name,image,rarity— whenattributesis offquery.Scarcity.ItemMetadata.getEntries(id, at)attributesis onattributesisRecord<string, string> | null, andnullmeans "not fetched". An empty object would claim the item carries no metadata, which is a different statement. The typed fields (name,image,rarity) are keys this package can name, so a page fetches them for its whole window with one exact-key read; the bag's keys are open by definition, so there is nothing to ask for by name and filling it means scanning the collection. Collection-level defaults are inherited either way.Other changes
packages/nfts/src/chain.ts— the client the reads take, typed structurally by the six storage entries they touch rather than by naming a descriptor, so no genesis hash is pinned to read a catalogue. It asks for exactly the accessors the reads use: making every read paged removed four of them. An app pruning its own descriptors must whitelist all six,Scarcity.NextCollectionIdincluded.packages/nfts/src/paging.ts—DEFAULT_PAGE_LIMIT,MAX_PAGE_LIMIT,pageBounds, andfillByIdWindow, shared by all three reads. They differ only in what makes an index interesting (a collection record, a minter entry, an item definition), so the density widening, scan budget and cursor semantics live in one place.packages/nfts/src/metadata.ts— the decode convention. Metadata is untypedVec<u8>→Vec<u8>in three layers, each overriding the last; a catalogue read merges the first two. Values decode as UTF-8 when the bytes are readable text and0x-hex otherwise. Exact-key reads take a plainUint8Array— PAPI 2.x generates[number, Uint8Array]for thisVec<u8>key.packages/nfts/src/errors.ts—NftsChainEntryErrornames the storage entry a read could not reach, carries it onentry, and keeps PAPI's error as thecause.packages/sdk/*— the@parity/product-sdk/nftssubpath, plussrc/nfts/contract.test.ts: compile-time assertions that a realgetChainAPIclient satisfiesNftsChain, with devnet Asset Hub as a negative control. These run underpnpm typecheck, not vitest.examples/nfts-demo/*— demo app plus Playwright specs. The panel forgetCollectionsrenders the ids withselection: nullnext to the registry's, because the gap between the two lists is the thing worth seeing on live data. It also walks the id space in pages of 2 withatpinned, and the spec asserts two things a type cannot: that names from a small-page walk match a single larger page (parameter bivariance means the contract check would accept a wrong exact-key type, and a wrong key silently returnsnullfor every name), and that the whole walk touched exactly one block.skills/product-sdk-nfts/SKILL.md,CLAUDE.md,.claude-plugin/marketplace.json,README.md,.changeset/config.json— the skill and its registration, the new package's row in the package table, and@parity/product-sdk-nfts-demoadded to the changeset ignore list alongside the other demos. Plus one changeset.Notes
Nothing on chain declares the metadata keys or the value types, so
name,imageandrarityare a convention this package applies, not a schema it enforces; every other key is reachable throughattributes. Unconfirmed with the pallet team.imageRefreports the same bytes ashexand astext(nullwhen unreadable): one deployment stores a 32-byte content digest there, another an ASCII CID, and nothing on chain says which, so the caller picks.transferabilityis not returned. It traces topallet_nfts'CollectionSetting::TransferableItemsand has no source inScarcity, not inItemDefs, not in any metadata key the live chain carries.