ibc_app_nft_transfer/
module.rs

1//! Provides IBC module callbacks implementation for the ICS-721 transfer.
2use ibc_core::channel::types::acknowledgement::{Acknowledgement, AcknowledgementStatus};
3use ibc_core::channel::types::channel::{Counterparty, Order};
4use ibc_core::channel::types::packet::Packet;
5use ibc_core::channel::types::Version;
6use ibc_core::host::types::identifiers::{ChannelId, ConnectionId, PortId};
7use ibc_core::primitives::prelude::*;
8use ibc_core::primitives::Signer;
9use ibc_core::router::types::module::ModuleExtras;
10
11use crate::context::{NftTransferExecutionContext, NftTransferValidationContext};
12use crate::handler::{
13    process_recv_packet_execute, refund_packet_nft_execute, refund_packet_nft_validate,
14};
15use crate::types::error::NftTransferError;
16use crate::types::events::{AckEvent, AckStatusEvent, RecvEvent, TimeoutEvent};
17use crate::types::packet::PacketData;
18use crate::types::{ack_success_b64, VERSION};
19
20pub fn on_chan_open_init_validate(
21    ctx: &impl NftTransferValidationContext,
22    order: Order,
23    _connection_hops: &[ConnectionId],
24    port_id: &PortId,
25    _channel_id: &ChannelId,
26    _counterparty: &Counterparty,
27    version: &Version,
28) -> Result<(), NftTransferError> {
29    if order != Order::Unordered {
30        return Err(NftTransferError::MismatchedChannelOrders {
31            expected: Order::Unordered,
32            actual: order,
33        });
34    }
35    let bound_port = ctx.get_port()?;
36    if port_id != &bound_port {
37        return Err(NftTransferError::MismatchedPortIds {
38            actual: port_id.clone(),
39            expected: bound_port,
40        });
41    }
42
43    if !version.is_empty() {
44        version.verify_is_expected(Version::new(VERSION.to_string()))?;
45    }
46
47    Ok(())
48}
49
50pub fn on_chan_open_init_execute(
51    _ctx: &mut impl NftTransferExecutionContext,
52    _order: Order,
53    _connection_hops: &[ConnectionId],
54    _port_id: &PortId,
55    _channel_id: &ChannelId,
56    _counterparty: &Counterparty,
57    _version: &Version,
58) -> Result<(ModuleExtras, Version), NftTransferError> {
59    Ok((ModuleExtras::empty(), Version::new(VERSION.to_string())))
60}
61
62pub fn on_chan_open_try_validate(
63    _ctx: &impl NftTransferValidationContext,
64    order: Order,
65    _connection_hops: &[ConnectionId],
66    _port_id: &PortId,
67    _channel_id: &ChannelId,
68    _counterparty: &Counterparty,
69    counterparty_version: &Version,
70) -> Result<(), NftTransferError> {
71    if order != Order::Unordered {
72        return Err(NftTransferError::MismatchedChannelOrders {
73            expected: Order::Unordered,
74            actual: order,
75        });
76    }
77
78    counterparty_version.verify_is_expected(Version::new(VERSION.to_string()))?;
79
80    Ok(())
81}
82
83pub fn on_chan_open_try_execute(
84    _ctx: &mut impl NftTransferExecutionContext,
85    _order: Order,
86    _connection_hops: &[ConnectionId],
87    _port_id: &PortId,
88    _channel_id: &ChannelId,
89    _counterparty: &Counterparty,
90    _counterparty_version: &Version,
91) -> Result<(ModuleExtras, Version), NftTransferError> {
92    Ok((ModuleExtras::empty(), Version::new(VERSION.to_string())))
93}
94
95pub fn on_chan_open_ack_validate(
96    _ctx: &impl NftTransferExecutionContext,
97    _port_id: &PortId,
98    _channel_id: &ChannelId,
99    counterparty_version: &Version,
100) -> Result<(), NftTransferError> {
101    counterparty_version.verify_is_expected(Version::new(VERSION.to_string()))?;
102    Ok(())
103}
104
105pub fn on_chan_open_ack_execute(
106    _ctx: &mut impl NftTransferExecutionContext,
107    _port_id: &PortId,
108    _channel_id: &ChannelId,
109    _counterparty_version: &Version,
110) -> Result<ModuleExtras, NftTransferError> {
111    Ok(ModuleExtras::empty())
112}
113
114pub fn on_chan_open_confirm_validate(
115    _ctx: &impl NftTransferValidationContext,
116    _port_id: &PortId,
117    _channel_id: &ChannelId,
118) -> Result<(), NftTransferError> {
119    Ok(())
120}
121
122pub fn on_chan_open_confirm_execute(
123    _ctx: &mut impl NftTransferExecutionContext,
124    _port_id: &PortId,
125    _channel_id: &ChannelId,
126) -> Result<ModuleExtras, NftTransferError> {
127    Ok(ModuleExtras::empty())
128}
129
130pub fn on_chan_close_init_validate(
131    _ctx: &impl NftTransferValidationContext,
132    _port_id: &PortId,
133    _channel_id: &ChannelId,
134) -> Result<(), NftTransferError> {
135    Err(NftTransferError::InvalidClosedChannel)
136}
137
138pub fn on_chan_close_init_execute(
139    _ctx: &mut impl NftTransferExecutionContext,
140    _port_id: &PortId,
141    _channel_id: &ChannelId,
142) -> Result<ModuleExtras, NftTransferError> {
143    Err(NftTransferError::InvalidClosedChannel)
144}
145
146pub fn on_chan_close_confirm_validate(
147    _ctx: &impl NftTransferValidationContext,
148    _port_id: &PortId,
149    _channel_id: &ChannelId,
150) -> Result<(), NftTransferError> {
151    Ok(())
152}
153
154pub fn on_chan_close_confirm_execute(
155    _ctx: &mut impl NftTransferExecutionContext,
156    _port_id: &PortId,
157    _channel_id: &ChannelId,
158) -> Result<ModuleExtras, NftTransferError> {
159    Ok(ModuleExtras::empty())
160}
161
162pub fn on_recv_packet_execute(
163    ctx_b: &mut impl NftTransferExecutionContext,
164    packet: &Packet,
165) -> (ModuleExtras, Acknowledgement) {
166    let Ok(data) = serde_json::from_slice::<PacketData>(&packet.data) else {
167        let ack =
168            AcknowledgementStatus::error(NftTransferError::FailedToDeserializePacketData.into());
169        return (ModuleExtras::empty(), ack.into());
170    };
171
172    let (mut extras, ack) = match process_recv_packet_execute(ctx_b, packet, data.clone()) {
173        Ok(extras) => (extras, AcknowledgementStatus::success(ack_success_b64())),
174        Err(boxed_error) => {
175            let (extras, error) = *boxed_error;
176            (extras, AcknowledgementStatus::error(error.into()))
177        }
178    };
179
180    let recv_event = RecvEvent {
181        sender: data.sender,
182        receiver: data.receiver,
183        class: data.class_id,
184        tokens: data.token_ids,
185        memo: data.memo.unwrap_or("".into()),
186        success: ack.is_successful(),
187    };
188    extras.events.push(recv_event.into());
189
190    (extras, ack.into())
191}
192
193pub fn on_acknowledgement_packet_validate(
194    ctx: &impl NftTransferValidationContext,
195    packet: &Packet,
196    acknowledgement: &Acknowledgement,
197    _relayer: &Signer,
198) -> Result<(), NftTransferError> {
199    let data = serde_json::from_slice::<PacketData>(&packet.data)
200        .map_err(|_| NftTransferError::FailedToDeserializePacketData)?;
201
202    let acknowledgement = serde_json::from_slice::<AcknowledgementStatus>(acknowledgement.as_ref())
203        .map_err(|_| NftTransferError::FailedToDeserializeAck)?;
204
205    if !acknowledgement.is_successful() {
206        refund_packet_nft_validate(ctx, packet, &data)?;
207    }
208
209    Ok(())
210}
211
212pub fn on_acknowledgement_packet_execute(
213    ctx: &mut impl NftTransferExecutionContext,
214    packet: &Packet,
215    acknowledgement: &Acknowledgement,
216    _relayer: &Signer,
217) -> (ModuleExtras, Result<(), NftTransferError>) {
218    let Ok(data) = serde_json::from_slice::<PacketData>(&packet.data) else {
219        return (
220            ModuleExtras::empty(),
221            Err(NftTransferError::FailedToDeserializePacketData),
222        );
223    };
224
225    let Ok(acknowledgement) =
226        serde_json::from_slice::<AcknowledgementStatus>(acknowledgement.as_ref())
227    else {
228        return (
229            ModuleExtras::empty(),
230            Err(NftTransferError::FailedToDeserializeAck),
231        );
232    };
233
234    if !acknowledgement.is_successful() {
235        if let Err(err) = refund_packet_nft_execute(ctx, packet, &data) {
236            return (ModuleExtras::empty(), Err(err));
237        }
238    }
239
240    let ack_event = AckEvent {
241        sender: data.sender,
242        receiver: data.receiver,
243        class: data.class_id,
244        tokens: data.token_ids,
245        memo: data.memo.unwrap_or("".into()),
246        acknowledgement: acknowledgement.clone(),
247    };
248
249    let extras = ModuleExtras {
250        events: vec![ack_event.into(), AckStatusEvent { acknowledgement }.into()],
251        log: Vec::new(),
252    };
253
254    (extras, Ok(()))
255}
256
257pub fn on_timeout_packet_validate(
258    ctx: &impl NftTransferValidationContext,
259    packet: &Packet,
260    _relayer: &Signer,
261) -> Result<(), NftTransferError> {
262    let data = serde_json::from_slice::<PacketData>(&packet.data)
263        .map_err(|_| NftTransferError::FailedToDeserializePacketData)?;
264
265    refund_packet_nft_validate(ctx, packet, &data)?;
266
267    Ok(())
268}
269
270pub fn on_timeout_packet_execute(
271    ctx: &mut impl NftTransferExecutionContext,
272    packet: &Packet,
273    _relayer: &Signer,
274) -> (ModuleExtras, Result<(), NftTransferError>) {
275    let Ok(data) = serde_json::from_slice::<PacketData>(&packet.data) else {
276        return (
277            ModuleExtras::empty(),
278            Err(NftTransferError::FailedToDeserializePacketData),
279        );
280    };
281
282    if let Err(err) = refund_packet_nft_execute(ctx, packet, &data) {
283        return (ModuleExtras::empty(), Err(err));
284    }
285
286    let timeout_event = TimeoutEvent {
287        refund_receiver: data.sender,
288        refund_class: data.class_id,
289        refund_tokens: data.token_ids,
290        memo: data.memo.unwrap_or("".into()),
291    };
292
293    let extras = ModuleExtras {
294        events: vec![timeout_event.into()],
295        log: Vec::new(),
296    };
297
298    (extras, Ok(()))
299}
300
301#[cfg(test)]
302mod test {
303    use super::*;
304
305    #[test]
306    fn test_ack_ser() {
307        fn ser_json_assert_eq(ack: AcknowledgementStatus, json_str: &str) {
308            let ser = serde_json::to_string(&ack).unwrap();
309            assert_eq!(ser, json_str)
310        }
311
312        ser_json_assert_eq(
313            AcknowledgementStatus::success(ack_success_b64()),
314            r#"{"result":"AQ=="}"#,
315        );
316        ser_json_assert_eq(
317            AcknowledgementStatus::error(NftTransferError::FailedToDeserializePacketData.into()),
318            r#"{"error":"failed to deserialize packet data"}"#,
319        );
320    }
321
322    #[test]
323    fn test_ack_success_to_vec() {
324        let ack_success: Vec<u8> = AcknowledgementStatus::success(ack_success_b64()).into();
325
326        // Check that it's the same output as ibc-go
327        // Note: this also implicitly checks that the ack bytes are non-empty,
328        // which would make the conversion to `Acknowledgement` panic
329        assert_eq!(ack_success, br#"{"result":"AQ=="}"#);
330    }
331
332    #[test]
333    fn test_ack_error_to_vec() {
334        let ack_error: Vec<u8> =
335            AcknowledgementStatus::error(NftTransferError::FailedToDeserializePacketData.into())
336                .into();
337
338        // Check that it's the same output as ibc-go
339        // Note: this also implicitly checks that the ack bytes are non-empty,
340        // which would make the conversion to `Acknowledgement` panic
341        assert_eq!(
342            ack_error,
343            br#"{"error":"failed to deserialize packet data"}"#
344        );
345    }
346
347    #[test]
348    fn test_ack_de() {
349        fn de_json_assert_eq(json_str: &str, ack: AcknowledgementStatus) {
350            let de = serde_json::from_str::<AcknowledgementStatus>(json_str).unwrap();
351            assert_eq!(de, ack)
352        }
353
354        de_json_assert_eq(
355            r#"{"result":"AQ=="}"#,
356            AcknowledgementStatus::success(ack_success_b64()),
357        );
358        de_json_assert_eq(
359            r#"{"error":"failed to deserialize packet data"}"#,
360            AcknowledgementStatus::error(NftTransferError::FailedToDeserializePacketData.into()),
361        );
362
363        assert!(serde_json::from_str::<AcknowledgementStatus>(r#"{"success":"AQ=="}"#).is_err());
364    }
365}