Rivellum

Rivellum Portal

Checking...
testnet

Rivellum NFT Standard

Overview

The Rivellum NFT Standard provides a canonical implementation for non-fungible tokens (NFTs) with built-in royalty support, metadata conventions, and indexer integration.

Architecture

Core Types

NftId (32 bytes)

Deterministic identifier computed as:

NftId = BLAKE3("NFT:" || collection_id || creator || mint_nonce)

Properties:

  • Globally unique
  • Deterministic (same inputs always produce same ID)
  • Collision-resistant

CollectionId (32 bytes)

Deterministic identifier computed as:

CollectionId = BLAKE3("COLLECTION:" || creator || name)

Properties:

  • Unique per creator + name combination
  • Deterministic
  • Collections are immutable once created

NftMeta

pub struct NftMeta {
    pub id: NftId,
    pub collection: CollectionId,
    pub owner: Address,
    pub name: String,
    pub description: Option<String>,
    pub media_uri: Option<String>,
    pub attributes: HashMap<String, String>,
}

Metadata Strategy:

  • Minimal on-chain: Name, owner, collection reference
  • Off-chain: Detailed metadata, images, attributes via media_uri
  • Hybrid: Optional on-chain attributes for critical properties

Collection

pub struct Collection {
    pub id: CollectionId,
    pub creator: Address,
    pub name: String,
    pub description: Option<String>,
    pub max_supply: Option<u64>,  // None = unlimited
    pub minted_count: u64,
    pub royalty_bps: u16,         // Basis points (100 = 1%)
    pub royalty_recipient: Address,
}

NFT Registry

Global state structure tracking all NFTs:

pub struct NftRegistry {
    /// NFT metadata by ID
    pub nfts: HashMap<NftId, NftMeta>,
    /// NFTs owned by address
    pub owned_by: HashMap<Address, Vec<NftId>>,
    /// Collections registry
    pub collections: HashMap<CollectionId, Collection>,
}

Operations:

  • create_collection(): Register a new collection
  • mint_nft(): Create a new NFT in a collection
  • transfer_nft(): Transfer ownership
  • burn_nft(): Permanently destroy an NFT
  • owner_of(): Query NFT owner
  • nfts_owned_by(): Get all NFTs owned by an address

Move Module

Location: crates/rivellum-move/sources/nft.move

Entry Functions

initialize(account: &signer)

Initialize the NFT module (one-time setup).

create_collection(...)

public entry fun create_collection(
    creator: &signer,
    name: String,
    description: String,
    max_supply: u64,
    royalty_bps: u16,
    royalty_recipient: address,
)

Parameters:

  • max_supply: Set to 0 for unlimited supply
  • royalty_bps: 0-10000 (e.g., 250 = 2.5%)

Events: CollectionCreatedEvent

mint_nft(...)

public entry fun mint_nft(
    minter: &signer,
    collection_id: vector<u8>,
    name: String,
    description: String,
    media_uri: String,
    recipient: address,
)

Checks:

  • Collection exists
  • Supply limit not exceeded
  • Minter is collection creator (current implementation)

Events: NftMintedEvent

transfer_nft(...)

public entry fun transfer_nft(
    from: &signer,
    nft_id: vector<u8>,
    to: address,
)

Checks:

  • NFT exists
  • Sender is current owner

Events: NftTransferredEvent

burn_nft(...)

public entry fun burn_nft(
    owner: &signer,
    nft_id: vector<u8>,
)

Checks:

  • NFT exists
  • Signer is current owner

Events: NftBurnedEvent

View Functions

#[view]
public fun get_nft(owner: address, nft_id: vector<u8>): (String, String, String, vector<u8>)

#[view]
public fun get_collection(collection_id: vector<u8>): (String, String, address, u64, u64)

#[view]
public fun owner_of(nft_id: vector<u8>): address

Payload Types

PlainPayload Variants

CreateCollection {
    name: String,
    description: String,
    max_supply: Option<u64>,
    royalty_bps: u16,
    royalty_recipient: Address,
}

MintNft {
    collection_id: CollectionId,
    name: String,
    description: Option<String>,
    media_uri: Option<String>,
    attributes: HashMap<String, String>,
    recipient: Address,
}

TransferNft {
    nft_id: NftId,
    to: Address,
}

BurnNft {
    nft_id: NftId,
}

Metadata Conventions

Off-Chain Metadata Format

JSON structure pointed to by media_uri:

{
  "name": "NFT Name",
  "description": "Detailed description of the NFT",
  "image": "ipfs://QmXxx.../image.png",
  "external_url": "https://example.com/nft/123",
  "attributes": [
    {
      "trait_type": "Background",
      "value": "Blue"
    },
    {
      "trait_type": "Rarity",
      "value": "Legendary",
      "display_type": "string"
    },
    {
      "trait_type": "Power",
      "value": 95,
      "display_type": "number",
      "max_value": 100
    }
  ],
  "properties": {
    "category": "Art",
    "creators": [
      {
        "address": "0x...",
        "share": 100
      }
    ]
  }
}

Standard Fields:

  • name: Display name
  • description: Long-form description
  • image: Primary asset URL (IPFS/HTTP/HTTPS)
  • external_url: External link
  • attributes: Array of trait objects
  • properties: Extended metadata

Trait Types:

  • string: Text traits
  • number: Numeric traits (with optional max_value)
  • boost_percentage: Percentage boosts
  • boost_number: Additive boosts
  • date: Unix timestamp

Storage Recommendations

IPFS: Recommended for decentralization and permanence

  • Use pinning services (Pinata, NFT.Storage)
  • Include CID in media_uri as ipfs://Qm...

