From f3806e549572c4e4bbd885c91d63f34aab9f5f43 Mon Sep 17 00:00:00 2001 From: yito88 Date: Mon, 22 Jan 2024 00:31:19 +0100 Subject: [PATCH] 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()); }