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.6.0"
ascii-armor = "0.2.0"
strict_encoding = "2.7.0-beta.1"
strict_types = "2.7.0-beta.1"
aluvm = { version = "0.11.0-beta.4", features = ["log"] }
bp-core = "0.11.0-beta.4"
rgb-std = { version = "0.11.0-beta.4", features = ["serde", "fs"] }
serde = "1.0"
serde_json = "1.0"
sha2 = "0.10.8"
rgb-schemata = "0.11.0-beta.4"
hex = "0.4.3"

[dev-dependencies]
chrono = "0.4.31"
serde_yaml = "0.9.27"

[features]
all = []

[patch.crates-io]
commit_verify = { git = "https://github.com/LNP-BP/client_side_validation", branch = "master" }
bp-consensus = { git = "https://github.com/BP-WG/bp-core", branch = "master" }
bp-dbc = { git = "https://github.com/BP-WG/bp-core", branch = "master" }
bp-seals = { git = "https://github.com/BP-WG/bp-core", branch = "master" }
bp-core = { git = "https://github.com/BP-WG/bp-core", branch = "master" }
rgb-core = { git = "https://github.com/RGB-WG/rgb-core", branch = "master" }
rgb-std = { git = "https://github.com/RGB-WG/rgb-std", branch = "master" }
rgb-schemata = { git = "https://github.com/RGB-WG/rgb-schemata", branch = "master" }

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

use std::convert::Infallible;
use std::fs;

use amplify::hex::FromHex;
use armor::AsciiArmor;
use bp::dbc::Method;
use bp::{Outpoint, Txid};
use rgb_schemata::NonInflatableAsset;
use rgbstd::containers::FileContent;
use rgbstd::interface::{FilterIncludeAll, FungibleAllocation, IfaceClass, IssuerClass, Rgb20};
use rgbstd::invoice::Precision;
use rgbstd::persistence::{Inventory, Stock};
use rgbstd::resolvers::ResolveHeight;
use rgbstd::validation::{ResolveWitness, WitnessResolverError};
use rgbstd::{WitnessAnchor, WitnessId, XAnchor, XPubWitness};
use strict_encoding::StrictDumb;

struct DumbResolver;

impl ResolveWitness for DumbResolver {
    fn resolve_pub_witness(&self, _: WitnessId) -> Result<XPubWitness, WitnessResolverError> {
        Ok(XPubWitness::strict_dumb())
    }
}

impl ResolveHeight for DumbResolver {
    type Error = Infallible;
    fn resolve_anchor(&mut self, _: &XAnchor) -> Result<WitnessAnchor, Self::Error> {
        Ok(WitnessAnchor::strict_dumb())
    }
}

