Skip to main content

RGB Interfaces

Standard interfaces for RGB protocol integration and interoperability. RGB interfaces provide a uniform API across different schema implementations, enabling wallets and applications to work with contracts in a standardized way.

Interface Architecture​

What are RGB Interfaces?​

RGB interfaces define standard methods and state structures that contracts must implement. They act as an abstraction layer between:

  • Schemas: Low-level contract logic and validation
  • Applications: High-level contract interaction
// Interface provides standard API
let rgb20: Rgb20 = stock.contract_iface_class::<Rgb20>(contract_id)?;

// Works with any schema that implements RGB20
let ticker = rgb20.spec().ticker; // Standardized access

Interface Implementation​

Schemas map their internal state to interface requirements:

pub struct IfaceImpl {
pub version: VerNo,
pub schema_id: SchemaId,
pub iface_id: IfaceId,
pub timestamp: i64,
pub developer: Identity,
pub metadata: Confined<BTreeMap<MetaKey, MetaValue>>,
pub global_state: Confined<BTreeSet<NamedField>>,
pub assignments: Confined<BTreeSet<NamedField>>,
pub valencies: Confined<BTreeSet<NamedField>>,
pub transitions: Confined<BTreeSet<NamedField>>,
pub extensions: Confined<BTreeSet<NamedField>>,
pub errors: Confined<BTreeSet<NamedVariant>>,
}

RGB20: Fungible Assets​

Overview​

RGB20 is the standard interface for fungible tokens (like ERC-20 on Ethereum). It supports:

  • Fixed or flexible supply
  • Fractional ownership (precision up to 18 decimals)
  • Transfer operations
  • Optional burn/reissue capabilities

Interface Specification​

interface IRGB20 {
// Asset Specification
spec(): AssetSpec;

// Supply Queries
totalSupply(): Amount;
issuedSupply(): Amount;
burnedSupply(): Amount;

// Ownership Queries
allocations(filter: AllocationFilter): Iterator<FungibleAllocation>;
balance(filter: OutpointFilter): Amount;

// Operations (via state transitions)
// - transfer: Move tokens between owners
// - burn: Destroy tokens (if enabled)
// - issue: Mint new tokens (if enabled)
}

Asset Specification​

pub struct AssetSpec {
pub ticker: Ticker, // 1-8 uppercase chars
pub name: Name, // Human-readable name
pub details: Option<Details>, // Optional description
pub precision: Precision, // Decimal places (0-18)
}

Example:

let spec = rgb20.spec();
println!("Ticker: {}", spec.ticker); // "USDT"
println!("Name: {}", spec.name); // "Tether USD"
println!("Precision: {}", spec.precision); // 2 decimals

Supply Information​

// Total issued supply (including burned)
let total = rgb20.total_supply();

// Currently issued (not burned)
let issued = rgb20.issued_supply();

// Burned tokens
let burned = rgb20.burned_supply();

// Circulating = issued - burned
let circulating = issued - burned;

Allocations​

use rgbstd::interface::{FilterIncludeAll, FungibleAllocation};

// Get all allocations
for FungibleAllocation {
seal, // Output seal
state, // Amount
witness, // Transaction witness
..
} in rgb20.allocations(&FilterIncludeAll) {
println!("Amount: {}, Owner: {}", state, seal);
}

Schema Implementation: NonInflatableAsset (NIA)​

Fixed-supply RGB20 implementation:

pub struct NonInflatableAsset;

impl IssuerWrapper for NonInflatableAsset {
const FEATURES: Rgb20 = Rgb20::FIXED;
type IssuingIface = Rgb20;

fn schema() -> Schema {
Schema {
name: tn!("NonInflatableAsset"),
global_types: tiny_bmap! {
GS_NOMINAL => GlobalStateSchema::once("AssetSpec"),
GS_TERMS => GlobalStateSchema::once("ContractTerms"),
GS_ISSUED_SUPPLY => GlobalStateSchema::once("Amount"),
},
owned_types: tiny_bmap! {
OS_ASSET => OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
},
transitions: tiny_bmap! {
TS_TRANSFER => TransitionSchema { /* ... */ }
},
// ...
}
}
}

Interface Mapping:

