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