schemata/
pfa.rs

1// RGB schemas
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2025 by
6//     Zoe FaltibĂ  <zoefaltiba@gmail.com>
7//
8// Copyright (C) 2025 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//! Permissioned Fungible Assets (PFA) schema.
23//! (!) Not safe to use in a production environment!
24
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::RgbIsa;
39use rgbstd::{rgbasm, Amount, SchemaId, TransitionDetails};
40use strict_types::TypeSystem;
41
42use crate::{
43    ERRNO_INVALID_SIGNATURE, ERRNO_ISSUED_MISMATCH, ERRNO_MISSING_PUBKEY, ERRNO_NON_EQUAL_IN_OUT,
44    GS_ISSUED_SUPPLY, GS_NOMINAL, GS_PUBKEY, GS_TERMS, OS_ASSET, TS_TRANSFER,
45};
46
47pub const PFA_SCHEMA_ID: SchemaId = SchemaId::from_array([
48    0x62, 0xfb, 0xef, 0x43, 0x85, 0x2c, 0x1e, 0xe3, 0xd0, 0x0d, 0x3d, 0xe7, 0x21, 0x0f, 0x66, 0x9e,
49    0x9b, 0x2a, 0x31, 0xba, 0xec, 0xe6, 0x56, 0x19, 0x45, 0xbc, 0xb2, 0x98, 0x75, 0x6b, 0x91, 0x8f,
50]);
51
52pub(crate) fn pfa_lib_transition() -> Lib {
53    let code = rgbasm! {
54        // Checking that the sum of inputs is equal to the sum of outputs
55        put     a8[0],ERRNO_NON_EQUAL_IN_OUT;  // set errno
56        svs     OS_ASSET;  // verify sum
57        test;  // check it didn't fail
58
59        // Check transition signature
60        put     a8[0],ERRNO_MISSING_PUBKEY;  // set errno
61        put     a32[0],0;  // set a32[0] to 0
62        ldc     GS_PUBKEY,a32[0],s16[0];  // get global pubkey
63        put     a8[0],ERRNO_INVALID_SIGNATURE;  // set errno
64        vts     s16[0];  // verify signature
65        test;  // check it didn't fail
66        ret;  // return execution flow
67    };
68    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code).expect("wrong non-inflatable asset script")
69}
70
71pub(crate) fn pfa_lib_genesis() -> Lib {
72    let code = rgbasm! {
73        // Check genesis assignments amount against reported amount of issued assets present in the
74        // global state
75        put     a8[0],ERRNO_ISSUED_MISMATCH;  // set errno
76        put     a8[1],0;  // set a8[1] to 0
77        put     a16[0],0;  // set a16[0] to 0
78        ldg     GS_ISSUED_SUPPLY,a8[1],s16[0];  // get global issued supply
79        extr    s16[0],a64[0],a16[0];  // extract 64 bits from the beginning of s16[0] into a64[0]
80        sas     OS_ASSET;  // verify sum of outputs against a64[0] value
81        test;  // check it didn't fail
82        ret;  // return execution flow
83    };
84    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code).expect("wrong non-inflatable asset script")
85}
86
87fn pfa_standard_types() -> StandardTypes { StandardTypes::with(rgb_contract_stl()) }
88
89fn pfa_schema() -> Schema {
90    let types = pfa_standard_types();
91
92    let alu_lib_genesis = pfa_lib_genesis();
93    let alu_id_genesis = alu_lib_genesis.id();
94
95    let alu_lib_transition = pfa_lib_transition();
96    let alu_id_transition = alu_lib_transition.id();
97
98    Schema {
99        ffv: zero!(),
100        name: tn!("PermissionedFungibleAsset"),
101        meta_types: none!(),
102        global_types: tiny_bmap! {
103            GS_NOMINAL => GlobalDetails {
104                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.AssetSpec")),
105                name: fname!("spec"),
106            },
107            GS_TERMS => GlobalDetails {
108                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.ContractTerms")),
109                name: fname!("terms"),
110            },
111            GS_ISSUED_SUPPLY => GlobalDetails {
112                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.Amount")),
113                name: fname!("issuedSupply"),
114            },
115            GS_PUBKEY => GlobalDetails {
116                global_state_schema: GlobalStateSchema::once(types.get("Bitcoin.CompressedPk")),
117                name: fname!("pubkey"),
118            },
119        },
120        owned_types: tiny_bmap! {
121            OS_ASSET => AssignmentDetails {
122                owned_state_schema: OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
123                name: fname!("assetOwner"),
124                default_transition: TS_TRANSFER,
125            }
126        },
127        genesis: GenesisSchema {
128            metadata: none!(),
129            globals: tiny_bmap! {
130                GS_NOMINAL => Occurrences::Once,
131                GS_TERMS => Occurrences::Once,
132                GS_ISSUED_SUPPLY => Occurrences::Once,
133                GS_PUBKEY => Occurrences::Once,
134            },
135            assignments: tiny_bmap! {
136                OS_ASSET => Occurrences::OnceOrMore,
137            },
138            validator: Some(LibSite::with(0, alu_id_genesis)),
139        },
140        transitions: tiny_bmap! {
141            TS_TRANSFER => TransitionDetails {
142                transition_schema: TransitionSchema {
143                    metadata: none!(),
144                    globals: none!(),
145                    inputs: tiny_bmap! {
146                        OS_ASSET => Occurrences::OnceOrMore
147                    },
148                    assignments: tiny_bmap! {
149                        OS_ASSET => Occurrences::OnceOrMore
150                    },
151                    validator: Some(LibSite::with(0, alu_id_transition))
152                },
153                name: fname!("transfer"),
154            }
155        },
156        default_assignment: Some(OS_ASSET),
157    }
158}
159
160#[derive(Default)]
161pub struct PermissionedFungibleAsset;
162
163impl IssuerWrapper for PermissionedFungibleAsset {
164    type Wrapper<S: ContractStateRead> = PfaWrapper<S>;
165
166    fn schema() -> Schema { pfa_schema() }
167
168    fn types() -> TypeSystem { pfa_standard_types().type_system(pfa_schema()) }
169
170    fn scripts() -> Scripts {
171        let alu_lib_genesis = pfa_lib_genesis();
172        let alu_id_genesis = alu_lib_genesis.id();
173
174        let alu_lib_transition = pfa_lib_transition();
175        let alu_id_transition = alu_lib_transition.id();
176
177        Confined::from_checked(bmap! {
178            alu_id_genesis => alu_lib_genesis,
179            alu_id_transition => alu_lib_transition,
180        })
181    }
182}
183
184#[derive(Clone, Eq, PartialEq, Debug, From)]
185pub struct PfaWrapper<S: ContractStateRead>(ContractData<S>);
186
187impl<S: ContractStateRead> SchemaWrapper<S> for PfaWrapper<S> {
188    fn with(data: ContractData<S>) -> Self {
189        if data.schema.schema_id() != PFA_SCHEMA_ID {
190            panic!("the provided schema is not PFA");
191        }
192        Self(data)
193    }
194}
195
196impl<S: ContractStateRead> PfaWrapper<S> {
197    pub fn spec(&self) -> AssetSpec {
198        let strict_val = &self
199            .0
200            .global("spec")
201            .next()
202            .expect("PFA requires global state `spec` to have at least one item");
203        AssetSpec::from_strict_val_unchecked(strict_val)
204    }
205
206    pub fn contract_terms(&self) -> ContractTerms {
207        let strict_val = &self
208            .0
209            .global("terms")
210            .next()
211            .expect("PFA requires global state `terms` to have at least one item");
212        ContractTerms::from_strict_val_unchecked(strict_val)
213    }
214
215    pub fn total_issued_supply(&self) -> Amount {
216        self.0
217            .global("issuedSupply")
218            .map(|amount| Amount::from_strict_val_unchecked(&amount))
219            .sum()
220    }
221
222    pub fn allocations<'c>(
223        &'c self,
224        filter: impl AssignmentsFilter + 'c,
225    ) -> impl Iterator<Item = FungibleAllocation> + 'c {
226        self.0.fungible_raw(OS_ASSET, filter).unwrap()
227    }
228}
229
230#[cfg(test)]
231mod test {
232    use super::*;
233
234    #[test]
235    fn schema_id() {
236        let schema_id = pfa_schema().schema_id();
237        eprintln!("{:#04x?}", schema_id.to_byte_array());
238        assert_eq!(PFA_SCHEMA_ID, schema_id);
239    }
240}