fn nia_rgb20() -> IfaceImpl {
IfaceImpl {
schema_id: nia_schema().schema_id(),
iface_id: Rgb20::FIXED.iface_id(),
global_state: tiny_bset! {
NamedField::with(GS_NOMINAL, fname!("spec")),
NamedField::with(GS_TERMS, fname!("terms")),
NamedField::with(GS_ISSUED_SUPPLY, fname!("issuedSupply")),
},
assignments: tiny_bset! {
NamedField::with(OS_ASSET, fname!("assetOwner")),
},
transitions: tiny_bset! {
NamedField::with(TS_TRANSFER, fname!("transfer")),
},
errors: tiny_bset![
NamedVariant::with(ERRNO_ISSUED_MISMATCH, vname!("issuedMismatch")),
NamedVariant::with(ERRNO_NON_EQUAL_IN_OUT, vname!("nonEqualAmounts")),
],
}
}

Validation Logic​

NIA uses AluVM for validation:

pub fn nia_lib() -> Lib {
let code = rgbasm! {
// Transfer validation
put a8[0],ERRNO_NON_EQUAL_IN_OUT;
pcvs OS_ASSET; // Pedersen commitment verification
test;
ret;

// Genesis validation
put a8[0],ERRNO_ISSUED_MISMATCH;
put a8[1],0;
put a16[0],0;
ldg GS_ISSUED_SUPPLY,a8[1],s16[0]; // Load global state
extr s16[0],a64[0],a16[0]; // Extract amount
pcas OS_ASSET; // Verify commitments vs. issued supply
test;
ret;
};
Lib::assemble(&code).expect("invalid NIA script")
}

Validation Rules:

  • Genesis: Sum of output amounts must equal declared issued supply
  • Transfer: Sum of input amounts must equal sum of output amounts
  • Pedersen Commitments: Cryptographically verify amounts without revealing them

RGB21: Non-Fungible Tokens​

Overview​

RGB21 is the standard interface for unique digital assets (NFTs). It supports:

  • Unique token identifiers
  • Rich metadata (embedded or external)
  • Media attachments
  • Fractionalization (optional)

Interface Specification​

interface IRGB21 {
// Collection Specification
spec(): AssetSpec;

// Token Queries
tokens(filter: AllocationFilter): Iterator<Allocation>;
tokenData(index: TokenIndex): TokenData;

// Ownership
balance(filter: OutpointFilter): Amount;
allocations(filter: AllocationFilter): Iterator<Allocation>;
}

Token Data​

pub struct TokenData {
pub index: TokenIndex, // Unique token ID
pub preview: Option<EmbeddedMedia>, // Preview image/media
pub media: Option<Attachment>, // Full media attachment
pub attachments: Confined<BTreeMap<u8, Attachment>>, // Additional files
pub reserves: Option<ProofOfReserves>, // Proof of reserves
}

Example:

let token_data = TokenData {
index: TokenIndex::from_inner(1),
preview: Some(EmbeddedMedia {
ty: MediaType::with("image/png"),
data: SmallBlob::try_from_iter(image_bytes)?,
}),
media: Some(Attachment {
ty: MediaType::with("image/jpeg"),
digest: Bytes::from_byte_array(sha256_hash),
}),
attachments: none!(),
reserves: None,
};

Embedded Media​

Small media can be embedded directly in contract:

pub struct EmbeddedMedia {
pub ty: MediaType, // MIME type
pub data: SmallBlob, // Up to 64KB
}

// Example
let preview = EmbeddedMedia {
ty: MediaType::with("image/png"),
data: SmallBlob::try_from_iter(thumbnail_bytes)?,
};

External Attachments​

Large media referenced via content hash:

pub struct Attachment {
pub ty: MediaType, // MIME type
pub digest: Bytes, // Content hash (SHA-256)
}

// Link to external media
let attachment = Attachment {
ty: MediaType::with("video/mp4"),
digest: Bytes::from_byte_array(file_hash),
};

Schema Implementation: UniqueDigitalAsset (UDA)​

pub struct UniqueDigitalAsset;

