Skip to Content
API ReferencenftsOverview

@parity/product-sdk-nfts

@parity/product-sdk-nfts reads Scarcity NFT collections and item catalogues on Asset Hub.

Three reads today, all of them pure catalogue and all of them paged: which collections a claim can mint into, every collection whether it accepts claims or not, and what is in one of them. None needs an identity, a purse, or a second chain, which is why they came first.

getClaimableCollections and getCollections are the subset and the superset of the same thing. There is one kind of collection, and a NftClaims.CollectionMinters entry is what makes one claimable. Both are four reads a page, so pick by which set you want. Prefer the registry read when only claimable collections belong in the answer.

Every read is paged, and none of them is unbounded. limit defaults to DEFAULT_PAGE_LIMIT and caps at MAX_PAGE_LIMIT; there is no “give me everything”, because nothing bounds how many collections exist or how many items a collection holds. The only ceilings the pallet has are index-space exhaustion, and the indices are u32. Follow nextId to walk the whole of anything, in bounded pieces.

One vocabulary across all three reads: limit and fromId in, idCeiling and nextId out, so a single pager works against any of them.

A page is four storage reads whatever the counts, but four reads is not four round trips. The PAPI getValues opens one storage operation per key, so the operations of a page scale with limit while its bytes stay flat. That is the other half of why MAX_PAGE_LIMIT exists.

import { getChainAPI } from "@parity/product-sdk-chain-client"; import { getClaimableCollections, getCollectionItems } from "@parity/product-sdk-nfts"; const chain = await getChainAPI("paseo"); // A page of the claim registry, and where the next one starts. const registry = await getClaimableCollections(chain, { limit: 20 }); if (registry.ok) { for (const collection of registry.value.collections) { console.log(collection.id, collection.name ?? "(unnamed)"); } console.log(registry.value.nextId); // null when the id space is exhausted } // A page of the catalogue of one collection. `attributes` is `null` unless asked for. const catalogue = await getCollectionItems(chain, 0, { limit: 20 }); if (catalogue.ok && catalogue.value.tag === "Found") { console.log(catalogue.value.collection.items, catalogue.value.nextId); }

Failures arrive on the err channel as a ProductNftsError, per the SDK-wide error model. A collection that does not exist is not a failure: it is ok({ tag: "NotFound", … }).

Every value in one result is read at a single pinned finalized block, reported as at. A catalogue pulls item definitions and two metadata layers separately, and reading them a block apart could return a catalogue the chain was never in.

Separate calls pin separate blocks, which is right for unrelated questions and wrong for one question asked in pieces. A paged walk over its own snapshots is not a walk of any single chain state. Pass the at of another result back in as the at option to join its block instead: every read here accepts it, so a whole walk, or a registry read and a catalogue read, can address one block.

Descriptor whitelists

The catalogue reads touch six entries, all Asset Hub:

query.Scarcity.NextCollectionId query.Scarcity.Collections query.Scarcity.ItemDefs query.Scarcity.CollectionMetadata query.Scarcity.ItemMetadata query.NftClaims.CollectionMinters

previewClaim adds one runtime API, api.NftClaimsApi.preview_mints, and getClaims adds two Asset Hub entries and four on the People chain:

query.NftClaims.CreditTrees query.NftClaims.ClaimedLeaves query.NftCredits.NftClaimCreditBlocks query.NftCredits.NftClaimCreditAwards api.NftCreditsApi.nft_claim_credit_roots api.NftCreditsApi.nft_claim_credit_proofs

An app that prunes its own descriptors with a PAPI whitelist has to list every entry a read it calls touches, including the ones its own code never reads. A missing entry surfaces as the PAPI Incompatible runtime entry Storage(...), which reads like descriptor drift; these reads report it as NftsChainEntryError instead, which names the entry in its message and carries it on entry. Regenerating the descriptors is not the whole fix when they are installed as a file: dependency, because the package manager keeps serving the previous copy until a forced reinstall.

What this package deliberately does not do yet

  • One runtime API, preview_mints, and no others. Display metadata is read from the CollectionMetadata / ItemMetadata storage layers, which answer the same question and are carried by the pinned descriptor. previewClaim has no storage equivalent, so it is the exception, and the fidelity guard in @parity/product-sdk checks its signature against the descriptor the same way it checks the storage entries.
  • attributes costs a prefix scan of the whole collection. The typed fields are keys this package can name, so a page fetches them for its window in one exact-key read. The keys of the open bag are not knowable in advance, so filling it means scanning the item metadata of one collection whole. That is one read, but bytes proportional to the catalogue rather than the page. Left off, the field is null, which says “not fetched” rather than “no metadata”.
  • Nothing bounds the size of a collection. The only item ceiling the pallet has is index-space exhaustion. TooManyItems reads “the per-collection item index space is exhausted”, and the index is a u32, so there is no configured limit to lean on and a ten-thousand-item collection is an afternoon of work. That is why getCollectionItems pages like the listing reads do, rather than assuming a small catalogue. itemCount from either listing read gives the size before you commit to walking a collection whole.
  • Nothing purse-scoped. getOwnedNfts, getNextEmptyPurse and findPurseHolding all need a purse primitive shared across apps, which the wallet does not expose yet. App-scoped product-account derivation is not a substitute: it is keyed by productId, so nothing derived under it can be shared between two SPAs.
  • Metadata keys are a convention, not a contract. Nothing in the runtime declares them. name, image and rarity are lifted into typed fields because every deployment read so far carries them; the rest of the bag is passed through untouched. image is reported as hex and as text both, since deployments disagree about which of the two they store. Unconfirmed with the pallet team.
npm install @parity/product-sdk-nfts

Exports

Classes

