diff --git a/.circleci/config.yml b/.circleci/config.yml index b1cb18cd7b..e1144eec9a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -726,7 +726,7 @@ jobs: keys: - cargocache-v2-contract_ibc_callbacks-rust:1.74-{{ checksum "Cargo.lock" }} - check_contract: - min_version: "2.1" + min_version: "3.0" - save_cache: paths: - /usr/local/cargo/registry @@ -1120,7 +1120,7 @@ jobs: name: Clippy linting on std (all feature flags) working_directory: ~/project/packages/std # change to --all-features once `abort` is removed - command: cargo clippy --all-targets --tests --features staking,stargate,cosmwasm_2_2 -- -D warnings + command: cargo clippy --all-targets --tests --features staking,stargate,cosmwasm_3_0 -- -D warnings - run: name: Clippy linting on vm (no feature flags) working_directory: ~/project/packages/vm diff --git a/contracts/ibc-callbacks/Cargo.toml b/contracts/ibc-callbacks/Cargo.toml index d839475e2c..989f1ee432 100644 --- a/contracts/ibc-callbacks/Cargo.toml +++ b/contracts/ibc-callbacks/Cargo.toml @@ -27,6 +27,7 @@ cosmwasm-schema = { path = "../../packages/schema" } cosmwasm-std = { path = "../../packages/std", features = [ "iterator", "stargate", + "cosmwasm_3_0", ] } schemars = "0.8.3" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/ibc-callbacks/schema/ibc-callbacks.json b/contracts/ibc-callbacks/schema/ibc-callbacks.json index 6b34f4afc8..0e843e8d31 100644 --- a/contracts/ibc-callbacks/schema/ibc-callbacks.json +++ b/contracts/ibc-callbacks/schema/ibc-callbacks.json @@ -23,6 +23,7 @@ "type": "object", "required": [ "channel_id", + "channel_version", "timeout_seconds", "to_address" ], @@ -40,6 +41,14 @@ "description": "The channel to send the packet through", "type": "string" }, + "channel_version": { + "description": "IBC channel version", + "allOf": [ + { + "$ref": "#/definitions/ChannelVersion" + } + ] + }, "timeout_seconds": { "description": "The amount of seconds from now the transfer should timeout at", "type": "integer", @@ -82,6 +91,13 @@ ] } ] + }, + "ChannelVersion": { + "type": "string", + "enum": [ + "v1", + "v2" + ] } } }, diff --git a/contracts/ibc-callbacks/schema/raw/execute.json b/contracts/ibc-callbacks/schema/raw/execute.json index e5bed95bb4..a8272cc420 100644 --- a/contracts/ibc-callbacks/schema/raw/execute.json +++ b/contracts/ibc-callbacks/schema/raw/execute.json @@ -12,6 +12,7 @@ "type": "object", "required": [ "channel_id", + "channel_version", "timeout_seconds", "to_address" ], @@ -29,6 +30,14 @@ "description": "The channel to send the packet through", "type": "string" }, + "channel_version": { + "description": "IBC channel version", + "allOf": [ + { + "$ref": "#/definitions/ChannelVersion" + } + ] + }, "timeout_seconds": { "description": "The amount of seconds from now the transfer should timeout at", "type": "integer", @@ -71,6 +80,13 @@ ] } ] + }, + "ChannelVersion": { + "type": "string", + "enum": [ + "v1", + "v2" + ] } } } diff --git a/contracts/ibc-callbacks/src/contract.rs b/contracts/ibc-callbacks/src/contract.rs index cd2f269f26..78ebeb3e29 100644 --- a/contracts/ibc-callbacks/src/contract.rs +++ b/contracts/ibc-callbacks/src/contract.rs @@ -1,10 +1,10 @@ use cosmwasm_std::{ entry_point, to_json_binary, Binary, Deps, DepsMut, Empty, Env, IbcBasicResponse, IbcDestinationCallbackMsg, IbcDstCallback, IbcSourceCallbackMsg, IbcSrcCallback, IbcTimeout, - MessageInfo, Response, StdError, StdResult, TransferMsgBuilder, + MessageInfo, Response, StdError, StdResult, TransferMsgBuilder, TransferMsgBuilderV2, }; -use crate::msg::{CallbackType, ExecuteMsg, QueryMsg}; +use crate::msg::{CallbackType, ChannelVersion, ExecuteMsg, QueryMsg}; use crate::state::{load_stats, save_stats, CallbackStats}; #[entry_point] @@ -35,6 +35,7 @@ pub fn execute( channel_id, timeout_seconds, callback_type, + channel_version, } => execute_transfer( env, info, @@ -42,6 +43,7 @@ pub fn execute( channel_id, timeout_seconds, callback_type, + channel_version, ), } } @@ -53,6 +55,7 @@ fn execute_transfer( channel_id: String, timeout_seconds: u32, callback_type: CallbackType, + channel_version: ChannelVersion, ) -> StdResult { let src_callback = IbcSrcCallback { address: env.contract.address, @@ -62,28 +65,50 @@ fn execute_transfer( address: to_address.clone(), gas_limit: None, }; - let coin = match &*info.funds { - [coin] if !coin.amount.is_zero() => coin, - _ => { - return Err(StdError::generic_err( - "Must send exactly one denom to trigger ics-20 transfer", - )) - } - }; - let builder = TransferMsgBuilder::new( - channel_id, - to_address.clone(), - coin.clone(), - IbcTimeout::with_timestamp(env.block.time.plus_seconds(timeout_seconds as u64)), - ); - let transfer_msg = match callback_type { - CallbackType::Both => builder - .with_src_callback(src_callback) - .with_dst_callback(dst_callback) - .build(), - CallbackType::Src => builder.with_src_callback(src_callback).build(), - CallbackType::Dst => builder.with_dst_callback(dst_callback).build(), + let transfer_msg = match channel_version { + ChannelVersion::V1 => { + let coin = match &*info.funds { + [coin] if !coin.amount.is_zero() => coin, + _ => { + return Err(StdError::generic_err( + "Must send exactly one denom to trigger ics-20 transfer", + )) + } + }; + let builder = TransferMsgBuilder::new( + channel_id, + to_address.clone(), + coin.clone(), + IbcTimeout::with_timestamp(env.block.time.plus_seconds(timeout_seconds as u64)), + ); + match callback_type { + CallbackType::Both => builder + .with_src_callback(src_callback) + .with_dst_callback(dst_callback) + .build(), + CallbackType::Src => builder.with_src_callback(src_callback).build(), + CallbackType::Dst => builder.with_dst_callback(dst_callback).build(), + } + } + ChannelVersion::V2 => { + let builder = TransferMsgBuilderV2::new( + 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 { + CallbackType::Both => builder + .with_src_callback(src_callback) + .with_dst_callback(dst_callback) + .build(), + CallbackType::Src => builder.with_src_callback(src_callback).build(), + CallbackType::Dst => builder.with_dst_callback(dst_callback).build(), + } + } }; Ok(Response::new() diff --git a/contracts/ibc-callbacks/src/msg.rs b/contracts/ibc-callbacks/src/msg.rs index d5cdb38f96..5714d3f853 100644 --- a/contracts/ibc-callbacks/src/msg.rs +++ b/contracts/ibc-callbacks/src/msg.rs @@ -8,6 +8,12 @@ pub enum QueryMsg { CallbackStats {}, } +#[cw_serde] +pub enum ChannelVersion { + V1, + V2, +} + #[cw_serde] pub enum ExecuteMsg { Transfer { @@ -20,6 +26,8 @@ pub enum ExecuteMsg { /// Who should receive callbacks for the message #[serde(default)] callback_type: CallbackType, + /// IBC channel version + channel_version: ChannelVersion, }, } diff --git a/packages/check/src/main.rs b/packages/check/src/main.rs index e211a405b5..c49c351417 100644 --- a/packages/check/src/main.rs +++ b/packages/check/src/main.rs @@ -14,7 +14,7 @@ use cosmwasm_vm::internals::{check_wasm, compile, make_compiling_engine, LogOutp use cosmwasm_vm::{capabilities_from_csv, WasmLimits}; const DEFAULT_AVAILABLE_CAPABILITIES: &str = - "iterator,staking,stargate,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2"; + "iterator,staking,stargate,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2,cosmwasm_3_0"; pub fn main() { let matches = Command::new("Contract checking") diff --git a/packages/go-gen/Cargo.toml b/packages/go-gen/Cargo.toml index 8f24c3d8a6..93731e2d58 100644 --- a/packages/go-gen/Cargo.toml +++ b/packages/go-gen/Cargo.toml @@ -10,7 +10,7 @@ release = false [dependencies] cosmwasm-std = { version = "2.2.0-rc.1", path = "../std", features = [ - "cosmwasm_2_2", + "cosmwasm_3_0", "staking", "stargate", ] } diff --git a/packages/go-gen/src/schema.rs b/packages/go-gen/src/schema.rs index 71955dd63d..9b41461d1b 100644 --- a/packages/go-gen/src/schema.rs +++ b/packages/go-gen/src/schema.rs @@ -286,6 +286,7 @@ pub fn custom_type_of(ty: &str) -> Option<&str> { match ty { "Uint64" => Some("Uint64"), "Uint128" => Some("string"), + "Uint256" => Some("string"), "Int64" => Some("Int64"), "Int128" => Some("string"), "Binary" => Some("[]byte"), diff --git a/packages/go-gen/tests/cosmwasm_std__IbcMsg.go b/packages/go-gen/tests/cosmwasm_std__IbcMsg.go index 992b258393..740e7f9711 100644 --- a/packages/go-gen/tests/cosmwasm_std__IbcMsg.go +++ b/packages/go-gen/tests/cosmwasm_std__IbcMsg.go @@ -1,14 +1,36 @@ type TransferMsg struct { - Amount Coin `json:"amount"` - ChannelID string `json:"channel_id"` - Memo string `json:"memo,omitempty"` // this is not yet in wasmvm, but will be soon - Timeout IBCTimeout `json:"timeout"` - ToAddress string `json:"to_address"` + // packet data only supports one coin https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20 + Amount Coin `json:"amount"` + // existing channel to send the tokens over + ChannelID string `json:"channel_id"` + // 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. + // + // This field is only supported on chains with CosmWasm >= 2.0 and silently ignored on older chains. If you need support for both 1.x and 2.x chain with the same codebase, it is recommended to use `CosmosMsg::Stargate` with a custom MsgTransfer protobuf encoder instead. + 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"` +} +type TransferV2Msg struct { + // 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"` + // 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"` - Data []byte `json:"data"` - Timeout IBCTimeout `json:"timeout"` + ChannelID string `json:"channel_id"` + Data []byte `json:"data"` + // when packet times out, measured on remote chain + Timeout IBCTimeout `json:"timeout"` } type WriteAcknowledgementMsg struct { // The acknowledgement to send back @@ -27,7 +49,9 @@ type PayPacketFeeMsg struct { Fee IBCFee `json:"fee"` // The port id on the chain where the packet is sent from (this chain). PortID string `json:"port_id"` - // Allowlist of relayer addresses that can receive the fee. This is currently not implemented and *must* be empty. + // Allowlist of relayer addresses that can receive the fee. An empty list means that any relayer can receive the fee. + // + // This is currently not implemented and *must* be empty. Relayers Array[string] `json:"relayers"` } type PayPacketFeeAsyncMsg struct { @@ -36,27 +60,49 @@ type PayPacketFeeAsyncMsg struct { Fee IBCFee `json:"fee"` // The port id on the chain where the packet is sent from (this chain). PortID string `json:"port_id"` - // Allowlist of relayer addresses that can receive the fee. This is currently not implemented and *must* be empty. + // Allowlist of relayer addresses that can receive the fee. An empty list means that any relayer can receive the fee. + // + // This is currently not implemented and *must* be empty. Relayers Array[string] `json:"relayers"` // The sequence number of the packet that should be incentivized. Sequence uint64 `json:"sequence"` } +// These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points) type IBCMsg struct { - Transfer *TransferMsg `json:"transfer,omitempty"` - SendPacket *SendPacketMsg `json:"send_packet,omitempty"` + // Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to. + Transfer *TransferMsg `json:"transfer,omitempty"` + // Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to. + TransferV2 *TransferV2Msg `json:"transfer_v2,omitempty"` + // Sends an IBC packet with given data over the existing channel. Data should be encoded in a format defined by the channel version, and the module on the other side should know how to parse this. + SendPacket *SendPacketMsg `json:"send_packet,omitempty"` + // Acknowledges a packet that this contract received over IBC. This allows acknowledging a packet that was not acknowledged yet in the `ibc_packet_receive` call. WriteAcknowledgement *WriteAcknowledgementMsg `json:"write_acknowledgement,omitempty"` - CloseChannel *CloseChannelMsg `json:"close_channel,omitempty"` - PayPacketFee *PayPacketFeeMsg `json:"pay_packet_fee,omitempty"` - PayPacketFeeAsync *PayPacketFeeAsyncMsg `json:"pay_packet_fee_async,omitempty"` + // This will close an existing channel that is owned by this contract. Port is auto-assigned to the contract's IBC port + CloseChannel *CloseChannelMsg `json:"close_channel,omitempty"` + // Incentivizes the next IBC packet sent after this message with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled. + // + // # Example + // + // Most commonly, you will attach this message to a response right before sending a packet using [`IbcMsg::SendPacket`] or [`IbcMsg::Transfer`]. + // + // ```rust # use cosmwasm_std::{IbcMsg, IbcEndpoint, IbcFee, IbcTimeout, Coin, coins, CosmosMsg, Response, Timestamp}; + // + // let incentivize = IbcMsg::PayPacketFee { port_id: "transfer".to_string(), channel_id: "source-channel".to_string(), fee: IbcFee { receive_fee: coins(100, "token"), ack_fee: coins(201, "token"), timeout_fee: coins(200, "token"), }, relayers: vec![], }; let transfer = IbcMsg::Transfer { channel_id: "source-channel".to_string(), to_address: "receiver".to_string(), amount: Coin::new(100u32, "token"), timeout: IbcTimeout::with_timestamp(Timestamp::from_nanos(0)), memo: None, }; + // + // # #[cfg(feature = "stargate")] let _: Response = Response::new() .add_message(CosmosMsg::Ibc(incentivize)) .add_message(CosmosMsg::Ibc(transfer)); ``` + PayPacketFee *PayPacketFeeMsg `json:"pay_packet_fee,omitempty"` + // Incentivizes the existing IBC packet with the given port, channel and sequence with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled. They are added to the existing fees on the packet. + PayPacketFeeAsync *PayPacketFeeAsyncMsg `json:"pay_packet_fee_async,omitempty"` } - -// Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) type Coin struct { - Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" - Denom string `json:"denom"` // type, eg. "ATOM" + Amount string `json:"amount"` + Denom string `json:"denom"` +} +type Hop struct { + ChannelID string `json:"channel_id"` + PortID string `json:"port_id"` } - type IBCAcknowledgement struct { Data []byte `json:"data"` } @@ -66,22 +112,39 @@ type IBCFee struct { TimeoutFee Array[Coin] `json:"timeout_fee"` } -// IBCTimeout is the timeout for an IBC packet. At least one of block and timestamp is required. +// In IBC each package must set at least one type of timeout: the timestamp or the block height. Using this rather complex enum instead of two timeout fields we ensure that at least one timeout is set. type IBCTimeout struct { - Block *IBCTimeoutBlock `json:"block,omitempty"` // in wasmvm, this does not have "omitempty" - // Nanoseconds since UNIX epoch - Timestamp *Uint64 `json:"timestamp,omitempty"` + Block *IBCTimeoutBlock `json:"block,omitempty"` + Timestamp *Uint64 `json:"timestamp,omitempty"` } -// IBCTimeoutBlock Height is a monotonically increasing data type -// that can be compared against another Height for the purposes of updating and -// freezing clients. -// Ordering is (revision_number, timeout_height) +// IBCTimeoutHeight Height is a monotonically increasing data type that can be compared against another Height for the purposes of updating and freezing clients. Ordering is (revision_number, timeout_height) type IBCTimeoutBlock struct { - // block height after which the packet times out. - // the height within the given revision + // block height after which the packet times out. the height within the given revision Height uint64 `json:"height"` - // the version that the client is currently on - // (eg. after resetting the chain this could increment 1 as height drops to 0) + // 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/Cargo.toml b/packages/std/Cargo.toml index 7d15f3e17b..6d1d3d8445 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -55,6 +55,9 @@ cosmwasm_2_1 = ["cosmwasm_2_0"] # This enables functionality that is only available on 2.2 chains. # It adds `IbcMsg::PayPacketFee` and `IbcMsg::PayPacketFeeAsync`. cosmwasm_2_2 = ["cosmwasm_2_1"] +# This enables functionality that is only available on 3.0 chains. +# It adds `IbcMsg::TransferV2` message +cosmwasm_3_0 = ["cosmwasm_2_2"] [dependencies] base64 = "0.22.0" diff --git a/packages/std/src/exports.rs b/packages/std/src/exports.rs index 17e2cf22d7..c445a996ea 100644 --- a/packages/std/src/exports.rs +++ b/packages/std/src/exports.rs @@ -76,6 +76,10 @@ extern "C" fn requires_cosmwasm_2_1() {} #[no_mangle] extern "C" fn requires_cosmwasm_2_2() {} +#[cfg(feature = "cosmwasm_3_0")] +#[no_mangle] +extern "C" fn requires_cosmwasm_3_0() {} + /// interface_version_* exports mark which Wasm VM interface level this contract is compiled for. /// They can be checked by cosmwasm_vm. /// Update this whenever the Wasm VM interface breaks. diff --git a/packages/std/src/ibc.rs b/packages/std/src/ibc.rs index 5612cf5ebb..4d3754592d 100644 --- a/packages/std/src/ibc.rs +++ b/packages/std/src/ibc.rs @@ -14,9 +14,53 @@ use crate::{Addr, Timestamp}; mod callbacks; mod transfer_msg_builder; +#[cfg(feature = "cosmwasm_3_0")] +mod transfer_msg_builder_v2; pub use callbacks::*; pub use transfer_msg_builder::*; +#[cfg(feature = "cosmwasm_3_0")] +pub use transfer_msg_builder_v2::*; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Hop { + pub port_id: String, + pub channel_id: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +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 /// (contracts that directly speak the IBC protocol via 6 entry points) @@ -52,6 +96,29 @@ pub enum IbcMsg { /// protobuf encoder instead. memo: Option, }, + /// Sends bank tokens owned by the contract to the given address on another chain. + /// The channel must already be established between the ibctransfer module on this chain + /// and a matching module on the remote chain. + /// We cannot select the port_id, this is whatever the local chain has bound the ibctransfer + /// module to. + #[cfg(feature = "cosmwasm_3_0")] + TransferV2 { + /// 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, + /// 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, + }, /// Sends an IBC packet with given data over the existing channel. /// Data should be encoded in a format defined by the channel version, /// and the module on the other side should know how to parse this. diff --git a/packages/std/src/ibc/transfer_msg_builder.rs b/packages/std/src/ibc/transfer_msg_builder.rs index 484ef4038b..6d90e9e7c1 100644 --- a/packages/std/src/ibc/transfer_msg_builder.rs +++ b/packages/std/src/ibc/transfer_msg_builder.rs @@ -7,21 +7,25 @@ use crate::{ #[derive(Clone, Debug, PartialEq, Eq)] pub struct EmptyMemo; #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct WithMemo { - memo: String, + pub(crate) memo: String, } #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct WithSrcCallback { - src_callback: IbcSrcCallback, + pub(crate) src_callback: IbcSrcCallback, } #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct WithDstCallback { - dst_callback: IbcDstCallback, + pub(crate) dst_callback: IbcDstCallback, } #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct WithCallbacks { - src_callback: IbcSrcCallback, - dst_callback: IbcDstCallback, + pub(crate) src_callback: IbcSrcCallback, + pub(crate) dst_callback: IbcDstCallback, } pub trait MemoSource { diff --git a/packages/std/src/ibc/transfer_msg_builder_v2.rs b/packages/std/src/ibc/transfer_msg_builder_v2.rs new file mode 100644 index 0000000000..8d8481b7c8 --- /dev/null +++ b/packages/std/src/ibc/transfer_msg_builder_v2.rs @@ -0,0 +1,409 @@ +use crate::{Coin, IbcDstCallback, IbcMsg, IbcSrcCallback, Timestamp}; + +use super::{ + EmptyMemo, Hop, IbcTimeout, MemoSource, TransferV2Type, WithCallbacks, WithDstCallback, + WithMemo, WithSrcCallback, +}; + +impl> TransferMsgBuilderV2 { + pub fn build(self) -> IbcMsg { + IbcMsg::TransferV2 { + transfer_type: self.transfer_type.into(), + to_address: self.to_address, + tokens: self.tokens, + memo: self.memo.into_memo(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TransferMsgBuilderV2 { + transfer_type: TransferType, + to_address: String, + tokens: Vec, + memo: MemoData, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct EmptyTransferType; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct Direct { + transfer_type: TransferV2Type, +} + +#[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 } + } +} + +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 { + /// Creates a new transfer message with the given parameters and no memo. + pub fn new(to_address: impl Into, tokens: Vec) -> Self { + Self { + transfer_type: EmptyTransferType {}, + to_address: to_address.into(), + tokens, + memo: EmptyMemo, + } + } +} + +impl TransferMsgBuilderV2 { + /// Adds a memo text to the transfer message. + pub fn with_memo( + self, + memo: impl Into, + ) -> TransferMsgBuilderV2 { + TransferMsgBuilderV2 { + transfer_type: self.transfer_type, + to_address: self.to_address, + tokens: self.tokens, + memo: WithMemo { memo: memo.into() }, + } + } +} + +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 { + transfer_type: self.transfer_type, + to_address: self.to_address, + tokens: self.tokens, + 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. + /// + /// For more info check out [`crate::IbcDestinationCallbackMsg`]. + pub fn with_dst_callback( + self, + dst_callback: IbcDstCallback, + ) -> TransferMsgBuilderV2 { + TransferMsgBuilderV2 { + transfer_type: self.transfer_type, + to_address: self.to_address, + tokens: self.tokens, + 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_direct_transfer( + self, + channel_id: String, + ibc_timeout: IbcTimeout, + ) -> TransferMsgBuilderV2 { + TransferMsgBuilderV2 { + transfer_type: Direct { + transfer_type: TransferV2Type::Direct { + channel_id, + ibc_timeout, + }, + }, + to_address: self.to_address, + tokens: self.tokens, + memo: self.memo, + } + } + + /// 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, + channel_id: String, + hops: Vec, + timeout: Timestamp, + ) -> TransferMsgBuilderV2 { + TransferMsgBuilderV2 { + transfer_type: Forwarding { + transfer_type: TransferV2Type::MultiHop { + channel_id, + hops, + timeout, + }, + }, + to_address: self.to_address, + tokens: self.tokens, + memo: self.memo, + } + } + + /// 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_unwinding( + self, + hops: Vec, + timeout: Timestamp, + ) -> TransferMsgBuilderV2 { + TransferMsgBuilderV2 { + transfer_type: Forwarding { + transfer_type: TransferV2Type::Unwinding { hops, timeout }, + }, + to_address: self.to_address, + tokens: self.tokens, + memo: self.memo, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{coin, to_json_string, Addr, IbcCallbackRequest, Timestamp, Uint64}; + + use super::*; + + #[test] + fn test_transfer_msg_builder() { + let src_callback = IbcSrcCallback { + address: Addr::unchecked("src"), + gas_limit: Some(Uint64::new(12345)), + }; + let dst_callback = IbcDstCallback { + address: "dst".to_string(), + gas_limit: None, + }; + + 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 unwinding_builder = empty_builder.clone().with_forwarding_unwinding( + vec![Hop { + port_id: "port-id".to_owned(), + channel_id: "channel-id".to_owned(), + }], + Timestamp::from_seconds(12345), + ); + let unwinding = unwinding_builder + .with_dst_callback(dst_callback.clone()) + .build(); + + let with_memo = forwarding_builder.with_memo("memo").build(); + + 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 = direct_builder + .clone() + .with_dst_callback(dst_callback.clone()); + let with_dst_callback = with_dst_callback_builder.clone().build(); + + let with_both_callbacks1 = with_src_callback_builder + .with_dst_callback(dst_callback.clone()) + .build(); + + let with_both_callbacks2 = with_dst_callback_builder + .with_src_callback(src_callback.clone()) + .build(); + + // assert all the different messages + assert_eq!( + direct, + IbcMsg::TransferV2 { + 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")], + memo: None, + } + ); + assert_eq!( + forwarding, + IbcMsg::TransferV2 { + 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) + }, + to_address: "cosmos1example".to_string(), + tokens: vec![coin(10, "ucoin")], + memo: None, + } + ); + assert_eq!( + with_memo, + IbcMsg::TransferV2 { + 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) + }, + to_address: "cosmos1example".to_string(), + tokens: vec![coin(10, "ucoin")], + memo: Some("memo".to_string()), + } + ); + assert_eq!( + with_src_callback, + IbcMsg::TransferV2 { + 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")], + memo: Some( + to_json_string(&IbcCallbackRequest::source(src_callback.clone())).unwrap() + ), + } + ); + assert_eq!( + with_dst_callback, + IbcMsg::TransferV2 { + 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")], + memo: Some( + to_json_string(&IbcCallbackRequest::destination(dst_callback.clone())).unwrap() + ), + } + ); + assert_eq!( + with_both_callbacks1, + IbcMsg::TransferV2 { + 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")], + memo: Some( + to_json_string(&IbcCallbackRequest::both( + src_callback, + dst_callback.clone() + )) + .unwrap() + ), + } + ); + 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()), + } + ); + } +} diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index df970fa6eb..bf176e79e4 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -69,8 +69,10 @@ pub use crate::errors::{ pub use crate::eureka::{EurekaMsg, EurekaPayload}; pub use crate::hex_binary::HexBinary; pub use crate::ibc::IbcChannelOpenResponse; +#[cfg(feature = "cosmwasm_3_0")] +pub use crate::ibc::TransferMsgBuilderV2; pub use crate::ibc::{ - Ibc3ChannelOpenResponse, IbcAckCallbackMsg, IbcAcknowledgement, IbcBasicResponse, + Hop, Ibc3ChannelOpenResponse, IbcAckCallbackMsg, IbcAcknowledgement, IbcBasicResponse, IbcCallbackRequest, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcDestinationCallbackMsg, IbcDstCallback, IbcEndpoint, IbcFee, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, diff --git a/packages/vm/src/testing/instance.rs b/packages/vm/src/testing/instance.rs index 6701cab256..94785968aa 100644 --- a/packages/vm/src/testing/instance.rs +++ b/packages/vm/src/testing/instance.rs @@ -98,7 +98,7 @@ impl MockInstanceOptions<'_> { fn default_capabilities() -> HashSet { #[allow(unused_mut)] let mut out = capabilities_from_csv( - "iterator,staking,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2", + "iterator,staking,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2,cosmwasm_3_0", ); #[cfg(feature = "stargate")] out.insert("stargate".to_string());