impl IssuerWrapper for UniqueDigitalAsset {
type IssuingIface = Rgb21;
const FEATURES: Rgb21 = Rgb21::NONE;

fn schema() -> Schema {
Schema {
name: tn!("UniqueDigitalAsset"),
global_types: tiny_bmap! {
GS_TOKENS => GlobalStateSchema::once("Rgb21.TokenData"),
GS_NOMINAL => GlobalStateSchema::once("AssetSpec"),
GS_TERMS => GlobalStateSchema::once("ContractTerms"),
},
owned_types: tiny_bmap! {
OS_ASSET => OwnedStateSchema::Structured("Rgb21.Allocation"),
},
transitions: tiny_bmap! {
TS_TRANSFER => TransitionSchema { /* ... */ }
},
// ...
}
}
}

Creating RGB21 Tokens​

use schemata::UniqueDigitalAsset;
use ifaces::Rgb21;

// Define asset spec
let spec = AssetSpec::new("UNIQ", "Unique Art", Precision::Indivisible);

// Define token data
let token_data = TokenData {
index: TokenIndex::from_inner(1),
preview: Some(preview_media),
media: Some(media_attachment),
..Default::default()
};

// Create contract
let contract = stock.contract_builder(
"ssi:artist",
UniqueDigitalAsset::schema().schema_id(),
"RGB21",
)?
.add_global_state("spec", spec)?
.add_global_state("tokens", token_data)?
.add_data("assetOwner", seal, Allocation::with(index, 1))?
.issue_contract()?;

// Query via RGB21 interface
let rgb21 = stock.contract_iface_class::<Rgb21>(contract_id)?;
let tokens: Vec<_> = rgb21.tokens(&FilterIncludeAll).collect();

RGB25: Collectible Fungible Assets​

Overview​

RGB25 combines aspects of RGB20 and RGB21, providing:

  • Fungible fractional ownership
  • Rich metadata (like NFTs)
  • Collection management
  • Proof of reserves

Use cases: Fractionalized NFTs, tokenized real-world assets, game items

Interface Specification​

interface IRGB25 {
// Collection Info
name(): Name;
details(): Option<Details>;
precision(): Precision;

// Asset Specification
art(): Option<Attachment>; // Collection artwork

// Supply & Ownership (like RGB20)
totalSupply(): Amount;
allocations(filter: AllocationFilter): Iterator<FungibleAllocation>;
balance(filter: OutpointFilter): Amount;
}

Schema Implementation: CollectibleFungibleAsset (CFA)​

pub struct CollectibleFungibleAsset;

impl IssuerWrapper for CollectibleFungibleAsset {
type IssuingIface = Rgb25;
const FEATURES: Rgb25 = Rgb25::NONE;

fn schema() -> Schema {
Schema {
name: tn!("CollectibleFungibleAsset"),
global_types: tiny_bmap! {
GS_ART => GlobalStateSchema::once("Article"),
GS_NAME => GlobalStateSchema::once("Name"),
GS_DETAILS => GlobalStateSchema::once("Details"),
GS_PRECISION => GlobalStateSchema::once("Precision"),
GS_TERMS => GlobalStateSchema::once("ContractTerms"),
GS_ISSUED_SUPPLY => GlobalStateSchema::once("Amount"),
},
owned_types: tiny_bmap! {
OS_ASSET => OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
},
// Uses same validation as NIA
genesis: GenesisSchema {
validator: Some(LibSite::with(FN_NIA_GENESIS_OFFSET, nia_lib_id)),
// ...
},
transitions: tiny_bmap! {
TS_TRANSFER => TransitionSchema {
validator: Some(LibSite::with(FN_NIA_TRANSFER_OFFSET, nia_lib_id)),
// ...
}
},
}
}
}

Creating RGB25 Assets​

// Example: Fractionalized artwork
let contract = stock.contract_builder(
"ssi:gallery",
CollectibleFungibleAsset::schema().schema_id(),
"RGB25",
)?
.add_global_state("name", Name::from("Mona Lisa Fraction"))?
.add_global_state("details", Details::from("1/1000 ownership"))?
.add_global_state("precision", Precision::Centi)? // 2 decimals
.add_global_state("art", artwork_attachment)?
.add_fungible_state("assetOwner", seal, 1000_00)? // 1000 fractions
.issue_contract()?;

