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