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 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 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 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 let msg = DepositMessage::new("alice.near".parse().unwrap());
153 let json = serde_json::to_string(&msg).unwrap();
154
155 assert!(json.contains("\"receiver_id\":\"alice.near\""));
157 assert!(!json.contains("action"));
158 }
159
160 #[test]
161 fn test_serialize_with_notify() {
162 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 assert!(json.contains("\"receiver_id\":\"alice.near\""));
173 assert!(json.contains("\"msg\":\"hello\""));
174 }
175
176 #[test]
177 fn test_serialize_with_execute() {
178 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 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 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 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 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 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 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 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 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 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}