#[rustfmt::skip]
fn main() {
    let beneficiary_txid =
        Txid::from_hex("d6afd1233f2c3a7228ae2f07d64b2091db0d66f2e8ef169cf01217617f51b8fb").unwrap();
    let beneficiary = Outpoint::new(beneficiary_txid, 1);

    let contract = NonInflatableAsset::testnet("TEST", "Test asset", None, Precision::CentiMicro)
        .expect("invalid contract data")
        .allocate(Method::TapretFirst, beneficiary, 100_000_000_000_u64.into())
        .expect("invalid allocations")
        .issue_contract()
        .expect("invalid contract data");

    let contract_id = contract.contract_id();

    eprintln!("{contract}");
    contract.save_file("examples/rgb20-simplest.rgb").expect("unable to save contract");
    fs::write("examples/rgb20-simplest.rgba", contract.to_ascii_armored_string()).expect("unable to save contract");

    // Let's create some stock - an in-memory stash and inventory around it:
    let mut stock = Stock::default();
    stock.import_iface(Rgb20::iface()).unwrap();
    stock.import_schema(NonInflatableAsset::schema()).unwrap();
    stock.import_iface_impl(NonInflatableAsset::issue_impl()).unwrap();

    stock.import_contract(contract, &mut DumbResolver).unwrap();

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

    for FungibleAllocation  { seal, state, witness, .. } in allocations {
        eprintln!("amount={state}, owner={seal}, witness={witness}");
    }
    eprintln!("totalSupply={}", contract.total_supply());
    eprintln!("created={}", contract.created().to_local().unwrap());
}


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 modifing 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

    let code = [RgbIsa::Contract(ContractOp::PcVs(OS_ASSETS))];
    let alu_lib = Lib::assemble(&code).unwrap();
    let alu_id = alu_lib.id();
    let code = aluasm! {
        clr     r1024[5]                        ;
        put     5,a16[8]                        ;
        putif   0xaf67937b5498dc,r256[1]        ;
        putif   13,a8[1]                        ;
        swp     a8[1],a8[2]                     ;
        swp     f256[8],f256[7]                 ;
        dup     a256[1],a256[7]                 ;
        mov     a16[1],a16[2]                   ;
        mov     r256[8],r256[7]                 ;
        cpy     a256[1],a256[7]                 ;
        cnv     f128[4],a128[3]                 ;
        spy     a1024[15],r1024[24]             ;
        gt.u    a8[5],a64[9]                    ;
        lt.s    a8[5],a64[9]                    ;
        gt.e    f64[5],f64[9]                   ;
        lt.r    f64[5],f64[9]                   ;
        gt      r160[5],r256[9]                 ;
        lt      r160[5],r256[9]                 ;
        eq.e    a8[5],a8[9]                    ;
        eq.n    r160[5],r160[9]                 ;
        eq.e    f64[19],f64[29]                 ;
        ifn     a32[32]                         ;
        ifz     r2048[17]                       ;
        inv     st0                             ;
        st.s    a8[1]                           ;
        put     13,a32[12]                      ;
        put     66,a32[13]                      ;
        add.uc  a32[12],a32[13]                 ;
        add.sw  a32[12],a32[13]                 ;
        sub.sc  a32[13],a32[12]                 ;
        mul.uw  a32[12],a32[13]                 ;
        div.cu  a32[12],a32[13]                 ;
        put     2.13,f32[12]                    ;
        put     5.18,f32[13]                    ;
        add.z   f32[12],f32[13]                 ;
        sub.n   f32[13],f32[12]                 ;
        mul.c   f32[12],f32[13]                 ;
        div.f   f32[12],f32[13]                 ;
        rem     a64[8],a8[2]                    ;
        inc     a16[3]                          ;
        add     5,a16[4]                        ;
        dec     a16[8]                          ;
        sub     82,a16[4]                       ;
        neg     a64[16]                         ;
        abs     f128[11]                        ;
        and     a32[5],a32[6],a32[5]            ;
        xor     r128[5],r128[6],r128[5]         ;
        shr.u   a256[12],a16[2]                 ;
        shr.s   a256[12],a16[2]                 ;
        shl     r256[24],a16[22]                ;
        shr     r256[24],a16[22]                ;
        scr     r256[24],a16[22]                ;
        scl     r256[24],a16[22]                ;
        rev     a512[28]                        ;
        ripemd  s16[9],r160[7]                  ;
        sha2    s16[19],r256[2]                 ;
        secpgen r256[1],r512[1]                 ;
        dup     r512[1],r512[22]                ;
        spy     a512[1],r512[22]                ;
        secpmul r256[1],r512[1],r512[2]         ;
        secpadd r512[22],r512[1]                ;
        secpneg r512[1],r512[3]                 ;
        ifz     a16[8]                          ;
        jif     190                             ;
        jmp     6                               ;
        call    56 @ alu1wnhusevxmdphv3dh8ada44k0xw66ahq9nzhkv39z07hmudhp380sq0dtml ;
        ret                                     ;
    };

    let lib = Lib::assemble(&code).expect("invalid program);

Creating custom schema

fn schema() -> SubSchema {
    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 rgb20() -> 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 = rgb20();

    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!(),
    }
}