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:
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:
@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_idhas at most one owner balance_of(owner)returns the number of live tokens owned byownertransfermoves a token owned byctx.callerapprovegives one spender authority over one tokenset_approval_for_allgives an operator authority over all caller-owned tokenstransfer_frommay be called by the owner, token-approved spender, or approved operator- successful transfers clear the single-token approval
token_metadatareturns the token owner plus metadata needed to render or verify the asset
Required Metadata
The collection-level metadata hash must include:
standard: exactlyXSC-0005collection_namecollection_symbolcollection_description
Recommended additional fields:
collection_imagecollection_websiteoperator
Token Metadata
The recommended token metadata shape is:
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:
content_chunks[token_id, index] = chunk
token_data[token_id, "chunk_count"] = count
token_data[token_id, "content_hash"] = full_payload_hashChunking 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:
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:
@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:
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:
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:
TransferApprovalApprovalForAllMetadataUpdate
Recommended indexed fields:
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:
@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:
token_data[token_id, "royalty_bps"] = 500 # 5%
token_data[token_id, "royalty_receiver"] = creatorCompatibility Notes
- keep function names and argument names stable so
importlib.enforce_interfacecan 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