From e5e08f7e1b2f5d5e35ec106b75b4311e3f18a553 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Wed, 6 Sep 2023 16:02:17 -0400 Subject: [PATCH 1/8] Add IRC metadata helpers --- sdk/src/types/block/address/bech32.rs | 2 +- .../types/block/output/feature/metadata.rs | 327 ++++++++++++++++++ sdk/src/types/block/output/feature/mod.rs | 5 + 3 files changed, 333 insertions(+), 1 deletion(-) diff --git a/sdk/src/types/block/address/bech32.rs b/sdk/src/types/block/address/bech32.rs index 309e64cd7c..d23109060d 100644 --- a/sdk/src/types/block/address/bech32.rs +++ b/sdk/src/types/block/address/bech32.rs @@ -137,7 +137,7 @@ impl + Send> ConvertTo for T { } /// An address and its network type. -#[derive(Copy, Clone, Eq, PartialEq, Hash, AsRef, Deref)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, AsRef, Deref, Ord, PartialOrd)] pub struct Bech32Address { pub(crate) hrp: Hrp, #[as_ref] diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index 51fba6e8f6..80d15fff87 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -74,6 +74,333 @@ impl core::fmt::Debug for MetadataFeature { } } +#[cfg(feature = "serde")] +pub(crate) mod irc_27 { + use alloc::collections::{BTreeMap, BTreeSet}; + + 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 { + serde_json::to_string(self).unwrap().into_bytes() + } + } + + impl TryFrom for MetadataFeature { + type Error = Error; + fn try_from(value: Irc27Metadata) -> Result { + Self::new(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 super::*; + use crate::types::block::{address::ToBech32Ext, rand::address::rand_address}; + + #[test] + fn serialization() { + let address_1 = rand_address().to_bech32_unchecked("iota1"); + let address_2 = rand_address().to_bech32_unchecked("iota1"); + let json = serde_json::json!( + { + "standard": "IRC27", + "version": "v1.0", + "type": "image/jpeg", + "uri": "https://mywebsite.com/my-nft-files-1.jpeg", + "name": "My NFT #0001", + "collectionName": "My Collection of Art", + "royalties": { + address_1.to_string(): 0.025, + address_2.to_string(): 0.025 + }, + "issuerName": "My Artist Name", + "description": "A little information about my NFT collection", + "attributes": [ + { + "trait_type": "Attack", + "value": 150 + }, + { + "trait_type": "Background", + "value": "Purple" + }, + { + "trait_type": "Element", + "value": "Water" + }, + { + "trait_type": "Health", + "value": 500 + } + ] + } + ); + let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); + 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(address_1, 0.025) + .add_royalty(address_2, 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)); + assert_eq!(metadata, metadata_deser); + assert_eq!(json, serde_json::to_value(metadata).unwrap()) + } + } +} + +#[cfg(feature = "serde")] +pub(crate) mod irc_30 { + use getset::Getters; + use serde::{Deserialize, Serialize}; + use url::Url; + + use super::*; + + /// The IRC30 NFT 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 human-readable description of the token. + description: 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). + #[serde(default, skip_serializing_if = "Option::is_none")] + decimals: 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, description: impl Into, symbol: impl Into) -> Self { + Self { + name: name.into(), + description: description.into(), + symbol: symbol.into(), + decimals: Default::default(), + url: Default::default(), + logo_url: Default::default(), + logo: Default::default(), + } + } + + pub fn with_decimals(mut self, decimals: u32) -> Self { + self.decimals.replace(decimals); + 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 { + serde_json::to_string(self).unwrap().into_bytes() + } + } + + impl TryFrom for MetadataFeature { + type Error = Error; + fn try_from(value: Irc30Metadata) -> Result { + Self::new(value.to_bytes()) + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn serialization() { + let json = serde_json::json!( + { + "standard": "IRC30", + "name": "FooCoin", + "description": "FooCoin is the utility and governance token of FooLand, a revolutionary protocol in the play-to-earn crypto gaming field.", + "symbol": "FOO", + "decimals": 3, + "url": "https://foocoin.io/", + "logoUrl": "https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR" + } + ); + let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); + let metadata = Irc30Metadata::new( + "FooCoin", + "FooCoin is the utility and governance token of FooLand, a revolutionary protocol in the play-to-earn crypto gaming field.", + "FOO", + ) + .with_decimals(3) + .with_url("https://foocoin.io".parse().unwrap()) + .with_logo_url("https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR".parse().unwrap()); + assert_eq!(metadata, metadata_deser); + assert_eq!(json, serde_json::to_value(metadata).unwrap()) + } + } +} + #[cfg(feature = "serde")] pub(crate) mod dto { use alloc::boxed::Box; diff --git a/sdk/src/types/block/output/feature/mod.rs b/sdk/src/types/block/output/feature/mod.rs index 716db19d7f..5a3fd96ed9 100644 --- a/sdk/src/types/block/output/feature/mod.rs +++ b/sdk/src/types/block/output/feature/mod.rs @@ -13,6 +13,11 @@ use derive_more::{Deref, From}; use iterator_sorted::is_unique_sorted; use packable::{bounded::BoundedU8, prefix::BoxedSlicePrefix, Packable}; +#[cfg(feature = "serde")] +pub use self::metadata::{ + irc_27::{Attribute, Irc27Metadata}, + irc_30::Irc30Metadata, +}; pub use self::{issuer::IssuerFeature, metadata::MetadataFeature, sender::SenderFeature, tag::TagFeature}; pub(crate) use self::{metadata::MetadataFeatureLength, tag::TagFeatureLength}; use crate::types::block::{create_bitflags, Error}; From c3931fa331eb396f6ba4880e228b84bd78f7f753 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Wed, 6 Sep 2023 16:14:30 -0400 Subject: [PATCH 2/8] Use metadata in a test --- .../client/input_selection/nft_outputs.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/sdk/tests/client/input_selection/nft_outputs.rs b/sdk/tests/client/input_selection/nft_outputs.rs index cfc449d908..3a3933dd7a 100644 --- a/sdk/tests/client/input_selection/nft_outputs.rs +++ b/sdk/tests/client/input_selection/nft_outputs.rs @@ -1188,9 +1188,16 @@ fn changed_immutable_metadata() { let protocol_parameters = protocol_parameters(); let nft_id_1 = NftId::from_str(NFT_ID_1).unwrap(); + let metadata = iota_sdk::types::block::output::feature::Irc27Metadata::new( + "image/jpeg", + "https://mywebsite.com/my-nft-files-1.jpeg".parse().unwrap(), + "name", + ) + .with_issuer_name("Alice"); + let nft_output = NftOutputBuilder::new_with_minimum_storage_deposit(*protocol_parameters.rent_structure(), nft_id_1) - .with_immutable_features(MetadataFeature::new([1, 2, 3])) + .with_immutable_features(MetadataFeature::try_from(metadata)) .add_unlock_condition(AddressUnlockCondition::new( Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), )) @@ -1203,14 +1210,17 @@ fn changed_immutable_metadata() { chain: None, }]; + let metadata = + iota_sdk::types::block::output::feature::Irc30Metadata::new("name", "description", "symbol").with_decimals(5); + // New nft output with changed immutable metadata feature - let updated_alias_output = NftOutputBuilder::from(nft_output.as_nft()) + let updated_nft_output = NftOutputBuilder::from(nft_output.as_nft()) .with_minimum_storage_deposit(*protocol_parameters.rent_structure()) - .with_immutable_features(MetadataFeature::new([4, 5, 6])) + .with_immutable_features(MetadataFeature::try_from(metadata)) .finish_output(protocol_parameters.token_supply()) .unwrap(); - let outputs = [updated_alias_output]; + let outputs = [updated_nft_output]; let selected = InputSelection::new( inputs, @@ -1220,6 +1230,8 @@ fn changed_immutable_metadata() { ) .select(); + println!("{selected:?}"); + assert!(matches!( selected, Err(Error::UnfulfillableRequirement(Requirement::Nft( From 2b19889dbef3d7e7c024c651de3cc447dbf3fe44 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 7 Sep 2023 07:53:44 -0400 Subject: [PATCH 3/8] fix test and optional fields --- .../types/block/output/feature/metadata.rs | 23 +++++++++---------- .../client/input_selection/nft_outputs.rs | 12 ++++++---- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index 80d15fff87..05aa7a5303 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -187,7 +187,6 @@ pub(crate) mod irc_27 { #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq, Eq)] #[getset(get = "pub")] - pub struct Attribute { trait_type: String, value: serde_json::Value, @@ -306,13 +305,13 @@ pub(crate) mod irc_30 { pub struct Irc30Metadata { /// The human-readable name of the native token. name: String, - /// The human-readable description of the token. - description: 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")] - decimals: Option, + description: Option, /// URL pointing to more resources about the token. #[serde(default, skip_serializing_if = "Option::is_none")] url: Option, @@ -325,20 +324,20 @@ pub(crate) mod irc_30 { } impl Irc30Metadata { - pub fn new(name: impl Into, description: impl Into, symbol: impl Into) -> Self { + pub fn new(name: impl Into, symbol: impl Into, decimals: u32) -> Self { Self { name: name.into(), - description: description.into(), symbol: symbol.into(), - decimals: Default::default(), + decimals, + description: Default::default(), url: Default::default(), logo_url: Default::default(), logo: Default::default(), } } - pub fn with_decimals(mut self, decimals: u32) -> Self { - self.decimals.replace(decimals); + pub fn with_description(mut self, description: impl Into) -> Self { + self.description.replace(description.into()); self } @@ -389,10 +388,10 @@ pub(crate) mod irc_30 { let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); let metadata = Irc30Metadata::new( "FooCoin", - "FooCoin is the utility and governance token of FooLand, a revolutionary protocol in the play-to-earn crypto gaming field.", - "FOO", + + "FOO",3 ) - .with_decimals(3) + .with_description("FooCoin is the utility and governance token of FooLand, a revolutionary protocol in the play-to-earn crypto gaming field.") .with_url("https://foocoin.io".parse().unwrap()) .with_logo_url("https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR".parse().unwrap()); assert_eq!(metadata, metadata_deser); diff --git a/sdk/tests/client/input_selection/nft_outputs.rs b/sdk/tests/client/input_selection/nft_outputs.rs index 3a3933dd7a..aa59bcf5e9 100644 --- a/sdk/tests/client/input_selection/nft_outputs.rs +++ b/sdk/tests/client/input_selection/nft_outputs.rs @@ -1191,7 +1191,7 @@ fn changed_immutable_metadata() { let metadata = iota_sdk::types::block::output::feature::Irc27Metadata::new( "image/jpeg", "https://mywebsite.com/my-nft-files-1.jpeg".parse().unwrap(), - "name", + "file 1", ) .with_issuer_name("Alice"); @@ -1210,8 +1210,12 @@ fn changed_immutable_metadata() { chain: None, }]; - let metadata = - iota_sdk::types::block::output::feature::Irc30Metadata::new("name", "description", "symbol").with_decimals(5); + let metadata = iota_sdk::types::block::output::feature::Irc27Metadata::new( + "image/jpeg", + "https://mywebsite.com/my-nft-files-2.jpeg".parse().unwrap(), + "file 2", + ) + .with_issuer_name("Alice"); // New nft output with changed immutable metadata feature let updated_nft_output = NftOutputBuilder::from(nft_output.as_nft()) @@ -1230,8 +1234,6 @@ fn changed_immutable_metadata() { ) .select(); - println!("{selected:?}"); - assert!(matches!( selected, Err(Error::UnfulfillableRequirement(Requirement::Nft( From 74107a277f68e1565adaa8e9f73bfb7715edcdcb Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 7 Sep 2023 08:11:46 -0400 Subject: [PATCH 4/8] features --- sdk/Cargo.toml | 2 ++ sdk/src/types/block/output/feature/metadata.rs | 7 +++++-- sdk/src/types/block/output/feature/mod.rs | 9 ++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e92473afea..c0f2f41b13 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -140,6 +140,8 @@ tokio = { version = "1.31.0", default-features = false, features = [ default = ["client", "wallet", "tls"] events = [] +irc_27 = ["url", "serde"] +irc_30 = ["url", "serde"] ledger_nano = ["iota-ledger-nano"] mqtt = ["std", "regex", "rumqttc", "dep:once_cell"] participation = ["storage"] diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index 05aa7a5303..aa04910aa8 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -74,13 +74,15 @@ impl core::fmt::Debug for MetadataFeature { } } -#[cfg(feature = "serde")] +#[cfg(feature = "irc_27")] pub(crate) mod irc_27 { use alloc::collections::{BTreeMap, BTreeSet}; use getset::Getters; use serde::{Deserialize, Serialize}; use url::Url; + use alloc::string::String; + use alloc::borrow::ToOwned; use super::*; use crate::types::block::address::Bech32Address; @@ -289,11 +291,12 @@ pub(crate) mod irc_27 { } } -#[cfg(feature = "serde")] +#[cfg(feature = "irc_30")] pub(crate) mod irc_30 { use getset::Getters; use serde::{Deserialize, Serialize}; use url::Url; + use alloc::string::String; use super::*; diff --git a/sdk/src/types/block/output/feature/mod.rs b/sdk/src/types/block/output/feature/mod.rs index 5a3fd96ed9..363fb1befc 100644 --- a/sdk/src/types/block/output/feature/mod.rs +++ b/sdk/src/types/block/output/feature/mod.rs @@ -13,11 +13,10 @@ use derive_more::{Deref, From}; use iterator_sorted::is_unique_sorted; use packable::{bounded::BoundedU8, prefix::BoxedSlicePrefix, Packable}; -#[cfg(feature = "serde")] -pub use self::metadata::{ - irc_27::{Attribute, Irc27Metadata}, - irc_30::Irc30Metadata, -}; +#[cfg(feature = "irc_27")] +pub use self::metadata::irc_27::{Attribute, Irc27Metadata}; +#[cfg(feature = "irc_30")] +pub use self::metadata::irc_30::Irc30Metadata; pub use self::{issuer::IssuerFeature, metadata::MetadataFeature, sender::SenderFeature, tag::TagFeature}; pub(crate) use self::{metadata::MetadataFeatureLength, tag::TagFeatureLength}; use crate::types::block::{create_bitflags, Error}; From 2d7fd17140c8e4097dfdf2098b923859b21f4ef2 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 7 Sep 2023 09:23:38 -0400 Subject: [PATCH 5/8] fmt and refactor tests --- .../types/block/output/feature/metadata.rs | 99 ++++++++----------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index aa04910aa8..c0b7dafb2f 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -76,13 +76,15 @@ impl core::fmt::Debug for MetadataFeature { #[cfg(feature = "irc_27")] pub(crate) mod irc_27 { - use alloc::collections::{BTreeMap, BTreeSet}; + use alloc::{ + borrow::ToOwned, + collections::{BTreeMap, BTreeSet}, + string::String, + }; use getset::Getters; use serde::{Deserialize, Serialize}; use url::Url; - use alloc::string::String; - use alloc::borrow::ToOwned; use super::*; use crate::types::block::address::Bech32Address; @@ -234,57 +236,36 @@ pub(crate) mod irc_27 { #[test] fn serialization() { - let address_1 = rand_address().to_bech32_unchecked("iota1"); - let address_2 = rand_address().to_bech32_unchecked("iota1"); - let json = serde_json::json!( - { - "standard": "IRC27", - "version": "v1.0", - "type": "image/jpeg", - "uri": "https://mywebsite.com/my-nft-files-1.jpeg", - "name": "My NFT #0001", - "collectionName": "My Collection of Art", - "royalties": { - address_1.to_string(): 0.025, - address_2.to_string(): 0.025 - }, - "issuerName": "My Artist Name", - "description": "A little information about my NFT collection", - "attributes": [ - { - "trait_type": "Attack", - "value": 150 - }, - { - "trait_type": "Background", - "value": "Purple" - }, - { - "trait_type": "Element", - "value": "Water" - }, - { - "trait_type": "Health", - "value": 500 - } - ] - } - ); - let metadata_deser = serde_json::from_value::(json.clone()).unwrap(); 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(address_1, 0.025) - .add_royalty(address_2, 0.025) + .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()) } @@ -293,10 +274,11 @@ pub(crate) mod irc_27 { #[cfg(feature = "irc_30")] pub(crate) mod irc_30 { + use alloc::string::String; + use getset::Getters; use serde::{Deserialize, Serialize}; use url::Url; - use alloc::string::String; use super::*; @@ -377,26 +359,29 @@ pub(crate) mod irc_30 { #[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": "FooCoin", - "description": "FooCoin is the utility and governance token of FooLand, a revolutionary protocol in the play-to-earn crypto gaming field.", - "symbol": "FOO", - "decimals": 3, - "url": "https://foocoin.io/", - "logoUrl": "https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR" + "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(); - let metadata = Irc30Metadata::new( - "FooCoin", - - "FOO",3 - ) - .with_description("FooCoin is the utility and governance token of FooLand, a revolutionary protocol in the play-to-earn crypto gaming field.") - .with_url("https://foocoin.io".parse().unwrap()) - .with_logo_url("https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR".parse().unwrap()); + assert_eq!(metadata, metadata_deser); assert_eq!(json, serde_json::to_value(metadata).unwrap()) } From 39909a52003072c5cf8885d790a34dc618a44d2d Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 7 Sep 2023 09:47:32 -0400 Subject: [PATCH 6/8] clippy --- .../api/block_builder/input_selection/core/transition.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/client/api/block_builder/input_selection/core/transition.rs b/sdk/src/client/api/block_builder/input_selection/core/transition.rs index 224274a2ed..aaa7e9a3d4 100644 --- a/sdk/src/client/api/block_builder/input_selection/core/transition.rs +++ b/sdk/src/client/api/block_builder/input_selection/core/transition.rs @@ -54,7 +54,7 @@ impl InputSelection { } // Remove potential sender feature because it will not be needed anymore as it only needs to be verified once. - let features = input.features().iter().cloned().filter(|feature| !feature.is_sender()); + let features = input.features().iter().filter(|feature| !feature.is_sender()).cloned(); let mut builder = AliasOutputBuilder::from(input) .with_alias_id(alias_id) @@ -101,7 +101,7 @@ impl InputSelection { } // Remove potential sender feature because it will not be needed anymore as it only needs to be verified once. - let features = input.features().iter().cloned().filter(|feature| !feature.is_sender()); + let features = input.features().iter().filter(|feature| !feature.is_sender()).cloned(); let output = NftOutputBuilder::from(input) .with_nft_id(nft_id) From 54022901cb336e5b3fd0cb5b771ad60a011315ef Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 7 Sep 2023 10:23:06 -0400 Subject: [PATCH 7/8] allow more types for feature constructor and feature gate the test --- .../types/block/output/feature/metadata.rs | 27 +++++++++++++++++-- .../client/input_selection/nft_outputs.rs | 6 +++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index c0b7dafb2f..fda86f2380 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -19,6 +19,29 @@ pub struct MetadataFeature( BoxedSlicePrefix, ); +macro_rules! impl_from_vec { + ($type:ty) => { + impl TryFrom<$type> for MetadataFeature { + 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 MetadataFeature { + type Error = Error; + + fn try_from(value: [u8; N]) -> Result { + value.to_vec().try_into() + } +} + impl TryFrom> for MetadataFeature { type Error = Error; @@ -51,8 +74,8 @@ impl MetadataFeature { /// Creates a new [`MetadataFeature`]. #[inline(always)] - pub fn new(data: impl Into>) -> Result { - Self::try_from(data.into()) + pub fn new>(data: T) -> Result { + data.try_into() } /// Returns the data. diff --git a/sdk/tests/client/input_selection/nft_outputs.rs b/sdk/tests/client/input_selection/nft_outputs.rs index aa59bcf5e9..59980c680b 100644 --- a/sdk/tests/client/input_selection/nft_outputs.rs +++ b/sdk/tests/client/input_selection/nft_outputs.rs @@ -1188,12 +1188,15 @@ fn changed_immutable_metadata() { let protocol_parameters = protocol_parameters(); let nft_id_1 = NftId::from_str(NFT_ID_1).unwrap(); + #[cfg(feature = "irc_27")] let metadata = iota_sdk::types::block::output::feature::Irc27Metadata::new( "image/jpeg", "https://mywebsite.com/my-nft-files-1.jpeg".parse().unwrap(), "file 1", ) .with_issuer_name("Alice"); + #[cfg(not(feature = "irc_27"))] + let metadata = [1, 2, 3]; let nft_output = NftOutputBuilder::new_with_minimum_storage_deposit(*protocol_parameters.rent_structure(), nft_id_1) @@ -1210,12 +1213,15 @@ fn changed_immutable_metadata() { chain: None, }]; + #[cfg(feature = "irc_27")] let metadata = iota_sdk::types::block::output::feature::Irc27Metadata::new( "image/jpeg", "https://mywebsite.com/my-nft-files-2.jpeg".parse().unwrap(), "file 2", ) .with_issuer_name("Alice"); + #[cfg(not(feature = "irc_27"))] + let metadata = [4, 5, 6]; // New nft output with changed immutable metadata feature let updated_nft_output = NftOutputBuilder::from(nft_output.as_nft()) From f31a505025443a0cf44cfdb56e59c2d74549a50e Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 7 Sep 2023 10:31:27 -0400 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/src/types/block/output/feature/metadata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index fda86f2380..6a48cecc81 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -1,7 +1,7 @@ // Copyright 2021-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use alloc::{boxed::Box, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; use core::{ops::RangeInclusive, str::FromStr}; use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix};