Skip to content
⌘K

Contracts

Items contract

The rare_items Move package: game items as kiosk objects, studio classes and kinds, signed claim and state vouchers, royalties, events and error codes.

The rare_items package turns game items into Sui objects. Each class of items is its own type Item<C>, with one marker type C per class. Studios mint items from their server or hand players signed vouchers, and the items trade on any Sui marketplace because their TransferPolicy uses only Mysten's standard kiosk rules. The package depends only on the Sui framework and Mysten's kiosk rules.

Published on mainnet. No game has registered item classes yet.

Published on Sui mainnetView on Suiscan
Package id0xda09fa9db4a54d4b0c337fb7ee41311f4679a7f6049e58372a4951968a32f73c
Config0xd0b9551e6559ce3a93e4778bb37decf3951cf30c358f6fe38f4bac358a9be097

On this page, PACKAGE_ID is the package id, CONFIG_ID is the shared Config, a Class<C> is the shared object for one class, POL is that class's shared TransferPolicy<Item<C>> and 0x6 is the Clock.

Modules

ModuleWhat it does
configThe shared Config: the package Publisher and the marker type to class index.
classClasses, kinds, the game key, pause and royalties.
itemThe Item<C> object and its attributes.
mintStudio mints, voucher claims and burns.
stateAttribute updates from state vouchers.
voucherVoucher message format and signature checks.
eventsEvery event the package emits.

Shared objects and caps

ObjectOwnerWhat it is
config::Configshared, oneHolds the package Publisher, so only class::create can make a TransferPolicy<Item<C>> or Display<Item<C>>. Maps each marker type C to its one class. Only class creation writes it.
config::AdminCapplatformPauses class creation and migrates the Config. It has no power over any class, item, policy or royalty.
class::Class<C>shared, one per CGame slug, name, the studio's ed25519 game key (issuer), the kinds, the policy id and its TransferPolicyCap, the Display, lock, paused, counters and the claim nonces.
class::ItemCap<C>the studioAdds kinds, sets the game key, pauses, withdraws royalties and mints. key + store, so a multisig can hold it.
TransferPolicy<Item<C>>shared, one per Croyalty_rule, plus kiosk_lock_rule when the class is locked. The policy cap stays inside the class and no call adds other rules.
item::Item<C>the player, usually in a kioskclass, kind, serial (1-based per kind), name, description, image_url (copied from the kind at mint), attrs: VecMap<String, String>, rev.

Each class is its own shared object, so claims in different classes never wait on each other.

Locked and unlocked classes

ClasslockPolicy rulesIn kiosksBurn
Cosmeticstrueroyalty + kiosk locklocked; kiosk::take is refused, so every sale pays the royaltyno (302)
Gearfalseroyaltyplaced; the owner can kiosk::take ityes

Royalties are paid on every marketplace sale of either class. A purchase that skips the royalty cannot confirm its TransferRequest.

Studio calls

Move
public fun create<C>(cfg: &mut Config, publisher: &Publisher, game: String, name: String, royalty_bps: u16, royalty_min_mist: u64, lock: bool, issuer: vector<u8>, ctx: &mut TxContext): (Class<C>, ItemCap<C>)
public fun share<C>(class: Class<C>)
public fun add_kind<C>(cap: &ItemCap<C>, class: &mut Class<C>, name: String, description: String, image_url: String, max_supply: u64): u64
public fun set_issuer<C>(cap: &ItemCap<C>, class: &mut Class<C>, pk: vector<u8>)
public fun set_paused<C>(cap: &ItemCap<C>, class: &mut Class<C>, paused: bool)
public fun withdraw_royalties<C>(cap: &ItemCap<C>, class: &Class<C>, policy: &mut TransferPolicy<Item<C>>, ctx: &mut TxContext): Coin<SUI>
  • create needs the Publisher of the module that defines C (201), and each C gets one class (103). The marker is a one-module package the studio publishes from its wallet. It shares the policy and returns the class unshared with its ItemCap, so kinds can be added in the same PTB before share. Royalty bps is at most 10,000 (203); 0 bps and a 0 floor add no royalty rule. issuer is 32 bytes, or empty for no vouchers (202).
  • add_kind returns kind ids in order from 0. max_supply = 0 is open supply.
  • set_issuer sets or rotates the game key. The old key stops working in the same transaction. Empty turns vouchers off; studio mints still work.
  • set_paused stops claims, studio mints and state updates. Trading and burning are never paused.
  • withdraw_royalties takes every royalty collected in the class's policy.
  • mint::mint<C>(cap, class, kind, keys, vals): Item<C> mints from the studio's own PTB, for airdrops. For a locked class, the same PTB must lock the item in a kiosk, or it is not tradable under the policy.

Text limits: game slug 1 to 96 bytes, names 1 to 128, description 1,024, URL 512 (204). Attribute keys are 1 to 64 bytes, values up to 512 (601), at most 64 per item (602). A key given twice keeps the last value.

Player calls

