From 8e8a6ef02965fa458367c67e64b07382f8e9dfb0 Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Fri, 7 Feb 2025 12:28:05 +0100 Subject: [PATCH] feat: Improve TransferV2 API --- contracts/ibc-callbacks/src/contract.rs | 4 +- packages/go-gen/tests/cosmwasm_std__IbcMsg.go | 34 +- packages/std/src/ibc.rs | 46 ++- .../std/src/ibc/transfer_msg_builder_v2.rs | 340 +++++++++++------- 4 files changed, 264 insertions(+), 160 deletions(-) diff --git a/contracts/ibc-callbacks/src/contract.rs b/contracts/ibc-callbacks/src/contract.rs index 545424d64..78ebeb3e2 100644 --- a/contracts/ibc-callbacks/src/contract.rs +++ b/contracts/ibc-callbacks/src/contract.rs @@ -93,9 +93,11 @@ fn execute_transfer( } ChannelVersion::V2 => { let builder = TransferMsgBuilderV2::new( - channel_id, to_address.clone(), info.funds.into_iter().map(Into::into).collect(), + ) + .with_direct_transfer( + channel_id, IbcTimeout::with_timestamp(env.block.time.plus_seconds(timeout_seconds as u64)), ); match callback_type { diff --git a/packages/go-gen/tests/cosmwasm_std__IbcMsg.go b/packages/go-gen/tests/cosmwasm_std__IbcMsg.go index 3c4a1eaea..740e7f971 100644 --- a/packages/go-gen/tests/cosmwasm_std__IbcMsg.go +++ b/packages/go-gen/tests/cosmwasm_std__IbcMsg.go @@ -15,19 +15,16 @@ type TransferMsg struct { ToAddress string `json:"to_address"` } type TransferV2Msg struct { - // Existing channel to send the tokens over. - ChannelID string `json:"channel_id"` - Forwarding *Forwarding `json:"forwarding,omitempty"` // An optional memo. See the blog post ["Moving Beyond Simple Token Transfers"](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b) for more information. // // There is no difference between setting this to `None` or an empty string. Memo string `json:"memo,omitempty"` - // when packet times out, measured on remote chain. - Timeout IBCTimeout `json:"timeout"` // Address on the remote chain to receive these tokens. ToAddress string `json:"to_address"` // MsgTransfer in v2 version supports multiple coins. Tokens Array[Coin] `json:"tokens"` + // The transfer can be of type: * Direct, * Forwarding, * Forwarding with unwind flag set. + TransferType TransferV2Type `json:"transfer_type"` } type SendPacketMsg struct { ChannelID string `json:"channel_id"` @@ -102,10 +99,6 @@ type Coin struct { Amount string `json:"amount"` Denom string `json:"denom"` } -type Forwarding struct { - Hops Array[Hop] `json:"hops"` - Unwind bool `json:"unwind"` -} type Hop struct { ChannelID string `json:"channel_id"` PortID string `json:"port_id"` @@ -131,4 +124,27 @@ type IBCTimeoutBlock struct { Height uint64 `json:"height"` // the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0) Revision uint64 `json:"revision"` +} +type DirectType struct { + // Existing channel to send the tokens over. + ChannelID string `json:"channel_id"` + // When packet times out, measured on remote chain. + IBCTimeout IBCTimeout `json:"ibc_timeout"` +} +type MultiHopType struct { + // Existing channel to send the tokens over. + ChannelID string `json:"channel_id"` + Hops Array[Hop] `json:"hops"` + // When packet times out, measured on remote chain. TimestampHeight is not supported in ibc-go Transfer V2. + Timeout Uint64 `json:"timeout"` +} +type UnwindingType struct { + Hops Array[Hop] `json:"hops"` + // When packet times out, measured on remote chain. TimestampHeight is not supported in ibc-go Transfer V2. + Timeout Uint64 `json:"timeout"` +} +type TransferV2Type struct { + Direct *DirectType `json:"direct,omitempty"` + MultiHop *MultiHopType `json:"multi_hop,omitempty"` + Unwinding *UnwindingType `json:"unwinding,omitempty"` } \ No newline at end of file diff --git a/packages/std/src/ibc.rs b/packages/std/src/ibc.rs index 0db6ecb6d..4d3754592 100644 --- a/packages/std/src/ibc.rs +++ b/packages/std/src/ibc.rs @@ -31,9 +31,35 @@ pub struct Hop { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct Forwarding { - pub unwind: bool, - pub hops: Vec, +pub enum TransferV2Type { + Direct { + /// Existing channel to send the tokens over. + channel_id: String, + /// When packet times out, measured on remote chain. + ibc_timeout: IbcTimeout, + }, + MultiHop { + /// Existing channel to send the tokens over. + channel_id: String, + // A struct containing the list of next hops, + // determining where the tokens must be forwarded next. + // More info can be found in the `MsgTransfer` IBC docs: + // https://ibc.cosmos.network/main/apps/transfer/messages/ + hops: Vec, + /// When packet times out, measured on remote chain. + /// TimestampHeight is not supported in ibc-go Transfer V2. + timeout: Timestamp, + }, + Unwinding { + // A struct containing the list of next hops, + // determining where the tokens must be forwarded next. + // More info can be found in the `MsgTransfer` IBC docs: + // https://ibc.cosmos.network/main/apps/transfer/messages/ + hops: Vec, + /// When packet times out, measured on remote chain. + /// TimestampHeight is not supported in ibc-go Transfer V2. + timeout: Timestamp, + }, } /// These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts @@ -77,25 +103,21 @@ pub enum IbcMsg { /// module to. #[cfg(feature = "cosmwasm_3_0")] TransferV2 { - /// Existing channel to send the tokens over. - channel_id: String, + /// The transfer can be of type: + /// * Direct, + /// * Forwarding, + /// * Forwarding with unwind flag set. + transfer_type: TransferV2Type, /// Address on the remote chain to receive these tokens. to_address: String, /// MsgTransfer in v2 version supports multiple coins. tokens: Vec, - /// when packet times out, measured on remote chain. - timeout: IbcTimeout, /// An optional memo. See the blog post /// ["Moving Beyond Simple Token Transfers"](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b) /// for more information. /// /// There is no difference between setting this to `None` or an empty string. memo: Option, - // A struct containing the list of next hops, - // determining where the tokens must be forwarded next. - // More info can be found in the `MsgTransfer` IBC docs: - // https://ibc.cosmos.network/main/apps/transfer/messages/ - forwarding: Option, }, /// Sends an IBC packet with given data over the existing channel. /// Data should be encoded in a format defined by the channel version, diff --git a/packages/std/src/ibc/transfer_msg_builder_v2.rs b/packages/std/src/ibc/transfer_msg_builder_v2.rs index 1c608b31d..4e89d5fb7 100644 --- a/packages/std/src/ibc/transfer_msg_builder_v2.rs +++ b/packages/std/src/ibc/transfer_msg_builder_v2.rs @@ -1,92 +1,136 @@ -use crate::{Coin, IbcDstCallback, IbcMsg, IbcSrcCallback, IbcTimeout}; +use crate::{Coin, IbcDstCallback, IbcMsg, IbcSrcCallback, Timestamp}; use super::{ - EmptyMemo, Forwarding, Hop, MemoSource, WithCallbacks, WithDstCallback, WithMemo, - WithSrcCallback, + EmptyMemo, Hop, IbcTimeout, MemoSource, TransferV2Type, WithCallbacks, WithDstCallback, + WithMemo, WithSrcCallback, }; -impl>> TransferMsgBuilderV2 { +impl> TransferMsgBuilderV2 { pub fn build(self) -> IbcMsg { IbcMsg::TransferV2 { - channel_id: self.channel_id, + transfer_type: self.transfer_type.into(), to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, memo: self.memo.into_memo(), - forwarding: self.forwarding.into(), } } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct TransferMsgBuilderV2 { - channel_id: String, +pub struct TransferMsgBuilderV2 { + transfer_type: TransferType, to_address: String, tokens: Vec, - timeout: IbcTimeout, memo: MemoData, - forwarding: ForwardingData, } #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] -pub struct WithoutForwarding; +pub struct EmptyTransferType; #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] -pub struct WithForwarding { - pub(crate) unwind: bool, - pub(crate) hops: Vec, +pub struct Direct { + transfer_type: TransferV2Type, } -impl From for Option { - fn from(_val: WithoutForwarding) -> Self { - None +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct Forwarding { + transfer_type: TransferV2Type, +} + +impl From for TransferV2Type { + fn from(val: Direct) -> Self { + val.transfer_type + } +} + +impl From for TransferV2Type { + fn from(val: Forwarding) -> Self { + val.transfer_type + } +} + +pub trait ForwardingPossible {} +impl ForwardingPossible for WithMemo {} +impl ForwardingPossible for EmptyMemo {} +impl ForwardingPossible for WithDstCallback {} + +pub trait SrcCallbackPossible {} +impl SrcCallbackPossible for Direct {} +impl SrcCallbackPossible for EmptyTransferType {} + +pub trait AddDstCallbackPossible { + type CallbackType; + fn add_dst_callback(self, dst_callback: IbcDstCallback) -> Self::CallbackType; +} +impl AddDstCallbackPossible for WithSrcCallback { + type CallbackType = WithCallbacks; + fn add_dst_callback(self, dst_callback: IbcDstCallback) -> Self::CallbackType { + WithCallbacks { + src_callback: self.src_callback, + dst_callback, + } + } +} +impl AddDstCallbackPossible for EmptyMemo { + type CallbackType = WithDstCallback; + fn add_dst_callback(self, dst_callback: IbcDstCallback) -> Self::CallbackType { + WithDstCallback { dst_callback } } } -impl From for Option { - fn from(val: WithForwarding) -> Self { - Some(Forwarding { - unwind: val.unwind, - hops: val.hops, - }) +pub trait AddSrcCallbackPossible { + type CallbackType; + fn add_src_callback(self, dst_callback: IbcSrcCallback) -> Self::CallbackType; +} +impl AddSrcCallbackPossible for WithDstCallback { + type CallbackType = WithCallbacks; + fn add_src_callback(self, src_callback: IbcSrcCallback) -> Self::CallbackType { + WithCallbacks { + dst_callback: self.dst_callback, + src_callback, + } + } +} +impl AddSrcCallbackPossible for EmptyMemo { + type CallbackType = WithSrcCallback; + fn add_src_callback(self, src_callback: IbcSrcCallback) -> Self::CallbackType { + WithSrcCallback { src_callback } } } -impl TransferMsgBuilderV2 { +impl TransferMsgBuilderV2 { /// Creates a new transfer message with the given parameters and no memo. - pub fn new( - channel_id: impl Into, - to_address: impl Into, - tokens: Vec, - timeout: impl Into, - ) -> Self { + pub fn new(to_address: impl Into, tokens: Vec) -> Self { Self { - channel_id: channel_id.into(), + transfer_type: EmptyTransferType {}, to_address: to_address.into(), tokens, - timeout: timeout.into(), memo: EmptyMemo, - forwarding: WithoutForwarding, } } +} +impl TransferMsgBuilderV2 { /// Adds a memo text to the transfer message. pub fn with_memo( self, memo: impl Into, - ) -> TransferMsgBuilderV2 { + ) -> TransferMsgBuilderV2 { TransferMsgBuilderV2 { - channel_id: self.channel_id, + transfer_type: self.transfer_type, to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, memo: WithMemo { memo: memo.into() }, - forwarding: self.forwarding, } } +} +impl + TransferMsgBuilderV2 +{ /// Adds an IBC source callback entry to the memo field. /// Use this if you want to receive IBC callbacks on the source chain. /// @@ -94,17 +138,17 @@ impl TransferMsgBuilderV2 { pub fn with_src_callback( self, src_callback: IbcSrcCallback, - ) -> TransferMsgBuilderV2 { + ) -> TransferMsgBuilderV2 { TransferMsgBuilderV2 { - channel_id: self.channel_id, + transfer_type: self.transfer_type, to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, - memo: WithSrcCallback { src_callback }, - forwarding: self.forwarding, + memo: self.memo.add_src_callback(src_callback), } } +} +impl TransferMsgBuilderV2 { /// Adds an IBC destination callback entry to the memo field. /// Use this if you want to receive IBC callbacks on the destination chain. /// @@ -112,79 +156,60 @@ impl TransferMsgBuilderV2 { pub fn with_dst_callback( self, dst_callback: IbcDstCallback, - ) -> TransferMsgBuilderV2 { + ) -> TransferMsgBuilderV2 { TransferMsgBuilderV2 { - channel_id: self.channel_id, + transfer_type: self.transfer_type, to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, - memo: WithDstCallback { dst_callback }, - forwarding: self.forwarding, + memo: self.memo.add_dst_callback(dst_callback), } } +} +impl TransferMsgBuilderV2 { /// Adds forwarding data. /// It is worth to notice that the builder does not allow to add forwarding data along with /// source callback. It is discouraged in the IBC docs: /// https://ibc.cosmos.network/v9/middleware/callbacks/overview/#known-limitations - pub fn with_forwarding( + pub fn with_direct_transfer( self, - hops: Vec, - unwind: bool, - ) -> TransferMsgBuilderV2 { + channel_id: String, + ibc_timeout: IbcTimeout, + ) -> TransferMsgBuilderV2 { TransferMsgBuilderV2 { - channel_id: self.channel_id, + transfer_type: Direct { + transfer_type: TransferV2Type::Direct { + channel_id, + ibc_timeout, + }, + }, to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, memo: self.memo, - forwarding: WithForwarding { unwind, hops }, } } -} -impl TransferMsgBuilderV2 { - /// Adds an IBC destination callback entry to the memo field. - /// Use this if you want to receive IBC callbacks on the destination chain. - /// - /// For more info check out [`crate::IbcDestinationCallbackMsg`]. - pub fn with_dst_callback( + /// Adds forwarding data. + /// It is worth to notice that the builder does not allow to add forwarding data along with + /// source callback. It is discouraged in the IBC docs: + /// https://ibc.cosmos.network/v9/middleware/callbacks/overview/#known-limitations + pub fn with_forwarding( self, - dst_callback: IbcDstCallback, - ) -> TransferMsgBuilderV2 { + channel_id: String, + hops: Vec, + timeout: Timestamp, + ) -> TransferMsgBuilderV2 { TransferMsgBuilderV2 { - channel_id: self.channel_id, - to_address: self.to_address, - tokens: self.tokens, - timeout: self.timeout, - memo: WithCallbacks { - src_callback: self.memo.src_callback, - dst_callback, + transfer_type: Forwarding { + transfer_type: TransferV2Type::MultiHop { + channel_id, + hops, + timeout, + }, }, - forwarding: self.forwarding, - } - } -} - -impl TransferMsgBuilderV2 { - /// Adds an IBC source callback entry to the memo field. - /// Use this if you want to receive IBC callbacks on the source chain. - /// - /// For more info check out [`crate::IbcSourceCallbackMsg`]. - pub fn with_src_callback( - self, - src_callback: IbcSrcCallback, - ) -> TransferMsgBuilderV2 { - TransferMsgBuilderV2 { - channel_id: self.channel_id, to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, - memo: WithCallbacks { - src_callback, - dst_callback: self.memo.dst_callback, - }, - forwarding: self.forwarding, + memo: self.memo, } } @@ -192,18 +217,18 @@ impl TransferMsgBuilderV2 { /// It is worth to notice that the builder does not allow to add forwarding data along with /// source callback. It is discouraged in the IBC docs: /// https://ibc.cosmos.network/v9/middleware/callbacks/overview/#known-limitations - pub fn with_forwarding( + pub fn with_forwarding_unwinding( self, hops: Vec, - unwind: bool, - ) -> TransferMsgBuilderV2 { + timeout: Timestamp, + ) -> TransferMsgBuilderV2 { TransferMsgBuilderV2 { - channel_id: self.channel_id, + transfer_type: Forwarding { + transfer_type: TransferV2Type::Unwinding { hops, timeout }, + }, to_address: self.to_address, tokens: self.tokens, - timeout: self.timeout, memo: self.memo, - forwarding: WithForwarding { hops, unwind }, } } } @@ -225,30 +250,42 @@ mod tests { gas_limit: None, }; - let empty_memo_builder = TransferMsgBuilderV2::new( - "channel-0", - "cosmos1example", - vec![coin(10, "ucoin")], + let empty_builder = TransferMsgBuilderV2::new("cosmos1example", vec![coin(10, "ucoin")]); + + let direct_builder = empty_builder.clone().with_direct_transfer( + "channel-0".to_owned(), + IbcTimeout::with_timestamp(Timestamp::from_seconds(12345)), + ); + let direct = direct_builder.clone().build(); + + let forwarding_builder = empty_builder.clone().with_forwarding( + "channel-0".to_owned(), + vec![Hop { + port_id: "port-id".to_owned(), + channel_id: "channel-id".to_owned(), + }], Timestamp::from_seconds(12345), ); + let forwarding = forwarding_builder.clone().build(); - let forwarding = empty_memo_builder.clone().with_forwarding( + let unwinding_builder = empty_builder.clone().with_forwarding_unwinding( vec![Hop { - port_id: "portid".to_owned(), - channel_id: "chnid".to_owned(), + port_id: "port-id".to_owned(), + channel_id: "channel-id".to_owned(), }], - false, + Timestamp::from_seconds(12345), ); - let forwarding = forwarding.build(); + let unwinding = unwinding_builder + .with_dst_callback(dst_callback.clone()) + .build(); - let empty = empty_memo_builder.clone().build(); - let with_memo = empty_memo_builder.clone().with_memo("memo").build(); + let with_memo = forwarding_builder.with_memo("memo").build(); - let with_src_callback_builder = empty_memo_builder + let with_src_callback_builder = direct_builder .clone() .with_src_callback(src_callback.clone()); let with_src_callback = with_src_callback_builder.clone().build(); - let with_dst_callback_builder = empty_memo_builder + let with_dst_callback_builder = direct_builder .clone() .with_dst_callback(dst_callback.clone()); let with_dst_callback = with_dst_callback_builder.clone().build(); @@ -263,83 +300,110 @@ mod tests { // assert all the different messages assert_eq!( - empty, + direct, IbcMsg::TransferV2 { - channel_id: "channel-0".to_string(), + transfer_type: TransferV2Type::Direct { + channel_id: "channel-0".to_string(), + ibc_timeout: Timestamp::from_seconds(12345).into() + }, to_address: "cosmos1example".to_string(), tokens: vec![coin(10, "ucoin")], - timeout: Timestamp::from_seconds(12345).into(), memo: None, - forwarding: None, } ); assert_eq!( forwarding, IbcMsg::TransferV2 { - channel_id: "channel-0".to_string(), + transfer_type: TransferV2Type::MultiHop { + channel_id: "channel-0".to_string(), + hops: vec![Hop { + port_id: "port-id".to_owned(), + channel_id: "channel-id".to_owned() + }], + timeout: Timestamp::from_seconds(12345).into() + }, to_address: "cosmos1example".to_string(), tokens: vec![coin(10, "ucoin")], - timeout: Timestamp::from_seconds(12345).into(), memo: None, - forwarding: Some(Forwarding { - hops: vec![Hop { - port_id: "portid".to_owned(), - channel_id: "chnid".to_owned() - }], - unwind: false - }), } ); assert_eq!( with_memo, IbcMsg::TransferV2 { - channel_id: "channel-0".to_string(), + transfer_type: TransferV2Type::MultiHop { + channel_id: "channel-0".to_string(), + hops: vec![Hop { + port_id: "port-id".to_owned(), + channel_id: "channel-id".to_owned() + }], + timeout: Timestamp::from_seconds(12345).into() + }, to_address: "cosmos1example".to_string(), tokens: vec![coin(10, "ucoin")], - timeout: Timestamp::from_seconds(12345).into(), memo: Some("memo".to_string()), - forwarding: None } ); assert_eq!( with_src_callback, IbcMsg::TransferV2 { - channel_id: "channel-0".to_string(), + transfer_type: TransferV2Type::Direct { + channel_id: "channel-0".to_string(), + ibc_timeout: Timestamp::from_seconds(12345).into() + }, to_address: "cosmos1example".to_string(), tokens: vec![coin(10, "ucoin")], - timeout: Timestamp::from_seconds(12345).into(), memo: Some( to_json_string(&IbcCallbackRequest::source(src_callback.clone())).unwrap() ), - forwarding: None } ); assert_eq!( with_dst_callback, IbcMsg::TransferV2 { - channel_id: "channel-0".to_string(), + transfer_type: TransferV2Type::Direct { + channel_id: "channel-0".to_string(), + ibc_timeout: Timestamp::from_seconds(12345).into() + }, to_address: "cosmos1example".to_string(), tokens: vec![coin(10, "ucoin")], - timeout: Timestamp::from_seconds(12345).into(), memo: Some( to_json_string(&IbcCallbackRequest::destination(dst_callback.clone())).unwrap() ), - forwarding: None } ); assert_eq!( with_both_callbacks1, IbcMsg::TransferV2 { - channel_id: "channel-0".to_string(), + transfer_type: TransferV2Type::Direct { + channel_id: "channel-0".to_string(), + ibc_timeout: Timestamp::from_seconds(12345).into() + }, to_address: "cosmos1example".to_string(), tokens: vec![coin(10, "ucoin")], - timeout: Timestamp::from_seconds(12345).into(), memo: Some( - to_json_string(&IbcCallbackRequest::both(src_callback, dst_callback)).unwrap() + to_json_string(&IbcCallbackRequest::both( + src_callback, + dst_callback.clone() + )) + .unwrap() ), - forwarding: None, } ); assert_eq!(with_both_callbacks1, with_both_callbacks2); + assert_eq!( + unwinding, + IbcMsg::TransferV2 { + transfer_type: TransferV2Type::Unwinding { + hops: vec![Hop { + port_id: "port-id".to_owned(), + channel_id: "channel-id".to_owned() + }], + timeout: Timestamp::from_seconds(12345) + }, + to_address: "cosmos1example".to_string(), + tokens: vec![coin(10, "ucoin")], + memo: Some(to_json_string(&IbcCallbackRequest::destination(dst_callback)).unwrap()), + } + ); } }