1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::error::{Error, Result};
10use crate::message::agent::TapParticipant;
11use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
12use crate::message::{Agent, Party};
13use crate::TapMessage;
14
15#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
21#[tap(
22 message_type = "https://tap.rsvp/schema/1.0#Exchange",
23 initiator,
24 authorizable,
25 transactable
26)]
27pub struct Exchange {
28 #[serde(rename = "fromAssets")]
30 pub from_assets: Vec<String>,
31
32 #[serde(rename = "toAssets")]
34 pub to_assets: Vec<String>,
35
36 #[serde(rename = "fromAmount", skip_serializing_if = "Option::is_none")]
38 pub from_amount: Option<String>,
39
40 #[serde(rename = "toAmount", skip_serializing_if = "Option::is_none")]
42 pub to_amount: Option<String>,
43
44 #[tap(participant)]
46 pub requester: Party,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 #[tap(participant)]
51 pub provider: Option<Party>,
52
53 #[serde(default)]
55 #[tap(participant_list)]
56 pub agents: Vec<Agent>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub policies: Option<Vec<serde_json::Value>>,
61
62 #[serde(skip)]
64 #[tap(transaction_id)]
65 pub transaction_id: Option<String>,
66
67 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
69 pub metadata: HashMap<String, serde_json::Value>,
70}
71
72impl Exchange {
73 pub fn new_from(
75 from_assets: Vec<String>,
76 to_assets: Vec<String>,
77 from_amount: String,
78 requester: Party,
79 agents: Vec<Agent>,
80 ) -> Self {
81 Self {
82 from_assets,
83 to_assets,
84 from_amount: Some(from_amount),
85 to_amount: None,
86 requester,
87 provider: None,
88 agents,
89 policies: None,
90 transaction_id: None,
91 metadata: HashMap::new(),
92 }
93 }
94
95 pub fn new_to(
97 from_assets: Vec<String>,
98 to_assets: Vec<String>,
99 to_amount: String,
100 requester: Party,
101 agents: Vec<Agent>,
102 ) -> Self {
103 Self {
104 from_assets,
105 to_assets,
106 from_amount: None,
107 to_amount: Some(to_amount),
108 requester,
109 provider: None,
110 agents,
111 policies: None,
112 transaction_id: None,
113 metadata: HashMap::new(),
114 }
115 }
116
117 pub fn with_provider(mut self, provider: Party) -> Self {
119 self.provider = Some(provider);
120 self
121 }
122
123 pub fn with_policies(mut self, policies: Vec<serde_json::Value>) -> Self {
125 self.policies = Some(policies);
126 self
127 }
128
129 pub fn validate(&self) -> Result<()> {
131 if self.from_assets.is_empty() {
132 return Err(Error::Validation(
133 "fromAssets must not be empty".to_string(),
134 ));
135 }
136 if self.to_assets.is_empty() {
137 return Err(Error::Validation("toAssets must not be empty".to_string()));
138 }
139 if self.from_amount.is_none() && self.to_amount.is_none() {
140 return Err(Error::Validation(
141 "Either fromAmount or toAmount must be provided".to_string(),
142 ));
143 }
144 if self.requester.id().is_empty() {
145 return Err(Error::Validation(
146 "Requester ID cannot be empty".to_string(),
147 ));
148 }
149 Ok(())
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
158#[tap(message_type = "https://tap.rsvp/schema/1.0#Quote")]
159pub struct Quote {
160 #[serde(rename = "fromAsset")]
162 pub from_asset: String,
163
164 #[serde(rename = "toAsset")]
166 pub to_asset: String,
167
168 #[serde(rename = "fromAmount")]
170 pub from_amount: String,
171
172 #[serde(rename = "toAmount")]
174 pub to_amount: String,
175
176 #[tap(participant)]
178 pub provider: Party,
179
180 #[serde(default)]
182 #[tap(participant_list)]
183 pub agents: Vec<Agent>,
184
185 pub expires: String,
187
188 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
190 pub metadata: HashMap<String, serde_json::Value>,
191}
192
193impl Quote {
194 pub fn new(
196 from_asset: String,
197 to_asset: String,
198 from_amount: String,
199 to_amount: String,
200 provider: Party,
201 agents: Vec<Agent>,
202 expires: String,
203 ) -> Self {
204 Self {
205 from_asset,
206 to_asset,
207 from_amount,
208 to_amount,
209 provider,
210 agents,
211 expires,
212 metadata: HashMap::new(),
213 }
214 }
215
216 pub fn validate(&self) -> Result<()> {
218 if self.from_asset.is_empty() {
219 return Err(Error::Validation("fromAsset must not be empty".to_string()));
220 }
221 if self.to_asset.is_empty() {
222 return Err(Error::Validation("toAsset must not be empty".to_string()));
223 }
224 if self.from_amount.is_empty() {
225 return Err(Error::Validation(
226 "fromAmount must not be empty".to_string(),
227 ));
228 }
229 if self.to_amount.is_empty() {
230 return Err(Error::Validation("toAmount must not be empty".to_string()));
231 }
232 if self.provider.id().is_empty() {
233 return Err(Error::Validation("Provider ID cannot be empty".to_string()));
234 }
235 if self.expires.is_empty() {
236 return Err(Error::Validation("expires must not be empty".to_string()));
237 }
238 Ok(())
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use serde_json;
246
247 #[test]
248 fn test_exchange_creation() {
249 let exchange = Exchange::new_from(
250 vec!["eip155:1/erc20:0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string()],
251 vec!["eip155:1/erc20:0xB00b00b00b00b00b00b00b00b00b00b00b00b00b".to_string()],
252 "1000.00".to_string(),
253 Party::new("did:web:business.example"),
254 vec![Agent::new_without_role(
255 "did:web:wallet.example",
256 "did:web:business.example",
257 )],
258 )
259 .with_provider(Party::new("did:web:liquidity.provider"));
260
261 assert_eq!(exchange.from_assets.len(), 1);
262 assert_eq!(exchange.to_assets.len(), 1);
263 assert_eq!(exchange.from_amount, Some("1000.00".to_string()));
264 assert!(exchange.to_amount.is_none());
265 assert!(exchange.provider.is_some());
266 assert!(exchange.validate().is_ok());
267 }
268
269 #[test]
270 fn test_exchange_serialization() {
271 let exchange = Exchange::new_from(
272 vec!["USD".to_string()],
273 vec!["eip155:1/erc20:0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string()],
274 "1000.00".to_string(),
275 Party::new("did:web:user.entity"),
276 vec![Agent::new_without_role(
277 "did:web:user.wallet",
278 "did:web:user.entity",
279 )],
280 );
281
282 let json = serde_json::to_value(&exchange).unwrap();
283 assert_eq!(json["fromAssets"][0], "USD");
284 assert_eq!(json["fromAmount"], "1000.00");
285 assert!(json.get("toAmount").is_none());
286
287 let deserialized: Exchange = serde_json::from_value(json).unwrap();
288 assert_eq!(deserialized.from_assets, exchange.from_assets);
289 }
290
291 #[test]
292 fn test_exchange_validation_no_amount() {
293 let exchange = Exchange {
294 from_assets: vec!["USD".to_string()],
295 to_assets: vec!["EUR".to_string()],
296 from_amount: None,
297 to_amount: None,
298 requester: Party::new("did:example:user"),
299 provider: None,
300 agents: vec![],
301 policies: None,
302 transaction_id: None,
303 metadata: HashMap::new(),
304 };
305
306 let result = exchange.validate();
307 assert!(result.is_err());
308 assert!(result
309 .unwrap_err()
310 .to_string()
311 .contains("Either fromAmount or toAmount"));
312 }
313
314 #[test]
315 fn test_quote_creation() {
316 let quote = Quote::new(
317 "eip155:1/erc20:0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
318 "eip155:1/erc20:0xB00b00b00b00b00b00b00b00b00b00b00b00b00b".to_string(),
319 "1000.00".to_string(),
320 "908.50".to_string(),
321 Party::new("did:web:liquidity.provider"),
322 vec![
323 Agent::new_without_role("did:web:wallet.example", "did:web:business.example"),
324 Agent::new_without_role("did:web:lp.example", "did:web:liquidity.provider"),
325 ],
326 "2025-07-21T00:00:00Z".to_string(),
327 );
328
329 assert_eq!(quote.from_amount, "1000.00");
330 assert_eq!(quote.to_amount, "908.50");
331 assert_eq!(quote.agents.len(), 2);
332 assert!(quote.validate().is_ok());
333 }
334
335 #[test]
336 fn test_quote_serialization() {
337 let quote = Quote::new(
338 "USD".to_string(),
339 "eip155:1/erc20:0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
340 "1000.00".to_string(),
341 "996.00".to_string(),
342 Party::new("did:web:onramp.company"),
343 vec![],
344 "2025-07-21T00:00:00Z".to_string(),
345 );
346
347 let json = serde_json::to_value("e).unwrap();
348 assert_eq!(json["fromAsset"], "USD");
349 assert_eq!(json["toAmount"], "996.00");
350 assert_eq!(json["expires"], "2025-07-21T00:00:00Z");
351
352 let deserialized: Quote = serde_json::from_value(json).unwrap();
353 assert_eq!(deserialized.from_amount, quote.from_amount);
354 }
355}