1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Serialize, Deserialize, Clone, Debug)]
11#[serde(rename_all = "camelCase")]
12pub struct SendListParams {
13 pub recipients: Vec<String>,
14 pub message_box: String,
15 pub body: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub skip_encryption: Option<bool>,
18}
19
20#[derive(Serialize, Deserialize, Clone, Debug)]
22#[serde(rename_all = "camelCase")]
23pub struct SentRecipient {
24 pub recipient: String,
25 pub message_id: String,
26}
27
28#[derive(Serialize, Deserialize, Clone, Debug)]
30#[serde(rename_all = "camelCase")]
31pub struct FailedRecipient {
32 pub recipient: String,
33 pub error: String,
34}
35
36#[derive(Serialize, Deserialize, Clone, Debug)]
38#[serde(rename_all = "camelCase")]
39pub struct SendListTotals {
40 pub delivery_fees: i64,
41 pub recipient_fees: i64,
42 pub total_for_payable_recipients: i64,
43}
44
45#[derive(Serialize, Deserialize, Clone, Debug)]
47#[serde(rename_all = "camelCase")]
48pub struct SendListResult {
49 pub status: String,
50 pub description: String,
51 pub sent: Vec<SentRecipient>,
52 pub blocked: Vec<String>,
53 pub failed: Vec<FailedRecipient>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub totals: Option<SendListTotals>,
56}
57
58#[derive(Serialize, Deserialize, Clone, Debug)]
60#[serde(rename_all = "camelCase")]
61pub struct RecipientQuote {
62 pub recipient: String,
63 pub message_box: String,
64 pub delivery_fee: i64,
65 pub recipient_fee: i64,
66 pub status: String,
67}
68
69#[derive(Serialize, Deserialize, Clone, Debug)]
71#[serde(rename_all = "camelCase")]
72pub struct MessageBoxMultiQuote {
73 pub quotes_by_recipient: Vec<RecipientQuote>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub totals: Option<SendListTotals>,
76 pub blocked_recipients: Vec<String>,
77 pub delivery_agent_identity_key_by_host: HashMap<String, String>,
78}
79
80#[derive(Serialize, Deserialize, Clone, Debug)]
84#[serde(rename_all = "camelCase")]
85pub struct PaymentRemittanceInfo {
86 pub derivation_prefix: String,
87 pub derivation_suffix: String,
88 pub sender_identity_key: String,
89}
90
91#[derive(Serialize, Deserialize, Clone, Debug)]
93#[serde(rename_all = "camelCase")]
94pub struct InsertionRemittanceInfo {
95 pub basket: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub custom_instructions: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub tags: Option<Vec<String>>,
100}
101
102#[derive(Serialize, Deserialize, Clone, Debug)]
104#[serde(rename_all = "camelCase")]
105pub struct PaymentOutput {
106 pub output_index: u32,
107 pub protocol: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub payment_remittance: Option<PaymentRemittanceInfo>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub insertion_remittance: Option<InsertionRemittanceInfo>,
112}
113
114#[derive(Serialize, Deserialize, Clone, Debug)]
119#[serde(rename_all = "camelCase")]
120pub struct Payment {
121 pub tx: Vec<u8>,
122 pub outputs: Vec<PaymentOutput>,
123 pub description: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub labels: Option<Vec<String>>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub seek_permission: Option<bool>,
128}
129
130#[derive(Clone, Debug)]
139pub struct AdvertisementToken {
140 pub host: String,
142 pub txid: String,
144 pub output_index: u32,
146 pub locking_script: String,
148 pub beef: Vec<u8>,
150}
151
152#[derive(Serialize, Clone, Debug)]
157#[serde(rename_all = "camelCase")]
158pub struct RegisterDeviceRequest {
159 pub fcm_token: String,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub device_id: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub platform: Option<String>,
164}
165
166#[derive(Deserialize, Debug, Clone)]
172#[serde(rename_all = "camelCase")]
173pub struct RegisteredDevice {
174 pub id: Option<i64>,
175 pub device_id: Option<String>,
176 pub platform: Option<String>,
177 pub fcm_token: String,
178 pub active: Option<bool>,
179 pub created_at: Option<String>,
180 pub updated_at: Option<String>,
181 pub last_used: Option<String>,
182}
183
184#[derive(Deserialize, Clone, Debug)]
188#[serde(rename_all = "camelCase")]
189pub struct RegisterDeviceResponse {
190 pub status: String,
191 pub message: Option<String>,
192 pub device_id: Option<i64>,
193}
194
195#[derive(Deserialize, Clone, Debug)]
197#[serde(rename_all = "camelCase")]
198pub struct ListDevicesResponse {
199 pub status: String,
200 pub devices: Vec<RegisteredDevice>,
201}
202
203#[derive(Serialize, Deserialize, Clone, Debug)]
212#[serde(rename_all = "camelCase")]
213pub struct SetPermissionParams {
214 pub message_box: String,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub sender: Option<String>,
217 pub recipient_fee: i64,
219}
220
221#[derive(Serialize, Deserialize, Clone, Debug)]
227pub struct MessageBoxPermission {
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub sender: Option<String>,
230 #[serde(alias = "messageBox")]
231 pub message_box: String,
232 #[serde(alias = "recipientFee")]
233 pub recipient_fee: i64,
234 #[serde(alias = "createdAt")]
235 pub created_at: String,
236 #[serde(alias = "updatedAt")]
237 pub updated_at: String,
238}
239
240impl MessageBoxPermission {
241 pub fn status(&self) -> &str {
246 match self.recipient_fee {
247 f if f < 0 => "blocked",
248 0 => "always_allow",
249 _ => "payment_required",
250 }
251 }
252}
253
254#[derive(Clone, Debug)]
260pub struct MessageBoxQuote {
261 pub delivery_fee: i64,
262 pub recipient_fee: i64,
263 pub delivery_agent_identity_key: String,
265}
266
267#[derive(Serialize, Deserialize, Clone, Debug)]
269#[serde(rename_all = "camelCase")]
270pub struct SendMessageParams {
271 pub recipient: String,
272 pub message_box: String,
273 pub body: String,
274 pub message_id: String,
275}
276
277#[derive(Serialize, Deserialize, Clone, Debug)]
281#[serde(rename_all = "camelCase")]
282pub struct MessagePayment {
283 pub tx: Vec<u8>,
285 pub outputs: Vec<MessagePaymentOutput>,
287}
288
289#[derive(Serialize, Deserialize, Clone, Debug)]
291#[serde(rename_all = "camelCase")]
292pub struct MessagePaymentOutput {
293 pub output_index: u32,
294 pub derivation_prefix: Vec<u8>,
295 pub derivation_suffix: Vec<u8>,
296 pub sender_identity_key: String,
297}
298
299#[derive(Serialize, Deserialize, Clone, Debug)]
301pub struct SendMessageRequest {
302 pub message: SendMessageParams,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub payment: Option<MessagePayment>,
305}
306
307
308#[derive(Serialize, Deserialize, Clone, Debug)]
310#[serde(rename_all = "camelCase")]
311pub struct ListMessagesParams {
312 pub message_box: String,
313}
314
315#[derive(Serialize, Deserialize, Clone, Debug)]
317#[serde(rename_all = "camelCase")]
318pub struct AcknowledgeMessageParams {
319 pub message_ids: Vec<String>,
320}
321
322#[derive(Serialize, Deserialize, Clone, Debug)]
329pub struct ServerPeerMessage {
330 #[serde(rename = "messageId")]
331 pub message_id: String,
332 pub body: String,
333 pub sender: String,
334 #[serde(alias = "createdAt")]
335 pub created_at: String,
336 #[serde(alias = "updatedAt")]
337 pub updated_at: String,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub acknowledged: Option<bool>,
340}
341
342#[derive(Serialize, Deserialize, Clone, Debug)]
344#[serde(rename_all = "camelCase")]
345pub struct ListMessagesResponse {
346 pub status: String,
347 pub messages: Vec<ServerPeerMessage>,
348}
349
350#[derive(Serialize, Deserialize, Clone, Debug)]
352#[serde(rename_all = "camelCase")]
353pub struct SendMessageResponse {
354 pub status: String,
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub message_id: Option<String>,
358}
359
360#[derive(Serialize, Deserialize, Clone, Debug)]
369#[serde(rename_all = "camelCase")]
370pub struct WsSendMessageData {
371 pub room_id: String,
372 pub message: WsSendMessagePayload,
373}
374
375#[derive(Serialize, Deserialize, Clone, Debug)]
377#[serde(rename_all = "camelCase")]
378pub struct WsSendMessagePayload {
379 pub message_id: String,
380 pub recipient: String,
381 pub body: String,
382}
383
384pub const PAYMENT_REQUESTS_MESSAGEBOX: &str = "payment_requests";
390pub const PAYMENT_REQUEST_RESPONSES_MESSAGEBOX: &str = "payment_request_responses";
391
392pub const DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT: u64 = 1000;
394pub const DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT: u64 = 10_000_000;
395
396#[derive(Serialize, Deserialize, Clone, Debug)]
401#[serde(rename_all = "camelCase")]
402pub struct PaymentRequestMessage {
403 pub request_id: String,
404 pub sender_identity_key: String,
405 pub request_proof: String,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub amount: Option<u64>,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub description: Option<String>,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub expires_at: Option<u64>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub cancelled: Option<bool>,
414}
415
416#[derive(Serialize, Deserialize, Clone, Debug)]
419#[serde(rename_all = "camelCase")]
420pub struct PaymentRequestResponse {
421 pub request_id: String,
422 pub status: String, #[serde(skip_serializing_if = "Option::is_none")]
424 pub note: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub amount_paid: Option<u64>,
427}
428
429#[derive(Clone, Debug)]
431pub struct IncomingPaymentRequest {
432 pub message_id: String,
433 pub sender: String,
434 pub request_id: String,
435 pub amount: u64,
436 pub description: String,
437 pub expires_at: u64,
438}
439
440#[derive(Clone, Debug)]
442pub struct PaymentRequestLimits {
443 pub min_amount: u64,
444 pub max_amount: u64,
445}
446
447impl Default for PaymentRequestLimits {
448 fn default() -> Self {
449 Self {
450 min_amount: DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT,
451 max_amount: DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT,
452 }
453 }
454}
455
456#[derive(Clone, Debug)]
458pub struct PaymentRequestResult {
459 pub request_id: String,
460 pub request_proof: String,
461}
462
463#[derive(Serialize, Deserialize, Clone, Debug)]
472#[serde(rename_all = "camelCase")]
473pub struct PaymentCustomInstructions {
474 pub derivation_prefix: String,
475 pub derivation_suffix: String,
476 #[serde(skip_serializing_if = "Option::is_none")]
477 pub payee: Option<String>,
478}
479
480#[derive(Serialize, Deserialize, Clone, Debug)]
486#[serde(rename_all = "camelCase")]
487pub struct PaymentToken {
488 pub custom_instructions: PaymentCustomInstructions,
489 pub transaction: Vec<u8>,
491 pub amount: u64,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub output_index: Option<u32>,
495}
496
497#[derive(Clone, Debug)]
502pub struct IncomingPayment {
503 pub token: PaymentToken,
504 pub sender: String,
505 pub message_id: String,
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
517 fn set_permission_params_serializes_camel_case() {
518 let p = SetPermissionParams {
519 message_box: "payment_inbox".to_string(),
520 sender: None,
521 recipient_fee: 100,
522 };
523 let json = serde_json::to_string(&p).unwrap();
524 assert!(json.contains("\"messageBox\""), "messageBox field name");
525 assert!(json.contains("\"recipientFee\""), "recipientFee field name");
526 assert!(!json.contains("message_box"), "no snake_case leakage");
527 assert!(!json.contains("recipient_fee"), "no snake_case leakage");
528 assert!(!json.contains("sender"), "sender absent when None");
529 }
530
531 #[test]
532 fn set_permission_params_includes_sender_when_some() {
533 let p = SetPermissionParams {
534 message_box: "inbox".to_string(),
535 sender: Some("03abc".to_string()),
536 recipient_fee: 0,
537 };
538 let json = serde_json::to_string(&p).unwrap();
539 assert!(json.contains("\"sender\""), "sender present when Some");
540 assert!(json.contains("\"03abc\""), "sender value correct");
541 }
542
543 #[test]
544 fn message_box_permission_deserializes_camel_case() {
545 let raw = r#"{
546 "messageBox": "payment_inbox",
547 "recipientFee": 50,
548 "createdAt": "2024-01-01T00:00:00Z",
549 "updatedAt": "2024-01-02T00:00:00Z"
550 }"#;
551 let perm: MessageBoxPermission = serde_json::from_str(raw).unwrap();
552 assert_eq!(perm.message_box, "payment_inbox");
553 assert_eq!(perm.recipient_fee, 50);
554 assert_eq!(perm.created_at, "2024-01-01T00:00:00Z");
555 assert_eq!(perm.updated_at, "2024-01-02T00:00:00Z");
556 }
557
558 #[test]
559 fn message_box_permission_deserializes_snake_case() {
560 let raw = r#"{
562 "message_box": "payment_inbox",
563 "recipient_fee": 75,
564 "created_at": "2024-01-01T00:00:00Z",
565 "updated_at": "2024-01-02T00:00:00Z"
566 }"#;
567 let perm: MessageBoxPermission = serde_json::from_str(raw).unwrap();
568 assert_eq!(perm.message_box, "payment_inbox");
569 assert_eq!(perm.recipient_fee, 75);
570 assert_eq!(perm.created_at, "2024-01-01T00:00:00Z");
571 assert_eq!(perm.updated_at, "2024-01-02T00:00:00Z");
572 }
573
574 #[test]
575 fn message_box_permission_status_computes_correctly() {
576 let make = |fee: i64| MessageBoxPermission {
577 sender: None,
578 message_box: "inbox".to_string(),
579 recipient_fee: fee,
580 created_at: "2024-01-01".to_string(),
581 updated_at: "2024-01-01".to_string(),
582 };
583 assert_eq!(make(-1).status(), "blocked");
584 assert_eq!(make(0).status(), "always_allow");
585 assert_eq!(make(100).status(), "payment_required");
586 assert_eq!(make(1).status(), "payment_required");
587 }
588
589 #[test]
590 fn message_box_quote_can_be_constructed() {
591 let quote = MessageBoxQuote {
593 delivery_fee: 10,
594 recipient_fee: 50,
595 delivery_agent_identity_key: "03deadbeef".to_string(),
596 };
597 assert_eq!(quote.delivery_fee, 10);
598 assert_eq!(quote.recipient_fee, 50);
599 assert_eq!(quote.delivery_agent_identity_key, "03deadbeef");
600 }
601
602 #[test]
607 fn payment_custom_instructions_round_trip() {
608 let ci = PaymentCustomInstructions {
609 derivation_prefix: "pfx123".to_string(),
610 derivation_suffix: "sfx456".to_string(),
611 payee: Some("03abc".to_string()),
612 };
613 let json = serde_json::to_string(&ci).unwrap();
614 let back: PaymentCustomInstructions = serde_json::from_str(&json).unwrap();
615 assert_eq!(back.derivation_prefix, "pfx123");
616 assert_eq!(back.derivation_suffix, "sfx456");
617 assert_eq!(back.payee, Some("03abc".to_string()));
618 }
619
620 #[test]
621 fn payment_token_serializes_camel_case() {
622 let token = PaymentToken {
623 custom_instructions: PaymentCustomInstructions {
624 derivation_prefix: "pfx".to_string(),
625 derivation_suffix: "sfx".to_string(),
626 payee: Some("03recipient".to_string()),
627 },
628 transaction: vec![1, 2, 3],
629 amount: 1000,
630 output_index: Some(0),
631 };
632 let json = serde_json::to_string(&token).unwrap();
633 assert!(json.contains("\"customInstructions\""), "customInstructions field name");
635 assert!(json.contains("\"derivationPrefix\""), "derivationPrefix field name");
636 assert!(json.contains("\"derivationSuffix\""), "derivationSuffix field name");
637 assert!(json.contains("\"payee\""), "payee present when Some");
638 assert!(json.contains("\"outputIndex\""), "outputIndex present when Some");
639 assert!(!json.contains("custom_instructions"), "no snake_case leakage");
641 assert!(!json.contains("derivation_prefix"), "no snake_case leakage");
642 assert!(!json.contains("output_index"), "no snake_case leakage");
643 }
644
645 #[test]
646 fn payment_token_no_output_index_by_default() {
647 let token = PaymentToken {
648 custom_instructions: PaymentCustomInstructions {
649 derivation_prefix: "pfx".to_string(),
650 derivation_suffix: "sfx".to_string(),
651 payee: None,
652 },
653 transaction: vec![0xab, 0xcd],
654 amount: 500,
655 output_index: None,
656 };
657 let json = serde_json::to_string(&token).unwrap();
658 assert!(!json.contains("outputIndex"), "outputIndex absent when None");
660 assert!(!json.contains("payee"), "payee absent when None");
662 }
663
664 #[test]
665 fn incoming_payment_can_be_constructed() {
666 let token = PaymentToken {
667 custom_instructions: PaymentCustomInstructions {
668 derivation_prefix: "p".to_string(),
669 derivation_suffix: "s".to_string(),
670 payee: None,
671 },
672 transaction: vec![0x01],
673 amount: 2000,
674 output_index: None,
675 };
676 let incoming = IncomingPayment {
677 token: token.clone(),
678 sender: "03sender".to_string(),
679 message_id: "msg001".to_string(),
680 };
681 assert_eq!(incoming.sender, "03sender");
682 assert_eq!(incoming.message_id, "msg001");
683 assert_eq!(incoming.token.amount, 2000);
684 }
685
686 #[test]
691 fn register_device_request_camel_case() {
692 let req = RegisterDeviceRequest {
693 fcm_token: "tok123".to_string(),
694 device_id: Some("dev-abc".to_string()),
695 platform: Some("android".to_string()),
696 };
697 let json = serde_json::to_string(&req).unwrap();
698 assert!(json.contains("\"fcmToken\""), "fcmToken field name");
699 assert!(json.contains("\"deviceId\""), "deviceId field name");
700 assert!(json.contains("\"platform\""), "platform field name");
701 assert!(!json.contains("fcm_token"), "no snake_case leakage");
702 assert!(!json.contains("device_id"), "no snake_case leakage");
703 }
704
705 #[test]
706 fn register_device_request_omits_optional_fields_when_none() {
707 let req = RegisterDeviceRequest {
708 fcm_token: "tok456".to_string(),
709 device_id: None,
710 platform: None,
711 };
712 let json = serde_json::to_string(&req).unwrap();
713 assert!(json.contains("\"fcmToken\""), "fcmToken present");
714 assert!(!json.contains("deviceId"), "deviceId absent when None");
715 assert!(!json.contains("platform"), "platform absent when None");
716 }
717
718 #[test]
719 fn registered_device_deserializes_camel_case() {
720 let raw = r#"{
721 "id": 42,
722 "deviceId": "dev-123",
723 "platform": "ios",
724 "fcmToken": "fcm-abc",
725 "active": true,
726 "createdAt": "2024-01-01T00:00:00Z",
727 "updatedAt": "2024-01-02T00:00:00Z",
728 "lastUsed": "2024-01-03T00:00:00Z"
729 }"#;
730 let dev: RegisteredDevice = serde_json::from_str(raw).unwrap();
731 assert_eq!(dev.id, Some(42));
732 assert_eq!(dev.device_id.as_deref(), Some("dev-123"));
733 assert_eq!(dev.platform.as_deref(), Some("ios"));
734 assert_eq!(dev.fcm_token, "fcm-abc");
735 assert_eq!(dev.active, Some(true));
736 assert_eq!(dev.created_at.as_deref(), Some("2024-01-01T00:00:00Z"));
737 assert_eq!(dev.last_used.as_deref(), Some("2024-01-03T00:00:00Z"));
738 }
739
740 #[test]
745 fn send_message_params_serializes_camel_case() {
746 let p = SendMessageParams {
747 recipient: "03abc".to_string(),
748 message_box: "inbox".to_string(),
749 body: "hello".to_string(),
750 message_id: "deadbeef".to_string(),
751 };
752 let json = serde_json::to_string(&p).unwrap();
753 assert!(json.contains("\"messageBox\""), "messageBox field name");
754 assert!(json.contains("\"messageId\""), "messageId field name");
755 assert!(!json.contains("message_box"), "no snake_case leakage");
756 assert!(!json.contains("message_id"), "no snake_case leakage");
757 }
758
759 #[test]
760 fn acknowledge_params_serializes_camel_case() {
761 let p = AcknowledgeMessageParams {
762 message_ids: vec!["id1".to_string(), "id2".to_string()],
763 };
764 let json = serde_json::to_string(&p).unwrap();
765 assert!(json.contains("\"messageIds\""), "messageIds field name");
766 assert!(!json.contains("message_ids"), "no snake_case leakage");
767 }
768
769 #[test]
770 fn server_peer_message_tolerates_unknown_fields() {
771 let raw = r#"{
773 "messageId": "abc123",
774 "body": "hello",
775 "sender": "03xyz",
776 "created_at": "2024-01-01T00:00:00Z",
777 "updated_at": "2024-01-01T00:00:00Z",
778 "acknowledged": false,
779 "unknownField": "should be ignored",
780 "anotherExtra": 42
781 }"#;
782 let msg: ServerPeerMessage = serde_json::from_str(raw).unwrap();
783 assert_eq!(msg.message_id, "abc123");
784 assert_eq!(msg.body, "hello");
785 assert_eq!(msg.acknowledged, Some(false));
786 }
787}