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..34fec88dd 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. @@ -50,11 +50,18 @@ 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`] 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, - class_uri: &ClassUri, - class_data: &ClassData, + class_uri: Option<&ClassUri>, + class_data: Option<&ClassData>, ) -> Result<(), NftTransferError>; /// Validates that the tokens can be escrowed successfully. @@ -83,13 +90,20 @@ 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`] 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, 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 +147,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 +181,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..2adbb47f6 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.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(()) } } @@ -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.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 768a7f757..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 @@ -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.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), 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..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,19 +81,35 @@ 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)?; - packet_data.token_uris.push(nft.get_uri().clone()); - packet_data.token_data.push(nft.get_data().clone()); + // Set the URI and the data if both exists + if let (Some(uri), Some(data)) = (nft.get_uri(), nft.get_data()) { + 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()]), + } + } } + packet_data.validate_basic()?; + 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) @@ -150,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( @@ -160,19 +184,33 @@ 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)?; - packet_data.token_uris.push(nft.get_uri().clone()); - packet_data.token_data.push(nft.get_data().clone()); + // Set the URI and the data if both exists + if let (Some(uri), Some(data)) = (nft.get_uri(), nft.get_data()) { + 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()]), + } + } } 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 = { @@ -204,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/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 a5fe516c0..098c8c094 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/data.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -2,11 +2,73 @@ 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; use crate::error::NftTransferError; +#[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 = "schema", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, Default, PartialEq, Eq, derive_more::From)] +pub struct Data(String); + +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) + } +} + +impl FromStr for Data { + type Err = NftTransferError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +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( @@ -22,7 +84,15 @@ 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(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) + } +} #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DataValue { @@ -169,23 +239,6 @@ impl schemars::JsonSchema for DataValue { } } -impl Display for Data { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", serde_json::to_string(&self.0).expect("infallible")) - } -} - -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(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..08ab598e5 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/error.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/error.rs @@ -46,10 +46,12 @@ 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, + /// 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/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 7db6a3504..a870ae436 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -11,14 +11,12 @@ use ibc_proto::ibc::applications::nft_transfer::v1::NonFungibleTokenPacketData a 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,15 +28,19 @@ 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 { @@ -54,16 +56,23 @@ 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 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, @@ -73,7 +82,33 @@ 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 + .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); + } + Ok(()) } } @@ -144,15 +179,21 @@ impl From for RawPacketData { .iter() .map(|t| t.to_string()) .collect(), - token_uris: pkt_data.token_uris.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 - .iter() - .map(|t| BASE64_STANDARD.encode(t.to_string())) - .collect(), + .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.to_string(), + memo: pkt_data.memo.map(|m| m.to_string()).unwrap_or_default(), } } } @@ -167,10 +208,10 @@ 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 { + pub fn new_dummy(memo: Option<&str>) -> Self { let address: Signer = DUMMY_ADDRESS.to_string().into(); Self { @@ -179,17 +220,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()), } } @@ -201,11 +242,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, } } @@ -216,6 +257,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 let Some(token_data) = &deser.token_data { + for data in token_data.iter() { + assert!(data.as_ref().parse_as_ics721_data().is_ok()); + } + } + assert_eq!(&deser, self); } } @@ -224,8 +276,12 @@ 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":""}"# + 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 { @@ -236,16 +292,17 @@ 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()); + PacketData::new_min_dummy().deser_json_assert_eq(dummy_min_json_packet_data_with_null()); } #[test] 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 { 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,