Interface Compliance​

Checking Implementation​

Verify that a schema correctly implements an interface:

let iface = Rgb20::FIXED.iface();
let schema = NonInflatableAsset::schema();
let iface_impl = NonInflatableAsset::issue_impl();

if let Err(errors) = iface_impl.check(&iface, &schema) {
for error in errors {
eprintln!("Compliance error: {}", error);
}
panic!("Invalid interface implementation");
}

Runtime Interface Detection​

// Check if contract implements RGB20
fn implements_rgb20(contract: &Contract) -> bool {
contract.iface_id() == Rgb20::FIXED.iface_id()
}

// Query via interface
if implements_rgb20(&contract) {
let rgb20 = stock.contract_iface_class::<Rgb20>(contract_id)?;
println!("Token: {}", rgb20.spec().ticker);
}

Custom Interfaces​

Defining Custom Interfaces​

For specialized use cases, define custom interfaces:

use rgbstd::interface::{Iface, IfaceOp, OpName, GlobalStateName, AssignmentName};

fn stablecoin_iface() -> Iface {
Iface {
name: tn!("Stablecoin"),
timestamp: 1713343888,
developer: Identity::from("ssi:developer"),
global_state: tiny_bset! {
GlobalStateName::from("spec"),
GlobalStateName::from("collateralRatio"),
GlobalStateName::from("oraclePrice"),
},
assignments: tiny_bset! {
AssignmentName::from("assetOwner"),
},
transitions: tiny_bset! {
IfaceOp {
name: OpName::from("mint"),
global: tiny_bset![GlobalStateName::from("collateral")],
assignments: tiny_bset![AssignmentName::from("assetOwner")],
// ...
},
IfaceOp {
name: OpName::from("redeem"),
// ...
},
},
// ...
}
}

Implementing Custom Interface​

fn stablecoin_impl() -> IfaceImpl {
let schema = stablecoin_schema();

IfaceImpl {
schema_id: schema.schema_id(),
iface_id: stablecoin_iface().iface_id(),
global_state: tiny_bset! {
NamedField::with(0, fname!("spec")),
NamedField::with(1, fname!("collateralRatio")),
NamedField::with(2, fname!("oraclePrice")),
},
transitions: tiny_bset! {
NamedField::with(0, fname!("mint")),
NamedField::with(1, fname!("redeem")),
},
// ...
}
}

Interface Versioning​

Version Compatibility​

pub enum VerNo {
V1, // Version 1
V2, // Version 2
// Future versions
}

// Interface implementations specify version
impl IfaceImpl {
pub version: VerNo = VerNo::V1;
}

Backward Compatibility​

// Check interface version
match iface_impl.version {
VerNo::V1 => {
// Handle V1 interface
}
VerNo::V2 => {
// Handle V2 interface (backward compatible)
}
}

State Filters​

Allocation Filters​

Query specific subsets of contract state:

use rgbstd::interface::{AllocationFilter, OutpointFilter};

// Include all allocations
let filter = FilterIncludeAll;
let all: Vec<_> = rgb20.allocations(&filter).collect();

// Filter by outpoint
let outpoint_filter = OutpointFilter::with(specific_outpoint);
let balance = rgb20.balance(&outpoint_filter);

// Custom filters
pub trait AllocationFilter {
fn include_outpoint(&self, outpoint: &Outpoint) -> bool;
fn include_witness(&self, witness: &XWitnessId) -> bool;
}

State Access​

// Global state (immutable)
for (name, state) in contract.state().immutable {
println!("Global {}: {:?}", name, state);
}

// Owned state (mutable)
for (name, allocations) in contract.state().owned {
for alloc in allocations {
println!("Owned {}: {:?}", name, alloc);
}
}

// Aggregated state (computed)
for (name, value) in contract.state().aggregated {
println!("Aggregated {}: {:?}", name, value);
}

Error Variants​

Interface-Defined Errors​

Interfaces can define standard error codes:

// RGB20 errors
pub const ERRNO_ISSUED_MISMATCH: u8 = 1; // Issued supply mismatch
pub const ERRNO_NON_EQUAL_IN_OUT: u8 = 2; // Input != Output amounts

