use crate::error::{MppError, Result};
use crate::protocol::intents::SessionRequest;
use serde::{Deserialize, Serialize};
fn deserialize_transaction_literal<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s != "transaction" {
return Err(serde::de::Error::custom(format!(
"expected \"transaction\", got \"{}\"",
s
)));
}
Ok(s)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TempoSessionMethodDetails {
pub escrow_contract: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_voucher_delta: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chain_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_payer: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "camelCase")]
pub enum SessionCredentialPayload {
#[serde(rename = "open")]
Open {
#[serde(rename = "type", deserialize_with = "deserialize_transaction_literal")]
payload_type: String,
#[serde(rename = "channelId")]
channel_id: String,
transaction: String,
#[serde(rename = "authorizedSigner", skip_serializing_if = "Option::is_none")]
authorized_signer: Option<String>,
#[serde(rename = "cumulativeAmount")]
cumulative_amount: String,
signature: String,
},
#[serde(rename = "topUp")]
TopUp {
#[serde(rename = "type", deserialize_with = "deserialize_transaction_literal")]
payload_type: String,
#[serde(rename = "channelId")]
channel_id: String,
transaction: String,
#[serde(rename = "additionalDeposit")]
additional_deposit: String,
},
#[serde(rename = "voucher")]
Voucher {
#[serde(rename = "channelId")]
channel_id: String,
#[serde(rename = "cumulativeAmount")]
cumulative_amount: String,
signature: String,
},
#[serde(rename = "close")]
Close {
#[serde(rename = "channelId")]
channel_id: String,
#[serde(rename = "cumulativeAmount")]
cumulative_amount: String,
signature: String,
},
}
pub trait TempoSessionExt {
fn escrow_contract(&self) -> Result<String>;
fn channel_id(&self) -> Option<String>;
fn min_voucher_delta(&self) -> Option<String>;
fn chain_id(&self) -> Option<u64>;
fn fee_payer(&self) -> bool;
fn tempo_session_details(&self) -> Result<TempoSessionMethodDetails>;
fn network(&self) -> Option<crate::protocol::methods::tempo::network::TempoNetwork>;
}
impl TempoSessionExt for SessionRequest {
fn escrow_contract(&self) -> Result<String> {
self.method_details
.as_ref()
.and_then(|v| v.get("escrowContract"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| {
MppError::invalid_challenge_reason(
"Missing escrowContract in methodDetails".to_string(),
)
})
}
fn channel_id(&self) -> Option<String> {
self.method_details
.as_ref()
.and_then(|v| v.get("channelId"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn min_voucher_delta(&self) -> Option<String> {
self.method_details
.as_ref()
.and_then(|v| v.get("minVoucherDelta"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn chain_id(&self) -> Option<u64> {
self.method_details
.as_ref()
.and_then(|v| v.get("chainId"))
.and_then(|v| v.as_u64())
}
fn fee_payer(&self) -> bool {
self.method_details
.as_ref()
.and_then(|v| v.get("feePayer"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
fn tempo_session_details(&self) -> Result<TempoSessionMethodDetails> {
match &self.method_details {
Some(value) => serde_json::from_value(value.clone()).map_err(|e| {
MppError::invalid_challenge_reason(format!(
"Invalid Tempo session method details: {}",
e
))
}),
None => Err(MppError::invalid_challenge_reason(
"Missing methodDetails for session intent".to_string(),
)),
}
}
fn network(&self) -> Option<crate::protocol::methods::tempo::network::TempoNetwork> {
self.chain_id()
.and_then(crate::protocol::methods::tempo::network::TempoNetwork::from_chain_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_session_request() -> SessionRequest {
SessionRequest {
amount: "1000".to_string(),
unit_type: Some("second".to_string()),
currency: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
recipient: Some("0x742d35Cc6634C0532925a3b844Bc9e7595f1B0F2".to_string()),
suggested_deposit: Some("60000".to_string()),
method_details: Some(serde_json::json!({
"escrowContract": "0xEscrow1234567890abcdef1234567890abcdef1234",
"channelId": "0xchannel123",
"minVoucherDelta": "500",
"chainId": 42431,
"feePayer": true
})),
..Default::default()
}
}
#[test]
fn test_session_method_details_serialization() {
let details = TempoSessionMethodDetails {
escrow_contract: "0xEscrow".to_string(),
channel_id: Some("0xchannel".to_string()),
min_voucher_delta: Some("1000".to_string()),
chain_id: Some(42431),
fee_payer: Some(true),
};
let json = serde_json::to_string(&details).unwrap();
assert!(json.contains("\"escrowContract\":\"0xEscrow\""));
assert!(json.contains("\"channelId\":\"0xchannel\""));
assert!(json.contains("\"minVoucherDelta\":\"1000\""));
assert!(json.contains("\"chainId\":42431"));
assert!(json.contains("\"feePayer\":true"));
let parsed: TempoSessionMethodDetails = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.escrow_contract, "0xEscrow");
assert_eq!(parsed.channel_id.as_deref(), Some("0xchannel"));
assert_eq!(parsed.min_voucher_delta.as_deref(), Some("1000"));
assert_eq!(parsed.chain_id, Some(42431));
assert_eq!(parsed.fee_payer, Some(true));
}
#[test]
fn test_session_method_details_optional_fields_omitted() {
let details = TempoSessionMethodDetails {
escrow_contract: "0xEscrow".to_string(),
channel_id: None,
min_voucher_delta: None,
chain_id: None,
fee_payer: None,
};
let json = serde_json::to_string(&details).unwrap();
assert!(json.contains("\"escrowContract\""));
assert!(!json.contains("channelId"));
assert!(!json.contains("minVoucherDelta"));
assert!(!json.contains("chainId"));
assert!(!json.contains("feePayer"));
}
#[test]
fn test_open_payload_serialization() {
let payload = SessionCredentialPayload::Open {
payload_type: "transaction".to_string(),
channel_id: "0xchannel123".to_string(),
transaction: "0xtx456".to_string(),
authorized_signer: Some("0xsigner789".to_string()),
cumulative_amount: "10000".to_string(),
signature: "0xsig".to_string(),
};
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"action\":\"open\""));
assert!(json.contains("\"type\":\"transaction\""));
assert!(json.contains("\"channelId\":\"0xchannel123\""));
assert!(json.contains("\"transaction\":\"0xtx456\""));
assert!(json.contains("\"authorizedSigner\":\"0xsigner789\""));
assert!(json.contains("\"cumulativeAmount\":\"10000\""));
assert!(json.contains("\"signature\":\"0xsig\""));
let parsed: SessionCredentialPayload = serde_json::from_str(&json).unwrap();
match parsed {
SessionCredentialPayload::Open {
payload_type,
channel_id,
authorized_signer,
..
} => {
assert_eq!(payload_type, "transaction");
assert_eq!(channel_id, "0xchannel123");
assert_eq!(authorized_signer.as_deref(), Some("0xsigner789"));
}
_ => panic!("Expected Open variant"),
}
}
#[test]
fn test_open_payload_without_authorized_signer() {
let payload = SessionCredentialPayload::Open {
payload_type: "transaction".to_string(),
channel_id: "0xchannel123".to_string(),
transaction: "0xtx456".to_string(),
authorized_signer: None,
cumulative_amount: "10000".to_string(),
signature: "0xsig".to_string(),
};
let json = serde_json::to_string(&payload).unwrap();
assert!(!json.contains("authorizedSigner"));
let parsed: SessionCredentialPayload = serde_json::from_str(&json).unwrap();
match parsed {
SessionCredentialPayload::Open {
authorized_signer, ..
} => {
assert!(authorized_signer.is_none());
}
_ => panic!("Expected Open variant"),
}
}
#[test]
fn test_topup_payload_serialization() {
let payload = SessionCredentialPayload::TopUp {
payload_type: "transaction".to_string(),
channel_id: "0xchannel123".to_string(),
transaction: "0xtx789".to_string(),
additional_deposit: "5000".to_string(),
};
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"action\":\"topUp\""));
assert!(json.contains("\"type\":\"transaction\""));
assert!(json.contains("\"channelId\":\"0xchannel123\""));
assert!(json.contains("\"transaction\":\"0xtx789\""));
assert!(json.contains("\"additionalDeposit\":\"5000\""));
let parsed: SessionCredentialPayload = serde_json::from_str(&json).unwrap();
match parsed {
SessionCredentialPayload::TopUp {
payload_type,
channel_id,
additional_deposit,
..
} => {
assert_eq!(payload_type, "transaction");
assert_eq!(channel_id, "0xchannel123");
assert_eq!(additional_deposit, "5000");
}
_ => panic!("Expected TopUp variant"),
}
}
#[test]
fn test_voucher_payload_serialization() {
let payload = SessionCredentialPayload::Voucher {
channel_id: "0xchannel123".to_string(),
cumulative_amount: "15000".to_string(),
signature: "0xvouchersig".to_string(),
};
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"action\":\"voucher\""));
assert!(json.contains("\"channelId\":\"0xchannel123\""));
assert!(json.contains("\"cumulativeAmount\":\"15000\""));
assert!(json.contains("\"signature\":\"0xvouchersig\""));
assert!(!json.contains("\"type\""));
let parsed: SessionCredentialPayload = serde_json::from_str(&json).unwrap();
match parsed {
SessionCredentialPayload::Voucher {
channel_id,
cumulative_amount,
signature,
} => {
assert_eq!(channel_id, "0xchannel123");
assert_eq!(cumulative_amount, "15000");
assert_eq!(signature, "0xvouchersig");
}
_ => panic!("Expected Voucher variant"),
}
}
#[test]
fn test_close_payload_serialization() {
let payload = SessionCredentialPayload::Close {
channel_id: "0xchannel123".to_string(),
cumulative_amount: "20000".to_string(),
signature: "0xclosesig".to_string(),
};
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"action\":\"close\""));
assert!(json.contains("\"channelId\":\"0xchannel123\""));
assert!(json.contains("\"cumulativeAmount\":\"20000\""));
assert!(json.contains("\"signature\":\"0xclosesig\""));
let parsed: SessionCredentialPayload = serde_json::from_str(&json).unwrap();
match parsed {
SessionCredentialPayload::Close {
channel_id,
cumulative_amount,
signature,
} => {
assert_eq!(channel_id, "0xchannel123");
assert_eq!(cumulative_amount, "20000");
assert_eq!(signature, "0xclosesig");
}
_ => panic!("Expected Close variant"),
}
}
#[test]
fn test_payload_deserialization_from_json_string() {
let open_json = r#"{"action":"open","type":"transaction","channelId":"0xabc","transaction":"0xtx","cumulativeAmount":"100","signature":"0xsig"}"#;
let parsed: SessionCredentialPayload = serde_json::from_str(open_json).unwrap();
assert!(matches!(parsed, SessionCredentialPayload::Open { .. }));
let topup_json = r#"{"action":"topUp","type":"transaction","channelId":"0xabc","transaction":"0xtx","additionalDeposit":"200"}"#;
let parsed: SessionCredentialPayload = serde_json::from_str(topup_json).unwrap();
assert!(matches!(parsed, SessionCredentialPayload::TopUp { .. }));
let voucher_json = r#"{"action":"voucher","channelId":"0xabc","cumulativeAmount":"300","signature":"0xsig"}"#;
let parsed: SessionCredentialPayload = serde_json::from_str(voucher_json).unwrap();
assert!(matches!(parsed, SessionCredentialPayload::Voucher { .. }));
let close_json = r#"{"action":"close","channelId":"0xabc","cumulativeAmount":"400","signature":"0xsig"}"#;
let parsed: SessionCredentialPayload = serde_json::from_str(close_json).unwrap();
assert!(matches!(parsed, SessionCredentialPayload::Close { .. }));
}
#[test]
fn test_escrow_contract() {
let req = test_session_request();
assert_eq!(
req.escrow_contract().unwrap(),
"0xEscrow1234567890abcdef1234567890abcdef1234"
);
}
#[test]
fn test_escrow_contract_missing() {
let req = SessionRequest {
method_details: None,
..test_session_request()
};
assert!(req.escrow_contract().is_err());
}
#[test]
fn test_channel_id() {
let req = test_session_request();
assert_eq!(req.channel_id(), Some("0xchannel123".to_string()));
let req_no_channel = SessionRequest {
method_details: Some(serde_json::json!({
"escrowContract": "0xEscrow"
})),
..test_session_request()
};
assert!(req_no_channel.channel_id().is_none());
}
#[test]
fn test_min_voucher_delta() {
let req = test_session_request();
assert_eq!(req.min_voucher_delta(), Some("500".to_string()));
let req_no_delta = SessionRequest {
method_details: Some(serde_json::json!({
"escrowContract": "0xEscrow"
})),
..test_session_request()
};
assert!(req_no_delta.min_voucher_delta().is_none());
}
#[test]
fn test_chain_id() {
let req = test_session_request();
assert_eq!(req.chain_id(), Some(42431));
let req_no_chain = SessionRequest {
method_details: Some(serde_json::json!({
"escrowContract": "0xEscrow"
})),
..test_session_request()
};
assert!(req_no_chain.chain_id().is_none());
}
#[test]
fn test_fee_payer() {
let req = test_session_request();
assert!(req.fee_payer());
let req_no_fee = SessionRequest {
method_details: Some(serde_json::json!({
"escrowContract": "0xEscrow"
})),
..test_session_request()
};
assert!(!req_no_fee.fee_payer());
let req_no_details = SessionRequest {
method_details: None,
..test_session_request()
};
assert!(!req_no_details.fee_payer());
}
#[test]
fn test_tempo_session_details() {
let req = test_session_request();
let details = req.tempo_session_details().unwrap();
assert_eq!(
details.escrow_contract,
"0xEscrow1234567890abcdef1234567890abcdef1234"
);
assert_eq!(details.channel_id.as_deref(), Some("0xchannel123"));
assert_eq!(details.min_voucher_delta.as_deref(), Some("500"));
assert_eq!(details.chain_id, Some(42431));
assert_eq!(details.fee_payer, Some(true));
}
#[test]
fn test_tempo_session_details_missing() {
let req = SessionRequest {
method_details: None,
..test_session_request()
};
assert!(req.tempo_session_details().is_err());
}
#[test]
fn test_open_payload_rejects_non_transaction_type() {
let json = r#"{"action":"open","type":"hash","channelId":"0xabc","transaction":"0xtx","cumulativeAmount":"100","signature":"0xsig"}"#;
let result = serde_json::from_str::<SessionCredentialPayload>(json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("expected \"transaction\""), "error was: {err}");
}
#[test]
fn test_topup_payload_rejects_non_transaction_type() {
let json = r#"{"action":"topUp","type":"hash","channelId":"0xabc","transaction":"0xtx","additionalDeposit":"200"}"#;
let result = serde_json::from_str::<SessionCredentialPayload>(json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("expected \"transaction\""), "error was: {err}");
}
#[test]
fn test_network_moderato() {
let req = test_session_request();
let network = req.network();
assert_eq!(
network,
Some(crate::protocol::methods::tempo::network::TempoNetwork::Moderato)
);
}
#[test]
fn test_network_none() {
let req = SessionRequest {
method_details: Some(serde_json::json!({
"escrowContract": "0xEscrow",
"chainId": 1
})),
..test_session_request()
};
assert!(req.network().is_none());
}
}