From e5e8464972c04c21cba9ed8686c6d723c85aa71a Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Wed, 20 Dec 2023 09:45:24 -0800 Subject: [PATCH 1/8] feat: establish ICS-721 boilerplate, ready for new additions (#1012) * chore: establish ics721 boilerplate, ready for new additions * nit --- Cargo.toml | 2 + ibc-apps/Cargo.toml | 8 +- ibc-apps/README.md | 5 + ibc-apps/ics721-nft-transfer/Cargo.toml | 53 ++++++ ibc-apps/ics721-nft-transfer/src/context.rs | 8 + .../ics721-nft-transfer/src/handler/mod.rs | 2 + ibc-apps/ics721-nft-transfer/src/lib.rs | 30 ++++ ibc-apps/ics721-nft-transfer/src/module.rs | 167 ++++++++++++++++++ ibc-apps/ics721-nft-transfer/types/Cargo.toml | 71 ++++++++ .../ics721-nft-transfer/types/src/error.rs | 50 ++++++ .../ics721-nft-transfer/types/src/events.rs | 1 + ibc-apps/ics721-nft-transfer/types/src/lib.rs | 22 +++ .../ics721-nft-transfer/types/src/msgs/mod.rs | 1 + ibc-apps/src/lib.rs | 8 + 14 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 ibc-apps/ics721-nft-transfer/Cargo.toml create mode 100644 ibc-apps/ics721-nft-transfer/src/context.rs create mode 100644 ibc-apps/ics721-nft-transfer/src/handler/mod.rs create mode 100644 ibc-apps/ics721-nft-transfer/src/lib.rs create mode 100644 ibc-apps/ics721-nft-transfer/src/module.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/Cargo.toml create mode 100644 ibc-apps/ics721-nft-transfer/types/src/error.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/events.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/lib.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 39f779901..8f52db0f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ members = [ "ibc-clients", "ibc-apps/ics20-transfer/types", "ibc-apps/ics20-transfer", + "ibc-apps/ics721-nft-transfer/types", + "ibc-apps/ics721-nft-transfer", "ibc-apps", "ibc-core/ics24-host/cosmos", "ibc-data-types", diff --git a/ibc-apps/Cargo.toml b/ibc-apps/Cargo.toml index 9057c5e7f..eb5811982 100644 --- a/ibc-apps/Cargo.toml +++ b/ibc-apps/Cargo.toml @@ -17,24 +17,30 @@ description = """ all-features = true [dependencies] -ibc-app-transfer = { workspace = true } +ibc-app-transfer = { workspace = true } +ibc-app-nft-transfer = { workspace = true } [features] default = ["std"] std = [ "ibc-app-transfer/std", + "ibc-app-nft-transfer/std", ] serde = [ "ibc-app-transfer/serde", + "ibc-app-nft-transfer/serde", ] schema = [ "ibc-app-transfer/schema", + "ibc-app-nft-transfer/schema", "serde", "std", ] borsh = [ "ibc-app-transfer/borsh", + "ibc-app-nft-transfer/borsh", ] parity-scale-codec = [ "ibc-app-transfer/parity-scale-codec", + "ibc-app-nft-transfer/parity-scale-codec", ] diff --git a/ibc-apps/README.md b/ibc-apps/README.md index 71c8848df..103e68bc2 100644 --- a/ibc-apps/README.md +++ b/ibc-apps/README.md @@ -26,6 +26,11 @@ applications: - [ibc-app-transfer](./../ibc-apps/ics20-transfer) - [ibc-app-transfer-types](./../ibc-apps/ics20-transfer/types) +### ICS-721: Non-Fungible Token Transfer Application + +- [ibc-app-nft-transfer](./../ibc-apps/ics721-nft-transfer) +- [ibc-app-nft-transfer-types](./../ibc-apps/ics721-nft-transfer/types) + ## Contributing IBC is specified in English in the [cosmos/ibc diff --git a/ibc-apps/ics721-nft-transfer/Cargo.toml b/ibc-apps/ics721-nft-transfer/Cargo.toml new file mode 100644 index 000000000..02cad3270 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "ibc-app-nft-transfer" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +keywords = ["cosmos", "ibc", "nft", "transfer", "ics721"] +readme = "./../README.md" +description = """ + Maintained by `ibc-rs`, contains the implementation of the ICS-721 Non-Fungible Token Transfer + application logic and re-exports essential data structures and domain types from + `ibc-app-nft-transfer-types` crate. +""" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +# external dependencies +serde_json = { workspace = true, optional = true } + +# ibc dependencies +ibc-app-nft-transfer-types = { workspace = true } +ibc-core = { workspace = true } + +[features] +default = ["std"] +std = [ + "ibc-app-nft-transfer-types/std", + "ibc-core/std", + "serde_json/std", +] +serde = [ + "ibc-app-nft-transfer-types/serde", + "ibc-core/serde", + "serde_json" +] +schema = [ + "ibc-app-nft-transfer-types/schema", + "ibc-core/schema", + "serde", + "std", +] +borsh = [ + "ibc-app-nft-transfer-types/borsh", + "ibc-core/borsh", +] +parity-scale-codec = [ + "ibc-app-nft-transfer-types/parity-scale-codec", + "ibc-core/parity-scale-codec", +] \ No newline at end of file diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs new file mode 100644 index 000000000..fe7967a17 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -0,0 +1,8 @@ +//! Defines the required context traits for ICS-721 to interact with host +//! machine. + +/// Read-only methods required in NFT transfer validation context. +pub trait NftTransferValidationContext {} + +/// Read-write methods required in NFT transfer execution context. +pub trait NftTransferExecutionContext: NftTransferValidationContext {} diff --git a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs new file mode 100644 index 000000000..d56606e9d --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs @@ -0,0 +1,2 @@ +//! Implements IBC handlers responsible for processing Non-Fungible Token +//! Transfers (ICS-721) messages. diff --git a/ibc-apps/ics721-nft-transfer/src/lib.rs b/ibc-apps/ics721-nft-transfer/src/lib.rs new file mode 100644 index 000000000..fe73af5a9 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/src/lib.rs @@ -0,0 +1,30 @@ +//! Implementation of the IBC [Non-Fungible Token +//! Transfer](https://github.com/cosmos/ibc/blob/main/spec/app/ics-721-nft-transfer/README.md) +//! (ICS-721) application logic. +#![no_std] +#![forbid(unsafe_code)] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::disallowed_methods, clippy::disallowed_types))] +#![deny( + warnings, + trivial_casts, + trivial_numeric_casts, + unused_import_braces, + unused_qualifications, + rust_2018_idioms +)] + +#[cfg(any(test, feature = "std"))] +extern crate std; + +pub mod context; +pub mod handler; +pub mod module; + +/// Re-exports the implementation of the IBC [Non-Fungible Token +/// Transfer](https://github.com/cosmos/ibc/blob/main/spec/app/ics-020-fungible-token-transfer/README.md) +/// (ICS-721) data structures. +pub mod types { + #[doc(inline)] + pub use ibc_app_nft_transfer_types::*; +} diff --git a/ibc-apps/ics721-nft-transfer/src/module.rs b/ibc-apps/ics721-nft-transfer/src/module.rs new file mode 100644 index 000000000..7e83bf870 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/src/module.rs @@ -0,0 +1,167 @@ +//! Provides IBC module callbacks implementation for the ICS-721 transfer. + +use ibc_app_nft_transfer_types::error::NftTransferError; +use ibc_core::channel::types::acknowledgement::Acknowledgement; +use ibc_core::channel::types::channel::{Counterparty, Order}; +use ibc_core::channel::types::packet::Packet; +use ibc_core::channel::types::Version; +use ibc_core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc_core::primitives::Signer; +use ibc_core::router::types::module::ModuleExtras; + +use crate::context::{NftTransferExecutionContext, NftTransferValidationContext}; + +pub fn on_chan_open_init_validate( + _ctx: &impl NftTransferValidationContext, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + _version: &Version, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_open_init_execute( + _ctx: &mut impl NftTransferExecutionContext, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + _version: &Version, +) -> Result<(ModuleExtras, Version), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_open_try_validate( + _ctx: &impl NftTransferValidationContext, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + _counterparty_version: &Version, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_open_try_execute( + _ctx: &mut impl NftTransferExecutionContext, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + _counterparty_version: &Version, +) -> Result<(ModuleExtras, Version), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_open_ack_validate( + _ctx: &impl NftTransferExecutionContext, + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty_version: &Version, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_open_ack_execute( + _ctx: &mut impl NftTransferExecutionContext, + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty_version: &Version, +) -> Result { + unimplemented!() +} + +pub fn on_chan_open_confirm_validate( + _ctx: &impl NftTransferValidationContext, + _port_id: &PortId, + _channel_id: &ChannelId, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_open_confirm_execute( + _ctx: &mut impl NftTransferExecutionContext, + _port_id: &PortId, + _channel_id: &ChannelId, +) -> Result { + unimplemented!() +} + +pub fn on_chan_close_init_validate( + _ctx: &impl NftTransferValidationContext, + _port_id: &PortId, + _channel_id: &ChannelId, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_close_init_execute( + _ctx: &mut impl NftTransferExecutionContext, + _port_id: &PortId, + _channel_id: &ChannelId, +) -> Result { + unimplemented!() +} + +pub fn on_chan_close_confirm_validate( + _ctx: &impl NftTransferValidationContext, + _port_id: &PortId, + _channel_id: &ChannelId, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_chan_close_confirm_execute( + _ctx: &mut impl NftTransferExecutionContext, + _port_id: &PortId, + _channel_id: &ChannelId, +) -> Result { + unimplemented!() +} + +pub fn on_recv_packet_execute( + _ctx_b: &mut impl NftTransferExecutionContext, + _packet: &Packet, +) -> (ModuleExtras, Acknowledgement) { + unimplemented!() +} + +pub fn on_acknowledgement_packet_validate( + _ctx: &impl NftTransferValidationContext, + _packet: &Packet, + _acknowledgement: &Acknowledgement, + _relayer: &Signer, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_acknowledgement_packet_execute( + _ctx: &mut impl NftTransferExecutionContext, + _packet: &Packet, + _acknowledgement: &Acknowledgement, + _relayer: &Signer, +) -> (ModuleExtras, Result<(), NftTransferError>) { + unimplemented!() +} + +pub fn on_timeout_packet_validate( + _ctx: &impl NftTransferValidationContext, + _packet: &Packet, + _relayer: &Signer, +) -> Result<(), NftTransferError> { + unimplemented!() +} + +pub fn on_timeout_packet_execute( + _ctx: &mut impl NftTransferExecutionContext, + _packet: &Packet, + _relayer: &Signer, +) -> (ModuleExtras, Result<(), NftTransferError>) { + unimplemented!() +} diff --git a/ibc-apps/ics721-nft-transfer/types/Cargo.toml b/ibc-apps/ics721-nft-transfer/types/Cargo.toml new file mode 100644 index 000000000..ca9467185 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "ibc-app-nft-transfer-types" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +keywords = ["cosmos", "ibc", "transfer", "nft", "ics721"] +readme = "./../../README.md" +description = """ + Maintained by `ibc-rs`, encapsulates essential ICS-721 Non-Fungible Token Transfer data structures and + domain types, as specified in the Inter-Blockchain Communication (IBC) protocol. Designed for universal + applicability to facilitate development and integration across diverse IBC-enabled projects. +""" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +# external dependencies +borsh = { workspace = true, optional = true } +derive_more = { workspace = true } +displaydoc = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true, optional = true } + +# ibc dependencies +ibc-core = { workspace = true } +ibc-proto = { workspace = true } + +## parity dependencies +parity-scale-codec = { workspace = true , optional = true } +scale-info = { workspace = true , optional = true } + +[dev-dependencies] +serde_json = { workspace = true } +rstest = { workspace = true } + +[features] +default = ["std"] +std = [ + "serde/std", + "serde_json/std", + "displaydoc/std", + "ibc-core/std", + "ibc-proto/std", +] +serde = [ + "dep:serde", + "ibc-core/serde", + "ibc-proto/serde", +] +schema = [ + "dep:schemars", + "ibc-core/schema", + "ibc-proto/json-schema", + "serde", + "std" +] +borsh = [ + "dep:borsh", + "ibc-core/borsh", + "ibc-proto/borsh" +] +parity-scale-codec = [ + "dep:parity-scale-codec", + "dep:scale-info", + "ibc-core/parity-scale-codec", + "ibc-proto/parity-scale-codec" +] diff --git a/ibc-apps/ics721-nft-transfer/types/src/error.rs b/ibc-apps/ics721-nft-transfer/types/src/error.rs new file mode 100644 index 000000000..65b857dcf --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/error.rs @@ -0,0 +1,50 @@ +//! Defines the Non-Fungible Token Transfer (ICS-721) error types. +use core::convert::Infallible; + +use displaydoc::Display; +use ibc_core::channel::types::acknowledgement::StatusValue; +use ibc_core::handler::types::error::ContextError; +use ibc_core::host::types::error::IdentifierError; +use ibc_core::primitives::prelude::*; + +#[derive(Display, Debug)] +pub enum NftTransferError { + /// context error: `{0}` + ContextError(ContextError), + /// invalid identifier: `{0}` + InvalidIdentifier(IdentifierError), +} + +#[cfg(feature = "std")] +impl std::error::Error for NftTransferError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self { + Self::ContextError(e) => Some(e), + Self::InvalidIdentifier(e) => Some(e), + } + } +} + +impl From for NftTransferError { + fn from(e: Infallible) -> Self { + match e {} + } +} + +impl From for NftTransferError { + fn from(err: ContextError) -> NftTransferError { + Self::ContextError(err) + } +} + +impl From for NftTransferError { + fn from(err: IdentifierError) -> NftTransferError { + Self::InvalidIdentifier(err) + } +} + +impl From for StatusValue { + fn from(err: NftTransferError) -> Self { + StatusValue::new(err.to_string()).expect("error message must not be empty") + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/events.rs b/ibc-apps/ics721-nft-transfer/types/src/events.rs new file mode 100644 index 000000000..8130bb580 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/events.rs @@ -0,0 +1 @@ +//! Defines Non-Fungible Token Transfer (ICS-721) event types. diff --git a/ibc-apps/ics721-nft-transfer/types/src/lib.rs b/ibc-apps/ics721-nft-transfer/types/src/lib.rs new file mode 100644 index 000000000..a77f26c1b --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/lib.rs @@ -0,0 +1,22 @@ +//! Implementation of the IBC [Non-Fungible Token +//! Transfer](https://github.com/cosmos/ibc/blob/main/spec/app/ics-721-nft-transfer/README.md) +//! (ICS-721) data structures. +#![no_std] +#![forbid(unsafe_code)] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::disallowed_methods, clippy::disallowed_types))] +#![deny( + warnings, + trivial_casts, + trivial_numeric_casts, + unused_import_braces, + unused_qualifications, + rust_2018_idioms +)] + +#[cfg(any(test, feature = "std"))] +extern crate std; + +pub mod error; +pub mod events; +pub mod msgs; diff --git a/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs b/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs new file mode 100644 index 000000000..0c735cd44 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs @@ -0,0 +1 @@ +//! Defines the Non-Fungible Token Transfer (ICS-721) message types. diff --git a/ibc-apps/src/lib.rs b/ibc-apps/src/lib.rs index 246713011..e33de2971 100644 --- a/ibc-apps/src/lib.rs +++ b/ibc-apps/src/lib.rs @@ -18,3 +18,11 @@ pub mod transfer { #[doc(inline)] pub use ibc_app_transfer::*; } + +/// Re-exports the implementation of the IBC [Non-Fungible Token +/// Transfer](https://github.com/cosmos/ibc/blob/main/spec/app/ics-721-nft-transfer/README.md) +/// (ICS-721) application logic. +pub mod nft_transfer { + #[doc(inline)] + pub use ibc_app_nft_transfer::*; +} From b7b42e5cfe4de5e8f7b3e063d84f5b5ae0d71c72 Mon Sep 17 00:00:00 2001 From: Yuji Ito Date: Thu, 4 Jan 2024 22:19:29 +0100 Subject: [PATCH 2/8] Implement ICS-721 NFT transfer (#1020) * WIP: add types and contexts * WIP: add events * WIP: implement modules * add send_transfer * add recv and refund handlers * add tests * fix send and recv * fix context and add tests * fix fmt * fix for CI * fix messages and serde * fix comments --- Cargo.toml | 5 +- ci/no-std-check/Cargo.lock | 639 ++++++++++++------ .../ics20-transfer/types/src/msgs/transfer.rs | 4 +- ibc-apps/ics721-nft-transfer/src/context.rs | 177 ++++- .../ics721-nft-transfer/src/handler/mod.rs | 87 +++ .../src/handler/on_recv_packet.rs | 140 ++++ .../src/handler/send_transfer.rs | 215 ++++++ ibc-apps/ics721-nft-transfer/src/module.rs | 297 ++++++-- ibc-apps/ics721-nft-transfer/types/Cargo.toml | 4 +- .../ics721-nft-transfer/types/src/class.rs | 561 +++++++++++++++ .../ics721-nft-transfer/types/src/data.rs | 187 +++++ .../ics721-nft-transfer/types/src/error.rs | 88 +++ .../ics721-nft-transfer/types/src/events.rs | 208 ++++++ ibc-apps/ics721-nft-transfer/types/src/lib.rs | 47 +- .../ics721-nft-transfer/types/src/memo.rs | 54 ++ .../ics721-nft-transfer/types/src/msgs/mod.rs | 1 + .../types/src/msgs/transfer.rs | 128 ++++ .../ics721-nft-transfer/types/src/packet.rs | 246 +++++++ .../types/src/serializers.rs | 27 + .../ics721-nft-transfer/types/src/token.rs | 221 ++++++ ibc-primitives/src/prelude.rs | 1 + .../src/testapp/ibc/applications/mod.rs | 1 + .../ibc/applications/nft_transfer/context.rs | 185 +++++ .../ibc/applications/nft_transfer/mod.rs | 4 + .../ibc/applications/nft_transfer/module.rs | 107 +++ .../ibc/applications/nft_transfer/types.rs | 61 ++ ibc-testkit/tests/applications/mod.rs | 2 + .../tests/applications/nft_transfer.rs | 135 ++++ 28 files changed, 3561 insertions(+), 271 deletions(-) create mode 100644 ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs create mode 100644 ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/class.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/data.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/memo.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/packet.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/serializers.rs create mode 100644 ibc-apps/ics721-nft-transfer/types/src/token.rs create mode 100644 ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs create mode 100644 ibc-testkit/src/testapp/ibc/applications/nft_transfer/mod.rs create mode 100644 ibc-testkit/src/testapp/ibc/applications/nft_transfer/module.rs create mode 100644 ibc-testkit/src/testapp/ibc/applications/nft_transfer/types.rs create mode 100644 ibc-testkit/tests/applications/nft_transfer.rs diff --git a/Cargo.toml b/Cargo.toml index 8f52db0f2..65f0f7695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ ibc-core-handler = { version = "0.49.1", path = "./ibc-core/ics25-handler", ibc-core-router = { version = "0.49.1", path = "./ibc-core/ics26-routing", default-features = false } ibc-client-tendermint = { version = "0.49.1", path = "./ibc-clients/ics07-tendermint", default-features = false } ibc-app-transfer = { version = "0.49.1", path = "./ibc-apps/ics20-transfer", default-features = false } +ibc-app-nft-transfer = { version = "0.49.1", path = "./ibc-apps/ics721-nft-transfer", default-features = false } ibc-core-client-context = { version = "0.49.1", path = "./ibc-core/ics02-client/context", default-features = false } ibc-core-client-types = { version = "0.49.1", path = "./ibc-core/ics02-client/types", default-features = false } @@ -87,8 +88,10 @@ ibc-core-handler-types = { version = "0.49.1", path = "./ibc-core/ics25-han ibc-core-router-types = { version = "0.49.1", path = "./ibc-core/ics26-routing/types", default-features = false } ibc-client-tendermint-types = { version = "0.49.1", path = "./ibc-clients/ics07-tendermint/types", default-features = false } 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 = { 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 } # cosmos dependencies tendermint = { version = "0.34.0", default-features = false } diff --git a/ci/no-std-check/Cargo.lock b/ci/no-std-check/Cargo.lock index 40f4a67b2..9470b926c 100644 --- a/ci/no-std-check/Cargo.lock +++ b/ci/no-std-check/Cargo.lock @@ -27,7 +27,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.28.0", + "gimli 0.28.1", ] [[package]] @@ -38,25 +38,26 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -94,9 +95,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "ark-bls12-377" @@ -229,9 +230,9 @@ dependencies = [ [[package]] name = "array-bytes" -version = "6.1.0" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b1c5a481ec30a5abd8dfbd94ab5cf1bb4e9a66be7f1b3b322f2f1170c200fd" +checksum = "6f840fb7195bcfc5e17ea40c26e5ce6d5b9ce5d584466e17703209657e459ae0" [[package]] name = "arrayref" @@ -268,7 +269,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.32.1", + "object 0.32.2", "rustc-demangle", ] @@ -280,9 +281,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -307,9 +308,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bitvec" @@ -510,9 +511,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" @@ -528,9 +529,9 @@ checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpp_demangle" @@ -543,9 +544,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -649,13 +650,13 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -692,7 +693,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -703,7 +704,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -718,9 +719,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] name = "derivative" @@ -781,7 +785,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -807,15 +811,15 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ed25519" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", @@ -836,14 +840,15 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" dependencies = [ "curve25519-dalek 4.1.1", "ed25519", "serde", "sha2 0.10.8", + "subtle", "zeroize", ] @@ -881,12 +886,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -903,9 +908,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "fixed-hash" @@ -936,9 +941,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -951,9 +956,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -966,9 +971,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -976,15 +981,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -994,38 +999,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1071,9 +1076,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -1093,9 +1098,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hash-db" @@ -1118,7 +1123,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", ] [[package]] @@ -1127,14 +1132,14 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", ] [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hermit-abi" @@ -1188,18 +1193,29 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -1223,6 +1239,29 @@ dependencies = [ "ibc-primitives", ] +[[package]] +name = "ibc-app-nft-transfer" +version = "0.49.1" +dependencies = [ + "ibc-app-nft-transfer-types", + "ibc-core", + "serde-json-wasm", +] + +[[package]] +name = "ibc-app-nft-transfer-types" +version = "0.49.1" +dependencies = [ + "derive_more", + "displaydoc", + "http", + "ibc-core", + "ibc-proto 0.40.0", + "mime", + "serde", + "serde-json-wasm", +] + [[package]] name = "ibc-app-transfer" version = "0.49.1" @@ -1239,7 +1278,7 @@ dependencies = [ "derive_more", "displaydoc", "ibc-core", - "ibc-proto", + "ibc-proto 0.40.0", "primitive-types", "serde", "uint", @@ -1249,6 +1288,7 @@ dependencies = [ name = "ibc-apps" version = "0.49.1" dependencies = [ + "ibc-app-nft-transfer", "ibc-app-transfer", ] @@ -1276,7 +1316,7 @@ dependencies = [ "ibc-core-commitment-types", "ibc-core-host-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "tendermint", "tendermint-light-client-verifier", @@ -1329,7 +1369,7 @@ dependencies = [ "ibc-core-connection-types", "ibc-core-host-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "sha2 0.10.8", "subtle-encoding", @@ -1373,7 +1413,7 @@ dependencies = [ "ibc-core-commitment-types", "ibc-core-host-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "subtle-encoding", "tendermint", @@ -1386,7 +1426,7 @@ dependencies = [ "derive_more", "displaydoc", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "ics23", "serde", "subtle-encoding", @@ -1413,7 +1453,7 @@ dependencies = [ "ibc-core-commitment-types", "ibc-core-host-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "subtle-encoding", "tendermint", @@ -1446,7 +1486,7 @@ dependencies = [ "ibc-core-host-types", "ibc-core-router-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "subtle-encoding", "tendermint", @@ -1484,7 +1524,7 @@ dependencies = [ "ibc-core-handler-types", "ibc-core-host-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "sha2 0.10.8", "subtle-encoding", @@ -1522,7 +1562,7 @@ dependencies = [ "displaydoc", "ibc-core-host-types", "ibc-primitives", - "ibc-proto", + "ibc-proto 0.40.0", "serde", "subtle-encoding", "tendermint", @@ -1535,7 +1575,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -1544,7 +1584,7 @@ version = "0.49.1" dependencies = [ "derive_more", "displaydoc", - "ibc-proto", + "ibc-proto 0.40.0", "prost", "serde", "tendermint", @@ -1557,7 +1597,7 @@ version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8a8b1356652b9f160f5a010dd6b084675b8a28e163bf2b41ca5abecf27d9701" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "borsh", "bytes", "flex-error", @@ -1570,6 +1610,22 @@ dependencies = [ "tendermint-proto", ] +[[package]] +name = "ibc-proto" +version = "0.40.0" +source = "git+https://github.com/heliaxdev/ibc-proto-rs?branch=yuji/feat/ics721-impl#65354628a1085937d506c7ac94c3819315aa8494" +dependencies = [ + "base64 0.21.5", + "bytes", + "flex-error", + "ics23", + "informalsystems-pbjson 0.7.0", + "prost", + "serde", + "subtle-encoding", + "tendermint-proto", +] + [[package]] name = "ics23" version = "0.11.0" @@ -1579,7 +1635,7 @@ dependencies = [ "anyhow", "bytes", "hex", - "informalsystems-pbjson", + "informalsystems-pbjson 0.6.0", "prost", "ripemd", "serde", @@ -1595,9 +1651,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1645,12 +1701,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", ] [[package]] @@ -1663,6 +1719,16 @@ dependencies = [ "serde", ] +[[package]] +name = "informalsystems-pbjson" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa4a0980c8379295100d70854354e78df2ee1c6ca0f96ffe89afeb3140e3a3d" +dependencies = [ + "base64 0.21.5", + "serde", +] + [[package]] name = "integer-sqrt" version = "0.1.5" @@ -1703,15 +1769,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -1733,9 +1799,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libsecp256k1" @@ -1793,15 +1859,15 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1833,9 +1899,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memfd" @@ -1843,7 +1909,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix 0.38.19", + "rustix 0.38.28", ] [[package]] @@ -1876,6 +1942,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1890,12 +1962,12 @@ name = "no-std-check" version = "0.1.0" dependencies = [ "ibc", - "ibc-proto", + "ibc-proto 0.39.1", "sp-core", "sp-io", "sp-runtime", "sp-std", - "syn 2.0.38", + "syn 2.0.48", "tendermint", "tendermint-light-client-verifier", "tendermint-proto", @@ -1982,18 +2054,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -2009,9 +2081,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parity-scale-codec" -version = "3.6.5" +version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" +checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe" dependencies = [ "arrayvec 0.7.4", "bitvec", @@ -2024,11 +2096,11 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.5" +version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" +checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.1", "proc-macro2", "quote", "syn 1.0.109", @@ -2046,9 +2118,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", @@ -2083,9 +2155,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" @@ -2111,9 +2183,15 @@ dependencies = [ [[package]] name = "platforms" -version = "3.1.2" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -2150,14 +2228,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" dependencies = [ "unicode-ident", ] @@ -2182,14 +2270,14 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "prost-types" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ "prost", ] @@ -2205,9 +2293,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2277,7 +2365,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -2291,43 +2379,43 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "ref-cast" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acde58d073e9c79da00f2b5b84eed919c8326832648a5b109b3fce1bb1175280" +checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" +checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "regex" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.1", - "regex-syntax 0.8.1", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -2341,13 +2429,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.1", + "regex-syntax 0.8.2", ] [[package]] @@ -2358,9 +2446,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ripemd" @@ -2400,9 +2488,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.16" +version = "0.36.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da3636faa25820d8648e0e31c5d519bbb01f72fdf57131f0f5f7da5fed36eab" +checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" dependencies = [ "bitflags 1.3.2", "errno", @@ -2414,15 +2502,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.19" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.10", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", ] [[package]] @@ -2433,9 +2521,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "scale-info" @@ -2469,7 +2557,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "772575a524feeb803e5b0fcbc6dd9f367e579488197c94c6e4023aad2305774d" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", "cfg-if", "hashbrown 0.13.2", ] @@ -2527,15 +2615,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" dependencies = [ "serde_derive", ] @@ -2551,29 +2639,29 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.12" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -2582,13 +2670,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2648,9 +2736,12 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] [[package]] name = "slab" @@ -2663,9 +2754,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "sp-application-crypto" @@ -2765,7 +2856,7 @@ checksum = "50535e1a5708d3ba5c1195b59ebefac61cc8679c2c24716b87a86e8b7ed2e4a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2881,7 +2972,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -2945,7 +3036,7 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e359b358263cc322c3f678c272a3a519621d9853dcfa1374dfcbdb5f54c6f85" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", "hash-db", "hashbrown 0.13.2", "lazy_static", @@ -2996,9 +3087,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -3006,9 +3097,9 @@ dependencies = [ [[package]] name = "ss58-registry" -version = "1.43.0" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6915280e2d0db8911e5032a5c275571af6bdded2916abd691a659be25d3439" +checksum = "3c0c74081753a8ce1c8eb10b9f262ab6f7017e5ad3317c17a54c7ab65fcb3c6e" dependencies = [ "Inflector", "num-format", @@ -3039,9 +3130,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "substrate-bip39" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eee6965196b32f882dd2ee85a92b1dbead41b04e53907f269de3b0dc04733c" +checksum = "e620c7098893ba667438b47169c00aacdd9e7c10e042250ce2b60b087ec97328" dependencies = [ "hmac 0.11.0", "pbkdf2 0.8.0", @@ -3084,9 +3175,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3101,9 +3192,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-lexicon" -version = "0.12.11" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tendermint" @@ -3167,22 +3258,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -3197,11 +3288,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", + "powerfmt", "time-core", "time-macros", ] @@ -3276,7 +3368,18 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.1.0", "toml_datetime", "winnow", ] @@ -3300,7 +3403,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] [[package]] @@ -3315,12 +3418,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -3410,9 +3513,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -3437,9 +3540,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -3496,9 +3599,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3506,24 +3609,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3531,22 +3634,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasmparser" @@ -3671,7 +3774,7 @@ dependencies = [ "memoffset", "paste", "rand 0.8.5", - "rustix 0.36.16", + "rustix 0.36.17", "wasmtime-asm-macros", "wasmtime-environ", "wasmtime-jit-debug", @@ -3713,12 +3816,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -3739,6 +3842,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3769,6 +3881,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3781,6 +3908,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3793,6 +3926,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3805,6 +3944,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3817,6 +3962,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3829,6 +3980,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3841,6 +3998,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3853,11 +4016,17 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" -version = "0.5.16" +version = "0.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" +checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6" dependencies = [ "memchr", ] @@ -3871,11 +4040,31 @@ dependencies = [ "tap", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -3888,5 +4077,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.48", ] diff --git a/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs b/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs index 06bd48027..dcaaa521f 100644 --- a/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs +++ b/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs @@ -1,4 +1,4 @@ -//! Defines the token transfer message type +//! Defines the Non-Fungible Token Transfer message type use ibc_core::channel::types::error::PacketError; use ibc_core::channel::types::timeout::TimeoutHeight; @@ -15,7 +15,7 @@ use crate::packet::PacketData; pub(crate) const TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; -/// Message used to build an ICS20 token transfer packet. +/// Message used to build an ICS-721 Non-Fungible Token Transfer packet. /// /// Note that this message is not a packet yet, as it lacks the proper sequence /// number, and destination port/channel. This is by design. The sender of the diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs index fe7967a17..97511fd4a 100644 --- a/ibc-apps/ics721-nft-transfer/src/context.rs +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -1,8 +1,181 @@ //! Defines the required context traits for ICS-721 to interact with host //! machine. +use ibc_core::host::types::identifiers::{ChannelId, PortId}; +use ibc_core::primitives::prelude::*; +use ibc_core::primitives::Signer; + +use crate::types::error::NftTransferError; +use crate::types::{ + ClassData, ClassId, ClassUri, Memo, PrefixedClassId, TokenData, TokenId, TokenUri, +}; + +pub trait NftContext { + /// Get the class ID of the token + fn get_class_id(&self) -> &ClassId; + + /// Get the token ID + fn get_id(&self) -> &TokenId; + + /// Get the token URI + fn get_uri(&self) -> &TokenUri; + + /// Get the token Data + fn get_data(&self) -> &TokenData; +} + +pub trait NftClassContext { + /// Get the class ID + fn get_id(&self) -> &ClassId; + + /// Get the class URI + fn get_uri(&self) -> &ClassUri; + + /// Get the class Data + fn get_data(&self) -> &ClassData; +} /// Read-only methods required in NFT transfer validation context. -pub trait NftTransferValidationContext {} +pub trait NftTransferValidationContext { + type AccountId: TryFrom + PartialEq; + type Nft: NftContext; + type NftClass: NftClassContext; + + /// get_port returns the portID for the transfer module. + fn get_port(&self) -> Result; + + /// Returns Ok() if the host chain supports sending NFTs. + fn can_send_nft(&self) -> Result<(), NftTransferError>; + + /// Returns Ok() if the host chain supports receiving NFTs. + fn can_receive_nft(&self) -> Result<(), NftTransferError>; + + /// Validates that the NFT can be created or updated successfully. + fn create_or_update_class_validate( + &self, + class_id: &PrefixedClassId, + class_uri: &ClassUri, + class_data: &ClassData, + ) -> Result<(), NftTransferError>; + + /// Validates that the tokens can be escrowed successfully. + /// + /// The owner of the NFT should be checked in this validation. + /// `memo` field allows to incorporate additional contextual details in the + /// escrow validation. + fn escrow_nft_validate( + &self, + from_account: &Self::AccountId, + port_id: &PortId, + channel_id: &ChannelId, + class_id: &PrefixedClassId, + token_id: &TokenId, + memo: &Memo, + ) -> Result<(), NftTransferError>; + + /// Validates that the NFT can be unescrowed successfully. + fn unescrow_nft_validate( + &self, + to_account: &Self::AccountId, + port_id: &PortId, + channel_id: &ChannelId, + class_id: &PrefixedClassId, + token_id: &TokenId, + ) -> Result<(), NftTransferError>; + + /// Validates the receiver account and the NFT input + fn mint_nft_validate( + &self, + account: &Self::AccountId, + class_id: &PrefixedClassId, + token_id: &TokenId, + token_uri: &TokenUri, + token_data: &TokenData, + ) -> Result<(), NftTransferError>; + + /// Validates the sender account and the coin input before burning. + /// + /// The owner of the NFT should be checked in this validation. + /// `memo` field allows to incorporate additional contextual details in the + /// burn validation. + fn burn_nft_validate( + &self, + account: &Self::AccountId, + class_id: &PrefixedClassId, + token_id: &TokenId, + memo: &Memo, + ) -> Result<(), NftTransferError>; + + /// Returns a hash of the prefixed class ID. + /// Implement only if the host chain supports hashed class ID. + fn class_hash_string(&self, _class_id: &PrefixedClassId) -> Option { + None + } + + /// Returns the NFT + fn get_nft( + &self, + class_id: &PrefixedClassId, + token_id: &TokenId, + ) -> Result; + + /// Returns the NFT class + fn get_nft_class(&self, class_id: &PrefixedClassId) + -> Result; +} /// Read-write methods required in NFT transfer execution context. -pub trait NftTransferExecutionContext: NftTransferValidationContext {} +pub trait NftTransferExecutionContext: NftTransferValidationContext { + /// Creates a new NFT Class identified by classId. If the class ID already exists, it updates the class metadata. + fn create_or_update_class_execute( + &self, + class_id: &PrefixedClassId, + class_uri: &ClassUri, + class_data: &ClassData, + ) -> Result<(), NftTransferError>; + + /// Executes the escrow of the NFT in a user account. + /// + /// `memo` field allows to incorporate additional contextual details in the + /// escrow execution. + fn escrow_nft_execute( + &mut self, + from_account: &Self::AccountId, + port_id: &PortId, + channel_id: &ChannelId, + class_id: &PrefixedClassId, + token_id: &TokenId, + memo: &Memo, + ) -> Result<(), NftTransferError>; + + /// Executes the unescrow of the NFT in a user account. + fn unescrow_nft_execute( + &mut self, + to_account: &Self::AccountId, + port_id: &PortId, + channel_id: &ChannelId, + class_id: &PrefixedClassId, + token_id: &TokenId, + ) -> Result<(), NftTransferError>; + + /// Executes minting of the NFT in a user account. + fn mint_nft_execute( + &mut self, + account: &Self::AccountId, + class_id: &PrefixedClassId, + token_id: &TokenId, + token_uri: &TokenUri, + token_data: &TokenData, + ) -> Result<(), NftTransferError>; + + /// Executes burning of the NFT in a user account. + /// + /// `memo` field allows to incorporate additional contextual details in the + /// burn execution. + fn burn_nft_execute( + &mut self, + account: &Self::AccountId, + class_id: &PrefixedClassId, + token_id: &TokenId, + memo: &Memo, + ) -> Result<(), NftTransferError>; +} diff --git a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs index d56606e9d..3d577b709 100644 --- a/ibc-apps/ics721-nft-transfer/src/handler/mod.rs +++ b/ibc-apps/ics721-nft-transfer/src/handler/mod.rs @@ -1,2 +1,89 @@ //! Implements IBC handlers responsible for processing Non-Fungible Token //! Transfers (ICS-721) messages. +mod on_recv_packet; +mod send_transfer; + +use ibc_core::channel::types::packet::Packet; +pub use on_recv_packet::*; +pub use send_transfer::*; + +use crate::context::{NftTransferExecutionContext, NftTransferValidationContext}; +use crate::types::error::NftTransferError; +use crate::types::is_sender_chain_source; +use crate::types::packet::PacketData; + +pub fn refund_packet_nft_execute( + ctx_a: &mut impl NftTransferExecutionContext, + packet: &Packet, + data: &PacketData, +) -> Result<(), NftTransferError> { + let sender = data + .sender + .clone() + .try_into() + .map_err(|_| NftTransferError::ParseAccountFailure)?; + + if is_sender_chain_source( + packet.port_id_on_a.clone(), + packet.chan_id_on_a.clone(), + &data.class_id, + ) { + data.token_ids.as_ref().iter().try_for_each(|token_id| { + ctx_a.unescrow_nft_execute( + &sender, + &packet.port_id_on_a, + &packet.chan_id_on_a, + &data.class_id, + token_id, + ) + }) + } + // 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) + }) + } +} + +pub fn refund_packet_nft_validate( + ctx_a: &impl NftTransferValidationContext, + packet: &Packet, + data: &PacketData, +) -> Result<(), NftTransferError> { + let sender = data + .sender + .clone() + .try_into() + .map_err(|_| NftTransferError::ParseAccountFailure)?; + + if is_sender_chain_source( + packet.port_id_on_a.clone(), + packet.chan_id_on_a.clone(), + &data.class_id, + ) { + data.token_ids.0.iter().try_for_each(|token_id| { + ctx_a.unescrow_nft_validate( + &sender, + &packet.port_id_on_a, + &packet.chan_id_on_a, + &data.class_id, + token_id, + ) + }) + } 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) + }) + } +} 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 new file mode 100644 index 000000000..61a01fef1 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/src/handler/on_recv_packet.rs @@ -0,0 +1,140 @@ +use ibc_core::channel::types::packet::Packet; +use ibc_core::primitives::prelude::*; +use ibc_core::router::types::module::ModuleExtras; + +use crate::context::NftTransferExecutionContext; +use crate::types::error::NftTransferError; +use crate::types::events::ClassTraceEvent; +use crate::types::packet::PacketData; +use crate::types::{is_receiver_chain_source, TracePrefix}; + +/// This function handles the transfer receiving logic. +/// +/// Note that `send/mint_nft_validate` steps are performed on the host chain +/// to validate accounts and NFT info. But the result is then used for execution +/// on the IBC side, including storing acknowledgements and emitting events. +pub fn process_recv_packet_execute( + ctx_b: &mut Ctx, + packet: &Packet, + data: PacketData, +) -> Result> +where + Ctx: NftTransferExecutionContext, +{ + ctx_b + .can_receive_nft() + .map_err(|err| (ModuleExtras::empty(), err))?; + + let receiver_account = data + .receiver + .clone() + .try_into() + .map_err(|_| (ModuleExtras::empty(), NftTransferError::ParseAccountFailure))?; + + let extras = if is_receiver_chain_source( + packet.port_id_on_a.clone(), + packet.chan_id_on_a.clone(), + &data.class_id, + ) { + // sender chain is not the source, unescrow the NFT + let prefix = TracePrefix::new(packet.port_id_on_a.clone(), packet.chan_id_on_a.clone()); + let class_id = { + let mut c = data.class_id; + c.remove_trace_prefix(&prefix); + c + }; + + // Note: the validation is called before the execution. + // Refer to ICS-20 `process_recv_packet_execute()`. + for token_id in data.token_ids.as_ref() { + ctx_b + .unescrow_nft_validate( + &receiver_account, + &packet.port_id_on_b, + &packet.chan_id_on_b, + &class_id, + token_id, + ) + .map_err(|nft_error| (ModuleExtras::empty(), nft_error))?; + ctx_b + .unescrow_nft_execute( + &receiver_account, + &packet.port_id_on_b, + &packet.chan_id_on_b, + &class_id, + token_id, + ) + .map_err(|nft_error| (ModuleExtras::empty(), nft_error))?; + } + + ModuleExtras::empty() + } else { + // sender chain is the source, mint vouchers + let prefix = TracePrefix::new(packet.port_id_on_b.clone(), packet.chan_id_on_b.clone()); + let class_id = { + let mut c = data.class_id; + c.add_trace_prefix(prefix); + c + }; + + let extras = { + let class_trace_event = ClassTraceEvent { + trace_hash: ctx_b.class_hash_string(&class_id), + class: class_id.clone(), + }; + ModuleExtras { + events: vec![class_trace_event.into()], + log: Vec::new(), + } + }; + + for ((token_id, token_uri), token_data) in data + .token_ids + .as_ref() + .iter() + .zip(data.token_uris.iter()) + .zip(data.token_data.iter()) + { + // 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) + .map_err(|nft_error| (ModuleExtras::empty(), nft_error))?; + ctx_b + .create_or_update_class_execute(&class_id, class_uri, class_data) + .map_err(|nft_error| (ModuleExtras::empty(), nft_error))?; + + ctx_b + .mint_nft_validate( + &receiver_account, + &class_id, + token_id, + token_uri, + token_data, + ) + .map_err(|nft_error| (extras.clone(), nft_error))?; + ctx_b + .mint_nft_execute( + &receiver_account, + &class_id, + token_id, + token_uri, + token_data, + ) + .map_err(|nft_error| (extras.clone(), nft_error))?; + } + + extras + }; + + Ok(extras) +} diff --git a/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs new file mode 100644 index 000000000..a197bee5c --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/src/handler/send_transfer.rs @@ -0,0 +1,215 @@ +use ibc_core::channel::context::{SendPacketExecutionContext, SendPacketValidationContext}; +use ibc_core::channel::handler::{send_packet_execute, send_packet_validate}; +use ibc_core::channel::types::packet::Packet; +use ibc_core::handler::types::events::MessageEvent; +use ibc_core::host::types::path::{ChannelEndPath, SeqSendPath}; +use ibc_core::primitives::prelude::*; +use ibc_core::router::types::event::ModuleEvent; + +use crate::context::{ + NftClassContext, NftContext, NftTransferExecutionContext, NftTransferValidationContext, +}; +use crate::types::error::NftTransferError; +use crate::types::events::TransferEvent; +use crate::types::msgs::transfer::MsgTransfer; +use crate::types::{is_sender_chain_source, MODULE_ID_STR}; + +/// Initiate a token transfer. Equivalent to calling [`send_nft_transfer_validate`], followed by [`send_nft_transfer_execute`]. +pub fn send_nft_transfer( + send_packet_ctx_a: &mut SendPacketCtx, + transfer_ctx: &mut TransferCtx, + msg: MsgTransfer, +) -> Result<(), NftTransferError> +where + SendPacketCtx: SendPacketExecutionContext, + TransferCtx: NftTransferExecutionContext, +{ + send_nft_transfer_validate(send_packet_ctx_a, transfer_ctx, msg.clone())?; + send_nft_transfer_execute(send_packet_ctx_a, transfer_ctx, msg) +} + +/// Validates the NFT transfer +pub fn send_nft_transfer_validate( + send_packet_ctx_a: &SendPacketCtx, + transfer_ctx: &TransferCtx, + msg: MsgTransfer, +) -> Result<(), NftTransferError> +where + SendPacketCtx: SendPacketValidationContext, + TransferCtx: NftTransferValidationContext, +{ + transfer_ctx.can_send_nft()?; + + let chan_end_path_on_a = ChannelEndPath::new(&msg.port_id_on_a, &msg.chan_id_on_a); + let chan_end_on_a = send_packet_ctx_a.channel_end(&chan_end_path_on_a)?; + + let port_id_on_b = chan_end_on_a.counterparty().port_id().clone(); + let chan_id_on_b = chan_end_on_a + .counterparty() + .channel_id() + .ok_or_else(|| NftTransferError::DestinationChannelNotFound { + port_id: msg.port_id_on_a.clone(), + channel_id: msg.chan_id_on_a.clone(), + })? + .clone(); + + let seq_send_path_on_a = SeqSendPath::new(&msg.port_id_on_a, &msg.chan_id_on_a); + let sequence = send_packet_ctx_a.get_next_sequence_send(&seq_send_path_on_a)?; + + let sender: TransferCtx::AccountId = msg + .packet_data + .sender + .clone() + .try_into() + .map_err(|_| NftTransferError::ParseAccountFailure)?; + + let mut packet_data = msg.packet_data; + 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(); + 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( + &sender, + &msg.port_id_on_a, + &msg.chan_id_on_a, + class_id, + token_id, + &packet_data.memo, + )?; + } else { + 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()); + } + + 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()); + + let packet = { + let data = serde_json::to_vec(&packet_data) + .expect("PacketData's infallible Serialize impl failed"); + + Packet { + seq_on_a: sequence, + port_id_on_a: msg.port_id_on_a, + chan_id_on_a: msg.chan_id_on_a, + port_id_on_b, + chan_id_on_b, + data, + timeout_height_on_b: msg.timeout_height_on_b, + timeout_timestamp_on_b: msg.timeout_timestamp_on_b, + } + }; + + send_packet_validate(send_packet_ctx_a, &packet)?; + + Ok(()) +} + +/// Executes the token transfer. A prior call to [`send_nft_transfer_validate`] MUST have succeeded. +pub fn send_nft_transfer_execute( + send_packet_ctx_a: &mut SendPacketCtx, + transfer_ctx: &mut TransferCtx, + msg: MsgTransfer, +) -> Result<(), NftTransferError> +where + SendPacketCtx: SendPacketExecutionContext, + TransferCtx: NftTransferExecutionContext, +{ + let chan_end_path_on_a = ChannelEndPath::new(&msg.port_id_on_a, &msg.chan_id_on_a); + let chan_end_on_a = send_packet_ctx_a.channel_end(&chan_end_path_on_a)?; + + let port_on_b = chan_end_on_a.counterparty().port_id().clone(); + let chan_on_b = chan_end_on_a + .counterparty() + .channel_id() + .ok_or_else(|| NftTransferError::DestinationChannelNotFound { + port_id: msg.port_id_on_a.clone(), + channel_id: msg.chan_id_on_a.clone(), + })? + .clone(); + + // get the next sequence + let seq_send_path_on_a = SeqSendPath::new(&msg.port_id_on_a, &msg.chan_id_on_a); + let sequence = send_packet_ctx_a.get_next_sequence_send(&seq_send_path_on_a)?; + + let sender = msg + .packet_data + .sender + .clone() + .try_into() + .map_err(|_| NftTransferError::ParseAccountFailure)?; + + let mut packet_data = msg.packet_data; + 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(); + 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( + &sender, + &msg.port_id_on_a, + &msg.chan_id_on_a, + class_id, + token_id, + &packet_data.memo, + )?; + } else { + 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()); + } + + 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()); + + let packet = { + let data = { + serde_json::to_vec(&packet_data).expect("PacketData's infallible Serialize impl failed") + }; + + Packet { + seq_on_a: sequence, + port_id_on_a: msg.port_id_on_a, + chan_id_on_a: msg.chan_id_on_a, + port_id_on_b: port_on_b, + chan_id_on_b: chan_on_b, + data, + timeout_height_on_b: msg.timeout_height_on_b, + timeout_timestamp_on_b: msg.timeout_timestamp_on_b, + } + }; + + send_packet_execute(send_packet_ctx_a, packet)?; + + { + send_packet_ctx_a.log_message(format!( + "IBC NFT transfer: {} --({}, [{}])--> {}", + packet_data.sender, class_id, token_ids, packet_data.receiver + ))?; + + let transfer_event = TransferEvent { + sender: packet_data.sender, + receiver: packet_data.receiver, + class: packet_data.class_id, + tokens: packet_data.token_ids, + memo: packet_data.memo, + }; + send_packet_ctx_a.emit_ibc_event(ModuleEvent::from(transfer_event).into())?; + + send_packet_ctx_a.emit_ibc_event(MessageEvent::Module(MODULE_ID_STR.to_string()).into())?; + } + + Ok(()) +} diff --git a/ibc-apps/ics721-nft-transfer/src/module.rs b/ibc-apps/ics721-nft-transfer/src/module.rs index 7e83bf870..efbd10b18 100644 --- a/ibc-apps/ics721-nft-transfer/src/module.rs +++ b/ibc-apps/ics721-nft-transfer/src/module.rs @@ -1,26 +1,54 @@ //! Provides IBC module callbacks implementation for the ICS-721 transfer. -use ibc_app_nft_transfer_types::error::NftTransferError; -use ibc_core::channel::types::acknowledgement::Acknowledgement; +use ibc_core::channel::types::acknowledgement::{Acknowledgement, AcknowledgementStatus}; use ibc_core::channel::types::channel::{Counterparty, Order}; use ibc_core::channel::types::packet::Packet; use ibc_core::channel::types::Version; +use ibc_core::handler::types::error::ContextError; use ibc_core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc_core::primitives::prelude::*; use ibc_core::primitives::Signer; use ibc_core::router::types::module::ModuleExtras; use crate::context::{NftTransferExecutionContext, NftTransferValidationContext}; +use crate::handler::{ + process_recv_packet_execute, refund_packet_nft_execute, refund_packet_nft_validate, +}; +use crate::types::error::NftTransferError; +use crate::types::events::{AckEvent, AckStatusEvent, RecvEvent, TimeoutEvent}; +use crate::types::packet::PacketData; +use crate::types::{ack_success_b64, VERSION}; pub fn on_chan_open_init_validate( - _ctx: &impl NftTransferValidationContext, - _order: Order, + ctx: &impl NftTransferValidationContext, + order: Order, _connection_hops: &[ConnectionId], - _port_id: &PortId, + port_id: &PortId, _channel_id: &ChannelId, _counterparty: &Counterparty, - _version: &Version, + version: &Version, ) -> Result<(), NftTransferError> { - unimplemented!() + if order != Order::Unordered { + return Err(NftTransferError::ChannelNotUnordered { + expect_order: Order::Unordered, + got_order: order, + }); + } + let bound_port = ctx.get_port()?; + if port_id != &bound_port { + return Err(NftTransferError::InvalidPort { + port_id: port_id.clone(), + exp_port_id: bound_port, + }); + } + + if !version.is_empty() { + version + .verify_is_expected(Version::new(VERSION.to_string())) + .map_err(ContextError::from)?; + } + + Ok(()) } pub fn on_chan_open_init_execute( @@ -32,19 +60,30 @@ pub fn on_chan_open_init_execute( _counterparty: &Counterparty, _version: &Version, ) -> Result<(ModuleExtras, Version), NftTransferError> { - unimplemented!() + Ok((ModuleExtras::empty(), Version::new(VERSION.to_string()))) } pub fn on_chan_open_try_validate( _ctx: &impl NftTransferValidationContext, - _order: Order, + order: Order, _connection_hops: &[ConnectionId], _port_id: &PortId, _channel_id: &ChannelId, _counterparty: &Counterparty, - _counterparty_version: &Version, + counterparty_version: &Version, ) -> Result<(), NftTransferError> { - unimplemented!() + if order != Order::Unordered { + return Err(NftTransferError::ChannelNotUnordered { + expect_order: Order::Unordered, + got_order: order, + }); + } + + counterparty_version + .verify_is_expected(Version::new(VERSION.to_string())) + .map_err(ContextError::from)?; + + Ok(()) } pub fn on_chan_open_try_execute( @@ -56,16 +95,20 @@ pub fn on_chan_open_try_execute( _counterparty: &Counterparty, _counterparty_version: &Version, ) -> Result<(ModuleExtras, Version), NftTransferError> { - unimplemented!() + Ok((ModuleExtras::empty(), Version::new(VERSION.to_string()))) } pub fn on_chan_open_ack_validate( _ctx: &impl NftTransferExecutionContext, _port_id: &PortId, _channel_id: &ChannelId, - _counterparty_version: &Version, + counterparty_version: &Version, ) -> Result<(), NftTransferError> { - unimplemented!() + counterparty_version + .verify_is_expected(Version::new(VERSION.to_string())) + .map_err(ContextError::from)?; + + Ok(()) } pub fn on_chan_open_ack_execute( @@ -74,7 +117,7 @@ pub fn on_chan_open_ack_execute( _channel_id: &ChannelId, _counterparty_version: &Version, ) -> Result { - unimplemented!() + Ok(ModuleExtras::empty()) } pub fn on_chan_open_confirm_validate( @@ -82,7 +125,7 @@ pub fn on_chan_open_confirm_validate( _port_id: &PortId, _channel_id: &ChannelId, ) -> Result<(), NftTransferError> { - unimplemented!() + Ok(()) } pub fn on_chan_open_confirm_execute( @@ -90,7 +133,7 @@ pub fn on_chan_open_confirm_execute( _port_id: &PortId, _channel_id: &ChannelId, ) -> Result { - unimplemented!() + Ok(ModuleExtras::empty()) } pub fn on_chan_close_init_validate( @@ -98,7 +141,7 @@ pub fn on_chan_close_init_validate( _port_id: &PortId, _channel_id: &ChannelId, ) -> Result<(), NftTransferError> { - unimplemented!() + Err(NftTransferError::CantCloseChannel) } pub fn on_chan_close_init_execute( @@ -106,7 +149,7 @@ pub fn on_chan_close_init_execute( _port_id: &PortId, _channel_id: &ChannelId, ) -> Result { - unimplemented!() + Err(NftTransferError::CantCloseChannel) } pub fn on_chan_close_confirm_validate( @@ -114,7 +157,7 @@ pub fn on_chan_close_confirm_validate( _port_id: &PortId, _channel_id: &ChannelId, ) -> Result<(), NftTransferError> { - unimplemented!() + Ok(()) } pub fn on_chan_close_confirm_execute( @@ -122,46 +165,222 @@ pub fn on_chan_close_confirm_execute( _port_id: &PortId, _channel_id: &ChannelId, ) -> Result { - unimplemented!() + Ok(ModuleExtras::empty()) } pub fn on_recv_packet_execute( - _ctx_b: &mut impl NftTransferExecutionContext, - _packet: &Packet, + ctx_b: &mut impl NftTransferExecutionContext, + packet: &Packet, ) -> (ModuleExtras, Acknowledgement) { - unimplemented!() + let data = match serde_json::from_slice::(&packet.data) { + Ok(data) => data, + Err(_) => { + let ack = + AcknowledgementStatus::error(NftTransferError::PacketDataDeserialization.into()); + return (ModuleExtras::empty(), ack.into()); + } + }; + + let (mut extras, ack) = match process_recv_packet_execute(ctx_b, packet, data.clone()) { + Ok(extras) => (extras, AcknowledgementStatus::success(ack_success_b64())), + Err(boxed_error) => { + let (extras, error) = *boxed_error; + (extras, AcknowledgementStatus::error(error.into())) + } + }; + + let recv_event = RecvEvent { + sender: data.sender, + receiver: data.receiver, + class: data.class_id, + tokens: data.token_ids, + memo: data.memo, + success: ack.is_successful(), + }; + extras.events.push(recv_event.into()); + + (extras, ack.into()) } -pub fn on_acknowledgement_packet_validate( - _ctx: &impl NftTransferValidationContext, - _packet: &Packet, - _acknowledgement: &Acknowledgement, +pub fn on_acknowledgement_packet_validate( + ctx: &impl NftTransferValidationContext, + packet: &Packet, + acknowledgement: &Acknowledgement, _relayer: &Signer, ) -> Result<(), NftTransferError> { - unimplemented!() + let data = serde_json::from_slice::(&packet.data) + .map_err(|_| NftTransferError::PacketDataDeserialization)?; + + let acknowledgement = serde_json::from_slice::(acknowledgement.as_ref()) + .map_err(|_| NftTransferError::AckDeserialization)?; + + if !acknowledgement.is_successful() { + refund_packet_nft_validate(ctx, packet, &data)?; + } + + Ok(()) } pub fn on_acknowledgement_packet_execute( - _ctx: &mut impl NftTransferExecutionContext, - _packet: &Packet, - _acknowledgement: &Acknowledgement, + ctx: &mut impl NftTransferExecutionContext, + packet: &Packet, + acknowledgement: &Acknowledgement, _relayer: &Signer, ) -> (ModuleExtras, Result<(), NftTransferError>) { - unimplemented!() + let data = match serde_json::from_slice::(&packet.data) { + Ok(data) => data, + Err(_) => { + return ( + ModuleExtras::empty(), + Err(NftTransferError::PacketDataDeserialization), + ); + } + }; + + let acknowledgement = + match serde_json::from_slice::(acknowledgement.as_ref()) { + Ok(ack) => ack, + Err(_) => { + return ( + ModuleExtras::empty(), + Err(NftTransferError::AckDeserialization), + ); + } + }; + + if !acknowledgement.is_successful() { + if let Err(err) = refund_packet_nft_execute(ctx, packet, &data) { + return (ModuleExtras::empty(), Err(err)); + } + } + + let ack_event = AckEvent { + sender: data.sender, + receiver: data.receiver, + class: data.class_id, + tokens: data.token_ids, + memo: data.memo, + acknowledgement: acknowledgement.clone(), + }; + + let extras = ModuleExtras { + events: vec![ack_event.into(), AckStatusEvent { acknowledgement }.into()], + log: Vec::new(), + }; + + (extras, Ok(())) } pub fn on_timeout_packet_validate( - _ctx: &impl NftTransferValidationContext, - _packet: &Packet, + ctx: &impl NftTransferValidationContext, + packet: &Packet, _relayer: &Signer, ) -> Result<(), NftTransferError> { - unimplemented!() + let data = serde_json::from_slice::(&packet.data) + .map_err(|_| NftTransferError::PacketDataDeserialization)?; + + refund_packet_nft_validate(ctx, packet, &data)?; + + Ok(()) } pub fn on_timeout_packet_execute( - _ctx: &mut impl NftTransferExecutionContext, - _packet: &Packet, + ctx: &mut impl NftTransferExecutionContext, + packet: &Packet, _relayer: &Signer, ) -> (ModuleExtras, Result<(), NftTransferError>) { - unimplemented!() + let data = match serde_json::from_slice::(&packet.data) { + Ok(data) => data, + Err(_) => { + return ( + ModuleExtras::empty(), + Err(NftTransferError::PacketDataDeserialization), + ); + } + }; + + if let Err(err) = refund_packet_nft_execute(ctx, packet, &data) { + return (ModuleExtras::empty(), Err(err)); + } + + let timeout_event = TimeoutEvent { + refund_receiver: data.sender, + refund_class: data.class_id, + refund_tokens: data.token_ids, + memo: data.memo, + }; + + let extras = ModuleExtras { + events: vec![timeout_event.into()], + log: Vec::new(), + }; + + (extras, Ok(())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::ack_success_b64; + use crate::types::error::NftTransferError; + + #[test] + fn test_ack_ser() { + fn ser_json_assert_eq(ack: AcknowledgementStatus, json_str: &str) { + let ser = serde_json::to_string(&ack).unwrap(); + assert_eq!(ser, json_str) + } + + ser_json_assert_eq( + AcknowledgementStatus::success(ack_success_b64()), + r#"{"result":"AQ=="}"#, + ); + ser_json_assert_eq( + AcknowledgementStatus::error(NftTransferError::PacketDataDeserialization.into()), + r#"{"error":"failed to deserialize packet data"}"#, + ); + } + + #[test] + fn test_ack_success_to_vec() { + let ack_success: Vec = AcknowledgementStatus::success(ack_success_b64()).into(); + + // Check that it's the same output as ibc-go + // Note: this also implicitly checks that the ack bytes are non-empty, + // which would make the conversion to `Acknowledgement` panic + assert_eq!(ack_success, r#"{"result":"AQ=="}"#.as_bytes()); + } + + #[test] + fn test_ack_error_to_vec() { + let ack_error: Vec = + AcknowledgementStatus::error(NftTransferError::PacketDataDeserialization.into()).into(); + + // Check that it's the same output as ibc-go + // Note: this also implicitly checks that the ack bytes are non-empty, + // which would make the conversion to `Acknowledgement` panic + assert_eq!( + ack_error, + r#"{"error":"failed to deserialize packet data"}"#.as_bytes() + ); + } + + #[test] + fn test_ack_de() { + fn de_json_assert_eq(json_str: &str, ack: AcknowledgementStatus) { + let de = serde_json::from_str::(json_str).unwrap(); + assert_eq!(de, ack) + } + + de_json_assert_eq( + r#"{"result":"AQ=="}"#, + AcknowledgementStatus::success(ack_success_b64()), + ); + de_json_assert_eq( + r#"{"error":"failed to deserialize packet data"}"#, + AcknowledgementStatus::error(NftTransferError::PacketDataDeserialization.into()), + ); + + assert!(serde_json::from_str::(r#"{"success":"AQ=="}"#).is_err()); + } } diff --git a/ibc-apps/ics721-nft-transfer/types/Cargo.toml b/ibc-apps/ics721-nft-transfer/types/Cargo.toml index ca9467185..38aba1545 100644 --- a/ibc-apps/ics721-nft-transfer/types/Cargo.toml +++ b/ibc-apps/ics721-nft-transfer/types/Cargo.toml @@ -22,8 +22,11 @@ all-features = true borsh = { workspace = true, optional = true } derive_more = { workspace = true } displaydoc = { workspace = true } +http = "1.0.0" +mime = "0.3.17" schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } +serde_json = { workspace = true } # ibc dependencies ibc-core = { workspace = true } @@ -34,7 +37,6 @@ parity-scale-codec = { workspace = true , optional = true } scale-info = { workspace = true , optional = true } [dev-dependencies] -serde_json = { workspace = true } rstest = { workspace = true } [features] diff --git a/ibc-apps/ics721-nft-transfer/types/src/class.rs b/ibc-apps/ics721-nft-transfer/types/src/class.rs new file mode 100644 index 000000000..de8b4111b --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/class.rs @@ -0,0 +1,561 @@ +//! Defines Non-Fungible Token Transfer (ICS-721) class types. +use core::fmt::{self, Display, Error as FmtError, Formatter}; +use core::str::FromStr; + +use derive_more::From; +use http::Uri; +use ibc_core::host::types::identifiers::{ChannelId, PortId}; +use ibc_core::primitives::prelude::*; +use ibc_proto::ibc::applications::nft_transfer::v1::ClassTrace as RawClassTrace; + +use crate::data::Data; +use crate::error::NftTransferError; +use crate::serializers; + +/// Class ID for an NFT +#[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, PartialEq, Eq, PartialOrd, Ord)] +pub struct ClassId(String); + +impl AsRef for ClassId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Display for ClassId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for ClassId { + type Err = NftTransferError; + + fn from_str(class_id: &str) -> Result { + if class_id.trim().is_empty() { + Err(NftTransferError::EmptyBaseClassId) + } else { + Ok(Self(class_id.to_string())) + } + } +} + +/// Class prefix, the same as ICS-20 TracePrefix +#[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, Ord, PartialOrd, Eq, PartialEq)] +pub struct TracePrefix { + port_id: PortId, + channel_id: ChannelId, +} + +impl TracePrefix { + pub fn new(port_id: PortId, channel_id: ChannelId) -> Self { + Self { + port_id, + channel_id, + } + } +} + +impl Display for TracePrefix { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + write!(f, "{}/{}", self.port_id, self.channel_id) + } +} + +/// Class trace path, the same as ICS-20 TracePath +#[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, Eq, PartialEq, PartialOrd, Ord, From)] +pub struct TracePath(Vec); + +impl TracePath { + /// Returns true iff this path starts with the specified prefix + pub fn starts_with(&self, prefix: &TracePrefix) -> bool { + self.0.last().map(|p| p == prefix).unwrap_or(false) + } + + /// Removes the specified prefix from the path if there is a match, otherwise does nothing. + pub fn remove_prefix(&mut self, prefix: &TracePrefix) { + if self.starts_with(prefix) { + self.0.pop(); + } + } + + /// Adds the specified prefix to the path. + pub fn add_prefix(&mut self, prefix: TracePrefix) { + self.0.push(prefix) + } + + /// Returns true if the path is empty and false otherwise. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl<'a> TryFrom> for TracePath { + type Error = NftTransferError; + + fn try_from(v: Vec<&'a str>) -> Result { + if v.len() % 2 != 0 { + return Err(NftTransferError::InvalidTraceLength { + len: v.len() as u64, + }); + } + + let mut trace = vec![]; + let id_pairs = v.chunks_exact(2).map(|paths| (paths[0], paths[1])); + for (pos, (port_id, channel_id)) in id_pairs.rev().enumerate() { + let port_id = + PortId::from_str(port_id).map_err(|e| NftTransferError::InvalidTracePortId { + pos: pos as u64, + validation_error: e, + })?; + let channel_id = ChannelId::from_str(channel_id).map_err(|e| { + NftTransferError::InvalidTraceChannelId { + pos: pos as u64, + validation_error: e, + } + })?; + trace.push(TracePrefix { + port_id, + channel_id, + }); + } + + Ok(trace.into()) + } +} + +impl FromStr for TracePath { + type Err = NftTransferError; + + fn from_str(s: &str) -> Result { + let parts = { + let parts: Vec<&str> = s.split('/').collect(); + if parts.len() == 1 && parts[0].trim().is_empty() { + vec![] + } else { + parts + } + }; + parts.try_into() + } +} + +impl Display for TracePath { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + let path = self + .0 + .iter() + .rev() + .map(|prefix| prefix.to_string()) + .collect::>() + .join("/"); + write!(f, "{path}") + } +} + +/// Prefixed class to trace sources like ICS-20 PrefixedDenom +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[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) +)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct PrefixedClassId { + /// A series of `{port-id}/{channel-id}`s for tracing the source of the class. + #[cfg_attr(feature = "serde", serde(with = "serializers"))] + #[cfg_attr(feature = "schema", schemars(with = "String"))] + pub trace_path: TracePath, + /// Base class of the relayed non-fungible token. + pub base_class_id: ClassId, +} + +impl PrefixedClassId { + /// Removes the specified prefix from the trace path if there is a match, otherwise does nothing. + pub fn remove_trace_prefix(&mut self, prefix: &TracePrefix) { + self.trace_path.remove_prefix(prefix) + } + + /// Adds the specified prefix to the trace path. + pub fn add_trace_prefix(&mut self, prefix: TracePrefix) { + self.trace_path.add_prefix(prefix) + } +} + +/// Returns true if the class ID originally came from the sender chain and false otherwise. +pub fn is_sender_chain_source( + source_port: PortId, + source_channel: ChannelId, + class_id: &PrefixedClassId, +) -> bool { + !is_receiver_chain_source(source_port, source_channel, class_id) +} + +/// Returns true if the class ID originally came from the receiving chain and false otherwise. +pub fn is_receiver_chain_source( + source_port: PortId, + source_channel: ChannelId, + class_id: &PrefixedClassId, +) -> bool { + // For example, let + // A: sender chain in this transfer, port "transfer" and channel "c2b" (to B) + // B: receiver chain in this transfer, port "transfer" and channel "c2a" (to A) + // + // If B had originally sent the token in a previous transfer, then A would have stored the token as + // "transfer/c2b/{token_denom}". Now, A is sending to B, so to check if B is the source of the token, + // we need to check if the token starts with "transfer/c2b". + let prefix = TracePrefix::new(source_port, source_channel); + class_id.trace_path.starts_with(&prefix) +} + +impl FromStr for PrefixedClassId { + type Err = NftTransferError; + + fn from_str(s: &str) -> Result { + let mut parts: Vec<&str> = s.split('/').collect(); + let last_part = parts.pop().expect("split() returned an empty iterator"); + + let (base_class_id, trace_path) = { + if last_part == s { + (ClassId::from_str(s)?, TracePath::default()) + } else { + let base_class_id = ClassId::from_str(last_part)?; + let trace_path = TracePath::try_from(parts)?; + (base_class_id, trace_path) + } + }; + + Ok(Self { + trace_path, + base_class_id, + }) + } +} + +impl TryFrom for PrefixedClassId { + type Error = NftTransferError; + + fn try_from(value: RawClassTrace) -> Result { + let base_class_id = ClassId::from_str(&value.base_class_id)?; + let trace_path = TracePath::from_str(&value.path)?; + Ok(Self { + trace_path, + base_class_id, + }) + } +} + +impl From for RawClassTrace { + fn from(value: PrefixedClassId) -> Self { + Self { + path: value.trace_path.to_string(), + base_class_id: value.base_class_id.to_string(), + } + } +} + +impl From for PrefixedClassId { + fn from(class_id: ClassId) -> Self { + Self { + trace_path: Default::default(), + base_class_id: class_id, + } + } +} + +impl Display for PrefixedClassId { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + if self.trace_path.0.is_empty() { + write!(f, "{}", self.base_class_id) + } else { + write!(f, "{}/{}", self.trace_path, self.base_class_id) + } + } +} + +/// Class URI for an NFT +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ClassUri( + #[cfg_attr(feature = "serde", serde(with = "serializers"))] + #[cfg_attr(feature = "schema", schemars(with = "String"))] + Uri, +); + +#[cfg(feature = "borsh")] +impl borsh::BorshSerialize for ClassUri { + fn serialize( + &self, + writer: &mut W, + ) -> borsh::maybestd::io::Result<()> { + borsh::BorshSerialize::serialize(&self.to_string(), writer) + } +} + +#[cfg(feature = "borsh")] +impl borsh::BorshDeserialize for ClassUri { + fn deserialize_reader( + reader: &mut R, + ) -> borsh::maybestd::io::Result { + let uri = String::deserialize_reader(reader)?; + Ok(ClassUri::from_str(&uri).map_err(|_| borsh::maybestd::io::ErrorKind::Other)?) + } +} + +#[cfg(feature = "parity-scale-codec")] +impl parity_scale_codec::Encode for ClassUri { + fn encode_to(&self, writer: &mut T) { + self.to_string().encode_to(writer); + } +} + +#[cfg(feature = "parity-scale-codec")] +impl parity_scale_codec::Decode for ClassUri { + fn decode( + input: &mut I, + ) -> Result { + let uri = String::decode(input)?; + ClassUri::from_str(&uri).map_err(|_| parity_scale_codec::Error::from("from str error")) + } +} + +#[cfg(feature = "parity-scale-codec")] +impl scale_info::TypeInfo for ClassUri { + type Identity = Self; + + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("ClassUri", module_path!())) + .composite( + scale_info::build::Fields::unnamed() + .field(|f| f.ty::().type_name("String")), + ) + } +} + +impl Display for ClassUri { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for ClassUri { + type Err = NftTransferError; + + fn from_str(class_uri: &str) -> Result { + match Uri::from_str(class_uri) { + Ok(uri) => Ok(Self(uri)), + Err(err) => Err(NftTransferError::InvalidUri { + uri: class_uri.to_string(), + validation_error: err, + }), + } + } +} + +/// Class data for an NFT +#[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, PartialEq, Eq)] +pub struct ClassData(Data); + +impl Display for ClassData { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for ClassData { + type Err = NftTransferError; + + fn from_str(class_data: &str) -> Result { + // validate the data + let data = Data::from_str(class_data)?; + Ok(Self(data)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_class_id_validation() -> Result<(), NftTransferError> { + assert!(ClassId::from_str("").is_err(), "empty base class ID"); + assert!(ClassId::from_str("myclass").is_ok(), "valid base class ID"); + assert!(PrefixedClassId::from_str("").is_err(), "empty class trace"); + assert!( + PrefixedClassId::from_str("transfer/channel-0/").is_err(), + "empty base class ID with trace" + ); + assert!( + PrefixedClassId::from_str("/myclass").is_err(), + "empty prefix" + ); + assert!(PrefixedClassId::from_str("//myclass").is_err(), "empty ids"); + assert!( + PrefixedClassId::from_str("transfer/").is_err(), + "single trace" + ); + assert!( + PrefixedClassId::from_str("transfer/myclass").is_err(), + "single trace with base class ID" + ); + assert!( + PrefixedClassId::from_str("transfer/channel-0/myclass").is_ok(), + "valid single trace info" + ); + assert!( + PrefixedClassId::from_str("transfer/channel-0/transfer/channel-1/myclass").is_ok(), + "valid multiple trace info" + ); + assert!( + PrefixedClassId::from_str("(transfer)/channel-0/myclass").is_err(), + "invalid port" + ); + assert!( + PrefixedClassId::from_str("transfer/(channel-0)/myclass").is_err(), + "invalid channel" + ); + + Ok(()) + } + + #[test] + fn test_class_id_trace() -> Result<(), NftTransferError> { + assert_eq!( + PrefixedClassId::from_str("transfer/channel-0/myclass")?, + PrefixedClassId { + trace_path: "transfer/channel-0".parse()?, + base_class_id: "myclass".parse()? + }, + "valid single trace info" + ); + assert_eq!( + PrefixedClassId::from_str("transfer/channel-0/transfer/channel-1/myclass")?, + PrefixedClassId { + trace_path: "transfer/channel-0/transfer/channel-1".parse()?, + base_class_id: "myclass".parse()? + }, + "valid multiple trace info" + ); + + Ok(()) + } + + #[test] + fn test_class_id_serde() -> Result<(), NftTransferError> { + let dt_str = "transfer/channel-0/myclass"; + let dt = PrefixedClassId::from_str(dt_str)?; + assert_eq!(dt.to_string(), dt_str, "valid single trace info"); + + let dt_str = "transfer/channel-0/transfer/channel-1/myclass"; + let dt = PrefixedClassId::from_str(dt_str)?; + assert_eq!(dt.to_string(), dt_str, "valid multiple trace info"); + + Ok(()) + } + + #[test] + fn test_trace_path() -> Result<(), NftTransferError> { + assert!(TracePath::from_str("").is_ok(), "empty trace path"); + assert!( + TracePath::from_str("transfer/myclass").is_err(), + "invalid trace path: bad ChannelId" + ); + assert!( + TracePath::from_str("transfer//myclass").is_err(), + "malformed trace path: missing ChannelId" + ); + assert!( + TracePath::from_str("transfer/channel-0/").is_err(), + "malformed trace path: trailing delimiter" + ); + + let prefix_1 = TracePrefix::new("transfer".parse().unwrap(), "channel-1".parse().unwrap()); + let prefix_2 = TracePrefix::new("transfer".parse().unwrap(), "channel-0".parse().unwrap()); + let mut trace_path = TracePath(vec![prefix_1.clone()]); + + trace_path.add_prefix(prefix_2.clone()); + assert_eq!( + TracePath::from_str("transfer/channel-0/transfer/channel-1")?, + trace_path + ); + assert_eq!( + TracePath(vec![prefix_1.clone(), prefix_2.clone()]), + trace_path + ); + + trace_path.remove_prefix(&prefix_2); + assert_eq!(TracePath::from_str("transfer/channel-1")?, trace_path); + assert_eq!(TracePath(vec![prefix_1.clone()]), trace_path); + + trace_path.remove_prefix(&prefix_1); + assert!(trace_path.is_empty()); + + Ok(()) + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/data.rs b/ibc-apps/ics721-nft-transfer/types/src/data.rs new file mode 100644 index 000000000..936dbf54a --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -0,0 +1,187 @@ +//! Defines Non-Fungible Token Transfer (ICS-721) data types. +use core::fmt::{self, Display, Formatter}; +use core::str::FromStr; + +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 = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Data(BTreeMap); + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DataValue { + value: String, + mime: Option, +} + +#[cfg(feature = "serde")] +impl serde::Serialize for DataValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("DataValue", 2)?; + state.serialize_field("value", &self.value)?; + match &self.mime { + Some(mime) if *mime != "" => { + state.serialize_field("mime", &mime.to_string())?; + } + _ => {} + } + state.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for DataValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct StringDataValue { + value: String, + mime: Option, + } + + let data_value = StringDataValue::deserialize(deserializer)?; + let mime = data_value + .mime + .map(|s| Mime::from_str(&s).map_err(serde::de::Error::custom)) + .transpose()?; + + Ok(DataValue { + value: data_value.value, + mime, + }) + } +} + +#[cfg(feature = "borsh")] +impl borsh::BorshSerialize for DataValue { + fn serialize( + &self, + writer: &mut W, + ) -> borsh::maybestd::io::Result<()> { + borsh::BorshSerialize::serialize(&self.value, writer)?; + let mime = match &self.mime { + Some(mime) => mime.to_string(), + None => String::default(), + }; + borsh::BorshSerialize::serialize(&mime.to_string(), writer)?; + Ok(()) + } +} + +#[cfg(feature = "borsh")] +impl borsh::BorshDeserialize for DataValue { + fn deserialize_reader( + reader: &mut R, + ) -> borsh::maybestd::io::Result { + let value = String::deserialize_reader(reader)?; + let mime = String::deserialize_reader(reader)?; + let mime = if mime.is_empty() { + None + } else { + Some(Mime::from_str(&mime).map_err(|_| borsh::maybestd::io::ErrorKind::Other)?) + }; + + Ok(Self { value, mime }) + } +} + +#[cfg(feature = "parity-scale-codec")] +impl parity_scale_codec::Encode for DataValue { + fn encode_to(&self, writer: &mut T) { + self.value.encode_to(writer); + if let Some(mime) = &self.mime { + mime.to_string().encode_to(writer); + } else { + "".encode_to(writer); + } + } +} + +#[cfg(feature = "parity-scale-codec")] +impl parity_scale_codec::Decode for DataValue { + fn decode( + input: &mut I, + ) -> Result { + let value = String::decode(input)?; + let mime_str = String::decode(input)?; + let mime = if mime_str.is_empty() { + None + } else { + Some( + Mime::from_str(&mime_str) + .map_err(|_| parity_scale_codec::Error::from("from str error"))?, + ) + }; + + Ok(DataValue { value, mime }) + } +} + +#[cfg(feature = "parity-scale-codec")] +impl scale_info::TypeInfo for DataValue { + type Identity = Self; + + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("DataValue", module_path!())) + .composite( + scale_info::build::Fields::named() + .field(|f| f.ty::().name("value").type_name("String")) + .field(|f| f.ty::().name("mime").type_name("String")), + ) + } +} + +#[cfg(feature = "schema")] +impl schemars::JsonSchema for DataValue { + fn schema_name() -> String { + "DataValue".to_string() + } + + fn schema_id() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(concat!(module_path!(), "::DataValue")) + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + gen.subschema_for::() + } +} + +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)) + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/error.rs b/ibc-apps/ics721-nft-transfer/types/src/error.rs index 65b857dcf..fc3b03591 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/error.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/error.rs @@ -1,10 +1,13 @@ //! Defines the Non-Fungible Token Transfer (ICS-721) error types. use core::convert::Infallible; +use core::str::Utf8Error; use displaydoc::Display; use ibc_core::channel::types::acknowledgement::StatusValue; +use ibc_core::channel::types::channel::Order; use ibc_core::handler::types::error::ContextError; use ibc_core::host::types::error::IdentifierError; +use ibc_core::host::types::identifiers::{ChannelId, PortId}; use ibc_core::primitives::prelude::*; #[derive(Display, Debug)] @@ -13,6 +16,78 @@ pub enum NftTransferError { ContextError(ContextError), /// invalid identifier: `{0}` InvalidIdentifier(IdentifierError), + /// invalid URI: `{uri}`, validation error: `{validation_error}`` + InvalidUri { + uri: String, + validation_error: http::uri::InvalidUri, + }, + /// destination channel not found in the counterparty of port_id `{port_id}` and channel_id `{channel_id}` + DestinationChannelNotFound { + port_id: PortId, + channel_id: ChannelId, + }, + /// base class ID is empty + EmptyBaseClassId, + /// invalid prot id n trace at position: `{pos}`, validation error: `{validation_error}` + InvalidTracePortId { + pos: u64, + validation_error: IdentifierError, + }, + /// invalid channel id in trace at position: `{pos}`, validation error: `{validation_error}` + InvalidTraceChannelId { + pos: u64, + validation_error: IdentifierError, + }, + /// trace length must be even but got: `{len}` + InvalidTraceLength { len: u64 }, + /// no token ID + NoTokenId, + /// invalid token ID + InvalidTokenId, + /// duplicated token IDs + DuplicatedTokenIds, + /// invalid token ID + TokenMismatched, + /// invalid json data + InvalidJsonData, + /// expected `{expect_order}` channel, got `{got_order}` + ChannelNotUnordered { + expect_order: Order, + got_order: Order, + }, + /// channel cannot be closed + CantCloseChannel, + /// `{sender}` doesn't own the NFT + InvalidOwner { sender: String }, + /// owner is not found + OwnerNotFound, + /// nft is not found + NftNotFound, + /// nft class is not found + NftClassNotFound, + /// failed to deserialize packet data + PacketDataDeserialization, + /// failed to deserialize acknowledgement + AckDeserialization, + /// receive is not enabled + ReceiveDisabled { reason: String }, + /// send is not enabled + SendDisabled { reason: String }, + /// failed to parse as AccountId + ParseAccountFailure, + /// invalid port: `{port_id}`, expected `{exp_port_id}` + InvalidPort { + port_id: PortId, + exp_port_id: PortId, + }, + /// decoding raw msg error: `{reason}` + DecodeRawMsg { reason: String }, + /// unknown msg type: `{msg_type}` + UnknownMsgType { msg_type: String }, + /// decoding raw bytes as UTF8 string error: `{0}` + Utf8Decode(Utf8Error), + /// other error: `{0}` + Other(String), } #[cfg(feature = "std")] @@ -21,6 +96,19 @@ impl std::error::Error for NftTransferError { match &self { Self::ContextError(e) => Some(e), Self::InvalidIdentifier(e) => Some(e), + Self::InvalidUri { + validation_error: e, + .. + } => Some(e), + Self::InvalidTracePortId { + validation_error: e, + .. + } => Some(e), + Self::InvalidTraceChannelId { + validation_error: e, + .. + } => Some(e), + _ => None, } } } diff --git a/ibc-apps/ics721-nft-transfer/types/src/events.rs b/ibc-apps/ics721-nft-transfer/types/src/events.rs index 8130bb580..b2e236a1b 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/events.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/events.rs @@ -1 +1,209 @@ //! Defines Non-Fungible Token Transfer (ICS-721) event types. +use ibc_core::channel::types::acknowledgement::AcknowledgementStatus; +use ibc_core::primitives::prelude::*; +use ibc_core::primitives::Signer; +use ibc_core::router::types::event::ModuleEvent; + +use super::Memo; +use crate::{PrefixedClassId, TokenIds, MODULE_ID_STR}; + +const EVENT_TYPE_PACKET: &str = "non_fungible_token_packet"; +const EVENT_TYPE_TIMEOUT: &str = "timeout"; +const EVENT_TYPE_CLASS_TRACE: &str = "class_trace"; +const EVENT_TYPE_TRANSFER: &str = "ibc_nft_transfer"; + +/// Contains all events variants that can be emitted from the NFT transfer application +pub enum Event { + Recv(RecvEvent), + Ack(AckEvent), + AckStatus(AckStatusEvent), + Timeout(TimeoutEvent), + ClassTrace(ClassTraceEvent), + Transfer(TransferEvent), +} + +/// Event emitted by the `onRecvPacket` module callback to indicate the that the +/// `RecvPacket` message was processed +pub struct RecvEvent { + pub sender: Signer, + pub receiver: Signer, + pub class: PrefixedClassId, + pub tokens: TokenIds, + pub memo: Memo, + pub success: bool, +} + +impl From for ModuleEvent { + fn from(ev: RecvEvent) -> Self { + let RecvEvent { + sender, + receiver, + class, + tokens, + memo, + success, + } = ev; + Self { + kind: EVENT_TYPE_PACKET.to_string(), + attributes: vec![ + ("module", MODULE_ID_STR).into(), + ("sender", sender).into(), + ("receiver", receiver).into(), + ("class", class).into(), + ("tokens", tokens).into(), + ("memo", memo).into(), + ("success", success).into(), + ], + } + } +} + +/// Event emitted in the `onAcknowledgePacket` module callback +pub struct AckEvent { + pub sender: Signer, + pub receiver: Signer, + pub class: PrefixedClassId, + pub tokens: TokenIds, + pub memo: Memo, + pub acknowledgement: AcknowledgementStatus, +} + +impl From for ModuleEvent { + fn from(ev: AckEvent) -> Self { + let AckEvent { + sender, + receiver, + class, + tokens, + memo, + acknowledgement, + } = ev; + Self { + kind: EVENT_TYPE_PACKET.to_string(), + attributes: vec![ + ("module", MODULE_ID_STR).into(), + ("sender", sender).into(), + ("receiver", receiver).into(), + ("class", class).into(), + ("tokens", tokens).into(), + ("memo", memo).into(), + ("acknowledgement", acknowledgement).into(), + ], + } + } +} + +/// Event emitted in the `onAcknowledgePacket` module callback to indicate +/// whether the acknowledgement is a success or a failure +pub struct AckStatusEvent { + pub acknowledgement: AcknowledgementStatus, +} + +impl From for ModuleEvent { + fn from(ev: AckStatusEvent) -> Self { + let AckStatusEvent { acknowledgement } = ev; + let attr_label = match acknowledgement { + AcknowledgementStatus::Success(_) => "success", + AcknowledgementStatus::Error(_) => "error", + }; + + Self { + kind: EVENT_TYPE_PACKET.to_string(), + attributes: vec![(attr_label, acknowledgement.to_string()).into()], + } + } +} + +/// Event emitted in the `onTimeoutPacket` module callback +pub struct TimeoutEvent { + pub refund_receiver: Signer, + pub refund_class: PrefixedClassId, + pub refund_tokens: TokenIds, + pub memo: Memo, +} + +impl From for ModuleEvent { + fn from(ev: TimeoutEvent) -> Self { + let TimeoutEvent { + refund_receiver, + refund_class, + refund_tokens, + memo, + } = ev; + Self { + kind: EVENT_TYPE_TIMEOUT.to_string(), + attributes: vec![ + ("module", MODULE_ID_STR).into(), + ("refund_receiver", refund_receiver).into(), + ("refund_class", refund_class).into(), + ("refund_tokens", refund_tokens).into(), + ("memo", memo).into(), + ], + } + } +} + +/// Event emitted in the `onRecvPacket` module callback when new tokens are minted +pub struct ClassTraceEvent { + pub trace_hash: Option, + pub class: PrefixedClassId, +} + +impl From for ModuleEvent { + fn from(ev: ClassTraceEvent) -> Self { + let ClassTraceEvent { trace_hash, class } = ev; + let mut ev = Self { + kind: EVENT_TYPE_CLASS_TRACE.to_string(), + attributes: vec![("class", class).into()], + }; + if let Some(hash) = trace_hash { + ev.attributes.push(("trace_hash", hash).into()); + } + ev + } +} + +/// Event emitted after a successful `sendTransfer` +pub struct TransferEvent { + pub sender: Signer, + pub receiver: Signer, + pub class: PrefixedClassId, + pub tokens: TokenIds, + pub memo: Memo, +} + +impl From for ModuleEvent { + fn from(ev: TransferEvent) -> Self { + let TransferEvent { + sender, + receiver, + class, + tokens, + memo, + } = ev; + + Self { + kind: EVENT_TYPE_TRANSFER.to_string(), + attributes: vec![ + ("sender", sender).into(), + ("receiver", receiver).into(), + ("class", class).into(), + ("tokens", tokens).into(), + ("memo", memo).into(), + ], + } + } +} + +impl From for ModuleEvent { + fn from(ev: Event) -> Self { + match ev { + Event::Recv(ev) => ev.into(), + Event::Ack(ev) => ev.into(), + Event::AckStatus(ev) => ev.into(), + Event::Timeout(ev) => ev.into(), + Event::ClassTrace(ev) => ev.into(), + Event::Transfer(ev) => ev.into(), + } + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/lib.rs b/ibc-apps/ics721-nft-transfer/types/src/lib.rs index a77f26c1b..74fed4f5e 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/lib.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/lib.rs @@ -17,6 +17,51 @@ #[cfg(any(test, feature = "std"))] extern crate std; -pub mod error; +#[cfg(feature = "serde")] +mod class; +#[cfg(feature = "serde")] +pub use class::*; +#[cfg(feature = "serde")] +mod data; +#[cfg(feature = "serde")] pub mod events; +#[cfg(feature = "serde")] pub mod msgs; +#[cfg(feature = "serde")] +pub mod packet; +#[cfg(feature = "serde")] +mod token; +#[cfg(feature = "serde")] +pub use token::*; + +#[cfg(feature = "serde")] +pub(crate) mod serializers; + +pub mod error; +mod memo; +pub use memo::*; + +/// Re-exports ICS-721 NFT transfer proto types from the `ibc-proto` crate. +pub mod proto { + pub use ibc_proto::ibc::apps::nft_transfer; +} + +/// Module identifier for the ICS-721 application. +pub const MODULE_ID_STR: &str = "nft_transfer"; + +/// The port identifier that the ICS-721 applications typically bind with. +pub const PORT_ID_STR: &str = "nft-transfer"; + +/// ICS-721 application current version. +pub const VERSION: &str = "ics721-1"; + +/// The successful string used for creating an acknowledgement status, +/// equivalent to `base64::encode(0x01)`. +pub const ACK_SUCCESS_B64: &str = "AQ=="; + +use ibc_core::channel::types::acknowledgement::StatusValue; + +/// Returns a successful acknowledgement status for the NFT transfer application. +pub fn ack_success_b64() -> StatusValue { + StatusValue::new(ACK_SUCCESS_B64).expect("ack status value is never supposed to be empty") +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/memo.rs b/ibc-apps/ics721-nft-transfer/types/src/memo.rs new file mode 100644 index 000000000..9db8e4e11 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/memo.rs @@ -0,0 +1,54 @@ +//! Defines the memo type, which represents the string that users can include +//! with a Non-Fungible Token Transfer + +use core::convert::Infallible; +use core::fmt::{ + Display, {self}, +}; +use core::str::FromStr; + +use ibc_core::primitives::prelude::*; + +/// Represents the token transfer memo +#[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, PartialEq, Eq)] +pub struct Memo(String); + +impl AsRef for Memo { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Display for Memo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Memo { + fn from(memo: String) -> Self { + Self(memo) + } +} + +impl FromStr for Memo { + type Err = Infallible; + + fn from_str(memo: &str) -> Result { + Ok(Self(memo.to_owned())) + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs b/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs index 0c735cd44..70f4adfef 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/msgs/mod.rs @@ -1 +1,2 @@ //! Defines the Non-Fungible Token Transfer (ICS-721) message types. +pub mod transfer; diff --git a/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs b/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs new file mode 100644 index 000000000..49ea39363 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/msgs/transfer.rs @@ -0,0 +1,128 @@ +//! Defines the Non-Fungible Token Transfer message type + +use ibc_core::channel::types::error::PacketError; +use ibc_core::channel::types::timeout::TimeoutHeight; +use ibc_core::handler::types::error::ContextError; +use ibc_core::host::types::identifiers::{ChannelId, PortId}; +use ibc_core::primitives::prelude::*; +use ibc_core::primitives::Timestamp; +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::applications::nft_transfer::v1::MsgTransfer as RawMsgTransfer; +use ibc_proto::Protobuf; + +use crate::error::NftTransferError; +use crate::packet::PacketData; + +pub(crate) const TYPE_URL: &str = "/ibc.applications.nft_transfer.v1.MsgTransfer"; + +/// Message used to build an ICS-721 Non-Fungible Token Transfer packet. +/// +/// Note that this message is not a packet yet, as it lacks the proper sequence +/// number, and destination port/channel. This is by design. The sender of the +/// packet, which might be the user of a command line application, should only +/// have to specify the information related to the transfer of the token, and +/// let the library figure out how to build the packet properly. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode,) +)] +#[cfg_attr( + feature = "borsh", + derive(borsh::BorshSerialize, borsh::BorshDeserialize) +)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub struct MsgTransfer { + /// the port on which the packet will be sent + pub port_id_on_a: PortId, + /// the channel by which the packet will be sent + pub chan_id_on_a: ChannelId, + /// NFT transfer packet data of the packet that will be sent + pub packet_data: PacketData, + /// Timeout height relative to the current block height. + /// The timeout is disabled when set to None. + pub timeout_height_on_b: TimeoutHeight, + /// Timeout timestamp relative to the current block timestamp. + /// The timeout is disabled when set to 0. + pub timeout_timestamp_on_b: Timestamp, +} + +impl TryFrom for MsgTransfer { + type Error = NftTransferError; + + fn try_from(raw_msg: RawMsgTransfer) -> Result { + let timeout_timestamp_on_b = Timestamp::from_nanoseconds(raw_msg.timeout_timestamp) + .map_err(PacketError::InvalidPacketTimestamp) + .map_err(ContextError::from)?; + + let timeout_height_on_b: TimeoutHeight = raw_msg + .timeout_height + .try_into() + .map_err(ContextError::from)?; + + // Packet timeout height and packet timeout timestamp cannot both be unset. + if !timeout_height_on_b.is_set() && !timeout_timestamp_on_b.is_set() { + return Err(ContextError::from(PacketError::MissingTimeout))?; + } + + Ok(MsgTransfer { + port_id_on_a: raw_msg.source_port.parse()?, + chan_id_on_a: raw_msg.source_channel.parse()?, + packet_data: PacketData { + class_id: raw_msg.class_id.parse()?, + class_uri: None, + class_data: None, + token_ids: raw_msg.token_ids.try_into()?, + token_uris: vec![], + token_data: vec![], + sender: raw_msg.sender.into(), + receiver: raw_msg.receiver.into(), + memo: raw_msg.memo.into(), + }, + timeout_height_on_b, + timeout_timestamp_on_b, + }) + } +} + +impl From for RawMsgTransfer { + fn from(domain_msg: MsgTransfer) -> Self { + RawMsgTransfer { + source_port: domain_msg.port_id_on_a.to_string(), + source_channel: domain_msg.chan_id_on_a.to_string(), + class_id: domain_msg.packet_data.class_id.to_string(), + token_ids: domain_msg + .packet_data + .token_ids + .as_ref() + .iter() + .map(|t| t.to_string()) + .collect(), + sender: domain_msg.packet_data.sender.to_string(), + 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(), + } + } +} + +impl Protobuf for MsgTransfer {} + +impl TryFrom for MsgTransfer { + type Error = NftTransferError; + + fn try_from(raw: Any) -> Result { + match raw.type_url.as_str() { + TYPE_URL => { + MsgTransfer::decode_vec(&raw.value).map_err(|e| NftTransferError::DecodeRawMsg { + reason: e.to_string(), + }) + } + _ => Err(NftTransferError::UnknownMsgType { + msg_type: raw.type_url, + }), + } + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs new file mode 100644 index 000000000..296f7ac06 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -0,0 +1,246 @@ +//! Contains the `PacketData` type that defines the structure of NFT transfers' packet bytes + +use core::convert::TryFrom; + +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::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") +)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode,) +)] +#[cfg_attr( + feature = "borsh", + derive(borsh::BorshSerialize, borsh::BorshDeserialize) +)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PacketData { + pub class_id: PrefixedClassId, + pub class_uri: Option, + pub class_data: Option, + pub token_ids: TokenIds, + pub token_uris: Vec, + pub token_data: Vec, + pub sender: Signer, + pub receiver: Signer, + pub memo: Memo, +} + +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 { + 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 { + class_id, + class_uri, + class_data, + token_ids, + token_uris, + token_data, + sender, + receiver, + memo, + }) + } +} + +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 { + Some(raw_pkt_data.class_data.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(|t| t.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| 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| 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; + + use super::*; + + const DUMMY_ADDRESS: &str = "cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"; + 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"}}"#; + + impl PacketData { + pub fn new_dummy() -> Self { + let address: Signer = DUMMY_ADDRESS.to_string().into(); + + Self { + class_id: PrefixedClassId::from_str(DUMMY_CLASS_ID).unwrap(), + class_uri: Some(ClassUri::from_str(DUMMY_URI).unwrap()), + 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![ + TokenUri::from_str(DUMMY_URI).unwrap(), + TokenUri::from_str(DUMMY_URI).unwrap(), + ], + token_data: vec![ + TokenData::from_str(DUMMY_DATA).unwrap(), + TokenData::from_str(DUMMY_DATA).unwrap(), + ], + sender: address.clone(), + receiver: address, + memo: "".to_string().into(), + } + } + + pub fn new_min_dummy() -> Self { + let address: Signer = DUMMY_ADDRESS.to_string().into(); + + Self { + class_id: PrefixedClassId::from_str(DUMMY_CLASS_ID).unwrap(), + class_uri: None, + class_data: None, + token_ids: TokenIds::try_from(vec!["token_0".to_string()]).unwrap(), + token_uris: vec![], + token_data: vec![], + sender: address.clone(), + receiver: address, + memo: "".to_string().into(), + } + } + + pub fn ser_json_assert_eq(&self, json: &str) { + let ser = serde_json::to_string(&self).unwrap(); + assert_eq!(ser, json); + } + + pub fn deser_json_assert_eq(&self, json: &str) { + let deser: Self = serde_json::from_str(json).unwrap(); + assert_eq!(&deser, self); + } + } + + fn dummy_min_json_packet_data() -> &'static str { + r#"{"class_id":"class","token_ids":["token_0"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# + } + + fn dummy_json_packet_data() -> &'static str { + r#"{"class_id":"class","class_uri":"http://example.com/","class_data":"{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","token_ids":["token_0","token_1"],"token_uris":["http://example.com/","http://example.com/"],"token_data":["{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":""}"# + } + + fn dummy_json_packet_data_without_memo() -> &'static str { + r#"{"class_id":"class","class_uri":"http://example.com","class_data":"{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","token_ids":["token_0","token_1"],"token_uris":["http://example.com","http://example.com"],"token_data":["{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# + } + + /// Ensures `PacketData` properly encodes to JSON by first converting to a + /// `RawPacketData` and then serializing that. + #[test] + fn test_packet_data_ser() { + PacketData::new_dummy().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_min_dummy().deser_json_assert_eq(dummy_min_json_packet_data()); + } + + #[test] + fn test_invalid_packet_data() { + // the number of tokens is mismatched + let packet_data = r#"{"class_id":"class","token_ids":["token_0","token_1"],"token_uris":["http://example.com"],"token_data":["{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":""}"#; + assert!( + serde_json::from_str::(packet_data).is_err(), + "num of token data is unmatched" + ); + + // No token ID + let packet_data = r#"{"class_id":"class","token_ids":[],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":""}"#; + assert!( + serde_json::from_str::(packet_data).is_err(), + "no token ID" + ); + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/serializers.rs b/ibc-apps/ics721-nft-transfer/types/src/serializers.rs new file mode 100644 index 000000000..65c4d2ba6 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/serializers.rs @@ -0,0 +1,27 @@ +use core::fmt::Display; +use core::str::FromStr; + +use ibc_core::primitives::prelude::*; +use serde::{de, Deserialize, Deserializer, Serializer}; + +// Note: This method serializes to a String instead of a str +// in order to avoid a wasm compilation issue. Specifically, +// str (de)serialization hits some kind of f64/f32 case +// when compiled into wasm, but this fails validation on +// f32/f64 wasm runtimes. +pub fn serialize(value: &T, serializer: S) -> Result +where + T: Display, + S: Serializer, +{ + serializer.serialize_str(value.to_string().as_ref()) +} + +pub fn deserialize<'de, T, D>(deserializer: D) -> Result +where + T: FromStr, + T::Err: Display, + D: Deserializer<'de>, +{ + T::from_str(::deserialize(deserializer)?.as_str()).map_err(de::Error::custom) +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/token.rs b/ibc-apps/ics721-nft-transfer/types/src/token.rs new file mode 100644 index 000000000..10303a250 --- /dev/null +++ b/ibc-apps/ics721-nft-transfer/types/src/token.rs @@ -0,0 +1,221 @@ +//! Defines Non-Fungible Token Transfer (ICS-721) token types. +use core::fmt::{self, Display}; +use core::str::FromStr; + +use http::Uri; +use ibc_core::primitives::prelude::*; + +use crate::data::Data; +use crate::error::NftTransferError; +use crate::serializers; + +/// Token ID for an NFT +#[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, PartialEq, Eq, PartialOrd, Ord)] +pub struct TokenId(String); + +impl AsRef for TokenId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Display for TokenId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for TokenId { + type Err = NftTransferError; + + fn from_str(token_id: &str) -> Result { + if token_id.trim().is_empty() { + Err(NftTransferError::InvalidTokenId) + } else { + Ok(Self(token_id.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, PartialEq, Eq)] +pub struct TokenIds(pub Vec); + +impl TokenIds { + pub fn as_ref(&self) -> Vec<&TokenId> { + self.0.iter().collect() + } +} + +impl Display for TokenIds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + self.0 + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(",") + ) + } +} + +impl TryFrom> for TokenIds { + type Error = NftTransferError; + + fn try_from(token_ids: Vec) -> Result { + if token_ids.is_empty() { + return Err(NftTransferError::NoTokenId); + } + let ids: Result, _> = token_ids.iter().map(|t| t.parse()).collect(); + let mut ids = ids?; + ids.sort(); + ids.dedup(); + if ids.len() != token_ids.len() { + return Err(NftTransferError::DuplicatedTokenIds); + } + Ok(Self(ids)) + } +} + +/// Token URI for an NFT +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TokenUri( + #[cfg_attr(feature = "serde", serde(with = "serializers"))] + #[cfg_attr(feature = "schema", schemars(with = "String"))] + Uri, +); + +#[cfg(feature = "borsh")] +impl borsh::BorshSerialize for TokenUri { + fn serialize( + &self, + writer: &mut W, + ) -> borsh::maybestd::io::Result<()> { + borsh::BorshSerialize::serialize(&self.to_string(), writer) + } +} + +#[cfg(feature = "borsh")] +impl borsh::BorshDeserialize for TokenUri { + fn deserialize_reader( + reader: &mut R, + ) -> borsh::maybestd::io::Result { + let uri = String::deserialize_reader(reader)?; + Ok(TokenUri::from_str(&uri).map_err(|_| borsh::maybestd::io::ErrorKind::Other)?) + } +} + +#[cfg(feature = "parity-scale-codec")] +impl parity_scale_codec::Encode for TokenUri { + fn encode_to(&self, writer: &mut T) { + self.to_string().encode_to(writer); + } +} + +#[cfg(feature = "parity-scale-codec")] +impl parity_scale_codec::Decode for TokenUri { + fn decode( + input: &mut I, + ) -> Result { + let uri = String::decode(input)?; + TokenUri::from_str(&uri).map_err(|_| parity_scale_codec::Error::from("from str error")) + } +} + +#[cfg(feature = "parity-scale-codec")] +impl scale_info::TypeInfo for TokenUri { + type Identity = Self; + + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("TokenUri", module_path!())) + .composite( + scale_info::build::Fields::unnamed() + .field(|f| f.ty::().type_name("String")), + ) + } +} + +impl Display for TokenUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for TokenUri { + type Err = NftTransferError; + + fn from_str(token_uri: &str) -> Result { + match Uri::from_str(token_uri) { + Ok(uri) => Ok(Self(uri)), + Err(err) => Err(NftTransferError::InvalidUri { + uri: token_uri.to_string(), + validation_error: err, + }), + } + } +} + +/// Token data for an NFT +#[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, PartialEq, Eq)] +pub struct TokenData(Data); + +impl Display for TokenData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for TokenData { + type Err = NftTransferError; + + fn from_str(token_data: &str) -> Result { + let data = Data::from_str(token_data)?; + Ok(Self(data)) + } +} diff --git a/ibc-primitives/src/prelude.rs b/ibc-primitives/src/prelude.rs index 3662f7bb9..b7ac674e9 100644 --- a/ibc-primitives/src/prelude.rs +++ b/ibc-primitives/src/prelude.rs @@ -2,6 +2,7 @@ // https://doc.rust-lang.org/src/alloc/prelude/v1.rs.html pub use alloc::borrow::ToOwned; pub use alloc::boxed::Box; +pub use alloc::collections::BTreeMap; pub use alloc::string::{String, ToString}; pub use alloc::vec::Vec; pub use alloc::{format, str, vec}; diff --git a/ibc-testkit/src/testapp/ibc/applications/mod.rs b/ibc-testkit/src/testapp/ibc/applications/mod.rs index 014e52f27..85baa285f 100644 --- a/ibc-testkit/src/testapp/ibc/applications/mod.rs +++ b/ibc-testkit/src/testapp/ibc/applications/mod.rs @@ -1 +1,2 @@ +pub mod nft_transfer; pub mod transfer; diff --git a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs new file mode 100644 index 000000000..491ec9ca4 --- /dev/null +++ b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/context.rs @@ -0,0 +1,185 @@ +use ibc::apps::nft_transfer::context::{ + NftClassContext, NftContext, NftTransferExecutionContext, NftTransferValidationContext, +}; +use ibc::apps::nft_transfer::types::error::NftTransferError; +use ibc::apps::nft_transfer::types::{ + ClassData, ClassId, ClassUri, Memo, PrefixedClassId, TokenData, TokenId, TokenUri, +}; +use ibc::core::host::types::identifiers::{ChannelId, PortId}; +use ibc::core::primitives::prelude::*; +use ibc::core::primitives::Signer; + +use super::types::{DummyNft, DummyNftClass, DummyNftTransferModule}; + +impl NftContext for DummyNft { + fn get_class_id(&self) -> &ClassId { + &self.class_id + } + + fn get_id(&self) -> &TokenId { + &self.token_id + } + + fn get_uri(&self) -> &TokenUri { + &self.token_uri + } + + fn get_data(&self) -> &TokenData { + &self.token_data + } +} + +impl NftClassContext for DummyNftClass { + fn get_id(&self) -> &ClassId { + &self.class_id + } + + fn get_uri(&self) -> &ClassUri { + &self.class_uri + } + + fn get_data(&self) -> &ClassData { + &self.class_data + } +} + +impl NftTransferValidationContext for DummyNftTransferModule { + type AccountId = Signer; + type Nft = DummyNft; + type NftClass = DummyNftClass; + + fn get_port(&self) -> Result { + Ok(PortId::transfer()) + } + + fn can_send_nft(&self) -> Result<(), NftTransferError> { + Ok(()) + } + + fn can_receive_nft(&self) -> Result<(), NftTransferError> { + Ok(()) + } + + fn create_or_update_class_validate( + &self, + _class_id: &PrefixedClassId, + _class_uri: &ClassUri, + _class_data: &ClassData, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn escrow_nft_validate( + &self, + _from_account: &Self::AccountId, + _port_id: &PortId, + _channel_id: &ChannelId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + _memo: &Memo, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn unescrow_nft_validate( + &self, + _to_account: &Self::AccountId, + _port_id: &PortId, + _channel_id: &ChannelId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn mint_nft_validate( + &self, + _account: &Self::AccountId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + _token_uri: &TokenUri, + _token_data: &TokenData, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn burn_nft_validate( + &self, + _account: &Self::AccountId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + _memo: &Memo, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn get_nft( + &self, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + ) -> Result { + Ok(DummyNft::default()) + } + + fn get_nft_class( + &self, + _class_id: &PrefixedClassId, + ) -> Result { + Ok(DummyNftClass::default()) + } +} + +impl NftTransferExecutionContext for DummyNftTransferModule { + fn create_or_update_class_execute( + &self, + _class_id: &PrefixedClassId, + _class_uri: &ClassUri, + _class_data: &ClassData, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn escrow_nft_execute( + &mut self, + _from_account: &Self::AccountId, + _port_id: &PortId, + _channel_id: &ChannelId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + _memo: &Memo, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn unescrow_nft_execute( + &mut self, + _to_account: &Self::AccountId, + _port_id: &PortId, + _channel_id: &ChannelId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn mint_nft_execute( + &mut self, + _account: &Self::AccountId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + _token_uri: &TokenUri, + _token_data: &TokenData, + ) -> Result<(), NftTransferError> { + Ok(()) + } + + fn burn_nft_execute( + &mut self, + _account: &Self::AccountId, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + _memo: &Memo, + ) -> Result<(), NftTransferError> { + Ok(()) + } +} diff --git a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/mod.rs b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/mod.rs new file mode 100644 index 000000000..a811b728e --- /dev/null +++ b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "serde")] +pub mod context; +pub mod module; +pub mod types; diff --git a/ibc-testkit/src/testapp/ibc/applications/nft_transfer/module.rs b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/module.rs new file mode 100644 index 000000000..06652a1dd --- /dev/null +++ b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/module.rs @@ -0,0 +1,107 @@ +use ibc::core::channel::types::acknowledgement::Acknowledgement; +use ibc::core::channel::types::channel::{Counterparty, Order}; +use ibc::core::channel::types::error::{ChannelError, PacketError}; +use ibc::core::channel::types::packet::Packet; +use ibc::core::channel::types::Version; +use ibc::core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc::core::primitives::prelude::*; +use ibc::core::primitives::Signer; +use ibc::core::router::module::Module; +use ibc::core::router::types::module::ModuleExtras; + +use super::types::DummyNftTransferModule; + +impl Module for DummyNftTransferModule { + fn on_chan_open_init_validate( + &self, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + version: &Version, + ) -> Result { + Ok(version.clone()) + } + + fn on_chan_open_init_execute( + &mut self, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + version: &Version, + ) -> Result<(ModuleExtras, Version), ChannelError> { + Ok((ModuleExtras::empty(), version.clone())) + } + + fn on_chan_open_try_validate( + &self, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + counterparty_version: &Version, + ) -> Result { + Ok(counterparty_version.clone()) + } + + fn on_chan_open_try_execute( + &mut self, + _order: Order, + _connection_hops: &[ConnectionId], + _port_id: &PortId, + _channel_id: &ChannelId, + _counterparty: &Counterparty, + counterparty_version: &Version, + ) -> Result<(ModuleExtras, Version), ChannelError> { + Ok((ModuleExtras::empty(), counterparty_version.clone())) + } + + fn on_recv_packet_execute( + &mut self, + _packet: &Packet, + _relayer: &Signer, + ) -> (ModuleExtras, Acknowledgement) { + ( + ModuleExtras::empty(), + Acknowledgement::try_from(vec![1u8]).expect("Never fails"), + ) + } + + fn on_timeout_packet_validate( + &self, + _packet: &Packet, + _relayer: &Signer, + ) -> Result<(), PacketError> { + Ok(()) + } + + fn on_timeout_packet_execute( + &mut self, + _packet: &Packet, + _relayer: &Signer, + ) -> (ModuleExtras, Result<(), PacketError>) { + (ModuleExtras::empty(), Ok(())) + } + + fn on_acknowledgement_packet_validate( + &self, + _packet: &Packet, + _acknowledgement: &Acknowledgement, + _relayer: &Signer, + ) -> Result<(), PacketError> { + Ok(()) + } + + fn on_acknowledgement_packet_execute( + &mut self, + _packet: &Packet, + _acknowledgement: &Acknowledgement, + _relayer: &Signer, + ) -> (ModuleExtras, Result<(), PacketError>) { + (ModuleExtras::empty(), 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 new file mode 100644 index 000000000..6eb65e285 --- /dev/null +++ b/ibc-testkit/src/testapp/ibc/applications/nft_transfer/types.rs @@ -0,0 +1,61 @@ +use ibc::apps::nft_transfer::types::{ClassData, ClassId, ClassUri, TokenData, TokenId, TokenUri}; + +#[derive(Debug)] +pub struct DummyNftTransferModule; + +#[derive(Debug)] +pub struct DummyNft { + pub class_id: ClassId, + pub token_id: TokenId, + pub token_uri: TokenUri, + pub token_data: TokenData, +} + +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 data = r#"{"name":{"value":"Crypto Creatures"},"image":{"value":"binary","mime":"image/png"}}"#; + let token_data = data.parse().expect("infallible"); + Self { + class_id, + token_id, + token_uri, + token_data, + } + } +} + +#[derive(Debug)] +pub struct DummyNftClass { + pub class_id: ClassId, + pub class_uri: ClassUri, + pub class_data: ClassData, +} + +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 data = r#"{"name":{"value":"Crypto Creatures"},"image":{"value":"binary","mime":"image/png"}}"#; + let class_data = data.parse().expect("infallible"); + Self { + class_id, + class_uri, + class_data, + } + } +} + +impl DummyNftTransferModule { + pub fn new() -> Self { + Self + } +} + +impl Default for DummyNftTransferModule { + fn default() -> Self { + Self::new() + } +} diff --git a/ibc-testkit/tests/applications/mod.rs b/ibc-testkit/tests/applications/mod.rs index 544cab2c5..2b046ccd5 100644 --- a/ibc-testkit/tests/applications/mod.rs +++ b/ibc-testkit/tests/applications/mod.rs @@ -1,2 +1,4 @@ #[cfg(feature = "serde")] +pub mod nft_transfer; +#[cfg(feature = "serde")] pub mod transfer; diff --git a/ibc-testkit/tests/applications/nft_transfer.rs b/ibc-testkit/tests/applications/nft_transfer.rs new file mode 100644 index 000000000..05996a534 --- /dev/null +++ b/ibc-testkit/tests/applications/nft_transfer.rs @@ -0,0 +1,135 @@ +use ibc::apps::nft_transfer::module::{ + on_chan_open_init_execute, on_chan_open_init_validate, on_chan_open_try_execute, + on_chan_open_try_validate, +}; +use ibc::apps::nft_transfer::types::VERSION; +use ibc::core::channel::types::channel::{Counterparty, Order}; +use ibc::core::channel::types::Version; +use ibc::core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc::core::primitives::prelude::*; +use ibc_testkit::testapp::ibc::applications::nft_transfer::types::DummyNftTransferModule; + +fn get_defaults() -> ( + DummyNftTransferModule, + Order, + Vec, + PortId, + ChannelId, + Counterparty, +) { + let order = Order::Unordered; + let connection_hops = vec![ConnectionId::new(1)]; + let port_id = PortId::transfer(); + let channel_id = ChannelId::new(1); + let counterparty = Counterparty::new(port_id.clone(), Some(channel_id.clone())); + + ( + DummyNftTransferModule, + order, + connection_hops, + port_id, + channel_id, + counterparty, + ) +} + +/// If the relayer passed "", indicating that it wants us to return the versions we support. +#[test] +fn test_on_chan_open_init_empty_version() { + let (mut ctx, order, connection_hops, port_id, channel_id, counterparty) = get_defaults(); + + let in_version = Version::new("".to_string()); + + let (_, out_version) = on_chan_open_init_execute( + &mut ctx, + order, + &connection_hops, + &port_id, + &channel_id, + &counterparty, + &in_version, + ) + .unwrap(); + + assert_eq!(out_version, Version::new(VERSION.to_string())); +} + +/// If the relayer passed in the only supported version (ics721-1), then return ics721-1 +#[test] +fn test_on_chan_open_init_ics721_version() { + let (mut ctx, order, connection_hops, port_id, channel_id, counterparty) = get_defaults(); + + let in_version = Version::new(VERSION.to_string()); + let (_, out_version) = on_chan_open_init_execute( + &mut ctx, + order, + &connection_hops, + &port_id, + &channel_id, + &counterparty, + &in_version, + ) + .unwrap(); + + assert_eq!(out_version, Version::new(VERSION.to_string())); +} + +/// If the relayer passed in an unsupported version, then fail +#[test] +fn test_on_chan_open_init_incorrect_version() { + let (ctx, order, connection_hops, port_id, channel_id, counterparty) = get_defaults(); + + let in_version = Version::new("some-unsupported-version".to_string()); + let res = on_chan_open_init_validate( + &ctx, + order, + &connection_hops, + &port_id, + &channel_id, + &counterparty, + &in_version, + ); + + assert!(res.is_err()); +} + +/// If the counterparty supports ics721, then return ics721 +#[test] +fn test_on_chan_open_try_counterparty_correct_version() { + let (mut ctx, order, connection_hops, port_id, channel_id, counterparty) = get_defaults(); + + let counterparty_version = Version::new(VERSION.to_string()); + + let (_, out_version) = on_chan_open_try_execute( + &mut ctx, + order, + &connection_hops, + &port_id, + &channel_id, + &counterparty, + &counterparty_version, + ) + .unwrap(); + + assert_eq!(out_version, Version::new(VERSION.to_string())); +} + +/// If the counterparty doesn't support ics721, then fail +#[test] +fn test_on_chan_open_try_counterparty_incorrect_version() { + let (ctx, order, connection_hops, port_id, channel_id, counterparty) = get_defaults(); + + let counterparty_version = Version::new("some-unsupported-version".to_string()); + + let res = on_chan_open_try_validate( + &ctx, + order, + &connection_hops, + &port_id, + &channel_id, + &counterparty, + &counterparty_version, + ); + + assert!(res.is_err()); +} From 8fa64dd02b6d779ff289a34273b208676cc48f63 Mon Sep 17 00:00:00 2001 From: Sean Chen Date: Fri, 5 Jan 2024 18:19:22 -0600 Subject: [PATCH 3/8] Add (de)serialization tests for `DataValue`, `TokenUri`, and `ClassUri` types (#1027) * WIP: add types and contexts * WIP: add events * WIP: implement modules * add send_transfer * add recv and refund handlers * add tests * fix send and recv * fix context and add tests * fix fmt * fix for CI * fix messages and serde * fix comments * Stub out DataValue Borsh unit test * Add basic borsh (de)ser roundtrip tests * Add basic serde roundtrip tests for DataValue * Add json (de)serialization tests * Add roundtrip tests for TokenUri * Add roundtrip tests for ClassUri * Remove ignore statement on a test * Resolve clippy warning * Change packet data dummy json strings to use camel case * Configure nft-transfer app under std feature flag * Move cfg statement * Add nft-transfer feature * Add nft-transfer feature * Remove nft-transfer feature from default features * Remove `optional = true` from `http` dependency --------- Co-authored-by: yito88 --- ci/no-std-check/Cargo.lock | 41 ----------- ibc-apps/Cargo.toml | 11 ++- ibc-apps/ics721-nft-transfer/types/Cargo.toml | 1 + .../ics721-nft-transfer/types/src/class.rs | 41 +++++++++++ .../ics721-nft-transfer/types/src/data.rs | 69 +++++++++++++++++++ .../ics721-nft-transfer/types/src/packet.rs | 6 +- .../ics721-nft-transfer/types/src/token.rs | 47 +++++++++++++ ibc-apps/src/lib.rs | 1 + 8 files changed, 167 insertions(+), 50 deletions(-) diff --git a/ci/no-std-check/Cargo.lock b/ci/no-std-check/Cargo.lock index 9470b926c..e3d5c38ca 100644 --- a/ci/no-std-check/Cargo.lock +++ b/ci/no-std-check/Cargo.lock @@ -1193,17 +1193,6 @@ dependencies = [ "hmac 0.8.1", ] -[[package]] -name = "http" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "iana-time-zone" version = "0.1.59" @@ -1239,29 +1228,6 @@ dependencies = [ "ibc-primitives", ] -[[package]] -name = "ibc-app-nft-transfer" -version = "0.49.1" -dependencies = [ - "ibc-app-nft-transfer-types", - "ibc-core", - "serde-json-wasm", -] - -[[package]] -name = "ibc-app-nft-transfer-types" -version = "0.49.1" -dependencies = [ - "derive_more", - "displaydoc", - "http", - "ibc-core", - "ibc-proto 0.40.0", - "mime", - "serde", - "serde-json-wasm", -] - [[package]] name = "ibc-app-transfer" version = "0.49.1" @@ -1288,7 +1254,6 @@ dependencies = [ name = "ibc-apps" version = "0.49.1" dependencies = [ - "ibc-app-nft-transfer", "ibc-app-transfer", ] @@ -1942,12 +1907,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "miniz_oxide" version = "0.7.1" diff --git a/ibc-apps/Cargo.toml b/ibc-apps/Cargo.toml index eb5811982..a6fe583b3 100644 --- a/ibc-apps/Cargo.toml +++ b/ibc-apps/Cargo.toml @@ -18,29 +18,28 @@ all-features = true [dependencies] ibc-app-transfer = { workspace = true } -ibc-app-nft-transfer = { workspace = true } +ibc-app-nft-transfer = { workspace = true, optional = true, features = [ "std", "serde", "schema", "borsh", "parity-scale-codec" ] } [features] default = ["std"] std = [ "ibc-app-transfer/std", - "ibc-app-nft-transfer/std", + "nft-transfer", ] serde = [ "ibc-app-transfer/serde", - "ibc-app-nft-transfer/serde", ] schema = [ "ibc-app-transfer/schema", - "ibc-app-nft-transfer/schema", "serde", "std", ] borsh = [ "ibc-app-transfer/borsh", - "ibc-app-nft-transfer/borsh", ] parity-scale-codec = [ "ibc-app-transfer/parity-scale-codec", - "ibc-app-nft-transfer/parity-scale-codec", +] +nft-transfer = [ + "ibc-app-nft-transfer" ] diff --git a/ibc-apps/ics721-nft-transfer/types/Cargo.toml b/ibc-apps/ics721-nft-transfer/types/Cargo.toml index 38aba1545..f9384a426 100644 --- a/ibc-apps/ics721-nft-transfer/types/Cargo.toml +++ b/ibc-apps/ics721-nft-transfer/types/Cargo.toml @@ -45,6 +45,7 @@ std = [ "serde/std", "serde_json/std", "displaydoc/std", + "http/std", "ibc-core/std", "ibc-proto/std", ] diff --git a/ibc-apps/ics721-nft-transfer/types/src/class.rs b/ibc-apps/ics721-nft-transfer/types/src/class.rs index de8b4111b..f5d5e590f 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/class.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/class.rs @@ -558,4 +558,45 @@ mod tests { Ok(()) } + + #[test] + fn test_serde_json_roundtrip() { + fn serde_roundtrip(class_uri: ClassUri) { + let serialized = + serde_json::to_string(&class_uri).expect("failed to serialize ClassUri"); + let deserialized = serde_json::from_str::(&serialized) + .expect("failed to deserialize ClassUri"); + + assert_eq!(deserialized, class_uri); + } + + let uri = "/foo/bar?baz".parse::().unwrap(); + serde_roundtrip(ClassUri(uri)); + + let uri = "https://www.rust-lang.org/install.html" + .parse::() + .unwrap(); + serde_roundtrip(ClassUri(uri)); + } + + #[cfg(feature = "borsh")] + #[test] + fn test_borsh_roundtrip() { + fn borsh_roundtrip(class_uri: ClassUri) { + use borsh::{BorshDeserialize, BorshSerialize}; + + let class_uri_bytes = class_uri.try_to_vec().unwrap(); + let res = ClassUri::try_from_slice(&class_uri_bytes).unwrap(); + + assert_eq!(class_uri, res); + } + + let uri = "/foo/bar?baz".parse::().unwrap(); + borsh_roundtrip(ClassUri(uri)); + + let uri = "https://www.rust-lang.org/install.html" + .parse::() + .unwrap(); + borsh_roundtrip(ClassUri(uri)); + } } diff --git a/ibc-apps/ics721-nft-transfer/types/src/data.rs b/ibc-apps/ics721-nft-transfer/types/src/data.rs index 936dbf54a..a5fe516c0 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/data.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/data.rs @@ -185,3 +185,72 @@ impl FromStr for Data { Ok(Self(data)) } } + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[cfg(feature = "serde")] + #[rstest] + #[case(r#"{"value":"foo"}"#)] + #[case(r#"{"value":"foo-42","mime":"multipart/form-data; boundary=ABCDEFG"}"#)] + fn test_valid_json_deserialization(#[case] data_value_json: &str) { + assert!(serde_json::from_str::(data_value_json).is_ok()); + } + + #[cfg(feature = "serde")] + #[rstest] + #[case(r#"{"value":"foo-42","mime":"invalid"}"#)] + #[case(r#"{"value":"invalid","mime":""}"#)] + fn test_invalid_json_deserialization(#[case] data_value_json: &str) { + assert!(serde_json::from_str::(data_value_json).is_err()); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serde_json_roundtrip() { + fn serde_roundtrip(data_value: DataValue) { + let serialized = + serde_json::to_string(&data_value).expect("failed to serialize DataValue"); + let deserialized = serde_json::from_str::(&serialized) + .expect("failed to deserialize DataValue"); + + assert_eq!(deserialized, data_value); + } + + serde_roundtrip(DataValue { + value: String::from("foo"), + mime: None, + }); + + serde_roundtrip(DataValue { + value: String::from("foo"), + mime: Some(mime::TEXT_PLAIN_UTF_8), + }); + } + + #[cfg(feature = "borsh")] + #[test] + fn test_borsh_roundtrip() { + fn borsh_roundtrip(data_value: DataValue) { + use borsh::{BorshDeserialize, BorshSerialize}; + + let data_value_bytes = data_value.try_to_vec().unwrap(); + let res = DataValue::try_from_slice(&data_value_bytes).unwrap(); + + assert_eq!(data_value, res); + } + + borsh_roundtrip(DataValue { + value: String::from("foo"), + mime: None, + }); + + borsh_roundtrip(DataValue { + value: String::from("foo"), + mime: Some(mime::TEXT_PLAIN_UTF_8), + }); + } +} diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 296f7ac06..36ef6131b 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -200,15 +200,15 @@ mod tests { } fn dummy_min_json_packet_data() -> &'static str { - r#"{"class_id":"class","token_ids":["token_0"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# + r#"{"classId":"class","tokenIds":["token_0"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# } fn dummy_json_packet_data() -> &'static str { - r#"{"class_id":"class","class_uri":"http://example.com/","class_data":"{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","token_ids":["token_0","token_1"],"token_uris":["http://example.com/","http://example.com/"],"token_data":["{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":""}"# + r#"{"classId":"class","classUri":"http://example.com/","classData":"{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com/","http://example.com/"],"tokenData":["{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","memo":""}"# } fn dummy_json_packet_data_without_memo() -> &'static str { - r#"{"class_id":"class","class_uri":"http://example.com","class_data":"{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","token_ids":["token_0","token_1"],"token_uris":["http://example.com","http://example.com"],"token_data":["{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# + r#"{"classId":"class","classUri":"http://example.com","classData":"{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com","http://example.com"],"tokenData":["{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# } /// Ensures `PacketData` properly encodes to JSON by first converting to a diff --git a/ibc-apps/ics721-nft-transfer/types/src/token.rs b/ibc-apps/ics721-nft-transfer/types/src/token.rs index 10303a250..ff8f407bb 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/token.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/token.rs @@ -219,3 +219,50 @@ impl FromStr for TokenData { Ok(Self(data)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "serde")] + #[test] + fn test_serde_json_roundtrip() { + fn serde_roundtrip(token_uri: TokenUri) { + let serialized = + serde_json::to_string(&token_uri).expect("failed to serialize TokenUri"); + let deserialized = serde_json::from_str::(&serialized) + .expect("failed to deserialize TokenUri"); + + assert_eq!(deserialized, token_uri); + } + + let uri = "/foo/bar?baz".parse::().unwrap(); + serde_roundtrip(TokenUri(uri)); + + let uri = "https://www.rust-lang.org/install.html" + .parse::() + .unwrap(); + serde_roundtrip(TokenUri(uri)); + } + + #[cfg(feature = "borsh")] + #[test] + fn test_borsh_roundtrip() { + fn borsh_roundtrip(token_uri: TokenUri) { + use borsh::{BorshDeserialize, BorshSerialize}; + + let token_uri_bytes = token_uri.try_to_vec().unwrap(); + let res = TokenUri::try_from_slice(&token_uri_bytes).unwrap(); + + assert_eq!(token_uri, res); + } + + let uri = "/foo/bar?baz".parse::().unwrap(); + borsh_roundtrip(TokenUri(uri)); + + let uri = "https://www.rust-lang.org/install.html" + .parse::() + .unwrap(); + borsh_roundtrip(TokenUri(uri)); + } +} diff --git a/ibc-apps/src/lib.rs b/ibc-apps/src/lib.rs index e33de2971..306624bc0 100644 --- a/ibc-apps/src/lib.rs +++ b/ibc-apps/src/lib.rs @@ -24,5 +24,6 @@ pub mod transfer { /// (ICS-721) application logic. pub mod nft_transfer { #[doc(inline)] + #[cfg(feature = "nft-transfer")] pub use ibc_app_nft_transfer::*; } From db435986db3217be2a65b87da33b2ba7735b1fc2 Mon Sep 17 00:00:00 2001 From: Yuji Ito Date: Mon, 8 Jan 2024 22:21:10 +0100 Subject: [PATCH 4/8] fix: calculate trace hash from both class ID and token ID (#1032) * trace hash with class ID and token ID * add serde flag --- ibc-apps/ics721-nft-transfer/src/context.rs | 10 +++++--- .../src/handler/on_recv_packet.rs | 24 +++++++++--------- ibc-apps/ics721-nft-transfer/src/lib.rs | 3 +++ .../ics721-nft-transfer/types/src/events.rs | 25 +++++++++++-------- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/src/context.rs b/ibc-apps/ics721-nft-transfer/src/context.rs index 97511fd4a..508f94b6b 100644 --- a/ibc-apps/ics721-nft-transfer/src/context.rs +++ b/ibc-apps/ics721-nft-transfer/src/context.rs @@ -105,9 +105,13 @@ pub trait NftTransferValidationContext { memo: &Memo, ) -> Result<(), NftTransferError>; - /// Returns a hash of the prefixed class ID. - /// Implement only if the host chain supports hashed class ID. - fn class_hash_string(&self, _class_id: &PrefixedClassId) -> Option { + /// Returns a hash of the prefixed class ID and the token ID. + /// Implement only if the host chain supports hashed class ID and token ID. + fn token_hash_string( + &self, + _class_id: &PrefixedClassId, + _token_id: &TokenId, + ) -> Option { None } 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 61a01fef1..768a7f757 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 @@ -4,7 +4,7 @@ use ibc_core::router::types::module::ModuleExtras; use crate::context::NftTransferExecutionContext; use crate::types::error::NftTransferError; -use crate::types::events::ClassTraceEvent; +use crate::types::events::TokenTraceEvent; use crate::types::packet::PacketData; use crate::types::{is_receiver_chain_source, TracePrefix}; @@ -77,24 +77,24 @@ where c }; - let extras = { - let class_trace_event = ClassTraceEvent { - trace_hash: ctx_b.class_hash_string(&class_id), - class: class_id.clone(), - }; - ModuleExtras { - events: vec![class_trace_event.into()], - log: Vec::new(), - } + let mut extras = ModuleExtras { + events: vec![], + log: Vec::new(), }; - for ((token_id, token_uri), token_data) in data .token_ids - .as_ref() + .0 .iter() .zip(data.token_uris.iter()) .zip(data.token_data.iter()) { + let trace_event = TokenTraceEvent { + trace_hash: ctx_b.token_hash_string(&class_id, token_id), + class: class_id.clone(), + token: token_id.clone(), + }; + extras.events.push(trace_event.into()); + // Note: the validation is called before the execution. // Refer to ICS-20 `process_recv_packet_execute()`. diff --git a/ibc-apps/ics721-nft-transfer/src/lib.rs b/ibc-apps/ics721-nft-transfer/src/lib.rs index fe73af5a9..66693ac1d 100644 --- a/ibc-apps/ics721-nft-transfer/src/lib.rs +++ b/ibc-apps/ics721-nft-transfer/src/lib.rs @@ -17,8 +17,11 @@ #[cfg(any(test, feature = "std"))] extern crate std; +#[cfg(feature = "serde")] pub mod context; +#[cfg(feature = "serde")] pub mod handler; +#[cfg(feature = "serde")] pub mod module; /// Re-exports the implementation of the IBC [Non-Fungible Token diff --git a/ibc-apps/ics721-nft-transfer/types/src/events.rs b/ibc-apps/ics721-nft-transfer/types/src/events.rs index b2e236a1b..6f0c90579 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/events.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/events.rs @@ -5,11 +5,11 @@ use ibc_core::primitives::Signer; use ibc_core::router::types::event::ModuleEvent; use super::Memo; -use crate::{PrefixedClassId, TokenIds, MODULE_ID_STR}; +use crate::{PrefixedClassId, TokenId, TokenIds, MODULE_ID_STR}; const EVENT_TYPE_PACKET: &str = "non_fungible_token_packet"; const EVENT_TYPE_TIMEOUT: &str = "timeout"; -const EVENT_TYPE_CLASS_TRACE: &str = "class_trace"; +const EVENT_TYPE_TOKEN_TRACE: &str = "token_trace"; const EVENT_TYPE_TRANSFER: &str = "ibc_nft_transfer"; /// Contains all events variants that can be emitted from the NFT transfer application @@ -18,7 +18,7 @@ pub enum Event { Ack(AckEvent), AckStatus(AckStatusEvent), Timeout(TimeoutEvent), - ClassTrace(ClassTraceEvent), + TokenTrace(TokenTraceEvent), Transfer(TransferEvent), } @@ -144,17 +144,22 @@ impl From for ModuleEvent { } /// Event emitted in the `onRecvPacket` module callback when new tokens are minted -pub struct ClassTraceEvent { +pub struct TokenTraceEvent { pub trace_hash: Option, pub class: PrefixedClassId, + pub token: TokenId, } -impl From for ModuleEvent { - fn from(ev: ClassTraceEvent) -> Self { - let ClassTraceEvent { trace_hash, class } = ev; +impl From for ModuleEvent { + fn from(ev: TokenTraceEvent) -> Self { + let TokenTraceEvent { + trace_hash, + class, + token, + } = ev; let mut ev = Self { - kind: EVENT_TYPE_CLASS_TRACE.to_string(), - attributes: vec![("class", class).into()], + kind: EVENT_TYPE_TOKEN_TRACE.to_string(), + attributes: vec![("class", class).into(), ("token", token).into()], }; if let Some(hash) = trace_hash { ev.attributes.push(("trace_hash", hash).into()); @@ -202,7 +207,7 @@ impl From for ModuleEvent { Event::Ack(ev) => ev.into(), Event::AckStatus(ev) => ev.into(), Event::Timeout(ev) => ev.into(), - Event::ClassTrace(ev) => ev.into(), + Event::TokenTrace(ev) => ev.into(), Event::Transfer(ev) => ev.into(), } } From 3c0fddd45630500ee6ec1449088ef3e79e6fe4e3 Mon Sep 17 00:00:00 2001 From: Yuji Ito Date: Fri, 12 Jan 2024 00:12:46 +0100 Subject: [PATCH 5/8] Fix ClassData and TokenData encoding in NonFungiblePacketData (#1038) * fix encoding for ClassData and TokenData * fix Cargo.toml --- ibc-apps/ics721-nft-transfer/types/Cargo.toml | 2 ++ .../ics721-nft-transfer/types/src/packet.rs | 35 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/ibc-apps/ics721-nft-transfer/types/Cargo.toml b/ibc-apps/ics721-nft-transfer/types/Cargo.toml index f9384a426..b63d0fb14 100644 --- a/ibc-apps/ics721-nft-transfer/types/Cargo.toml +++ b/ibc-apps/ics721-nft-transfer/types/Cargo.toml @@ -20,6 +20,7 @@ all-features = true [dependencies] # external dependencies borsh = { workspace = true, optional = true } +base64 = { version = "0.21.6", default-features = false } derive_more = { workspace = true } displaydoc = { workspace = true } http = "1.0.0" @@ -44,6 +45,7 @@ default = ["std"] std = [ "serde/std", "serde_json/std", + "base64/std", "displaydoc/std", "http/std", "ibc-core/std", diff --git a/ibc-apps/ics721-nft-transfer/types/src/packet.rs b/ibc-apps/ics721-nft-transfer/types/src/packet.rs index 36ef6131b..7db6a3504 100644 --- a/ibc-apps/ics721-nft-transfer/types/src/packet.rs +++ b/ibc-apps/ics721-nft-transfer/types/src/packet.rs @@ -2,6 +2,8 @@ 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; @@ -87,14 +89,29 @@ impl TryFrom for PacketData { let class_data = if raw_pkt_data.class_data.is_empty() { None } else { - Some(raw_pkt_data.class_data.parse()?) + 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(|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, @@ -119,7 +136,7 @@ impl From for RawPacketData { .unwrap_or_default(), class_data: pkt_data .class_data - .map(|c| c.to_string()) + .map(|c| BASE64_STANDARD.encode(c.to_string())) .unwrap_or_default(), token_ids: pkt_data .token_ids @@ -128,7 +145,11 @@ impl From for RawPacketData { .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| 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(), @@ -204,11 +225,11 @@ mod tests { } fn dummy_json_packet_data() -> &'static str { - r#"{"classId":"class","classUri":"http://example.com/","classData":"{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com/","http://example.com/"],"tokenData":["{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}","{\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"},\"name\":{\"value\":\"Crypto Creatures\"}}"],"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":""}"# } fn dummy_json_packet_data_without_memo() -> &'static str { - r#"{"classId":"class","classUri":"http://example.com","classData":"{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","tokenIds":["token_0","token_1"],"tokenUris":["http://example.com","http://example.com"],"tokenData":["{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}","{\"name\":{\"value\":\"Crypto Creatures\"},\"image\":{\"value\":\"binary\",\"mime\":\"image/png\"}}"],"sender":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng","receiver":"cosmos1wxeyh7zgn4tctjzs0vtqpc6p5cxq5t2muzl7ng"}"# + 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"}"# } /// Ensures `PacketData` properly encodes to JSON by first converting to a From 1de2bd4300be2068a7b3ffdccc99ccd955c28b84 Mon Sep 17 00:00:00 2001 From: Yuji Ito Date: Mon, 22 Jan 2024 23:18:31 +0100 Subject: [PATCH 6/8] Support ClassData and TokenData not according to ICS-721 spec (#1039) * skip validation, make some data optional * check the length of token_uri and token_data * fix to set TokenData and TokenUri at once * imp: add validate_basic method for PacketData * imp: allow any format for Data + define parse_as_ics721_data method * fmt and clippy * custom serde packet data with option * add a test * restore conversions --------- Co-authored-by: Farhad Shabani --- Cargo.toml | 3 +- ibc-apps/ics721-nft-transfer/src/context.rs | 38 +++-- .../ics721-nft-transfer/src/handler/mod.rs | 28 ++-- .../src/handler/on_recv_packet.rs | 31 ++--- .../src/handler/send_transfer.rs | 72 +++++++--- ibc-apps/ics721-nft-transfer/src/module.rs | 6 +- .../ics721-nft-transfer/types/src/class.rs | 2 +- .../ics721-nft-transfer/types/src/data.rs | 89 +++++++++--- .../ics721-nft-transfer/types/src/error.rs | 4 +- ibc-apps/ics721-nft-transfer/types/src/lib.rs | 2 + .../ics721-nft-transfer/types/src/memo.rs | 2 +- .../types/src/msgs/transfer.rs | 18 ++- .../ics721-nft-transfer/types/src/packet.rs | 131 +++++++++++++----- .../ics721-nft-transfer/types/src/token.rs | 2 +- .../ibc/applications/nft_transfer/context.rs | 32 ++--- .../ibc/applications/nft_transfer/types.rs | 16 +-- 16 files changed, 322 insertions(+), 154 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..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, From 1376d5775651c9c7fa07cefc33c70aee94fbdb4e Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Tue, 23 Jan 2024 07:29:06 -0800 Subject: [PATCH 7/8] chore: add unclog --- .../unreleased/features/346-implement-ics721-nft-transfer.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/346-implement-ics721-nft-transfer.md diff --git a/.changelog/unreleased/features/346-implement-ics721-nft-transfer.md b/.changelog/unreleased/features/346-implement-ics721-nft-transfer.md new file mode 100644 index 000000000..0a68909ff --- /dev/null +++ b/.changelog/unreleased/features/346-implement-ics721-nft-transfer.md @@ -0,0 +1,2 @@ +- [ibc-app-nft-transfer] Implement ICS-721 NFT transfer application + ([\#346](https://github.com/cosmos/ibc-rs/issues/346)) From 9333f775d3670c1f1922eb9b110015db4b7d5330 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Tue, 23 Jan 2024 17:29:29 -0800 Subject: [PATCH 8/8] nit: fix docstrings --- ibc-apps/ics20-transfer/types/src/msgs/transfer.rs | 4 ++-- ibc-apps/ics721-nft-transfer/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs b/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs index dcaaa521f..06bd48027 100644 --- a/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs +++ b/ibc-apps/ics20-transfer/types/src/msgs/transfer.rs @@ -1,4 +1,4 @@ -//! Defines the Non-Fungible Token Transfer message type +//! Defines the token transfer message type use ibc_core::channel::types::error::PacketError; use ibc_core::channel::types::timeout::TimeoutHeight; @@ -15,7 +15,7 @@ use crate::packet::PacketData; pub(crate) const TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; -/// Message used to build an ICS-721 Non-Fungible Token Transfer packet. +/// Message used to build an ICS20 token transfer packet. /// /// Note that this message is not a packet yet, as it lacks the proper sequence /// number, and destination port/channel. This is by design. The sender of the diff --git a/ibc-apps/ics721-nft-transfer/Cargo.toml b/ibc-apps/ics721-nft-transfer/Cargo.toml index 02cad3270..aaba18862 100644 --- a/ibc-apps/ics721-nft-transfer/Cargo.toml +++ b/ibc-apps/ics721-nft-transfer/Cargo.toml @@ -50,4 +50,4 @@ borsh = [ parity-scale-codec = [ "ibc-app-nft-transfer-types/parity-scale-codec", "ibc-core/parity-scale-codec", -] \ No newline at end of file +]