-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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:
- Catalog: Manages the structure and relationships between slots and parts
- 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:
- FIXED: A slot that can contain parts but cannot be added as a part to another slot
- 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:
- A flexible catalog system that can be reused across different NFT implementations
- A direct bridge to actual NFTs, enabling real-world composable NFT applications
Security Considerations
- Slot IDs are generated using a secure hashing function to prevent collisions
- All operations that modify the catalog state MUST validate the slot types and relationships
- The standard prevents circular references by design
- All state changes MUST emit appropriate events for tracking
- 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);