NameSummary
NftsChainEntryErrorA storage entry a read needs cannot be read on the client it was given.
NftsDecodeErrorA raw storage value did not match the shape the descriptor promised: an
NftsIdErrorAn id or index a read was given cannot address chain state.
ProductNftsErrorBase class for errors raised by @parity/product-sdk-nfts.

Functions

NameSummary
artworkAddress()Decode an image value into an address, whichever form the chain stored.
gatewaySource()A source over an IPFS gateway by CID. A non-2xx answer is null, since a
getClaimableCollections()Read every claimable collection, from one pinned finalized block.
getClaims()Read every credit a claimant holds, newest award block first.
getCollectionItems()Read one page of the item catalogue of a collection.
getCollections()Read every collection, claimable or not, from one pinned finalized
getVerifiedArtwork()Fetch the bytes an ImageRef names and check them against its digest.
preimageSource()A source over the host preimage manager, keyed by the digest.
previewClaim()Preview one credit against each of collections, in one runtime call.
toClaimantKey()The enum PAPI expects for AccountOrPerson.

Interfaces

NameSummary
ArtworkAddressWhat an image metadata value names, once decoded.
ClaimOne NFT claim credit, as the People chain awarded it and Asset Hub sees it.
ClaimableCollectionA collection registered to accept claims.
ClaimableCollectionsResultWhat one getClaimableCollections call returns.
ClaimsResultWhat one getClaims call returns.
CollectionA collection, claimable or not.
CollectionDetailOne page of the item catalogue of a collection.
CollectionItemOne item definition in a collection, with its display metadata merged in.
CollectionsResultWhat one getCollections call returns.
EntryOne getEntries row: the keys that were not fixed by the query, and the value.
FinalizedSnapshotThe finalized block every value in one result was read at.
GetClaimableCollectionsOptionsWhat every paged read here accepts besides its own window: the block to read
GetClaimsOptions—
GetCollectionItemsOptionsWhat every paged read here accepts besides its own window: the block to read
GetCollectionsOptionsWhat every paged read here accepts besides its own window: the block to read
GetVerifiedArtworkOptions—
ImageRefThe image metadata of an item, read both ways.
MintPreviewWhat claiming one credit into one collection would mint.
MintPreviewResultWhat one previewClaim call returns.
NftsChainThe client these reads take: six storage entries, and the raw client they pin
NftsCreditsChainThe second chain a credits read needs, beside NftsChain.
PinnedReadOptionsWhat every paged read here accepts besides its own window: the block to read
PreviewClaimOptions—
RawCollectionScarcity.Collections, narrowed to the fields these reads use.
RawCreditAwardOne row of NftCredits.NftClaimCreditAwards.
RawCreditProofOne entry of a successful NftCreditsApi.nft_claim_credit_proofs.
RawCreditRootNftCredits.NftClaimCreditRoots, and the same shape NftClaims.CreditTrees stores.
RawItemDefScarcity.ItemDefs.
RawMetadataEntryScarcity.CollectionMetadata / ItemMetadata / InstanceMetadata.
RawMinterNftClaims.CollectionMinters.
ReadAtOptions every pinned storage read is given, so all of them agree on a block.

Type Aliases

NameSummary
ArtworkSourceWhere the bytes for a digest come from. null when the source has nothing.
ClaimantWhose credits to read: the People chain keys them by account or by person alias.
ClaimStateWhere one credit stands between being awarded and being spent.
CollectionItemsResultThe answer to “what is in collection N”: the catalogue, or a clean miss.
ItemSelectionHow a claim picks the item it mints, from NftClaims.CollectionMinters.
RawBytesThe value of a metadata entry: the raw bytes, or the PAPI Binary wrapper
RawMintOutcomeOne NftClaimsApi.preview_mints outcome, positionally matched to its query.
RuntimeResultThe runtime Result a PAPI runtime API call resolves to.
TransferabilityWhether a minted instance of an item can be sent on, from ItemDefs.transferability.
VerifiedArtworkThe answer, on the ok channel whatever the bytes did.

Variables

NameSummary
DEFAULT_PAGE_LIMITHow many entries a read returns when the caller does not say.
MAX_PAGE_LIMITThe largest page a read will return, and the widest window one probe may ask
SCAN_BUDGET_FACTORHow many ids a page may scan per collection it was asked for.

Classes

class NftsChainEntryError

A storage entry a read needs cannot be read on the client it was given.

Either the descriptors do not carry the entry or the runtime does not, and the message says which. Carries the entry as a field for programmatic handling, the same shape as GenesisMismatchError in @parity/product-sdk-chain-client.

Naming the entry in the message is deliberate and does not contradict the rule on NftsDecodeError: the entry is a pallet and storage name PAPI reported, not chain content an author supplied.

Extends: ProductNftsError

Constructors

constructor
new NftsChainEntryError(message: string, entry: string | null, options?: ErrorOptions): NftsChainEntryError

Properties

entry
propertyreadonlystring | null

The Pallet.Entry PAPI named, or null when its message did not.

class NftsDecodeError

A raw storage value did not match the shape the descriptor promised: an ItemSelection variant this package does not know, or a metadata entry whose value is neither raw bytes nor a Binary wrapper.

Messages must be fixed strings. Never interpolate a decoded value into one: item metadata is author-supplied content, and an error message is the least controlled place it can end up. The entry that failed is identifiable from the message text alone.

Extends: ProductNftsError

Constructors

constructor
new NftsDecodeError(message: string, options?: ErrorOptions): NftsDecodeError

class NftsIdError

An id or index a read was given cannot address chain state.

Collection ids and item indices are u32, and the PAPI codec truncates rather than rejecting, so an unchecked NaN or 1.5 would read a real collection and report it under the id the caller asked for. Refusing is the only answer that cannot be mistaken for a catalogue.

The message carries the value because it is caller input, not chain content. The rule on NftsDecodeError is about author-supplied metadata.

Extends: ProductNftsError

Constructors

