1use crate::error::{Error, Result};
7use crate::message::agent::Agent;
8use crate::message::party::Party;
9use crate::message::tap_message_trait::TapMessageBody;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19#[serde(rename_all = "camelCase")]
20pub struct Escrow {
21 #[serde(skip_serializing_if = "Option::is_none")]
24 pub asset: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
29 pub currency: Option<String>,
30
31 pub amount: String,
33
34 pub originator: Party,
36
37 pub beneficiary: Party,
39
40 pub expiry: String,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub agreement: Option<String>,
46
47 pub agents: Vec<Agent>,
49
50 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
52 pub metadata: HashMap<String, Value>,
53}
54
55impl Escrow {
56 pub fn new_with_asset(
58 asset: String,
59 amount: String,
60 originator: Party,
61 beneficiary: Party,
62 expiry: String,
63 agents: Vec<Agent>,
64 ) -> Self {
65 Self {
66 asset: Some(asset),
67 currency: None,
68 amount,
69 originator,
70 beneficiary,
71 expiry,
72 agreement: None,
73 agents,
74 metadata: HashMap::new(),
75 }
76 }
77
78 pub fn new_with_currency(
80 currency: String,
81 amount: String,
82 originator: Party,
83 beneficiary: Party,
84 expiry: String,
85 agents: Vec<Agent>,
86 ) -> Self {
87 Self {
88 asset: None,
89 currency: Some(currency),
90 amount,
91 originator,
92 beneficiary,
93 expiry,
94 agreement: None,
95 agents,
96 metadata: HashMap::new(),
97 }
98 }
99
100 pub fn with_agreement(mut self, agreement: String) -> Self {
102 self.agreement = Some(agreement);
103 self
104 }
105
106 pub fn with_metadata(mut self, key: String, value: Value) -> Self {
108 self.metadata.insert(key, value);
109 self
110 }
111
112 pub fn escrow_agent(&self) -> Option<&Agent> {
114 self.agents
115 .iter()
116 .find(|a| a.role == Some("EscrowAgent".to_string()))
117 }
118
119 pub fn authorizing_agents(&self) -> Vec<&Agent> {
121 self.agents
122 .iter()
123 .filter(|a| a.for_parties.0.contains(&self.beneficiary.id))
124 .collect()
125 }
126}
127
128impl TapMessageBody for Escrow {
129 fn message_type() -> &'static str {
130 "https://tap.rsvp/schema/1.0#Escrow"
131 }
132
133 fn validate(&self) -> Result<()> {
134 match (&self.asset, &self.currency) {
136 (Some(_), Some(_)) => {
137 return Err(Error::Validation(
138 "Escrow cannot have both asset and currency specified".to_string(),
139 ));
140 }
141 (None, None) => {
142 return Err(Error::Validation(
143 "Escrow must have either asset or currency specified".to_string(),
144 ));
145 }
146 _ => {}
147 }
148
149 if self.amount.is_empty() {
151 return Err(Error::Validation(
152 "Escrow amount cannot be empty".to_string(),
153 ));
154 }
155
156 if self.expiry.is_empty() {
158 return Err(Error::Validation(
159 "Escrow expiry cannot be empty".to_string(),
160 ));
161 }
162
163 let escrow_agent_count = self
165 .agents
166 .iter()
167 .filter(|a| a.role == Some("EscrowAgent".to_string()))
168 .count();
169
170 if escrow_agent_count == 0 {
171 return Err(Error::Validation(
172 "Escrow must have exactly one agent with role 'EscrowAgent'".to_string(),
173 ));
174 }
175
176 if escrow_agent_count > 1 {
177 return Err(Error::Validation(
178 "Escrow cannot have more than one agent with role 'EscrowAgent'".to_string(),
179 ));
180 }
181
182 if self.originator.id == self.beneficiary.id {
184 return Err(Error::Validation(
185 "Escrow originator and beneficiary must be different parties".to_string(),
186 ));
187 }
188
189 Ok(())
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198#[serde(rename_all = "camelCase")]
199pub struct Capture {
200 #[serde(skip_serializing_if = "Option::is_none")]
203 pub amount: Option<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub settlement_address: Option<String>,
208
209 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
211 pub metadata: HashMap<String, Value>,
212}
213
214impl Capture {
215 pub fn new() -> Self {
217 Self {
218 amount: None,
219 settlement_address: None,
220 metadata: HashMap::new(),
221 }
222 }
223
224 pub fn with_amount(amount: String) -> Self {
226 Self {
227 amount: Some(amount),
228 settlement_address: None,
229 metadata: HashMap::new(),
230 }
231 }
232
233 pub fn with_settlement_address(mut self, address: String) -> Self {
235 self.settlement_address = Some(address);
236 self
237 }
238
239 pub fn with_metadata(mut self, key: String, value: Value) -> Self {
241 self.metadata.insert(key, value);
242 self
243 }
244}
245
246impl Default for Capture {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl TapMessageBody for Capture {
253 fn message_type() -> &'static str {
254 "https://tap.rsvp/schema/1.0#Capture"
255 }
256
257 fn validate(&self) -> Result<()> {
258 if let Some(ref amount) = self.amount {
260 if amount.is_empty() {
261 return Err(Error::Validation(
262 "Capture amount cannot be empty".to_string(),
263 ));
264 }
265 }
266
267 if let Some(ref address) = self.settlement_address {
269 if address.is_empty() {
270 return Err(Error::Validation(
271 "Capture settlement_address cannot be empty".to_string(),
272 ));
273 }
274 }
275
276 Ok(())
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_escrow_with_asset() {
286 let originator = Party::new("did:example:alice");
287 let beneficiary = Party::new("did:example:bob");
288 let agent1 = Agent::new(
289 "did:example:alice-wallet",
290 "OriginatorAgent",
291 "did:example:alice",
292 );
293 let agent2 = Agent::new(
294 "did:example:bob-wallet",
295 "BeneficiaryAgent",
296 "did:example:bob",
297 );
298 let escrow_agent = Agent::new(
299 "did:example:escrow-service",
300 "EscrowAgent",
301 "did:example:escrow-service",
302 );
303
304 let escrow = Escrow::new_with_asset(
305 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
306 "100.00".to_string(),
307 originator,
308 beneficiary,
309 "2025-06-25T00:00:00Z".to_string(),
310 vec![agent1, agent2, escrow_agent],
311 );
312
313 assert!(escrow.validate().is_ok());
314 assert!(escrow.escrow_agent().is_some());
315 assert_eq!(
316 escrow.escrow_agent().unwrap().role,
317 Some("EscrowAgent".to_string())
318 );
319 }
320
321 #[test]
322 fn test_escrow_with_currency() {
323 let originator = Party::new("did:example:buyer");
324 let beneficiary = Party::new("did:example:seller");
325 let escrow_agent = Agent::new(
326 "did:example:escrow-bank",
327 "EscrowAgent",
328 "did:example:escrow-bank",
329 );
330
331 let escrow = Escrow::new_with_currency(
332 "USD".to_string(),
333 "500.00".to_string(),
334 originator,
335 beneficiary,
336 "2025-07-01T00:00:00Z".to_string(),
337 vec![escrow_agent],
338 )
339 .with_agreement("https://marketplace.example/purchase/98765".to_string());
340
341 assert!(escrow.validate().is_ok());
342 assert_eq!(escrow.currency, Some("USD".to_string()));
343 assert_eq!(
344 escrow.agreement,
345 Some("https://marketplace.example/purchase/98765".to_string())
346 );
347 }
348
349 #[test]
350 fn test_escrow_validation_errors() {
351 let originator = Party::new("did:example:alice");
352 let beneficiary = Party::new("did:example:bob");
353
354 let escrow_no_agent = Escrow::new_with_asset(
356 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
357 "100.00".to_string(),
358 originator.clone(),
359 beneficiary.clone(),
360 "2025-06-25T00:00:00Z".to_string(),
361 vec![],
362 );
363 assert!(escrow_no_agent.validate().is_err());
364
365 let mut escrow_both = Escrow::new_with_asset(
367 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
368 "100.00".to_string(),
369 originator.clone(),
370 beneficiary.clone(),
371 "2025-06-25T00:00:00Z".to_string(),
372 vec![Agent::new(
373 "did:example:escrow",
374 "EscrowAgent",
375 "did:example:escrow",
376 )],
377 );
378 escrow_both.currency = Some("USD".to_string());
379 assert!(escrow_both.validate().is_err());
380
381 let escrow_same_party = Escrow::new_with_currency(
383 "USD".to_string(),
384 "100.00".to_string(),
385 originator.clone(),
386 originator.clone(),
387 "2025-06-25T00:00:00Z".to_string(),
388 vec![Agent::new(
389 "did:example:escrow",
390 "EscrowAgent",
391 "did:example:escrow",
392 )],
393 );
394 assert!(escrow_same_party.validate().is_err());
395 }
396
397 #[test]
398 fn test_capture() {
399 let capture = Capture::new();
400 assert!(capture.validate().is_ok());
401 assert!(capture.amount.is_none());
402 assert!(capture.settlement_address.is_none());
403
404 let capture_with_amount = Capture::with_amount("95.00".to_string())
405 .with_settlement_address(
406 "eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f1234".to_string(),
407 );
408 assert!(capture_with_amount.validate().is_ok());
409 assert_eq!(capture_with_amount.amount, Some("95.00".to_string()));
410 assert_eq!(
411 capture_with_amount.settlement_address,
412 Some("eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f1234".to_string())
413 );
414 }
415
416 #[test]
417 fn test_capture_validation_errors() {
418 let mut capture = Capture::new();
419 capture.amount = Some("".to_string());
420 assert!(capture.validate().is_err());
421
422 let mut capture2 = Capture::new();
423 capture2.settlement_address = Some("".to_string());
424 assert!(capture2.validate().is_err());
425 }
426}