TAP Message
Core message processing for the Transaction Authorization Protocol (TAP) providing secure message types and validation.
Features
- TAP Message Types: Complete implementation of all TAP message types
- Message Security: Support for secure message formats with JWS (signed) and JWE (encrypted) capabilities
- Attachments Support: Full support for message attachments in Base64, JSON, and Links formats with optional JWS
- 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::Transfer;
use tap_msg::message::Participant;
use tap_msg::message::tap_message_trait::TapMessageBody;
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,
name: None,
};
let beneficiary = Participant {
id: "did:example:receiver".to_string(),
role: Some("beneficiary".to_string()),
policies: None,
leiCode: None,
name: None,
};
let transfer = Transfer {
asset,
originator,
beneficiary: Some(beneficiary),
amount: "100.0".to_string(),
agents: vec![],
settlement_id: None,
memo: Some("Test transfer".to_string()),
metadata: HashMap::new(),
};
transfer.validate()?;
Payment Request with Invoice
use tap_msg::message::{Payment, 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,
name: 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 = Payment::with_currency(
"USD".to_string(),
"100.0".to_string(),
merchant,
vec![], );
payment_request.invoice = Some(invoice);
payment_request.validate()?;
Message Types
Plain Message
The PlainMessage struct is the core representation of a message in TAP:
pub struct PlainMessage {
pub id: String,
pub typ: String,
pub type_: String,
pub body: Value,
pub from: String,
pub to: Vec<String>,
pub thid: Option<String>,
pub pthid: Option<String>,
pub extra_headers: HashMap<String, Value>,
pub created_time: Option<u64>,
pub expires_time: Option<u64>,
pub from_prior: Option<String>,
pub attachments: Option<Vec<Attachment>>,
}
The attachments field supports all attachment formats and can be used to include additional data with messages.
Transfer
The Transfer struct represents a TAP transfer message, which is the core message type in the protocol:
pub struct Transfer {
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)]),
leiCode: None,
name: None,
};
let proof_policy = RequireProofOfControl::default(); let update_policies = UpdatePolicies {
transaction_id: "transfer_12345".to_string(),
policies: vec![Policy::RequireProofOfControl(proof_policy)],
};
update_policies.validate()?;
Authorization Messages
TAP supports various authorization messages for compliance workflows:
pub struct Authorize {
pub transfer_id: String,
pub note: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
pub struct Reject {
pub transaction_id: String,
pub reason: String,
pub note: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
pub struct Settle {
pub transaction_id: String,
pub settlement_id: String,
pub amount: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
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>,
}
Presentation
The Presentation struct represents a verifiable presentation message:
pub struct Presentation {
pub formats: Vec<String>,
pub attachments: Vec<Attachment>,
pub thid: Option<String>,
}
This structure enables compatibility with the present-proof protocol and enforces the requirement for format field and attachments validation. The format field is required for each attachment and must match one of the formats specified in the presentation.
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::message::{Payment, Invoice, LineItem, TaxCategory, TaxTotal, TaxSubtotal};
use tap_msg::message::tap_message_trait::TapMessageBody;
use tap_msg::message::Participant;
use std::collections::HashMap;
let merchant = Participant {
id: "did:example:merchant".to_string(),
role: Some("merchant".to_string()),
policies: None,
leiCode: None,
name: 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 = Payment::with_currency(
"USD".to_string(),
"25.0".to_string(),
merchant.clone(),
vec![], );
payment_request.invoice = Some(invoice);
Message Security
The TAP protocol provides several security modes:
- Plain: No security, for testing only
- Signed: Messages are signed to ensure integrity
- AuthCrypt: Messages are both signed and encrypted for confidentiality
When using the tap-agent crate with this message library, you can specify the security mode when sending messages:
use tap_agent::agent::Agent;
use tap_agent::message::SecurityMode;
use tap_msg::message::Transfer;
async fn send_secure_message(
agent: &impl Agent,
transfer: &Transfer,
recipient: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let (packed_message, _) = agent.send_message(
transfer,
vec![recipient],
SecurityMode::AuthCrypt,
false, ).await?;
Ok(packed_message)
}
Message Attachments
TAP supports message attachments through the Attachment struct and related types:
use tap_msg::didcomm::{Attachment, AttachmentData, JsonAttachmentData};
use serde_json::json;
let attachment = Attachment {
id: Some("attachment-1".to_string()),
media_type: Some("application/json".to_string()),
data: AttachmentData::Json {
value: JsonAttachmentData {
json: json!({
"key": "value",
"nested": {
"data": "example"
}
}),
jws: None,
},
},
description: Some("Example attachment".to_string()),
filename: None,
format: Some("json/schema@v1".to_string()),
lastmod_time: None,
byte_count: None,
};
let presentation = Presentation {
formats: vec!["dif/presentation-exchange/submission@v1.0".to_string()],
attachments: vec![attachment],
thid: Some("thread-123".to_string()),
};
presentation.validate()?;
Message Validation
TAP messages implement the TapMessageBody trait, which provides a validate() method for checking message correctness:
pub trait TapMessageBody: DeserializeOwned + Serialize + Send + Sync {
fn message_type() -> &'static str;
fn to_wire(&self) -> Result<Value>;
fn validate(&self) -> Result<()>;
}
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, reason: String, note: Option<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;
}
Adding New Message Types
To add a new TAP message type, follow these steps:
- Define your message struct with required fields
- Implement the
TapMessageBody trait for your struct
- Optional: Implement the
Authorizable trait for messages that can be authorized
Here's an example:
use tap_msg::message::tap_message_trait::TapMessageBody;
use tap_msg::error::Result;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyNewMessage {
pub 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 to_wire(&self) -> Result<serde_json::Value> {
serde_json::to_value(self).map_err(|e| tap_msg::error::Error::Serialization(e.to_string()))
}
fn validate(&self) -> Result<()> {
if self.id.is_empty() {
return Err(tap_msg::error::Error::Validation("ID is required".to_string()));
}
if self.field1.is_empty() {
return Err(tap_msg::error::Error::Validation("Field1 is required".to_string()));
}
Ok(())
}
}
Examples
See the examples directory for more detailed examples of using TAP messages.