1use tracing::instrument;
2
3use crate::{error::map_error, EncryptedLink, PaykitAppId, PrivateApplicationMessage, Result};
4
5use super::{
6 types::{
7 AllowanceAcceptance, AllowanceEnd, AllowanceEvent, AllowanceEventMessage,
8 AllowanceProposal, AllowanceRejection,
9 },
10 wire::{
11 parse_allowance_json, parse_event_header_ids, serialize_acceptance_json,
12 serialize_allowance_json, serialize_end_json, serialize_proposal_json,
13 serialize_rejection_json,
14 },
15};
16
17pub fn parse_allowance_event_message(
23 message: &PrivateApplicationMessage,
24) -> Option<AllowanceEventMessage> {
25 let kind = message.known_kind()?;
26 let event = parse_allowance_json(kind, &message.raw_json)?;
27 let (event_id, allowance_id) = match &event {
28 Ok(event) => (
29 Some(event.event_id().clone()),
30 Some(event.allowance_id().clone()),
31 ),
32 Err(_) => parse_event_header_ids(&message.raw_json),
33 };
34 Some(AllowanceEventMessage {
35 kind,
36 app_id: serde_json::from_str::<serde_json::Value>(&message.raw_json)
37 .ok()
38 .and_then(|value| {
39 value
40 .get("app_id")?
41 .as_str()
42 .and_then(|id| PaykitAppId::new(id).ok())
43 }),
44 event_id,
45 allowance_id,
46 raw_json: message.raw_json.clone(),
47 event: event.map_err(|error| error.to_string()),
48 })
49}
50
51pub fn serialize_allowance_event(app_id: &PaykitAppId, event: &AllowanceEvent) -> Result<String> {
56 serialize_allowance_json(app_id, event)
57}
58
59#[instrument(skip(link, event))]
65pub async fn send_allowance_proposal(
66 link: &mut EncryptedLink,
67 app_id: &PaykitAppId,
68 event: &AllowanceProposal,
69) -> Result<()> {
70 send_json(
71 link,
72 serialize_proposal_json(app_id, event),
73 "send_allowance_proposal",
74 )
75 .await
76}
77
78#[instrument(skip(link, event))]
83pub async fn send_allowance_acceptance(
84 link: &mut EncryptedLink,
85 app_id: &PaykitAppId,
86 event: &AllowanceAcceptance,
87) -> Result<()> {
88 send_json(
89 link,
90 serialize_acceptance_json(app_id, event),
91 "send_allowance_acceptance",
92 )
93 .await
94}
95
96#[instrument(skip(link, event))]
101pub async fn send_allowance_rejection(
102 link: &mut EncryptedLink,
103 app_id: &PaykitAppId,
104 event: &AllowanceRejection,
105) -> Result<()> {
106 send_json(
107 link,
108 serialize_rejection_json(app_id, event),
109 "send_allowance_rejection",
110 )
111 .await
112}
113
114#[instrument(skip(link, event))]
119pub async fn send_allowance_end(
120 link: &mut EncryptedLink,
121 app_id: &PaykitAppId,
122 event: &AllowanceEnd,
123) -> Result<()> {
124 send_json(
125 link,
126 serialize_end_json(app_id, event),
127 "send_allowance_end",
128 )
129 .await
130}
131
132async fn send_json(
133 link: &mut EncryptedLink,
134 json: Result<String>,
135 context: &'static str,
136) -> Result<()> {
137 let json = json.map_err(|error| map_error(context, error))?;
138 link.send_allowance_message(json.as_bytes())
139 .await
140 .map_err(|error| map_error(context, error))
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::{
147 allowance::test_fixtures::minimal_proposal, PrivateApplicationMessage, PrivateMessageKind,
148 };
149
150 fn proposal() -> AllowanceEvent {
151 AllowanceEvent::Proposal(minimal_proposal())
152 }
153
154 #[test]
155 fn test_parser_uses_raw_json_kind_and_redacts_debug() {
156 let json =
157 serialize_allowance_event(&crate::PaykitAppId::new("bitkit").unwrap(), &proposal())
158 .unwrap();
159 let message = PrivateApplicationMessage {
160 app_id: Some("bitkit".into()),
161
162 version: Some(1),
163 kind: Some(PrivateMessageKind::ReceiptAccess.as_str().to_string()),
164 raw_json: json,
165 };
166 let parsed = parse_allowance_event_message(&message).unwrap();
167 assert!(parsed.is_valid());
168 assert_eq!(parsed.kind(), PrivateMessageKind::AllowanceProposal);
169 assert_eq!(parsed.parsed_event(), Some(&proposal()));
170 let debug = format!("{parsed:?}");
171 assert!(!debug.contains("\"asset\":\"btc\""));
172 assert!(debug.contains("<redacted:"));
173 }
174
175 #[test]
176 fn test_parser_preserves_malformed_recognized_message_for_audit() {
177 let valid =
178 serialize_allowance_event(&crate::PaykitAppId::new("bitkit").unwrap(), &proposal())
179 .unwrap();
180 let raw_json = valid.replacen("{", "{\"private_sentinel\":true,", 1);
181 let message = PrivateApplicationMessage {
182 app_id: Some("bitkit".into()),
183
184 version: Some(1),
185 kind: Some(PrivateMessageKind::AllowanceProposal.as_str().to_string()),
186 raw_json: raw_json.clone(),
187 };
188
189 let parsed = parse_allowance_event_message(&message).unwrap();
190 assert!(!parsed.is_valid());
191 assert_eq!(parsed.event_id(), Some(proposal().event_id()));
192 assert_eq!(parsed.allowance_id(), Some(proposal().allowance_id()));
193 assert_eq!(parsed.raw_json(), raw_json);
194 assert!(!format!("{parsed:?}").contains("private_sentinel"));
195 }
196
197 #[test]
198 fn test_parser_flags_reused_causal_event_ids_as_malformed() {
199 let proposal = proposal();
200 let event_id = proposal.event_id().clone();
201 let kind = PrivateMessageKind::AllowanceAcceptance;
202 let raw_json = format!(
204 "{{\"version\":1,\"kind\":\"{}\",\"event_id\":\"{event_id}\",\"allowance_id\":\"{}\",\"proposal_event_id\":\"{event_id}\"}}",
205 kind.as_str(),
206 proposal.allowance_id()
207 );
208 let message = PrivateApplicationMessage {
209 app_id: Some("bitkit".into()),
210
211 version: Some(1),
212 kind: Some(kind.as_str().to_string()),
213 raw_json,
214 };
215
216 let parsed = parse_allowance_event_message(&message).unwrap();
217 assert!(!parsed.is_valid());
218 assert_eq!(parsed.kind(), kind);
219 assert_eq!(parsed.event_id(), Some(&event_id));
220 assert_eq!(parsed.allowance_id(), Some(proposal.allowance_id()));
221 }
222
223 #[test]
224 fn test_parser_ignores_non_allowance_kinds() {
225 let message = PrivateApplicationMessage {
226 app_id: Some("bitkit".into()),
227
228 version: Some(1),
229 kind: Some(PrivateMessageKind::PaymentRequest.as_str().to_string()),
230 raw_json: format!(
231 "{{\"version\":1,\"kind\":\"{}\"}}",
232 PrivateMessageKind::PaymentRequest.as_str()
233 ),
234 };
235 assert!(parse_allowance_event_message(&message).is_none());
236 }
237}