use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SendListParams {
pub recipients: Vec<String>,
pub message_box: String,
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_encryption: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SentRecipient {
pub recipient: String,
pub message_id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FailedRecipient {
pub recipient: String,
pub error: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SendListTotals {
pub delivery_fees: i64,
pub recipient_fees: i64,
pub total_for_payable_recipients: i64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SendListResult {
pub status: String,
pub description: String,
pub sent: Vec<SentRecipient>,
pub blocked: Vec<String>,
pub failed: Vec<FailedRecipient>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals: Option<SendListTotals>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RecipientQuote {
pub recipient: String,
pub message_box: String,
pub delivery_fee: i64,
pub recipient_fee: i64,
pub status: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MessageBoxMultiQuote {
pub quotes_by_recipient: Vec<RecipientQuote>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals: Option<SendListTotals>,
pub blocked_recipients: Vec<String>,
pub delivery_agent_identity_key_by_host: HashMap<String, String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRemittanceInfo {
pub derivation_prefix: String,
pub derivation_suffix: String,
pub sender_identity_key: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct InsertionRemittanceInfo {
pub basket: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PaymentOutput {
pub output_index: u32,
pub protocol: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_remittance: Option<PaymentRemittanceInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub insertion_remittance: Option<InsertionRemittanceInfo>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Payment {
pub tx: Vec<u8>,
pub outputs: Vec<PaymentOutput>,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek_permission: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct AdvertisementToken {
pub host: String,
pub txid: String,
pub output_index: u32,
pub locking_script: String,
pub beef: Vec<u8>,
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RegisterDeviceRequest {
pub fcm_token: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RegisteredDevice {
pub id: Option<i64>,
pub device_id: Option<String>,
pub platform: Option<String>,
pub fcm_token: String,
pub active: Option<bool>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub last_used: Option<String>,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RegisterDeviceResponse {
pub status: String,
pub message: Option<String>,
pub device_id: Option<i64>,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ListDevicesResponse {
pub status: String,
pub devices: Vec<RegisteredDevice>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SetPermissionParams {
pub message_box: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender: Option<String>,
pub recipient_fee: i64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MessageBoxPermission {
#[serde(skip_serializing_if = "Option::is_none")]
pub sender: Option<String>,
#[serde(alias = "messageBox")]
pub message_box: String,
#[serde(alias = "recipientFee")]
pub recipient_fee: i64,
#[serde(alias = "createdAt")]
pub created_at: String,
#[serde(alias = "updatedAt")]
pub updated_at: String,
}
impl MessageBoxPermission {
pub fn status(&self) -> &str {
match self.recipient_fee {
f if f < 0 => "blocked",
0 => "always_allow",
_ => "payment_required",
}
}
}
#[derive(Clone, Debug)]
pub struct MessageBoxQuote {
pub delivery_fee: i64,
pub recipient_fee: i64,
pub delivery_agent_identity_key: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageParams {
pub recipient: String,
pub message_box: String,
pub body: String,
pub message_id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MessagePayment {
pub tx: Vec<u8>,
pub outputs: Vec<MessagePaymentOutput>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MessagePaymentOutput {
pub output_index: u32,
pub derivation_prefix: Vec<u8>,
pub derivation_suffix: Vec<u8>,
pub sender_identity_key: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SendMessageRequest {
pub message: SendMessageParams,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment: Option<MessagePayment>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ListMessagesParams {
pub message_box: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AcknowledgeMessageParams {
pub message_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPeerMessage {
#[serde(rename = "messageId")]
pub message_id: String,
pub body: String,
pub sender: String,
#[serde(alias = "createdAt")]
pub created_at: String,
#[serde(alias = "updatedAt")]
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub acknowledged: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ListMessagesResponse {
pub status: String,
pub messages: Vec<ServerPeerMessage>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageResponse {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WsSendMessageData {
pub room_id: String,
pub message: WsSendMessagePayload,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WsSendMessagePayload {
pub message_id: String,
pub recipient: String,
pub body: String,
}
pub const PAYMENT_REQUESTS_MESSAGEBOX: &str = "payment_requests";
pub const PAYMENT_REQUEST_RESPONSES_MESSAGEBOX: &str = "payment_request_responses";
pub const DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT: u64 = 1000;
pub const DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT: u64 = 10_000_000;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequestMessage {
pub request_id: String,
pub sender_identity_key: String,
pub request_proof: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cancelled: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequestResponse {
pub request_id: String,
pub status: String, #[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount_paid: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct IncomingPaymentRequest {
pub message_id: String,
pub sender: String,
pub request_id: String,
pub amount: u64,
pub description: String,
pub expires_at: u64,
}
#[derive(Clone, Debug)]
pub struct PaymentRequestLimits {
pub min_amount: u64,
pub max_amount: u64,
}
impl Default for PaymentRequestLimits {
fn default() -> Self {
Self {
min_amount: DEFAULT_PAYMENT_REQUEST_MIN_AMOUNT,
max_amount: DEFAULT_PAYMENT_REQUEST_MAX_AMOUNT,
}
}
}
#[derive(Clone, Debug)]
pub struct PaymentRequestResult {
pub request_id: String,
pub request_proof: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PaymentCustomInstructions {
pub derivation_prefix: String,
pub derivation_suffix: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub payee: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PaymentToken {
pub custom_instructions: PaymentCustomInstructions,
pub transaction: Vec<u8>,
pub amount: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_index: Option<u32>,
}
#[derive(Clone, Debug)]
pub struct IncomingPayment {
pub token: PaymentToken,
pub sender: String,
pub message_id: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_permission_params_serializes_camel_case() {
let p = SetPermissionParams {
message_box: "payment_inbox".to_string(),
sender: None,
recipient_fee: 100,
};
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"messageBox\""), "messageBox field name");
assert!(json.contains("\"recipientFee\""), "recipientFee field name");
assert!(!json.contains("message_box"), "no snake_case leakage");
assert!(!json.contains("recipient_fee"), "no snake_case leakage");
assert!(!json.contains("sender"), "sender absent when None");
}
#[test]
fn set_permission_params_includes_sender_when_some() {
let p = SetPermissionParams {
message_box: "inbox".to_string(),
sender: Some("03abc".to_string()),
recipient_fee: 0,
};
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"sender\""), "sender present when Some");
assert!(json.contains("\"03abc\""), "sender value correct");
}
#[test]
fn message_box_permission_deserializes_camel_case() {
let raw = r#"{
"messageBox": "payment_inbox",
"recipientFee": 50,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-02T00:00:00Z"
}"#;
let perm: MessageBoxPermission = serde_json::from_str(raw).unwrap();
assert_eq!(perm.message_box, "payment_inbox");
assert_eq!(perm.recipient_fee, 50);
assert_eq!(perm.created_at, "2024-01-01T00:00:00Z");
assert_eq!(perm.updated_at, "2024-01-02T00:00:00Z");
}
#[test]
fn message_box_permission_deserializes_snake_case() {
let raw = r#"{
"message_box": "payment_inbox",
"recipient_fee": 75,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-02T00:00:00Z"
}"#;
let perm: MessageBoxPermission = serde_json::from_str(raw).unwrap();
assert_eq!(perm.message_box, "payment_inbox");
assert_eq!(perm.recipient_fee, 75);
assert_eq!(perm.created_at, "2024-01-01T00:00:00Z");
assert_eq!(perm.updated_at, "2024-01-02T00:00:00Z");
}
#[test]
fn message_box_permission_status_computes_correctly() {
let make = |fee: i64| MessageBoxPermission {
sender: None,
message_box: "inbox".to_string(),
recipient_fee: fee,
created_at: "2024-01-01".to_string(),
updated_at: "2024-01-01".to_string(),
};
assert_eq!(make(-1).status(), "blocked");
assert_eq!(make(0).status(), "always_allow");
assert_eq!(make(100).status(), "payment_required");
assert_eq!(make(1).status(), "payment_required");
}
#[test]
fn message_box_quote_can_be_constructed() {
let quote = MessageBoxQuote {
delivery_fee: 10,
recipient_fee: 50,
delivery_agent_identity_key: "03deadbeef".to_string(),
};
assert_eq!(quote.delivery_fee, 10);
assert_eq!(quote.recipient_fee, 50);
assert_eq!(quote.delivery_agent_identity_key, "03deadbeef");
}
#[test]
fn payment_custom_instructions_round_trip() {
let ci = PaymentCustomInstructions {
derivation_prefix: "pfx123".to_string(),
derivation_suffix: "sfx456".to_string(),
payee: Some("03abc".to_string()),
};
let json = serde_json::to_string(&ci).unwrap();
let back: PaymentCustomInstructions = serde_json::from_str(&json).unwrap();
assert_eq!(back.derivation_prefix, "pfx123");
assert_eq!(back.derivation_suffix, "sfx456");
assert_eq!(back.payee, Some("03abc".to_string()));
}
#[test]
fn payment_token_serializes_camel_case() {
let token = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "pfx".to_string(),
derivation_suffix: "sfx".to_string(),
payee: Some("03recipient".to_string()),
},
transaction: vec![1, 2, 3],
amount: 1000,
output_index: Some(0),
};
let json = serde_json::to_string(&token).unwrap();
assert!(json.contains("\"customInstructions\""), "customInstructions field name");
assert!(json.contains("\"derivationPrefix\""), "derivationPrefix field name");
assert!(json.contains("\"derivationSuffix\""), "derivationSuffix field name");
assert!(json.contains("\"payee\""), "payee present when Some");
assert!(json.contains("\"outputIndex\""), "outputIndex present when Some");
assert!(!json.contains("custom_instructions"), "no snake_case leakage");
assert!(!json.contains("derivation_prefix"), "no snake_case leakage");
assert!(!json.contains("output_index"), "no snake_case leakage");
}
#[test]
fn payment_token_no_output_index_by_default() {
let token = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "pfx".to_string(),
derivation_suffix: "sfx".to_string(),
payee: None,
},
transaction: vec![0xab, 0xcd],
amount: 500,
output_index: None,
};
let json = serde_json::to_string(&token).unwrap();
assert!(!json.contains("outputIndex"), "outputIndex absent when None");
assert!(!json.contains("payee"), "payee absent when None");
}
#[test]
fn incoming_payment_can_be_constructed() {
let token = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "p".to_string(),
derivation_suffix: "s".to_string(),
payee: None,
},
transaction: vec![0x01],
amount: 2000,
output_index: None,
};
let incoming = IncomingPayment {
token: token.clone(),
sender: "03sender".to_string(),
message_id: "msg001".to_string(),
};
assert_eq!(incoming.sender, "03sender");
assert_eq!(incoming.message_id, "msg001");
assert_eq!(incoming.token.amount, 2000);
}
#[test]
fn register_device_request_camel_case() {
let req = RegisterDeviceRequest {
fcm_token: "tok123".to_string(),
device_id: Some("dev-abc".to_string()),
platform: Some("android".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"fcmToken\""), "fcmToken field name");
assert!(json.contains("\"deviceId\""), "deviceId field name");
assert!(json.contains("\"platform\""), "platform field name");
assert!(!json.contains("fcm_token"), "no snake_case leakage");
assert!(!json.contains("device_id"), "no snake_case leakage");
}
#[test]
fn register_device_request_omits_optional_fields_when_none() {
let req = RegisterDeviceRequest {
fcm_token: "tok456".to_string(),
device_id: None,
platform: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"fcmToken\""), "fcmToken present");
assert!(!json.contains("deviceId"), "deviceId absent when None");
assert!(!json.contains("platform"), "platform absent when None");
}
#[test]
fn registered_device_deserializes_camel_case() {
let raw = r#"{
"id": 42,
"deviceId": "dev-123",
"platform": "ios",
"fcmToken": "fcm-abc",
"active": true,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-02T00:00:00Z",
"lastUsed": "2024-01-03T00:00:00Z"
}"#;
let dev: RegisteredDevice = serde_json::from_str(raw).unwrap();
assert_eq!(dev.id, Some(42));
assert_eq!(dev.device_id.as_deref(), Some("dev-123"));
assert_eq!(dev.platform.as_deref(), Some("ios"));
assert_eq!(dev.fcm_token, "fcm-abc");
assert_eq!(dev.active, Some(true));
assert_eq!(dev.created_at.as_deref(), Some("2024-01-01T00:00:00Z"));
assert_eq!(dev.last_used.as_deref(), Some("2024-01-03T00:00:00Z"));
}
#[test]
fn send_message_params_serializes_camel_case() {
let p = SendMessageParams {
recipient: "03abc".to_string(),
message_box: "inbox".to_string(),
body: "hello".to_string(),
message_id: "deadbeef".to_string(),
};
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"messageBox\""), "messageBox field name");
assert!(json.contains("\"messageId\""), "messageId field name");
assert!(!json.contains("message_box"), "no snake_case leakage");
assert!(!json.contains("message_id"), "no snake_case leakage");
}
#[test]
fn acknowledge_params_serializes_camel_case() {
let p = AcknowledgeMessageParams {
message_ids: vec!["id1".to_string(), "id2".to_string()],
};
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"messageIds\""), "messageIds field name");
assert!(!json.contains("message_ids"), "no snake_case leakage");
}
#[test]
fn server_peer_message_tolerates_unknown_fields() {
let raw = r#"{
"messageId": "abc123",
"body": "hello",
"sender": "03xyz",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"acknowledged": false,
"unknownField": "should be ignored",
"anotherExtra": 42
}"#;
let msg: ServerPeerMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.message_id, "abc123");
assert_eq!(msg.body, "hello");
assert_eq!(msg.acknowledged, Some(false));
}
}