constructor
new NftsIdError(id: number, options?: ErrorOptions): NftsIdError

Properties

id
propertyreadonlynumber

The value that could not address anything.

class ProductNftsError

Base class for errors raised by @parity/product-sdk-nfts.

Implements the cross-package SdkError marker so isSdkError(e) also recognizes it.

Extends: Error

Implements: SdkError

Constructors

constructor
new ProductNftsError(message: string, options?: ErrorOptions): ProductNftsError

Properties

isSdkError
propertyreadonlytrue

Discriminant present on all SDK errors.

source
propertyreadonly"nfts"

The package that raised the error, e.g. "host", "signer", "contracts".

Functions

artworkAddress()

Decode an image value into an address, whichever form the chain stored.

text wins when it parses as a CIDv1 with a 32-byte digest, because it carries the multihash code. Otherwise hex is taken as a blake2b-256 digest, the only convention seen on a live deployment. null when neither reads.

artworkAddress(image: ImageRef): ArtworkAddress | null

gatewaySource()

A source over an IPFS gateway by CID. A non-2xx answer is null, since a gateway 404 is the ordinary way of saying it does not have the bytes.

gatewaySource(baseUrl: string, fetchImpl: (input: URL | RequestInfo, init?: RequestInit) => Promise<Response> = fetch): ArtworkSource

getClaimableCollections()

Read every claimable collection, from one pinned finalized block.

Four storage reads a page, over two hops: the id ceiling and the registry entries for the window together, then the records and names of the collections that window found, each in one keyed read. Nothing is read one collection at a time, and nothing pulls a byte for a collection the registry does not name, so the cost is proportional to the page rather than to the chain.

The walk is over the collection id space, not the registry. See GetClaimableCollectionsOptions.limit, including why a sparse registry gives short pages and why a short page is not the end.

The registry is not small by construction, whatever its size on any deployment read so far: nothing stops most of the collections on a chain from registering. So the cost here is stated in terms of how many do, not in terms of a number that happens to hold today.

Returns a Result, per the SDK-wide error model. A collection registered for claims whose Scarcity.Collections record is missing is not an error: it comes back with itemCount and owner null, because the caller can still render it and the inconsistency is the chain’s, not the read’s.

Names come from the Scarcity.CollectionMetadata storage layer.

getClaimableCollections(chain: NftsChain, options: GetClaimableCollectionsOptions = {}): Promise<Result<ClaimableCollectionsResult, ProductNftsError>>

Examples

const chain = await getChainAPI("paseo"); // One page. Follow `nextId` for the rest, passing `at` back in to pin the walk. const result = await getClaimableCollections(chain, { limit: 100 }); if (result.ok) { for (const collection of result.value.collections) { console.log(collection.id, collection.name ?? "(unnamed)", collection.itemCount); } console.log(result.value.nextId); // null when the id space is exhausted }

getClaims()

Read every credit a claimant holds, newest award block first.

Two pinned blocks, one per chain, both reported in at. Nothing here is proportional to the chain: the award blocks and roots come back in two keyed reads, the proofs in one runtime call per rooted block, the award chunks in one keyed read per rootless block window, and the claim state in two keyed reads over the rooted blocks, CreditTrees and the ClaimedLeaves bitmaps. A claimant with nothing awarded is an empty list, not an error.

getClaims(chain: NftsChain & NftsCreditsChain, options: GetClaimsOptions): Promise<Result<ClaimsResult, ProductNftsError>>

Examples

const chain = await getChainAPI("paseo"); const result = await getClaims(chain, { claimant: { tag: "Account", address } }); if (result.ok) { for (const credit of result.value.claims) { console.log(credit.hash, credit.state, credit.awardedAt); } }

getCollectionItems()

Read one page of the item catalogue of a collection.

Four reads per page whatever the collection holds: the collection record, its metadata defaults, the item definitions in the window, and the metadata for those items. Nothing here is proportional to the catalogue unless attributes: true asks for the open bag, which no exact-key read can supply.

Metadata resolves in two layers, the collection defaults underneath and the item overrides on top, which is what “override collection defaults for the same key” means for ItemMetadata. InstanceMetadata is the third layer and is deliberately not consulted: it keys on an instance id, so it describes a minted NFT rather than a catalogue entry.

Returns a Result. A collection nobody created is not an error. It resolves to ok({ tag: "NotFound", … }), because the chain was asked and answered. An existing collection with no items resolves to Found with an empty items.

An id that is not a u32 is an error, NftsIdError, raised before anything is read. The PAPI codec truncates rather than rejecting, so NaN would otherwise return the catalogue of collection 0 labelled id: NaN, on the ok channel, which no caller could tell from a real answer.

transferability comes from the item definition, the same read that carries supply, so it costs nothing extra.

getCollectionItems(chain: NftsChain, id: number, options: GetCollectionItemsOptions = {}): Promise<Result<CollectionItemsResult, ProductNftsError>>

Examples

const chain = await getChainAPI("paseo"); // One page, then the rest. `at` pins the whole walk to one block. const first = await getCollectionItems(chain, 0, { limit: 100 }); if (!first.ok || first.value.tag !== "Found") return; let page = first.value; const at = page.at; for (;;) { for (const item of page.collection.items) { console.log(item.index, item.name, item.rarity, `${item.liveSupply}/${item.supply}`); } if (page.nextId === null) break; const next = await getCollectionItems(chain, 0, { limit: 100, fromId: page.nextId, at }); // A failed page is not the end of the walk. Report it, or re-pin by // dropping `at` and reading again from `page.nextId`. if (!next.ok) throw next.error; // Deleted mid-walk: a real answer, and the walk is over. if (next.value.tag !== "Found") break; page = next.value; }

getCollections()

Read every collection, claimable or not, from one pinned finalized block.

