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 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 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 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 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 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 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 let padding_needed = length.saturating_sub(base_length);
153 let padded_token_id = format!("{base_token_id}0{}", "x".repeat(padding_needed));
154
155 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 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}