Progress pill
Programming on RGB

Implementing RGB contracts

RGB programming

Implementing RGB contracts

  • The components of an RGB contract
  • Schema
  • Interface
  • Interfaces standardized by the LNP/BP association
  • Interface Implementation
In this chapter, we'll take a closer look at how an RGB contract is defined and implemented. We'll see what the components of an RGB contract are, what their roles are and how they are constructed.

The components of an RGB contract

So far, we've already discussed the Genesis, which represents the starting point of a contract, and we've seen how it fits in with the logic of a Contract Operation and the state of the protocol. The complete definition of an RGB contract, however, is not limited to the Genesis alone: it involves three complementary components which, together, form the heart of the implementation.
The first component is called the Schema. This is a file describing the fundamental structure and business logic (business logic) of the contract. It specifies the data types used, the validation rules, the operations permitted (e.g. initial token issuance, transfers, special conditions, etc.) - in short, the general framework that dictates how the contract works.
The second component is the Interface. It focuses on how users (and by extension, portfolio software) will interact with this contract. It describes the semantics, i.e. the readable representation of the various fields and actions. So, while the Schema defines how the contract works technically, the Interface defines how to present and expose these functionalities: method names, data display, etc.
The third component is the Interface Implementation, which complements the previous two by acting as a kind of bridge between the Schema and the Interface. In other words, it associates the semantics expressed by the Interface with the underlying rules defined in the Schema. It is this implementation that will manage, for example, the conversion between a parameter entered in the wallet and the binary structure imposed by the protocol, or the compilation of validation rules in machine language.
This modularity is an interesting feature of RGB, as it allows different groups of developers to work separately on these aspects (Schema, Interface, Implementation), as long as they follow the protocol's consensus rules.
To sum up, each contract consists of:
  • Genesis, which is the initial state of the contract (and can be likened to a special transaction defining the first ownership of an asset, a right, or any other parameterizable data);
  • Schema, which describes the contract's business logic (data types, validation rules, etc.);
  • Interface, which provides a semantic layer for both wallets and human users, clarifying the reading and execution of transactions;
  • Implementation interface, which bridges the gap between business logic and presentation, to ensure that contract definition is consistent with the user experience.
It's important to note that for a wallet to manage an RGB asset (be it a fungible token or a right of any kind), it must have all these elements compiled: Schema, Interface, Interface Implementation and Genesis. This is transmitted via a contract consignment, i.e. a data package containing everything needed to validate the client-side contract.
To help clarify these notions, here is a summary table comparing the components of an RGB contract with concepts already known either in object-oriented programming (OOP) or in the Ethereum ecosystem:
RGB Contract ComponentMeaningOOP EquivalentEthereum Equivalent
GenesisInitial state of the contractClass constructorContract constructor
SchemaBusiness logic of the contractClassContract
InterfaceSemantics of the contractInterface (Java) / Trait (Rust) / Protocol (Swift)ERC Standard
Interface ImplementationMapping semantics and logicImpl (Rust) / Implements (Java)Application Binary Interface (ABI)
The left-hand column shows the elements specific to the RGB protocol. The middle column shows the concrete function of each component. Then, in the "OOP equivalent" column, we find the equivalent term in object-oriented programming:
  • The Genesis plays a role similar to that of a Class constructor: this is where the state of the contract is initialized;
  • The Schema is the description of a class, i.e. the definition of its properties, methods and underlying logic;
  • The Interface corresponds to interfaces (Java), traits (Rust) or protocols (Swift): these are the public definitions of functions, events, fields...;
  • The Interface Implementation corresponds to Impl in Rust or Implements in Java, where we specify how the code will actually execute the methods announced in the interface.
In the Ethereum context, the Genesis is closer to the contract constructor, the Schema to the contract definition, the Interface to a standard such as ERC-20 or ERC-721, and the Interface Implementation to the ABI (Application Binary Interface), which specifies the format of interactions with the contract.
The advantage of RGB's modularity also lies in the fact that different stakeholders can write, for example, their own Interface Implementation, as long as they respect the logic of the Schema and the semantics of the Interface. Thus, an issuer could develop a new, more user-friendly front-end (Interface), without modifying the logic of the contract, or conversely, one could extend the Schema to add functionality, and provide a new version of the adapted Interface Implementation, while the old implementations would remain valid for basic functionality.
When we compile a new contract, we generate a Genesis (the first step in issuing or distributing the asset), as well as its components (Schema, Interface, Interface Implementation). After this, the contract is fully operational and can be propagated to wallets and users. This method, where Genesis is combined with these three components, guarantees a high degree of customization (each contract can have its own logic), decentralization (everyone can contribute to a given component), and security (validation remains strictly defined by the protocol, without depending on arbitrary on-chain code as is often the case on other blockchains).
I'd now like to take a closer look at each of these components: the Schema, the Interface and the Interface Implementation.

