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