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)]
19pub struct ConnectAgent {
20 #[serde(rename = "@id")]
22 pub id: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub name: Option<String>,
27
28 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
30 pub agent_type: Option<String>,
31
32 #[serde(rename = "serviceUrl", skip_serializing_if = "Option::is_none")]
34 pub service_url: Option<String>,
35
36 #[serde(flatten)]
38 pub metadata: HashMap<String, serde_json::Value>,
39}
40
41impl TapParticipant for ConnectAgent {
42 fn id(&self) -> &str {
43 &self.id
44 }
45}
46
47impl ConnectAgent {
48 pub fn new(id: &str) -> Self {
50 Self {
51 id: id.to_string(),
52 name: None,
53 agent_type: None,
54 service_url: None,
55 metadata: HashMap::new(),
56 }
57 }
58
59 pub fn to_agent(&self, for_party: &str) -> Agent {
61 let mut agent = Agent::new_without_role(&self.id, for_party);
62
63 if let Some(name) = &self.name {
65 agent
66 .metadata
67 .insert("name".to_string(), serde_json::Value::String(name.clone()));
68 }
69 if let Some(agent_type) = &self.agent_type {
70 agent.metadata.insert(
71 "type".to_string(),
72 serde_json::Value::String(agent_type.clone()),
73 );
74 }
75 if let Some(service_url) = &self.service_url {
76 agent.metadata.insert(
77 "serviceUrl".to_string(),
78 serde_json::Value::String(service_url.clone()),
79 );
80 }
81
82 for (k, v) in &self.metadata {
84 agent.metadata.insert(k.clone(), v.clone());
85 }
86
87 agent
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TransactionLimits {
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub per_transaction: Option<String>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub per_day: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub per_week: Option<String>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub per_month: Option<String>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub per_year: Option<String>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub currency: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ConnectionConstraints {
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub purposes: Option<Vec<String>>,
125
126 #[serde(rename = "categoryPurposes", skip_serializing_if = "Option::is_none")]
128 pub category_purposes: Option<Vec<String>>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub limits: Option<TransactionLimits>,
133
134 #[serde(
136 rename = "allowedBeneficiaries",
137 skip_serializing_if = "Option::is_none"
138 )]
139 pub allowed_beneficiaries: Option<Vec<Party>>,
140
141 #[serde(
143 rename = "allowedSettlementAddresses",
144 skip_serializing_if = "Option::is_none"
145 )]
146 pub allowed_settlement_addresses: Option<Vec<String>>,
147
148 #[serde(rename = "allowedAssets", skip_serializing_if = "Option::is_none")]
150 pub allowed_assets: Option<Vec<String>>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
155#[tap(
156 message_type = "https://tap.rsvp/schema/1.0#Connect",
157 initiator,
158 authorizable
159)]
160pub struct Connect {
161 #[serde(skip)]
163 #[tap(transaction_id)]
164 pub transaction_id: Option<String>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 #[tap(participant)]
169 pub requester: Option<Party>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 #[tap(participant)]
174 pub principal: Option<Party>,
175
176 #[serde(default)]
178 #[tap(participant_list)]
179 pub agents: Vec<Agent>,
180
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub constraints: Option<ConnectionConstraints>,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub agreement: Option<String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub expiry: Option<String>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
196 pub agent_id: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub agent: Option<ConnectAgent>,
201
202 #[serde(rename = "for", skip_serializing_if = "Option::is_none", default)]
204 pub for_: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub role: Option<String>,
209}
210
211impl Connect {
212 pub fn new_v2(
214 requester: Party,
215 principal: Party,
216 agents: Vec<Agent>,
217 constraints: ConnectionConstraints,
218 ) -> Self {
219 Self {
220 transaction_id: None,
221 requester: Some(requester),
222 principal: Some(principal),
223 agents,
224 constraints: Some(constraints),
225 agreement: None,
226 expiry: None,
227 agent_id: None,
228 agent: None,
229 for_: None,
230 role: None,
231 }
232 }
233
234 pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
236 Self {
237 transaction_id: Some(transaction_id.to_string()),
238 requester: None,
239 principal: None,
240 agents: vec![],
241 constraints: None,
242 agreement: None,
243 expiry: None,
244 agent_id: Some(agent_id.to_string()),
245 agent: None,
246 for_: Some(for_id.to_string()),
247 role: role.map(|s| s.to_string()),
248 }
249 }
250
251 pub fn new_with_agent_and_principal(
253 transaction_id: &str,
254 agent: ConnectAgent,
255 principal: Party,
256 ) -> Self {
257 Self {
258 transaction_id: Some(transaction_id.to_string()),
259 requester: None,
260 principal: Some(principal),
261 agents: vec![],
262 constraints: None,
263 agreement: None,
264 expiry: None,
265 agent_id: None,
266 agent: Some(agent),
267 for_: None,
268 role: None,
269 }
270 }
271
272 pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
274 self.constraints = Some(constraints);
275 self
276 }
277
278 pub fn with_agreement(mut self, agreement: String) -> Self {
280 self.agreement = Some(agreement);
281 self
282 }
283
284 pub fn with_expiry(mut self, expiry: String) -> Self {
286 self.expiry = Some(expiry);
287 self
288 }
289}
290
291impl Connect {
292 pub fn validate_connect(&self) -> Result<()> {
294 if self.requester.is_some() {
296 if self.principal.is_none() {
297 return Err(Error::Validation("principal is required".to_string()));
298 }
299 if self.agents.is_empty() {
300 return Err(Error::Validation(
301 "at least one agent is required".to_string(),
302 ));
303 }
304 if self.constraints.is_none() {
305 return Err(Error::Validation(
306 "Connection request must include constraints".to_string(),
307 ));
308 }
309 return Ok(());
310 }
311
312 if self.agent_id.is_none() && self.agent.is_none() {
314 return Err(Error::Validation(
315 "either agent_id or agent is required".to_string(),
316 ));
317 }
318
319 let for_empty = self.for_.as_ref().is_none_or(|s| s.is_empty());
320 if for_empty && self.principal.is_none() {
321 return Err(Error::Validation(
322 "either for or principal is required".to_string(),
323 ));
324 }
325
326 if self.constraints.is_none() {
327 return Err(Error::Validation(
328 "Connection request must include constraints".to_string(),
329 ));
330 }
331
332 Ok(())
333 }
334
335 pub fn validate(&self) -> Result<()> {
337 self.validate_connect()
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
343#[tap(message_type = "https://tap.rsvp/schema/1.0#OutOfBand")]
344pub struct OutOfBand {
345 pub goal_code: String,
347
348 pub goal: String,
350
351 pub service: String,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub accept: Option<Vec<String>>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub handshake_protocols: Option<Vec<String>>,
361
362 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
364 pub metadata: HashMap<String, serde_json::Value>,
365}
366
367impl OutOfBand {
368 pub fn new(goal_code: String, goal: String, service: String) -> Self {
370 Self {
371 goal_code,
372 goal,
373 service,
374 accept: None,
375 handshake_protocols: None,
376 metadata: HashMap::new(),
377 }
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
385#[tap(message_type = "https://tap.rsvp/schema/1.0#AuthorizationRequired")]
386pub struct AuthorizationRequired {
387 #[serde(rename = "authorizationUrl")]
389 pub authorization_url: String,
390
391 pub expires: String,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub from: Option<String>,
397
398 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
400 pub metadata: HashMap<String, serde_json::Value>,
401}
402
403impl AuthorizationRequired {
404 pub fn new(authorization_url: String, expires: String) -> Self {
406 Self {
407 authorization_url,
408 expires,
409 from: None,
410 metadata: HashMap::new(),
411 }
412 }
413
414 pub fn new_with_from(authorization_url: String, expires: String, from: String) -> Self {
416 Self {
417 authorization_url,
418 expires,
419 from: Some(from),
420 metadata: HashMap::new(),
421 }
422 }
423
424 pub fn with_from(mut self, from: String) -> Self {
426 self.from = Some(from);
427 self
428 }
429
430 pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
432 self.metadata.insert(key.to_string(), value);
433 self
434 }
435}
436
437impl OutOfBand {
438 pub fn validate_out_of_band(&self) -> Result<()> {
440 if self.goal_code.is_empty() {
441 return Err(Error::Validation("Goal code is required".to_string()));
442 }
443
444 if self.service.is_empty() {
445 return Err(Error::Validation("Service is required".to_string()));
446 }
447
448 Ok(())
449 }
450
451 pub fn validate(&self) -> Result<()> {
453 self.validate_out_of_band()
454 }
455}
456
457impl AuthorizationRequired {
458 pub fn validate_authorization_required(&self) -> Result<()> {
460 if self.authorization_url.is_empty() {
461 return Err(Error::Validation(
462 "Authorization URL is required".to_string(),
463 ));
464 }
465
466 if self.expires.is_empty() {
467 return Err(Error::Validation(
468 "Expires timestamp is required".to_string(),
469 ));
470 }
471
472 if !self.expires.contains('T') || !self.expires.contains(':') {
473 return Err(Error::Validation(
474 "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
475 ));
476 }
477
478 if let Some(ref from) = self.from {
479 let valid_from_values = ["customer", "principal", "originator", "beneficiary"];
480 if !valid_from_values.contains(&from.as_str()) {
481 return Err(Error::Validation(
482 format!("Invalid 'from' value '{}'. Expected one of: customer, principal, originator, beneficiary", from),
483 ));
484 }
485 }
486
487 Ok(())
488 }
489
490 pub fn validate(&self) -> Result<()> {
492 self.validate_authorization_required()
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use serde_json;
500
501 #[test]
502 fn test_connect_v2_creation() {
503 let requester = Party::new("did:example:b2b-service");
504 let principal = Party::new("did:example:customer");
505 let agent = Agent::new_without_role("did:example:b2b-service", "did:example:b2b-service");
506 let constraints = ConnectionConstraints {
507 purposes: Some(vec!["BEXP".to_string()]),
508 category_purposes: None,
509 limits: Some(TransactionLimits {
510 per_transaction: Some("10000.00".to_string()),
511 per_day: Some("50000.00".to_string()),
512 per_week: None,
513 per_month: None,
514 per_year: None,
515 currency: Some("USD".to_string()),
516 }),
517 allowed_beneficiaries: None,
518 allowed_settlement_addresses: None,
519 allowed_assets: None,
520 };
521
522 let connect = Connect::new_v2(requester, principal, vec![agent], constraints)
523 .with_agreement("https://example.com/terms".to_string())
524 .with_expiry("2024-03-22T15:00:00Z".to_string());
525
526 assert!(connect.requester.is_some());
527 assert!(connect.principal.is_some());
528 assert_eq!(connect.agents.len(), 1);
529 assert!(connect.constraints.is_some());
530 assert_eq!(
531 connect.agreement,
532 Some("https://example.com/terms".to_string())
533 );
534 assert_eq!(connect.expiry, Some("2024-03-22T15:00:00Z".to_string()));
535 assert!(connect.validate().is_ok());
536 }
537
538 #[test]
539 fn test_connect_v2_serialization() {
540 let requester = Party::new("did:example:b2b-service");
541 let principal = Party::new("did:example:customer");
542 let agent = Agent::new_without_role("did:example:b2b-service", "did:example:b2b-service");
543 let constraints = ConnectionConstraints {
544 purposes: Some(vec!["BEXP".to_string()]),
545 category_purposes: None,
546 limits: Some(TransactionLimits {
547 per_transaction: Some("10000.00".to_string()),
548 per_day: Some("50000.00".to_string()),
549 per_week: None,
550 per_month: None,
551 per_year: None,
552 currency: Some("USD".to_string()),
553 }),
554 allowed_beneficiaries: Some(vec![Party::new("did:example:vendor-1")]),
555 allowed_settlement_addresses: Some(vec![
556 "eip155:1:0x742d35Cc6e4dfE2eDFaD2C0b91A8b0780EDAEb58".to_string(),
557 ]),
558 allowed_assets: Some(vec!["eip155:1/slip44:60".to_string()]),
559 };
560
561 let connect = Connect::new_v2(requester, principal, vec![agent], constraints);
562 let json = serde_json::to_value(&connect).unwrap();
563
564 assert!(json.get("requester").is_some());
565 assert!(json.get("principal").is_some());
566 assert!(json.get("agents").is_some());
567 assert!(json.get("constraints").is_some());
568
569 let constraints = json.get("constraints").unwrap();
570 assert!(constraints.get("allowedBeneficiaries").is_some());
571 assert!(constraints.get("allowedSettlementAddresses").is_some());
572 assert!(constraints.get("allowedAssets").is_some());
573
574 let limits = constraints.get("limits").unwrap();
575 assert_eq!(limits.get("per_day").unwrap(), "50000.00");
576 }
577
578 #[test]
579 fn test_connect_v2_validation_missing_principal() {
580 let connect = Connect {
581 transaction_id: None,
582 requester: Some(Party::new("did:example:service")),
583 principal: None,
584 agents: vec![Agent::new_without_role(
585 "did:example:service",
586 "did:example:service",
587 )],
588 constraints: Some(ConnectionConstraints {
589 purposes: Some(vec!["BEXP".to_string()]),
590 category_purposes: None,
591 limits: None,
592 allowed_beneficiaries: None,
593 allowed_settlement_addresses: None,
594 allowed_assets: None,
595 }),
596 agreement: None,
597 expiry: None,
598 agent_id: None,
599 agent: None,
600 for_: None,
601 role: None,
602 };
603
604 let result = connect.validate();
605 assert!(result.is_err());
606 assert!(result
607 .unwrap_err()
608 .to_string()
609 .contains("principal is required"));
610 }
611
612 #[test]
613 fn test_connect_v2_validation_no_agents() {
614 let connect = Connect {
615 transaction_id: None,
616 requester: Some(Party::new("did:example:service")),
617 principal: Some(Party::new("did:example:customer")),
618 agents: vec![],
619 constraints: Some(ConnectionConstraints {
620 purposes: Some(vec!["BEXP".to_string()]),
621 category_purposes: None,
622 limits: None,
623 allowed_beneficiaries: None,
624 allowed_settlement_addresses: None,
625 allowed_assets: None,
626 }),
627 agreement: None,
628 expiry: None,
629 agent_id: None,
630 agent: None,
631 for_: None,
632 role: None,
633 };
634
635 let result = connect.validate();
636 assert!(result.is_err());
637 assert!(result
638 .unwrap_err()
639 .to_string()
640 .contains("at least one agent"));
641 }
642
643 #[test]
644 fn test_authorization_required_creation() {
645 let auth_req = AuthorizationRequired::new(
646 "https://vasp.com/authorize?request=abc123".to_string(),
647 "2024-12-31T23:59:59Z".to_string(),
648 );
649
650 assert_eq!(
651 auth_req.authorization_url,
652 "https://vasp.com/authorize?request=abc123"
653 );
654 assert_eq!(auth_req.expires, "2024-12-31T23:59:59Z");
655 assert!(auth_req.from.is_none());
656 assert!(auth_req.metadata.is_empty());
657 }
658
659 #[test]
660 fn test_authorization_required_with_from() {
661 let auth_req = AuthorizationRequired::new_with_from(
662 "https://vasp.com/authorize".to_string(),
663 "2024-12-31T23:59:59Z".to_string(),
664 "customer".to_string(),
665 );
666
667 assert_eq!(auth_req.from, Some("customer".to_string()));
668 }
669
670 #[test]
671 fn test_authorization_required_builder_pattern() {
672 let auth_req = AuthorizationRequired::new(
673 "https://vasp.com/authorize".to_string(),
674 "2024-12-31T23:59:59Z".to_string(),
675 )
676 .with_from("principal".to_string())
677 .add_metadata("custom_field", serde_json::json!("value"));
678
679 assert_eq!(auth_req.from, Some("principal".to_string()));
680 assert_eq!(
681 auth_req.metadata.get("custom_field"),
682 Some(&serde_json::json!("value"))
683 );
684 }
685
686 #[test]
687 fn test_authorization_required_serialization() {
688 let auth_req = AuthorizationRequired::new_with_from(
689 "https://vasp.com/authorize?request=abc123".to_string(),
690 "2024-12-31T23:59:59Z".to_string(),
691 "customer".to_string(),
692 );
693
694 let json = serde_json::to_value(&auth_req).unwrap();
695
696 assert_eq!(
697 json["authorizationUrl"],
698 "https://vasp.com/authorize?request=abc123"
699 );
700 assert_eq!(json["expires"], "2024-12-31T23:59:59Z");
701 assert_eq!(json["from"], "customer");
702
703 let deserialized: AuthorizationRequired = serde_json::from_value(json).unwrap();
704 assert_eq!(deserialized.authorization_url, auth_req.authorization_url);
705 assert_eq!(deserialized.expires, auth_req.expires);
706 assert_eq!(deserialized.from, auth_req.from);
707 }
708
709 #[test]
710 fn test_authorization_required_validation_success() {
711 let auth_req = AuthorizationRequired::new(
712 "https://vasp.com/authorize".to_string(),
713 "2024-12-31T23:59:59Z".to_string(),
714 );
715
716 assert!(auth_req.validate().is_ok());
717 }
718
719 #[test]
720 fn test_authorization_required_validation_with_valid_from() {
721 let valid_from_values = ["customer", "principal", "originator", "beneficiary"];
722
723 for from_value in &valid_from_values {
724 let auth_req = AuthorizationRequired::new_with_from(
725 "https://vasp.com/authorize".to_string(),
726 "2024-12-31T23:59:59Z".to_string(),
727 from_value.to_string(),
728 );
729
730 assert!(
731 auth_req.validate().is_ok(),
732 "Validation failed for from value: {}",
733 from_value
734 );
735 }
736 }
737
738 #[test]
739 fn test_authorization_required_validation_empty_url() {
740 let auth_req =
741 AuthorizationRequired::new("".to_string(), "2024-12-31T23:59:59Z".to_string());
742
743 let result = auth_req.validate();
744 assert!(result.is_err());
745 assert!(result
746 .unwrap_err()
747 .to_string()
748 .contains("Authorization URL is required"));
749 }
750
751 #[test]
752 fn test_authorization_required_validation_empty_expires() {
753 let auth_req = AuthorizationRequired {
754 authorization_url: "https://vasp.com/authorize".to_string(),
755 expires: "".to_string(),
756 from: None,
757 metadata: HashMap::new(),
758 };
759
760 let result = auth_req.validate();
761 assert!(result.is_err());
762 assert!(result
763 .unwrap_err()
764 .to_string()
765 .contains("Expires timestamp is required"));
766 }
767
768 #[test]
769 fn test_authorization_required_validation_invalid_expires_format() {
770 let auth_req = AuthorizationRequired::new(
771 "https://vasp.com/authorize".to_string(),
772 "2024-12-31".to_string(),
773 );
774
775 let result = auth_req.validate();
776 assert!(result.is_err());
777 assert!(result
778 .unwrap_err()
779 .to_string()
780 .contains("Invalid expiry date format"));
781 }
782
783 #[test]
784 fn test_authorization_required_validation_invalid_from() {
785 let auth_req = AuthorizationRequired::new_with_from(
786 "https://vasp.com/authorize".to_string(),
787 "2024-12-31T23:59:59Z".to_string(),
788 "invalid_party".to_string(),
789 );
790
791 let result = auth_req.validate();
792 assert!(result.is_err());
793 assert!(result
794 .unwrap_err()
795 .to_string()
796 .contains("Invalid 'from' value"));
797 }
798
799 #[test]
800 fn test_authorization_required_json_compliance_with_taip4() {
801 let auth_req = AuthorizationRequired::new_with_from(
802 "https://beneficiary.vasp/authorize?request=abc123".to_string(),
803 "2024-01-01T12:00:00Z".to_string(),
804 "customer".to_string(),
805 );
806
807 let json = serde_json::to_value(&auth_req).unwrap();
808
809 assert!(json.get("authorizationUrl").is_some());
810 assert!(json.get("expires").is_some());
811 assert!(json.get("from").is_some());
812
813 assert!(json.get("authorization_url").is_none());
814 assert!(json.get("url").is_none());
815 }
816}