Schema

In the previous section, we saw that in the RGB ecosystem, a contract is made up of several elements: the Genesis, which establishes the initial state, and several other complementary components. The purpose of the Schema is to declaratively describe all the business logic of the contract, i.e. the data structure, the types used, the permitted operations and their conditions. It is therefore a very important element in making a contract operational on the client side, since each participant (a wallet, for example) must check that the state transitions it receives conform to the logic defined in the Schema.
A Schema can be likened to a "class" in object-oriented programming (OOP). Generally speaking, it serves as a model defining the components of a contract, such as:
  • The different types of Owned States and Assignments;
  • Valencies, i.e. special rights that can be triggered (redeemed) for certain operations;
  • Global State fields, which describe global, public and shared properties of the contract;
  • The Genesis structure (the very first operation that activates the contract);
  • The permitted forms of State Transitions and State Extensions, and how these operations can modify the;
  • Metadata associated with each operation, to store temporary or additional information;
  • Rules that determine how internal contract data can evolve (for example, whether a field is mutable or cumulative);
  • Sequences of operations considered valid: for example, an order of transitions to be respected or a set of logical conditions to be satisfied.
When the issuer of an asset on RGB publishes a contract, it provides the Genesis and Schema associated with it. Users or wallets who wish to interact with the asset retrieve this Schema to understand the logic behind the contract, and to be able to verify later that the transitions they will participate in are legitimate.
The first step, for anyone receiving information about an RGB asset (e.g. a token transfer), is to validate this information against the Schema. This involves using the Schema compilation to:
  • Check that Owned States, Assignments and other elements are correctly defined and that they respect the imposed types (the so-called strict type system);
  • Check that transition rules (validation scripts) are satisfied. These scripts can be run via AluVM, which is present on the client side and is responsible for validating the consistency of business logic (transfer amount, special conditions, etc.).
In practice, Schema is not executable code, as can be seen in blockchains that store on-chain code (EVM on Ethereum). On the contrary, RGB separates business logic (declarative) from executable code on the blockchain (which is limited to cryptographic anchors). Thus, the Schema determines the rules, but the application of these rules takes place outside the blockchain, at each participant's site, according to the Client-side Validation principle.
A Schema must be compiled before it can be used by RGB applications. This compilation produces a binary file (e.g. .rgb) or an encrypted binary file (.rgba). When the wallet imports this file, it knows:
  • What each data type (integers, structures, arrays...) looks like thanks to the strict type system;
  • How Genesis should be structured (to understand asset initialization);
  • The different types of operations (State Transitions, State Extensions) and how they can modify state;
  • The scripting rules (introduced in the Schema) that the AluVM engine will apply to check the validity of operations.
As explained in previous chapters, the strict type system gives us a stable, deterministic encoding format: all variables, whether Owned States, Global States or Valencies, are described precisely (size, lower and upper bounds if necessary, signed or unsigned type, etc.). It is also possible to define nested structures, for example to support complex use cases.
Optionally, the Schema can reference a root SchemaId, which facilitates the reuse of an existing basic structure (a template). In this way, you can evolve a contract or create variations (e.g. a new type of token) from an already proven template. This modularity avoids the need to recreate entire contracts, and encourages the standardization of best practices.
Another important point is that the logic of state evolution (transfers, updates, etc.) is described in the Schema in the form of scripts, rules and conditions. So, if the contract designer wishes to authorize a reissue or impose a burn mechanism (destruction of tokens), he can specify the corresponding scripts for AluVM in the validation part of the Schema.

Difference from programmable on-chain blockchains

Unlike systems like Ethereum, where the smart contract code (executable) is written into the blockchain itself, RGB stores the contract (its logic) off-chain, in the form of a compiled declarative document. This implies that:
  • There is no Turing-complete VM running in every node of the Bitcoin network. The rules of an RGB contract are not executed on the blockchain, but in each user who wishes to validate a state;
  • Contract data does not pollute the blockchain: only cryptographic evidence (commitments) is embedded in Bitcoin transactions (via Tapret or Opret);
  • The Schema can be updated or declined (fast-forward, push-back, etc.), without requiring a fork on the Bitcoin blockchain. Wallets simply need to import the new Schema and adapt to consensus changes.

Use by the issuer and by users

When a issuer creates an asset (for example, a non-inflationary fungible token), it prepares:
  • A Schema describing the rules of emission, transfer, etc.;
  • A Genesis adapted to this Schema (with the total number of tokens issued, the identity of the initial owner, any special Valencies for reissue, etc.).
