Skip to main content

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