TAP Message
Core message processing for the Transaction Authorization Protocol (TAP) with integrated DIDComm support.
Features
- TAP Message Types: Complete implementation of all TAP message types
- DIDComm Integration: Direct conversion between TAP messages and DIDComm messages
- Validation: Proper validation of all message fields and formats
- CAIP Support: Validation for chain-agnostic identifiers (CAIP-2, CAIP-10, CAIP-19)
- Authorization Flows: Support for authorization, rejection, and settlement flows
- Agent Policies: TAIP-7 compliant policy implementation for defining agent requirements
- Invoice Support: TAIP-16 compliant structured invoice implementation with tax and line item support
- Payment Requests: TAIP-14 compliant payment requests with currency and asset options
- Extensibility: Easy addition of new message types
Usage
Basic Transfer Message
use tap_msg::message::types::{Transfer, Participant};
use tap_msg::message::tap_message_trait::{TapMessageBody, TapMessage};
use tap_caip::AssetId;
use std::collections::HashMap;
use std::str::FromStr;
let asset = AssetId::from_str("eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7")?;
let originator = Participant {
id: "did:example:sender".to_string(),
role: Some("originator".to_string()),
policies: None,
leiCode: None,
};
let beneficiary = Participant {
id: "did:example:receiver".to_string(),
role: Some("beneficiary".to_string()),
policies: None,
leiCode: None,
};
let transfer = Transfer {
transaction_id: uuid::Uuid::new_v4().to_string(),
asset,
originator,
beneficiary: Some(beneficiary),
amount: "100.0".to_string(),
agents: vec![],
settlement_id: None,
memo: Some("Test transfer".to_string()),
metadata: HashMap::new(),
};
let message = transfer.to_didcomm(Some("did:example:sender"))?;
let message_with_route = transfer.to_didcomm_with_route(
Some("did:example:sender"),
["did:example:receiver"].iter().copied()
)?;
let received_transfer = Transfer::from_didcomm(&message)?;
let thread_id = received_transfer.thread_id(); let message_id = received_transfer.message_id();
Payment Request with Invoice
use tap_msg::{PaymentRequest, Invoice, LineItem, Participant};
use tap_msg::message::tap_message_trait::TapMessageBody;
use std::collections::HashMap;
let merchant = Participant {
id: "did:example:merchant".to_string(),
role: Some("merchant".to_string()),
policies: None,
leiCode: None,
};
let line_items = vec![
LineItem {
id: "1".to_string(),
description: "Premium Service".to_string(),
quantity: 1.0,
unit_code: None,
unit_price: 100.0,
line_total: 100.0,
tax_category: None,
}
];
let invoice = Invoice::new(
"INV-2023-001".to_string(),
"2023-06-01".to_string(),
"USD".to_string(),
line_items,
100.0,
);
let mut payment_request = PaymentRequest::with_currency(
"USD".to_string(),
"100.0".to_string(),
merchant,
vec![], );
payment_request.invoice = Some(invoice);
let message = payment_request.to_didcomm_with_route(
Some("did:example:merchant"),
["did:example:customer"].iter().copied()
)?;
Message Types
Transfer
The Transfer struct represents a TAP transfer message, which is the core message type in the protocol:
pub struct Transfer {
pub transaction_id: String,
pub asset: AssetId,
pub originator: Participant,
pub beneficiary: Option<Participant>,
pub amount: String,
pub agents: Vec<Participant>,
pub settlement_id: Option<String>,
pub memo: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
Agent Policies (TAIP-7)
The TAP protocol supports defining agent policies according to TAIP-7, which allows agents to specify requirements for authorizing transactions:
use tap_msg::message::{
Participant, Policy, RequireAuthorization, RequirePresentation,
RequireProofOfControl, UpdatePolicies
};
use std::collections::HashMap;
let auth_policy = RequireAuthorization {
type_: "RequireAuthorization".to_string(),
from: Some(vec!["did:example:alice".to_string()]),
from_role: None,
from_agent: None,
purpose: Some("Authorization required from Alice".to_string()),
};
let participant = Participant {
id: "did:example:bob".to_string(),
role: Some("beneficiary".to_string()),
policies: Some(vec![Policy::RequireAuthorization(auth_policy)]),
};
let proof_policy = RequireProofOfControl::default(); let update_policies = UpdatePolicies {
transaction_id: "transfer_12345".to_string(),
policies: vec![Policy::RequireProofOfControl(proof_policy)],
};
update_policies.validate().unwrap();
let didcomm_msg = update_policies.to_didcomm_with_route(
Some("did:example:originator_vasp"),
["did:example:beneficiary", "did:example:beneficiary_vasp"].iter().copied()
).unwrap();
Authorization Messages
TAP supports various authorization messages for compliance workflows:
pub struct Authorize {
pub transaction_id: String,
pub note: Option<String>,
}
pub struct Reject {
pub transaction_id: String,
pub reason: String,
}
pub struct Settle {
pub transaction_id: String,
pub settlement_id: String,
pub amount: Option<String>,
}
Error Messages
TAP provides standardized error messages:
pub struct ErrorBody {
pub code: String,
pub description: String,
pub original_message_id: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
Invoice and Payment Requests (TAIP-14, TAIP-16)
TAP supports structured invoices according to TAIP-16, which can be embedded in payment requests (TAIP-14):
use tap_msg::{PaymentRequest, Invoice, LineItem, TaxCategory, TaxTotal, TaxSubtotal};
use tap_msg::message::tap_message_trait::TapMessageBody;
use tap_msg::Participant;
use std::collections::HashMap;
let merchant = Participant {
id: "did:example:merchant".to_string(),
role: Some("merchant".to_string()),
policies: None,
leiCode: None,
};
let invoice = Invoice {
id: "INV001".to_string(),
issue_date: "2023-05-15".to_string(),
currency_code: "USD".to_string(),
line_items: vec![
LineItem {
id: "1".to_string(),
description: "Product A".to_string(),
quantity: 2.0,
unit_code: Some("EA".to_string()),
unit_price: 10.0,
line_total: 20.0,
tax_category: None,
},
LineItem {
id: "2".to_string(),
description: "Product B".to_string(),
quantity: 1.0,
unit_code: Some("EA".to_string()),
unit_price: 5.0,
line_total: 5.0,
tax_category: None,
},
],
tax_total: None,
total: 25.0,
sub_total: Some(25.0),
due_date: None,
note: None,
payment_terms: None,
accounting_cost: None,
order_reference: None,
additional_document_reference: None,
metadata: HashMap::new(),
};
let mut payment_request = PaymentRequest::with_currency(
"USD".to_string(),
"25.0".to_string(),
merchant.clone(),
vec![], );
payment_request.invoice = Some(invoice);
let message = payment_request.to_didcomm_with_route(
Some("did:example:merchant"),
["did:example:customer"].iter().copied()
).unwrap();
let received_request = PaymentRequest::from_didcomm(&message).unwrap();
received_request.validate().unwrap();
if let Some(received_invoice) = received_request.invoice {
println!("Invoice ID: {}", received_invoice.id);
println!("Total amount: {}", received_invoice.total);
for item in received_invoice.line_items {
println!("{} x {} @ ${} = ${}",
item.quantity,
item.description,
item.unit_price,
item.line_total
);
}
}
DIDComm Integration
The TapMessageBody trait provides methods for converting between TAP messages and DIDComm messages:
pub trait TapMessageBody: DeserializeOwned + Serialize + Send + Sync {
fn message_type() -> &'static str;
fn from_didcomm(msg: &Message) -> Result<Self, Error>;
fn validate(&self) -> Result<(), Error>;
fn to_didcomm(&self) -> Result<Message, Error>;
fn to_didcomm_with_route<'a, I>(&self, from: Option<&str>, to: I) -> Result<Message, Error>
where
I: Iterator<Item = &'a str>;
}
Authorizable Trait
The Authorizable trait provides methods for handling authorization, rejection, and settlement flows:
pub trait Authorizable {
fn message_id(&self) -> &str;
fn authorize(&self, note: Option<String>) -> Authorize;
fn reject(&self, code: String, description: String) -> Reject;
fn settle(
&self,
settlement_id: String,
amount: Option<String>,
) -> Settle;
fn cancel(&self, reason: Option<String>, note: Option<String>) -> Cancel;
fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies;
fn add_agents(&self, transaction_id: String, agents: Vec<Participant>) -> AddAgents;
fn replace_agent(
&self,
transaction_id: String,
original: String,
replacement: Participant,
) -> ReplaceAgent;
fn remove_agent(&self, transaction_id: String, agent: String) -> RemoveAgent;
}
Message Threading
TAP messages support thread correlation through the TapMessage trait, which is implemented for all message types using the impl_tap_message! macro. This provides:
- Thread ID tracking
- Parent thread ID tracking
- Message correlation
- Reply creation
pub trait TapMessage {
fn validate(&self) -> Result<()>;
fn is_tap_message(&self) -> bool;
fn get_tap_type(&self) -> Option<String>;
fn body_as<T: TapMessageBody>(&self) -> Result<T>;
fn get_all_participants(&self) -> Vec<String>;
fn create_reply<T: TapMessageBody>(
&self,
body: &T,
creator_did: &str,
) -> Result<Message>;
fn message_type(&self) -> &'static str;
fn thread_id(&self) -> Option<&str>;
fn parent_thread_id(&self) -> Option<&str>;
fn message_id(&self) -> &str;
}
Adding New Message Types
To add a new TAP message type, follow these steps:
- Define your message struct with required fields (must include
transaction_id: String for most messages)
- Implement the
TapMessageBody trait for your struct
- Apply the
impl_tap_message! macro to implement the TapMessage trait
Here's an example:
use tap_msg::message::tap_message_trait::{TapMessageBody, TapMessage};
use tap_msg::impl_tap_message;
use tap_msg::error::Result;
use didcomm::Message;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyNewMessage {
pub transaction_id: String,
pub field1: String,
pub field2: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl TapMessageBody for MyNewMessage {
fn message_type() -> &'static str {
"https://tap.rsvp/schema/1.0#mynewmessage"
}
fn validate(&self) -> Result<()> {
if self.transaction_id.is_empty() {
return Err(Error::Validation("Transaction ID is required".to_string()));
}
if self.field1.is_empty() {
return Err(Error::Validation("Field1 is required".to_string()));
}
Ok(())
}
fn to_didcomm(&self, from_did: Option<&str>) -> Result<Message> {
}
fn from_didcomm(message: &Message) -> Result<Self> {
}
}
impl_tap_message!(MyNewMessage);
For message types with non-standard fields, you can use the specialized variants of the macro:
impl_tap_message!(MessageType) - For standard messages with transaction_id: String
impl_tap_message!(MessageType, optional_transaction_id) - For messages with transaction_id: Option<String>
impl_tap_message!(MessageType, thread_based) - For messages using thid: Option<String> instead of transaction_id
impl_tap_message!(MessageType, generated_id) - For messages with neither transaction_id nor thread_id
Examples
See the examples directory for more detailed examples of using TAP messages.