From 26ea4232499fae15e240986596374099f4c58f06 Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 10 Jun 2024 16:30:01 +0800 Subject: [PATCH] rgb21 features --- interfaces/RGB21.con | 176 +++++++++++++++++++++++++++++++++++++ src/main.rs | 16 ++++ src/rgb21/iface.rs | 6 ++ src/rgb21/issuer.rs | 203 +++++++++++++++++++++++++++++++++++++++++++ src/rgb21/mod.rs | 2 +- src/rgb21/wrapper.rs | 79 ++++++++++++++++- 6 files changed, 479 insertions(+), 3 deletions(-) create mode 100644 interfaces/RGB21.con create mode 100644 src/rgb21/issuer.rs diff --git a/interfaces/RGB21.con b/interfaces/RGB21.con new file mode 100644 index 0000000..990b3f6 --- /dev/null +++ b/interfaces/RGB21.con @@ -0,0 +1,176 @@ +@version(1) +@id(rgb:ifc:zLZ5H1If-IedNbQ3-kzhoYO1-12djZWD-CHeHlEH-4CjQYM4#quasi-noise-martin) +@developer("ssi:LZS1ux-gjD9nXPF-OcetUUkW-6r3uSCS6-aQhs9W5f-8JE7w") +@timestamp(1711405444) +interface RGB21Base: NamedAsset, NonFungibleToken + global attachmentTypes(*): RGB21.AttachmentType + global spec: RGBContract.AssetSpec + global terms: RGBContract.ContractTerms + global tokens(*): RGB21.TokenData + + owned assetOwner(*): RGBContract.Allocation + + error fractionOverflow + "the amount of token fractions in outputs exceeds 1" + error invalidAttachmentType + "attachment has a type which is not allowed for the token" + error nonEqualValues + "the sum of spent token fractions doesn't equal to the sum of token fractions in outputs" + error nonFractionalToken + "attempt to transfer a fraction of non-fractionable token" + error unknownToken + "allocation of unknown token ID" + + genesis: abstract + errors: fractionOverflow, invalidAttachmentType, unknownToken + globals: attachmentTypes(*), spec, terms, tokens(*) + assigns: assetOwner(*) + + transition transfer: required, default, final + errors: fractionOverflow, nonEqualValues, nonFractionalToken, unknownToken + assigns: assetOwner(+) + default: assetOwner + inputs: assetOwner(+) + + +@version(1) +@id(rgb:ifc:3AEpY1Iv-Ybh$o5x-yJRdQjA-dmwy3@b-Rx5XZUI-8yldqFg#brazil-graph-license) +@developer("ssi:LZS1ux-gjD9nXPF-OcetUUkW-6r3uSCS6-aQhs9W5f-8JE7w") +@timestamp(1711405444) +interface RGB21Renamable: NamedAsset, NonFungibleToken, RGB21Base, RenameableAsset + global attachmentTypes(*): RGB21.AttachmentType + global spec: RGBContract.AssetSpec + global terms: RGBContract.ContractTerms + global tokens(*): RGB21.TokenData + + owned assetOwner(*): RGBContract.Allocation + public updateRight: Rights + + error fractionOverflow + "the amount of token fractions in outputs exceeds 1" + error invalidAttachmentType + "attachment has a type which is not allowed for the token" + error nonEqualValues + "the sum of spent token fractions doesn't equal to the sum of token fractions in outputs" + error nonFractionalToken + "attempt to transfer a fraction of non-fractionable token" + error unknownToken + "allocation of unknown token ID" + + genesis: abstract + errors: fractionOverflow, invalidAttachmentType, unknownToken + globals: attachmentTypes(*), spec, terms, tokens(*) + assigns: assetOwner(*), updateRight + + transition rename: required, final + globals: spec + assigns: updateRight(?) + default: updateRight + inputs: updateRight + + transition transfer: required, default, final + errors: fractionOverflow, nonEqualValues, nonFractionalToken, unknownToken + assigns: assetOwner(+) + default: assetOwner + inputs: assetOwner(+) + + +@version(1) +@id(rgb:ifc:zaiUh27F-2cYWfcd-FfL5lBc-uUenO66-IYZE0D9-GeVIvGU#forest-heroic-energy) +@developer("ssi:LZS1ux-gjD9nXPF-OcetUUkW-6r3uSCS6-aQhs9W5f-8JE7w") +@timestamp(1711405444) +interface RGB21Unique: NamedAsset, NonFungibleToken, RGB21Base, UniqueNft + global attachmentTypes: RGB21.AttachmentType + global spec: RGBContract.AssetSpec + global terms: RGBContract.ContractTerms + global tokens: RGB21.TokenData + + owned assetOwner(+): RGBContract.Allocation + + error fractionOverflow + "the amount of token fractions in outputs exceeds 1" + error invalidAttachmentType + "attachment has a type which is not allowed for the token" + error nonEqualValues + "the sum of spent token fractions doesn't equal to the sum of token fractions in outputs" + error nonFractionalToken + "attempt to transfer a fraction of non-fractionable token" + error unknownToken + "allocation of unknown token ID" + + genesis: abstract + errors: fractionOverflow, invalidAttachmentType, unknownToken + globals: attachmentTypes, spec, terms, tokens + assigns: assetOwner(+) + + transition transfer: required, default, final + errors: fractionOverflow, nonEqualValues, nonFractionalToken, unknownToken + assigns: assetOwner(+) + default: assetOwner + inputs: assetOwner(+) + + +@version(1) +@id(rgb:ifc:MTbC0xIy-t4kjnRO-6lrefMT-QDDxtSN-SNOHen$-WEeyyfc#cubic-motif-corona) +@developer("ssi:LZS1ux-gjD9nXPF-OcetUUkW-6r3uSCS6-aQhs9W5f-8JE7w") +@timestamp(1711405444) +interface RGB21Issuable: NamedAsset, NonFungibleToken, RGB21Base, RenameableAsset, gBnlwlsh-tuErFnK-EX4V40v-mx1P0OP-orty2gq-10yH4fc#hair-unicorn-initial +, EngravableNft, $@VBEHez-6$MHRU0-kPIulCv-oTDRpmd-OnRANMo-3$Snnng#current-metal-guitar +, IssuableNft + global attachmentTypes(*): RGB21.AttachmentType + global engravings(*): RGB21.EngravingData + global spec: RGBContract.AssetSpec + global terms: RGBContract.ContractTerms + global tokens(*): RGB21.TokenData + + owned assetOwner(*): RGBContract.Allocation + public inflationAllowance(+): RGB21.ItemsCount + public updateRight: Rights + + error fractionOverflow + "the amount of token fractions in outputs exceeds 1" + error invalidAttachmentType + "attachment has a type which is not allowed for the token" + error issueExceedsAllowance + "you try to issue more assets than allowed by the contract terms" + error nonEngravableToken + "attempt to engrave on a token which prohibit engraving" + error nonEqualValues + "the sum of spent token fractions doesn't equal to the sum of token fractions in outputs" + error nonFractionalToken + "attempt to transfer a fraction of non-fractionable token" + error unknownToken + "allocation of unknown token ID" + + genesis: abstract + errors: fractionOverflow, invalidAttachmentType, unknownToken + globals: attachmentTypes(*), spec, terms, tokens(*) + assigns: assetOwner(*), inflationAllowance(+), updateRight + + transition engrave: required, final + errors: fractionOverflow, nonEngravableToken, nonEqualValues, nonFractionalToken, unknownToken + globals: engravings + assigns: assetOwner(+) + default: assetOwner + inputs: assetOwner(+) + + transition issue: required, abstract + errors: fractionOverflow, invalidAttachmentType, issueExceedsAllowance, unknownToken + globals: attachmentTypes(*), tokens(*) + assigns: assetOwner(*), inflationAllowance(*) + default: assetOwner + inputs: inflationAllowance(+) + + transition rename: required, final + globals: spec + assigns: updateRight(?) + default: updateRight + inputs: updateRight + + transition transfer: required, default, final + errors: fractionOverflow, nonEqualValues, nonFractionalToken, unknownToken + assigns: assetOwner(+) + default: assetOwner + inputs: assetOwner(+) + + diff --git a/src/main.rs b/src/main.rs index f60150a..7234ba0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -180,5 +180,21 @@ fn main() -> io::Result<()> { writeln!(file, "{}", iface.display(&map, &ifsys)).unwrap(); } + let mut ifaces = vec![rgb21::iface::rgb21_base(), rgb21::iface::rgb21_renamable()]; + ifaces.extend(rgb21::Features::ENUMERATE.iter().copied().map(Rgb21::iface)); + + map.extend( + ifaces + .iter() + .map(|iface| (iface.iface_id(), iface.name.clone())), + ); + + filename.pop(); + filename.push("RGB21.con"); + let mut file = fs::File::create(&filename).unwrap(); + for iface in ifaces { + writeln!(file, "{}", iface.display(&map, &ifsys)).unwrap(); + } + Ok(()) } diff --git a/src/rgb21/iface.rs b/src/rgb21/iface.rs index 77a9f96..e7eb7c4 100644 --- a/src/rgb21/iface.rs +++ b/src/rgb21/iface.rs @@ -26,9 +26,15 @@ use rgbstd::interface::{ use rgbstd::stl::StandardTypes; use rgbstd::{Identity, Occurrences}; +use crate::rgb20::iface::{named_asset, renameable}; use crate::rgb21::wrapper::Rgb21; use crate::LNPBP_IDENTITY; +pub fn rgb21_base() -> Iface { named_asset().expect_extended(nft(), tn!("RGB21Base")) } +pub fn rgb21_renamable() -> Iface { + rgb21_base().expect_extended(renameable(), tn!("RGB21Renamable")) +} + pub fn nft() -> Iface { let types = StandardTypes::with(Rgb21::stl()); Iface { diff --git a/src/rgb21/issuer.rs b/src/rgb21/issuer.rs new file mode 100644 index 0000000..2e98003 --- /dev/null +++ b/src/rgb21/issuer.rs @@ -0,0 +1,203 @@ +use std::str::FromStr; + +use amplify::confinement::SmallBlob; +use amplify::Wrapper; +use bp::dbc::Method; +use rgbstd::containers::ValidContract; +use rgbstd::interface::{BuilderError, ContractBuilder, IfaceClass, TxOutpoint}; +use rgbstd::invoice::{Allocation, Precision}; +use rgbstd::stl::{AssetSpec, Attachment, ContractTerms, MediaType, RicardianContract}; +use rgbstd::{AltLayer1, AssetTag, GenesisSeal, Identity, TokenIndex}; +use strict_encoding::InvalidRString; + +use super::{EmbeddedMedia, Rgb21, TokenData}; +use crate::{IssuerWrapper, SchemaIssuer}; + +#[derive(Clone, Eq, PartialEq, Hash, Debug, Display, Error)] +#[display(doc_comments)] +pub enum IssuerError { + /// contract genesis doesn't support allocating to liquid seals; request + /// liquid support first. + NoLiquidSupport, + /// the amount of token fractions in outputs exceeds 1. + FractionOverflow, + /// attachment has a type which is not allowed for the token + InvalidAttachmentType, + /// allocation of unknown token ID {0} + UnknownToken, +} + +impl From for IssuerError { + fn from(err: BuilderError) -> Self { + match err { + BuilderError::InvalidLayer1(_) => IssuerError::NoLiquidSupport, + err => panic!("invalid RGB21 schema. Details: {err}"), + } + } +} + +#[derive(Clone, Debug)] +pub struct Rgb21PrimaryIssue { + builder: ContractBuilder, + terms: ContractTerms, +} + +impl Rgb21PrimaryIssue { + pub fn testnet_with( + issuer: SchemaIssuer, + by: &str, + ticker: &str, + name: &str, + details: Option<&str>, + precision: Precision, + ) -> Result { + Self::testnet_int(issuer, by, ticker, name, details, precision, false) + } + + pub fn testnet>( + by: &str, + ticker: &str, + name: &str, + details: Option<&str>, + precision: Precision, + ) -> Result { + Self::testnet_int(C::issuer(), by, ticker, name, details, precision, false) + } + + pub fn testnet_det>( + by: &str, + ticker: &str, + name: &str, + details: Option<&str>, + precision: Precision, + asset_tag: AssetTag, + ) -> Result { + let mut me = Self::testnet_int(C::issuer(), by, ticker, name, details, precision, true)?; + me.builder = me + .builder + .add_asset_tag("assetOwner", asset_tag) + .expect("invalid RGB20 schema (assetOwner mismatch)"); + Ok(me) + } + fn testnet_int( + issuer: SchemaIssuer, + by: &str, + ticker: &str, + name: &str, + details: Option<&str>, + precision: Precision, + deterministic: bool, + ) -> Result { + let spec = AssetSpec::with(ticker, name, precision, details)?; + let terms = ContractTerms { + text: RicardianContract::default(), + media: None, + }; + + let (schema, main_iface_impl, types, scripts, features) = issuer.into_split(); + let mut builder = match deterministic { + false => ContractBuilder::with( + Identity::from_str(by).expect("invalid issuer identity string"), + Rgb21::iface(features), + schema, + main_iface_impl, + types, + scripts, + ), + true => ContractBuilder::deterministic( + Identity::from_str(by).expect("invalid issuer identity string"), + Rgb21::iface(features), + schema, + main_iface_impl, + types, + scripts, + ), + }; + + builder = builder + .add_global_state("spec", spec) + .expect("invalid RGB21 schema (token specification mismatch)"); + + Ok(Self { builder, terms }) + } + + pub fn support_liquid(mut self) -> Self { + self.builder = self + .builder + .add_layer1(AltLayer1::Liquid) + .expect("only one layer1 can be added"); + self + } + + pub fn add_terms( + mut self, + contract: &str, + media: Option, + ) -> Result { + let terms = RicardianContract::from_str(contract)?; + self.terms = ContractTerms { text: terms, media }; + Ok(self) + } + + pub fn allocate( + mut self, + method: Method, + beneficiary: O, + allocation: impl Into, + ) -> Result { + let allocation = allocation.into(); + let beneficiary = beneficiary.map_to_xchain(|outpoint| { + GenesisSeal::new_random(method, outpoint.txid, outpoint.vout) + }); + self.builder = self + .builder + .add_data("assetOwner", beneficiary, allocation)?; + Ok(self) + } + + pub fn allocate_all( + mut self, + method: Method, + allocations: impl IntoIterator)>, + ) -> Result { + for (beneficiary, allocation) in allocations { + self = self.allocate(method, beneficiary, allocation)?; + } + Ok(self) + } + + #[allow(clippy::result_large_err)] + pub fn issue_contract( + self, + token_index: u32, + image_url: &str, + ) -> Result { + Ok(self + .pre_issue_contract(token_index, image_url)? + .issue_contract()?) + } + + #[allow(clippy::result_large_err)] + fn pre_issue_contract( + self, + token_index: u32, + image_url: &str, + ) -> Result { + let index = TokenIndex::from_inner(token_index); + let image_bytes = image_url.as_bytes().to_vec(); + let preview = EmbeddedMedia { + ty: MediaType::with("image/*"), + data: SmallBlob::try_from_iter(image_bytes).expect("invalid data"), + }; + let token_data = TokenData { + index, + preview: Some(preview), + ..Default::default() + }; + + Ok(self + .builder + .add_global_state("token", token_data)? + .add_global_state("terms", self.terms)?) + } +} diff --git a/src/rgb21/mod.rs b/src/rgb21/mod.rs index 0959662..d4673c0 100644 --- a/src/rgb21/mod.rs +++ b/src/rgb21/mod.rs @@ -22,7 +22,7 @@ pub mod iface; mod types; mod wrapper; - +mod issuer; use amplify::confinement::Confined; use rgbstd::info::FeatureList; pub use types::{ diff --git a/src/rgb21/wrapper.rs b/src/rgb21/wrapper.rs index aa73815..d461805 100644 --- a/src/rgb21/wrapper.rs +++ b/src/rgb21/wrapper.rs @@ -20,19 +20,22 @@ // limitations under the License. use rgbstd::interface::{ - ContractIface, DataAllocation, Iface, IfaceClass, IfaceId, OutpointFilter, + ContractIface, DataAllocation, Iface, IfaceClass, IfaceId, OutpointFilter, RightsAllocation, }; use rgbstd::stl::{bp_tx_stl, rgb_contract_stl, AssetSpec, ContractTerms}; -use rgbstd::Allocation; +use rgbstd::{Allocation, AssetTag, Precision}; +use strict_encoding::InvalidRString; use strict_types::stl::std_stl; use strict_types::{CompileError, LibBuilder, TypeLib}; use super::iface::*; +use super::issuer::Rgb21PrimaryIssue; use super::{ AttachmentType, EngravingData, Features, Issues, ItemsCount, TokenData, LIB_NAME_RGB21, }; use crate::rgb20::iface::{named_asset, renameable}; use crate::rgb20::Rgb20Info; +use crate::IssuerWrapper; pub const RGB21_UNIQUE_IFACE_ID: IfaceId = IfaceId::from_array([ 0xcd, 0xa8, 0x94, 0x87, 0x6e, 0xc5, 0xd9, 0xc6, 0x16, 0x7d, 0xc7, 0x45, 0x7c, 0xbe, 0x65, 0x05, @@ -114,6 +117,27 @@ impl IfaceClass for Rgb21 { } impl Rgb21 { + pub fn testnet>( + issuer: &str, + ticker: &str, + name: &str, + details: Option<&str>, + precision: Precision, + ) -> Result { + Rgb21PrimaryIssue::testnet::(issuer, ticker, name, details, precision) + } + + pub fn testnet_det>( + issuer: &str, + ticker: &str, + name: &str, + details: Option<&str>, + precision: Precision, + asset_tag: AssetTag, + ) -> Result { + Rgb21PrimaryIssue::testnet_det::(issuer, ticker, name, details, precision, asset_tag) + } + pub fn spec(&self) -> AssetSpec { let strict_val = &self .0 @@ -146,6 +170,15 @@ impl Rgb21 { EngravingData::from_strict_val_unchecked(strict_val) } + pub fn update_right<'c>( + &'c self, + filter: impl OutpointFilter + 'c, + ) -> impl Iterator + 'c { + self.0 + .rights("updateRight", filter) + .expect("RGB21 interface requires `updateRight` state") + } + pub fn allocations<'c>( &'c self, filter: impl OutpointFilter + 'c, @@ -154,4 +187,46 @@ impl Rgb21 { .data("assetOwner", filter) .expect("RGB21 interface requires `assetOwner` state") } + + pub fn features(&self) -> Features { + let renaming = self + .0 + .iface + .transitions + .iter() + .any(|field| field.name.as_str() == "rename"); + + let engraving = self + .0 + .iface + .transitions + .iter() + .any(|field| field.name.as_str() == "engrave"); + let inflatable = self + .0 + .iface + .transitions + .iter() + .any(|field| field.name.as_str() == "issue"); + let issues = match inflatable { + true => Issues::MultiIssue, + false => Issues::Unique, + _ => Issues::Limited, + }; + + Features { + renaming, + engraving, + issues, + } + } + + pub fn inflation_allowance_allocations<'c>( + &'c self, + filter: impl OutpointFilter + 'c, + ) -> impl Iterator + 'c { + self.0 + .data("inflationAllowance", filter) + .expect("RGB21 interface requires `inflationAllowance` state") + } }