use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KdfParamsDto {
pub m_cost: u32,
pub t_cost: u32,
pub p_cost: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShareAAuthMethod {
Password,
Pin,
Passkey,
ApiKey,
}
impl fmt::Display for ShareAAuthMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ShareAAuthMethod::Password => write!(f, "password"),
ShareAAuthMethod::Pin => write!(f, "pin"),
ShareAAuthMethod::Passkey => write!(f, "passkey"),
ShareAAuthMethod::ApiKey => write!(f, "api_key"),
}
}
}
impl std::str::FromStr for ShareAAuthMethod {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"password" => Ok(ShareAAuthMethod::Password),
"pin" => Ok(ShareAAuthMethod::Pin),
"passkey" => Ok(ShareAAuthMethod::Passkey),
"api_key" => Ok(ShareAAuthMethod::ApiKey),
_ => Err(format!("Invalid auth method: {}", s)),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletEnrollRequest {
pub solana_pubkey: String,
pub share_a_auth_method: ShareAAuthMethod,
pub share_a_ciphertext: String,
pub share_a_nonce: String,
#[serde(default)]
pub share_a_kdf_salt: Option<String>,
#[serde(default)]
pub share_a_kdf_params: Option<KdfParamsDto>,
#[serde(default)]
pub prf_salt: Option<String>,
#[serde(default)]
pub pin: Option<String>,
pub share_b: String,
#[serde(default)]
pub recovery_data: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletMaterialResponse {
pub solana_pubkey: String,
pub scheme_version: i16,
pub share_a_auth_method: ShareAAuthMethod,
#[serde(skip_serializing_if = "Option::is_none")]
pub prf_salt: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletUnlockRequest {
#[serde(flatten)]
pub credential: UnlockCredential,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletUnlockResponse {
pub unlocked: bool,
pub ttl_seconds: u64,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletStatusResponse {
pub enrolled: bool,
pub unlocked: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub solana_pubkey: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_method: Option<ShareAAuthMethod>,
#[serde(default)]
pub has_external_wallet: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignTransactionRequest {
pub transaction: String,
#[serde(flatten)]
pub credential: Option<UnlockCredential>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UnlockCredential {
Password(String),
Pin(String),
PrfOutput(String),
ApiKey(String),
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignTransactionResponse {
pub signature: String,
pub pubkey: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RotateUserSecretRequest {
#[serde(flatten)]
pub current_credential: UnlockCredential,
pub new_auth_method: ShareAAuthMethod,
pub share_a_ciphertext: String,
pub share_a_nonce: String,
#[serde(default)]
pub share_a_kdf_salt: Option<String>,
#[serde(default)]
pub share_a_kdf_params: Option<KdfParamsDto>,
#[serde(default)]
pub prf_salt: Option<String>,
#[serde(default)]
pub new_pin: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShareCRecoveryRequest {
pub share_c: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShareCRecoveryResponse {
pub share_b: String,
pub solana_pubkey: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletRecoverRequest {
pub solana_pubkey: String,
pub share_a_auth_method: ShareAAuthMethod,
pub share_a_ciphertext: String,
pub share_a_nonce: String,
#[serde(default)]
pub share_a_kdf_salt: Option<String>,
#[serde(default)]
pub share_a_kdf_params: Option<KdfParamsDto>,
#[serde(default)]
pub prf_salt: Option<String>,
#[serde(default)]
pub pin: Option<String>,
pub share_b: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingWalletRecoveryResponse {
pub has_pending_recovery: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub recovery_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recovery_phrase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcknowledgeRecoveryRequest {
pub confirmed: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletRotateRequest {
pub solana_pubkey: String,
pub share_a_auth_method: ShareAAuthMethod,
pub share_a_ciphertext: String,
pub share_a_nonce: String,
#[serde(default)]
pub share_a_kdf_salt: Option<String>,
#[serde(default)]
pub share_a_kdf_params: Option<KdfParamsDto>,
#[serde(default)]
pub prf_salt: Option<String>,
#[serde(default)]
pub pin: Option<String>,
pub share_b: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletSummary {
pub id: Uuid,
pub solana_pubkey: String,
pub share_a_auth_method: ShareAAuthMethod,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key_label: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletListResponse {
pub wallets: Vec<WalletSummary>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_share_a_auth_method_serialization() {
assert_eq!(
serde_json::to_string(&ShareAAuthMethod::Password).unwrap(),
"\"password\""
);
assert_eq!(
serde_json::to_string(&ShareAAuthMethod::Pin).unwrap(),
"\"pin\""
);
assert_eq!(
serde_json::to_string(&ShareAAuthMethod::Passkey).unwrap(),
"\"passkey\""
);
assert_eq!(
serde_json::to_string(&ShareAAuthMethod::ApiKey).unwrap(),
"\"api_key\""
);
}
#[test]
fn test_share_a_auth_method_deserialization() {
let password: ShareAAuthMethod = serde_json::from_str("\"password\"").unwrap();
assert_eq!(password, ShareAAuthMethod::Password);
let pin: ShareAAuthMethod = serde_json::from_str("\"pin\"").unwrap();
assert_eq!(pin, ShareAAuthMethod::Pin);
let passkey: ShareAAuthMethod = serde_json::from_str("\"passkey\"").unwrap();
assert_eq!(passkey, ShareAAuthMethod::Passkey);
let api_key: ShareAAuthMethod = serde_json::from_str("\"api_key\"").unwrap();
assert_eq!(api_key, ShareAAuthMethod::ApiKey);
}
#[test]
fn test_wallet_enroll_request_password_method() {
let json = r#"{
"solanaPubkey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"shareAAuthMethod": "password",
"shareACiphertext": "YWJjZGVm",
"shareANonce": "MTIzNDU2Nzg5MDEy",
"shareAKdfSalt": "c2FsdHNhbHRzYWx0c2FsdA==",
"shareAKdfParams": {"mCost": 19456, "tCost": 2, "pCost": 1},
"shareB": "c2hhcmViZGF0YQ=="
}"#;
let request: WalletEnrollRequest = serde_json::from_str(json).unwrap();
assert_eq!(
request.solana_pubkey,
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
);
assert_eq!(request.share_a_auth_method, ShareAAuthMethod::Password);
assert!(request.share_a_kdf_params.is_some());
assert_eq!(request.share_a_kdf_params.as_ref().unwrap().m_cost, 19456);
}
#[test]
fn test_wallet_enroll_request_pin_method() {
let json = r#"{
"solanaPubkey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"shareAAuthMethod": "pin",
"shareACiphertext": "YWJjZGVm",
"shareANonce": "MTIzNDU2Nzg5MDEy",
"shareAKdfSalt": "c2FsdHNhbHRzYWx0c2FsdA==",
"shareAKdfParams": {"mCost": 19456, "tCost": 2, "pCost": 1},
"pin": "123456",
"shareB": "c2hhcmViZGF0YQ=="
}"#;
let request: WalletEnrollRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.share_a_auth_method, ShareAAuthMethod::Pin);
assert_eq!(request.pin, Some("123456".to_string()));
}
#[test]
fn test_wallet_enroll_request_passkey_method() {
let json = r#"{
"solanaPubkey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"shareAAuthMethod": "passkey",
"shareACiphertext": "YWJjZGVm",
"shareANonce": "MTIzNDU2Nzg5MDEy",
"prfSalt": "cHJmc2FsdHByZnNhbHRwcmZzYWx0cHJmc2FsdA==",
"shareB": "c2hhcmViZGF0YQ=="
}"#;
let request: WalletEnrollRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.share_a_auth_method, ShareAAuthMethod::Passkey);
assert!(request.prf_salt.is_some());
assert!(request.share_a_kdf_salt.is_none());
}
#[test]
fn test_wallet_material_response_serialization() {
let response = WalletMaterialResponse {
solana_pubkey: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
scheme_version: 2,
share_a_auth_method: ShareAAuthMethod::Password,
prf_salt: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"solanaPubkey\":\"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU\""));
assert!(json.contains("\"schemeVersion\":2"));
assert!(json.contains("\"shareAAuthMethod\":\"password\""));
assert!(!json.contains("prfSalt"));
}
#[test]
fn test_wallet_material_response_with_prf_salt() {
let response = WalletMaterialResponse {
solana_pubkey: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
scheme_version: 2,
share_a_auth_method: ShareAAuthMethod::Passkey,
prf_salt: Some("cHJmc2FsdA==".to_string()),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"shareAAuthMethod\":\"passkey\""));
assert!(json.contains("\"prfSalt\":\"cHJmc2FsdA==\""));
}
#[test]
fn test_sign_transaction_response_serialization() {
let response = SignTransactionResponse {
signature: "c2lnbmF0dXJlZGF0YQ==".to_string(),
pubkey: "2ZjUShWy5eqJThLvnA6x3gPQFGwjHWPYdJnDhGMxCkKs".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"signature\":\"c2lnbmF0dXJlZGF0YQ==\""));
assert!(json.contains("\"pubkey\":\"2ZjUShWy5eqJThLvnA6x3gPQFGwjHWPYdJnDhGMxCkKs\""));
}
}