It then makes the compiled Schema (a .rgb file) available to users, so that anyone receiving a transfer of this token can check the consistency of the operation locally. Without this Schema, a user would not be able to interpret the status data or check that it complies with the contract rules.
So when a new wallet wants to support an asset, it simply needs to integrate the relevant Schema. This mechanism makes it possible to add compatibility to new RGB asset types, without invasively changing the wallet's software base: all that's required is to import the Schema binary and understand its structure.
The Schema defines the business logic in RGB. It lists the evolution rules of a contract, the structure of its data (Owned States, Global State, Valencies) and the associated validation scripts (executable by AluVM). Thanks to this declarative document, the definition of a contract (compiled file) is clearly separated from the actual execution of the rules (client-side). This decoupling gives RGB great flexibility, enabling a wide range of use cases (fungible tokens, NFT, more sophisticated contracts) while avoiding the complexity and flaws typical of programmable on-chain blockchains.

Schema example

Let's take a look at a concrete example of Schema for an RGB contract. This is an extract in Rust from the file nia.rs (initials for "Non-Inflatable Assets"), which defines a model for fungible tokens that cannot be reissued beyond their initial supply (a non-inflationary asset). This type of token can be seen as the equivalent, in the RGB universe, of the ERC20 on Ethereum, i.e. fungible tokens that respect certain basic rules (e.g. on transfers, supply initialization, etc.).
Before diving into the code, it's worth recalling the general structure of an RGB Schema. There is a series of declarations framing:
  • A possible SchemaId indicating the use of another basic Schema as a template;
  • The Global States and Owned States (with their strict types);
  • Valencies (if any);
  • The Operations (Genesis, State Transitions, State Extensions) that can reference these states and valencies;
  • The Strict Type System used to describe and validate data;
  • Validation scripts (run via AluVM).
The code below shows the complete definition of the Rust Schema. We will comment it part by part, following the annotations (1) to (9) below:
// ===== PART 1: Function Header and SubSchema ===== fn nia_schema() -> SubSchema { // definitions of libraries and variables // ===== PART 2: General Properties (ffv, subset_of, type_system) ===== Schema { ffv: zero!(), subset_of: None, type_system: types.type_system(), // ===== PART 3: Global States ===== global_types: tiny_bmap! { GS_NOMINAL => GlobalStateSchema::once(types.get("RGBContract.DivisibleAssetSpec")), GS_DATA => GlobalStateSchema::once(types.get("RGBContract.ContractData")), GS_TIMESTAMP => GlobalStateSchema::once(types.get("RGBContract.Timestamp")), GS_ISSUED_SUPPLY => GlobalStateSchema::once(types.get("RGBContract.Amount")), }, // ===== PART 4: Owned Types ===== owned_types: tiny_bmap! { OS_ASSET => StateSchema::Fungible(FungibleType::Unsigned64Bit), }, // ===== PART 5: Valencies ===== valency_types: none!(), // ===== PART 6: Genesis: Initial Operations ===== genesis: GenesisSchema { metadata: Ty::<SemId>::UNIT.id(None), globals: tiny_bmap! { GS_NOMINAL => Occurrences::Once, GS_DATA => Occurrences::Once, GS_TIMESTAMP => Occurrences::Once, GS_ISSUED_SUPPLY => Occurrences::Once, }, assignments: tiny_bmap! { OS_ASSET => Occurrences::OnceOrMore, }, valencies: none!(), }, // ===== PART 7: Extensions ===== extensions: none!(), // ===== PART 8: Transitions: TS_TRANSFER ===== transitions: tiny_bmap! { TS_TRANSFER => TransitionSchema { metadata: Ty::<SemId>::UNIT.id(None), globals: none!(), inputs: tiny_bmap! { OS_ASSET => Occurrences::OnceOrMore, }, assignments: tiny_bmap! { OS_ASSET => Occurrences::OnceOrMore, }, valencies: none!(), } }, // ===== PART 9: Script AluVM and Entry Points ===== script: Script::AluVM(AluScript { libs: confined_bmap! { alu_id => alu_lib }, entry_points: confined_bmap! { EntryPoint::ValidateGenesis => LibSite::with(FN_GENESIS_OFFSET, alu_id), EntryPoint::ValidateTransition(TS_TRANSFER) => LibSite::with(FN_TRANSFER_OFFSET, alu_id), }, }), } }
  • (1) - Function header and SubSchema
The nia_schema() function returns a SubSchema, indicating that this Schema can partially inherit from a more generic schema. In the RGB ecosystem, this flexibility makes it possible to reuse certain standard elements of a master schema, and then define rules specific to the contract in question. Here, we choose not to enable inheritance, since subset_of will be None.
  • (2) - General properties: ffv, subset_of, type_system
The ffv property corresponds to the fast-forward version of the contract. A value of zero!() here indicates that we are at version 0 or the initial version of this schema. If you later wish to add new functionalities (new type of operation, etc.), you can increment this version to indicate a consensus change.
The subset_of: None property confirms the absence of inheritance. The type_system field refers to the strict type system already defined in the types library. This line indicates that all data used by the contract uses the strict serialization implementation provided by the library in question.
  • (3) - Global States
