Skip to content

pallet-scarcity: coinage-style NFT ownership with feeless rest-time-prioritized transfers - #12730

Draft
shawntabrizi wants to merge 10 commits into
masterfrom
shawntabrizi/pallet-scarcity
Draft

pallet-scarcity: coinage-style NFT ownership with feeless rest-time-prioritized transfers#12730
shawntabrizi wants to merge 10 commits into
masterfrom
shawntabrizi/pallet-scarcity

Conversation

@shawntabrizi

Copy link
Copy Markdown
Member

Summary

A new FRAME pallet for Scarcity collectibles (Parity's gaming/Trinity initiative): NFT-like instances under a coinage-style ownership model — one instance per fresh, balance-less purse public key — with feeless transfers authorized by the purse key itself through a TransactionExtension, prioritized by the instance's rest time (seconds since it last moved).

Draft: seeking directional review on the transaction-extension approach and priority calibration before polish (benchmarks are placeholder weights).

Model

  • Collection → ItemDefinition → instance: item definitions are immutable once written (typed, on-chain-comparable Stat pairs + an opaque metadata blob; Kind/next_variant tagging for a later fusion mechanic). The same definition can be minted many times; each copy is a discrete instance with a permanent InstanceId.
  • One instance per key, globally (the coinage rule): NftsByOwner: Map<AccountId, Nft> plus a stable reverse index Instances: Map<InstanceId, AccountId>. Wallets derive a fresh key per instance from the root secret; instance identity (not key identity) is the stable external handle.
  • No mutable state: the pallet stores immutable definitions and mint facts only; application state belongs to higher layers keyed by InstanceId.

The AsScarcity transaction extension

Mirrors the origin-modifier pattern (validate → elevate a custom Origin::Nft { owner, nft } → consume the instance in prepare → restore + quadratic backoff lock on failed dispatch):

  • Feeless by construction: intended to sit in the runtime's origin-modifier group so the origin is non-Signed before CheckNonce/payment run — purse keys need neither nonce nor balance.
  • Priority = min(now − last_moved, cap): recently-moved instances sink under congestion; rested ones rise. This is the anti-spam for free transfers (a per-instance wall-clock rate limit).
  • Nonce-free replay model: consumption-on-transfer + provides pool tag + transaction mortality + failure locks; destination pre-validation at the pool keeps user error from reaching dispatch.
  • A strictly-monotonic moves counter on each instance serves as the authorization epoch for future detached-signature flows.

Testing

21 unit tests, including direct validate/prepare/post_dispatch_details pipeline coverage: priority scaling + cap, same-block double-use, restore-and-lock on failed dispatch with quadratic backoff, pool-side rejections (side-effect-free), one-instance-per-key on both mint and transfer, reverse-index consistency, item-definition immutability.

Deliberately out of scope here

Claim/distribution (a separate ring-membership-based pallet downstream), fusion, burn, storage deposits (companion allowance systems), benchmarking (placeholder WeightInfo), and runtime integration (to follow; requires the origin-modifier placement noted above).

🤖 Generated with Claude Code

@shawntabrizi

Copy link
Copy Markdown
Member Author

Pushed two design updates from downstream review:

Metadata is now a two-level mutable key/value store. The item definition's separate stats (typed numerics) and metadata (opaque blob) fields are replaced by one metadata field of bounded raw-byte key/value entries, stored at collection level (defaults) and item level (per-key overrides), resolved item-first. Written only by the collection owner, per-entry storage deposits (an overwrite updates the existing ticket rather than charging a second one), per-scope entry caps so enumeration stays bounded, and no freeze — a consumer needing stability snapshots or hash-commits what it depends on. Values are raw bytes as in pallet_nfts; SCALE-encoded u128 is a documented convention the pallet does not enforce.

Metadata lives in its own storage maps rather than on the definition, chiefly for PoV: in-struct, a full metadata set (~9 KB at these bounds) would be pulled into the proof by every operation touching the definition, including mint. Split out, the definition is ~26 bytes.

The Nft.moves replay epoch is removed. It had no consumer: if fee sponsorship is a general runtime feature the purse key still signs the transaction, so consumption-in-prepare, the emptied source key and mortality already prevent replay and no detached signature forms; contract-side flows carry their own nonces. last_moved is untouched — it drives rest-time transfer priority.

39 tests / 47 with runtime-benchmarks / 39 with try-runtime; no-default-features, all-features, clippy and nightly fmt clean; prdoc updated.

Note call_index(5) is deliberately left unused: a collection "state contract" anchor exists in the downstream copy and will port separately, and reserving its index avoids a breaking renumber.

shawntabrizi and others added 4 commits July 28, 2026 16:35
Ports a downstream fix that had not reached this branch: an instance is
backed by NftsByOwner, the Instances reverse index, and its own
InstanceDeposits ticket — the footprint counted two, and omitted the
instance key and ticket size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant