@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.CollectionMinterspreviewClaim 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_proofsAn 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 theCollectionMetadata/ItemMetadatastorage layers, which answer the same question and are carried by the pinned descriptor.previewClaimhas no storage equivalent, so it is the exception, and the fidelity guard in@parity/product-sdkchecks its signature against the descriptor the same way it checks the storage entries. attributescosts 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 isnull, 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.
TooManyItemsreads “the per-collection item index space is exhausted”, and the index is au32, so there is no configured limit to lean on and a ten-thousand-item collection is an afternoon of work. That is whygetCollectionItemspages like the listing reads do, rather than assuming a small catalogue.itemCountfrom either listing read gives the size before you commit to walking a collection whole. - Nothing purse-scoped.
getOwnedNfts,getNextEmptyPurseandfindPurseHoldingall 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 byproductId, 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,imageandrarityare lifted into typed fields because every deployment read so far carries them; the rest of the bag is passed through untouched.imageis 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-nftsExports
Classes
| Name | Summary |
|---|---|
NftsChainEntryError | A storage entry a read needs cannot be read on the client it was given. |
NftsDecodeError | A raw storage value did not match the shape the descriptor promised: an |
NftsIdError | An id or index a read was given cannot address chain state. |
ProductNftsError | Base class for errors raised by @parity/product-sdk-nfts. |
Functions
| Name | Summary |
|---|---|
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
| Name | Summary |
|---|---|
ArtworkAddress | What an image metadata value names, once decoded. |
Claim | One NFT claim credit, as the People chain awarded it and Asset Hub sees it. |
ClaimableCollection | A collection registered to accept claims. |
ClaimableCollectionsResult | What one getClaimableCollections call returns. |
ClaimsResult | What one getClaims call returns. |
Collection | A collection, claimable or not. |
CollectionDetail | One page of the item catalogue of a collection. |
CollectionItem | One item definition in a collection, with its display metadata merged in. |
CollectionsResult | What one getCollections call returns. |
Entry | One getEntries row: the keys that were not fixed by the query, and the value. |
FinalizedSnapshot | The finalized block every value in one result was read at. |
GetClaimableCollectionsOptions | What every paged read here accepts besides its own window: the block to read |
GetClaimsOptions | — |
GetCollectionItemsOptions | What every paged read here accepts besides its own window: the block to read |
GetCollectionsOptions | What every paged read here accepts besides its own window: the block to read |
GetVerifiedArtworkOptions | — |
ImageRef | The image metadata of an item, read both ways. |
MintPreview | What claiming one credit into one collection would mint. |
MintPreviewResult | What one previewClaim call returns. |
NftsChain | The client these reads take: six storage entries, and the raw client they pin |
NftsCreditsChain | The second chain a credits read needs, beside NftsChain. |
PinnedReadOptions | What every paged read here accepts besides its own window: the block to read |
PreviewClaimOptions | — |
RawCollection | Scarcity.Collections, narrowed to the fields these reads use. |
RawCreditAward | One row of NftCredits.NftClaimCreditAwards. |
RawCreditProof | One entry of a successful NftCreditsApi.nft_claim_credit_proofs. |
RawCreditRoot | NftCredits.NftClaimCreditRoots, and the same shape NftClaims.CreditTrees stores. |
RawItemDef | Scarcity.ItemDefs. |
RawMetadataEntry | Scarcity.CollectionMetadata / ItemMetadata / InstanceMetadata. |
RawMinter | NftClaims.CollectionMinters. |
ReadAt | Options every pinned storage read is given, so all of them agree on a block. |
Type Aliases
| Name | Summary |
|---|---|
ArtworkSource | Where the bytes for a digest come from. null when the source has nothing. |
Claimant | Whose credits to read: the People chain keys them by account or by person alias. |
ClaimState | Where one credit stands between being awarded and being spent. |
CollectionItemsResult | The answer to “what is in collection N”: the catalogue, or a clean miss. |
ItemSelection | How a claim picks the item it mints, from NftClaims.CollectionMinters. |
RawBytes | The value of a metadata entry: the raw bytes, or the PAPI Binary wrapper |
RawMintOutcome | One NftClaimsApi.preview_mints outcome, positionally matched to its query. |
RuntimeResult | The runtime Result a PAPI runtime API call resolves to. |
Transferability | Whether a minted instance of an item can be sent on, from ItemDefs.transferability. |
VerifiedArtwork | The answer, on the ok channel whatever the bytes did. |
Variables
| Name | Summary |
|---|---|
DEFAULT_PAGE_LIMIT | How many entries a read returns when the caller does not say. |
MAX_PAGE_LIMIT | The largest page a read will return, and the widest window one probe may ask |
SCAN_BUDGET_FACTOR | How 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): NftsChainEntryErrorProperties
entry
string | nullThe 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): NftsDecodeErrorclass 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): NftsIdErrorProperties
id
numberThe 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): ProductNftsErrorProperties
isSdkError
trueDiscriminant present on all SDK errors.
source
"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 | nullgatewaySource()
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): ArtworkSourcegetClaimableCollections()
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): ArtworkSourcepreviewClaim()
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
stringThe CID, base32 lower case, built from the digest when the chain stored only that.
digest
`0x${string}`The 32-byte content digest, 0x prefixed. The Bulletin preimage key.
multihash
numberThe 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
numberThe People chain block the credit was awarded in.
awardedAt
number | nullWhen the award block was rooted, in Unix seconds, or null for a block
whose root has not been recorded yet.
gameIndex
number | nullThe game the award block belongs to, or null before its root exists.
hash
string | nullThe 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
number | nullWhere 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
string[] | nullThe 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
ClaimStateinterface 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
numberThe Scarcity collection id.
itemCount
number | nullLive 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
string | nullThe 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
string | nullThe 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
ItemSelectionHow a claim into this collection picks its item.
interface ClaimableCollectionsResult
What one getClaimableCollections call returns.
Properties
at
FinalizedSnapshotcollections
ClaimableCollection[]idCeiling
numberThe 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
number | nullThe 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
{ assetHub: FinalizedSnapshot; individuality: FinalizedSnapshot }Two chains, so two pinned blocks. Every value came from one or the other.
claims
Claim[]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
numberThe Scarcity collection id.
itemCount
numberLive item definitions, from Collections.item_count.
name
string | nullThe name metadata of the collection, or null when it sets none.
owner
stringThe Scarcity owner of the collection.
selection
ItemSelection | nullHow 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
numberitemCount
numberLive 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
CollectionItem[]The definitions in this page, ascending by index.
name
string | nullThe 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
Record<string, string> | nullEvery 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
ImageRef | nullThe 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
numberThe item index within its collection.
liveSupply
numberInstances currently alive, which is supply less those burned.
name
string | nullThe name metadata of the item, or null when neither it nor its collection sets one.
rarity
string | nullThe rarity metadata of the item, or null when unset.
supply
numberInstances the definition may ever mint.
transferability
TransferabilityWhether a minted instance can be sent on. A Soulbound one stays with its first owner.
interface CollectionsResult
What one getCollections call returns.
Properties
at
FinalizedSnapshotcollections
Collection[]idCeiling
numberThe 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
number | nullThe 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
Keysvalue
Valueinterface FinalizedSnapshot
The finalized block every value in one result was read at.
Properties
blockHash
stringblockNumber
numberinterface 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
numberWhere 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
numberHow 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
FinalizedSnapshotJoin a block another Asset Hub read already pinned.
claimant
ClaimantWhose credits. Accounts and person aliases are keyed apart on chain.
signal
AbortSignalForwarded 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
booleanFill 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
numberWhere 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
numberHow 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
numberWhere 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
numberHow 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
AbortSignalsource
ArtworkSourceinterface 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
stringThe raw bytes as 0x-prefixed hex. Always present.
text
string | nullThe 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
numberoutcome
{ 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
FinalizedSnapshotpreviews
MintPreview[]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
{ 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
{ 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 firstNftClaimCreditAwards 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
{ query: { NftClaims: { ClaimedLeaves: { getValues: unknown }; CreditTrees: { getValues: unknown } } } }individuality
{ apis: { NftCreditsApi: { nft_claim_credit_proofs: unknown; nft_claim_credit_roots: unknown } }; query: { NftCredits: { NftClaimCreditAwards: { getValues: unknown }; NftClaimCreditBlocks: { getValue: unknown } } } }raw
{ 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
FinalizedSnapshotAddress 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
AbortSignalForwarded 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
FinalizedSnapshotJoin a block another read already pinned.
collections
number[]The collections to preview into, from getClaimableCollections.
credit
stringThe credit hash, 0x prefixed, as getClaims reports it.
signal
AbortSignalForwarded 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
numbernext_item_index
numberThe 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
stringinterface RawCreditAward
One row of NftCredits.NftClaimCreditAwards.
Properties
claimant
{ type: "Account" | "Person"; value: string }credit
stringinterface RawCreditProof
One entry of a successful NftCreditsApi.nft_claim_credit_proofs.
Properties
credit
stringleaf_index
numberproof
string[]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
numberleaf_count
numberroot
stringtimestamp
numberinterface RawItemDef
Scarcity.ItemDefs.
Properties
live_supply
numbersupply
numbertransferability
{ type: Transferability }interface RawMetadataEntry
Scarcity.CollectionMetadata / ItemMetadata / InstanceMetadata.
Properties
value
RawBytesinterface RawMinter
NftClaims.CollectionMinters.
Properties
owner
stringselection
{ type: string; value?: unknown }interface ReadAt
Options every pinned storage read is given, so all of them agree on a block.
Properties
at
stringsignal
AbortSignalType 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 = 100MAX_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 = 1000SCAN_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