schemata/
nia.rs

1// RGB schemas
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2023-2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2023-2024 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22//! Non-Inflatable Assets (NIA) schema.
23
24use aluvm::isa::opcodes::INSTR_PUTA;
25use aluvm::isa::Instr;
26use aluvm::library::{Lib, LibSite};
27use amplify::confinement::Confined;
28use rgbstd::contract::{
29    AssignmentsFilter, ContractData, FungibleAllocation, IssuerWrapper, SchemaWrapper,
30};
31use rgbstd::persistence::{ContractStateRead, MemContract};
32use rgbstd::schema::{
33    AssignmentDetails, FungibleType, GenesisSchema, GlobalDetails, GlobalStateSchema, Occurrences,
34    OwnedStateSchema, Schema, TransitionSchema,
35};
36use rgbstd::stl::{rgb_contract_stl, AssetSpec, ContractTerms, StandardTypes};
37use rgbstd::validation::Scripts;
38use rgbstd::vm::opcodes::INSTR_SVS;
39use rgbstd::vm::RgbIsa;
40use rgbstd::{rgbasm, Amount, SchemaId, TransitionDetails};
41use strict_types::TypeSystem;
42
43use crate::{
44    ERRNO_ISSUED_MISMATCH, ERRNO_NON_EQUAL_IN_OUT, GS_ISSUED_SUPPLY, GS_NOMINAL, GS_TERMS,
45    OS_ASSET, TS_TRANSFER,
46};
47
48pub const NIA_SCHEMA_ID: SchemaId = SchemaId::from_array([
49    0x45, 0x68, 0x70, 0x51, 0xf4, 0xcc, 0xa6, 0xe3, 0xf6, 0x65, 0xfc, 0x75, 0xfe, 0x3e, 0x27, 0xb3,
50    0x00, 0x80, 0x34, 0x67, 0x89, 0xad, 0x83, 0xaa, 0x0d, 0xc2, 0x9e, 0x95, 0xa3, 0x15, 0xe3, 0x35,
51]);
52
53pub(crate) fn nia_lib() -> Lib {
54    let code = rgbasm! {
55        // SUBROUTINE Transfer validation
56        // Set errno
57        put     a8[0],ERRNO_NON_EQUAL_IN_OUT;
58        // Checking that the sum of inputs is equal to the sum of outputs.
59        svs     OS_ASSET;
60        test;
61        ret;
62
63        // SUBROUTINE Genesis validation
64        // Checking genesis assignments amount against reported amount of issued assets present in
65        // the global state.
66        put     a8[0],ERRNO_ISSUED_MISMATCH;
67        put     a8[1],0;
68        put     a16[0],0;
69        // Read global state into s16[0]
70        ldg     GS_ISSUED_SUPPLY,a8[1],s16[0];
71        // Extract 64 bits from the beginning of s16[0] into a64[0]
72        // NB: if the global state is invalid, we will fail here and fail the validation
73        extr    s16[0],a64[0],a16[0];
74        // verify sum of outputs against a64[0] value
75        sas     OS_ASSET;
76        test;
77        ret;
78    };
79    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code).expect("wrong non-inflatable asset script")
80}
81pub(crate) const FN_NIA_GENESIS_OFFSET: u16 = 4 + 3 + 2;
82pub(crate) const FN_NIA_TRANSFER_OFFSET: u16 = 0;
83
84fn nia_standard_types() -> StandardTypes { StandardTypes::with(rgb_contract_stl()) }
85
86fn nia_schema() -> Schema {
87    let types = nia_standard_types();
88
89    let alu_lib = nia_lib();
90    let alu_id = alu_lib.id();
91    assert_eq!(alu_lib.code.as_ref()[FN_NIA_TRANSFER_OFFSET as usize + 4], INSTR_SVS);
92    assert_eq!(alu_lib.code.as_ref()[FN_NIA_GENESIS_OFFSET as usize], INSTR_PUTA);
93    assert_eq!(alu_lib.code.as_ref()[FN_NIA_GENESIS_OFFSET as usize + 4], INSTR_PUTA);
94    assert_eq!(alu_lib.code.as_ref()[FN_NIA_GENESIS_OFFSET as usize + 8], INSTR_PUTA);
95
96    Schema {
97        ffv: zero!(),
98        name: tn!("NonInflatableAsset"),
99        meta_types: none!(),
100        global_types: tiny_bmap! {
101            GS_NOMINAL => GlobalDetails {
102                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.AssetSpec")),
103                name: fname!("spec"),
104            },
105            GS_TERMS => GlobalDetails {
106                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.ContractTerms")),
107                name: fname!("terms"),
108            },
109            GS_ISSUED_SUPPLY => GlobalDetails {
110                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.Amount")),
111                name: fname!("issuedSupply"),
112            },
113        },
114        owned_types: tiny_bmap! {
115            OS_ASSET => AssignmentDetails {
116                owned_state_schema: OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
117                name: fname!("assetOwner"),
118                default_transition: TS_TRANSFER,
119            }
120        },
121        genesis: GenesisSchema {
122            metadata: none!(),
123            globals: tiny_bmap! {
124                GS_NOMINAL => Occurrences::Once,
125                GS_TERMS => Occurrences::Once,
126                GS_ISSUED_SUPPLY => Occurrences::Once,
127            },
128            assignments: tiny_bmap! {
129                OS_ASSET => Occurrences::OnceOrMore,
130            },
131            validator: Some(LibSite::with(FN_NIA_GENESIS_OFFSET, alu_id)),
132        },
133        transitions: tiny_bmap! {
134            TS_TRANSFER => TransitionDetails {
135                transition_schema: TransitionSchema {
136                    metadata: none!(),
137                    globals: none!(),
138                    inputs: tiny_bmap! {
139                        OS_ASSET => Occurrences::OnceOrMore
140                    },
141                    assignments: tiny_bmap! {
142                        OS_ASSET => Occurrences::OnceOrMore
143                    },
144                    validator: Some(LibSite::with(FN_NIA_TRANSFER_OFFSET, alu_id))
145                },
146                name: fname!("transfer"),
147            }
148        },
149        default_assignment: Some(OS_ASSET),
150    }
151}
152
153#[derive(Default)]
154pub struct NonInflatableAsset;
155
156impl IssuerWrapper for NonInflatableAsset {
157    type Wrapper<S: ContractStateRead> = NiaWrapper<S>;
158
159    fn schema() -> Schema { nia_schema() }
160
161    fn types() -> TypeSystem { nia_standard_types().type_system(nia_schema()) }
162
163    fn scripts() -> Scripts {
164        let lib = nia_lib();
165        Confined::from_checked(bmap! { lib.id() => lib })
166    }
167}
168
169#[derive(Clone, Eq, PartialEq, Debug, From)]
170pub struct NiaWrapper<S: ContractStateRead>(ContractData<S>);
171
172impl<S: ContractStateRead> SchemaWrapper<S> for NiaWrapper<S> {
173    fn with(data: ContractData<S>) -> Self {
174        if data.schema.schema_id() != NIA_SCHEMA_ID {
175            panic!("the provided schema is not NIA");
176        }
177        Self(data)
178    }
179}
180
181impl<S: ContractStateRead> NiaWrapper<S> {
182    pub fn spec(&self) -> AssetSpec {
183        let strict_val = &self
184            .0
185            .global("spec")
186            .next()
187            .expect("NIA requires global state `spec` to have at least one item");
188        AssetSpec::from_strict_val_unchecked(strict_val)
189    }
190
191    pub fn contract_terms(&self) -> ContractTerms {
192        let strict_val = &self
193            .0
194            .global("terms")
195            .next()
196            .expect("NIA requires global state `terms` to have at least one item");
197        ContractTerms::from_strict_val_unchecked(strict_val)
198    }
199
200    pub fn total_issued_supply(&self) -> Amount {
201        self.0
202            .global("issuedSupply")
203            .map(|amount| Amount::from_strict_val_unchecked(&amount))
204            .sum()
205    }
206
207    pub fn allocations<'c>(
208        &'c self,
209        filter: impl AssignmentsFilter + 'c,
210    ) -> impl Iterator<Item = FungibleAllocation> + 'c {
211        self.0.fungible_raw(OS_ASSET, filter).unwrap()
212    }
213}
214
215#[cfg(test)]
216mod test {
217    use std::str::FromStr;
218
219    use rgbstd::containers::{BuilderSeal, ConsignmentExt};
220    use rgbstd::contract::*;
221    use rgbstd::invoice::Precision;
222    use rgbstd::stl::*;
223    use rgbstd::txout::BlindSeal;
224    use rgbstd::*;
225
226    use super::*;
227
228    #[test]
229    fn schema_id() {
230        let schema_id = nia_schema().schema_id();
231        eprintln!("{:#04x?}", schema_id.to_byte_array());
232        assert_eq!(NIA_SCHEMA_ID, schema_id);
233    }
234
235    #[test]
236    fn deterministic_contract_id() {
237        let created_at = 1713261744;
238        let terms = ContractTerms {
239            text: RicardianContract::default(),
240            media: None,
241        };
242        let spec = AssetSpec {
243            ticker: Ticker::from("TICKER"),
244            name: Name::from("NAME"),
245            details: None,
246            precision: Precision::try_from(2).unwrap(),
247        };
248        let issued_supply = 999u64;
249        let seal: BlindSeal<Txid> = GenesisSeal::from(BlindSeal::with_blinding(
250            Txid::from_str("8d54c98d4c29a1ec4fd90635f543f0f7a871a78eb6a6e706342f831d92e3ba19")
251                .unwrap(),
252            0,
253            654321,
254        ));
255
256        let builder = ContractBuilder::with(
257            Identity::default(),
258            NonInflatableAsset::schema(),
259            NonInflatableAsset::types(),
260            NonInflatableAsset::scripts(),
261            ChainNet::BitcoinTestnet4,
262        )
263        .add_global_state("spec", spec)
264        .unwrap()
265        .add_global_state("terms", terms)
266        .unwrap()
267        .add_global_state("issuedSupply", Amount::from(issued_supply))
268        .unwrap()
269        .add_fungible_state("assetOwner", BuilderSeal::from(seal), issued_supply)
270        .unwrap();
271
272        let contract = builder.issue_contract_raw(created_at).unwrap();
273
274        assert_eq!(
275            contract.contract_id().to_string(),
276            s!("rgb:663wqep~-0pVYnjS-ieA0N3r-58wUTIY-zgCGO_1-QQkuMMs")
277        );
278    }
279}