1use crate::chain::ChainType;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct EncryptedWallet {
8 #[serde(alias = "lws_version")]
9 pub ows_version: u32,
10 pub id: String,
11 pub name: String,
12 pub created_at: String,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub chain_type: Option<ChainType>,
16 pub accounts: Vec<WalletAccount>,
17 pub crypto: serde_json::Value,
18 pub key_type: KeyType,
19 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
20 pub metadata: serde_json::Value,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct WalletAccount {
26 pub account_id: String,
27 pub address: String,
28 pub chain_id: String,
29 pub derivation_path: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum KeyType {
36 Mnemonic,
37 PrivateKey,
40}
41
42impl EncryptedWallet {
43 pub fn new(
44 id: String,
45 name: String,
46 accounts: Vec<WalletAccount>,
47 crypto: serde_json::Value,
48 key_type: KeyType,
49 ) -> Self {
50 EncryptedWallet {
51 ows_version: 2,
52 id,
53 name,
54 created_at: chrono::Utc::now().to_rfc3339(),
55 chain_type: None,
56 accounts,
57 crypto,
58 key_type,
59 metadata: serde_json::Value::Null,
60 }
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 fn dummy_wallet() -> EncryptedWallet {
69 EncryptedWallet::new(
70 "test-id".to_string(),
71 "test-wallet".to_string(),
72 vec![WalletAccount {
73 account_id: "eip155:1:0xabc".to_string(),
74 address: "0xabc".to_string(),
75 chain_id: "eip155:1".to_string(),
76 derivation_path: "m/44'/60'/0'/0/0".to_string(),
77 }],
78 serde_json::json!({"cipher": "aes-256-gcm"}),
79 KeyType::Mnemonic,
80 )
81 }
82
83 #[test]
84 fn test_serde_roundtrip() {
85 let wallet = dummy_wallet();
86 let json = serde_json::to_string_pretty(&wallet).unwrap();
87 let deserialized: EncryptedWallet = serde_json::from_str(&json).unwrap();
88 assert_eq!(deserialized.id, "test-id");
89 assert_eq!(deserialized.name, "test-wallet");
90 assert_eq!(deserialized.ows_version, 2);
91 assert!(deserialized.chain_type.is_none());
92 }
93
94 #[test]
95 fn test_key_type_serde() {
96 let json = serde_json::to_string(&KeyType::Mnemonic).unwrap();
97 assert_eq!(json, "\"mnemonic\"");
98 let json = serde_json::to_string(&KeyType::PrivateKey).unwrap();
99 assert_eq!(json, "\"private_key\"");
100 }
101
102 #[test]
103 fn test_v2_no_chain_type_field() {
104 let wallet = dummy_wallet();
105 let json = serde_json::to_value(&wallet).unwrap();
106 assert!(
107 json.get("chain_type").is_none(),
108 "v2 wallets should not serialize chain_type"
109 );
110 }
111
112 #[test]
113 fn test_matches_spec_format() {
114 let wallet = dummy_wallet();
115 let json = serde_json::to_value(&wallet).unwrap();
116 for key in [
117 "ows_version",
118 "id",
119 "name",
120 "created_at",
121 "accounts",
122 "crypto",
123 "key_type",
124 ] {
125 assert!(json.get(key).is_some(), "missing key: {key}");
126 }
127 }
128
129 #[test]
130 fn test_metadata_omitted_when_null() {
131 let wallet = dummy_wallet();
132 let json = serde_json::to_value(&wallet).unwrap();
133 assert!(json.get("metadata").is_none());
134 }
135
136 #[test]
137 fn test_v1_backward_compat() {
138 let v1_json = serde_json::json!({
140 "lws_version": 1,
141 "id": "old-id",
142 "name": "old-wallet",
143 "created_at": "2024-01-01T00:00:00Z",
144 "chain_type": "evm",
145 "accounts": [{
146 "account_id": "eip155:1:0xabc",
147 "address": "0xabc",
148 "chain_id": "eip155:1",
149 "derivation_path": "m/44'/60'/0'/0/0"
150 }],
151 "crypto": {"cipher": "aes-256-gcm"},
152 "key_type": "mnemonic"
153 });
154 let wallet: EncryptedWallet = serde_json::from_value(v1_json).unwrap();
155 assert_eq!(wallet.ows_version, 1);
156 assert_eq!(wallet.chain_type, Some(ChainType::Evm));
157 }
158}