Skip to content

XSC-0005: Non-Fungible Token

XSC-0005 is the core non-fungible token interface for Xian. It defines the ownership, approval, metadata, and event surface that wallets, explorers, marketplaces, and indexers can rely on for NFT collections.

Required Surface

An XSC-0005 collection exposes these storage hashes:

python
owners = Hash(default_value="")
balances = Hash(default_value=0)
approvals = Hash(default_value="")
operator_approvals = Hash(default_value=False)
metadata = Hash()
token_data = Hash(default_value="")

The standard function surface is:

python
@export
def change_metadata(key: str, value: Any):
    ...

@export
def balance_of(owner: str) -> int:
    ...

@export
def owner_of(token_id: str) -> str:
    ...

@export
def exists(token_id: str) -> bool:
    ...

@export
def transfer(token_id: str, to: str):
    ...

@export
def approve(token_id: str, to: str):
    ...

@export
def revoke(token_id: str):
    ...

@export
def get_approved(token_id: str) -> str:
    ...

@export
def set_approval_for_all(operator: str, approved: bool):
    ...

@export
def is_approved_for_all(owner: str, operator: str) -> bool:
    ...

@export
def transfer_from(token_id: str, to: str, main_account: str):
    ...

@export
def token_metadata(token_id: str) -> dict:
    ...

@export
def contract_metadata() -> dict:
    ...

Expected Semantics

  • each token_id has at most one owner
  • balance_of(owner) returns the number of live tokens owned by owner
  • transfer moves a token owned by ctx.caller
  • approve gives one spender authority over one token
  • set_approval_for_all gives an operator authority over all caller-owned tokens
  • transfer_from may be called by the owner, token-approved spender, or approved operator
  • successful transfers clear the single-token approval
  • token_metadata returns the token owner plus metadata needed to render or verify the asset

Required Metadata

The collection-level metadata hash must include:

  • standard: exactly XSC-0005
  • collection_name
  • collection_symbol
  • collection_description

Recommended additional fields:

  • collection_image
  • collection_website
  • operator

Token Metadata

The recommended token metadata shape is:

python
token_data[token_id, "name"] = "Example"
token_data[token_id, "description"] = "On-chain asset"
token_data[token_id, "creator"] = ctx.caller
token_data[token_id, "created"] = now
token_data[token_id, "mime_type"] = "image/svg+xml"
token_data[token_id, "encoding"] = "utf8"
token_data[token_id, "content"] = "<svg>...</svg>"
token_data[token_id, "content_hash"] = hashlib.sha256_text(content)
token_data[token_id, "uri"] = ""

For larger assets, collections may store chunks:

python
content_chunks[token_id, index] = chunk
token_data[token_id, "chunk_count"] = count
token_data[token_id, "content_hash"] = full_payload_hash

Chunking is an extension, not part of the minimum compliance surface.

Optional PixelGrid Extension

Pixel-art collections can expose custom palettes and compact frame data without making palette rendering part of the required XSC-0005 surface. The reference contract uses this storage shape:

python
palettes[palette_id, "size"] = 4
palettes[palette_id, "locked"] = True
palettes[palette_id, index] = "#ff00aa"

token_data[token_id, "render_schema"] = "xian.pixelgrid.v1"
token_data[token_id, "palette_id"] = palette_id
token_data[token_id, "width"] = 25
token_data[token_id, "height"] = 25
token_data[token_id, "frame_count"] = 8
token_data[token_id, "frame_delay_ms"] = 120
token_data[token_id, "pixel_encoding"] = "palette-index-64"
token_data[token_id, "content"] = "0123..."

Recommended exports:

python
@export
def create_palette(palette_id: str, colors: list, name: str = "", locked: bool = True):
    ...

@export
def set_palette_color(palette_id: str, index: int, color: str):
    ...

@export
def lock_palette(palette_id: str):
    ...

@export
def palette_info(palette_id: str) -> dict:
    ...

@export
def palette_color(palette_id: str, index: int) -> str:
    ...

@export
def mint_pixel_grid(...):
    ...

@export
def pixel_grid_info(token_id: str) -> dict:
    ...

palette-index-64 uses this alphabet:

text
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-

Each character is one pixel whose index must be lower than the locked palette size. A renderer reconstructs animation frames by reading width * height * frame_count pixels in order. Lock palettes before minting so token art does not change after issuance.

For PixelGrid assets, hash a domain-separated render source instead of hashing the raw pixel string alone:

python
hash_source = (
    "xian.pixelgrid.v1"
    + ":"
    + palette_id
    + ":"
    + str(width)
    + ":"
    + str(height)
    + ":"
    + str(frame_count)
    + ":"
    + str(frame_delay_ms)
    + ":"
    + pixels
)
token_data[token_id, "content_hash"] = hashlib.sha256_text(hash_source)

This avoids ambiguity for hex-looking pixel strings and binds the hash to the render-critical dimensions.

Expected Events

Core XSC-0005 collections should emit:

  • Transfer
  • Approval
  • ApprovalForAll
  • MetadataUpdate

Recommended indexed fields:

python
Transfer = LogEvent("Transfer", {
    "from": {"type": str, "idx": True},
    "to": {"type": str, "idx": True},
    "token_id": {"type": str, "idx": True},
})

Events are not currently enforced by the on-chain interface checker, so wallets and indexers should still treat event conformance as a behavioral requirement.

Optional Marketplace Extension

Marketplace helpers should be additive and must not be required for XSC-0005 compliance. A collection can expose:

python
@export
def list_for_sale(token_id: str, currency_contract: str, price: float, reserved_for: str = ""):
    ...

@export
def cancel_listing(token_id: str):
    ...

@export
def buy(token_id: str):
    ...

@export
def royalty_info(token_id: str, sale_price: float) -> dict:
    ...

Use basis points for royalties:

python
token_data[token_id, "royalty_bps"] = 500  # 5%
token_data[token_id, "royalty_receiver"] = creator

Compatibility Notes

  • keep function names and argument names stable so importlib.enforce_interface can validate collections
  • prefer one contract for ordinary NFT collections
  • split metadata or rendering into a companion contract only when there is a strong reason, and guard every mutating companion function with a controller check
  • store arbitrary media through MIME type, encoding, content, content hash, or chunks; keep collection-specific color palettes as an additive extension
  • keep return payloads under the chain return-size limit