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