The superset getClaimableCollections filters. One page is the id ceiling plus three keyed reads for the ids of the page: Scarcity.Collections for the records, NftClaims.CollectionMinters to fill in selection, and Scarcity.CollectionMetadata for the names. Add one more record read for each stretch of deleted ids it steps over. selection is null for a collection that accepts no claims.

A minter entry whose Scarcity.Collections record is missing cannot appear here, because this enumerates the records. getClaimableCollections reports that case with null fields.

This read is always paged, at limit collections a page, defaulting to DEFAULT_PAGE_LIMIT and capped at MAX_PAGE_LIMIT. Dumping the three maps whole would keep the operation count constant but not the payload: a CollectionMetadata dump carries every metadata key of every collection when only name is wanted, so most of what arrived would be discarded. At ten thousand collections that is on the order of fifteen megabytes to produce ten thousand summaries. That is too much for a browser tab, and enough that a public endpoint may refuse the operation.

So the read walks the id space in windows instead, at a flat four storage reads per page, the id ceiling plus three keyed reads over the window, whatever the chain holds. Four reads, which is not four round trips: PAPI opens one operation per key, so the operations of a page scale with limit and it is the bytes that stay flat, see chain.ts. That works because the id space is knowable and dense: create_collection takes no id, so the runtime allocates sequentially from Scarcity.NextCollectionId, and delete_collection documents that identifiers are never reused. So every collection in [fromId, fromId + limit) can be fetched by exact key, the records, registry entries and name rows alike, with nothing proportional to the chain anywhere in the read.

A page comes back full. Ids of deleted collections are holes, with no record and no metadata since the runtime requires it gone before deletion, so the read walks past them until it has limit collections rather than handing back a short page. That costs an extra record read where holes appear and nothing else: a hole never gets a name or registry lookup. A page is short only at the end of the id space, or when a mostly-deleted range hits SCAN_BUDGET_FACTOR; nextId === null is the only end signal.

PAPI has no storage cursor of its own (its options are at and signal, and a prefix scan is all-or-nothing), but resuming by id is the better primitive here regardless: because ids are only appended and never reused, it is stable. Paging forward cannot skip or repeat a collection while the chain is being written to, which offset-based paging over a mutable set cannot promise.

getCollections(chain: NftsChain, options: GetCollectionsOptions = {}): Promise<Result<CollectionsResult, ProductNftsError>>

Examples

// One page, then the rest. `at` pins the whole walk to one block. const first = await getCollections(chain, { limit: 100 }); if (!first.ok) return; let page = first.value; const at = page.at; for (;;) { for (const c of page.collections) { console.log(c.id, c.name ?? "(unnamed)", c.selection ? "claimable" : "not claimable"); } if (page.nextId === null) break; const next = await getCollections(chain, { limit: 100, fromId: page.nextId, at }); // A failed page is not the end of the walk. Report it, or re-pin by // dropping `at` and reading again from `page.nextId`. if (!next.ok) throw next.error; page = next.value; }

getVerifiedArtwork()

Fetch the bytes an ImageRef names and check them against its digest.

Returns a Result. The four outcomes an address can have are all on the ok channel, see VerifiedArtwork. The err channel is for a source that threw or a signal that aborted.

getVerifiedArtwork(image: ImageRef | null, options: GetVerifiedArtworkOptions): Promise<Result<VerifiedArtwork, ProductNftsError>>

Examples

const manager = await getPreimageManager(); const art = await getVerifiedArtwork(item.imageRef, { source: preimageSource(manager) }); if (art.ok && art.value.tag === "Verified") { img.src = URL.createObjectURL(new Blob([art.value.bytes], { type: "image/webp" })); }

preimageSource()

A source over the host preimage manager, keyed by the digest.

On Bulletin the digest is the preimage key, so no CID round trip is needed. Typed structurally to the shape getPreimageManager() returns in @parity/product-sdk-host, so this package does not depend on it. null after timeoutMs without an answer.

preimageSource(manager: { lookup: unknown }, timeoutMs: number = 20_000): ArtworkSource

previewClaim()

Preview one credit against each of collections, in one runtime call.

Returns a Result. A collection the credit cannot mint into is a Fails outcome with the reason the runtime gave, not an error, because the chain was asked and answered. An empty collections is an empty list and no round trip.

previewClaim(chain: NftsChain, options: PreviewClaimOptions): Promise<Result<MintPreviewResult, ProductNftsError>>

Examples

const registry = await getClaimableCollections(chain); if (!registry.ok) return; const preview = await previewClaim(chain, { credit, collections: registry.value.collections.map((c) => c.id), at: registry.value.at, }); if (preview.ok) { for (const p of preview.value.previews) { if (p.outcome.tag === "Mints") console.log(p.collection, p.outcome.name); } }

toClaimantKey()

The enum PAPI expects for AccountOrPerson.

toClaimantKey(claimant: Claimant): { type: "Account" | "Person"; value: string }

Interfaces

interface ArtworkAddress

What an image metadata value names, once decoded.

Properties

cid
propertystring

The CID, base32 lower case, built from the digest when the chain stored only that.

digest
property`0x${string}`

The 32-byte content digest, 0x prefixed. The Bulletin preimage key.

multihash
propertynumber

The multihash code, which says how to hash the bytes back to the digest.

interface Claim

One NFT claim credit, as the People chain awarded it and Asset Hub sees it.

Properties

awardBlock
propertynumber

The People chain block the credit was awarded in.

awardedAt
propertynumber | null

When the award block was rooted, in Unix seconds, or null for a block whose root has not been recorded yet.

gameIndex
propertynumber | null

The game the award block belongs to, or null before its root exists.

hash
propertystring | null

The credit hash, 0x prefixed, as NftClaimCreditAwards stores it, or null when the awards of its block are gone and the hash with them. A null entry stands for a whole block, whose credit count is unknown.

leafIndex
propertynumber | null

