RGB in Rust

NB: Work in progress – the chapter is not fully written yet and lack explanation details for the code.
Please contribute at our GitHub.

We will start with showing how a simple RGB20 contract can be written in Rust using pre-existing schema and no advanced functionality. The rest of the chapter will guide you through adding more and more customization to it, which will require creating a custom state data types, writing validation scripts for them, packing them into a new schema – and providing a custom interface to this schema.

Sample project providing the source code from this chapter can be found in examples directory of RGB smart contract compiler repository.

Writing simple contract

Firstly, using cargo to create a new project: cargo new rgb20-token, and enter it: cd rgb20-token. Then create a directory to store the contract file. You may name it whatever you like, here we set it to examples: mkdir examples. See source codes at RGB20 Contract.

Here is the complete project file tree:

➜  rgb20-token git:(main) ✗ tree .
.
├── Cargo.lock
├── Cargo.toml
├── contracts
└── src
    └── main.rs

3 directories, 3 files

Secondly, we add some libs in Cargo.toml file:

[package]
name = "rgb20-token"
version = "0.1.0"
edition = "2021"
resolver = "2"

[dependencies]
amplify = "4.7.0"
bp-core = "0.11.0"
rgb-std = { version = "0.11.0", features = ["serde", "fs"] }
rgb-schemata = "0.11.0"
serde = "1.0"
serde_json = "1.0"

[features]
all = []

Then, in src/main.rs, write following codes:

use amplify::hex::FromHex;
use bp::dbc::Method;
use bp::{Outpoint, Txid};
use ifaces::Rgb20;
use rgbstd::containers::{ConsignmentExt, FileContent};
use rgbstd::interface::{FilterIncludeAll, FungibleAllocation};
use rgbstd::invoice::Precision;
use rgbstd::persistence::Stock;
use rgbstd::XWitnessId;
use schemata::dumb::NoResolver;
use schemata::NonInflatableAsset;

fn main() {
    let beneficiary_txid = 
        Txid::from_hex("311ec7d43f0f33cda5a0c515a737b5e0bbce3896e6eb32e67db0e868a58f4150").unwrap();
    let beneficiary = Outpoint::new(beneficiary_txid, 1);

    let contract = NonInflatableAsset::testnet(
        "ssi:anonymous",
        "TEST",
        "Test asset",
        None,
        Precision::CentiMicro,
        [(Method::TapretFirst, beneficiary, 1_000_000_000_00u64)]
    )
    .expect("invalid contract data");

    let contract_id = contract.contract_id();

    eprintln!("{contract}");
    contract.save_file("examples/rgb20-simplest.rgb").expect("unable to save contract");
    contract.save_armored("examples/rgb20-simplest.rgba").expect("unable to save armored contract");

    // Let's create some stock - an in-memory stash and inventory around it:
    let mut stock = Stock::in_memory();
    stock.import_contract(contract, NoResolver).unwrap();

    // Reading contract state through the interface from the stock:
    let contract = stock.contract_iface_class::<Rgb20>(contract_id).unwrap();
    let allocations = contract.allocations(&FilterIncludeAll);
    eprintln!("\nThe issued contract data:");
    eprintln!("{}", serde_json::to_string(&contract.spec()).unwrap());

    for FungibleAllocation  { seal, state, witness, .. } in allocations {
        let witness = witness.as_ref().map(XWitnessId::to_string).unwrap_or("~".to_owned());
        eprintln!("amount={state}, owner={seal}, witness={witness}");
    }
    eprintln!("totalSupply={}", contract.total_supply());
}

Save it, and execute cargo run, after that, contract file would save in examples directory. You can specify your own token name, decimal, description, beneficiary and supply by modifying corresponding variable's value, as well as contracts saving fold.

Now, you can import the contract with rgb import command: rgb import examples/rgb20-simplest.rgb.

Creating custom state

Custom state can be added to a contract by defining a data type which will hold it. Data type is a rust structure or enum, which implements strict_encoding traits. The simplest way to add this implementation is through derive macros:

#[derive(Clone, Eq, PartialEq, Debug)]
#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub struct Nominal {
    ticker: Ticker,
    name: ContractName,
    details: Option<ContractDetails>,
    precision: Precision,
}
impl StrictSerialize for Nominal {}
impl StrictDeserialize for Nominal {}

Once declared, the type can be compiled into a type library:

let lib = LibBuilder::new(libname!(LIB_NAME_RGB_CONTRACT))
    .process::<Nominal>()?
    .compile(none!())?;
let types = SystemBuilder::new()
    .import(lib)?
    .finalize()?;

Scripting

