use serde::{Deserialize, Serialize};
use crate::error::MppError;
use crate::protocol::core::PaymentChallenge;
use super::transport::Transport;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WsClientMessage {
Credential {
credential: String,
},
#[serde(rename = "message")]
Data {
data: serde_json::Value,
},
}
impl WsClientMessage {
pub fn to_text(&self) -> String {
serde_json::to_string(self).expect("WsClientMessage serialization cannot fail")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WsServerMessage {
Challenge {
challenge: serde_json::Value,
#[serde(default)]
error: Option<String>,
},
#[serde(rename = "message")]
Data {
data: String,
},
NeedVoucher {
#[serde(rename = "channelId")]
channel_id: String,
#[serde(rename = "requiredCumulative")]
required_cumulative: String,
#[serde(rename = "acceptedCumulative")]
accepted_cumulative: String,
deposit: String,
},
Receipt {
receipt: serde_json::Value,
},
Error {
error: String,
},
}
pub struct WsTransport;
pub fn ws() -> WsTransport {
WsTransport
}
impl Transport for WsTransport {
type Request = WsClientMessage;
type Response = WsServerMessage;
fn name(&self) -> &str {
"ws"
}
fn is_payment_required(&self, response: &Self::Response) -> bool {
matches!(response, WsServerMessage::Challenge { .. })
}
fn get_challenge(&self, response: &Self::Response) -> Result<PaymentChallenge, MppError> {
let WsServerMessage::Challenge { challenge, .. } = response else {
return Err(MppError::MissingHeader(
"no challenge in WS message".to_string(),
));
};
serde_json::from_value(challenge.clone()).map_err(|e| {
MppError::MalformedCredential(Some(format!("failed to parse WS challenge: {e}")))
})
}
fn set_credential(&self, _request: Self::Request, credential: &str) -> Self::Request {
WsClientMessage::Credential {
credential: credential.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ws_transport_name() {
let transport = ws();
assert_eq!(transport.name(), "ws");
}
#[test]
fn test_ws_client_message_credential_serde() {
let msg = WsClientMessage::Credential {
credential: "Payment id=\"abc\"".to_string(),
};
let json = msg.to_text();
assert!(json.contains("\"type\":\"credential\""));
let parsed: WsClientMessage = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, WsClientMessage::Credential { .. }));
}
#[test]
fn test_ws_client_message_data_serde() {
let msg = WsClientMessage::Data {
data: serde_json::json!({"prompt": "hello"}),
};
let json = msg.to_text();
assert!(json.contains("\"type\":\"message\""));
}
#[test]
fn test_ws_server_message_challenge() {
let json = r#"{"type":"challenge","challenge":{"id":"ch-1","realm":"test","method":"tempo","intent":"charge","request":"eyJ0ZXN0IjoidmFsdWUifQ"}}"#;
let parsed: WsServerMessage = serde_json::from_str(json).unwrap();
assert!(matches!(parsed, WsServerMessage::Challenge { .. }));
}
#[test]
fn test_ws_server_message_need_voucher() {
let json = r#"{"type":"needVoucher","channelId":"0xabc","requiredCumulative":"2000","acceptedCumulative":"1000","deposit":"5000"}"#;
let parsed: WsServerMessage = serde_json::from_str(json).unwrap();
match parsed {
WsServerMessage::NeedVoucher { channel_id, .. } => {
assert_eq!(channel_id, "0xabc");
}
_ => panic!("expected NeedVoucher"),
}
}
#[test]
fn test_is_payment_required() {
let transport = ws();
let challenge = WsServerMessage::Challenge {
challenge: serde_json::json!({}),
error: None,
};
assert!(transport.is_payment_required(&challenge));
let data = WsServerMessage::Data {
data: "hello".into(),
};
assert!(!transport.is_payment_required(&data));
}
#[test]
fn test_set_credential() {
let transport = ws();
let dummy = WsClientMessage::Data {
data: serde_json::json!({}),
};
let result = transport.set_credential(dummy, "Payment id=\"abc\"");
match result {
WsClientMessage::Credential { credential } => {
assert_eq!(credential, "Payment id=\"abc\"");
}
_ => panic!("expected Credential message"),
}
}
}