use tracing::instrument;
use crate::{error::map_error, EncryptedLink, PaykitAppId, PrivateApplicationMessage, Result};
use super::{
types::{
AllowanceAcceptance, AllowanceEnd, AllowanceEvent, AllowanceEventMessage,
AllowanceProposal, AllowanceRejection,
},
wire::{
parse_allowance_json, parse_event_header_ids, serialize_acceptance_json,
serialize_allowance_json, serialize_end_json, serialize_proposal_json,
serialize_rejection_json,
},
};
pub fn parse_allowance_event_message(
message: &PrivateApplicationMessage,
) -> Option<AllowanceEventMessage> {
let kind = message.known_kind()?;
let event = parse_allowance_json(kind, &message.raw_json)?;
let (event_id, allowance_id) = match &event {
Ok(event) => (
Some(event.event_id().clone()),
Some(event.allowance_id().clone()),
),
Err(_) => parse_event_header_ids(&message.raw_json),
};
Some(AllowanceEventMessage {
kind,
app_id: serde_json::from_str::<serde_json::Value>(&message.raw_json)
.ok()
.and_then(|value| {
value
.get("app_id")?
.as_str()
.and_then(|id| PaykitAppId::new(id).ok())
}),
event_id,
allowance_id,
raw_json: message.raw_json.clone(),
event: event.map_err(|error| error.to_string()),
})
}
pub fn serialize_allowance_event(app_id: &PaykitAppId, event: &AllowanceEvent) -> Result<String> {
serialize_allowance_json(app_id, event)
}
#[instrument(skip(link, event))]
pub async fn send_allowance_proposal(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &AllowanceProposal,
) -> Result<()> {
send_json(
link,
serialize_proposal_json(app_id, event),
"send_allowance_proposal",
)
.await
}
#[instrument(skip(link, event))]
pub async fn send_allowance_acceptance(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &AllowanceAcceptance,
) -> Result<()> {
send_json(
link,
serialize_acceptance_json(app_id, event),
"send_allowance_acceptance",
)
.await
}
#[instrument(skip(link, event))]
pub async fn send_allowance_rejection(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &AllowanceRejection,
) -> Result<()> {
send_json(
link,
serialize_rejection_json(app_id, event),
"send_allowance_rejection",
)
.await
}
#[instrument(skip(link, event))]
pub async fn send_allowance_end(
link: &mut EncryptedLink,
app_id: &PaykitAppId,
event: &AllowanceEnd,
) -> Result<()> {
send_json(
link,
serialize_end_json(app_id, event),
"send_allowance_end",
)
.await
}
async fn send_json(
link: &mut EncryptedLink,
json: Result<String>,
context: &'static str,
) -> Result<()> {
let json = json.map_err(|error| map_error(context, error))?;
link.send_allowance_message(json.as_bytes())
.await
.map_err(|error| map_error(context, error))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
allowance::test_fixtures::minimal_proposal, PrivateApplicationMessage, PrivateMessageKind,
};
fn proposal() -> AllowanceEvent {
AllowanceEvent::Proposal(minimal_proposal())
}
#[test]
fn test_parser_uses_raw_json_kind_and_redacts_debug() {
let json =
serialize_allowance_event(&crate::PaykitAppId::new("bitkit").unwrap(), &proposal())
.unwrap();
let message = PrivateApplicationMessage {
app_id: Some("bitkit".into()),
version: Some(1),
kind: Some(PrivateMessageKind::ReceiptAccess.as_str().to_string()),
raw_json: json,
};
let parsed = parse_allowance_event_message(&message).unwrap();
assert!(parsed.is_valid());
assert_eq!(parsed.kind(), PrivateMessageKind::AllowanceProposal);
assert_eq!(parsed.parsed_event(), Some(&proposal()));
let debug = format!("{parsed:?}");
assert!(!debug.contains("\"asset\":\"btc\""));
assert!(debug.contains("<redacted:"));
}
#[test]
fn test_parser_preserves_malformed_recognized_message_for_audit() {
let valid =
serialize_allowance_event(&crate::PaykitAppId::new("bitkit").unwrap(), &proposal())
.unwrap();
let raw_json = valid.replacen("{", "{\"private_sentinel\":true,", 1);
let message = PrivateApplicationMessage {
app_id: Some("bitkit".into()),
version: Some(1),
kind: Some(PrivateMessageKind::AllowanceProposal.as_str().to_string()),
raw_json: raw_json.clone(),
};
let parsed = parse_allowance_event_message(&message).unwrap();
assert!(!parsed.is_valid());
assert_eq!(parsed.event_id(), Some(proposal().event_id()));
assert_eq!(parsed.allowance_id(), Some(proposal().allowance_id()));
assert_eq!(parsed.raw_json(), raw_json);
assert!(!format!("{parsed:?}").contains("private_sentinel"));
}
#[test]
fn test_parser_flags_reused_causal_event_ids_as_malformed() {
let proposal = proposal();
let event_id = proposal.event_id().clone();
let kind = PrivateMessageKind::AllowanceAcceptance;
let raw_json = format!(
"{{\"version\":1,\"kind\":\"{}\",\"event_id\":\"{event_id}\",\"allowance_id\":\"{}\",\"proposal_event_id\":\"{event_id}\"}}",
kind.as_str(),
proposal.allowance_id()
);
let message = PrivateApplicationMessage {
app_id: Some("bitkit".into()),
version: Some(1),
kind: Some(kind.as_str().to_string()),
raw_json,
};
let parsed = parse_allowance_event_message(&message).unwrap();
assert!(!parsed.is_valid());
assert_eq!(parsed.kind(), kind);
assert_eq!(parsed.event_id(), Some(&event_id));
assert_eq!(parsed.allowance_id(), Some(proposal.allowance_id()));
}
#[test]
fn test_parser_ignores_non_allowance_kinds() {
let message = PrivateApplicationMessage {
app_id: Some("bitkit".into()),
version: Some(1),
kind: Some(PrivateMessageKind::PaymentRequest.as_str().to_string()),
raw_json: format!(
"{{\"version\":1,\"kind\":\"{}\"}}",
PrivateMessageKind::PaymentRequest.as_str()
),
};
assert!(parse_allowance_event_message(&message).is_none());
}
}