use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::message::agent::TapParticipant;
use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
use crate::message::{Agent, Party};
use crate::TapMessage;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectAgent {
#[serde(rename = "@id")]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
#[serde(rename = "serviceUrl", skip_serializing_if = "Option::is_none")]
pub service_url: Option<String>,
#[serde(flatten)]
pub metadata: HashMap<String, serde_json::Value>,
}
impl TapParticipant for ConnectAgent {
fn id(&self) -> &str {
&self.id
}
}
impl ConnectAgent {
pub fn new(id: &str) -> Self {
Self {
id: id.to_string(),
name: None,
agent_type: None,
service_url: None,
metadata: HashMap::new(),
}
}
pub fn to_agent(&self, for_party: &str) -> Agent {
let mut agent = Agent::new_without_role(&self.id, for_party);
if let Some(name) = &self.name {
agent
.metadata
.insert("name".to_string(), serde_json::Value::String(name.clone()));
}
if let Some(agent_type) = &self.agent_type {
agent.metadata.insert(
"type".to_string(),
serde_json::Value::String(agent_type.clone()),
);
}
if let Some(service_url) = &self.service_url {
agent.metadata.insert(
"serviceUrl".to_string(),
serde_json::Value::String(service_url.clone()),
);
}
for (k, v) in &self.metadata {
agent.metadata.insert(k.clone(), v.clone());
}
agent
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionLimits {
#[serde(skip_serializing_if = "Option::is_none")]
pub per_transaction: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_day: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_week: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_month: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_year: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub purposes: Option<Vec<String>>,
#[serde(rename = "categoryPurposes", skip_serializing_if = "Option::is_none")]
pub category_purposes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limits: Option<TransactionLimits>,
#[serde(
rename = "allowedBeneficiaries",
skip_serializing_if = "Option::is_none"
)]
pub allowed_beneficiaries: Option<Vec<Party>>,
#[serde(
rename = "allowedSettlementAddresses",
skip_serializing_if = "Option::is_none"
)]
pub allowed_settlement_addresses: Option<Vec<String>>,
#[serde(rename = "allowedAssets", skip_serializing_if = "Option::is_none")]
pub allowed_assets: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
#[tap(
message_type = "https://tap.rsvp/schema/1.0#Connect",
initiator,
authorizable
)]
pub struct Connect {
#[serde(skip)]
#[tap(transaction_id)]
pub transaction_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[tap(participant)]
pub requester: Option<Party>,
#[serde(skip_serializing_if = "Option::is_none")]
#[tap(participant)]
pub principal: Option<Party>,
#[serde(default)]
#[tap(participant_list)]
pub agents: Vec<Agent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<ConnectionConstraints>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agreement: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<ConnectAgent>,
#[serde(rename = "for", skip_serializing_if = "Option::is_none", default)]
pub for_: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
}
impl Connect {
pub fn new_v2(
requester: Party,
principal: Party,
agents: Vec<Agent>,
constraints: ConnectionConstraints,
) -> Self {
Self {
transaction_id: None,
requester: Some(requester),
principal: Some(principal),
agents,
constraints: Some(constraints),
agreement: None,
expiry: None,
agent_id: None,
agent: None,
for_: None,
role: None,
}
}
pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
Self {
transaction_id: Some(transaction_id.to_string()),
requester: None,
principal: None,
agents: vec![],
constraints: None,
agreement: None,
expiry: None,
agent_id: Some(agent_id.to_string()),
agent: None,
for_: Some(for_id.to_string()),
role: role.map(|s| s.to_string()),
}
}
pub fn new_with_agent_and_principal(
transaction_id: &str,
agent: ConnectAgent,
principal: Party,
) -> Self {
Self {
transaction_id: Some(transaction_id.to_string()),
requester: None,
principal: Some(principal),
agents: vec![],
constraints: None,
agreement: None,
expiry: None,
agent_id: None,
agent: Some(agent),
for_: None,
role: None,
}
}
pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
self.constraints = Some(constraints);
self
}
pub fn with_agreement(mut self, agreement: String) -> Self {
self.agreement = Some(agreement);
self
}
pub fn with_expiry(mut self, expiry: String) -> Self {
self.expiry = Some(expiry);
self
}
}
impl Connect {
pub fn validate_connect(&self) -> Result<()> {
if self.requester.is_some() {
if self.principal.is_none() {
return Err(Error::Validation("principal is required".to_string()));
}
if self.agents.is_empty() {
return Err(Error::Validation(
"at least one agent is required".to_string(),
));
}
if self.constraints.is_none() {
return Err(Error::Validation(
"Connection request must include constraints".to_string(),
));
}
return Ok(());
}
if self.agent_id.is_none() && self.agent.is_none() {
return Err(Error::Validation(
"either agent_id or agent is required".to_string(),
));
}
let for_empty = self.for_.as_ref().is_none_or(|s| s.is_empty());
if for_empty && self.principal.is_none() {
return Err(Error::Validation(
"either for or principal is required".to_string(),
));
}
if self.constraints.is_none() {
return Err(Error::Validation(
"Connection request must include constraints".to_string(),
));
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
self.validate_connect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
#[tap(message_type = "https://tap.rsvp/schema/1.0#OutOfBand")]
pub struct OutOfBand {
pub goal_code: String,
pub goal: String,
pub service: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub accept: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handshake_protocols: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl OutOfBand {
pub fn new(goal_code: String, goal: String, service: String) -> Self {
Self {
goal_code,
goal,
service,
accept: None,
handshake_protocols: None,
metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
#[tap(message_type = "https://tap.rsvp/schema/1.0#AuthorizationRequired")]
pub struct AuthorizationRequired {
#[serde(rename = "authorizationUrl")]
pub authorization_url: String,
pub expires: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl AuthorizationRequired {
pub fn new(authorization_url: String, expires: String) -> Self {
Self {
authorization_url,
expires,
from: None,
metadata: HashMap::new(),
}
}
pub fn new_with_from(authorization_url: String, expires: String, from: String) -> Self {
Self {
authorization_url,
expires,
from: Some(from),
metadata: HashMap::new(),
}
}
pub fn with_from(mut self, from: String) -> Self {
self.from = Some(from);
self
}
pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
self.metadata.insert(key.to_string(), value);
self
}
}
impl OutOfBand {
pub fn validate_out_of_band(&self) -> Result<()> {
if self.goal_code.is_empty() {
return Err(Error::Validation("Goal code is required".to_string()));
}
if self.service.is_empty() {
return Err(Error::Validation("Service is required".to_string()));
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
self.validate_out_of_band()
}
}
impl AuthorizationRequired {
pub fn validate_authorization_required(&self) -> Result<()> {
if self.authorization_url.is_empty() {
return Err(Error::Validation(
"Authorization URL is required".to_string(),
));
}
if self.expires.is_empty() {
return Err(Error::Validation(
"Expires timestamp is required".to_string(),
));
}
if !self.expires.contains('T') || !self.expires.contains(':') {
return Err(Error::Validation(
"Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
));
}
if let Some(ref from) = self.from {
let valid_from_values = ["customer", "principal", "originator", "beneficiary"];
if !valid_from_values.contains(&from.as_str()) {
return Err(Error::Validation(
format!("Invalid 'from' value '{}'. Expected one of: customer, principal, originator, beneficiary", from),
));
}
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
self.validate_authorization_required()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_connect_v2_creation() {
let requester = Party::new("did:example:b2b-service");
let principal = Party::new("did:example:customer");
let agent = Agent::new_without_role("did:example:b2b-service", "did:example:b2b-service");
let constraints = ConnectionConstraints {
purposes: Some(vec!["BEXP".to_string()]),
category_purposes: None,
limits: Some(TransactionLimits {
per_transaction: Some("10000.00".to_string()),
per_day: Some("50000.00".to_string()),
per_week: None,
per_month: None,
per_year: None,
currency: Some("USD".to_string()),
}),
allowed_beneficiaries: None,
allowed_settlement_addresses: None,
allowed_assets: None,
};
let connect = Connect::new_v2(requester, principal, vec![agent], constraints)
.with_agreement("https://example.com/terms".to_string())
.with_expiry("2024-03-22T15:00:00Z".to_string());
assert!(connect.requester.is_some());
assert!(connect.principal.is_some());
assert_eq!(connect.agents.len(), 1);
assert!(connect.constraints.is_some());
assert_eq!(
connect.agreement,
Some("https://example.com/terms".to_string())
);
assert_eq!(connect.expiry, Some("2024-03-22T15:00:00Z".to_string()));
assert!(connect.validate().is_ok());
}
#[test]
fn test_connect_v2_serialization() {
let requester = Party::new("did:example:b2b-service");
let principal = Party::new("did:example:customer");
let agent = Agent::new_without_role("did:example:b2b-service", "did:example:b2b-service");
let constraints = ConnectionConstraints {
purposes: Some(vec!["BEXP".to_string()]),
category_purposes: None,
limits: Some(TransactionLimits {
per_transaction: Some("10000.00".to_string()),
per_day: Some("50000.00".to_string()),
per_week: None,
per_month: None,
per_year: None,
currency: Some("USD".to_string()),
}),
allowed_beneficiaries: Some(vec![Party::new("did:example:vendor-1")]),
allowed_settlement_addresses: Some(vec![
"eip155:1:0x742d35Cc6e4dfE2eDFaD2C0b91A8b0780EDAEb58".to_string(),
]),
allowed_assets: Some(vec!["eip155:1/slip44:60".to_string()]),
};
let connect = Connect::new_v2(requester, principal, vec![agent], constraints);
let json = serde_json::to_value(&connect).unwrap();
assert!(json.get("requester").is_some());
assert!(json.get("principal").is_some());
assert!(json.get("agents").is_some());
assert!(json.get("constraints").is_some());
let constraints = json.get("constraints").unwrap();
assert!(constraints.get("allowedBeneficiaries").is_some());
assert!(constraints.get("allowedSettlementAddresses").is_some());
assert!(constraints.get("allowedAssets").is_some());
let limits = constraints.get("limits").unwrap();
assert_eq!(limits.get("per_day").unwrap(), "50000.00");
}
#[test]
fn test_connect_v2_validation_missing_principal() {
let connect = Connect {
transaction_id: None,
requester: Some(Party::new("did:example:service")),
principal: None,
agents: vec![Agent::new_without_role(
"did:example:service",
"did:example:service",
)],
constraints: Some(ConnectionConstraints {
purposes: Some(vec!["BEXP".to_string()]),
category_purposes: None,
limits: None,
allowed_beneficiaries: None,
allowed_settlement_addresses: None,
allowed_assets: None,
}),
agreement: None,
expiry: None,
agent_id: None,
agent: None,
for_: None,
role: None,
};
let result = connect.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("principal is required"));
}
#[test]
fn test_connect_v2_validation_no_agents() {
let connect = Connect {
transaction_id: None,
requester: Some(Party::new("did:example:service")),
principal: Some(Party::new("did:example:customer")),
agents: vec![],
constraints: Some(ConnectionConstraints {
purposes: Some(vec!["BEXP".to_string()]),
category_purposes: None,
limits: None,
allowed_beneficiaries: None,
allowed_settlement_addresses: None,
allowed_assets: None,
}),
agreement: None,
expiry: None,
agent_id: None,
agent: None,
for_: None,
role: None,
};
let result = connect.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("at least one agent"));
}
#[test]
fn test_authorization_required_creation() {
let auth_req = AuthorizationRequired::new(
"https://vasp.com/authorize?request=abc123".to_string(),
"2024-12-31T23:59:59Z".to_string(),
);
assert_eq!(
auth_req.authorization_url,
"https://vasp.com/authorize?request=abc123"
);
assert_eq!(auth_req.expires, "2024-12-31T23:59:59Z");
assert!(auth_req.from.is_none());
assert!(auth_req.metadata.is_empty());
}
#[test]
fn test_authorization_required_with_from() {
let auth_req = AuthorizationRequired::new_with_from(
"https://vasp.com/authorize".to_string(),
"2024-12-31T23:59:59Z".to_string(),
"customer".to_string(),
);
assert_eq!(auth_req.from, Some("customer".to_string()));
}
#[test]
fn test_authorization_required_builder_pattern() {
let auth_req = AuthorizationRequired::new(
"https://vasp.com/authorize".to_string(),
"2024-12-31T23:59:59Z".to_string(),
)
.with_from("principal".to_string())
.add_metadata("custom_field", serde_json::json!("value"));
assert_eq!(auth_req.from, Some("principal".to_string()));
assert_eq!(
auth_req.metadata.get("custom_field"),
Some(&serde_json::json!("value"))
);
}
#[test]
fn test_authorization_required_serialization() {
let auth_req = AuthorizationRequired::new_with_from(
"https://vasp.com/authorize?request=abc123".to_string(),
"2024-12-31T23:59:59Z".to_string(),
"customer".to_string(),
);
let json = serde_json::to_value(&auth_req).unwrap();
assert_eq!(
json["authorizationUrl"],
"https://vasp.com/authorize?request=abc123"
);
assert_eq!(json["expires"], "2024-12-31T23:59:59Z");
assert_eq!(json["from"], "customer");
let deserialized: AuthorizationRequired = serde_json::from_value(json).unwrap();
assert_eq!(deserialized.authorization_url, auth_req.authorization_url);
assert_eq!(deserialized.expires, auth_req.expires);
assert_eq!(deserialized.from, auth_req.from);
}
#[test]
fn test_authorization_required_validation_success() {
let auth_req = AuthorizationRequired::new(
"https://vasp.com/authorize".to_string(),
"2024-12-31T23:59:59Z".to_string(),
);
assert!(auth_req.validate().is_ok());
}
#[test]
fn test_authorization_required_validation_with_valid_from() {
let valid_from_values = ["customer", "principal", "originator", "beneficiary"];
for from_value in &valid_from_values {
let auth_req = AuthorizationRequired::new_with_from(
"https://vasp.com/authorize".to_string(),
"2024-12-31T23:59:59Z".to_string(),
from_value.to_string(),
);
assert!(
auth_req.validate().is_ok(),
"Validation failed for from value: {}",
from_value
);
}
}
#[test]
fn test_authorization_required_validation_empty_url() {
let auth_req =
AuthorizationRequired::new("".to_string(), "2024-12-31T23:59:59Z".to_string());
let result = auth_req.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Authorization URL is required"));
}
#[test]
fn test_authorization_required_validation_empty_expires() {
let auth_req = AuthorizationRequired {
authorization_url: "https://vasp.com/authorize".to_string(),
expires: "".to_string(),
from: None,
metadata: HashMap::new(),
};
let result = auth_req.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Expires timestamp is required"));
}
#[test]
fn test_authorization_required_validation_invalid_expires_format() {
let auth_req = AuthorizationRequired::new(
"https://vasp.com/authorize".to_string(),
"2024-12-31".to_string(),
);
let result = auth_req.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid expiry date format"));
}
#[test]
fn test_authorization_required_validation_invalid_from() {
let auth_req = AuthorizationRequired::new_with_from(
"https://vasp.com/authorize".to_string(),
"2024-12-31T23:59:59Z".to_string(),
"invalid_party".to_string(),
);
let result = auth_req.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid 'from' value"));
}
#[test]
fn test_authorization_required_json_compliance_with_taip4() {
let auth_req = AuthorizationRequired::new_with_from(
"https://beneficiary.vasp/authorize?request=abc123".to_string(),
"2024-01-01T12:00:00Z".to_string(),
"customer".to_string(),
);
let json = serde_json::to_value(&auth_req).unwrap();
assert!(json.get("authorizationUrl").is_some());
assert!(json.get("expires").is_some());
assert!(json.get("from").is_some());
assert!(json.get("authorization_url").is_none());
assert!(json.get("url").is_none());
}
}