Skip to main content

tap_node/message/
travel_rule_processor.rs

1//! Travel Rule message processor for TAIP-10 compliance
2//!
3//! This processor handles the Travel Rule flow as specified in TAIP-10, including:
4//! - Handling UpdatePolicies messages that require IVMS101 presentations
5//! - Processing Presentation messages containing IVMS101 data
6//! - Generating and attaching IVMS101 presentations to outgoing Transfer messages
7
8use async_trait::async_trait;
9use log::{info, warn};
10use serde_json::{json, Value};
11use std::sync::Arc;
12use tap_msg::didcomm::{Attachment, AttachmentData, PlainMessage};
13
14use crate::customer::CustomerManager;
15use crate::error::Result;
16use crate::message::processor::PlainMessageProcessor;
17
18/// Travel Rule processor that handles TAIP-10 compliant message flows
19#[derive(Clone)]
20pub struct TravelRuleProcessor {
21    customer_manager: Arc<CustomerManager>,
22}
23
24impl TravelRuleProcessor {
25    /// Create a new Travel Rule processor
26    pub fn new(customer_manager: Arc<CustomerManager>) -> Self {
27        Self { customer_manager }
28    }
29
30    /// Process UpdatePolicies message to check for IVMS101 requirements
31    async fn handle_update_policies(&self, message: &PlainMessage) -> Result<()> {
32        if let Some(policies) = message.body.get("policies").and_then(|p| p.as_array()) {
33            for policy in policies {
34                if let Some(policy_type) = policy.get("@type").and_then(|t| t.as_str()) {
35                    if policy_type == "RequirePresentation" {
36                        // Check if IVMS101 data is being requested
37                        if let Some(context) = policy.get("@context").and_then(|c| c.as_array()) {
38                            let requires_ivms = context.iter().any(|ctx| {
39                                ctx.as_str()
40                                    .map(|s| s.contains("ivms") || s.contains("intervasp"))
41                                    .unwrap_or(false)
42                            });
43
44                            if requires_ivms {
45                                info!(
46                                    "Received IVMS101 presentation request in message {}",
47                                    message.id
48                                );
49                                // Store this requirement for later response
50                                // In a production system, you'd store this in a proper state manager
51                            }
52                        }
53                    }
54                }
55            }
56        }
57        Ok(())
58    }
59
60    /// Process Presentation message containing IVMS101 data
61    async fn handle_presentation(&self, message: &PlainMessage) -> Result<()> {
62        // Look for IVMS101 data in attachments
63        if let Some(attachments) = message.attachments.as_ref() {
64            for attachment in attachments {
65                if let Some(media_type) = &attachment.media_type {
66                    if media_type == "application/json" {
67                        match &attachment.data {
68                            AttachmentData::Json { value } => {
69                                let json_data = &value.json;
70                                // Check if this is IVMS101 data
71                                if self.is_ivms101_credential(json_data) {
72                                    info!(
73                                        "Received IVMS101 presentation in message {}",
74                                        message.id
75                                    );
76
77                                    // Extract IVMS101 data from the credential
78                                    if let Some(ivms_data) = self.extract_ivms101_data(json_data) {
79                                        // Update customer records with received IVMS101 data
80                                        let from_did = &message.from;
81                                        if let Ok(customer_id) =
82                                            self.find_customer_by_did(from_did).await
83                                        {
84                                            if let Err(e) = self
85                                                .customer_manager
86                                                .update_customer_from_ivms101(
87                                                    &customer_id,
88                                                    &ivms_data,
89                                                )
90                                                .await
91                                            {
92                                                warn!(
93                                                    "Failed to update customer {} with IVMS101 data: {}",
94                                                    customer_id, e
95                                                );
96                                            }
97                                        }
98                                    }
99                                }
100                            }
101                            _ => {
102                                // Skip non-JSON attachments
103                            }
104                        }
105                    }
106                }
107            }
108        }
109        Ok(())
110    }
111
112    /// Check if a JSON value contains IVMS101 credential data
113    fn is_ivms101_credential(&self, data: &Value) -> bool {
114        // Check for IVMS101 context
115        if let Some(context) = data.get("@context").and_then(|c| c.as_array()) {
116            if context.iter().any(|ctx| {
117                ctx.as_str()
118                    .map(|s| s.contains("ivms") || s.contains("intervasp"))
119                    .unwrap_or(false)
120            }) {
121                return true;
122            }
123        }
124
125        // Check for verifiable credential with IVMS101 data
126        if let Some(credentials) = data
127            .get("verifiableCredential")
128            .and_then(|vc| vc.as_array())
129        {
130            return credentials.iter().any(|cred| {
131                cred.get("credentialSubject")
132                    .map(|cs| {
133                        cs.get("originator").is_some()
134                            || cs.get("beneficiary").is_some()
135                            || cs.get("naturalPerson").is_some()
136                            || cs.get("legalPerson").is_some()
137                    })
138                    .unwrap_or(false)
139            });
140        }
141
142        false
143    }
144
145    /// Extract IVMS101 data from a verifiable credential
146    fn extract_ivms101_data(&self, credential_data: &Value) -> Option<Value> {
147        // Look for credential subject containing IVMS101 data
148        if let Some(credentials) = credential_data
149            .get("verifiableCredential")
150            .and_then(|vc| vc.as_array())
151        {
152            for cred in credentials {
153                if let Some(subject) = cred.get("credentialSubject") {
154                    // Return the originator or beneficiary data
155                    if let Some(originator) = subject.get("originator") {
156                        return Some(originator.clone());
157                    }
158                    if let Some(beneficiary) = subject.get("beneficiary") {
159                        return Some(beneficiary.clone());
160                    }
161                    // Direct natural/legal person data
162                    if subject.get("naturalPerson").is_some()
163                        || subject.get("legalPerson").is_some()
164                    {
165                        return Some(subject.clone());
166                    }
167                }
168            }
169        }
170        None
171    }
172
173    /// Find customer by DID
174    async fn find_customer_by_did(&self, did: &str) -> Result<String> {
175        // In a production system, this would query the database
176        // For now, we'll use the DID as the customer ID
177        Ok(did.to_string())
178    }
179
180    /// Check if a Transfer message should include proactive IVMS101 data
181    async fn should_attach_ivms101(&self, _message: &PlainMessage) -> bool {
182        // In a production system, this would check:
183        // 1. Regulatory requirements for the jurisdiction
184        // 2. Transaction amount thresholds
185        // 3. Counterparty policies
186        // 4. Internal compliance policies
187
188        // For now, we'll attach IVMS101 data to all transfers
189        true
190    }
191
192    /// Generate IVMS101 presentation for a party
193    async fn generate_ivms101_presentation(&self, party_id: &str, role: &str) -> Result<Value> {
194        // Generate IVMS101 data for the customer
195        let ivms_data = self
196            .customer_manager
197            .generate_ivms101_data(party_id)
198            .await?;
199
200        // Create a verifiable credential with the IVMS101 data
201        let credential = json!({
202            "@context": [
203                "https://www.w3.org/2018/credentials/v1",
204                "https://intervasp.org/ivms101"
205            ],
206            "type": ["VerifiableCredential", "TravelRuleCredential"],
207            "issuer": party_id, // In production, this would be the VASP's DID
208            "credentialSubject": {
209                role: ivms_data
210            }
211        });
212
213        // Create a verifiable presentation
214        let presentation = json!({
215            "@context": [
216                "https://www.w3.org/2018/credentials/v1",
217                "https://intervasp.org/ivms101"
218            ],
219            "type": ["VerifiablePresentation", "PresentationSubmission"],
220            "verifiableCredential": [credential]
221        });
222
223        Ok(presentation)
224    }
225}
226
227#[async_trait]
228impl PlainMessageProcessor for TravelRuleProcessor {
229    async fn process_incoming(&self, message: PlainMessage) -> Result<Option<PlainMessage>> {
230        // Check message type
231        let message_type = &message.type_;
232
233        // Handle UpdatePolicies messages
234        if message_type.contains("UpdatePolicies") {
235            if let Err(e) = self.handle_update_policies(&message).await {
236                warn!("Failed to handle UpdatePolicies message: {}", e);
237            }
238        }
239
240        // Handle Presentation messages
241        if message_type.contains("Presentation") || message_type.contains("present-proof") {
242            if let Err(e) = self.handle_presentation(&message).await {
243                warn!("Failed to handle Presentation message: {}", e);
244            }
245        }
246
247        // Pass the message through
248        Ok(Some(message))
249    }
250
251    async fn process_outgoing(&self, mut message: PlainMessage) -> Result<Option<PlainMessage>> {
252        // Check if this is a Transfer message that needs IVMS101 data
253        if message.type_.contains("Transfer") && self.should_attach_ivms101(&message).await {
254            // Extract originator information from the message body
255            if let Some(originator) = message.body.get("originator") {
256                if let Some(originator_id) = originator.get("@id").and_then(|id| id.as_str()) {
257                    match self
258                        .generate_ivms101_presentation(originator_id, "originator")
259                        .await
260                    {
261                        Ok(presentation) => {
262                            // Add IVMS101 presentation as an attachment
263                            let attachment = Attachment::json(presentation)
264                                .id("ivms101-vp".to_string())
265                                .media_type("application/json".to_string())
266                                .format("dif/presentation-exchange/submission@v1.0".to_string())
267                                .finalize();
268
269                            // Add attachment to message
270                            if message.attachments.is_none() {
271                                message.attachments = Some(vec![]);
272                            }
273                            if let Some(attachments) = &mut message.attachments {
274                                attachments.push(attachment);
275                            }
276
277                            info!(
278                                "Attached IVMS101 presentation to Transfer message {}",
279                                message.id
280                            );
281                        }
282                        Err(e) => {
283                            warn!(
284                                "Failed to generate IVMS101 presentation for Transfer: {}",
285                                e
286                            );
287                        }
288                    }
289                }
290            }
291        }
292
293        // Pass the message through
294        Ok(Some(message))
295    }
296}
297
298impl std::fmt::Debug for TravelRuleProcessor {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        f.debug_struct("TravelRuleProcessor").finish()
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::storage::Storage;
308    use tempfile::tempdir;
309
310    #[tokio::test]
311    async fn test_ivms101_detection() {
312        let dir = tempdir().unwrap();
313        let db_path = dir.path().join("test.db");
314        let storage = Arc::new(Storage::new(Some(db_path)).await.unwrap());
315        let customer_manager = Arc::new(CustomerManager::new(storage));
316        let processor = TravelRuleProcessor::new(customer_manager);
317
318        // Test IVMS101 credential detection
319        let ivms_credential = json!({
320            "@context": [
321                "https://www.w3.org/2018/credentials/v1",
322                "https://intervasp.org/ivms101"
323            ],
324            "verifiableCredential": [{
325                "credentialSubject": {
326                    "originator": {
327                        "naturalPerson": {
328                            "name": {
329                                "nameIdentifiers": [{
330                                    "primaryIdentifier": "Smith",
331                                    "secondaryIdentifier": "Alice"
332                                }]
333                            }
334                        }
335                    }
336                }
337            }]
338        });
339
340        assert!(processor.is_ivms101_credential(&ivms_credential));
341
342        // Test non-IVMS101 credential
343        let other_credential = json!({
344            "@context": ["https://www.w3.org/2018/credentials/v1"],
345            "type": ["VerifiableCredential"]
346        });
347
348        assert!(!processor.is_ivms101_credential(&other_credential));
349    }
350
351    #[tokio::test]
352    async fn test_ivms101_extraction() {
353        let dir = tempdir().unwrap();
354        let db_path = dir.path().join("test.db");
355        let storage = Arc::new(Storage::new(Some(db_path)).await.unwrap());
356        let customer_manager = Arc::new(CustomerManager::new(storage));
357        let processor = TravelRuleProcessor::new(customer_manager);
358
359        let credential = json!({
360            "verifiableCredential": [{
361                "credentialSubject": {
362                    "originator": {
363                        "naturalPerson": {
364                            "name": {
365                                "nameIdentifiers": [{
366                                    "primaryIdentifier": "Smith",
367                                    "secondaryIdentifier": "Alice"
368                                }]
369                            }
370                        }
371                    }
372                }
373            }]
374        });
375
376        let extracted = processor.extract_ivms101_data(&credential);
377        assert!(extracted.is_some());
378
379        let data = extracted.unwrap();
380        assert!(data.get("naturalPerson").is_some());
381    }
382}