In the global_types block, we declare four elements. We use the key, such as GS_NOMINAL or GS_ISSUED_SUPPLY, to reference them later:
  • GS_NOMINAL refers to a DivisibleAssetSpec type, which describes various fields of the created token (full name, ticker, precision...);
  • GS_DATA represents general data, such as a disclaimer, metadata, or other text;
  • GS_TIMESTAMP refers to an issue date;
  • GS_ISSUED_SUPPLY sets the total supply, i.e. the maximum number of tokens that can be created.
The keyword once(...) means that each of these fields can only appear once.
  • (4) - Owned Types
In owned_types, we declare OS_ASSET, which describes a fungible state. We use StateSchema::Fungible(FungibleType::Unsigned64Bit), indicating that the quantity of assets (tokens) is stored as a 64-bit unsigned integer. Thus, any transaction will send a certain amount of units of this token, which will be validated according to this strictly typed numerical structure.
  • (5) - Valencies
We indicate valency_types: none!(), which means that there are no Valencies in this schema, in other words no special or extra rights (such as reissue, conditional burn, etc.). If a schema included any, they would be declared in this section.
  • (6) - Genesis: first operations
Here we enter the part that declares Contract Operations. The Genesis is described by:
  • The absence of metadata (field metadata: Ty::<SemId>::UNIT.id(None));
  • Global States which must be present once each (Once);
  • An Assignments list where OS_ASSET must appear OnceOrMore. This means that Genesis requires at least one OS_ASSET Assignment (an initial holder);
  • No Valency: valencies: none!().
This is how we limit the definition of the initial token issue: we must declare the supply issued (GS_ISSUED_SUPPLY), plus at least one holder (an Owned State of type OS_ASSET).
  • (7) - Extensions
The extensions: none!() field indicates that no State Extension is foreseen in this contract. This means that there is no operation to redeem a digital right (Valency) or to perform a state extension before a Transition. Everything is done via Genesis or State Transitions.
  • (8) - Transitions: TS_TRANSFER
In transitions, we define the TS_TRANSFER type of operation. We explain that:
  • It has no metadata;
  • It does not modify the Global State (which is already defined in Genesis);
  • It takes one or more OS_ASSETs as inputs. This means it must spend existing Owned States;
  • It creates (assignments) at least one new OS_ASSET (in other words, the recipient or recipients receive tokens);
  • It generates no new Valency.
This models the behavior of a basic transfer, which consumes tokens on a UTXO, then creates new Owned States in favor of the recipients, and thus preserves the equality of the total amount between inputs and outputs.
  • (9) - AluVM script and Entry Points (in French)
Finally, we declare an AluVM script (Script::AluVM(AluScript { ... })). This script contains:
  • One or more external libraries (libs) to be used during validation;
  • Entry points pointing to function offsets in the AluVM code, corresponding to validation of the Genesis (ValidateGenesis) and each declared Transition (ValidateTransition(TS_TRANSFER)).
This validation code is responsible for applying business logic. For example, it will check:
  • That the GS_ISSUED_SUPPLY is not exceeded during Genesis;
  • That the sum of inputs (tokens spent) equals the sum of assignments (tokens received) for TS_TRANSFER.
If these rules are not respected, the transition will be considered invalid.
This example of a "Non Inflatable Fungible Asset" Schema gives us a better understanding of the structure of a simple RGB fungible token contract. We can clearly see the separation between data description (Global and Owned States), operation declaration (Genesis, Transitions, Extensions) and validation implementation (AluVM scripts). Thanks to this model, a token behaves like a classic fungible token, but remains validated on the client side and does not depend on the on-chain infrastructure to execute its code. Only cryptographic commitments are anchored in the Bitcoin blockchain.

Interface

The interface is the layer designed to make a contract readable and manipulable, both by users (human reading) and by portfolios (software reading). The Interface therefore plays a role comparable to that of an interface in an object-oriented programming language (Java, Rust trait, etc.), in that it exposes and clarifies the functional structure of a contract, without necessarily revealing the internal details of the business logic.
Unlike Schema, which is purely declarative and compiled into a binary file that is difficult to use as is, Interface provides the reading keys needed to:
  • List and describe the Global States and Owned States included in the contract;
  • Access the names and values of each field, so that they can be displayed (e.g. for a token, find out its ticker, maximum amount, etc.);
  • Interpret and construct Contract Operations (Genesis, State Transition, or State Extension) by associating data with understandable names (e.g., perform a transfer by clearly specifying "amount" rather than a binary identifier).
Thanks to the Interface, you can, for example, write code in a wallet which, instead of manipulating fields, directly manipulates labels such as "number of tokens", "asset name", etc. This way, managing a contract becomes more intuitive. In this way, contract management becomes more intuitive.

General operation

This method has many advantages:
  • Standardization:
