tap_node/message/
travel_rule_processor.rs1use 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#[derive(Clone)]
20pub struct TravelRuleProcessor {
21 customer_manager: Arc<CustomerManager>,
22}
23
24impl TravelRuleProcessor {
25 pub fn new(customer_manager: Arc<CustomerManager>) -> Self {
27 Self { customer_manager }
28 }
29
30 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 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 }
52 }
53 }
54 }
55 }
56 }
57 Ok(())
58 }
59
60 async fn handle_presentation(&self, message: &PlainMessage) -> Result<()> {
62 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 if self.is_ivms101_credential(json_data) {
72 info!(
73 "Received IVMS101 presentation in message {}",
74 message.id
75 );
76
77 if let Some(ivms_data) = self.extract_ivms101_data(json_data) {
79 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 }
104 }
105 }
106 }
107 }
108 }
109 Ok(())
110 }
111
112 fn is_ivms101_credential(&self, data: &Value) -> bool {
114 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 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 fn extract_ivms101_data(&self, credential_data: &Value) -> Option<Value> {
147 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 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 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 async fn find_customer_by_did(&self, did: &str) -> Result<String> {
175 Ok(did.to_string())
178 }
179
180 async fn should_attach_ivms101(&self, _message: &PlainMessage) -> bool {
182 true
190 }
191
192 async fn generate_ivms101_presentation(&self, party_id: &str, role: &str) -> Result<Value> {
194 let ivms_data = self
196 .customer_manager
197 .generate_ivms101_data(party_id)
198 .await?;
199
200 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, "credentialSubject": {
209 role: ivms_data
210 }
211 });
212
213 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 let message_type = &message.type_;
232
233 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 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 Ok(Some(message))
249 }
250
251 async fn process_outgoing(&self, mut message: PlainMessage) -> Result<Option<PlainMessage>> {
252 if message.type_.contains("Transfer") && self.should_attach_ivms101(&message).await {
254 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 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 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 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 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 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}