Where the credit sits in the tree of its award block, which a proof binds to it and a claim spends, or null while the block is rootless or once its awards were pruned.

proof
propertystring[] | null

The Merkle proof a mint spends, sibling hashes from the leaf up to the root, lowercase and 0x prefixed. null for a claim whose block has no root yet and for an unprovable one, since neither has a proof to spend.

state
propertyClaimState

interface ClaimableCollection

A collection registered to accept claims.

Driven by NftClaims.CollectionMinters, not by Scarcity.Collections: a collection with no minter entry cannot be claimed into, so it does not belong in a collection picker even though its catalogue exists.

Properties

id
propertynumber

The Scarcity collection id.

itemCount
propertynumber | null

Live item definitions in the collection, from Collections.item_count.

null when the collection has a minter entry but no Scarcity.Collections record. The runtime clears registrations through pallet_scarcity::OnCollectionDeleted, so this should not happen. It is reported rather than papered over so a caller can tell “empty” from “inconsistent”.

name
propertystring | null

The name metadata of the collection, or null when it sets none.

Read from Scarcity.CollectionMetadata, not from a runtime API. See the note on CollectionItem.attributes.

owner
propertystring | null

The Scarcity owner of the collection, from Scarcity.Collections, or null when the record is missing.

CollectionMinters carries a registering owner of its own; they are the same account today, and this is the authoritative one.

selection
propertyItemSelection

How a claim into this collection picks its item.

interface ClaimableCollectionsResult

What one getClaimableCollections call returns.

Properties

at
propertyFinalizedSnapshot
collections
propertyClaimableCollection[]
idCeiling
propertynumber

The exclusive upper bound of the collection id space at at, which is what fromId and nextId are relative to.

Not the size of the registry: it counts every collection ever created, registered or not.

nextId
propertynumber | null

The fromId a next page should use, or null when the walk reached the end of the id space.

Every read here is paged, so null means the end of the space and nothing else. A non-null cursor is never “there happened to be more”.

interface ClaimsResult

What one getClaims call returns.

Properties

at
property{ assetHub: FinalizedSnapshot; individuality: FinalizedSnapshot }

Two chains, so two pinned blocks. Every value came from one or the other.

claims
propertyClaim[]

Newest award block first.

interface Collection

A collection, claimable or not.

The superset ClaimableCollection is drawn from: every Scarcity.Collections record, with selection filled in for the ones NftClaims.CollectionMinters also names. One deployment carries six collections and registers one, so the difference is not marginal.

itemCount and owner are non-null here, unlike on ClaimableCollection: this read enumerates the records themselves, so every entry it returns has one. The trade is the mirror image: a minter entry whose collection record is missing appears in ClaimableCollection-shaped reads and cannot appear here.

Properties

id
propertynumber

The Scarcity collection id.

itemCount
propertynumber

Live item definitions, from Collections.item_count.

name
propertystring | null

The name metadata of the collection, or null when it sets none.

owner
propertystring

The Scarcity owner of the collection.

selection
propertyItemSelection | null

How a claim into this collection picks its item, or null when the collection accepts no claims.

null is the “not claimable” signal. There is no separate boolean to drift out of sync with it. A collection with no CollectionMinters entry cannot be claimed into no matter how many items it holds.

interface CollectionDetail

One page of the item catalogue of a collection.

Properties

id
propertynumber
itemCount
propertynumber

Live item definitions in the whole collection, from Collections.item_count.

The size of the catalogue, not of this page. Compare items.length. It can also disagree with the stored definitions while one is being removed, since the count and the entries are separate writes; reported as the chain has it rather than recomputed.

items
propertyCollectionItem[]

The definitions in this page, ascending by index.

name
propertystring | null

The name metadata of the collection, or null when it sets none.

interface CollectionItem

One item definition in a collection, with its display metadata merged in.

Properties

attributes
propertyRecord<string, string> | null

Every metadata key on the item, collection defaults merged underneath, or null when the read was not asked for them.

null is “not fetched”, not “no metadata”. An empty object would claim the item carries no metadata, which is a different statement. Pass attributes: true to getCollectionItems to fill this in; it costs a prefix scan of the item metadata of the whole collection, because these keys are open and cannot be asked for by name the way name, image and rarity can.

The schema is open. Scarcity stores metadata as untyped Vec<u8> keys to Vec<u8> values in three layers (CollectionMetadata, ItemMetadata, InstanceMetadata), each overriding the last for the same key. Nothing declares which keys exist or how their values are typed. name, image and rarity are lifted into typed fields because every deployment read so far carries them; the keys around them do not agree. One item carries palette, energy and style, another description, which is why the whole bag is exposed rather than a closed shape.

Values are decoded as UTF-8 when the bytes are valid printable UTF-8, and as 0x-hex otherwise. Numbers are not parsed, and nothing is lost by that: on the live chain energy holds the two ASCII characters 2 and 1, so the chain stored the text “21” there rather than a binary number. A caller wanting a number parses the string and decides what a malformed one means.

imageRef
propertyImageRef | null

The image metadata of the item, or null when neither it nor its collection sets one.

Read as hex and as text both, since deployments disagree about which one they store. Which field to display follows the deployment convention, which is not something this package can read off the chain.

index
propertynumber

The item index within its collection.

liveSupply
propertynumber

Instances currently alive, which is supply less those burned.

name
propertystring | null

The name metadata of the item, or null when neither it nor its collection sets one.

rarity
propertystring | null

The rarity metadata of the item, or null when unset.

supply
propertynumber

Instances the definition may ever mint.

transferability
propertyTransferability

Whether a minted instance can be sent on. A Soulbound one stays with its first owner.

interface CollectionsResult

What one getCollections call returns.

Properties

at
propertyFinalizedSnapshot
collections
propertyCollection[]
idCeiling
propertynumber

The exclusive upper bound of the collection id space at at, from Scarcity.NextCollectionId.

