use alloy::sol;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum WalletType {
#[default]
Safe,
Proxy,
}
impl WalletType {
pub fn as_str(&self) -> &'static str {
match self {
WalletType::Safe => "SAFE",
WalletType::Proxy => "PROXY",
}
}
}
sol! {
#[derive(Debug, PartialEq, Eq)]
struct SafeTransaction {
address to;
uint8 operation;
bytes data;
uint256 value;
}
#[derive(Debug, PartialEq, Eq)]
struct SafeTransactionArgs {
address from_address;
uint256 nonce;
uint256 chain_id;
SafeTransaction[] transactions;
}
#[derive(Debug, PartialEq, Eq)]
struct SafeTx {
address to;
uint256 value;
bytes data;
uint8 operation;
uint256 safeTxGas;
uint256 baseGas;
uint256 gasPrice;
address gasToken;
address refundReceiver;
uint256 nonce;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionRequest {
#[serde(rename = "type")]
pub type_: String,
pub from: String,
pub to: String,
#[serde(rename = "proxyWallet")]
pub proxy_wallet: String,
pub data: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitResponse {
#[serde(rename = "transactionID")]
pub transaction_id: String,
pub state: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelayerTransaction {
#[serde(rename = "transactionID")]
pub transaction_id: String,
pub transaction_hash: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub proxy_address: Option<String>,
pub data: Option<String>,
pub nonce: Option<String>,
pub value: Option<String>,
pub signature: Option<String>,
pub state: String,
#[serde(rename = "type")]
pub kind: Option<String>,
pub owner: Option<String>,
pub metadata: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
pub fn deserialize_nonce<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct NonceVisitor;
impl<'de> de::Visitor<'de> for NonceVisitor {
type Value = u64;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a u64 or string representing a u64")
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
Ok(v)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
v.parse().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(NonceVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NonceResponse {
#[serde(deserialize_with = "deserialize_nonce")]
pub nonce: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelayerApiKey {
pub api_key: String,
pub address: String,
pub created_at: String,
pub updated_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nonce_from_integer() {
let json = r#"{"nonce": 42}"#;
let resp: NonceResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.nonce, 42);
}
#[test]
fn test_nonce_from_string() {
let json = r#"{"nonce": "123"}"#;
let resp: NonceResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.nonce, 123);
}
#[test]
fn test_nonce_from_zero_integer() {
let json = r#"{"nonce": 0}"#;
let resp: NonceResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.nonce, 0);
}
#[test]
fn test_nonce_from_zero_string() {
let json = r#"{"nonce": "0"}"#;
let resp: NonceResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.nonce, 0);
}
#[test]
fn test_nonce_from_large_integer() {
let json = r#"{"nonce": 18446744073709551615}"#;
let resp: NonceResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.nonce, u64::MAX);
}
#[test]
fn test_nonce_from_large_string() {
let json = r#"{"nonce": "18446744073709551615"}"#;
let resp: NonceResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.nonce, u64::MAX);
}
#[test]
fn test_nonce_from_non_numeric_string_fails() {
let json = r#"{"nonce": "abc"}"#;
let result = serde_json::from_str::<NonceResponse>(json);
assert!(result.is_err());
}
#[test]
fn test_nonce_from_empty_string_fails() {
let json = r#"{"nonce": ""}"#;
let result = serde_json::from_str::<NonceResponse>(json);
assert!(result.is_err());
}
#[test]
fn test_nonce_from_null_fails() {
let json = r#"{"nonce": null}"#;
let result = serde_json::from_str::<NonceResponse>(json);
assert!(result.is_err());
}
#[test]
fn test_nonce_missing_field_fails() {
let json = r#"{}"#;
let result = serde_json::from_str::<NonceResponse>(json);
assert!(result.is_err());
}
#[test]
fn test_wallet_type_as_str() {
assert_eq!(WalletType::Safe.as_str(), "SAFE");
assert_eq!(WalletType::Proxy.as_str(), "PROXY");
}
#[test]
fn test_wallet_type_default_is_safe() {
assert_eq!(WalletType::default(), WalletType::Safe);
}
#[test]
fn test_transaction_request_serialization() {
let tx = TransactionRequest {
type_: "SAFE".to_string(),
from: "0xabc".to_string(),
to: "0xdef".to_string(),
proxy_wallet: "0x123".to_string(),
data: "0xdeadbeef".to_string(),
signature: "0xsig".to_string(),
};
let json = serde_json::to_value(&tx).unwrap();
assert_eq!(json["type"], "SAFE");
assert_eq!(json["from"], "0xabc");
assert_eq!(json["proxyWallet"], "0x123");
}
#[test]
fn test_transaction_request_deserialization() {
let json = r#"{
"type": "PROXY",
"from": "0xabc",
"to": "0xdef",
"proxyWallet": "0x123",
"data": "0xdeadbeef",
"signature": "0xsig"
}"#;
let tx: TransactionRequest = serde_json::from_str(json).unwrap();
assert_eq!(tx.type_, "PROXY");
assert_eq!(tx.proxy_wallet, "0x123");
}
#[test]
fn test_submit_response_new_state() {
let json = r#"{
"transactionID": "tx-123",
"state": "STATE_NEW"
}"#;
let resp: SubmitResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.transaction_id, "tx-123");
assert_eq!(resp.state, "STATE_NEW");
}
#[test]
fn test_relayer_transaction_full() {
let json = r#"{
"transactionID": "0190b317-a1d3-7bec-9b91-eeb6dcd3a620",
"transactionHash": "0x38cbfbeae8fffa4e2b187ee5978d3ee9cafc53af0363ed90a35b7ea9016535d8",
"from": "0x6e0c80c90ea6c15917308f820eac91ce2724b5b5",
"to": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"proxyAddress": "0x6d8c4e9adf5748af82dabe2c6225207770d6b4fa",
"data": "0xdeadbeef",
"nonce": "60",
"value": "",
"signature": "0xabc",
"state": "STATE_CONFIRMED",
"type": "SAFE",
"owner": "0x6e0c80c90ea6c15917308f820eac91ce2724b5b5",
"metadata": "",
"createdAt": "2024-07-14T21:13:08.819782Z",
"updatedAt": "2024-07-14T21:13:46.576639Z"
}"#;
let resp: RelayerTransaction = serde_json::from_str(json).unwrap();
assert_eq!(resp.transaction_id, "0190b317-a1d3-7bec-9b91-eeb6dcd3a620");
assert_eq!(resp.state, "STATE_CONFIRMED");
assert_eq!(resp.kind.as_deref(), Some("SAFE"));
assert_eq!(resp.nonce.as_deref(), Some("60"));
assert!(resp.transaction_hash.is_some());
assert!(resp.created_at.is_some());
}
#[test]
fn test_relayer_transaction_pending_minimal() {
let json = r#"{
"transactionID": "tx-new",
"state": "STATE_NEW"
}"#;
let resp: RelayerTransaction = serde_json::from_str(json).unwrap();
assert_eq!(resp.transaction_id, "tx-new");
assert_eq!(resp.state, "STATE_NEW");
assert!(resp.transaction_hash.is_none());
assert!(resp.kind.is_none());
}
#[test]
fn test_relayer_api_key_deserializes_openapi_example() {
let json = r#"{
"apiKey": "01967c03-b8c8-7000-8f68-8b8eaec6fd3d",
"address": "0xabc...",
"createdAt": "2026-02-24T18:20:11.237485Z",
"updatedAt": "2026-02-24T18:20:11.237485Z"
}"#;
let key: RelayerApiKey = serde_json::from_str(json).unwrap();
assert_eq!(key.api_key, "01967c03-b8c8-7000-8f68-8b8eaec6fd3d");
assert_eq!(key.address, "0xabc...");
assert_eq!(key.created_at, "2026-02-24T18:20:11.237485Z");
assert_eq!(key.updated_at, "2026-02-24T18:20:11.237485Z");
}
#[test]
fn test_relayer_api_key_roundtrip_preserves_camel_case() {
let key = RelayerApiKey {
api_key: "abc".to_string(),
address: "0xdef".to_string(),
created_at: "2026-01-01T00:00:00Z".to_string(),
updated_at: "2026-01-02T00:00:00Z".to_string(),
};
let serialized = serde_json::to_value(&key).unwrap();
assert_eq!(serialized["apiKey"], "abc");
assert_eq!(serialized["address"], "0xdef");
assert_eq!(serialized["createdAt"], "2026-01-01T00:00:00Z");
assert_eq!(serialized["updatedAt"], "2026-01-02T00:00:00Z");
let round: RelayerApiKey = serde_json::from_value(serialized).unwrap();
assert_eq!(round.api_key, "abc");
assert_eq!(round.updated_at, "2026-01-02T00:00:00Z");
}
#[test]
fn test_relayer_api_key_list_deserializes() {
let json = r#"[
{
"apiKey": "key-1",
"address": "0xa1",
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
},
{
"apiKey": "key-2",
"address": "0xa2",
"createdAt": "2026-01-02T00:00:00Z",
"updatedAt": "2026-01-02T00:00:00Z"
}
]"#;
let keys: Vec<RelayerApiKey> = serde_json::from_str(json).unwrap();
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].api_key, "key-1");
assert_eq!(keys[1].api_key, "key-2");
}
}