From 1cb98df639a6bb854c230ec139d0cf840d885870 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Sun, 5 Nov 2023 18:07:36 +0100 Subject: [PATCH 1/3] Add GovernorMetadataFeature --- .../nodejs/lib/types/block/output/output.ts | 7 - bindings/python/iota_sdk/types/output.py | 3 - sdk/src/types/block/error.rs | 10 +- sdk/src/types/block/output/anchor.rs | 66 +-- .../block/output/feature/block_issuer.rs | 2 +- .../block/output/feature/governor_metadata.rs | 470 ++++++++++++++++++ sdk/src/types/block/output/feature/mod.rs | 27 +- sdk/src/types/block/output/feature/staking.rs | 2 +- sdk/src/types/block/output/feature/tag.rs | 2 +- sdk/src/types/block/output/mod.rs | 3 +- sdk/src/types/block/rand/output/feature.rs | 9 +- .../transaction/high_level/create_account.rs | 1 - 12 files changed, 525 insertions(+), 77 deletions(-) create mode 100644 sdk/src/types/block/output/feature/governor_metadata.rs diff --git a/bindings/nodejs/lib/types/block/output/output.ts b/bindings/nodejs/lib/types/block/output/output.ts index fb2fe10cb1..8d54898472 100644 --- a/bindings/nodejs/lib/types/block/output/output.ts +++ b/bindings/nodejs/lib/types/block/output/output.ts @@ -243,10 +243,6 @@ class AnchorOutput extends ImmutableFeaturesOutput { * The amount of (stored) Mana held by the output. */ readonly mana: u64; - /** - * Metadata that can only be changed by the state controller. - */ - readonly stateMetadata?: HexEncodedString; /** * @param amount The amount of the output. @@ -254,7 +250,6 @@ class AnchorOutput extends ImmutableFeaturesOutput { * @param anchorId The anchor ID as hex-encoded string. * @param stateIndex A counter that must increase by 1 every time the anchor output is state transitioned. * @param unlockConditions The unlock conditions of the output. - * @param stateMetadata Metadata that can only be changed by the state controller. */ constructor( amount: u64, @@ -262,13 +257,11 @@ class AnchorOutput extends ImmutableFeaturesOutput { anchorId: AnchorId, stateIndex: number, unlockConditions: UnlockCondition[], - stateMetadata?: HexEncodedString, ) { super(OutputType.Account, amount, unlockConditions); this.anchorId = anchorId; this.stateIndex = stateIndex; this.mana = mana; - this.stateMetadata = stateMetadata; } } diff --git a/bindings/python/iota_sdk/types/output.py b/bindings/python/iota_sdk/types/output.py index 92769c780a..f9272c282a 100644 --- a/bindings/python/iota_sdk/types/output.py +++ b/bindings/python/iota_sdk/types/output.py @@ -145,8 +145,6 @@ class AnchorOutput: Features that add utility to the output but do not impose unlocking conditions. immutable_features : Features that add utility to the output but do not impose unlocking conditions. These features need to be kept in future transitions of the UTXO state machine. - state_metadata : - Metadata that can only be changed by the state controller. native_tokens : Native tokens added to the new output. type : @@ -175,7 +173,6 @@ class AnchorOutput: metadata=config( decoder=deserialize_features )) - state_metadata: Optional[HexStr] = None native_tokens: Optional[List[NativeToken]] = None type: int = field( default_factory=lambda: int( diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index 33b23839be..5c6618ccc3 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -17,8 +17,8 @@ use crate::types::block::{ output::{ feature::{BlockIssuerKeyCount, FeatureCount}, unlock_condition::UnlockConditionCount, - AccountId, AnchorId, ChainId, MetadataFeatureLength, NativeTokenCount, NftId, OutputIndex, StateMetadataLength, - TagFeatureLength, + AccountId, AnchorId, ChainId, GovernorMetadataFeatureLength, MetadataFeatureLength, NativeTokenCount, NftId, + OutputIndex, TagFeatureLength, }, payload::{ContextInputCount, InputCount, OutputCount, TagLength, TaggedDataLength}, protocol::ProtocolParametersHash, @@ -86,9 +86,9 @@ pub enum Error { InvalidBech32Hrp(Bech32HrpError), InvalidCapabilitiesCount(>::Error), InvalidSignedBlockLength(usize), - InvalidStateMetadataLength(>::Error), InvalidManaValue(u64), InvalidMetadataFeatureLength(>::Error), + InvalidGovernorMetadataFeatureLength(>::Error), InvalidNativeTokenCount(>::Error), InvalidNetworkName(FromUtf8Error), InvalidManaDecayFactors, @@ -265,11 +265,13 @@ impl fmt::Display for Error { Self::InvalidInputCount(count) => write!(f, "invalid input count: {count}"), Self::InvalidInputOutputIndex(index) => write!(f, "invalid input or output index: {index}"), Self::InvalidSignedBlockLength(length) => write!(f, "invalid signed block length {length}"), - Self::InvalidStateMetadataLength(length) => write!(f, "invalid state metadata length: {length}"), Self::InvalidManaValue(mana) => write!(f, "invalid mana value: {mana}"), Self::InvalidMetadataFeatureLength(length) => { write!(f, "invalid metadata feature length: {length}") } + Self::InvalidGovernorMetadataFeatureLength(length) => { + write!(f, "invalid governor metadata feature length: {length}") + } Self::InvalidNativeTokenCount(count) => write!(f, "invalid native token count: {count}"), Self::InvalidNetworkName(err) => write!(f, "invalid network name: {err}"), Self::InvalidManaDecayFactors => write!(f, "invalid mana decay factors"), diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index c54fd11901..b8effd860c 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -5,10 +5,8 @@ use alloc::{collections::BTreeSet, vec::Vec}; use hashbrown::HashMap; use packable::{ - bounded::BoundedU16, error::{UnpackError, UnpackErrorExt}, packer::Packer, - prefix::BoxedSlicePrefix, unpacker::Unpacker, Packable, }; @@ -100,7 +98,6 @@ pub struct AnchorOutputBuilder { native_tokens: BTreeSet, anchor_id: AnchorId, state_index: Option, - state_metadata: Vec, unlock_conditions: BTreeSet, features: BTreeSet, immutable_features: BTreeSet, @@ -125,7 +122,6 @@ impl AnchorOutputBuilder { native_tokens: BTreeSet::new(), anchor_id, state_index: None, - state_metadata: Vec::new(), unlock_conditions: BTreeSet::new(), features: BTreeSet::new(), immutable_features: BTreeSet::new(), @@ -181,13 +177,6 @@ impl AnchorOutputBuilder { self } - /// - #[inline(always)] - pub fn with_state_metadata(mut self, state_metadata: impl Into>) -> Self { - self.state_metadata = state_metadata.into(); - self - } - /// Adds an [`UnlockCondition`] to the builder, if one does not already exist of that type. #[inline(always)] pub fn add_unlock_condition(mut self, unlock_condition: impl Into) -> Self { @@ -276,12 +265,6 @@ impl AnchorOutputBuilder { pub fn finish(self) -> Result { let state_index = self.state_index.unwrap_or(0); - let state_metadata = self - .state_metadata - .into_boxed_slice() - .try_into() - .map_err(Error::InvalidStateMetadataLength)?; - verify_index_counter(&self.anchor_id, state_index)?; let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?; @@ -302,7 +285,6 @@ impl AnchorOutputBuilder { native_tokens: NativeTokens::from_set(self.native_tokens)?, anchor_id: self.anchor_id, state_index, - state_metadata, unlock_conditions, features, immutable_features, @@ -345,7 +327,6 @@ impl From<&AnchorOutput> for AnchorOutputBuilder { native_tokens: output.native_tokens.iter().copied().collect(), anchor_id: output.anchor_id, state_index: Some(output.state_index), - state_metadata: output.state_metadata.to_vec(), unlock_conditions: output.unlock_conditions.iter().cloned().collect(), features: output.features.iter().cloned().collect(), immutable_features: output.immutable_features.iter().cloned().collect(), @@ -353,8 +334,6 @@ impl From<&AnchorOutput> for AnchorOutputBuilder { } } -pub(crate) type StateMetadataLength = BoundedU16<0, { AnchorOutput::STATE_METADATA_LENGTH_MAX }>; - /// Describes an anchor in the ledger that can be controlled by the state and governance controllers. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct AnchorOutput { @@ -367,8 +346,6 @@ pub struct AnchorOutput { anchor_id: AnchorId, /// A counter that must increase by 1 every time the anchor is state transitioned. state_index: u32, - /// Metadata that can only be changed by the state controller. - state_metadata: BoxedSlicePrefix, /// Define how the output can be unlocked in a transaction. unlock_conditions: UnlockConditions, /// Features of the output. @@ -380,8 +357,6 @@ pub struct AnchorOutput { impl AnchorOutput { /// The [`Output`](crate::types::block::output::Output) kind of an [`AnchorOutput`]. pub const KIND: u8 = 2; - /// Maximum possible length in bytes of the state metadata. - pub const STATE_METADATA_LENGTH_MAX: u16 = 8192; /// The set of allowed [`UnlockCondition`]s for an [`AnchorOutput`]. pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::STATE_CONTROLLER_ADDRESS.union(UnlockConditionFlags::GOVERNOR_ADDRESS); @@ -441,12 +416,6 @@ impl AnchorOutput { self.state_index } - /// - #[inline(always)] - pub fn state_metadata(&self) -> &[u8] { - &self.state_metadata - } - /// #[inline(always)] pub fn unlock_conditions(&self) -> &UnlockConditions { @@ -485,6 +454,16 @@ impl AnchorOutput { .unwrap() } + // /// Returns the governor metadata of the [`AnchorOutput`]. + // #[inline(always)] + // pub fn governor_metadata(&self) -> &[u8] { + // // An AnchorOutput must have a GovernorAddressUnlockCondition. + // self.unlock_conditions + // .governor_address() + // .map(|unlock_condition| unlock_condition.address()) + // .unwrap() + // } + /// #[inline(always)] pub fn chain_id(&self) -> ChainId { @@ -552,9 +531,8 @@ impl AnchorOutput { } } else if next_state.state_index == current_state.state_index { // Governance transition. - if current_state.amount != next_state.amount - || current_state.native_tokens != next_state.native_tokens - || current_state.state_metadata != next_state.state_metadata + if current_state.amount != next_state.amount || current_state.native_tokens != next_state.native_tokens + // || current_state.state_metadata != next_state.state_metadata { return Err(StateTransitionError::MutatedFieldWithoutRights); } @@ -619,7 +597,6 @@ impl Packable for AnchorOutput { self.native_tokens.pack(packer)?; self.anchor_id.pack(packer)?; self.state_index.pack(packer)?; - self.state_metadata.pack(packer)?; self.unlock_conditions.pack(packer)?; self.features.pack(packer)?; self.immutable_features.pack(packer)?; @@ -640,8 +617,6 @@ impl Packable for AnchorOutput { let native_tokens = NativeTokens::unpack::<_, VERIFY>(unpacker, &())?; let anchor_id = AnchorId::unpack::<_, VERIFY>(unpacker, &()).coerce()?; let state_index = u32::unpack::<_, VERIFY>(unpacker, &()).coerce()?; - let state_metadata = BoxedSlicePrefix::::unpack::<_, VERIFY>(unpacker, &()) - .map_packable_err(|err| Error::InvalidStateMetadataLength(err.into_prefix_err().into()))?; if VERIFY { verify_index_counter(&anchor_id, state_index).map_err(UnpackError::Packable)?; @@ -672,7 +647,6 @@ impl Packable for AnchorOutput { native_tokens, anchor_id, state_index, - state_metadata, unlock_conditions, features, immutable_features, @@ -715,7 +689,6 @@ fn verify_unlock_conditions(unlock_conditions: &UnlockConditions, anchor_id: &An #[cfg(feature = "serde")] pub(crate) mod dto { - use alloc::boxed::Box; use serde::{Deserialize, Serialize}; @@ -725,7 +698,7 @@ pub(crate) mod dto { block::{output::unlock_condition::dto::UnlockConditionDto, Error}, TryFromDto, }, - utils::serde::{prefix_hex_bytes, string}, + utils::serde::string, }; /// Describes an anchor in the ledger that can be controlled by the state and governance controllers. @@ -742,8 +715,6 @@ pub(crate) mod dto { pub native_tokens: Vec, pub anchor_id: AnchorId, pub state_index: u32, - #[serde(skip_serializing_if = "<[_]>::is_empty", default, with = "prefix_hex_bytes")] - pub state_metadata: Box<[u8]>, pub unlock_conditions: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub features: Vec, @@ -760,7 +731,6 @@ pub(crate) mod dto { native_tokens: value.native_tokens().to_vec(), anchor_id: *value.anchor_id(), state_index: value.state_index(), - state_metadata: value.state_metadata().into(), unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(), features: value.features().to_vec(), immutable_features: value.immutable_features().to_vec(), @@ -778,8 +748,7 @@ pub(crate) mod dto { .with_state_index(dto.state_index) .with_native_tokens(dto.native_tokens) .with_features(dto.features) - .with_immutable_features(dto.immutable_features) - .with_state_metadata(dto.state_metadata); + .with_immutable_features(dto.immutable_features); for u in dto.unlock_conditions { builder = builder.add_unlock_condition(UnlockCondition::try_from_dto_with_params(u, ¶ms)?); @@ -797,7 +766,6 @@ pub(crate) mod dto { native_tokens: Option>, anchor_id: &AnchorId, state_index: Option, - state_metadata: Option>, unlock_conditions: Vec, features: Option>, immutable_features: Option>, @@ -820,10 +788,6 @@ pub(crate) mod dto { builder = builder.with_state_index(state_index); } - if let Some(state_metadata) = state_metadata { - builder = builder.with_state_metadata(state_metadata); - } - let unlock_conditions = unlock_conditions .into_iter() .map(|u| UnlockCondition::try_from_dto_with_params(u, ¶ms)) @@ -878,7 +842,6 @@ mod tests { Some(output.native_tokens().to_vec()), output.anchor_id(), output.state_index().into(), - output.state_metadata().to_owned().into(), output.unlock_conditions().iter().map(Into::into).collect(), Some(output.features().to_vec()), Some(output.immutable_features().to_vec()), @@ -898,7 +861,6 @@ mod tests { Some(builder.native_tokens.iter().copied().collect()), &builder.anchor_id, builder.state_index, - builder.state_metadata.to_owned().into(), builder.unlock_conditions.iter().map(Into::into).collect(), Some(builder.features.iter().cloned().collect()), Some(builder.immutable_features.iter().cloned().collect()), diff --git a/sdk/src/types/block/output/feature/block_issuer.rs b/sdk/src/types/block/output/feature/block_issuer.rs index dde9271ab8..900085ed5e 100644 --- a/sdk/src/types/block/output/feature/block_issuer.rs +++ b/sdk/src/types/block/output/feature/block_issuer.rs @@ -184,7 +184,7 @@ pub struct BlockIssuerFeature { impl BlockIssuerFeature { /// The [`Feature`](crate::types::block::output::Feature) kind of a [`BlockIssuerFeature`]. - pub const KIND: u8 = 4; + pub const KIND: u8 = 5; /// Creates a new [`BlockIssuerFeature`]. #[inline(always)] diff --git a/sdk/src/types/block/output/feature/governor_metadata.rs b/sdk/src/types/block/output/feature/governor_metadata.rs new file mode 100644 index 0000000000..b7995e2c64 --- /dev/null +++ b/sdk/src/types/block/output/feature/governor_metadata.rs @@ -0,0 +1,470 @@ +// Copyright 2021-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use alloc::{boxed::Box, string::String, vec::Vec}; +use core::{ops::RangeInclusive, str::FromStr}; + +use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix}; + +use crate::types::block::Error; + +pub(crate) type GovernorMetadataFeatureLength = + BoundedU16<{ *GovernorMetadataFeature::LENGTH_RANGE.start() }, { *GovernorMetadataFeature::LENGTH_RANGE.end() }>; + +/// Defines metadata, arbitrary binary data, that will be stored in the output. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, packable::Packable)] +#[packable(unpack_error = Error, with = |err| Error::InvalidGovernorMetadataFeatureLength(err.into_prefix_err().into()))] +pub struct GovernorMetadataFeature( + // Binary data. + pub(crate) BoxedSlicePrefix, +); + +macro_rules! impl_from_vec { + ($type:ty) => { + impl TryFrom<$type> for GovernorMetadataFeature { + type Error = Error; + + fn try_from(value: $type) -> Result { + Vec::::from(value).try_into() + } + } + }; +} +impl_from_vec!(&str); +impl_from_vec!(String); +impl_from_vec!(&[u8]); + +impl TryFrom<[u8; N]> for GovernorMetadataFeature { + type Error = Error; + + fn try_from(value: [u8; N]) -> Result { + value.to_vec().try_into() + } +} + +impl TryFrom> for GovernorMetadataFeature { + type Error = Error; + + fn try_from(data: Vec) -> Result { + data.into_boxed_slice().try_into() + } +} + +impl TryFrom> for GovernorMetadataFeature { + type Error = Error; + + fn try_from(data: Box<[u8]>) -> Result { + data.try_into() + .map(Self) + .map_err(Error::InvalidGovernorMetadataFeatureLength) + } +} + +impl FromStr for GovernorMetadataFeature { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::new(prefix_hex::decode::>(s).map_err(Error::Hex)?) + } +} + +impl GovernorMetadataFeature { + /// The [`Feature`](crate::types::block::output::Feature) kind of [`GovernorMetadataFeature`]. + pub const KIND: u8 = 3; + /// Valid lengths for a [`GovernorMetadataFeature`]. + pub const LENGTH_RANGE: RangeInclusive = 1..=8192; + + /// Creates a new [`GovernorMetadataFeature`]. + #[inline(always)] + pub fn new(data: impl Into>) -> Result { + Self::try_from(data.into()) + } + + /// Returns the data. + #[inline(always)] + pub fn data(&self) -> &[u8] { + &self.0 + } +} + +impl core::fmt::Display for GovernorMetadataFeature { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", prefix_hex::encode(self.data())) + } +} + +impl core::fmt::Debug for GovernorMetadataFeature { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "GovernorMetadataFeature({self})") + } +} + +#[cfg(feature = "irc_27")] +pub(crate) mod irc_27 { + use alloc::{ + borrow::ToOwned, + collections::{BTreeMap, BTreeSet}, + string::String, + }; + + use getset::Getters; + use serde::{Deserialize, Serialize}; + use url::Url; + + use super::*; + use crate::types::block::address::Bech32Address; + + /// The IRC27 NFT standard schema. + #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq)] + #[serde(rename_all = "camelCase")] + #[serde(tag = "standard", rename = "IRC27")] + #[getset(get = "pub")] + pub struct Irc27Metadata { + version: String, + /// The media type (MIME) of the asset. + /// + /// ## Examples + /// - Image files: `image/jpeg`, `image/png`, `image/gif`, etc. + /// - Video files: `video/x-msvideo` (avi), `video/mp4`, `video/mpeg`, etc. + /// - Audio files: `audio/mpeg`, `audio/wav`, etc. + /// - 3D Assets: `model/obj`, `model/u3d`, etc. + /// - Documents: `application/pdf`, `text/plain`, etc. + #[serde(rename = "type")] + media_type: String, + /// URL pointing to the NFT file location. + uri: Url, + /// The human-readable name of the native token. + name: String, + /// The human-readable collection name of the native token. + #[serde(default, skip_serializing_if = "Option::is_none")] + collection_name: Option, + /// Royalty payment addresses mapped to the payout percentage. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + royalties: BTreeMap, + /// The human-readable name of the native token creator. + #[serde(default, skip_serializing_if = "Option::is_none")] + issuer_name: Option, + /// The human-readable description of the token. + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + /// Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + attributes: BTreeSet, + } + + impl Irc27Metadata { + pub fn new(media_type: impl Into, uri: Url, name: impl Into) -> Self { + Self { + version: "v1.0".to_owned(), + media_type: media_type.into(), + uri, + name: name.into(), + collection_name: Default::default(), + royalties: Default::default(), + issuer_name: Default::default(), + description: Default::default(), + attributes: Default::default(), + } + } + + pub fn with_collection_name(mut self, collection_name: impl Into) -> Self { + self.collection_name.replace(collection_name.into()); + self + } + + pub fn add_royalty(mut self, address: Bech32Address, percentage: f64) -> Self { + self.royalties.insert(address, percentage); + self + } + + pub fn with_royalties(mut self, royalties: BTreeMap) -> Self { + self.royalties = royalties; + self + } + + pub fn with_issuer_name(mut self, issuer_name: impl Into) -> Self { + self.issuer_name.replace(issuer_name.into()); + self + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description.replace(description.into()); + self + } + + pub fn add_attribute(mut self, attribute: Attribute) -> Self { + self.attributes.insert(attribute); + self + } + + pub fn with_attributes(mut self, attributes: BTreeSet) -> Self { + self.attributes = attributes; + self + } + + pub fn to_bytes(&self) -> Vec { + // Unwrap: Safe because this struct is known to be valid + serde_json::to_string(self).unwrap().into_bytes() + } + } + + impl TryFrom for GovernorMetadataFeature { + type Error = Error; + fn try_from(value: Irc27Metadata) -> Result { + Self::new(value.to_bytes()) + } + } + + impl From for Vec { + fn from(value: Irc27Metadata) -> Self { + value.to_bytes() + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq, Eq)] + #[getset(get = "pub")] + pub struct Attribute { + trait_type: String, + value: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + display_type: Option, + } + + impl Attribute { + pub fn new(trait_type: impl Into, value: impl Into) -> Self { + Self { + trait_type: trait_type.into(), + display_type: None, + value: value.into(), + } + } + + pub fn with_display_type(mut self, display_type: impl Into) -> Self { + self.display_type.replace(display_type.into()); + self + } + } + + impl Ord for Attribute { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.trait_type.cmp(&other.trait_type) + } + } + impl PartialOrd for Attribute { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + impl core::hash::Hash for Attribute { + fn hash(&self, state: &mut H) { + self.trait_type.hash(state); + } + } + + #[cfg(test)] + mod test { + use pretty_assertions::assert_eq; + + use super::*; + use crate::types::block::{address::ToBech32Ext, rand::address::rand_address}; + + #[test] + fn serialization() { + let metadata = Irc27Metadata::new( + "image/jpeg", + "https://mywebsite.com/my-nft-files-1.jpeg".parse().unwrap(), + "My NFT #0001", + ) + .with_collection_name("My Collection of Art") + .add_royalty(rand_address().to_bech32_unchecked("iota1"), 0.025) + .add_royalty(rand_address().to_bech32_unchecked("iota1"), 0.025) + .with_issuer_name("My Artist Name") + .with_description("A little information about my NFT collection") + .add_attribute(Attribute::new("Background", "Purple")) + .add_attribute(Attribute::new("Element", "Water")) + .add_attribute(Attribute::new("Attack", 150)) + .add_attribute(Attribute::new("Health", 500)); + let json = serde_json::json!( + { + "standard": "IRC27", + "version": metadata.version(), + "type": metadata.media_type(), + "uri": metadata.uri(), + "name": metadata.name(), + "collectionName": metadata.collection_name(), + "royalties": metadata.royalties(), + "issuerName": metadata.issuer_name(), + "description": metadata.description(), + "attributes": metadata.attributes() + } + ); + let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); + + assert_eq!(metadata, metadata_deser); + assert_eq!(json, serde_json::to_value(metadata).unwrap()) + } + } +} + +#[cfg(feature = "irc_30")] +pub(crate) mod irc_30 { + use alloc::string::String; + + use getset::Getters; + use serde::{Deserialize, Serialize}; + use url::Url; + + use super::*; + + /// The IRC30 native token metadata standard schema. + #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + #[serde(tag = "standard", rename = "IRC30")] + #[getset(get = "pub")] + pub struct Irc30Metadata { + /// The human-readable name of the native token. + name: String, + /// The symbol/ticker of the token. + symbol: String, + /// Number of decimals the token uses (divide the token amount by `10^decimals` to get its user + /// representation). + decimals: u32, + /// The human-readable description of the token. + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + /// URL pointing to more resources about the token. + #[serde(default, skip_serializing_if = "Option::is_none")] + url: Option, + /// URL pointing to an image resource of the token logo. + #[serde(default, skip_serializing_if = "Option::is_none")] + logo_url: Option, + /// The svg logo of the token encoded as a byte string. + #[serde(default, skip_serializing_if = "Option::is_none")] + logo: Option, + } + + impl Irc30Metadata { + pub fn new(name: impl Into, symbol: impl Into, decimals: u32) -> Self { + Self { + name: name.into(), + symbol: symbol.into(), + decimals, + description: Default::default(), + url: Default::default(), + logo_url: Default::default(), + logo: Default::default(), + } + } + + pub fn with_description(mut self, description: impl Into) -> Self { + self.description.replace(description.into()); + self + } + + pub fn with_url(mut self, url: Url) -> Self { + self.url.replace(url); + self + } + + pub fn with_logo_url(mut self, logo_url: Url) -> Self { + self.logo_url.replace(logo_url); + self + } + + pub fn with_logo(mut self, logo: impl Into) -> Self { + self.logo.replace(logo.into()); + self + } + + pub fn to_bytes(&self) -> Vec { + // Unwrap: Safe because this struct is known to be valid + serde_json::to_string(self).unwrap().into_bytes() + } + } + + impl TryFrom for GovernorMetadataFeature { + type Error = Error; + fn try_from(value: Irc30Metadata) -> Result { + Self::new(value.to_bytes()) + } + } + + impl From for Vec { + fn from(value: Irc30Metadata) -> Self { + value.to_bytes() + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn serialization() { + let description = "FooCoin is the utility and governance token of FooLand, \ + a revolutionary protocol in the play-to-earn crypto gaming field."; + let metadata = Irc30Metadata::new("FooCoin", "FOO", 3) + .with_description(description) + .with_url("https://foocoin.io/".parse().unwrap()) + .with_logo_url( + "https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR" + .parse() + .unwrap(), + ); + let json = serde_json::json!( + { + "standard": "IRC30", + "name": metadata.name(), + "description": metadata.description(), + "decimals": metadata.decimals(), + "symbol": metadata.symbol(), + "url": metadata.url(), + "logoUrl": metadata.logo_url() + } + ); + let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); + + assert_eq!(metadata, metadata_deser); + assert_eq!(json, serde_json::to_value(metadata).unwrap()) + } + } +} + +#[cfg(feature = "serde")] +pub(crate) mod dto { + use alloc::borrow::Cow; + + use serde::{Deserialize, Serialize}; + + use super::*; + use crate::utils::serde::cow_boxed_slice_prefix_hex_bytes; + + #[derive(Serialize, Deserialize)] + struct GovernorMetadataFeatureDto<'a> { + #[serde(rename = "type")] + kind: u8, + #[serde(with = "cow_boxed_slice_prefix_hex_bytes")] + data: Cow<'a, BoxedSlicePrefix>, + } + + impl<'a> From<&'a GovernorMetadataFeature> for GovernorMetadataFeatureDto<'a> { + fn from(value: &'a GovernorMetadataFeature) -> Self { + Self { + kind: GovernorMetadataFeature::KIND, + data: Cow::Borrowed(&value.0), + } + } + } + + impl<'a> From> for GovernorMetadataFeature { + fn from(value: GovernorMetadataFeatureDto<'a>) -> Self { + Self(value.data.into_owned()) + } + } + + crate::impl_serde_typed_dto!( + GovernorMetadataFeature, + GovernorMetadataFeatureDto<'_>, + "metadata feature" + ); +} diff --git a/sdk/src/types/block/output/feature/mod.rs b/sdk/src/types/block/output/feature/mod.rs index 897db63ef9..2f515597ab 100644 --- a/sdk/src/types/block/output/feature/mod.rs +++ b/sdk/src/types/block/output/feature/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 mod block_issuer; +mod governor_metadata; mod issuer; mod metadata; mod sender; @@ -19,9 +20,13 @@ use packable::{bounded::BoundedU8, prefix::BoxedSlicePrefix, Packable}; pub use self::metadata::irc_27::{Attribute, Irc27Metadata}; #[cfg(feature = "irc_30")] pub use self::metadata::irc_30::Irc30Metadata; -pub(crate) use self::{block_issuer::BlockIssuerKeyCount, metadata::MetadataFeatureLength, tag::TagFeatureLength}; +pub(crate) use self::{ + block_issuer::BlockIssuerKeyCount, governor_metadata::GovernorMetadataFeatureLength, + metadata::MetadataFeatureLength, tag::TagFeatureLength, +}; pub use self::{ block_issuer::{BlockIssuerFeature, BlockIssuerKey, BlockIssuerKeys, Ed25519BlockIssuerKey}, + governor_metadata::GovernorMetadataFeature, issuer::IssuerFeature, metadata::MetadataFeature, sender::SenderFeature, @@ -45,6 +50,9 @@ pub enum Feature { /// A metadata feature. #[packable(tag = MetadataFeature::KIND)] Metadata(MetadataFeature), + /// A governor metadata feature. + #[packable(tag = GovernorMetadataFeature::KIND)] + GovernorMetadata(GovernorMetadataFeature), /// A tag feature. #[packable(tag = TagFeature::KIND)] Tag(TagFeature), @@ -74,6 +82,7 @@ impl core::fmt::Debug for Feature { Self::Sender(feature) => feature.fmt(f), Self::Issuer(feature) => feature.fmt(f), Self::Metadata(feature) => feature.fmt(f), + Self::GovernorMetadata(feature) => feature.fmt(f), Self::Tag(feature) => feature.fmt(f), Self::BlockIssuer(feature) => feature.fmt(f), Self::Staking(feature) => feature.fmt(f), @@ -88,6 +97,7 @@ impl Feature { Self::Sender(_) => SenderFeature::KIND, Self::Issuer(_) => IssuerFeature::KIND, Self::Metadata(_) => MetadataFeature::KIND, + Self::GovernorMetadata(_) => GovernorMetadataFeature::KIND, Self::Tag(_) => TagFeature::KIND, Self::BlockIssuer(_) => BlockIssuerFeature::KIND, Self::Staking(_) => StakingFeature::KIND, @@ -100,13 +110,14 @@ impl Feature { Self::Sender(_) => FeatureFlags::SENDER, Self::Issuer(_) => FeatureFlags::ISSUER, Self::Metadata(_) => FeatureFlags::METADATA, + Self::GovernorMetadata(_) => FeatureFlags::GOVERNOR_METADATA, Self::Tag(_) => FeatureFlags::TAG, Self::BlockIssuer(_) => FeatureFlags::BLOCK_ISSUER, Self::Staking(_) => FeatureFlags::STAKING, } } - crate::def_is_as_opt!(Feature: Sender, Issuer, Metadata, Tag, BlockIssuer, Staking); + crate::def_is_as_opt!(Feature: Sender, Issuer, Metadata, GovernorMetadata, Tag, BlockIssuer, Staking); } create_bitflags!( @@ -117,6 +128,7 @@ create_bitflags!( (SENDER, SenderFeature), (ISSUER, IssuerFeature), (METADATA, MetadataFeature), + (GOVERNOR_METADATA, GovernorMetadataFeature), (TAG, TagFeature), (BLOCK_ISSUER, BlockIssuerFeature), (STAKING, StakingFeature), @@ -158,8 +170,8 @@ impl IntoIterator for Features { } impl Features { - /// - pub const COUNT_MAX: u8 = 5; + /// Maximum number of unique features. + pub const COUNT_MAX: u8 = 7; /// Creates a new [`Features`] from a vec. pub fn from_vec(features: Vec) -> Result { @@ -209,6 +221,12 @@ impl Features { self.get(MetadataFeature::KIND).map(Feature::as_metadata) } + /// Gets a reference to a [`GovernorMetadataFeature`], if any. + pub fn governor_metadata(&self) -> Option<&GovernorMetadataFeature> { + self.get(GovernorMetadataFeature::KIND) + .map(Feature::as_governor_metadata) + } + /// Gets a reference to a [`TagFeature`], if any. pub fn tag(&self) -> Option<&TagFeature> { self.get(TagFeature::KIND).map(Feature::as_tag) @@ -261,6 +279,7 @@ mod test { FeatureFlags::SENDER, FeatureFlags::ISSUER, FeatureFlags::METADATA, + FeatureFlags::GOVERNOR_METADATA, FeatureFlags::TAG, FeatureFlags::BLOCK_ISSUER, FeatureFlags::STAKING diff --git a/sdk/src/types/block/output/feature/staking.rs b/sdk/src/types/block/output/feature/staking.rs index a94b2634fe..a19aed69b6 100644 --- a/sdk/src/types/block/output/feature/staking.rs +++ b/sdk/src/types/block/output/feature/staking.rs @@ -18,7 +18,7 @@ pub struct StakingFeature { impl StakingFeature { /// The [`Feature`](crate::types::block::output::Feature) kind of [`StakingFeature`]. - pub const KIND: u8 = 5; + pub const KIND: u8 = 6; /// Creates a new [`StakingFeature`]. pub fn new( diff --git a/sdk/src/types/block/output/feature/tag.rs b/sdk/src/types/block/output/feature/tag.rs index 1a53dcb375..4541c84b14 100644 --- a/sdk/src/types/block/output/feature/tag.rs +++ b/sdk/src/types/block/output/feature/tag.rs @@ -37,7 +37,7 @@ impl TryFrom> for TagFeature { impl TagFeature { /// The [`Feature`](crate::types::block::output::Feature) kind of an [`TagFeature`]. - pub const KIND: u8 = 3; + pub const KIND: u8 = 4; /// Valid lengths for an [`TagFeature`]. pub const LENGTH_RANGE: RangeInclusive = 1..=64; diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index d6bd867569..34c299bc9e 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -52,8 +52,7 @@ pub use self::{ unlock_condition::{UnlockCondition, UnlockConditions}, }; pub(crate) use self::{ - anchor::StateMetadataLength, - feature::{MetadataFeatureLength, TagFeatureLength}, + feature::{GovernorMetadataFeatureLength, MetadataFeatureLength, TagFeatureLength}, native_token::NativeTokenCount, output_id::OutputIndex, unlock_condition::AddressUnlockCondition, diff --git a/sdk/src/types/block/rand/output/feature.rs b/sdk/src/types/block/rand/output/feature.rs index 85ebdbb374..dd8c8dcf22 100644 --- a/sdk/src/types/block/rand/output/feature.rs +++ b/sdk/src/types/block/rand/output/feature.rs @@ -6,7 +6,7 @@ use alloc::{collections::BTreeSet, vec::Vec}; use crate::types::block::{ output::feature::{ BlockIssuerFeature, BlockIssuerKey, BlockIssuerKeys, Ed25519BlockIssuerKey, Feature, FeatureFlags, - IssuerFeature, MetadataFeature, SenderFeature, StakingFeature, TagFeature, + GovernorMetadataFeature, IssuerFeature, MetadataFeature, SenderFeature, StakingFeature, TagFeature, }, rand::{ address::rand_address, @@ -32,6 +32,12 @@ pub fn rand_metadata_feature() -> MetadataFeature { MetadataFeature::new(bytes).unwrap() } +/// Generates a random [`GovernorMetadataFeature`]. +pub fn rand_governor_metadata_feature() -> GovernorMetadataFeature { + let bytes = rand_bytes(rand_number_range(GovernorMetadataFeature::LENGTH_RANGE) as usize); + GovernorMetadataFeature::new(bytes).unwrap() +} + /// Generates a random [`TagFeature`]. pub fn rand_tag_feature() -> TagFeature { let bytes = rand_bytes(rand_number_range(TagFeature::LENGTH_RANGE) as usize); @@ -83,6 +89,7 @@ fn rand_feature_from_flag(flag: &FeatureFlags) -> Feature { FeatureFlags::SENDER => Feature::Sender(rand_sender_feature()), FeatureFlags::ISSUER => Feature::Issuer(rand_issuer_feature()), FeatureFlags::METADATA => Feature::Metadata(rand_metadata_feature()), + FeatureFlags::GOVERNOR_METADATA => Feature::GovernorMetadata(rand_governor_metadata_feature()), FeatureFlags::TAG => Feature::Tag(rand_tag_feature()), FeatureFlags::BLOCK_ISSUER => Feature::BlockIssuer(rand_block_issuer_feature()), FeatureFlags::STAKING => Feature::Staking(rand_staking_feature()), diff --git a/sdk/src/wallet/operations/transaction/high_level/create_account.rs b/sdk/src/wallet/operations/transaction/high_level/create_account.rs index 4b9b318f20..fea5c33a62 100644 --- a/sdk/src/wallet/operations/transaction/high_level/create_account.rs +++ b/sdk/src/wallet/operations/transaction/high_level/create_account.rs @@ -45,7 +45,6 @@ where /// address: None, /// immutable_metadata: Some(b"some immutable account metadata".to_vec()), /// metadata: Some(b"some account metadata".to_vec()), - /// state_metadata: Some(b"some account state metadata".to_vec()), /// }; /// /// let transaction = account.create_account_output(params, None).await?; From 7d4ccf7dc2ad4268b1882745aa0ab50c6d5ee923 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Sun, 5 Nov 2023 18:17:06 +0100 Subject: [PATCH 2/3] Remove IRCs --- .../block/output/feature/governor_metadata.rs | 337 +----------------- 1 file changed, 3 insertions(+), 334 deletions(-) diff --git a/sdk/src/types/block/output/feature/governor_metadata.rs b/sdk/src/types/block/output/feature/governor_metadata.rs index b7995e2c64..478d3f9fab 100644 --- a/sdk/src/types/block/output/feature/governor_metadata.rs +++ b/sdk/src/types/block/output/feature/governor_metadata.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2022 IOTA Stiftung +// Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use alloc::{boxed::Box, string::String, vec::Vec}; @@ -11,7 +11,7 @@ use crate::types::block::Error; pub(crate) type GovernorMetadataFeatureLength = BoundedU16<{ *GovernorMetadataFeature::LENGTH_RANGE.start() }, { *GovernorMetadataFeature::LENGTH_RANGE.end() }>; -/// Defines metadata, arbitrary binary data, that will be stored in the output. +/// Defines governor metadata, arbitrary binary data, that will be stored in the anchor output. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, packable::Packable)] #[packable(unpack_error = Error, with = |err| Error::InvalidGovernorMetadataFeatureLength(err.into_prefix_err().into()))] pub struct GovernorMetadataFeature( @@ -99,337 +99,6 @@ impl core::fmt::Debug for GovernorMetadataFeature { } } -#[cfg(feature = "irc_27")] -pub(crate) mod irc_27 { - use alloc::{ - borrow::ToOwned, - collections::{BTreeMap, BTreeSet}, - string::String, - }; - - use getset::Getters; - use serde::{Deserialize, Serialize}; - use url::Url; - - use super::*; - use crate::types::block::address::Bech32Address; - - /// The IRC27 NFT standard schema. - #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq)] - #[serde(rename_all = "camelCase")] - #[serde(tag = "standard", rename = "IRC27")] - #[getset(get = "pub")] - pub struct Irc27Metadata { - version: String, - /// The media type (MIME) of the asset. - /// - /// ## Examples - /// - Image files: `image/jpeg`, `image/png`, `image/gif`, etc. - /// - Video files: `video/x-msvideo` (avi), `video/mp4`, `video/mpeg`, etc. - /// - Audio files: `audio/mpeg`, `audio/wav`, etc. - /// - 3D Assets: `model/obj`, `model/u3d`, etc. - /// - Documents: `application/pdf`, `text/plain`, etc. - #[serde(rename = "type")] - media_type: String, - /// URL pointing to the NFT file location. - uri: Url, - /// The human-readable name of the native token. - name: String, - /// The human-readable collection name of the native token. - #[serde(default, skip_serializing_if = "Option::is_none")] - collection_name: Option, - /// Royalty payment addresses mapped to the payout percentage. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - royalties: BTreeMap, - /// The human-readable name of the native token creator. - #[serde(default, skip_serializing_if = "Option::is_none")] - issuer_name: Option, - /// The human-readable description of the token. - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, - /// Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). - #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] - attributes: BTreeSet, - } - - impl Irc27Metadata { - pub fn new(media_type: impl Into, uri: Url, name: impl Into) -> Self { - Self { - version: "v1.0".to_owned(), - media_type: media_type.into(), - uri, - name: name.into(), - collection_name: Default::default(), - royalties: Default::default(), - issuer_name: Default::default(), - description: Default::default(), - attributes: Default::default(), - } - } - - pub fn with_collection_name(mut self, collection_name: impl Into) -> Self { - self.collection_name.replace(collection_name.into()); - self - } - - pub fn add_royalty(mut self, address: Bech32Address, percentage: f64) -> Self { - self.royalties.insert(address, percentage); - self - } - - pub fn with_royalties(mut self, royalties: BTreeMap) -> Self { - self.royalties = royalties; - self - } - - pub fn with_issuer_name(mut self, issuer_name: impl Into) -> Self { - self.issuer_name.replace(issuer_name.into()); - self - } - - pub fn with_description(mut self, description: impl Into) -> Self { - self.description.replace(description.into()); - self - } - - pub fn add_attribute(mut self, attribute: Attribute) -> Self { - self.attributes.insert(attribute); - self - } - - pub fn with_attributes(mut self, attributes: BTreeSet) -> Self { - self.attributes = attributes; - self - } - - pub fn to_bytes(&self) -> Vec { - // Unwrap: Safe because this struct is known to be valid - serde_json::to_string(self).unwrap().into_bytes() - } - } - - impl TryFrom for GovernorMetadataFeature { - type Error = Error; - fn try_from(value: Irc27Metadata) -> Result { - Self::new(value.to_bytes()) - } - } - - impl From for Vec { - fn from(value: Irc27Metadata) -> Self { - value.to_bytes() - } - } - - #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq, Eq)] - #[getset(get = "pub")] - pub struct Attribute { - trait_type: String, - value: serde_json::Value, - #[serde(default, skip_serializing_if = "Option::is_none")] - display_type: Option, - } - - impl Attribute { - pub fn new(trait_type: impl Into, value: impl Into) -> Self { - Self { - trait_type: trait_type.into(), - display_type: None, - value: value.into(), - } - } - - pub fn with_display_type(mut self, display_type: impl Into) -> Self { - self.display_type.replace(display_type.into()); - self - } - } - - impl Ord for Attribute { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.trait_type.cmp(&other.trait_type) - } - } - impl PartialOrd for Attribute { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - impl core::hash::Hash for Attribute { - fn hash(&self, state: &mut H) { - self.trait_type.hash(state); - } - } - - #[cfg(test)] - mod test { - use pretty_assertions::assert_eq; - - use super::*; - use crate::types::block::{address::ToBech32Ext, rand::address::rand_address}; - - #[test] - fn serialization() { - let metadata = Irc27Metadata::new( - "image/jpeg", - "https://mywebsite.com/my-nft-files-1.jpeg".parse().unwrap(), - "My NFT #0001", - ) - .with_collection_name("My Collection of Art") - .add_royalty(rand_address().to_bech32_unchecked("iota1"), 0.025) - .add_royalty(rand_address().to_bech32_unchecked("iota1"), 0.025) - .with_issuer_name("My Artist Name") - .with_description("A little information about my NFT collection") - .add_attribute(Attribute::new("Background", "Purple")) - .add_attribute(Attribute::new("Element", "Water")) - .add_attribute(Attribute::new("Attack", 150)) - .add_attribute(Attribute::new("Health", 500)); - let json = serde_json::json!( - { - "standard": "IRC27", - "version": metadata.version(), - "type": metadata.media_type(), - "uri": metadata.uri(), - "name": metadata.name(), - "collectionName": metadata.collection_name(), - "royalties": metadata.royalties(), - "issuerName": metadata.issuer_name(), - "description": metadata.description(), - "attributes": metadata.attributes() - } - ); - let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); - - assert_eq!(metadata, metadata_deser); - assert_eq!(json, serde_json::to_value(metadata).unwrap()) - } - } -} - -#[cfg(feature = "irc_30")] -pub(crate) mod irc_30 { - use alloc::string::String; - - use getset::Getters; - use serde::{Deserialize, Serialize}; - use url::Url; - - use super::*; - - /// The IRC30 native token metadata standard schema. - #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - #[serde(tag = "standard", rename = "IRC30")] - #[getset(get = "pub")] - pub struct Irc30Metadata { - /// The human-readable name of the native token. - name: String, - /// The symbol/ticker of the token. - symbol: String, - /// Number of decimals the token uses (divide the token amount by `10^decimals` to get its user - /// representation). - decimals: u32, - /// The human-readable description of the token. - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, - /// URL pointing to more resources about the token. - #[serde(default, skip_serializing_if = "Option::is_none")] - url: Option, - /// URL pointing to an image resource of the token logo. - #[serde(default, skip_serializing_if = "Option::is_none")] - logo_url: Option, - /// The svg logo of the token encoded as a byte string. - #[serde(default, skip_serializing_if = "Option::is_none")] - logo: Option, - } - - impl Irc30Metadata { - pub fn new(name: impl Into, symbol: impl Into, decimals: u32) -> Self { - Self { - name: name.into(), - symbol: symbol.into(), - decimals, - description: Default::default(), - url: Default::default(), - logo_url: Default::default(), - logo: Default::default(), - } - } - - pub fn with_description(mut self, description: impl Into) -> Self { - self.description.replace(description.into()); - self - } - - pub fn with_url(mut self, url: Url) -> Self { - self.url.replace(url); - self - } - - pub fn with_logo_url(mut self, logo_url: Url) -> Self { - self.logo_url.replace(logo_url); - self - } - - pub fn with_logo(mut self, logo: impl Into) -> Self { - self.logo.replace(logo.into()); - self - } - - pub fn to_bytes(&self) -> Vec { - // Unwrap: Safe because this struct is known to be valid - serde_json::to_string(self).unwrap().into_bytes() - } - } - - impl TryFrom for GovernorMetadataFeature { - type Error = Error; - fn try_from(value: Irc30Metadata) -> Result { - Self::new(value.to_bytes()) - } - } - - impl From for Vec { - fn from(value: Irc30Metadata) -> Self { - value.to_bytes() - } - } - - #[cfg(test)] - mod test { - use super::*; - - #[test] - fn serialization() { - let description = "FooCoin is the utility and governance token of FooLand, \ - a revolutionary protocol in the play-to-earn crypto gaming field."; - let metadata = Irc30Metadata::new("FooCoin", "FOO", 3) - .with_description(description) - .with_url("https://foocoin.io/".parse().unwrap()) - .with_logo_url( - "https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR" - .parse() - .unwrap(), - ); - let json = serde_json::json!( - { - "standard": "IRC30", - "name": metadata.name(), - "description": metadata.description(), - "decimals": metadata.decimals(), - "symbol": metadata.symbol(), - "url": metadata.url(), - "logoUrl": metadata.logo_url() - } - ); - let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); - - assert_eq!(metadata, metadata_deser); - assert_eq!(json, serde_json::to_value(metadata).unwrap()) - } - } -} - #[cfg(feature = "serde")] pub(crate) mod dto { use alloc::borrow::Cow; @@ -465,6 +134,6 @@ pub(crate) mod dto { crate::impl_serde_typed_dto!( GovernorMetadataFeature, GovernorMetadataFeatureDto<'_>, - "metadata feature" + "governor metadata feature" ); } From 066f1a62b3bf6235b707a8fec58a864c5954ad09 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Sun, 5 Nov 2023 18:24:46 +0100 Subject: [PATCH 3/3] Add GOVERNOR_METADATA to ALLOWED_FEATURES --- sdk/src/types/block/output/anchor.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index b8effd860c..8c76ced9f7 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -48,7 +48,11 @@ impl From<&OutputId> for AnchorId { impl AnchorId { /// pub fn or_from_output_id(self, output_id: &OutputId) -> Self { - if self.is_null() { Self::from(output_id) } else { self } + if self.is_null() { + Self::from(output_id) + } else { + self + } } } @@ -361,7 +365,9 @@ impl AnchorOutput { pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::STATE_CONTROLLER_ADDRESS.union(UnlockConditionFlags::GOVERNOR_ADDRESS); /// The set of allowed [`Feature`]s for an [`AnchorOutput`]. - pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER.union(FeatureFlags::METADATA); + pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER + .union(FeatureFlags::METADATA) + .union(FeatureFlags::GOVERNOR_METADATA); /// The set of allowed immutable [`Feature`]s for an [`AnchorOutput`]. pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags = FeatureFlags::ISSUER.union(FeatureFlags::METADATA);