Ids are allocated sequentially from 0 and never reused, so this is both the count of collections ever created and the end of the id space a page walks. It is not the number of live collections: deleting one leaves a permanent hole below the ceiling.

nextId
propertynumber | null

The fromId a next page should use, or null when this read reached the end of the id space.

Every read here is paged, so null means the end of the space and nothing else. A non-null cursor is never “there happened to be more”.

interface Entry

One getEntries row: the keys that were not fixed by the query, and the value.

Properties

keyArgs
propertyKeys
value
propertyValue

interface FinalizedSnapshot

The finalized block every value in one result was read at.

Properties

blockHash
propertystring
blockNumber
propertynumber

interface GetClaimableCollectionsOptions

What every paged read here accepts besides its own window: the block to read at, and the signal that stops it.

Extends: PinnedReadOptions

Properties

fromId
propertyoptionalnumber

Where the walk starts, defaulting to 0.

Take it from the nextId of the previous page. Ids are only ever appended, so resuming there cannot skip or repeat a collection.

limit
propertyoptionalnumber

How many claimable collections this page returns, defaulting to DEFAULT_PAGE_LIMIT and capped at MAX_PAGE_LIMIT.

Pages the same way GetCollectionsOptions.limit does, by walking the collection id space, since NftClaims.CollectionMinters is keyed by collection id too. One difference is worth knowing. The gaps a page steps over here are unregistered collections, not deleted ones, and how many there are is a property of the deployment: one carries six collections and registers one, another registers most of what it carries. So a page fills while roughly one collection in sixteen is registered, and comes back short below that.

A short page is not the end of the registry. nextId === null is the only end signal, so follow it rather than counting what came back. On a sparse registry that is how the rest of it arrives.

interface GetClaimsOptions

Properties

at
propertyoptionalFinalizedSnapshot

Join a block another Asset Hub read already pinned.

claimant
propertyClaimant

Whose credits. Accounts and person aliases are keyed apart on chain.

signal
propertyoptionalAbortSignal

Forwarded into every underlying pull, so an aborted caller stops the batch.

interface GetCollectionItemsOptions

What every paged read here accepts besides its own window: the block to read at, and the signal that stops it.

Extends: PinnedReadOptions

Properties

attributes
propertyoptionalboolean

Fill in CollectionItem.attributes, the open metadata bag, defaulting to false.

The typed fields (name, image, rarity) are keys this package can name, so a page fetches them for its whole window in one exact-key read. The keys of the bag are open by definition, so there is nothing to ask for by name: filling it means a prefix scan of the item metadata of the whole collection. That is still one read, but its bytes scale with the catalogue rather than the page, which is exactly what paging is for.

Nothing on the client caps that read. Each key is at most MaxKeyLen and each value at most MaxValueLen, 32 and 256 bytes on Paseo today, but no runtime constant bounds how many keys an item definition carries, so the response is items times keys times up to 288 bytes.

So: pass it for a collection you know is small, or when a caller genuinely needs app-specific keys. Leave it off and attributes is null, meaning “not fetched”, distinct from an empty bag meaning “no metadata”.

fromId
propertyoptionalnumber

Where the window starts, defaulting to 0.

Take it from the nextId of the previous page. delete_item documents that item indices are never reused, so resuming there cannot skip or repeat an item even while the collection is being written to.

limit
propertyoptionalnumber

How many items this page returns, defaulting to DEFAULT_PAGE_LIMIT and capped at MAX_PAGE_LIMIT.

There is no “give me everything” here, on purpose. Nothing bounds a collection. The only item ceiling the pallet has is index-space exhaustion, TooManyItems reads “the per-collection item index space is exhausted”, and the index is a u32. So a collection large enough to break this read is an afternoon of work for its owner. Follow nextId to walk the whole catalogue in bounded pieces.

A page walks past indices whose definitions were deleted rather than coming up short, so it returns exactly limit unless the index space runs out or a heavily-pruned stretch exhausts the scan budget.

interface GetCollectionsOptions

What every paged read here accepts besides its own window: the block to read at, and the signal that stops it.

Extends: PinnedReadOptions

Properties

fromId
propertyoptionalnumber

Where the window starts, defaulting to 0.

Take it from the nextId of the previous page, which is the id after the last one that page returned. Because ids are only ever appended and never reused, resuming there is stable: paging forward cannot skip or repeat a collection while the chain is written to, which offset-based paging over a mutable set cannot promise.

limit
propertyoptionalnumber

How many collections this page returns, defaulting to DEFAULT_PAGE_LIMIT and capped at MAX_PAGE_LIMIT.

There is no “give me everything” here, on purpose. Nothing bounds how many collections exist, so a read that returned all of them would be priced at the size of the chain. A page costs a constant four storage reads, the id ceiling plus three keyed reads over the window, whatever the chain holds. Walk nextId to the end for the rest.

A page returns exactly limit collections, walking past ids whose collections were deleted rather than coming up short. It returns fewer only when the id space runs out, or, pathologically, when a mostly-deleted range exhausts the scan budget. Either way nextId === null is the only end signal, so follow it rather than counting.

interface GetVerifiedArtworkOptions

Properties

signal
propertyoptionalAbortSignal
source
propertyArtworkSource

interface ImageRef

The image metadata of an item, read both ways.

One deployment stores a 32-byte content digest here, another an ASCII IPFS CID. Nothing declares which, so both readings are reported.

Properties

hex
propertystring

The raw bytes as 0x-prefixed hex. Always present.

text
propertystring | null

The same bytes as UTF-8, or null when they are not readable text.

interface MintPreview

What claiming one credit into one collection would mint.

preview_mints runs the real claim selector, so for a Random collection this is the item the claim will produce, and switching collection is the only way to change it. A Contract collection asks its contract, which can fail, and that failure is an outcome here rather than an error: the chain was asked and answered.

