1use super::ErrorLogTooLong;
2use crate::MtEvent;
3use defuse_near_utils::REFUND_MEMO;
4use near_sdk::events::AsNep297Event;
5
6const REFUND_STR_LEN: usize = REFUND_MEMO.len();
7pub const REFUND_EXTRA_BYTES: usize = r#","memo":"""#.len() + REFUND_STR_LEN;
8
9#[derive(Debug)]
12#[must_use = "call `.emit()` to emit the event"]
13pub struct CheckedMtEvent(pub(crate) String);
14
15impl CheckedMtEvent {
16 pub fn emit(self) {
17 near_sdk::env::log_str(&self.0);
18 }
19}
20
21#[derive(Default, Clone, Copy)]
22#[must_use]
23struct RefundLogDelta {
24 overhead: usize,
25 savings: usize,
26}
27
28impl RefundLogDelta {
29 const fn new(overhead: usize, savings: usize) -> Self {
30 Self {
31 overhead: overhead.saturating_sub(savings),
32 savings: savings.saturating_sub(overhead),
33 }
34 }
35
36 const fn overhead(self) -> usize {
37 self.overhead
38 }
39
40 const fn savings(self) -> usize {
41 self.savings
42 }
43
44 const fn saturating_add(self, other: Self) -> Self {
45 Self::new(
46 self.overhead.saturating_add(other.overhead),
47 self.savings.saturating_add(other.savings),
48 )
49 }
50}
51
52const fn refund_log_delta(memo: Option<&str>) -> RefundLogDelta {
53 let Some(m) = memo else {
54 return RefundLogDelta {
55 overhead: REFUND_EXTRA_BYTES,
56 savings: 0,
57 };
58 };
59 RefundLogDelta::new(REFUND_STR_LEN, m.len())
60}
61
62impl MtEvent<'_> {
63 pub fn check_refund(self) -> Result<CheckedMtEvent, ErrorLogTooLong> {
66 let log = self.to_nep297_event().to_event_log();
67 let delta = self.compute_refund_delta();
68 let refund_len = log
69 .len()
70 .saturating_add(delta.overhead())
71 .saturating_sub(delta.savings());
72
73 if refund_len > defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT {
74 return Err(ErrorLogTooLong);
75 }
76 Ok(CheckedMtEvent(log))
77 }
78
79 fn compute_refund_delta(&self) -> RefundLogDelta {
80 match self {
81 MtEvent::MtMint(events) => events
82 .iter()
83 .map(|e| refund_log_delta(e.memo.as_deref()))
84 .fold(RefundLogDelta::default(), RefundLogDelta::saturating_add),
85 MtEvent::MtBurn(events) => events
86 .iter()
87 .map(|e| refund_log_delta(e.memo.as_deref()))
88 .fold(RefundLogDelta::default(), RefundLogDelta::saturating_add),
89 MtEvent::MtTransfer(events) => events
90 .iter()
91 .map(|e| refund_log_delta(e.memo.as_deref()))
92 .fold(RefundLogDelta::default(), RefundLogDelta::saturating_add),
93 }
94 }
95}
96
97#[cfg(test)]
98mod test {
99 use std::borrow::Cow;
100
101 use near_account_id::AccountId;
102 use near_sdk::AsNep297Event;
103
104 use super::refund_log_delta;
105 use crate::MtTransferEvent;
106 use crate::checked::{REFUND_EXTRA_BYTES, REFUND_STR_LEN};
107 use crate::{ErrorLogTooLong, MtEvent};
108
109 #[test]
110 fn test_refund_log_delta_shorter_memo() {
111 let delta = refund_log_delta(Some("r"));
112 assert_eq!(delta.savings(), 0);
113 assert_eq!(delta.overhead(), 5);
114 }
115
116 #[test]
117 fn test_refund_log_delta_longer_memo() {
118 let delta = refund_log_delta(Some("refund123"));
119 assert_eq!(delta.savings(), 3);
120 assert_eq!(delta.overhead(), 0);
121 }
122
123 #[test]
124 fn test_refund_log_delta_equal_memo() {
125 let delta = refund_log_delta(Some("123456"));
126 assert_eq!(delta.savings(), 0);
127 assert_eq!(delta.overhead(), 0);
128 }
129
130 #[test]
131 fn test_refund_log_delta_empty_memo() {
132 let delta = refund_log_delta(None);
133 assert_eq!(delta.savings(), 0);
134 assert_eq!(delta.overhead(), REFUND_EXTRA_BYTES);
135 }
136
137 fn create_single_event_mt(length: usize, memo: Option<&str>) -> MtEvent<'static> {
140 let old_owner: AccountId = "aa".parse().unwrap();
141 let new_owner: AccountId = "bb".parse().unwrap();
142 let base_token_id = "t";
143
144 let base_event = MtTransferEvent {
146 authorized_id: None,
147 old_owner_id: Cow::Owned(old_owner.clone()),
148 new_owner_id: Cow::Owned(new_owner.clone()),
149 token_ids: Cow::Owned(vec![base_token_id.to_string()]),
150 amounts: Cow::Owned(vec![1]),
151 memo: memo.map(|m| Cow::Owned(m.to_string())),
152 };
153 let base_mt_event = MtEvent::MtTransfer(Cow::Owned(vec![base_event]));
154 let base_length = base_mt_event.to_nep297_event().to_event_log().len();
155
156 let padding_needed = length.saturating_sub(base_length);
158 let padded_token_id = format!("{}{}", base_token_id, "x".repeat(padding_needed));
159
160 let event = MtTransferEvent {
161 authorized_id: None,
162 old_owner_id: Cow::Owned(old_owner),
163 new_owner_id: Cow::Owned(new_owner),
164 token_ids: Cow::Owned(vec![padded_token_id]),
165 amounts: Cow::Owned(vec![1]),
166 memo: memo.map(|m| Cow::Owned(m.to_string())),
167 };
168
169 let mt_event = MtEvent::MtTransfer(Cow::Owned(vec![event]));
170 let log_len = mt_event.to_nep297_event().to_event_log().len();
171 assert_eq!(
172 log_len, length,
173 "Expected log length {length}, got {log_len}"
174 );
175
176 mt_event
177 }
178
179 fn create_triple_event_mt(length: usize, memos: [Option<&str>; 3]) -> MtEvent<'static> {
182 let old_owner: AccountId = "aa".parse().unwrap();
183 let new_owner: AccountId = "bb".parse().unwrap();
184 let base_token_id = "t";
185
186 let base_events: Vec<MtTransferEvent<'static>> = memos
188 .iter()
189 .enumerate()
190 .map(|(i, memo)| MtTransferEvent {
191 authorized_id: None,
192 old_owner_id: Cow::Owned(old_owner.clone()),
193 new_owner_id: Cow::Owned(new_owner.clone()),
194 token_ids: Cow::Owned(vec![format!("{base_token_id}{i}")]),
195 amounts: Cow::Owned(vec![1]),
196 memo: memo.map(|m| Cow::Owned(m.to_string())),
197 })
198 .collect();
199 let base_mt_event = MtEvent::MtTransfer(Cow::Owned(base_events));
200 let base_length = base_mt_event.to_nep297_event().to_event_log().len();
201
202 let padding_needed = length.saturating_sub(base_length);
204 let padded_token_id = format!("{base_token_id}0{}", "x".repeat(padding_needed));
205
206 let events: Vec<MtTransferEvent<'static>> = memos
208 .iter()
209 .enumerate()
210 .map(|(i, memo)| {
211 let token_id = if i == 0 {
212 padded_token_id.clone()
213 } else {
214 format!("{base_token_id}{i}")
215 };
216 MtTransferEvent {
217 authorized_id: None,
218 old_owner_id: Cow::Owned(old_owner.clone()),
219 new_owner_id: Cow::Owned(new_owner.clone()),
220 token_ids: Cow::Owned(vec![token_id]),
221 amounts: Cow::Owned(vec![1]),
222 memo: memo.map(|m| Cow::Owned(m.to_string())),
223 }
224 })
225 .collect();
226
227 let mt_event = MtEvent::MtTransfer(Cow::Owned(events));
228 let log_len = mt_event.to_nep297_event().to_event_log().len();
229 assert_eq!(
230 log_len, length,
231 "Expected log length {length}, got {log_len}"
232 );
233
234 mt_event
235 }
236
237 #[test]
238 fn single_event_no_memo_at_limit_minus_overhead_passes() {
239 let mt = create_single_event_mt(
240 defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT - REFUND_EXTRA_BYTES,
241 None,
242 );
243 assert!(mt.check_refund().is_ok());
244 }
245
246 #[test]
247 fn single_event_short_memo_at_limit_fails() {
248 let memo = "refu";
249 let mt = create_single_event_mt(defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT, Some(memo));
250 assert!(matches!(mt.check_refund().unwrap_err(), ErrorLogTooLong));
251 }
252
253 #[test]
254 fn triple_event_no_memo_at_limit_minus_overhead_passes() {
255 let mt = create_triple_event_mt(
256 defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT - 3 * REFUND_EXTRA_BYTES,
257 [None; 3],
258 );
259 assert!(mt.check_refund().is_ok());
260 }
261
262 #[test]
263 fn triple_event_short_memo_at_limit_fails() {
264 let mt =
265 create_triple_event_mt(defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT, [Some("refu"); 3]);
266 assert!(matches!(mt.check_refund().unwrap_err(), ErrorLogTooLong));
267 }
268
269 #[test]
270 fn triple_event_mixed_memos_overhead_equals_savings_at_limit_passes() {
271 let long_memo = "x".repeat(REFUND_EXTRA_BYTES + REFUND_STR_LEN);
279 assert_eq!(long_memo.len() - REFUND_STR_LEN, REFUND_EXTRA_BYTES);
280
281 let mt = create_triple_event_mt(
282 defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT,
283 [None, Some("refund"), Some(&long_memo)],
284 );
285 assert!(mt.check_refund().is_ok());
286 }
287}