The following fragment provides an example of how a script for RGB contract validation may look like, written in RGB assembly (a special version of AluVM assembly) right insite a rust program, which can compile it into a binary form for the use in a schema.

    let code = rgbasm! {
        // SUBROUTINE 2: Transfer validation
        // Put 0 to a16[0]
        put     a16[0],0;
        // Read previous state into s16[0]
        ldp     OS_ASSET,a16[0],s16[0];
        // jump into SUBROUTINE 3 to reuse the code
        jmp     FN_SHARED_OFFSET;

        // SUBROUTINE 1: Genesis validation
        // Set offset to read state from strings
        put     a16[0],0x00;
        // Set which state index to read
        put     a8[1],0x00;
        // Read global state into s16[0]
        ldg     GS_TOKENS,a8[1],s16[0];

        // SUBROUTINE 3: Shared code
        // Set errno
        put     a8[0],ERRNO_NON_EQUAL_IN_OUT;
        // Extract 128 bits from the beginning of s16[0] into a32[0]
        extr    s16[0],a32[0],a16[0];
        // Set which state index to read
        put     a16[1],0x00;
        // Read owned state into s16[1]
        lds     OS_ASSET,a16[1],s16[1];
        // Extract 128 bits from the beginning of s16[1] into a32[1]
        extr    s16[1],a32[1],a16[0];
        // Check that token indexes match
        eq.n    a32[0],a32[1];
        // Fail if they don't
        test;

        // Set errno
        put     a8[0],ERRNO_NON_FRACTIONAL;
        // Put offset for the data into a16[2]
        put     a16[2],4;
        // Extract 128 bits starting from the fifth byte of s16[1] into a64[0]
        extr    s16[1],a64[0],a16[2];
        // Check that owned fraction == 1
        put     a64[1],1;
        eq.n    a64[0],a64[1];
        // Fail if not
        test;
    };
    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code).expect("wrong unique digital asset script")

Creating custom schema

fn schema() -> Schema {
    Schema {
        ffv: zero!(),
        subset_of: None,
        type_system: types.type_system(),
        global_types: tiny_bmap! {
            GS_NOMINAL => GlobalStateSchema::once(types.get("RGBContract.Nominal")),
            GS_CONTRACT => GlobalStateSchema::once(types.get("RGBContract.ContractText")),
        },
        owned_types: tiny_bmap! {
            OS_ASSETS => StateSchema::Fungible(FungibleType::Unsigned64Bit),
        },
        valency_types: none!(),
        genesis: GenesisSchema {
            metadata: Ty::<SemId>::UNIT.id(None),
            globals: tiny_bmap! {
                GS_NOMINAL => Occurrences::Once,
                GS_CONTRACT => Occurrences::Once,
            },
            assignments: tiny_bmap! {
                OS_ASSETS => Occurrences::OnceOrMore,
            },
            valencies: none!(),
        },
        extensions: none!(),
        transitions: tiny_bmap! {
            TS_TRANSFER => TransitionSchema {
                metadata: Ty::<SemId>::UNIT.id(None),
                globals: none!(),
                inputs: tiny_bmap! {
                    OS_ASSETS => Occurrences::OnceOrMore
                },
                assignments: tiny_bmap! {
                    OS_ASSETS => Occurrences::OnceOrMore
                },
                valencies: none!(),
            }
        },
        script: Script::AluVM(AluScript {
            libs: confined_bmap! { alu_id => alu_lib },
            entry_points: confined_bmap! {
                EntryPoint::ValidateOwnedState(OS_ASSETS) => LibSite::with(0, alu_id)
            },
        }),
    }
}

Doing custom interface

pub fn iface() -> Iface {
    let types = StandardTypes::new();

    Iface {
        name: tn!("RGB20"),
        global_state: tiny_bmap! {
            tn!("Nominal") => Req::require(types.get("RGBContract.Nominal")),
            tn!("ContractText") => Req::require(types.get("RGBContract.ContractText")),
        },
        assignments: tiny_bmap! {
            tn!("Assets") => AssignIface::private(OwnedIface::Amount),
        },
        valencies: none!(),
        genesis: GenesisIface {
            metadata: None,
            global: tiny_bmap! {
                tn!("Nominal") => Occurrences::Once,
                tn!("ContractText") => Occurrences::Once,
            },
            assignments: tiny_bmap! {
                tn!("Assets") => Occurrences::OnceOrMore
            },
            valencies: none!(),
        },
        transitions: tiny_bmap! {
            tn!("Transfer") => TransitionIface {
                metadata: None,
                globals: none!(),
                inputs: tiny_bmap! {
                    tn!("Assets") => Occurrences::OnceOrMore,
                },
                assignments: tiny_bmap! {
                    tn!("Assets") => Occurrences::OnceOrMore,
                },
                valencies: none!(),
                default_assignment: Some(tn!("Assets")),
            }
        },
        extensions: none!(),
        default_operation: Some(tn!("Transfer")),
    }
}

Implementing interface


fn iface_impl() -> IfaceImpl {
    let schema = schema();
    let iface = iface();

    IfaceImpl {
        schema_id: schema.schema_id(),
        iface_id: iface.iface_id(),
        global_state: tiny_bset! {
            NamedType::with(GS_NOMINAL, tn!("Nominal")),
            NamedType::with(GS_CONTRACT, tn!("ContractText")),
        },
        assignments: tiny_bset! {
            NamedType::with(OS_ASSETS, tn!("Assets")),
        },
        valencies: none!(),
        transitions: tiny_bset! {
            NamedType::with(TS_TRANSFER, tn!("Transfer")),
        },
        extensions: none!(),
    }
}