Properties

collection
propertynumber
outcome
property{ imageRef: ImageRef | null; item: number; name: string | null; rarity: string | null; tag: "Mints"; via: ItemSelection } | { reason: string; tag: "Fails" }

interface MintPreviewResult

What one previewClaim call returns.

Properties

at
propertyFinalizedSnapshot
previews
propertyMintPreview[]

One per collection asked for, in the order asked.

interface NftsChain

The client these reads take: six storage entries, and the raw client they pin a block with.

A client from getChainAPI(...) or createChainClient({ chains: { assetHub } }) satisfies it whole. A TypedApi on its own does not, however complete its query surface is: every read pins a finalized block before it touches storage, and only the raw client answers for that.

Properties

assetHub
property{ apis: { NftClaimsApi: { preview_mints: unknown } }; query: { NftClaims: { CollectionMinters: { getValues: unknown } }; Scarcity: { CollectionMetadata: { getEntries: unknown; getValues: unknown }; Collections: { getValue: unknown; getValues: unknown }; ItemDefs: { getValues: unknown }; ItemMetadata: { getEntries: unknown; getValues: unknown }; NextCollectionId: { getValue: unknown } } } }
raw
property{ assetHub: { getFinalizedBlock: unknown } }

interface NftsCreditsChain

The second chain a credits read needs, beside NftsChain.

Credits are awarded on the People chain, which the chain client names individuality, and become spendable on Asset Hub once their award block is rooted there. So getClaims takes NftsChain & NftsCreditsChain, the same composition readCurrentGame uses in @parity/product-sdk-individuality, and the catalogue reads keep a contract that never asks for a chain they do not read. A client from getChainAPI(...) satisfies both at once.

Matched against the generated Paseo descriptors on 2026-09-25:

NftCredits.NftClaimCreditBlocks map AccountOrPerson -> Vec<u32> NftCredits.NftClaimCreditAwards map (u32, u32) -> Vec<{ claimant, credit }> NftCreditsApi.nft_claim_credit_roots(claimant) -> Vec<(u32, { game_index, root, leaf_count, timestamp })> NftCreditsApi.nft_claim_credit_proofs(block, claimant) -> Result<Vec<{ credit, leaf_index, proof }>, ProofError> NftClaims.CreditTrees map u32 -> { game_index, root, leaf_count, timestamp } NftClaims.ClaimedLeaves map u32 -> bitmap, bit `leaf_index`, least significant first

NftClaimCreditAwards is chunked: the first key is the award block, the second the chunk within it, and a block holds at most CHUNKS_PER_TREE chunks. PAPI renders the key as FixedSizeArray<2, number>, and on this map its prefix scan fails to decode the keys at runtime, so the read asks for the chunks by exact key instead: a window of [block, 0..n) keys in one getValues, widened while the last chunk in the window is non-empty. A missing chunk is an empty list, not undefined, because the map has a default.

AccountOrPerson is an enum, and PAPI represents it as { type, value }. The read builds it from a Claimant.

Properties

assetHub
property{ query: { NftClaims: { ClaimedLeaves: { getValues: unknown }; CreditTrees: { getValues: unknown } } } }
individuality
property{ apis: { NftCreditsApi: { nft_claim_credit_proofs: unknown; nft_claim_credit_roots: unknown } }; query: { NftCredits: { NftClaimCreditAwards: { getValues: unknown }; NftClaimCreditBlocks: { getValue: unknown } } } }
raw
property{ individuality: { getFinalizedBlock: unknown } }

interface PinnedReadOptions

What every paged read here accepts besides its own window: the block to read at, and the signal that stops it.

Properties

at
propertyoptionalFinalizedSnapshot

Address a block a previous read already pinned, instead of pinning a new one.

Pass a FinalizedSnapshot straight from the at of another result. Without it every call pins its own finalized block, which is right for unrelated questions and wrong for one question asked in pages: a walk over its own snapshots is not a walk of any single chain state. It is also how two reads are made to agree, the registry and the full list at one block.

The node must still have the block pinned, and a walk can outlive that. A page that fails on the err channel mid-walk is not the end of the walk: drop at, read again from the last nextId, and continue on the new snapshot. The examples on each read show the shape.

signal
propertyoptionalAbortSignal

Forwarded into every underlying pull, so an aborted caller stops the whole batch. No deadline is applied here, that belongs to the caller.

interface PreviewClaimOptions

Properties

at
propertyoptionalFinalizedSnapshot

Join a block another read already pinned.

collections
propertynumber[]

The collections to preview into, from getClaimableCollections.

credit
propertystring

The credit hash, 0x prefixed, as getClaims reports it.

signal
propertyoptionalAbortSignal

Forwarded into every underlying pull, so an aborted caller stops the batch.

interface RawCollection

Scarcity.Collections, narrowed to the fields these reads use.

Properties

item_count
propertynumber
next_item_index
propertynumber

The exclusive end of the item index space of this collection.

Distinct from item_count: indices are allocated sequentially and never reused, so this counts every item ever defined while item_count counts the live ones. A paged catalogue read walks against this, which is why it needs no extra read to learn where the space ends.

owner
propertystring

interface RawCreditAward

One row of NftCredits.NftClaimCreditAwards.

Properties

claimant
property{ type: "Account" | "Person"; value: string }
credit
propertystring

interface RawCreditProof

One entry of a successful NftCreditsApi.nft_claim_credit_proofs.

Properties

credit
propertystring
leaf_index
propertynumber
proof
propertystring[]

The sibling hashes from the leaf up to the root, 0x prefixed.

interface RawCreditRoot

NftCredits.NftClaimCreditRoots, and the same shape NftClaims.CreditTrees stores.

Properties

game_index
propertynumber
leaf_count
propertynumber
root
propertystring
timestamp
propertynumber

interface RawItemDef

