# 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
```rust
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;
// Create a Transfer message body
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(),
};
// Convert to a DIDComm message
let message = transfer.to_didcomm(Some("did:example:sender"))?;
// Or with routing information
let message_with_route = transfer.to_didcomm_with_route(
Some("did:example:sender"),
["did:example:receiver"].iter().copied()
)?;
// Create a TAP message from a DIDComm message
let received_transfer = Transfer::from_didcomm(&message)?;
// Using the TapMessage trait for thread correlation
let thread_id = received_transfer.thread_id(); // Returns the transaction_id
let message_id = received_transfer.message_id(); // Returns the transaction_id for standard messages
```
### Payment Request with Invoice
```rust
use tap_msg::{PaymentRequest, Invoice, LineItem, Participant};
use tap_msg::message::tap_message_trait::TapMessageBody;
use std::collections::HashMap;
// Create a merchant and a basic invoice
let merchant = Participant {
id: "did:example:merchant".to_string(),
role: Some("merchant".to_string()),
policies: None,
leiCode: None,
};
// Create line items for the invoice
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,
}
];
// Create a basic invoice
let invoice = Invoice::new(
"INV-2023-001".to_string(),
"2023-06-01".to_string(),
"USD".to_string(),
line_items,
100.0,
);
// Create a payment request with the invoice
let mut payment_request = PaymentRequest::with_currency(
"USD".to_string(),
"100.0".to_string(),
merchant,
vec![], // No additional agents in this simple example
);
payment_request.invoice = Some(invoice);
// Send the payment request to a customer
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:
```rust
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:
```rust
use tap_msg::message::{
Participant, Policy, RequireAuthorization, RequirePresentation,
RequireProofOfControl, UpdatePolicies
};
use std::collections::HashMap;
// Create a participant with a policy
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)]),
};
// Create an UpdatePolicies message to dynamically update policies
let proof_policy = RequireProofOfControl::default(); // Uses default values
let update_policies = UpdatePolicies {
transaction_id: "transfer_12345".to_string(),
policies: vec![Policy::RequireProofOfControl(proof_policy)],
};
// Validate the message
update_policies.validate().unwrap();
// Convert to DIDComm message and send to all participants
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:
```rust
// Authorization message
pub struct Authorize {
pub transaction_id: String,
pub note: Option<String>,
}
// Rejection message
pub struct Reject {
pub transaction_id: String,
pub reason: String,
}
// Settlement message
pub struct Settle {
pub transaction_id: String,
pub settlement_id: String,
pub amount: Option<String>,
}
```
### Error Messages
TAP provides standardized error messages:
```rust
pub struct ErrorBody {
/// Error code.
pub code: String,
/// Error description.
pub description: String,
/// Original message ID (if applicable).
pub original_message_id: Option<String>,
/// Additional metadata.
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):
```rust
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;
// Create a merchant participant
let merchant = Participant {
id: "did:example:merchant".to_string(),
role: Some("merchant".to_string()),
policies: None,
leiCode: None,
};
// Create a simple invoice with line items
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(),
};
// Create a payment request with the invoice
let mut payment_request = PaymentRequest::with_currency(
"USD".to_string(),
"25.0".to_string(),
merchant.clone(),
vec![], // Agents involved in the payment
);
// Add the invoice to the payment request
payment_request.invoice = Some(invoice);
// Convert to DIDComm message to send to the customer
let message = payment_request.to_didcomm_with_route(
Some("did:example:merchant"),
["did:example:customer"].iter().copied()
).unwrap();
// When receiving a payment request, extract and validate the invoice
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);
// Process line items
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:
```rust
pub trait TapMessageBody: DeserializeOwned + Serialize + Send + Sync {
/// Gets the message type string for this TAP message type
fn message_type() -> &'static str;
/// Converts a DIDComm message to this TAP message type
fn from_didcomm(msg: &Message) -> Result<Self, Error>;
/// Validates the message content
fn validate(&self) -> Result<(), Error>;
/// Converts this TAP message to a DIDComm message
fn to_didcomm(&self) -> Result<Message, Error>;
/// Converts this TAP message to a DIDComm message with routing information
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:
```rust
pub trait Authorizable {
/// Get the message ID for this message
fn message_id(&self) -> &str;
/// Create an authorization message for this message
fn authorize(&self, note: Option<String>) -> Authorize;
/// Create a rejection message for this message
fn reject(&self, code: String, description: String) -> Reject;
/// Create a settlement message for this message
fn settle(
&self,
settlement_id: String,
amount: Option<String>,
) -> Settle;
/// Create a cancellation message for this message
fn cancel(&self, reason: Option<String>, note: Option<String>) -> Cancel;
/// Updates policies for this message, creating an UpdatePolicies message as a response
fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies;
/// Add agents to this message, creating an AddAgents message as a response
fn add_agents(&self, transaction_id: String, agents: Vec<Participant>) -> AddAgents;
/// Replace an agent in this message, creating a ReplaceAgent message as a response
fn replace_agent(
&self,
transaction_id: String,
original: String,
replacement: Participant,
) -> ReplaceAgent;
/// Remove an agent from this message, creating a RemoveAgent message as a response
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:
1. Thread ID tracking
2. Parent thread ID tracking
3. Message correlation
4. Reply creation
```rust
pub trait TapMessage {
/// Validates the message content
fn validate(&self) -> Result<()>;
/// Checks if this is a TAP message
fn is_tap_message(&self) -> bool;
/// Gets the TAP message type for this message
fn get_tap_type(&self) -> Option<String>;
/// Attempts to convert the message to the specified TAP message type
fn body_as<T: TapMessageBody>(&self) -> Result<T>;
/// Gets all participants involved in this message
fn get_all_participants(&self) -> Vec<String>;
/// Creates a reply to this message with the specified body and creator DID
fn create_reply<T: TapMessageBody>(
&self,
body: &T,
creator_did: &str,
) -> Result<Message>;
/// Gets the message type for this message
fn message_type(&self) -> &'static str;
/// Gets the thread ID for this message, if any
fn thread_id(&self) -> Option<&str>;
/// Gets the parent thread ID for this message, if any
fn parent_thread_id(&self) -> Option<&str>;
/// Gets the unique message ID for this message
fn message_id(&self) -> &str;
}
```
## Adding New Message Types
To add a new TAP message type, follow these steps:
1. Define your message struct with required fields (must include `transaction_id: String` for most messages)
2. Implement the `TapMessageBody` trait for your struct
3. Apply the `impl_tap_message!` macro to implement the `TapMessage` trait
Here's an example:
```rust
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;
/// Define your new message struct
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyNewMessage {
/// Required transaction ID for most messages
pub transaction_id: String,
/// Other fields specific to your message
pub field1: String,
pub field2: Option<String>,
/// Optional metadata field
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
/// Implement the TapMessageBody trait
impl TapMessageBody for MyNewMessage {
/// Define the message type string (typically follows the TAP schema format)
fn message_type() -> &'static str {
"https://tap.rsvp/schema/1.0#mynewmessage"
}
/// Implement validation logic for your message fields
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(())
}
/// Implement conversion to DIDComm message
fn to_didcomm(&self, from_did: Option<&str>) -> Result<Message> {
// Implement conversion logic
// ...
}
/// Optional: Implement conversion from DIDComm message
fn from_didcomm(message: &Message) -> Result<Self> {
// Implement conversion logic
// ...
}
}
/// Apply the impl_tap_message! macro to implement the TapMessage trait
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](./examples) for more detailed examples of using TAP messages.