1#[cfg(feature = "imt")]
2pub mod imt;
3pub mod nep141;
4pub mod nep171;
5pub mod nep245;
6
7use core::{
8 fmt::{self, Debug, Display},
9 str::FromStr,
10};
11
12use defuse_core::{intents::tokens::NotifyOnTransfer, payload::multi::MultiPayload};
13use defuse_near_utils::UnwrapOrPanicError;
14use near_sdk::{AccountId, account_id::ParseAccountError, near, serde_json};
15use thiserror::Error as ThisError;
16
17#[must_use]
18#[near(serializers = [json])]
19#[derive(Debug, Clone)]
20pub struct DepositMessage {
21 pub receiver_id: AccountId,
22
23 #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
24 pub action: Option<DepositAction>,
25}
26
27impl DepositMessage {
28 #[inline]
29 pub const fn new(receiver_id: AccountId) -> Self {
30 Self {
31 receiver_id,
32 action: None,
33 }
34 }
35
36 #[inline]
37 pub fn with_action(mut self, action: impl Into<Option<DepositAction>>) -> Self {
38 self.action = action.into();
39 self
40 }
41}
42
43impl Display for DepositMessage {
44 #[inline]
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match &self.action {
47 None => f.write_str(self.receiver_id.as_str()),
48 Some(DepositAction::Execute(exec)) if exec.execute_intents.is_empty() => {
49 f.write_str(self.receiver_id.as_str())
50 }
51 Some(_) => f.write_str(&serde_json::to_string(self).unwrap_or_panic_display()),
52 }
53 }
54}
55
56impl FromStr for DepositMessage {
57 type Err = ParseDepositMessageError;
58
59 #[inline]
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 if s.starts_with('{') {
62 serde_json::from_str(s).map_err(Into::into)
63 } else {
64 s.parse().map(Self::new).map_err(Into::into)
65 }
66 }
67}
68
69#[must_use]
70#[near(serializers = [json])]
71#[serde(untagged)]
72#[derive(Debug, Clone)]
73pub enum DepositAction {
74 Execute(ExecuteIntents),
75 Notify(NotifyOnTransfer),
76}
77
78#[must_use]
79#[near(serializers = [json])]
80#[derive(Debug, Clone)]
81pub struct ExecuteIntents {
82 pub execute_intents: Vec<MultiPayload>,
83
84 #[serde(default, skip_serializing_if = "::core::ops::Not::not")]
85 pub refund_if_fails: bool,
86}
87
88#[derive(Debug, ThisError)]
89pub enum ParseDepositMessageError {
90 #[error(transparent)]
91 Account(#[from] ParseAccountError),
92 #[error("JSON: {0}")]
93 JSON(#[from] serde_json::Error),
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn test_deserialize_simple() {
102 let json = r#"{"receiver_id": "alice.near"}"#;
104 let msg: DepositMessage = serde_json::from_str(json).unwrap();
105
106 assert_eq!(msg.receiver_id.as_str(), "alice.near");
107 assert!(msg.action.is_none());
108 }
109
110 #[test]
111 fn test_deserialize_with_notify() {
112 let json = r#"{
114 "receiver_id": "alice.near",
115 "msg": "hello world",
116 "min_gas": null
117 }"#;
118 let msg: DepositMessage = serde_json::from_str(json).unwrap();
119
120 assert_eq!(msg.receiver_id.as_str(), "alice.near");
121 match msg.action {
122 Some(DepositAction::Notify(notify)) => {
123 assert_eq!(notify.msg, "hello world");
124 assert!(notify.min_gas.is_none());
125 }
126 _ => panic!("Expected Notify action"),
127 }
128 }
129
130 #[test]
131 fn test_deserialize_with_execute() {
132 let json = r#"{
134 "receiver_id": "alice.near",
135 "execute_intents": [],
136 "refund_if_fails": true
137 }"#;
138 let msg: DepositMessage = serde_json::from_str(json).unwrap();
139
140 assert_eq!(msg.receiver_id.as_str(), "alice.near");
141 match msg.action {
142 Some(DepositAction::Execute(exec)) => {
143 assert!(exec.execute_intents.is_empty());
144 assert!(exec.refund_if_fails);
145 }
146 _ => panic!("Expected Execute action"),
147 }
148 }
149
150 #[test]
151 fn test_serialize_simple() {
152 let msg = DepositMessage::new("alice.near".parse().unwrap());
154 let json = serde_json::to_string(&msg).unwrap();
155
156 assert!(json.contains("\"receiver_id\":\"alice.near\""));
158 assert!(!json.contains("action"));
159 }
160
161 #[test]
162 fn test_serialize_with_notify() {
163 let msg = DepositMessage {
165 receiver_id: "alice.near".parse().unwrap(),
166 action: Some(DepositAction::Notify(NotifyOnTransfer::new(
167 "hello".to_string(),
168 ))),
169 };
170 let json = serde_json::to_string(&msg).unwrap();
171
172 assert!(json.contains("\"receiver_id\":\"alice.near\""));
174 assert!(json.contains("\"msg\":\"hello\""));
175 }
176
177 #[test]
178 fn test_serialize_with_execute() {
179 let msg = DepositMessage {
181 receiver_id: "alice.near".parse().unwrap(),
182 action: Some(DepositAction::Execute(ExecuteIntents {
183 execute_intents: vec![],
184 refund_if_fails: true,
185 })),
186 };
187 let json = serde_json::to_string(&msg).unwrap();
188
189 assert!(json.contains("\"receiver_id\":\"alice.near\""));
191 assert!(json.contains("\"execute_intents\""));
192 assert!(json.contains("\"refund_if_fails\":true"));
193 }
194
195 #[test]
196 fn test_display_simple() {
197 let msg = DepositMessage::new("alice.near".parse().unwrap());
199 assert_eq!(msg.to_string(), "alice.near");
200 }
201
202 #[test]
203 fn test_display_with_action() {
204 let msg = DepositMessage {
206 receiver_id: "alice.near".parse().unwrap(),
207 action: Some(DepositAction::Notify(NotifyOnTransfer::new(
208 "test".to_string(),
209 ))),
210 };
211 let display = msg.to_string();
212
213 assert!(display.starts_with('{'));
214 assert!(display.contains("alice.near"));
215 }
216
217 #[test]
218 fn test_from_str_simple() {
219 let msg: DepositMessage = "alice.near".parse().unwrap();
221 assert_eq!(msg.receiver_id.as_str(), "alice.near");
222 assert!(msg.action.is_none());
223 }
224
225 #[test]
226 fn test_from_str_json_with_execute() {
227 let json = r#"{"receiver_id":"alice.near","execute_intents":[],"refund_if_fails":true}"#;
229 let msg: DepositMessage = json.parse().unwrap();
230
231 assert_eq!(msg.receiver_id.as_str(), "alice.near");
232 match msg.action {
233 Some(DepositAction::Execute(exec)) => {
234 assert!(exec.execute_intents.is_empty());
235 assert!(exec.refund_if_fails);
236 }
237 _ => panic!("Expected Execute action"),
238 }
239 }
240
241 #[test]
242 fn test_from_str_json_with_notify() {
243 let json = r#"{"receiver_id":"alice.near","msg":"test"}"#;
245 let msg: DepositMessage = json.parse().unwrap();
246
247 assert_eq!(msg.receiver_id.as_str(), "alice.near");
248 match msg.action {
249 Some(DepositAction::Notify(notify)) => {
250 assert_eq!(notify.msg, "test");
251 }
252 _ => panic!("Expected Notify action"),
253 }
254 }
255
256 #[test]
257 fn test_deserialize_execute_takes_precedence_when_both_fields_present() {
258 let json = r#"{
261 "receiver_id": "alice.near",
262 "execute_intents": [],
263 "refund_if_fails": true,
264 "msg": "this should be ignored"
265 }"#;
266 let deposit_msg: DepositMessage = serde_json::from_str(json).unwrap();
267
268 assert_eq!(deposit_msg.receiver_id.as_str(), "alice.near");
269 match deposit_msg.action {
270 Some(DepositAction::Execute(exec)) => {
271 assert!(exec.execute_intents.is_empty());
272 assert!(exec.refund_if_fails);
273 }
274 Some(DepositAction::Notify(_)) => {
275 panic!("Expected Execute action, got Notify instead");
276 }
277 None => panic!("Expected Execute action, got None"),
278 }
279 }
280
281 #[test]
282 fn test_builder_methods() {
283 let msg = DepositMessage {
285 receiver_id: "alice.near".parse().unwrap(),
286 action: Some(DepositAction::Execute(ExecuteIntents {
287 execute_intents: vec![],
288 refund_if_fails: true,
289 })),
290 };
291
292 assert_eq!(msg.receiver_id.as_str(), "alice.near");
293 match msg.action {
294 Some(DepositAction::Execute(exec)) => {
295 assert!(exec.refund_if_fails);
296 }
297 _ => panic!("Expected Execute action"),
298 }
299 }
300
301 #[test]
302 fn test_builder_with_notify() {
303 let msg = DepositMessage {
305 receiver_id: "alice.near".parse().unwrap(),
306 action: Some(DepositAction::Notify(NotifyOnTransfer::new(
307 "test".to_string(),
308 ))),
309 };
310
311 assert_eq!(msg.receiver_id.as_str(), "alice.near");
312 assert!(matches!(msg.action, Some(DepositAction::Notify(_))));
313 }
314}