Scarcity.ItemDefs.

Properties

live_supply
propertynumber
supply
propertynumber
transferability
property{ type: Transferability }

interface RawMetadataEntry

Scarcity.CollectionMetadata / ItemMetadata / InstanceMetadata.

Properties

value
propertyRawBytes

interface RawMinter

NftClaims.CollectionMinters.

Properties

owner
propertystring
selection
property{ type: string; value?: unknown }

interface ReadAt

Options every pinned storage read is given, so all of them agree on a block.

Properties

at
propertystring
signal
propertyoptionalAbortSignal

Type Aliases

type ArtworkSource

Where the bytes for a digest come from. null when the source has nothing.

type ArtworkSource = (address: ArtworkAddress, signal?: AbortSignal) => Promise<Uint8Array | null>

type Claimant

Whose credits to read: the People chain keys them by account or by person alias.

type Claimant = { address: string; tag: "Account" } | { alias: string; tag: "Person" }

type ClaimState

Where one credit stands between being awarded and being spent.

earned means the People chain awarded it and Asset Hub has not yet received the root of its award block, so a claim would be refused. claimable means the root arrived and the leaf is unspent. claimed means the leaf is spent, so the item it minted exists somewhere and the credit is done. unprovable means the awards of the block are gone, pruned or expired, so the block counted but this read cannot say how many credits it held or produce the leaf a claim needs.

An earned entry with a null hash is a block still to come: awarding spills into later blocks, and the credits are not known until that block is built.

type ClaimState = "earned" | "claimable" | "claimed" | "unprovable"

type CollectionItemsResult

The answer to “what is in collection N”: the catalogue, or a clean miss.

A collection nobody created is not a failure. It has no Scarcity.Collections record, the chain says so, and that answer travels on the ok channel.

type CollectionItemsResult = { at: FinalizedSnapshot; collection: CollectionDetail; idCeiling: number; nextId: number | null; tag: "Found" } | { at: FinalizedSnapshot; id: number; tag: "NotFound" }

type ItemSelection

How a claim picks the item it mints, from NftClaims.CollectionMinters.

Contract carries a pallet-revive H160, 0x-prefixed. The runtime validates it at registration through CollectionSelector::validate, so an address here always had code at the block it was registered in.

type ItemSelection = { tag: "Random" } | { address: string; tag: "Contract" }

type RawBytes

The value of a metadata entry: the raw bytes, or the PAPI Binary wrapper around them.

Both are accepted because PAPI ≥2.0 dropped the Binary class for some codecs and kept it for others. It is the same reason verify.ts in @parity/product-sdk-cloud-storage accepts both.

type RawBytes = Uint8Array | { asBytes: unknown }

type RawMintOutcome

One NftClaimsApi.preview_mints outcome, positionally matched to its query.

type RawMintOutcome = { type: "Mints"; value: { item: number; via: { type: string; value?: unknown } } } | { type: "Fails"; value: { reason: { type: string; value?: unknown } } }

type RuntimeResult

The runtime Result a PAPI runtime API call resolves to.

type RuntimeResult = { success: true; value: T } | { success: false; value: E }

type Transferability

Whether a minted instance of an item can be sent on, from ItemDefs.transferability.

type Transferability = "Transferable" | "Soulbound"

type VerifiedArtwork

The answer, on the ok channel whatever the bytes did.

Verified is the only tag that carries bytes. Missing means the source had nothing under that address. Mismatch means the source answered with bytes that do not hash to the digest, which is the case this read exists for: they are reported as absent rather than handed over. Unreadable means the ImageRef could not be decoded into an address, is a CID with a codec other than raw, or names a multihash other than blake2b-256 or sha2-256 that this read cannot check, which is a metadata problem rather than a source one.

type VerifiedArtwork = { address: ArtworkAddress; bytes: Uint8Array; tag: "Verified" } | { address: ArtworkAddress; tag: "Missing" } | { address: ArtworkAddress; tag: "Mismatch" } | { tag: "Unreadable" }

Variables

DEFAULT_PAGE_LIMIT

How many entries a read returns when the caller does not say.

Every read here is bounded. Nothing caps how many collections exist or how many items a collection holds. The only ceilings the pallet has are index-space exhaustion, and the indices are u32. So a read whose default is “everything” is a read that works until a deployment grows and then breaks a browser tab. A default page is the safe end of that trade: a caller who wants everything follows the cursor and gets it, in bounded pieces.

let DEFAULT_PAGE_LIMIT: 100 = 100

MAX_PAGE_LIMIT

The largest page a read will return, and the widest window one probe may ask for.

A page reads its window by exact key, and PAPI spends one storage operation per key rather than batching them into one request (see the cost note in chain.ts), so this is what bounds the concurrent operations a read opens. Both places it applies matter: the page a caller receives, and the window the scan widens to over a sparse range, which is not the same number once gaps make the walk read more ids than it keeps.

Asking for more than this clamps rather than fails: the cursor still reports where the page stopped, so following it is correct either way.

let MAX_PAGE_LIMIT: 1000 = 1000

SCAN_BUDGET_FACTOR

How many ids a page may scan per collection it was asked for.

A page fills itself by walking the id space, so a range that holds few of what it is looking for could in principle read a lot of ids to find a few. This bounds that: at limit * SCAN_BUDGET_FACTOR ids the page returns what it has and reports where to resume, rather than reading on.

What “few” means differs by read. For getCollections it is deleted collections, which are rare because delete_collection requires the collection to be emptied first, so the budget is a safety net. For getClaimableCollections it is unregistered collections, which are structural: a page fills only while roughly one collection in SCAN_BUDGET_FACTOR is registered, and below that the page comes back short. That is the right way round: a registry that sparse is small, so following nextId through the short pages still reads all of it in a few pages.

let SCAN_BUDGET_FACTOR: 16 = 16