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