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#[derive(Serialize, Deserialize, Clone, Debug)]
309#[serde(rename_all = "camelCase")]
310pub struct ListMessagesParams {
311 pub message_box: String,
312}
313
314#[derive(Serialize, Deserialize, Clone, Debug)]
316#[serde(rename_all = "camelCase")]
317pub struct AcknowledgeMessageParams {
318 pub message_ids: Vec<String>,
319}
320
321#[derive(Serialize, Deserialize, Clone, Debug)]
328pub struct ServerPeerMessage {
329 #[serde(rename = "messageId")]
330 pub message_id: String,
331 pub body: String,
332 pub sender: String,
333 #[serde(alias = "createdAt")]
334 pub created_at: String,
335 #[serde(alias = "updatedAt")]
336 pub updated_at: String,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub acknowledged: Option<bool>,
339 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
347 pub authenticated_decrypt: bool,
348}
349
350#[derive(Clone, Debug)]
360pub struct AuthenticatedPeerMessage {
361 pub message_id: String,
362 pub sender: String,
363 pub recipient: String,
364 pub message_box: String,
365 pub body: String,
366 pub authenticated_decrypt: bool,
369}
370
371#[derive(Serialize, Deserialize, Clone, Debug)]
373#[serde(rename_all = "camelCase")]
374pub struct ListMessagesResponse {
375 pub status: String,
376 pub messages: Vec<ServerPeerMessage>,
377}
378
379#[derive(Serialize, Deserialize, Clone, Debug)]
381#[serde(rename_all = "camelCase")]
382pub struct SendMessageResponse {
383 pub status: String,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub message_id: Option<String>,
387}
388
389#[derive(Serialize, Deserialize, Clone, Debug)]
398#[serde(rename_all = "camelCase")]
399pub struct WsSendMessageData {
400 pub room_id: String,
401 pub message: WsSendMessagePayload,
402}
403
404#[derive(Serialize, Deserialize, Clone, Debug)]
406#[serde(rename_all = "camelCase")]
407pub struct WsSendMessagePayload {
408 pub message_id: String,
409 pub recipient: String,
410 pub body: String,
411}
412
413pub const PAYMENT_REQUESTS_MESSAGEBOX: &str = "payment_requests";
419pub const PAYMENT_REQUEST_RESPONSES_MESSAGEBOX: &str = "payment_request_responses";
420
421pub const DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT: u64 = 1000;
423pub const DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT: u64 = 10_000_000;
424
425#[derive(Serialize, Deserialize, Clone, Debug)]
430#[serde(rename_all = "camelCase")]
431pub struct PaymentRequestMessage {
432 pub request_id: String,
433 pub sender_identity_key: String,
434 pub request_proof: String,
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub amount: Option<u64>,
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub description: Option<String>,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 pub expires_at: Option<u64>,
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub cancelled: Option<bool>,
443}
444
445#[derive(Serialize, Deserialize, Clone, Debug)]
448#[serde(rename_all = "camelCase")]
449pub struct PaymentRequestResponse {
450 pub request_id: String,
451 pub status: String, #[serde(skip_serializing_if = "Option::is_none")]
453 pub note: Option<String>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub amount_paid: Option<u64>,
456}
457
458#[derive(Clone, Debug)]
460pub struct IncomingPaymentRequest {
461 pub message_id: String,
462 pub sender: String,
463 pub request_id: String,
464 pub amount: u64,
465 pub description: String,
466 pub expires_at: u64,
467}
468
469#[derive(Clone, Debug)]
471pub struct PaymentRequestLimits {
472 pub min_amount: u64,
473 pub max_amount: u64,
474}
475
476impl Default for PaymentRequestLimits {
477 fn default() -> Self {
478 Self {
479 min_amount: DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT,
480 max_amount: DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT,
481 }
482 }
483}
484
485#[derive(Clone, Debug)]
487pub struct PaymentRequestResult {
488 pub request_id: String,
489 pub request_proof: String,
490}
491
492#[derive(Serialize, Deserialize, Clone, Debug)]
501#[serde(rename_all = "camelCase")]
502pub struct PaymentCustomInstructions {
503 pub derivation_prefix: String,
504 pub derivation_suffix: String,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub payee: Option<String>,
507}
508
509#[derive(Serialize, Deserialize, Clone, Debug)]
515#[serde(rename_all = "camelCase")]
516pub struct PaymentToken {
517 pub custom_instructions: PaymentCustomInstructions,
518 pub transaction: Vec<u8>,
520 pub amount: u64,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub output_index: Option<u32>,
524}
525
526#[derive(Clone, Debug)]
531pub struct IncomingPayment {
532 pub token: PaymentToken,
533 pub sender: String,
534 pub message_id: String,
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
546 fn set_permission_params_serializes_camel_case() {
547 let p = SetPermissionParams {
548 message_box: "payment_inbox".to_string(),
549 sender: None,
550 recipient_fee: 100,
551 };
552 let json = serde_json::to_string(&p).unwrap();
553 assert!(json.contains("\"messageBox\""), "messageBox field name");
554 assert!(json.contains("\"recipientFee\""), "recipientFee field name");
555 assert!(!json.contains("message_box"), "no snake_case leakage");
556 assert!(!json.contains("recipient_fee"), "no snake_case leakage");
557 assert!(!json.contains("sender"), "sender absent when None");
558 }
559
560 #[test]
561 fn set_permission_params_includes_sender_when_some() {
562 let p = SetPermissionParams {
563 message_box: "inbox".to_string(),
564 sender: Some("03abc".to_string()),
565 recipient_fee: 0,
566 };
567 let json = serde_json::to_string(&p).unwrap();
568 assert!(json.contains("\"sender\""), "sender present when Some");
569 assert!(json.contains("\"03abc\""), "sender value correct");
570 }
571
572 #[test]
573 fn message_box_permission_deserializes_camel_case() {
574 let raw = r#"{
575 "messageBox": "payment_inbox",
576 "recipientFee": 50,
577 "createdAt": "2024-01-01T00:00:00Z",
578 "updatedAt": "2024-01-02T00:00:00Z"
579 }"#;
580 let perm: MessageBoxPermission = serde_json::from_str(raw).unwrap();
581 assert_eq!(perm.message_box, "payment_inbox");
582 assert_eq!(perm.recipient_fee, 50);
583 assert_eq!(perm.created_at, "2024-01-01T00:00:00Z");
584 assert_eq!(perm.updated_at, "2024-01-02T00:00:00Z");
585 }
586
587 #[test]
588 fn message_box_permission_deserializes_snake_case() {
589 let raw = r#"{
591 "message_box": "payment_inbox",
592 "recipient_fee": 75,
593 "created_at": "2024-01-01T00:00:00Z",
594 "updated_at": "2024-01-02T00:00:00Z"
595 }"#;
596 let perm: MessageBoxPermission = serde_json::from_str(raw).unwrap();
597 assert_eq!(perm.message_box, "payment_inbox");
598 assert_eq!(perm.recipient_fee, 75);
599 assert_eq!(perm.created_at, "2024-01-01T00:00:00Z");
600 assert_eq!(perm.updated_at, "2024-01-02T00:00:00Z");
601 }
602
603 #[test]
604 fn message_box_permission_status_computes_correctly() {
605 let make = |fee: i64| MessageBoxPermission {
606 sender: None,
607 message_box: "inbox".to_string(),
608 recipient_fee: fee,
609 created_at: "2024-01-01".to_string(),
610 updated_at: "2024-01-01".to_string(),
611 };
612 assert_eq!(make(-1).status(), "blocked");
613 assert_eq!(make(0).status(), "always_allow");
614 assert_eq!(make(100).status(), "payment_required");
615 assert_eq!(make(1).status(), "payment_required");
616 }
617
618 #[test]
619 fn message_box_quote_can_be_constructed() {
620 let quote = MessageBoxQuote {
622 delivery_fee: 10,
623 recipient_fee: 50,
624 delivery_agent_identity_key: "03deadbeef".to_string(),
625 };
626 assert_eq!(quote.delivery_fee, 10);
627 assert_eq!(quote.recipient_fee, 50);
628 assert_eq!(quote.delivery_agent_identity_key, "03deadbeef");
629 }
630
631 #[test]
636 fn payment_custom_instructions_round_trip() {
637 let ci = PaymentCustomInstructions {
638 derivation_prefix: "pfx123".to_string(),
639 derivation_suffix: "sfx456".to_string(),
640 payee: Some("03abc".to_string()),
641 };
642 let json = serde_json::to_string(&ci).unwrap();
643 let back: PaymentCustomInstructions = serde_json::from_str(&json).unwrap();
644 assert_eq!(back.derivation_prefix, "pfx123");
645 assert_eq!(back.derivation_suffix, "sfx456");
646 assert_eq!(back.payee, Some("03abc".to_string()));
647 }
648
649 #[test]
650 fn payment_token_serializes_camel_case() {
651 let token = PaymentToken {
652 custom_instructions: PaymentCustomInstructions {
653 derivation_prefix: "pfx".to_string(),
654 derivation_suffix: "sfx".to_string(),
655 payee: Some("03recipient".to_string()),
656 },
657 transaction: vec![1, 2, 3],
658 amount: 1000,
659 output_index: Some(0),
660 };
661 let json = serde_json::to_string(&token).unwrap();
662 assert!(
664 json.contains("\"customInstructions\""),
665 "customInstructions field name"
666 );
667 assert!(
668 json.contains("\"derivationPrefix\""),
669 "derivationPrefix field name"
670 );
671 assert!(
672 json.contains("\"derivationSuffix\""),
673 "derivationSuffix field name"
674 );
675 assert!(json.contains("\"payee\""), "payee present when Some");
676 assert!(
677 json.contains("\"outputIndex\""),
678 "outputIndex present when Some"
679 );
680 assert!(
682 !json.contains("custom_instructions"),
683 "no snake_case leakage"
684 );
685 assert!(!json.contains("derivation_prefix"), "no snake_case leakage");
686 assert!(!json.contains("output_index"), "no snake_case leakage");
687 }
688
689 #[test]
690 fn payment_token_no_output_index_by_default() {
691 let token = PaymentToken {
692 custom_instructions: PaymentCustomInstructions {
693 derivation_prefix: "pfx".to_string(),
694 derivation_suffix: "sfx".to_string(),
695 payee: None,
696 },
697 transaction: vec![0xab, 0xcd],
698 amount: 500,
699 output_index: None,
700 };
701 let json = serde_json::to_string(&token).unwrap();
702 assert!(
704 !json.contains("outputIndex"),
705 "outputIndex absent when None"
706 );
707 assert!(!json.contains("payee"), "payee absent when None");
709 }
710
711 #[test]
712 fn incoming_payment_can_be_constructed() {
713 let token = PaymentToken {
714 custom_instructions: PaymentCustomInstructions {
715 derivation_prefix: "p".to_string(),
716 derivation_suffix: "s".to_string(),
717 payee: None,
718 },
719 transaction: vec![0x01],
720 amount: 2000,
721 output_index: None,
722 };
723 let incoming = IncomingPayment {
724 token: token.clone(),
725 sender: "03sender".to_string(),
726 message_id: "msg001".to_string(),
727 };
728 assert_eq!(incoming.sender, "03sender");
729 assert_eq!(incoming.message_id, "msg001");
730 assert_eq!(incoming.token.amount, 2000);
731 }
732
733 #[test]
738 fn register_device_request_camel_case() {
739 let req = RegisterDeviceRequest {
740 fcm_token: "tok123".to_string(),
741 device_id: Some("dev-abc".to_string()),
742 platform: Some("android".to_string()),
743 };
744 let json = serde_json::to_string(&req).unwrap();
745 assert!(json.contains("\"fcmToken\""), "fcmToken field name");
746 assert!(json.contains("\"deviceId\""), "deviceId field name");
747 assert!(json.contains("\"platform\""), "platform field name");
748 assert!(!json.contains("fcm_token"), "no snake_case leakage");
749 assert!(!json.contains("device_id"), "no snake_case leakage");
750 }
751
752 #[test]
753 fn register_device_request_omits_optional_fields_when_none() {
754 let req = RegisterDeviceRequest {
755 fcm_token: "tok456".to_string(),
756 device_id: None,
757 platform: None,
758 };
759 let json = serde_json::to_string(&req).unwrap();
760 assert!(json.contains("\"fcmToken\""), "fcmToken present");
761 assert!(!json.contains("deviceId"), "deviceId absent when None");
762 assert!(!json.contains("platform"), "platform absent when None");
763 }
764
765 #[test]
766 fn registered_device_deserializes_camel_case() {
767 let raw = r#"{
768 "id": 42,
769 "deviceId": "dev-123",
770 "platform": "ios",
771 "fcmToken": "fcm-abc",
772 "active": true,
773 "createdAt": "2024-01-01T00:00:00Z",
774 "updatedAt": "2024-01-02T00:00:00Z",
775 "lastUsed": "2024-01-03T00:00:00Z"
776 }"#;
777 let dev: RegisteredDevice = serde_json::from_str(raw).unwrap();
778 assert_eq!(dev.id, Some(42));
779 assert_eq!(dev.device_id.as_deref(), Some("dev-123"));
780 assert_eq!(dev.platform.as_deref(), Some("ios"));
781 assert_eq!(dev.fcm_token, "fcm-abc");
782 assert_eq!(dev.active, Some(true));
783 assert_eq!(dev.created_at.as_deref(), Some("2024-01-01T00:00:00Z"));
784 assert_eq!(dev.last_used.as_deref(), Some("2024-01-03T00:00:00Z"));
785 }
786
787 #[test]
792 fn send_message_params_serializes_camel_case() {
793 let p = SendMessageParams {
794 recipient: "03abc".to_string(),
795 message_box: "inbox".to_string(),
796 body: "hello".to_string(),
797 message_id: "deadbeef".to_string(),
798 };
799 let json = serde_json::to_string(&p).unwrap();
800 assert!(json.contains("\"messageBox\""), "messageBox field name");
801 assert!(json.contains("\"messageId\""), "messageId field name");
802 assert!(!json.contains("message_box"), "no snake_case leakage");
803 assert!(!json.contains("message_id"), "no snake_case leakage");
804 }
805
806 #[test]
807 fn acknowledge_params_serializes_camel_case() {
808 let p = AcknowledgeMessageParams {
809 message_ids: vec!["id1".to_string(), "id2".to_string()],
810 };
811 let json = serde_json::to_string(&p).unwrap();
812 assert!(json.contains("\"messageIds\""), "messageIds field name");
813 assert!(!json.contains("message_ids"), "no snake_case leakage");
814 }
815
816 #[test]
817 fn server_peer_message_tolerates_unknown_fields() {
818 let raw = r#"{
820 "messageId": "abc123",
821 "body": "hello",
822 "sender": "03xyz",
823 "created_at": "2024-01-01T00:00:00Z",
824 "updated_at": "2024-01-01T00:00:00Z",
825 "acknowledged": false,
826 "unknownField": "should be ignored",
827 "anotherExtra": 42
828 }"#;
829 let msg: ServerPeerMessage = serde_json::from_str(raw).unwrap();
830 assert_eq!(msg.message_id, "abc123");
831 assert_eq!(msg.body, "hello");
832 assert_eq!(msg.acknowledged, Some(false));
833 }
834}