From f12b2406751284b91c43951eb09217c7f3d2cf17 Mon Sep 17 00:00:00 2001 From: yito88 Date: Sun, 14 Jan 2024 00:20:13 +0100 Subject: [PATCH 1/9] skip validation, make some data optional --- Cargo.toml | 3 +- ibc-apps/ics721-nft-transfer/src/context.rs | 24 +++++----- .../ics721-nft-transfer/src/handler/mod.rs | 28 +++++------ .../src/handler/on_recv_packet.rs | 31 ++++++------ .../src/handler/send_transfer.rs | 29 +++++++---- .../ics721-nft-transfer/types/src/data.rs | 48 +++++++++++++++++++ .../ics721-nft-transfer/types/src/error.rs | 2 +- .../ics721-nft-transfer/types/src/packet.rs | 2 +- .../ibc/applications/nft_transfer/context.rs | 32 ++++++------- .../ibc/applications/nft_transfer/types.rs | 16 +++---- 10 files changed, 134 insertions(+), 81 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65f0f7695..fe14e7ed4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,8 +90,7 @@ ibc-client-tendermint-types = { version = "0.49.1", path = "./ibc-clients/ics07- ibc-app-transfer-types = { version = "0.49.1", path = "./ibc-apps/ics20-transfer/types", default-features = false } ibc-app-nft-transfer-types = { version = "0.49.1", path = "./ibc-apps/ics721-nft-transfer/types", default-features = false } -#ibc-proto = { version = "0.39.1", default-features = false } -ibc-proto = { git = "https://github.com/heliaxdev/ibc-proto-rs", branch = "yuji/feat/ics721-impl", default-features = false } +ibc-proto = { version = "0.41.0", default-features = false } # cosmos dependencies tendermint = { version = "0.34.0", default-features = false } diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs index 508f94b6b..6f0324d84 100644 --- a/ibc-apps/ics721-nft-transfer/src/context.rs +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -17,10 +17,10 @@ pub trait NftContext { fn get_id(&self) -> &TokenId; /// Get the token URI - fn get_uri(&self) -> &TokenUri; + fn get_uri(&self) -> Option<&TokenUri>; /// Get the token Data - fn get_data(&self) -> &TokenData; + fn get_data(&self) -> Option<&TokenData>; } pub trait NftClassContext { @@ -28,10 +28,10 @@ pub trait NftClassContext { fn get_id(&self) -> &ClassId; /// Get the class URI - fn get_uri(&self) -> &ClassUri; + fn get_uri(&self) -> Option<&ClassUri>; /// Get the class Data - fn get_data(&self) -> &ClassData; + fn get_data(&self) -> Option<&ClassData>; } /// Read-only methods required in NFT transfer validation context. @@ -53,8 +53,8 @@ pub trait NftTransferValidationContext { fn create_or_update_class_validate( &self, class_id: &PrefixedClassId, - class_uri: &ClassUri, - class_data: &ClassData, + class_uri: Option<&ClassUri>, + class_data: Option<&ClassData>, ) -> Result<(), NftTransferError>; /// Validates that the tokens can be escrowed successfully. @@ -88,8 +88,8 @@ pub trait NftTransferValidationContext { account: &Self::AccountId, class_id: &PrefixedClassId, token_id: &TokenId, - token_uri: &TokenUri, - token_data: &TokenData, + token_uri: Option<&TokenUri>, + token_data: Option<&TokenData>, ) -> Result<(), NftTransferError>; /// Validates the sender account and the coin input before burning. @@ -133,8 +133,8 @@ pub trait NftTransferExecutionContext: NftTransferValidationContext { fn create_or_update_class_execute( &self, class_id: &PrefixedClassId, - class_uri: &ClassUri, - class_data: &ClassData, + class_uri: Option<&ClassUri>, + class_data: Option<&ClassData>, ) -> Result<(), NftTransferError>; /// Executes the escrow of the NFT in a user account. @@ -167,8 +167,8 @@ pub trait NftTransferExecutionContext: NftTransferValidationContext { account: &Self::AccountId, class_id: &PrefixedClassId, token_id: &TokenId, - token_uri: &TokenUri, - token_data: &TokenData, + token_uri: Option<&TokenUri>, + token_data: Option<&TokenData>, ) -> Result<(), NftTransferError>; /// Executes burning of the NFT in a user account. diff --git a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs index 3d577b709..f9b9066af 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs @@ -40,14 +40,12 @@ pub fn refund_packet_nft_execute( } // mint vouchers back to sender else { - data.token_ids - .0 - .iter() - .zip(data.token_uris.iter()) - .zip(data.token_data.iter()) - .try_for_each(|((token_id, token_uri), token_data)| { - ctx_a.mint_nft_execute(&sender, &data.class_id, token_id, token_uri, token_data) - }) + for (i, token_id) in data.token_ids.0.iter().enumerate() { + let token_uri = data.token_uris.get(i); + let token_data = data.token_data.get(i); + ctx_a.mint_nft_execute(&sender, &data.class_id, token_id, token_uri, token_data)?; + } + Ok(()) } } @@ -77,13 +75,11 @@ pub fn refund_packet_nft_validate( ) }) } else { - data.token_ids - .0 - .iter() - .zip(data.token_uris.iter()) - .zip(data.token_data.iter()) - .try_for_each(|((token_id, token_uri), token_data)| { - ctx_a.mint_nft_validate(&sender, &data.class_id, token_id, token_uri, token_data) - }) + for (i, token_id) in data.token_ids.0.iter().enumerate() { + let token_uri = data.token_uris.get(i); + let token_data = data.token_data.get(i); + ctx_a.mint_nft_validate(&sender, &data.class_id, token_id, token_uri, token_data)?; + } + Ok(()) } } diff --git a/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs b/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs index 768a7f757..ec3dd2178 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs @@ -81,13 +81,10 @@ where events: vec![], log: Vec::new(), }; - for ((token_id, token_uri), token_data) in data - .token_ids - .0 - .iter() - .zip(data.token_uris.iter()) - .zip(data.token_data.iter()) - { + for (i, token_id) in data.token_ids.0.iter().enumerate() { + let token_uri = data.token_uris.get(i); + let token_data = data.token_data.get(i); + let trace_event = TokenTraceEvent { trace_hash: ctx_b.token_hash_string(&class_id, token_id), class: class_id.clone(), @@ -98,19 +95,19 @@ where // Note: the validation is called before the execution. // Refer to ICS-20 `process_recv_packet_execute()`. - let class_uri = data - .class_uri - .as_ref() - .ok_or((ModuleExtras::empty(), NftTransferError::NftClassNotFound))?; - let class_data = data - .class_data - .as_ref() - .ok_or((ModuleExtras::empty(), NftTransferError::NftClassNotFound))?; ctx_b - .create_or_update_class_validate(&class_id, class_uri, class_data) + .create_or_update_class_validate( + &class_id, + data.class_uri.as_ref(), + data.class_data.as_ref(), + ) .map_err(|nft_error| (ModuleExtras::empty(), nft_error))?; ctx_b - .create_or_update_class_execute(&class_id, class_uri, class_data) + .create_or_update_class_execute( + &class_id, + data.class_uri.as_ref(), + data.class_data.as_ref(), + ) .map_err(|nft_error| (ModuleExtras::empty(), nft_error))?; ctx_b diff --git a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs index a197bee5c..7883b701f 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs @@ -83,13 +83,22 @@ where transfer_ctx.burn_nft_validate(&sender, class_id, token_id, &packet_data.memo)?; } let nft = transfer_ctx.get_nft(class_id, token_id)?; - packet_data.token_uris.push(nft.get_uri().clone()); - packet_data.token_data.push(nft.get_data().clone()); + if let Some(uri) = nft.get_uri() { + packet_data.token_uris.push(uri.clone()); + } + if let Some(data) = nft.get_data() { + packet_data.token_data.push(data.clone()); + } + } + let token_len = packet_data.token_ids.0.len(); + if token_len != packet_data.token_uris.len() || token_len != packet_data.token_data.len() { + // When the length mismatched, we can't send these token URIs and data + return Err(NftTransferError::TokenMismatched); } let nft_class = transfer_ctx.get_nft_class(class_id)?; - packet_data.class_uri = Some(nft_class.get_uri().clone()); - packet_data.class_data = Some(nft_class.get_data().clone()); + packet_data.class_uri = nft_class.get_uri().cloned(); + packet_data.class_data = nft_class.get_data().cloned(); let packet = { let data = serde_json::to_vec(&packet_data) @@ -166,13 +175,17 @@ where transfer_ctx.burn_nft_execute(&sender, class_id, token_id, &packet_data.memo)?; } let nft = transfer_ctx.get_nft(class_id, token_id)?; - packet_data.token_uris.push(nft.get_uri().clone()); - packet_data.token_data.push(nft.get_data().clone()); + if let Some(uri) = nft.get_uri() { + packet_data.token_uris.push(uri.clone()); + } + if let Some(data) = nft.get_data() { + packet_data.token_data.push(data.clone()); + } } let nft_class = transfer_ctx.get_nft_class(class_id)?; - packet_data.class_uri = Some(nft_class.get_uri().clone()); - packet_data.class_data = Some(nft_class.get_data().clone()); + packet_data.class_uri = nft_class.get_uri().cloned(); + packet_data.class_data = nft_class.get_data().cloned(); let packet = { let data = { diff --git a/ibc-apps/ics721-nft-transfer/types/src/data.rs b/ibc-apps/ics721-nft-transfer/types/src/data.rs index a5fe516c0..60060aca7 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/data.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -3,10 +3,12 @@ use core::fmt::{self, Display, Formatter}; use core::str::FromStr; use ibc_core::primitives::prelude::*; +#[cfg(feature = "ics721-data")] use mime::Mime; use crate::error::NftTransferError; +#[cfg(not(feature = "ics721-data"))] #[cfg_attr( feature = "parity-scale-codec", derive( @@ -22,14 +24,50 @@ use crate::error::NftTransferError; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Data(String); + +#[cfg(not(feature = "ics721-data"))] +impl Display for Data { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(not(feature = "ics721-data"))] +impl FromStr for Data { + type Err = NftTransferError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +#[cfg_attr( + feature = "parity-scale-codec", + derive( + parity_scale_codec::Encode, + parity_scale_codec::Decode, + scale_info::TypeInfo + ) +)] +#[cfg_attr( + feature = "borsh", + derive(borsh::BorshSerialize, borsh::BorshDeserialize) +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg(feature = "ics721-data")] pub struct Data(BTreeMap); +#[cfg(feature = "ics721-data")] #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DataValue { value: String, mime: Option, } +#[cfg(feature = "ics721-data")] #[cfg(feature = "serde")] impl serde::Serialize for DataValue { fn serialize(&self, serializer: S) -> Result @@ -49,6 +87,7 @@ impl serde::Serialize for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for DataValue { fn deserialize(deserializer: D) -> Result @@ -74,6 +113,7 @@ impl<'de> serde::Deserialize<'de> for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "borsh")] impl borsh::BorshSerialize for DataValue { fn serialize( @@ -90,6 +130,7 @@ impl borsh::BorshSerialize for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "borsh")] impl borsh::BorshDeserialize for DataValue { fn deserialize_reader( @@ -107,6 +148,7 @@ impl borsh::BorshDeserialize for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "parity-scale-codec")] impl parity_scale_codec::Encode for DataValue { fn encode_to(&self, writer: &mut T) { @@ -119,6 +161,7 @@ impl parity_scale_codec::Encode for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "parity-scale-codec")] impl parity_scale_codec::Decode for DataValue { fn decode( @@ -139,6 +182,7 @@ impl parity_scale_codec::Decode for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "parity-scale-codec")] impl scale_info::TypeInfo for DataValue { type Identity = Self; @@ -154,6 +198,7 @@ impl scale_info::TypeInfo for DataValue { } } +#[cfg(feature = "ics721-data")] #[cfg(feature = "schema")] impl schemars::JsonSchema for DataValue { fn schema_name() -> String { @@ -169,12 +214,14 @@ impl schemars::JsonSchema for DataValue { } } +#[cfg(feature = "ics721-data")] impl Display for Data { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", serde_json::to_string(&self.0).expect("infallible")) } } +#[cfg(feature = "ics721-data")] impl FromStr for Data { type Err = NftTransferError; @@ -186,6 +233,7 @@ impl FromStr for Data { } } +#[cfg(feature = "ics721-data")] #[cfg(test)] mod tests { use rstest::rstest; diff --git a/ibc-apps/ics721-nft-transfer/types/src/error.rs b/ibc-apps/ics721-nft-transfer/types/src/error.rs index fc3b03591..611946e16 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/error.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/error.rs @@ -46,7 +46,7 @@ pub enum NftTransferError { InvalidTokenId, /// duplicated token IDs DuplicatedTokenIds, - /// invalid token ID + /// The length of token IDs mismatched that of token URIs or token data TokenMismatched, /// invalid json data InvalidJsonData, diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 7db6a3504..0d655cfb0 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -167,7 +167,7 @@ mod tests { const DUMMY_CLASS_ID: &str = "class"; const DUMMY_URI: &str = "http://example.com"; const DUMMY_DATA: &str = - r#"{"name":{"value":"Crypto Creatures"},"image":{"value":"binary","mime":"image/png"}}"#; + r#"{"image":{"value":"binary","mime":"image/png"},"name":{"value":"Crypto Creatures"}}"#; impl PacketData { pub fn new_dummy() -> Self { diff --git a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs index 491ec9ca4..a526dcf45 100644 --- a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs +++ b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs @@ -20,12 +20,12 @@ impl NftContext for DummyNft { &self.token_id } - fn get_uri(&self) -> &TokenUri { - &self.token_uri + fn get_uri(&self) -> Option<&TokenUri> { + self.token_uri.as_ref() } - fn get_data(&self) -> &TokenData { - &self.token_data + fn get_data(&self) -> Option<&TokenData> { + self.token_data.as_ref() } } @@ -34,12 +34,12 @@ impl NftClassContext for DummyNftClass { &self.class_id } - fn get_uri(&self) -> &ClassUri { - &self.class_uri + fn get_uri(&self) -> Option<&ClassUri> { + self.class_uri.as_ref() } - fn get_data(&self) -> &ClassData { - &self.class_data + fn get_data(&self) -> Option<&ClassData> { + self.class_data.as_ref() } } @@ -63,8 +63,8 @@ impl NftTransferValidationContext for DummyNftTransferModule { fn create_or_update_class_validate( &self, _class_id: &PrefixedClassId, - _class_uri: &ClassUri, - _class_data: &ClassData, + _class_uri: Option<&ClassUri>, + _class_data: Option<&ClassData>, ) -> Result<(), NftTransferError> { Ok(()) } @@ -97,8 +97,8 @@ impl NftTransferValidationContext for DummyNftTransferModule { _account: &Self::AccountId, _class_id: &PrefixedClassId, _token_id: &TokenId, - _token_uri: &TokenUri, - _token_data: &TokenData, + _token_uri: Option<&TokenUri>, + _token_data: Option<&TokenData>, ) -> Result<(), NftTransferError> { Ok(()) } @@ -133,8 +133,8 @@ impl NftTransferExecutionContext for DummyNftTransferModule { fn create_or_update_class_execute( &self, _class_id: &PrefixedClassId, - _class_uri: &ClassUri, - _class_data: &ClassData, + _class_uri: Option<&ClassUri>, + _class_data: Option<&ClassData>, ) -> Result<(), NftTransferError> { Ok(()) } @@ -167,8 +167,8 @@ impl NftTransferExecutionContext for DummyNftTransferModule { _account: &Self::AccountId, _class_id: &PrefixedClassId, _token_id: &TokenId, - _token_uri: &TokenUri, - _token_data: &TokenData, + _token_uri: Option<&TokenUri>, + _token_data: Option<&TokenData>, ) -> Result<(), NftTransferError> { Ok(()) } diff --git a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/types.rs b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/types.rs index 6eb65e285..6e282fe61 100644 --- a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/types.rs +++ b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/types.rs @@ -7,17 +7,17 @@ pub struct DummyNftTransferModule; pub struct DummyNft { pub class_id: ClassId, pub token_id: TokenId, - pub token_uri: TokenUri, - pub token_data: TokenData, + pub token_uri: Option, + pub token_data: Option, } impl Default for DummyNft { fn default() -> Self { let class_id = "class_0".parse().expect("infallible"); let token_id = "token_0".parse().expect("infallible"); - let token_uri = "http://example.com".parse().expect("infallible"); + let token_uri = Some("http://example.com".parse().expect("infallible")); let data = r#"{"name":{"value":"Crypto Creatures"},"image":{"value":"binary","mime":"image/png"}}"#; - let token_data = data.parse().expect("infallible"); + let token_data = Some(data.parse().expect("infallible")); Self { class_id, token_id, @@ -30,16 +30,16 @@ impl Default for DummyNft { #[derive(Debug)] pub struct DummyNftClass { pub class_id: ClassId, - pub class_uri: ClassUri, - pub class_data: ClassData, + pub class_uri: Option, + pub class_data: Option, } impl Default for DummyNftClass { fn default() -> Self { let class_id = "class_0".parse().expect("infallible"); - let class_uri = "http://example.com".parse().expect("infallible"); + let class_uri = Some("http://example.com".parse().expect("infallible")); let data = r#"{"name":{"value":"Crypto Creatures"},"image":{"value":"binary","mime":"image/png"}}"#; - let class_data = data.parse().expect("infallible"); + let class_data = Some(data.parse().expect("infallible")); Self { class_id, class_uri, From 8036d96def3104731adee71659b9151175ad47b2 Mon Sep 17 00:00:00 2001 From: yito88 Date: Tue, 16 Jan 2024 21:22:33 +0100 Subject: [PATCH 2/9] check the length of token_uri and token_data --- .../ics721-nft-transfer/src/handler/send_transfer.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs index 7883b701f..d5564bdb1 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs @@ -83,15 +83,17 @@ where transfer_ctx.burn_nft_validate(&sender, class_id, token_id, &packet_data.memo)?; } let nft = transfer_ctx.get_nft(class_id, token_id)?; - if let Some(uri) = nft.get_uri() { + // Set the URI and the data if both exists + if let (Some(uri), Some(data)) = (nft.get_uri(), nft.get_data()) { packet_data.token_uris.push(uri.clone()); - } - if let Some(data) = nft.get_data() { packet_data.token_data.push(data.clone()); } } + let token_len = packet_data.token_ids.0.len(); - if token_len != packet_data.token_uris.len() || token_len != packet_data.token_data.len() { + let uris_len = packet_data.token_uris.len(); + // The length of token_uris is equal to that of token_data in this case + if uris_len != 0 && token_len != uris_len { // When the length mismatched, we can't send these token URIs and data return Err(NftTransferError::TokenMismatched); } From 6eae3cb44a3997ce4e69ce392ef400cd573ef9da Mon Sep 17 00:00:00 2001 From: yito88 Date: Tue, 16 Jan 2024 21:42:53 +0100 Subject: [PATCH 3/9] fix to set TokenData and TokenUri at once --- ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs index d5564bdb1..7cf32432c 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs @@ -177,10 +177,9 @@ where transfer_ctx.burn_nft_execute(&sender, class_id, token_id, &packet_data.memo)?; } let nft = transfer_ctx.get_nft(class_id, token_id)?; - if let Some(uri) = nft.get_uri() { + // Set the URI and the data if both exists + if let (Some(uri), Some(data)) = (nft.get_uri(), nft.get_data()) { packet_data.token_uris.push(uri.clone()); - } - if let Some(data) = nft.get_data() { packet_data.token_data.push(data.clone()); } } From eeb2e4d0ff616693897cb7f0a4c220a9d87473f7 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Wed, 17 Jan 2024 15:16:42 -0800 Subject: [PATCH 4/9] imp: add validate_basic method for PacketData --- .../src/handler/send_transfer.rs | 8 +---- .../ics721-nft-transfer/types/src/packet.rs | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs index 7cf32432c..6bf28d367 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs @@ -90,13 +90,7 @@ where } } - let token_len = packet_data.token_ids.0.len(); - let uris_len = packet_data.token_uris.len(); - // The length of token_uris is equal to that of token_data in this case - if uris_len != 0 && token_len != uris_len { - // When the length mismatched, we can't send these token URIs and data - return Err(NftTransferError::TokenMismatched); - } + packet_data.validate_basic()?; let nft_class = transfer_ctx.get_nft_class(class_id)?; packet_data.class_uri = nft_class.get_uri().cloned(); diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 0d655cfb0..d16ce5b1b 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -54,16 +54,7 @@ impl PacketData { receiver: Signer, memo: Memo, ) -> Result { - if token_ids.0.is_empty() { - return Err(NftTransferError::NoTokenId); - } - let num = token_ids.0.len(); - let num_uri = token_uris.len(); - let num_data = token_data.len(); - if (num_uri != 0 && num_uri != num) || (num_data != 0 && num_data != num) { - return Err(NftTransferError::TokenMismatched); - } - Ok(Self { + let packet_data = Self { class_id, class_uri, class_data, @@ -73,7 +64,25 @@ impl PacketData { sender, receiver, memo, - }) + }; + + packet_data.validate_basic()?; + + Ok(packet_data) + } + + /// Performs the basic validation of the packet data fields. + pub fn validate_basic(&self) -> Result<(), NftTransferError> { + if self.token_ids.0.is_empty() { + return Err(NftTransferError::NoTokenId); + } + let num = self.token_ids.0.len(); + let num_uri = self.token_uris.len(); + let num_data = self.token_data.len(); + if (num_uri != 0 && num_uri != num) || (num_data != 0 && num_data != num) { + return Err(NftTransferError::TokenMismatched); + } + Ok(()) } } From 3edeaf679b18a59ea19c5e19a2561e1bdc390dd8 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Wed, 17 Jan 2024 17:12:52 -0800 Subject: [PATCH 5/9] imp: allow any format for Data + define parse_as_ics721_data method --- ibc-apps/ics721-nft-transfer/src/context.rs | 14 +++++ .../ics721-nft-transfer/types/src/class.rs | 2 +- .../ics721-nft-transfer/types/src/data.rs | 55 +++++++------------ .../ics721-nft-transfer/types/src/error.rs | 2 + ibc-apps/ics721-nft-transfer/types/src/lib.rs | 2 + .../ics721-nft-transfer/types/src/packet.rs | 11 ++++ .../ics721-nft-transfer/types/src/token.rs | 2 +- 7 files changed, 50 insertions(+), 38 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs index 6f0324d84..84d782e1d 100644 --- a/ibc-apps/ics721-nft-transfer/src/context.rs +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -50,6 +50,13 @@ pub trait NftTransferValidationContext { fn can_receive_nft(&self) -> Result<(), NftTransferError>; /// Validates that the NFT can be created or updated successfully. + /// + /// Note: some existing ICS-721 implementations may not strictly adhere to + /// the ICS-721 class data structure. The + /// [`ClassData`](crate::types::ClassData) associated with this + /// implementation can take any valid JSON format. If your project requires + /// ICS-721 format for the `ClassData`, ensure correctness by checking with + /// [`parse_as_ics721_data()`](crate::types::Data::parse_as_ics721_data). fn create_or_update_class_validate( &self, class_id: &PrefixedClassId, @@ -83,6 +90,13 @@ pub trait NftTransferValidationContext { ) -> Result<(), NftTransferError>; /// Validates the receiver account and the NFT input + /// + /// Note: some existing ICS-721 implementations may not strictly adhere to + /// the ICS-721 token data structure. The + /// [`TokenData`](crate::types::TokenData) associated with this + /// implementation can take any valid JSON format. If your project requires + /// ICS-721 format for `TokenData`, ensure correctness by checking with + /// [`parse_as_ics721_data()`](crate::types::Data::parse_as_ics721_data). fn mint_nft_validate( &self, account: &Self::AccountId, diff --git a/ibc-apps/ics721-nft-transfer/types/src/class.rs b/ibc-apps/ics721-nft-transfer/types/src/class.rs index f5d5e590f..278a90a07 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/class.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/class.rs @@ -419,7 +419,7 @@ impl FromStr for ClassUri { )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, derive_more::AsRef)] pub struct ClassData(Data); impl Display for ClassData { diff --git a/ibc-apps/ics721-nft-transfer/types/src/data.rs b/ibc-apps/ics721-nft-transfer/types/src/data.rs index 60060aca7..d7e98c640 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/data.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -3,12 +3,10 @@ use core::fmt::{self, Display, Formatter}; use core::str::FromStr; use ibc_core::primitives::prelude::*; -#[cfg(feature = "ics721-data")] use mime::Mime; use crate::error::NftTransferError; -#[cfg(not(feature = "ics721-data"))] #[cfg_attr( feature = "parity-scale-codec", derive( @@ -23,17 +21,23 @@ use crate::error::NftTransferError; )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, derive_more::From)] pub struct Data(String); -#[cfg(not(feature = "ics721-data"))] +impl Data { + /// Parses the data in the format specified by ICS-721. + pub fn parse_as_ics721_data(&self) -> Result + { + self.0.parse::() + } +} + impl Display for Data { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } -#[cfg(not(feature = "ics721-data"))] impl FromStr for Data { type Err = NftTransferError; @@ -57,17 +61,23 @@ impl FromStr for Data { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg(feature = "ics721-data")] -pub struct Data(BTreeMap); +pub struct Ics721Data(BTreeMap); + + +impl FromStr for Ics721Data { + type Err = NftTransferError; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(|_| NftTransferError::InvalidIcs721Data) + } +} -#[cfg(feature = "ics721-data")] #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DataValue { value: String, mime: Option, } -#[cfg(feature = "ics721-data")] #[cfg(feature = "serde")] impl serde::Serialize for DataValue { fn serialize(&self, serializer: S) -> Result @@ -87,7 +97,6 @@ impl serde::Serialize for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for DataValue { fn deserialize(deserializer: D) -> Result @@ -113,7 +122,6 @@ impl<'de> serde::Deserialize<'de> for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "borsh")] impl borsh::BorshSerialize for DataValue { fn serialize( @@ -130,7 +138,6 @@ impl borsh::BorshSerialize for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "borsh")] impl borsh::BorshDeserialize for DataValue { fn deserialize_reader( @@ -148,7 +155,6 @@ impl borsh::BorshDeserialize for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "parity-scale-codec")] impl parity_scale_codec::Encode for DataValue { fn encode_to(&self, writer: &mut T) { @@ -161,7 +167,6 @@ impl parity_scale_codec::Encode for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "parity-scale-codec")] impl parity_scale_codec::Decode for DataValue { fn decode( @@ -182,7 +187,6 @@ impl parity_scale_codec::Decode for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "parity-scale-codec")] impl scale_info::TypeInfo for DataValue { type Identity = Self; @@ -198,7 +202,6 @@ impl scale_info::TypeInfo for DataValue { } } -#[cfg(feature = "ics721-data")] #[cfg(feature = "schema")] impl schemars::JsonSchema for DataValue { fn schema_name() -> String { @@ -214,26 +217,6 @@ impl schemars::JsonSchema for DataValue { } } -#[cfg(feature = "ics721-data")] -impl Display for Data { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", serde_json::to_string(&self.0).expect("infallible")) - } -} - -#[cfg(feature = "ics721-data")] -impl FromStr for Data { - type Err = NftTransferError; - - fn from_str(s: &str) -> Result { - let data: BTreeMap = - serde_json::from_str(s).map_err(|_| NftTransferError::InvalidJsonData)?; - - Ok(Self(data)) - } -} - -#[cfg(feature = "ics721-data")] #[cfg(test)] mod tests { use rstest::rstest; diff --git a/ibc-apps/ics721-nft-transfer/types/src/error.rs b/ibc-apps/ics721-nft-transfer/types/src/error.rs index 611946e16..08ab598e5 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/error.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/error.rs @@ -50,6 +50,8 @@ pub enum NftTransferError { TokenMismatched, /// invalid json data InvalidJsonData, + /// the data is not in the JSON format specified by ICS-721 + InvalidIcs721Data, /// expected `{expect_order}` channel, got `{got_order}` ChannelNotUnordered { expect_order: Order, diff --git a/ibc-apps/ics721-nft-transfer/types/src/lib.rs b/ibc-apps/ics721-nft-transfer/types/src/lib.rs index 74fed4f5e..b429ce1a3 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/lib.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/lib.rs @@ -24,6 +24,8 @@ pub use class::*; #[cfg(feature = "serde")] mod data; #[cfg(feature = "serde")] +pub use data::*; +#[cfg(feature = "serde")] pub mod events; #[cfg(feature = "serde")] pub mod msgs; diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index d16ce5b1b..398552347 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -225,6 +225,17 @@ mod tests { pub fn deser_json_assert_eq(&self, json: &str) { let deser: Self = serde_json::from_str(json).unwrap(); + + if let Some(data) = &deser.class_data { + assert!(data.as_ref().parse_as_ics721_data().is_ok()); + }; + + if deser.token_data.len() > 0 { + for data in deser.token_data.iter() { + assert!(data.as_ref().parse_as_ics721_data().is_ok()); + } + } + assert_eq!(&deser, self); } } diff --git a/ibc-apps/ics721-nft-transfer/types/src/token.rs b/ibc-apps/ics721-nft-transfer/types/src/token.rs index ff8f407bb..4965d49be 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/token.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/token.rs @@ -202,7 +202,7 @@ impl FromStr for TokenUri { )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, derive_more::AsRef)] pub struct TokenData(Data); impl Display for TokenData { From 82165daa2c1094940af0202570b92fcacec7aecc Mon Sep 17 00:00:00 2001 From: yito88 Date: Thu, 18 Jan 2024 17:26:58 +0100 Subject: [PATCH 6/9] fmt and clippy --- ibc-apps/ics721-nft-transfer/src/context.rs | 4 ++-- ibc-apps/ics721-nft-transfer/types/src/data.rs | 4 +--- ibc-apps/ics721-nft-transfer/types/src/packet.rs | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs index 84d782e1d..81dda2f87 100644 --- a/ibc-apps/ics721-nft-transfer/src/context.rs +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -50,7 +50,7 @@ pub trait NftTransferValidationContext { fn can_receive_nft(&self) -> Result<(), NftTransferError>; /// Validates that the NFT can be created or updated successfully. - /// + /// /// Note: some existing ICS-721 implementations may not strictly adhere to /// the ICS-721 class data structure. The /// [`ClassData`](crate::types::ClassData) associated with this @@ -90,7 +90,7 @@ pub trait NftTransferValidationContext { ) -> Result<(), NftTransferError>; /// Validates the receiver account and the NFT input - /// + /// /// Note: some existing ICS-721 implementations may not strictly adhere to /// the ICS-721 token data structure. The /// [`TokenData`](crate::types::TokenData) associated with this diff --git a/ibc-apps/ics721-nft-transfer/types/src/data.rs b/ibc-apps/ics721-nft-transfer/types/src/data.rs index d7e98c640..8a7de5d4e 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/data.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -26,8 +26,7 @@ pub struct Data(String); impl Data { /// Parses the data in the format specified by ICS-721. - pub fn parse_as_ics721_data(&self) -> Result - { + pub fn parse_as_ics721_data(&self) -> Result { self.0.parse::() } } @@ -63,7 +62,6 @@ impl FromStr for Data { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Ics721Data(BTreeMap); - impl FromStr for Ics721Data { type Err = NftTransferError; diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 398552347..6ced166e5 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -230,7 +230,7 @@ mod tests { assert!(data.as_ref().parse_as_ics721_data().is_ok()); }; - if deser.token_data.len() > 0 { + if !deser.token_data.is_empty() { for data in deser.token_data.iter() { assert!(data.as_ref().parse_as_ics721_data().is_ok()); } From f3806e549572c4e4bbd885c91d63f34aab9f5f43 Mon Sep 17 00:00:00 2001 From: yito88 Date: Mon, 22 Jan 2024 00:31:19 +0100 Subject: [PATCH 7/9] custom serde packet data with option --- .../ics721-nft-transfer/src/handler/mod.rs | 8 +- .../src/handler/on_recv_packet.rs | 4 +- .../src/handler/send_transfer.rs | 56 ++++-- ibc-apps/ics721-nft-transfer/src/module.rs | 6 +- .../ics721-nft-transfer/types/src/data.rs | 26 ++- .../ics721-nft-transfer/types/src/memo.rs | 2 +- .../types/src/msgs/transfer.rs | 18 +- .../ics721-nft-transfer/types/src/packet.rs | 172 ++++-------------- 8 files changed, 126 insertions(+), 166 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs index f9b9066af..2adbb47f6 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs @@ -41,8 +41,8 @@ pub fn refund_packet_nft_execute( // mint vouchers back to sender else { for (i, token_id) in data.token_ids.0.iter().enumerate() { - let token_uri = data.token_uris.get(i); - let token_data = data.token_data.get(i); + let token_uri = data.token_uris.as_ref().and_then(|uris| uris.get(i)); + let token_data = data.token_data.as_ref().and_then(|data| data.get(i)); ctx_a.mint_nft_execute(&sender, &data.class_id, token_id, token_uri, token_data)?; } Ok(()) @@ -76,8 +76,8 @@ pub fn refund_packet_nft_validate( }) } else { for (i, token_id) in data.token_ids.0.iter().enumerate() { - let token_uri = data.token_uris.get(i); - let token_data = data.token_data.get(i); + let token_uri = data.token_uris.as_ref().and_then(|uris| uris.get(i)); + let token_data = data.token_data.as_ref().and_then(|data| data.get(i)); ctx_a.mint_nft_validate(&sender, &data.class_id, token_id, token_uri, token_data)?; } Ok(()) diff --git a/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs b/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs index ec3dd2178..8782bf7a2 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs @@ -82,8 +82,8 @@ where log: Vec::new(), }; for (i, token_id) in data.token_ids.0.iter().enumerate() { - let token_uri = data.token_uris.get(i); - let token_data = data.token_data.get(i); + let token_uri = data.token_uris.as_ref().and_then(|uris| uris.get(i)); + let token_data = data.token_data.as_ref().and_then(|data| data.get(i)); let trace_event = TokenTraceEvent { trace_hash: ctx_b.token_hash_string(&class_id, token_id), diff --git a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs index 6bf28d367..270164701 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs @@ -67,8 +67,12 @@ where let class_id = &packet_data.class_id; let token_ids = &packet_data.token_ids; // overwrite even if they are set in MsgTransfer - packet_data.token_uris.clear(); - packet_data.token_data.clear(); + if let Some(uris) = &mut packet_data.token_uris { + uris.clear(); + } + if let Some(data) = &mut packet_data.token_data { + data.clear(); + } for token_id in token_ids.as_ref() { if is_sender_chain_source(msg.port_id_on_a.clone(), msg.chan_id_on_a.clone(), class_id) { transfer_ctx.escrow_nft_validate( @@ -77,16 +81,27 @@ where &msg.chan_id_on_a, class_id, token_id, - &packet_data.memo, + &packet_data.memo.clone().unwrap_or_default(), )?; } else { - transfer_ctx.burn_nft_validate(&sender, class_id, token_id, &packet_data.memo)?; + transfer_ctx.burn_nft_validate( + &sender, + class_id, + token_id, + &packet_data.memo.clone().unwrap_or_default(), + )?; } let nft = transfer_ctx.get_nft(class_id, token_id)?; // Set the URI and the data if both exists if let (Some(uri), Some(data)) = (nft.get_uri(), nft.get_data()) { - packet_data.token_uris.push(uri.clone()); - packet_data.token_data.push(data.clone()); + match &mut packet_data.token_uris { + Some(uris) => uris.push(uri.clone()), + None => packet_data.token_uris = Some(vec![uri.clone()]), + } + match &mut packet_data.token_data { + Some(token_data) => token_data.push(data.clone()), + None => packet_data.token_data = Some(vec![data.clone()]), + } } } @@ -155,8 +170,12 @@ where let class_id = &packet_data.class_id; let token_ids = &packet_data.token_ids; // overwrite even if they are set in MsgTransfer - packet_data.token_uris.clear(); - packet_data.token_data.clear(); + if let Some(uris) = &mut packet_data.token_uris { + uris.clear(); + } + if let Some(data) = &mut packet_data.token_data { + data.clear(); + } for token_id in token_ids.as_ref() { if is_sender_chain_source(msg.port_id_on_a.clone(), msg.chan_id_on_a.clone(), class_id) { transfer_ctx.escrow_nft_execute( @@ -165,16 +184,27 @@ where &msg.chan_id_on_a, class_id, token_id, - &packet_data.memo, + &packet_data.memo.clone().unwrap_or_default(), )?; } else { - transfer_ctx.burn_nft_execute(&sender, class_id, token_id, &packet_data.memo)?; + transfer_ctx.burn_nft_execute( + &sender, + class_id, + token_id, + &packet_data.memo.clone().unwrap_or_default(), + )?; } let nft = transfer_ctx.get_nft(class_id, token_id)?; // Set the URI and the data if both exists if let (Some(uri), Some(data)) = (nft.get_uri(), nft.get_data()) { - packet_data.token_uris.push(uri.clone()); - packet_data.token_data.push(data.clone()); + match &mut packet_data.token_uris { + Some(uris) => uris.push(uri.clone()), + None => packet_data.token_uris = Some(vec![uri.clone()]), + } + match &mut packet_data.token_data { + Some(token_data) => token_data.push(data.clone()), + None => packet_data.token_data = Some(vec![data.clone()]), + } } } @@ -212,7 +242,7 @@ where receiver: packet_data.receiver, class: packet_data.class_id, tokens: packet_data.token_ids, - memo: packet_data.memo, + memo: packet_data.memo.unwrap_or_default(), }; send_packet_ctx_a.emit_ibc_event(ModuleEvent::from(transfer_event).into())?; diff --git a/ibc-apps/ics721-nft-transfer/src/module.rs b/ibc-apps/ics721-nft-transfer/src/module.rs index efbd10b18..fc4097016 100644 --- a/ibc-apps/ics721-nft-transfer/src/module.rs +++ b/ibc-apps/ics721-nft-transfer/src/module.rs @@ -194,7 +194,7 @@ pub fn on_recv_packet_execute( receiver: data.receiver, class: data.class_id, tokens: data.token_ids, - memo: data.memo, + memo: data.memo.unwrap_or_default(), success: ack.is_successful(), }; extras.events.push(recv_event.into()); @@ -259,7 +259,7 @@ pub fn on_acknowledgement_packet_execute( receiver: data.receiver, class: data.class_id, tokens: data.token_ids, - memo: data.memo, + memo: data.memo.unwrap_or_default(), acknowledgement: acknowledgement.clone(), }; @@ -307,7 +307,7 @@ pub fn on_timeout_packet_execute( refund_receiver: data.sender, refund_class: data.class_id, refund_tokens: data.token_ids, - memo: data.memo, + memo: data.memo.unwrap_or_default(), }; let extras = ModuleExtras { diff --git a/ibc-apps/ics721-nft-transfer/types/src/data.rs b/ibc-apps/ics721-nft-transfer/types/src/data.rs index 8a7de5d4e..098c8c094 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/data.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -2,6 +2,8 @@ use core::fmt::{self, Display, Formatter}; use core::str::FromStr; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; use ibc_core::primitives::prelude::*; use mime::Mime; @@ -19,7 +21,6 @@ use crate::error::NftTransferError; feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize) )] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, PartialEq, Eq, derive_more::From)] pub struct Data(String); @@ -45,6 +46,29 @@ impl FromStr for Data { } } +impl serde::Serialize for Data { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&BASE64_STANDARD.encode(&self.0)) + } +} + +impl<'de> serde::Deserialize<'de> for Data { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let encoded = String::deserialize(deserializer)?; + let decoded = BASE64_STANDARD + .decode(encoded) + .map_err(serde::de::Error::custom)?; + let decoded_str = String::from_utf8(decoded).map_err(serde::de::Error::custom)?; + Ok(Data(decoded_str)) + } +} + #[cfg_attr( feature = "parity-scale-codec", derive( diff --git a/ibc-apps/ics721-nft-transfer/types/src/memo.rs b/ibc-apps/ics721-nft-transfer/types/src/memo.rs index 9db8e4e11..432dd4fc6 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/memo.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/memo.rs @@ -24,7 +24,7 @@ use ibc_core::primitives::prelude::*; )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Memo(String); impl AsRef for Memo { diff --git a/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs b/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs index 49ea39363..357723f61 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs @@ -66,6 +66,12 @@ impl TryFrom for MsgTransfer { return Err(ContextError::from(PacketError::MissingTimeout))?; } + let memo = if raw_msg.memo.is_empty() { + None + } else { + Some(raw_msg.memo.into()) + }; + Ok(MsgTransfer { port_id_on_a: raw_msg.source_port.parse()?, chan_id_on_a: raw_msg.source_channel.parse()?, @@ -74,11 +80,11 @@ impl TryFrom for MsgTransfer { class_uri: None, class_data: None, token_ids: raw_msg.token_ids.try_into()?, - token_uris: vec![], - token_data: vec![], + token_uris: None, + token_data: None, sender: raw_msg.sender.into(), receiver: raw_msg.receiver.into(), - memo: raw_msg.memo.into(), + memo, }, timeout_height_on_b, timeout_timestamp_on_b, @@ -103,7 +109,11 @@ impl From for RawMsgTransfer { receiver: domain_msg.packet_data.receiver.to_string(), timeout_height: domain_msg.timeout_height_on_b.into(), timeout_timestamp: domain_msg.timeout_timestamp_on_b.nanoseconds(), - memo: domain_msg.packet_data.memo.to_string(), + memo: domain_msg + .packet_data + .memo + .map(|m| m.to_string()) + .unwrap_or_default(), } } } diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 6ced166e5..2484ae36e 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -1,24 +1,17 @@ //! Contains the `PacketData` type that defines the structure of NFT transfers' packet bytes -use core::convert::TryFrom; - -use base64::prelude::BASE64_STANDARD; -use base64::Engine; use ibc_core::primitives::prelude::*; use ibc_core::primitives::Signer; -use ibc_proto::ibc::applications::nft_transfer::v1::NonFungibleTokenPacketData as RawPacketData; use crate::class::{ClassData, ClassUri, PrefixedClassId}; use crate::error::NftTransferError; use crate::memo::Memo; +use crate::serializers; use crate::token::{TokenData, TokenIds, TokenUri}; /// Defines the structure of token transfers' packet bytes #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(try_from = "RawPacketData", into = "RawPacketData") -)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr( feature = "parity-scale-codec", @@ -30,55 +23,38 @@ use crate::token::{TokenData, TokenIds, TokenUri}; )] #[derive(Clone, Debug, PartialEq, Eq)] pub struct PacketData { + #[cfg_attr(feature = "serde", serde(with = "serializers"))] + #[cfg_attr(feature = "schema", schemars(with = "String"))] pub class_id: PrefixedClassId, pub class_uri: Option, pub class_data: Option, pub token_ids: TokenIds, - pub token_uris: Vec, - pub token_data: Vec, + // Need `Option` to decode `null` value + pub token_uris: Option>, + // Need `Option` to decode `null` value + pub token_data: Option>, pub sender: Signer, pub receiver: Signer, - pub memo: Memo, + pub memo: Option, } impl PacketData { - #[allow(clippy::too_many_arguments)] - pub fn new( - class_id: PrefixedClassId, - class_uri: Option, - class_data: Option, - token_ids: TokenIds, - token_uris: Vec, - token_data: Vec, - sender: Signer, - receiver: Signer, - memo: Memo, - ) -> Result { - let packet_data = Self { - class_id, - class_uri, - class_data, - token_ids, - token_uris, - token_data, - sender, - receiver, - memo, - }; - - packet_data.validate_basic()?; - - Ok(packet_data) - } - /// Performs the basic validation of the packet data fields. pub fn validate_basic(&self) -> Result<(), NftTransferError> { if self.token_ids.0.is_empty() { return Err(NftTransferError::NoTokenId); } let num = self.token_ids.0.len(); - let num_uri = self.token_uris.len(); - let num_data = self.token_data.len(); + let num_uri = self + .token_uris + .as_ref() + .map(|t| t.len()) + .unwrap_or_default(); + let num_data = self + .token_data + .as_ref() + .map(|t| t.len()) + .unwrap_or_default(); if (num_uri != 0 && num_uri != num) || (num_data != 0 && num_data != num) { return Err(NftTransferError::TokenMismatched); } @@ -86,86 +62,6 @@ impl PacketData { } } -impl TryFrom for PacketData { - type Error = NftTransferError; - - fn try_from(raw_pkt_data: RawPacketData) -> Result { - let class_uri = if raw_pkt_data.class_uri.is_empty() { - None - } else { - Some(raw_pkt_data.class_uri.parse()?) - }; - let class_data = if raw_pkt_data.class_data.is_empty() { - None - } else { - let decoded = BASE64_STANDARD - .decode(raw_pkt_data.class_data) - .map_err(|_| NftTransferError::InvalidJsonData)?; - let data_str = - String::from_utf8(decoded).map_err(|_| NftTransferError::InvalidJsonData)?; - Some(data_str.parse()?) - }; - - let token_ids = raw_pkt_data.token_ids.try_into()?; - let token_uris: Result, _> = - raw_pkt_data.token_uris.iter().map(|t| t.parse()).collect(); - let token_data: Result, _> = raw_pkt_data - .token_data - .iter() - .map(|data| { - let decoded = BASE64_STANDARD - .decode(data) - .map_err(|_| NftTransferError::InvalidJsonData)?; - let data_str = - String::from_utf8(decoded).map_err(|_| NftTransferError::InvalidJsonData)?; - data_str.parse() - }) - .collect(); - Self::new( - raw_pkt_data.class_id.parse()?, - class_uri, - class_data, - token_ids, - token_uris?, - token_data?, - raw_pkt_data.sender.into(), - raw_pkt_data.receiver.into(), - raw_pkt_data.memo.into(), - ) - } -} - -impl From for RawPacketData { - fn from(pkt_data: PacketData) -> Self { - Self { - class_id: pkt_data.class_id.to_string(), - class_uri: pkt_data - .class_uri - .map(|c| c.to_string()) - .unwrap_or_default(), - class_data: pkt_data - .class_data - .map(|c| BASE64_STANDARD.encode(c.to_string())) - .unwrap_or_default(), - token_ids: pkt_data - .token_ids - .as_ref() - .iter() - .map(|t| t.to_string()) - .collect(), - token_uris: pkt_data.token_uris.iter().map(|t| t.to_string()).collect(), - token_data: pkt_data - .token_data - .iter() - .map(|t| BASE64_STANDARD.encode(t.to_string())) - .collect(), - sender: pkt_data.sender.to_string(), - receiver: pkt_data.receiver.to_string(), - memo: pkt_data.memo.to_string(), - } - } -} - #[cfg(test)] mod tests { use core::str::FromStr; @@ -179,7 +75,7 @@ mod tests { r#"{"image":{"value":"binary","mime":"image/png"},"name":{"value":"Crypto Creatures"}}"#; impl PacketData { - pub fn new_dummy() -> Self { + pub fn new_dummy(memo: Option<&str>) -> Self { let address: Signer = DUMMY_ADDRESS.to_string().into(); Self { @@ -188,17 +84,17 @@ mod tests { class_data: Some(ClassData::from_str(DUMMY_DATA).unwrap()), token_ids: TokenIds::try_from(vec!["token_0".to_string(), "token_1".to_string()]) .unwrap(), - token_uris: vec![ + token_uris: Some(vec![ TokenUri::from_str(DUMMY_URI).unwrap(), TokenUri::from_str(DUMMY_URI).unwrap(), - ], - token_data: vec![ + ]), + token_data: Some(vec![ TokenData::from_str(DUMMY_DATA).unwrap(), TokenData::from_str(DUMMY_DATA).unwrap(), - ], + ]), sender: address.clone(), receiver: address, - memo: "".to_string().into(), + memo: memo.map(|m| m.to_string().into()), } } @@ -210,11 +106,11 @@ mod tests { class_uri: None, class_data: None, token_ids: TokenIds::try_from(vec!["token_0".to_string()]).unwrap(), - token_uris: vec![], - token_data: vec![], + token_uris: None, + token_data: None, sender: address.clone(), receiver: address, - memo: "".to_string().into(), + memo: None, } } @@ -230,8 +126,8 @@ mod tests { assert!(data.as_ref().parse_as_ics721_data().is_ok()); }; - if !deser.token_data.is_empty() { - for data in deser.token_data.iter() { + if let Some(token_data) = &deser.token_data { + for data in token_data.iter() { assert!(data.as_ref().parse_as_ics721_data().is_ok()); } } @@ -245,7 +141,7 @@ mod tests { } fn dummy_json_packet_data() -> &'static str { - r#"{"classId":"class","classUri":"http://example.com/","classData":"eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0=","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com/","http://example.com/"],"tokenData":["eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0=","eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0="],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":""}"# + r#"{"classId":"class","classUri":"http://example.com/","classData":"eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0=","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com/","http://example.com/"],"tokenData":["eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0=","eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0="],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":"memo"}"# } fn dummy_json_packet_data_without_memo() -> &'static str { @@ -256,15 +152,15 @@ mod tests { /// `RawPacketData` and then serializing that. #[test] fn test_packet_data_ser() { - PacketData::new_dummy().ser_json_assert_eq(dummy_json_packet_data()); + PacketData::new_dummy(Some("memo")).ser_json_assert_eq(dummy_json_packet_data()); } /// Ensures `PacketData` properly decodes from JSON by first deserializing to a /// `RawPacketData` and then converting from that. #[test] fn test_packet_data_deser() { - PacketData::new_dummy().deser_json_assert_eq(dummy_json_packet_data()); - PacketData::new_dummy().deser_json_assert_eq(dummy_json_packet_data_without_memo()); + PacketData::new_dummy(Some("memo")).deser_json_assert_eq(dummy_json_packet_data()); + PacketData::new_dummy(None).deser_json_assert_eq(dummy_json_packet_data_without_memo()); PacketData::new_min_dummy().deser_json_assert_eq(dummy_min_json_packet_data()); } From 5c9c52bf2bd39d689655da5b0b7dc27f4868137f Mon Sep 17 00:00:00 2001 From: yito88 Date: Mon, 22 Jan 2024 16:30:33 +0100 Subject: [PATCH 8/9] add a test --- ibc-apps/ics721-nft-transfer/types/src/packet.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 2484ae36e..79d4f08a2 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -140,6 +140,10 @@ mod tests { r#"{"classId":"class","tokenIds":["token_0"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# } + fn dummy_min_json_packet_data_with_null() -> &'static str { + r#"{"classId":"class","classUri":null,"classData":null,"tokenIds":["token_0"],"tokenUris":null,"tokenData":null,"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# + } + fn dummy_json_packet_data() -> &'static str { r#"{"classId":"class","classUri":"http://example.com/","classData":"eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0=","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com/","http://example.com/"],"tokenData":["eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0=","eyJpbWFnZSI6eyJ2YWx1ZSI6ImJpbmFyeSIsIm1pbWUiOiJpbWFnZS9wbmcifSwibmFtZSI6eyJ2YWx1ZSI6IkNyeXB0byBDcmVhdHVyZXMifX0="],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":"memo"}"# } @@ -162,6 +166,7 @@ mod tests { PacketData::new_dummy(Some("memo")).deser_json_assert_eq(dummy_json_packet_data()); PacketData::new_dummy(None).deser_json_assert_eq(dummy_json_packet_data_without_memo()); PacketData::new_min_dummy().deser_json_assert_eq(dummy_min_json_packet_data()); + PacketData::new_min_dummy().deser_json_assert_eq(dummy_min_json_packet_data_with_null()); } #[test] From d0a34999a7ddcb429b7cdf166bdebf7830afd5d5 Mon Sep 17 00:00:00 2001 From: yito88 Date: Mon, 22 Jan 2024 21:10:19 +0100 Subject: [PATCH 9/9] restore conversions --- ibc-apps/ics721-nft-transfer/src/context.rs | 4 +- .../ics721-nft-transfer/types/src/packet.rs | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs index 81dda2f87..34fec88dd 100644 --- a/ibc-apps/ics721-nft-transfer/src/context.rs +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -53,7 +53,7 @@ pub trait NftTransferValidationContext { /// /// Note: some existing ICS-721 implementations may not strictly adhere to /// the ICS-721 class data structure. The - /// [`ClassData`](crate::types::ClassData) associated with this + /// [`ClassData`] associated with this /// implementation can take any valid JSON format. If your project requires /// ICS-721 format for the `ClassData`, ensure correctness by checking with /// [`parse_as_ics721_data()`](crate::types::Data::parse_as_ics721_data). @@ -93,7 +93,7 @@ pub trait NftTransferValidationContext { /// /// Note: some existing ICS-721 implementations may not strictly adhere to /// the ICS-721 token data structure. The - /// [`TokenData`](crate::types::TokenData) associated with this + /// [`TokenData`] associated with this /// implementation can take any valid JSON format. If your project requires /// ICS-721 format for `TokenData`, ensure correctness by checking with /// [`parse_as_ics721_data()`](crate::types::Data::parse_as_ics721_data). diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 79d4f08a2..a870ae436 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -1,7 +1,12 @@ //! Contains the `PacketData` type that defines the structure of NFT transfers' packet bytes +use core::convert::TryFrom; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; use ibc_core::primitives::prelude::*; use ibc_core::primitives::Signer; +use ibc_proto::ibc::applications::nft_transfer::v1::NonFungibleTokenPacketData as RawPacketData; use crate::class::{ClassData, ClassUri, PrefixedClassId}; use crate::error::NftTransferError; @@ -39,6 +44,51 @@ pub struct PacketData { } impl PacketData { + #[allow(clippy::too_many_arguments)] + pub fn new( + class_id: PrefixedClassId, + class_uri: Option, + class_data: Option, + token_ids: TokenIds, + token_uris: Vec, + token_data: Vec, + sender: Signer, + receiver: Signer, + memo: Memo, + ) -> Result { + let token_uris = if token_uris.is_empty() { + None + } else { + Some(token_uris) + }; + let token_data = if token_data.is_empty() { + None + } else { + Some(token_data) + }; + let memo = if memo.as_ref().is_empty() { + None + } else { + Some(memo) + }; + + let packet_data = Self { + class_id, + class_uri, + class_data, + token_ids, + token_uris, + token_data, + sender, + receiver, + memo, + }; + + packet_data.validate_basic()?; + + Ok(packet_data) + } + /// Performs the basic validation of the packet data fields. pub fn validate_basic(&self) -> Result<(), NftTransferError> { if self.token_ids.0.is_empty() { @@ -62,6 +112,92 @@ impl PacketData { } } +impl TryFrom for PacketData { + type Error = NftTransferError; + + fn try_from(raw_pkt_data: RawPacketData) -> Result { + let class_uri = if raw_pkt_data.class_uri.is_empty() { + None + } else { + Some(raw_pkt_data.class_uri.parse()?) + }; + let class_data = if raw_pkt_data.class_data.is_empty() { + None + } else { + let decoded = BASE64_STANDARD + .decode(raw_pkt_data.class_data) + .map_err(|_| NftTransferError::InvalidJsonData)?; + let data_str = + String::from_utf8(decoded).map_err(|_| NftTransferError::InvalidJsonData)?; + Some(data_str.parse()?) + }; + + let token_ids = raw_pkt_data.token_ids.try_into()?; + let token_uris: Result, _> = + raw_pkt_data.token_uris.iter().map(|t| t.parse()).collect(); + let token_data: Result, _> = raw_pkt_data + .token_data + .iter() + .map(|data| { + let decoded = BASE64_STANDARD + .decode(data) + .map_err(|_| NftTransferError::InvalidJsonData)?; + let data_str = + String::from_utf8(decoded).map_err(|_| NftTransferError::InvalidJsonData)?; + data_str.parse() + }) + .collect(); + Self::new( + raw_pkt_data.class_id.parse()?, + class_uri, + class_data, + token_ids, + token_uris?, + token_data?, + raw_pkt_data.sender.into(), + raw_pkt_data.receiver.into(), + raw_pkt_data.memo.into(), + ) + } +} + +impl From for RawPacketData { + fn from(pkt_data: PacketData) -> Self { + Self { + class_id: pkt_data.class_id.to_string(), + class_uri: pkt_data + .class_uri + .map(|c| c.to_string()) + .unwrap_or_default(), + class_data: pkt_data + .class_data + .map(|c| BASE64_STANDARD.encode(c.to_string())) + .unwrap_or_default(), + token_ids: pkt_data + .token_ids + .as_ref() + .iter() + .map(|t| t.to_string()) + .collect(), + token_uris: pkt_data + .token_uris + .map(|uris| uris.iter().map(|t| t.to_string()).collect()) + .unwrap_or_default(), + token_data: pkt_data + .token_data + .map(|data| { + data.iter() + .map(|t| BASE64_STANDARD.encode(t.to_string())) + .collect() + }) + .unwrap_or_default(), + sender: pkt_data.sender.to_string(), + receiver: pkt_data.receiver.to_string(), + memo: pkt_data.memo.map(|m| m.to_string()).unwrap_or_default(), + } + } +} + #[cfg(test)] mod tests { use core::str::FromStr;