1use crate::error::{Error, Result};
14use crate::message::agent::Agent;
15use crate::message::party::Party;
16use crate::message::tap_message_trait::TapMessageBody;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27#[serde(rename_all = "camelCase")]
28pub struct Lock {
29 #[serde(skip_serializing_if = "Option::is_none")]
32 pub asset: Option<String>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
37 pub currency: Option<String>,
38
39 pub amount: String,
41
42 pub originator: Party,
44
45 pub beneficiary: Party,
47
48 pub expiry: String,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub agreement: Option<String>,
55
56 pub agents: Vec<Agent>,
58
59 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
61 pub metadata: HashMap<String, Value>,
62}
63
64pub type Escrow = Lock;
66
67impl Lock {
68 pub fn new_with_asset(
70 asset: String,
71 amount: String,
72 originator: Party,
73 beneficiary: Party,
74 expiry: String,
75 agents: Vec<Agent>,
76 ) -> Self {
77 Self {
78 asset: Some(asset),
79 currency: None,
80 amount,
81 originator,
82 beneficiary,
83 expiry,
84 agreement: None,
85 agents,
86 metadata: HashMap::new(),
87 }
88 }
89
90 pub fn new_with_currency(
92 currency: String,
93 amount: String,
94 originator: Party,
95 beneficiary: Party,
96 expiry: String,
97 agents: Vec<Agent>,
98 ) -> Self {
99 Self {
100 asset: None,
101 currency: Some(currency),
102 amount,
103 originator,
104 beneficiary,
105 expiry,
106 agreement: None,
107 agents,
108 metadata: HashMap::new(),
109 }
110 }
111
112 pub fn with_agreement(mut self, agreement: String) -> Self {
114 self.agreement = Some(agreement);
115 self
116 }
117
118 pub fn with_metadata(mut self, key: String, value: Value) -> Self {
120 self.metadata.insert(key, value);
121 self
122 }
123
124 pub fn escrow_agent(&self) -> Option<&Agent> {
127 self.agents
128 .iter()
129 .find(|a| a.role == Some("EscrowAgent".to_string()))
130 }
131
132 pub fn authorizing_agents(&self) -> Vec<&Agent> {
134 self.agents
135 .iter()
136 .filter(|a| a.for_parties.0.contains(&self.beneficiary.id))
137 .collect()
138 }
139}
140
141impl TapMessageBody for Lock {
142 fn message_type() -> &'static str {
143 "https://tap.rsvp/schema/1.0#Lock"
144 }
145
146 fn validate(&self) -> Result<()> {
147 match (&self.asset, &self.currency) {
148 (Some(_), Some(_)) => {
149 return Err(Error::Validation(
150 "Lock cannot have both asset and currency specified".to_string(),
151 ));
152 }
153 (None, None) => {
154 return Err(Error::Validation(
155 "Lock must have either asset or currency specified".to_string(),
156 ));
157 }
158 _ => {}
159 }
160
161 if self.amount.is_empty() {
162 return Err(Error::Validation("Lock amount cannot be empty".to_string()));
163 }
164
165 if self.expiry.is_empty() {
166 return Err(Error::Validation("Lock expiry cannot be empty".to_string()));
167 }
168
169 let escrow_agent_count = self
170 .agents
171 .iter()
172 .filter(|a| a.role == Some("EscrowAgent".to_string()))
173 .count();
174
175 if escrow_agent_count == 0 {
176 return Err(Error::Validation(
177 "Lock must have exactly one agent with role 'EscrowAgent'".to_string(),
178 ));
179 }
180
181 if escrow_agent_count > 1 {
182 return Err(Error::Validation(
183 "Lock cannot have more than one agent with role 'EscrowAgent'".to_string(),
184 ));
185 }
186
187 if self.originator.id == self.beneficiary.id {
188 return Err(Error::Validation(
189 "Lock originator and beneficiary must be different parties".to_string(),
190 ));
191 }
192
193 Ok(())
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202#[serde(rename_all = "camelCase")]
203pub struct Capture {
204 #[serde(skip_serializing_if = "Option::is_none")]
207 pub amount: Option<String>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
212 pub settlement_address: Option<String>,
213
214 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
216 pub metadata: HashMap<String, Value>,
217}
218
219impl Capture {
220 pub fn new() -> Self {
222 Self {
223 amount: None,
224 settlement_address: None,
225 metadata: HashMap::new(),
226 }
227 }
228
229 pub fn with_amount(amount: String) -> Self {
231 Self {
232 amount: Some(amount),
233 settlement_address: None,
234 metadata: HashMap::new(),
235 }
236 }
237
238 pub fn with_settlement_address(mut self, address: String) -> Self {
240 self.settlement_address = Some(address);
241 self
242 }
243
244 pub fn with_metadata(mut self, key: String, value: Value) -> Self {
246 self.metadata.insert(key, value);
247 self
248 }
249}
250
251impl Default for Capture {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257impl TapMessageBody for Capture {
258 fn message_type() -> &'static str {
259 "https://tap.rsvp/schema/1.0#Capture"
260 }
261
262 fn validate(&self) -> Result<()> {
263 if let Some(ref amount) = self.amount {
264 if amount.is_empty() {
265 return Err(Error::Validation(
266 "Capture amount cannot be empty".to_string(),
267 ));
268 }
269 }
270
271 if let Some(ref address) = self.settlement_address {
272 if address.is_empty() {
273 return Err(Error::Validation(
274 "Capture settlement_address cannot be empty".to_string(),
275 ));
276 }
277 }
278
279 Ok(())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_lock_with_asset() {
289 let originator = Party::new("did:example:alice");
290 let beneficiary = Party::new("did:example:bob");
291 let agent1 = Agent::new(
292 "did:example:alice-wallet",
293 "OriginatorAgent",
294 "did:example:alice",
295 );
296 let agent2 = Agent::new(
297 "did:example:bob-wallet",
298 "BeneficiaryAgent",
299 "did:example:bob",
300 );
301 let escrow_agent = Agent::new(
302 "did:example:escrow-service",
303 "EscrowAgent",
304 "did:example:escrow-service",
305 );
306
307 let lock = Lock::new_with_asset(
308 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
309 "100.00".to_string(),
310 originator,
311 beneficiary,
312 "2025-06-25T00:00:00Z".to_string(),
313 vec![agent1, agent2, escrow_agent],
314 );
315
316 assert!(lock.validate().is_ok());
317 assert!(lock.escrow_agent().is_some());
318 assert_eq!(
319 lock.escrow_agent().unwrap().role,
320 Some("EscrowAgent".to_string())
321 );
322 }
323
324 #[test]
325 fn test_lock_with_currency() {
326 let originator = Party::new("did:example:buyer");
327 let beneficiary = Party::new("did:example:seller");
328 let escrow_agent = Agent::new(
329 "did:example:escrow-bank",
330 "EscrowAgent",
331 "did:example:escrow-bank",
332 );
333
334 let lock = Lock::new_with_currency(
335 "USD".to_string(),
336 "500.00".to_string(),
337 originator,
338 beneficiary,
339 "2025-07-01T00:00:00Z".to_string(),
340 vec![escrow_agent],
341 )
342 .with_agreement("https://marketplace.example/purchase/98765".to_string());
343
344 assert!(lock.validate().is_ok());
345 assert_eq!(lock.currency, Some("USD".to_string()));
346 assert_eq!(
347 lock.agreement,
348 Some("https://marketplace.example/purchase/98765".to_string())
349 );
350 }
351
352 #[test]
353 fn test_lock_validation_errors() {
354 let originator = Party::new("did:example:alice");
355 let beneficiary = Party::new("did:example:bob");
356
357 let lock_no_agent = Lock::new_with_asset(
358 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
359 "100.00".to_string(),
360 originator.clone(),
361 beneficiary.clone(),
362 "2025-06-25T00:00:00Z".to_string(),
363 vec![],
364 );
365 assert!(lock_no_agent.validate().is_err());
366
367 let mut lock_both = Lock::new_with_asset(
368 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string(),
369 "100.00".to_string(),
370 originator.clone(),
371 beneficiary.clone(),
372 "2025-06-25T00:00:00Z".to_string(),
373 vec![Agent::new(
374 "did:example:escrow",
375 "EscrowAgent",
376 "did:example:escrow",
377 )],
378 );
379 lock_both.currency = Some("USD".to_string());
380 assert!(lock_both.validate().is_err());
381
382 let lock_same_party = Lock::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!(lock_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}