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