The same type of contract can be supported by a standard Interface, shared between several wallet implementations. This facilitates compatibility and code reuse.
  • Clear separation between Schema and Interface:
In RGB design, Schema (business logic) and Interface (presentation and manipulation) are two independent entities. The developers who write the contract logic can concentrate on the Schema, without worrying about ergonomics or data representation, while another team (or the same team, but on a different timeline) can develop the Interface.
  • Flexible evolution:
The Interface can be modified or added to after the asset has been issued, without having to change the contract itself. This is a major difference from some on-chain smart contract systems, where the Interface (often mixed with the execution code) is frozen in the blockchain.
  • Multi-interface capability
The same contract could be exposed through different Interfaces adapted to distinct needs: a simple Interface for the end-user, another more advanced one for the issuer who needs to manage complex configuration operations. The wallet can then choose which Interface to import, depending on its use.
In practice, when the wallet retrieves an RGB contract (via a .rgb or .rgba file), it also imports the associated Interface, which is also compiled. At runtime, the wallet can, for example:
  • Browse the list of states and read their names, to display Ticker, Initial Amount, Issue Date, etc. on the user interface, rather than an unreadable numeric identifier;
  • Build an operation (such as a transfer) using explicit parameter names: instead of writing assignments { OS_ASSET => 1 }, it can offer the user an "Amount" field in a form, and translate this information into the strictly typed fields expected by the contract.

Difference from Ethereum and other systems

On Ethereum, the Interface (described via the ABI, Application Binary Interface) is generally derived from on-chain stored code (the smart contract). It can be costly or complicated to modify a specific part of the interface without touching the contract itself. However, RGB is based on an entirely off-chain logic, with data anchored in commitments on Bitcoin. This design makes it possible to modify the Interface (or its implementation) without impacting the fundamental security of the contract, as the validation of the business rules remains in the Schema and the referenced AluVM code.

Interface compilation

As with Schema, the Interface is defined in source code (often in Rust) and compiled into a .rgb or .rgba file. This binary file contains all the information required by the wallet to:
  • Identify fields by name;
  • Link each field (and its value) to the strict system type defined in the contract;
  • Know the different operations allowed and how to build them.
Once the Interface has been imported, the wallet can correctly display the contract and propose interactions to the user.

Interfaces standardized by the LNP/BP association

In the RGB ecosystem, an Interface is used to give a readable and manipulable meaning to the data and operations of a contract. The Interface thus complements the Schema, which describes the business logic internally (strict types, validation scripts, etc.). In this section, we'll take a look at the standard Interfaces developed by the LNP/BP association for common contract types (fungible tokens, NFT, etc.).
As a reminder, the idea is that each Interface describes how to display and manipulate a contract on the wallet side, clearly naming the fields (such as spec, ticker, issuedSupply...) and defining the possible operations (such as Transfer, Burn, Rename...). Several Interfaces are already operational, but there will be more and more in the future.

Some ready-to-use interfaces

RGB20 is the Interface for fungible assets, which can be compared to Ethereum's ERC20 standard. However, it goes a step further, offering more extensive functionality:
  • For example, the ability to rename the asset (change of ticker or full name) after it has been issued, or to adjust its precision (stock splits);
  • It can also describe mechanisms for secondary reissuance (limited or unlimited) and for burn and then replacement, in order to authorize the issuer to destroy and then recreate assets under certain conditions;
For example, the RGB20 Interface can be linked to the Non-Inflatable Asset (NIA) scheme, which imposes a non-inflatable initial supply, or to other more advanced schemes as required.
RGB21 concerns NFT-type contracts, or more broadly, any unique digital content, such as the representation of digital media (images, music, etc.). In addition to describing the issue and transfer of a single asset, it includes features such as:
  • Integrated support for direct inclusion of a file (up to 16 MB) in the contract (for client-side retrieval);
  • The possibility for the owner to enter a "engraving" in the history to prove past ownership of an NFT.
RGB25 is a hybrid standard combining fungible and non-fungible aspects. It is designed for partially fungible assets, such as real estate tokenization, where you want to split up a property while retaining a link to a single root asset (in other words, you have fungible pieces of a house, linked to a non-fungible house). Technically, this interface can be linked to the Collectible Fungible Asset (CFA) schema, which takes into account the notion of splitting while tracing the original asset.

Interfaces under development

Other Interfaces are planned for more specialized uses, but are not yet available:
  • RGB22, dedicated to digital identities, to manage identifiers and on-chain profiles in the RGB ecosystem;
  • RGB23, for advanced time stamping, using some of the ideas of Opentimestamps, but with traceability features;
  • RGB24, which aims for the equivalent of a decentralized domain name system (DNS) similar to the Ethereum Name Service;
  • RGB26, designed to manage DAOs (Decentralized Autonomous Organization) in a more complex format (governance, voting, etc.);
  • RGB30, very similar to RGB20 but with the particularity of taking into account decentralized initial issuance and using State Extensions. This would be used for assets whose re-issuance is managed by several entities, or subject to finer conditions.