HTTP/HTTPS: Acceptable but centralized

  • Ensure high availability
  • Consider CDN for performance

On-Chain Storage: Use sparingly

  • Only critical attributes that affect on-chain logic
  • Store in attributes field of NftMeta

Indexer Integration

Schema Tables

CREATE TABLE collections (
    id BYTEA PRIMARY KEY,
    creator BYTEA NOT NULL,
    name TEXT NOT NULL,
    description TEXT,
    max_supply BIGINT,
    minted_count BIGINT NOT NULL,
    royalty_bps INTEGER NOT NULL,
    royalty_recipient BYTEA NOT NULL,
    created_at TIMESTAMP NOT NULL
);

CREATE TABLE nft_meta (
    id BYTEA PRIMARY KEY,
    collection_id BYTEA NOT NULL REFERENCES collections(id),
    owner BYTEA NOT NULL,
    name TEXT NOT NULL,
    description TEXT,
    media_uri TEXT,
    attributes JSONB,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

CREATE TABLE nft_transfers (
    id SERIAL PRIMARY KEY,
    nft_id BYTEA NOT NULL,
    from_address BYTEA NOT NULL,
    to_address BYTEA NOT NULL,
    block_height BIGINT NOT NULL,
    timestamp TIMESTAMP NOT NULL,
    transaction_hash BYTEA NOT NULL
);

CREATE INDEX idx_nft_owner ON nft_meta(owner);
CREATE INDEX idx_nft_collection ON nft_meta(collection_id);
CREATE INDEX idx_transfers_nft ON nft_transfers(nft_id);
CREATE INDEX idx_transfers_from ON nft_transfers(from_address);
CREATE INDEX idx_transfers_to ON nft_transfers(to_address);

Event Consumption

Indexer subscribes to:

  • CollectionCreatedEvent: Insert into collections
  • NftMintedEvent: Insert into nft_meta
  • NftTransferredEvent: Update nft_meta.owner, insert into nft_transfers
  • NftBurnedEvent: Delete from nft_meta

RPC Endpoints

Get NFT Metadata

GET /nfts/:nft_id

Response:

{
  "id": "0x...",
  "collection_id": "0x...",
  "owner": "0x...",
  "name": "NFT Name",
  "description": "Description",
  "media_uri": "ipfs://...",
  "attributes": {
    "key": "value"
  }
}

Get NFTs Owned by Address

GET /nfts/owned/:address?page=1&limit=50

Response:

{
  "nfts": [...],
  "total": 123,
  "page": 1,
  "limit": 50
}

Get Collection

GET /collections/:collection_id

Get Collection NFTs

GET /collections/:collection_id/nfts?page=1&limit=50

Get NFT Transfer History

GET /nfts/:nft_id/transfers

Security Considerations

Supply Limit Enforcement

  • Checked at mint time
  • Immutable after collection creation
  • Consider migration path for adjusting supply

Royalty Enforcement

  • Stored on-chain
  • Marketplaces should honor royalty_bps
  • Consider automatic royalty distribution in transfer logic

Ownership Verification

  • All operations require signature from owner
  • Use multi-sig for high-value NFTs (see PQ Accounts)

Metadata Integrity

  • Off-chain metadata can be modified
  • Consider content-addressed storage (IPFS)
  • Store critical attributes on-chain

SDK Integration

TypeScript SDK example:

import { RivellumClient } from '@rivellum/sdk';

const client = new RivellumClient('https://rpc.rivellum.network');

// Create collection
const collectionTx = await client.createCollection({
  name: 'My Collection',
  description: 'A collection of unique NFTs',
  maxSupply: 10000,
  royaltyBps: 250, // 2.5%
  royaltyRecipient: '0x...',
});

// Mint NFT
const mintTx = await client.mintNft({
  collectionId: '0x...',
  name: 'NFT #1',
  description: 'The first NFT',
  mediaUri: 'ipfs://Qm...',
  attributes: {
    rarity: 'legendary',
    power: '95',
  },
  recipient: '0x...',
});

// Transfer NFT
const transferTx = await client.transferNft({
  nftId: '0x...',
  to: '0x...',
});

// Query NFTs
const owned = await client.getNftsOwnedBy('0x...');
const nft = await client.getNftMeta('0x...');

Examples

Creating a Simple Collection

use rivellum_types::{CollectionId, Address, PlainPayload};

let creator = Address([1u8; 32]);
let payload = PlainPayload::CreateCollection {
    name: "Rivellum Heroes".to_string(),
    description: "A collection of legendary heroes".to_string(),
    max_supply: Some(1000),
    royalty_bps: 250, // 2.5%
    royalty_recipient: creator,
};

Minting an NFT

use rivellum_types::{NftId, CollectionId, PlainPayload};

let collection_id = CollectionId::new(&creator, "Rivellum Heroes");
let payload = PlainPayload::MintNft {
    collection_id,
    name: "Hero #1".to_string(),
    description: Some("The first legendary hero".to_string()),
    media_uri: Some("ipfs://QmXxx.../hero1.json".to_string()),
    attributes: {
        let mut attrs = HashMap::new();
        attrs.insert("class".to_string(), "warrior".to_string());
        attrs.insert("level".to_string(), "50".to_string());
        attrs
    },
    recipient: owner_address,
};

Future Enhancements

Composable NFTs

  • NFTs that own other NFTs
  • Nesting and parent-child relationships

Dynamic NFTs

  • On-chain state updates
  • Game-linked metadata

Fractional Ownership

  • Split NFT ownership among multiple addresses
  • Voting mechanisms for shared NFTs

Soulbound Tokens

  • Non-transferable NFTs
  • Permanent binding to an address

Batch Operations

  • Batch minting for airdrops
  • Batch transfers

References