defuse_nep245/
events.rs

1use super::TokenId;
2use crate::checked::{CheckedMtEvent, ErrorLogTooLong};
3use defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT;
4use derive_more::derive::From;
5use near_sdk::{AccountIdRef, AsNep297Event, json_types::U128, near, serde::Deserialize};
6use std::borrow::Cow;
7
8#[must_use = "make sure to `.emit()` this event"]
9#[near(event_json(standard = "nep245"))]
10#[derive(Debug, Clone, Deserialize, From)]
11pub enum MtEvent<'a> {
12    #[event_version("1.0.0")]
13    MtMint(Cow<'a, [MtMintEvent<'a>]>),
14    #[event_version("1.0.0")]
15    MtBurn(Cow<'a, [MtBurnEvent<'a>]>),
16    #[event_version("1.0.0")]
17    MtTransfer(Cow<'a, [MtTransferEvent<'a>]>),
18}
19
20impl MtEvent<'_> {
21    /// Validates that the event log (including potential refund overhead) fits within limits.
22    /// Returns a [`CheckedMtEvent`] that can be emitted.
23    pub fn check_refund(self) -> Result<CheckedMtEvent, ErrorLogTooLong> {
24        let log = self.to_nep297_event().to_event_log();
25        let delta = self.compute_refund_delta();
26        let refund_len = log
27            .len()
28            .saturating_add(delta.overhead())
29            .saturating_sub(delta.savings());
30
31        if refund_len > TOTAL_LOG_LENGTH_LIMIT {
32            return Err(ErrorLogTooLong);
33        }
34        Ok(CheckedMtEvent(log))
35    }
36}
37
38#[must_use = "make sure to `.emit()` this event"]
39#[near(serializers = [json])]
40#[derive(Debug, Clone)]
41pub struct MtMintEvent<'a> {
42    pub owner_id: Cow<'a, AccountIdRef>,
43    pub token_ids: Cow<'a, [TokenId]>,
44    pub amounts: Cow<'a, [U128]>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub memo: Option<Cow<'a, str>>,
47}
48
49#[must_use = "make sure to `.emit()` this event"]
50#[near(serializers = [json])]
51#[derive(Debug, Clone)]
52pub struct MtBurnEvent<'a> {
53    pub owner_id: Cow<'a, AccountIdRef>,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub authorized_id: Option<Cow<'a, AccountIdRef>>,
56    pub token_ids: Cow<'a, [TokenId]>,
57    pub amounts: Cow<'a, [U128]>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub memo: Option<Cow<'a, str>>,
60}
61
62#[must_use = "make sure to `.emit()` this event"]
63#[near(serializers = [json])]
64#[derive(Debug, Clone)]
65pub struct MtTransferEvent<'a> {
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub authorized_id: Option<Cow<'a, AccountIdRef>>,
68    pub old_owner_id: Cow<'a, AccountIdRef>,
69    pub new_owner_id: Cow<'a, AccountIdRef>,
70    pub token_ids: Cow<'a, [TokenId]>,
71    pub amounts: Cow<'a, [U128]>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub memo: Option<Cow<'a, str>>,
74}
75
76#[cfg(test)]
77mod tests {
78    use crate::checked::REFUND_EXTRA_BYTES;
79    use defuse_near_utils::REFUND_MEMO;
80
81    use super::*;
82    use near_sdk::json_types::U128;
83
84    const REFUND_STR_LEN: usize = REFUND_MEMO.len();
85
86    /// Create a single-event `MtTransfer` with exact log length.
87    /// Pads `token_id` to achieve the desired length.
88    fn create_single_event_mt(length: usize, memo: Option<&str>) -> MtEvent<'static> {
89        let old_owner: near_sdk::AccountId = "aa".parse().unwrap();
90        let new_owner: near_sdk::AccountId = "bb".parse().unwrap();
91        let base_token_id = "t";
92
93        // Measure base log length
94        let base_event = MtTransferEvent {
95            authorized_id: None,
96            old_owner_id: Cow::Owned(old_owner.clone()),
97            new_owner_id: Cow::Owned(new_owner.clone()),
98            token_ids: Cow::Owned(vec![base_token_id.to_string()]),
99            amounts: Cow::Owned(vec![U128(1)]),
100            memo: memo.map(|m| Cow::Owned(m.to_string())),
101        };
102        let base_mt_event = MtEvent::MtTransfer(Cow::Owned(vec![base_event]));
103        let base_length = base_mt_event.to_nep297_event().to_event_log().len();
104
105        // Calculate padding needed for token_id
106        let padding_needed = length.saturating_sub(base_length);
107        let padded_token_id = format!("{}{}", base_token_id, "x".repeat(padding_needed));
108
109        let event = MtTransferEvent {
110            authorized_id: None,
111            old_owner_id: Cow::Owned(old_owner),
112            new_owner_id: Cow::Owned(new_owner),
113            token_ids: Cow::Owned(vec![padded_token_id]),
114            amounts: Cow::Owned(vec![U128(1)]),
115            memo: memo.map(|m| Cow::Owned(m.to_string())),
116        };
117
118        let mt_event = MtEvent::MtTransfer(Cow::Owned(vec![event]));
119        let log_len = mt_event.to_nep297_event().to_event_log().len();
120        assert_eq!(
121            log_len, length,
122            "Expected log length {length}, got {log_len}"
123        );
124
125        mt_event
126    }
127
128    /// Create a triple-event `MtTransfer` with exact log length.
129    /// Each event has its own memo. Pads first event's `token_id` to achieve the desired length.
130    fn create_triple_event_mt(length: usize, memos: [Option<&str>; 3]) -> MtEvent<'static> {
131        let old_owner: near_sdk::AccountId = "aa".parse().unwrap();
132        let new_owner: near_sdk::AccountId = "bb".parse().unwrap();
133        let base_token_id = "t";
134
135        // Measure base log length with 3 events
136        let base_events: Vec<MtTransferEvent<'static>> = memos
137            .iter()
138            .enumerate()
139            .map(|(i, memo)| MtTransferEvent {
140                authorized_id: None,
141                old_owner_id: Cow::Owned(old_owner.clone()),
142                new_owner_id: Cow::Owned(new_owner.clone()),
143                token_ids: Cow::Owned(vec![format!("{base_token_id}{i}")]),
144                amounts: Cow::Owned(vec![U128(1)]),
145                memo: memo.map(|m| Cow::Owned(m.to_string())),
146            })
147            .collect();
148        let base_mt_event = MtEvent::MtTransfer(Cow::Owned(base_events));
149        let base_length = base_mt_event.to_nep297_event().to_event_log().len();
150
151        // Calculate padding needed (only pad the first event's token_id)
152        let padding_needed = length.saturating_sub(base_length);
153        let padded_token_id = format!("{base_token_id}0{}", "x".repeat(padding_needed));
154
155        // Create final events: first one with padded token_id, rest with base token_ids
156        let events: Vec<MtTransferEvent<'static>> = memos
157            .iter()
158            .enumerate()
159            .map(|(i, memo)| {
160                let token_id = if i == 0 {
161                    padded_token_id.clone()
162                } else {
163                    format!("{base_token_id}{i}")
164                };
165                MtTransferEvent {
166                    authorized_id: None,
167                    old_owner_id: Cow::Owned(old_owner.clone()),
168                    new_owner_id: Cow::Owned(new_owner.clone()),
169                    token_ids: Cow::Owned(vec![token_id]),
170                    amounts: Cow::Owned(vec![U128(1)]),
171                    memo: memo.map(|m| Cow::Owned(m.to_string())),
172                }
173            })
174            .collect();
175
176        let mt_event = MtEvent::MtTransfer(Cow::Owned(events));
177        let log_len = mt_event.to_nep297_event().to_event_log().len();
178        assert_eq!(
179            log_len, length,
180            "Expected log length {length}, got {log_len}"
181        );
182
183        mt_event
184    }
185
186    #[test]
187    fn single_event_no_memo_at_limit_minus_overhead_passes() {
188        let mt = create_single_event_mt(TOTAL_LOG_LENGTH_LIMIT - REFUND_EXTRA_BYTES, None);
189        assert!(mt.check_refund().is_ok());
190    }
191
192    #[test]
193    fn single_event_short_memo_at_limit_fails() {
194        let memo = "refu";
195        let mt = create_single_event_mt(TOTAL_LOG_LENGTH_LIMIT, Some(memo));
196        assert!(matches!(mt.check_refund().unwrap_err(), ErrorLogTooLong));
197    }
198
199    #[test]
200    fn triple_event_no_memo_at_limit_minus_overhead_passes() {
201        let mt = create_triple_event_mt(TOTAL_LOG_LENGTH_LIMIT - 3 * REFUND_EXTRA_BYTES, [None; 3]);
202        assert!(mt.check_refund().is_ok());
203    }
204
205    #[test]
206    fn triple_event_short_memo_at_limit_fails() {
207        let mt = create_triple_event_mt(TOTAL_LOG_LENGTH_LIMIT, [Some("refu"); 3]);
208        assert!(matches!(mt.check_refund().unwrap_err(), ErrorLogTooLong));
209    }
210
211    #[test]
212    fn triple_event_mixed_memos_overhead_equals_savings_at_limit_passes() {
213        // there are 3 events
214        // 1 without memo
215        // 2 with "refund" memo
216        // 3 with really long memo
217        // total log length is exactly TOTAL_LOG_LENGTH_LIMIT, but since really long memo will be
218        // replaced with just refund there will be enough buffer to set memo "refund" also for
219        // first event and still fit into TOTAL_LOG_LENGTH_LIMIT on refund
220        let long_memo = "x".repeat(REFUND_EXTRA_BYTES + REFUND_STR_LEN);
221        assert_eq!(long_memo.len() - REFUND_STR_LEN, REFUND_EXTRA_BYTES);
222
223        let mt = create_triple_event_mt(
224            TOTAL_LOG_LENGTH_LIMIT,
225            [None, Some("refund"), Some(&long_memo)],
226        );
227        assert!(mt.check_refund().is_ok());
228    }
229}