Of course, depending on the date on which you consult this course, these interfaces may already be operational and accessible.

Interface example

This Rust code snippet shows a RGB20 Interface (fungible asset). This code is taken from the rgb20.rs file in the standard RGB library. Let's take a look at it to understand the structure of an Interface and how it provides a bridge between, on the one hand, the business logic (defined in the Schema) and, on the other, the functionalities exposed to wallets and users.
// ... fn rgb20() -> Iface { let types = StandardTypes::with(rgb20_stl()); Iface { version: VerNo::V1, name: tn!("RGB20"), global_state: tiny_bmap! { fname!("spec") => GlobalIface::required(types.get("RGBContract.DivisibleAssetSpec")), fname!("data") => GlobalIface::required(types.get("RGBContract.ContractData")), fname!("created") => GlobalIface::required(types.get("RGBContract.Timestamp")), fname!("issuedSupply") => GlobalIface::one_or_many(types.get("RGBContract.Amount")), fname!("burnedSupply") => GlobalIface::none_or_many(types.get("RGBContract.Amount")), fname!("replacedSupply") => GlobalIface::none_or_many(types.get("RGBContract.Amount")), }, assignments: tiny_bmap! { fname!("inflationAllowance") => AssignIface::public(OwnedIface::Amount, Req::NoneOrMore), fname!("updateRight") => AssignIface::public(OwnedIface::Rights, Req::Optional), fname!("burnEpoch") => AssignIface::public(OwnedIface::Rights, Req::Optional), fname!("burnRight") => AssignIface::public(OwnedIface::Rights, Req::NoneOrMore), fname!("assetOwner") => AssignIface::private(OwnedIface::Amount, Req::NoneOrMore), }, valencies: none!(), genesis: GenesisIface { metadata: Some(types.get("RGBContract.IssueMeta")), global: tiny_bmap! { fname!("spec") => ArgSpec::required(), fname!("data") => ArgSpec::required(), fname!("created") => ArgSpec::required(), fname!("issuedSupply") => ArgSpec::required(), }, assignments: tiny_bmap! { fname!("assetOwner") => ArgSpec::many(), fname!("inflationAllowance") => ArgSpec::many(), fname!("updateRight") => ArgSpec::optional(), fname!("burnEpoch") => ArgSpec::optional(), }, valencies: none!(), errors: tiny_bset! { SUPPLY_MISMATCH, INVALID_PROOF, INSUFFICIENT_RESERVES }, }, transitions: tiny_bmap! { tn!("Transfer") => TransitionIface { optional: false, metadata: None, globals: none!(), inputs: tiny_bmap! { fname!("previous") => ArgSpec::from_non_empty("assetOwner"), }, assignments: tiny_bmap! { fname!("beneficiary") => ArgSpec::from_non_empty("assetOwner"), }, valencies: none!(), errors: tiny_bset! { NON_EQUAL_AMOUNTS }, default_assignment: Some(fname!("beneficiary")), }, tn!("Issue") => TransitionIface { optional: true, metadata: Some(types.get("RGBContract.IssueMeta")), globals: tiny_bmap! { fname!("issuedSupply") => ArgSpec::required(), }, inputs: tiny_bmap! { fname!("used") => ArgSpec::from_non_empty("inflationAllowance"), }, assignments: tiny_bmap! { fname!("beneficiary") => ArgSpec::from_many("assetOwner"), fname!("future") => ArgSpec::from_many("inflationAllowance"), }, valencies: none!(), errors: tiny_bset! { SUPPLY_MISMATCH, INVALID_PROOF, ISSUE_EXCEEDS_ALLOWANCE, INSUFFICIENT_RESERVES }, default_assignment: Some(fname!("beneficiary")), }, tn!("OpenEpoch") => TransitionIface { optional: true, metadata: None, globals: none!(), inputs: tiny_bmap! { fname!("used") => ArgSpec::from_required("burnEpoch"), }, assignments: tiny_bmap! { fname!("next") => ArgSpec::from_optional("burnEpoch"), fname!("burnRight") => ArgSpec::required() }, valencies: none!(), errors: none!(), default_assignment: Some(fname!("burnRight")), }, tn!("Burn") => TransitionIface { optional: true, metadata: Some(types.get("RGBContract.BurnMeta")), globals: tiny_bmap! { fname!("burnedSupply") => ArgSpec::required(), }, inputs: tiny_bmap! { fname!("used") => ArgSpec::from_required("burnRight"), }, assignments: tiny_bmap! { fname!("future") => ArgSpec::from_optional("burnRight"), }, valencies: none!(), errors: tiny_bset! { SUPPLY_MISMATCH, INVALID_PROOF, INSUFFICIENT_COVERAGE }, default_assignment: None, }, tn!("Replace") => TransitionIface { optional: true, metadata: Some(types.get("RGBContract.BurnMeta")), globals: tiny_bmap! { fname!("replacedSupply") => ArgSpec::required(), }, inputs: tiny_bmap! { fname!("used") => ArgSpec::from_required("burnRight"), }, assignments: tiny_bmap! { fname!("beneficiary") => ArgSpec::from_many("assetOwner"), fname!("future") => ArgSpec::from_optional("burnRight"), }, valencies: none!(), errors: tiny_bset! { NON_EQUAL_AMOUNTS, SUPPLY_MISMATCH, INVALID_PROOF, INSUFFICIENT_COVERAGE }, default_assignment: Some(fname!("beneficiary")), }, tn!("Rename") => TransitionIface { optional: true, metadata: None, globals: tiny_bmap! { fname!("new") => ArgSpec::from_required("spec"), }, inputs: tiny_bmap! { fname!("used") => ArgSpec::from_required("updateRight"), }, assignments: tiny_bmap! { fname!("future") => ArgSpec::from_optional("updateRight"), }, valencies: none!(), errors: none!(), default_assignment: Some(fname!("future")), }, }, extensions: none!(), error_type: types.get("RGB20.Error"), default_operation: Some(tn!("Transfer")), type_system: types.type_system(), } }
In this interface, we notice similarities with the Schema structure: we find a declaration of Global State, Owned States, Contract Operations (Genesis and Transitions), as well as error handling. But the Interface focuses on the presentation and manipulation of these elements for a wallet or any other application.
The difference with Schema lies in the nature of the types. Schema uses strict types (such as FungibleType::Unsigned64Bit) and more technical identifiers. The Interface uses field names, macros (fname!(), tn!()), and references to argument classes (ArgSpec, OwnedIface::Rights...). The aim here is to facilitate the functional understanding and organization of elements for the wallet.
In addition, the Interface can introduce additional functionality to the basic Schema (e.g. management of a burnEpoch right), as long as this remains consistent with the final validated client-side logic. The AluVM "script" section in the Schema will ensure cryptographic validity, while the Interface describes how the user (or wallet) interacts with these states and transitions.

