Skip to content

Catalog & Equippable Spec #3

@luisburigo

Description

@luisburigo

COMPOSABLE: Composable NFT Standard

The following standard allows for the implementation of a standard API for Composable NFTs using the Sway Language. This standard provides basic functionality for managing composable NFTs with fixed slots and parts, enabling complex NFT compositions while maintaining clear ownership and composition rules.

Motivation

A standard interface for Composable NFTs on Fuel allows external applications to interact with composable assets in a consistent way, whether that be NFT marketplaces, wallets, or other dApps. This standard enables the creation of complex NFT compositions while maintaining a clear and predictable structure.

The standard is divided into two main components:

  1. Catalog: Manages the structure and relationships between slots and parts
  2. Composable: Bridges the Catalog with actual NFTs, allowing assets to be composed together

Prior Art

The COMPOSABLE standard is inspired by various NFT composition standards like RMRK seen across different blockchains, but adapted for Fuel's unique architecture. The standard focuses on providing a simple yet powerful way to compose NFTs while maintaining clear ownership and composition rules.

Specification

Required Public Functions

Catalog Functions

The following functions MUST be implemented to follow the CATALOG standard:

fn register_slot(slot: Slot) -> SlotID

This function MUST register a new slot in the catalog and return its unique identifier. The slot can be either FIXED or PART.

fn get_slot(slot_id: SlotID) -> Option<Slot>

This function MUST return the slot type (FIXED or PART) for a given slot ID.

fn add_part(parent_slot: SlotID, part: SlotID)

This function MUST add a part to a parent slot. The parent slot MUST be FIXED and the part MUST be PART.

fn get_parts(parent_slot: SlotID) -> Vec<SlotID>

This function MUST return all parts associated with a parent slot.

fn remove_part(parent_slot: SlotID, part: SlotID)

This function MUST remove a part from a parent slot.

fn can_add_part(parent_slot: SlotID, part_slot: SlotID) -> bool

This function MUST return whether a part can be added to a parent slot based on the following rules:

  • Parent slot must exist and be FIXED
  • Part slot must exist and be PART
  • Part must not already be added to the parent slot

Composable Functions

The following functions MUST be implemented to follow the COMPOSABLE standard:

fn register_asset_slot(asset_id: AssetId, slot_id: SlotID)

This function MUST register an asset with a slot in the catalog. This creates a link between an NFT and its slot type.

fn get_asset_slot(asset_id: AssetId) -> Option<SlotID>

This function MUST return the slot ID associated with an asset.

fn add_asset_slot_part(parent_asset_id: AssetId)

This function MUST add the current asset as a part to a parent asset. The parent asset MUST be registered with a FIXED slot, and the current asset MUST be registered with a PART slot.

fn get_asset_slot_parts(asset_id: AssetId) -> Vec<AssetId>

This function MUST return all parts associated with a parent asset.

fn remove_asset_slot_part(parent_asset_id: AssetId, child_asset_id: AssetId)

This function MUST remove a part from a parent asset.

Slot Types

The standard defines two types of slots:

  1. FIXED: A slot that can contain parts but cannot be added as a part to another slot
  2. PART: A slot that can be added as a part to FIXED slots but cannot contain parts itself

Error Types

The following error types MUST be implemented:

Catalog Errors

enum CatalogError {
    PartAlreadyExists: (SlotID, SlotID),
    PartNotFound: (SlotID, SlotID),
    ParentNotFound: (SlotID),
    ParentNotFixed: (SlotID),
    PartNotPart: (SlotID, SlotID),
}

Composable Errors

enum ComposableError {
    ParentNotFound: (AssetId),
    SlotNotRegistered: (AssetId),
    ParentNotFixed: (AssetId),
    ChildNotFound: (AssetId),
    ChildNotPart: (AssetId),
    PartAlreadyExists: (AssetId, AssetId),
}

Events

The following events MUST be implemented:

PartAddedEvent

struct PartAddedEvent {
    parent_slot: SlotID,
    part_slot: SlotID,
}

PartRemovedEvent

struct PartRemovedEvent {
    parent_slot: SlotID,
    part_slot: SlotID,
}

ChildAddedEvent

struct ChildAddedEvent {
    parent_asset_id: AssetId,
    child_asset_id: AssetId,
}

Storage

The following storage MUST be implemented:

storage {
    catalog {
        slots: StorageMap<SlotID, Slot>,
        total_slots: u64,
        slots_parts: StorageMap<SlotID, StorageVec<SlotID>>,
    },
    composable {
        assets_slot: StorageMap<AssetId, SlotID>,
        assets_slot_parts: StorageMap<SlotID, StorageVec<AssetId>>,
    }
}

Rationale

The COMPOSABLE standard is designed to be simple yet powerful, allowing for complex NFT compositions while maintaining clear rules about what can be composed with what. The separation between FIXED and PART slots ensures a clear hierarchy and prevents circular references.

The standard uses Fuel's native storage capabilities to efficiently manage slot and part relationships, while providing a clear API for external applications to interact with.

The two-layer approach (Catalog and Composable) allows for:

  1. A flexible catalog system that can be reused across different NFT implementations
  2. A direct bridge to actual NFTs, enabling real-world composable NFT applications

Security Considerations

  1. Slot IDs are generated using a secure hashing function to prevent collisions
  2. All operations that modify the catalog state MUST validate the slot types and relationships
  3. The standard prevents circular references by design
  4. All state changes MUST emit appropriate events for tracking
  5. Asset composition is controlled through the UTXO model, ensuring atomic operations

Examples

Registering a Fixed Slot

let slot_id = catalog.register_slot(Slot::FIXED);

Adding a Part

let part_id = catalog.register_slot(Slot::PART);
catalog.add_part(slot_id, part_id);

Registering an Asset with a Slot

let asset_id = AssetId::from(0x1234...);
catalog.register_asset_slot(asset_id, slot_id);

Composing NFTs

// Parent NFT with a FIXED slot
let parent_asset_id = AssetId::from(0x1234...);
catalog.register_asset_slot(parent_asset_id, fixed_slot_id);

// Child NFT with a PART slot
let child_asset_id = AssetId::from(0x5678...);
catalog.register_asset_slot(child_asset_id, part_slot_id);

// Add the child as a part to the parent
catalog.add_asset_slot_part(parent_asset_id);

Getting All Parts of an Asset

let parts = catalog.get_asset_slot_parts(asset_id);

Removing a Part

catalog.remove_asset_slot_part(parent_asset_id, child_asset_id);

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions