cosmwasm_std/ibc/
callbacks.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//! This module contains types for the IBC callbacks defined in
//! [ADR-8](https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-008-app-caller-cbs.md).

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{Addr, IbcAcknowledgement, IbcPacket, Uint64};

/// This is just a type representing the data that has to be sent with the IBC message to receive
/// callbacks. It should be serialized and sent with the IBC message.
/// The specific field and format to send it in can vary depending on the IBC message,
/// but is usually the `memo` field by convention.
///
/// See [`IbcSourceCallbackMsg`] and [`IbcDestinationCallbackMsg`] for more details.
///
/// # Example
///
/// Using [`TransferMsgBuilder`](crate::TransferMsgBuilder):
/// ```rust
/// use cosmwasm_std::{
///     to_json_string, Coin, IbcCallbackRequest, TransferMsgBuilder, IbcSrcCallback, IbcTimeout, Response,
///     Timestamp,
/// };
/// # use cosmwasm_std::testing::mock_env;
/// # let env = mock_env();
///
/// let _msg = TransferMsgBuilder::new(
///     "channel-0".to_string(),
///     "cosmos1example".to_string(),
///     Coin::new(10u32, "ucoin"),
///     Timestamp::from_seconds(12345),
/// )
/// .with_src_callback(IbcSrcCallback {
///     address: env.contract.address,
///     gas_limit: None,
/// })
/// .build();
/// ```
///
/// Manual serialization:
/// ```rust
/// use cosmwasm_std::{
///     to_json_string, Coin, IbcCallbackRequest, IbcMsg, IbcSrcCallback, IbcTimeout, Response,
///     Timestamp,
/// };
/// # use cosmwasm_std::testing::mock_env;
/// # let env = mock_env();
///
/// let _transfer = IbcMsg::Transfer {
///     to_address: "cosmos1example".to_string(),
///     channel_id: "channel-0".to_string(),
///     amount: Coin::new(10u32, "ucoin"),
///     timeout: Timestamp::from_seconds(12345).into(),
///     memo: Some(to_json_string(&IbcCallbackRequest::source(IbcSrcCallback {
///         address: env.contract.address,
///         gas_limit: None,
///     })).unwrap()),
/// };
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct IbcCallbackRequest {
    // using private fields to force use of the constructors
    #[serde(skip_serializing_if = "Option::is_none")]
    src_callback: Option<IbcSrcCallback>,
    #[serde(skip_serializing_if = "Option::is_none")]
    dest_callback: Option<IbcDstCallback>,
}

impl IbcCallbackRequest {
    /// Use this if you want to execute callbacks on both the source and destination chain.
    pub fn both(src_callback: IbcSrcCallback, dest_callback: IbcDstCallback) -> Self {
        IbcCallbackRequest {
            src_callback: Some(src_callback),
            dest_callback: Some(dest_callback),
        }
    }

    /// Use this if you want to execute callbacks on the source chain, but not the destination chain.
    pub fn source(src_callback: IbcSrcCallback) -> Self {
        IbcCallbackRequest {
            src_callback: Some(src_callback),
            dest_callback: None,
        }
    }

    /// Use this if you want to execute callbacks on the destination chain, but not the source chain.
    pub fn destination(dest_callback: IbcDstCallback) -> Self {
        IbcCallbackRequest {
            src_callback: None,
            dest_callback: Some(dest_callback),
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct IbcSrcCallback {
    /// The source chain address that should receive the callback.
    /// For CosmWasm contracts, this *must* be `env.contract.address`.
    /// Other addresses are not allowed and will effectively be ignored.
    pub address: Addr,
    /// Optional gas limit for the callback (in Cosmos SDK gas units)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub gas_limit: Option<Uint64>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct IbcDstCallback {
    /// The destination chain address that should receive the callback.
    pub address: String,
    /// Optional gas limit for the callback (in Cosmos SDK gas units)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub gas_limit: Option<Uint64>,
}

/// The type of IBC source callback that is being called.
///
/// IBC source callbacks are needed for cases where your contract triggers the sending of an
/// IBC packet through some other message (i.e. not through [`crate::IbcMsg::SendPacket`]) and needs to
/// know whether or not the packet was successfully received on the other chain.
/// A prominent example is the [`crate::IbcMsg::Transfer`] message. Without callbacks, you cannot know
/// whether the transfer was successful or not.
///
/// Note that there are some prerequisites that need to be fulfilled to receive source callbacks:
/// - The contract must implement the `ibc_source_callback` entrypoint.
/// - The IBC application in the source chain must have support for the callbacks middleware.
/// - You have to add serialized [`IbcCallbackRequest`] to a specific field of the message.
///   For `IbcMsg::Transfer`, this is the `memo` field and it needs to be json-encoded.
/// - The receiver of the callback must also be the sender of the message.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum IbcSourceCallbackMsg {
    Acknowledgement(IbcAckCallbackMsg),
    Timeout(IbcTimeoutCallbackMsg),
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[non_exhaustive]
pub struct IbcAckCallbackMsg {
    pub acknowledgement: IbcAcknowledgement,
    pub original_packet: IbcPacket,
    pub relayer: Addr,
}

impl IbcAckCallbackMsg {
    pub fn new(
        acknowledgement: IbcAcknowledgement,
        original_packet: IbcPacket,
        relayer: Addr,
    ) -> Self {
        Self {
            acknowledgement,
            original_packet,
            relayer,
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[non_exhaustive]
pub struct IbcTimeoutCallbackMsg {
    pub packet: IbcPacket,
    pub relayer: Addr,
}

impl IbcTimeoutCallbackMsg {
    pub fn new(packet: IbcPacket, relayer: Addr) -> Self {
        Self { packet, relayer }
    }
}

/// The message type of the IBC destination callback.
///
/// The IBC destination callback is needed for cases where someone triggers the sending of an
/// IBC packet through some other message (i.e. not through [`crate::IbcMsg::SendPacket`]) and
/// your contract needs to know that it received this.
/// A prominent example is the [`crate::IbcMsg::Transfer`] message. Without callbacks, you cannot know
/// that someone sent you IBC coins.
///
/// It is important to validate that the packet and acknowledgement are what you expect them to be.
/// For example for a transfer message, the receiver is not necessarily the contract itself.
///
/// The callback is called when the packet is being acknowledged on the destination chain.
/// This happens for both synchronous and asynchronous acknowledgements.
///
/// Note that there are some prerequisites that need to be fulfilled to receive destination callbacks:
/// - The contract must implement the `ibc_destination_callback` entrypoint.
/// - The IBC application in the destination chain must have support for the callbacks middleware.
/// - You have to add serialized [`IbcCallbackRequest`] to a specific field of the message.
///   For `IbcMsg::Transfer`, this is the `memo` field and it needs to be json-encoded.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct IbcDestinationCallbackMsg {
    pub packet: IbcPacket,
    pub ack: IbcAcknowledgement,
}

#[cfg(test)]
mod tests {
    use crate::to_json_string;

    use super::*;

    #[test]
    fn ibc_callback_data_serialization() {
        let mut data = IbcCallbackRequest::both(
            IbcSrcCallback {
                address: Addr::unchecked("src_address"),
                gas_limit: Some(123u64.into()),
            },
            IbcDstCallback {
                address: "dst_address".to_string(),
                gas_limit: Some(1234u64.into()),
            },
        );

        // both
        let json = to_json_string(&data).unwrap();
        assert_eq!(
            json,
            r#"{"src_callback":{"address":"src_address","gas_limit":"123"},"dest_callback":{"address":"dst_address","gas_limit":"1234"}}"#
        );

        // dst only, without gas limit
        let mut src = data.src_callback.take().unwrap();
        data.dest_callback.as_mut().unwrap().gas_limit = None;
        let json = to_json_string(&data).unwrap();
        assert_eq!(json, r#"{"dest_callback":{"address":"dst_address"}}"#);

        // source only, without gas limit
        src.gas_limit = None;
        data.src_callback = Some(src);
        data.dest_callback = None;
        let json = to_json_string(&data).unwrap();
        assert_eq!(json, r#"{"src_callback":{"address":"src_address"}}"#);
    }
}