Global State and Assignments

In the global_state section, we find fields such as spec (asset description), data, created, issuedSupply, burnedSupply, replacedSupply. These are fields that the wallet can read and present. For example:
  • spec will display the token configuration;
  • issuedSupply or burnedSupply give us the total number of tokens issued or burned, etc.
In the assignments section, we define various roles or rights. For example:
  • assetOwner corresponds to the holding of tokens (it is the fungible Owned State);
  • burnRight corresponds to the ability to burn tokens;
  • updateRight` corresponds to the right to rename the asset.
The public or private keyword (e.g. AssignIface::public(...)) indicates whether these states are visible (public) or confidential (private). As for Req::NoneOrMore, Req::Optional, they indicate the expected occurrence.

Genesis and transitions

The genesis part describes how the asset is initialized:
  • The spec, data, created, issuedSupply fields are mandatory (ArgSpec::required());
  • Assignments such as assetOwner can be present in multiple copies (ArgSpec::many()), allowing tokens to be distributed to multiple initial holders;
  • Fields such as inflationAllowance or burnEpoch may (or may not) be included in Genesis.
Then, for each Transition (Transfer, Issue, Burn...), the Interface defines which fields the operation expects as input, which fields the operation will produce as output, and any errors that may occur. For example:
Transition:
  • Inputs: previous → must be an assetOwner;
  • Assignments: beneficiary → will be a new assetOwner;
  • Error: NON_EQUAL_AMOUNTS (the wallet will thus be able to handle cases where the input sum does not correspond to the output sum).
Transition Issue:
  • Optional (optional: true), as additional emission is not necessarily activated;
  • Inputs: used → an inflationAllowance, i.e. permission to add more tokens;
  • Assignments: beneficiary (new tokens received) and future (remaining inflationAllowance);
  • Possible errors: SUPPLY_MISMATCH, ISSUE_EXCEEDS_ALLOWANCE, etc.
Burn transition:
  • Inputs: used → a burnRight;
  • Globals: burnedSupply required;
  • Assignments: future → a possible continuation of the burnRight if we haven't burned everything;
  • Errors: SUPPLY_MISMATCH, INVALID_PROOF, INSUFFICIENT_COVERAGE.
Each operation is therefore described in a way that is readable for a wallet. This makes it possible to display a graphical interface where the user can clearly see: "You have the right to burn. Would you like to burn a certain amount? The code knows to fill in a burnedSupply field and check that the burnRight is valid.
To sum up, it's important to bear in mind that an Interface, however complete, does not by itself define the internal logic of the contract. The heart of the work is done by the Schema, which includes strict types, Genesis structure, transitions and so on. The Interface simply exposes these elements in a more intuitive and named way, for use in an application.
Thanks to RGB's modularity, the Interface can be upgraded (for example, by adding a Rename transition, correcting the display of a field, etc.) without having to rewrite the entire contract. Users of this Interface can then benefit immediately from these improvements, as soon as they update the .rgb or .rgba file.
But once you've declared an Interface, you need to link it to the corresponding Schema. This is done via the Interface Implementation, which specifies how to map each named field (such as fname!("assetOwner")) to the strict ID (such as OS_ASSET) defined in the Schema. This ensures, for example, that when a wallet manipulates a burnRight field, this is the state which, in the Schema, describes the ability to burn tokens.

Interface Implementation

In the RGB architecture, we have seen that each component (Schema, Interface, etc.) can be developed and compiled independently. However, there's still one indispensable element that links these different building blocks together: the Interface Implementation. This is what explicitly maps the identifiers or fields defined in the Schema (on the business logic side) to the names declared in the Interface (on the presentation and user interaction side). So when a wallet loads a contract, it can understand exactly which field corresponds to what, and how an operation named in the Interface relates to the logic of the Schema.
An important point is that Interface Implementation is not necessarily intended to expose all Schema functionalities, nor all Interface fields: it can be limited to a subset. In practice, this makes it possible to restrict or filter certain aspects of the Schema. For example, you could have a Schema with four types of operation, but an Implementation Interface that maps only two of them in a given context. Conversely, if an Interface proposes additional endpoints, we can choose not to implement them here.
Here's a classic example of Interface Implementation, where we associate a Non-Inflatable Asset (NIA) Schema with the RGB20 Interface:
fn nia_rgb20() -> IfaceImpl { let schema = nia_schema(); let iface = Rgb20::iface(); IfaceImpl { version: VerNo::V1, schema_id: schema.schema_id(), iface_id: iface.iface_id(), script: none!(), global_state: tiny_bset! { NamedField::with(GS_NOMINAL, fname!("spec")), NamedField::with(GS_DATA, fname!("data")), NamedField::with(GS_TIMESTAMP, fname!("created")), NamedField::with(GS_ISSUED_SUPPLY, fname!("issuedSupply")), }, assignments: tiny_bset! { NamedField::with(OS_ASSET, fname!("assetOwner")), }, valencies: none!(), transitions: tiny_bset! { NamedType::with(TS_TRANSFER, tn!("Transfer")), }, extensions: none!(), } }
In this Implementation Interface:
  • We explicitly reference the Schema, via nia_schema(), and the Interface, via Rgb20::iface(). The calls schema.schema_id() and iface.iface_id() are used to anchor the Interface Implementation on the compile side (this associates the cryptographic identifiers of these two components);
  • A mapping is established between Schema elements and Interface elements. For example, the GS_NOMINAL field in the Schema is linked to the string "spec" on the Interface side (NamedField::with(GS_NOMINAL, fname!("spec"))). We do the same for operations, such as TS_TRANSFER, which we link to "Transfer" in the Interface...;
  • We can see that there are no valencies (valencies: none!()) or extensions (extensions: none!()), reflecting the fact that this NIA contract doesn't use these features.
The result after compilation is a separate .rgb or .rgba file, to be imported into the wallet in addition to the Schema and Interface. Thus, the software knows how to concretely connect this NIA contract (whose logic is described by its Schema) to the "RGB20" Interface (which provides human names and an interaction mode for fungible tokens), applying this Interface Implementation as a gateway between the two.

Why separate Interface Implementation?

Separation enhances flexibility. A single Schema could have several distinct Interface Implementations, each mapping a different set of functionalities. What's more, the Interface Implementation itself can evolve or be rewritten without requiring a change in either the Schema or the Interface. This retains RGB's principle of modularity: each component (Schema, Interface, Interface Implementation) can be versioned and updated independently, as long as the compatibility rules imposed by the protocol are respected (same identifiers, consistency of types, etc.).
In concrete use, when the wallet loads a contract, it must:
  • Load the compiled Schema (to know the structure of the business logic);
  • Load compiled Interface (to understand names and user-side operations);
  • Load compiled Interface Implementation (to link Schema logic to Interface names, operation by operation, field by field).
This modular architecture makes possible use scenarios such as:
  • Limit certain operations for certain users: offer a partial Implementation Interface that only gives access to basic transfers, without offering burn or update functions, for example;
  • Change presentation: design an Interface Implementation that renames a field in the Interface or maps it differently, without altering the basis of the contract;
  • Support multiple schemes: a wallet can load multiple Interface Implementations for the same Interface type, to handle different schemes (different token logics), provided their structure is compatible.
In the next chapter, we'll look at how a contract transfer works, and how RGB invoices are generated.
Quiz
Quiz1/5
What is the main role of Schema in an RGB contract?