Move
public fun claim<C>(class: &mut Class<C>, policy: &TransferPolicy<Item<C>>, kiosk: &mut Kiosk, kcap: &KioskOwnerCap, kind: u64, keys: vector<String>, vals: vector<String>, nonce: u64, expires_ms: u64, sig: vector<u8>, clock: &Clock, ctx: &mut TxContext): ID
public fun claim_fresh<C>(class: &mut Class<C>, policy: &TransferPolicy<Item<C>>, kind: u64, keys: vector<String>, vals: vector<String>, nonce: u64, expires_ms: u64, sig: vector<u8>, clock: &Clock, ctx: &mut TxContext): ID
public fun apply<C>(class: &Class<C>, item: &mut Item<C>, keys: vector<String>, vals: vector<String>, nonce: u64, expires_ms: u64, sig: vector<u8>, clock: &Clock)
public fun burn<C>(class: &mut Class<C>, item: Item<C>, ctx: &TxContext)
  • mint::claim puts the item in the sender's kiosk, locked or placed as the class says. kcap must open that kiosk (301).
  • mint::claim_fresh creates a shared kiosk, claims into it and sends the kiosk cap to the sender.
  • state::apply takes the item by mutable reference. In a PTB: kiosk::borrow_val (works for locked and placed items, not while listed), apply, kiosk::return_val. It writes only the keys it is given and bumps rev.
  • mint::burn destroys an item of an unlocked class (302 for locked, 303 for the wrong class). Take it out of the kiosk first.

The player is always the transaction sender. A sponsor can pay the gas.

Voucher checks, in order

  1. The class is current and not paused (207, 208).
  2. Claims only: the policy is the class's (206).
  3. A game key is set (501).
  4. clock < expires_ms (502).
  5. The signature is the game key's over the message (503).
  6. Replay guard. Claims: the (player, nonce) pair is unused in this class (211), then recorded on the class. State: the message names the item's current rev, so a voucher works once and only in order.
  7. The kind exists (209) and is not sold out (210), and the attributes pass the limits above.

Voucher format

The studio's game server signs these raw bytes with ed25519 (no hashing, 64-byte signature):

Text
claim: "RARE_ITEM_V1"       || class id (32) || player (32) || kind (u64 LE) || bcs(keys) || bcs(vals) || nonce (u64 LE) || expires_ms (u64 LE)
state: "RARE_ITEM_STATE_V1" || item id (32)  || rev (u64 LE)                  || bcs(keys) || bcs(vals) || nonce (u64 LE) || expires_ms (u64 LE)

bcs(keys) is the BCS of vector<String>: a ULEB128 count, then each string as a ULEB128 byte length and its UTF-8 bytes. voucher::claim_message and voucher::state_message return the same bytes on chain. Object ids differ between networks, so a voucher can never be replayed on another network.

Display

Held inside the class, so it cannot be edited outside it: name, description, image_url, thumbnail_url (same as the image), project_url (https://rare.market/games/<game>), creator (the class name), attributes (from attrs), serial.

Views

  • config: version, paused, class_count, class_of<C>(): Option<ID>
  • class: cap_class, game, name, issuer, policy, lock, paused, version, minted, burned, kind_count, kind(id) with the kind_* getters, nonce_used(player, nonce), display
  • item: class, kind, serial, name, description, image_url, attrs, attr(key), rev
  • mint: source_mint() = 0, source_claim() = 1

Fees

The package charges no fees. Studios set their own royalty per class, collected in the class's policy.

Events

All events are in PACKAGE_ID::events.

EventFieldsWhen
ClassCreatedclass, cap, policy, marker, item_type, game, name, royalty_bps, royalty_min_mist, lock, issuer, creatorclass::create
KindAddedclass, kind, name, description, image_url, max_supplyadd_kind
IssuerSetclass, public_keyset_issuer
ClassPausedclass, pausedclass::set_paused
RoyaltiesWithdrawnclass, amount, bywithdraw_royalties
ItemMinteditem, class, kind, serial, source, owner, kiosk, nonce, keys, valsEvery new item. source 0 is a studio mint (owner, kiosk and nonce empty), 1 is a claim
ItemUpdateditem, class, rev, keys, valsstate::apply (rev is the new revision)
ItemBurneditem, class, kind, serial, bymint::burn
ConfigPausedconfig, pausedconfig::set_paused

Sales are indexed from kiosk and marketplace events, as for any kiosk item.

Error codes

ModuleCodes
config101 EOutdated, 102 EPaused, 103 EClassExists, 104 ENotNewer
class201 ENotTypeOwner, 202 EBadKey, 203 EBadRoyalty, 204 ETooLong, 205 EWrongCap, 206 EWrongPolicy, 207 EOutdated, 208 EPaused, 209 ENoKind, 210 ESoldOut, 211 ENonceUsed, 212 ENotNewer
mint301 ENotOwner, 302 EBurnLocked, 303 EWrongClass
state401 EWrongClass
voucher501 EVouchersOff, 502 EExpired, 503 EBadSignature
item601 EBadAttrs, 602 ETooManyAttrs

Example

Create a class with two kinds and share it, from the studio's wallet. MARKER is the studio's marker type, such as 0x...::hat::HAT.

TypeScript
import { Transaction } from "@mysten/sui/transactions";

const tx = new Transaction();
const [cls, cap] = tx.moveCall({
  target: `${PACKAGE_ID}::class::create`,
  typeArguments: [MARKER],
  arguments: [
    tx.object(CONFIG_ID),
    tx.object(PUBLISHER_ID),       // the marker package's Publisher
    tx.pure.string(gameSlug),
    tx.pure.string(gameName),
    tx.pure.u16(500),              // 5% royalty
    tx.pure.u64(0),                // no floor
    tx.pure.bool(true),            // locked (cosmetics)
    tx.pure.vector("u8", gameKey), // 32-byte ed25519 public key
  ],
});
for (const k of kinds) {
  tx.moveCall({
    target: `${PACKAGE_ID}::class::add_kind`,
    typeArguments: [MARKER],
    arguments: [cap, cls, tx.pure.string(k.name), tx.pure.string(k.description), tx.pure.string(k.imageUrl), tx.pure.u64(k.maxSupply)],
  });
}
tx.moveCall({ target: `${PACKAGE_ID}::class::share`, typeArguments: [MARKER], arguments: [cls] });
tx.transferObjects([cap], studioAddress);
--
TPS:--