defuse/tokens/
mod.rs

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