// Map to named variants
errors: tiny_bset![
NamedVariant::with(ERRNO_ISSUED_MISMATCH, vname!("issuedMismatch")),
NamedVariant::with(ERRNO_NON_EQUAL_IN_OUT, vname!("nonEqualAmounts")),
]

Handling Errors​

use rgb::validation::ValidationStatus;

match contract.validate_operation(opid) {
ValidationStatus::Valid => {
println!("Operation valid");
}
ValidationStatus::Invalid { error_code } => {
match error_code {
ERRNO_ISSUED_MISMATCH => {
eprintln!("Issued supply doesn't match allocations");
}
ERRNO_NON_EQUAL_IN_OUT => {
eprintln!("Transfer amounts don't balance");
}
_ => {
eprintln!("Unknown error: {}", error_code);
}
}
}
}

Best Practices​

Interface Design​

Do:

  • Keep interfaces focused and minimal
  • Use clear, descriptive names
  • Version interfaces for evolution
  • Document all methods and state

Don't:

  • Mix unrelated functionality
  • Change interface semantics in new versions
  • Break backward compatibility

Schema Implementation​

// Good: Clear mapping between schema and interface
fn clear_mapping() -> IfaceImpl {
IfaceImpl {
global_state: tiny_bset! {
NamedField::with(SCHEMA_TICKER, fname!("ticker")),
NamedField::with(SCHEMA_NAME, fname!("name")),
},
// Clear 1:1 mapping
}
}

// Bad: Confusing or arbitrary mappings
fn confusing_mapping() -> IfaceImpl {
IfaceImpl {
global_state: tiny_bset! {
NamedField::with(17, fname!("x")), // What is 17? What is x?
},
}
}

Performance Considerations​

// Cache interface queries
let rgb20 = stock.contract_iface_class::<Rgb20>(contract_id)?;
let spec = rgb20.spec(); // Query once
println!("Ticker: {}", spec.ticker);
println!("Name: {}", spec.name);

// Avoid repeated interface instantiation
for i in 0..1000 {
// Bad: Creates interface 1000 times
let rgb20 = stock.contract_iface_class::<Rgb20>(contract_id)?;
}

// Good: Reuse interface
let rgb20 = stock.contract_iface_class::<Rgb20>(contract_id)?;
for i in 0..1000 {
// Use rgb20
}

Testing Interfaces​

Interface Compliance Tests​

#[test]
fn test_rgb20_compliance() {
let iface = NonInflatableAsset::FEATURES.iface();
let schema = NonInflatableAsset::schema();
let impl = NonInflatableAsset::issue_impl();

// Should not have errors
assert!(impl.check(&iface, &schema).is_ok());
}

Interface Behavior Tests​

#[test]
fn test_rgb20_behavior() {
let contract = create_test_rgb20_contract();
let mut stock = Stock::in_memory();
stock.import_contract(contract, NoResolver).unwrap();

let rgb20 = stock.contract_iface_class::<Rgb20>(contract_id).unwrap();

// Test standard behavior
assert!(rgb20.total_supply() > 0);
assert_eq!(rgb20.spec().ticker.to_string(), "TEST");

let allocations: Vec<_> = rgb20.allocations(&FilterIncludeAll).collect();
assert!(allocations.len() > 0);
}

Future Interfaces​

Planned Standards​

  • RGB22: Identity & Authentication
  • RGB23: Audit & Compliance Tokens
  • RGB24: Decentralized Name Service
  • RGB30: Decentralized Identities
  • RGB40: Payment Channels

Extending Existing Interfaces​

New versions can add optional features:

// RGB20 V2 (future)
interface IRGB20_V2 extends IRGB20 {
// V1 methods remain
spec(): AssetSpec;
totalSupply(): Amount;

// V2 additions
metadata(): ContractMetadata; // New
proofOfReserves(): Option<Proof>; // New
}

Source Code:

  • RGB20 (NIA): /tmp/rgb-repos/rgb-schemata/src/nia.rs
  • RGB21 (UDA): /tmp/rgb-repos/rgb-schemata/src/uda.rs
  • RGB25 (CFA): /tmp/rgb-repos/rgb-schemata/src/cfa.rs