1use std::collections::HashMap;
2
3use crate::{
4 builder::VerificationScript,
5 codec::NeoSerializable,
6 neo_protocol::Account,
7 neo_wallets::{NEP6Contract, NEP6Parameter, WalletError},
8 Address, AddressOrScriptHash, Base64Encode, ContractParameterType, StringExt,
9};
10use getset::{Getters, Setters};
11use serde::{Deserialize, Serialize};
12
13#[derive(Clone, Debug, Serialize, Deserialize, Getters, Setters)]
15pub struct NEP6Account {
16 #[getset(get = "pub")]
18 #[serde(rename = "address")]
19 pub address: Address,
20
21 #[getset(get = "pub")]
23 #[serde(skip_serializing_if = "Option::is_none")]
24 #[serde(rename = "label")]
25 pub label: Option<String>,
26
27 #[getset(get = "pub")]
29 #[serde(default)]
30 #[serde(rename = "isDefault")]
31 pub is_default: bool,
32
33 #[getset(get = "pub")]
35 #[serde(rename = "lock")]
36 pub lock: bool,
37
38 #[getset(get = "pub")]
40 #[serde(skip_serializing_if = "Option::is_none")]
41 #[serde(rename = "key")]
42 pub key: Option<String>,
43
44 #[getset(get = "pub")]
46 #[serde(skip_serializing_if = "Option::is_none")]
47 #[serde(rename = "contract")]
48 pub contract: Option<NEP6Contract>,
49
50 #[getset(get = "pub")]
52 #[serde(skip_serializing_if = "Option::is_none")]
53 #[serde(rename = "extra")]
54 pub extra: Option<HashMap<String, String>>,
55}
56
57impl NEP6Account {
58 pub fn new(
87 address: Address,
88 label: Option<String>,
89 is_default: bool,
90 lock: bool,
91 key: Option<String>,
92 contract: Option<NEP6Contract>,
93 extra: Option<HashMap<String, String>>,
94 ) -> Self {
95 Self { address, label, is_default, lock, key, contract, extra }
96 }
97
98 pub fn from_account(account: &Account) -> Result<NEP6Account, WalletError> {
118 if account.key_pair.is_some() && account.encrypted_private_key.is_none() {
119 return Err(WalletError::AccountState(
120 "Account private key is available but not encrypted.".to_string(),
121 ));
122 }
123
124 let mut parameters = Vec::new();
125 if let Some(verification_script) = &account.verification_script {
126 if verification_script.is_multi_sig() {
127 for i in 0..verification_script.get_nr_of_accounts()? {
128 parameters.push(NEP6Parameter {
129 param_name: format!("signature{i}"),
130 param_type: ContractParameterType::Signature,
131 });
132 }
133 } else if verification_script.is_single_sig() {
134 parameters.push(NEP6Parameter {
135 param_name: "signature".to_string(),
136 param_type: ContractParameterType::Signature,
137 });
138 }
139 }
140
141 let contract = if !parameters.is_empty() {
142 Some(NEP6Contract {
143 script: account
144 .verification_script
145 .as_ref()
146 .map(|script| script.to_array().to_base64()),
147 is_deployed: false,
148 nep6_parameters: parameters,
149 })
150 } else {
151 None
152 };
153
154 Ok(NEP6Account {
155 address: account.address_or_scripthash.address().clone(),
156 label: account.label.clone(),
157 is_default: account.is_default,
158 lock: account.is_locked,
159 key: account.encrypted_private_key.clone(),
160 contract,
161 extra: None,
162 })
163 }
164
165 pub fn to_account(&self) -> Result<Account, WalletError> {
179 let mut verification_script: Option<VerificationScript> = None;
180 let mut signing_threshold: Option<u8> = None;
181 let mut nr_of_participants: Option<u8> = None;
182
183 if let Some(contract) = &self.contract {
184 if contract.script.is_some() {
185 verification_script = Some(VerificationScript::from(
186 contract
187 .script
188 .clone()
189 .ok_or_else(|| {
190 WalletError::AccountState("Contract script is missing".to_string())
191 })?
192 .base64_decoded()
193 .map_err(|e| {
194 WalletError::AccountState(format!(
195 "Failed to decode base64 script: {}",
196 e
197 ))
198 })?,
199 ));
200
201 if let Some(script) = verification_script.as_ref() {
202 if script.is_multi_sig() {
203 signing_threshold = Some(script.get_signing_threshold()? as u8);
204 nr_of_participants = Some(script.get_nr_of_accounts()? as u8);
205 }
206 }
207 }
208 }
209
210 Ok(Account {
211 address_or_scripthash: AddressOrScriptHash::Address(self.clone().address),
212 label: self.clone().label,
213 verification_script,
214 is_locked: self.clone().lock,
215 encrypted_private_key: self.clone().key,
216 signing_threshold: signing_threshold.map(|s| s as u32),
217 nr_of_participants: nr_of_participants.map(|s| s as u32),
218 ..Default::default()
219 })
220 }
221}
222
223impl PartialEq for NEP6Account {
224 fn eq(&self, other: &Self) -> bool {
237 self.address == other.address
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use crate::{
244 config::TestConstants,
245 crypto::{PrivateKeyExtension, Secp256r1PrivateKey, Secp256r1PublicKey},
246 neo_clients::ProviderError,
247 neo_protocol::{Account, AccountTrait},
248 neo_types::Base64Encode,
249 neo_wallets::NEP6Account,
250 ContractParameterType,
251 };
252
253 #[test]
254 fn test_decrypt_with_standard_scrypt_params() {
255 use crate::{
256 crypto::{KeyPair, PrivateKeyExtension},
257 neo_protocol::NEP2,
258 };
259
260 let private_key = Secp256r1PrivateKey::from_bytes(
261 &hex::decode(TestConstants::DEFAULT_ACCOUNT_PRIVATE_KEY)
262 .expect("Should be able to decode valid hex in test"),
263 )
264 .expect("Should be able to create private key from valid bytes in test");
265
266 let key_pair = KeyPair::from_secret_key(&private_key);
268 let encrypted_key = NEP2::encrypt(TestConstants::DEFAULT_ACCOUNT_PASSWORD, &key_pair)
269 .expect("Should be able to encrypt key pair");
270
271 let nep6_account =
272 NEP6Account::new("".to_string(), None, true, false, Some(encrypted_key), None, None);
273
274 let mut account = nep6_account
275 .to_account()
276 .expect("Should be able to convert NEP6Account to Account in test");
277
278 account
279 .decrypt_private_key(TestConstants::DEFAULT_ACCOUNT_PASSWORD)
280 .expect("Should be able to decrypt private key with correct password in test");
281
282 assert_eq!(
283 account
284 .key_pair
285 .clone()
286 .expect("Key pair should be present after decryption")
287 .private_key
288 .to_vec(),
289 private_key.to_vec()
290 );
291
292 account
294 .decrypt_private_key(TestConstants::DEFAULT_ACCOUNT_PASSWORD)
295 .expect("Should be able to decrypt private key with correct password in test");
296 assert_eq!(
297 account
298 .key_pair
299 .clone()
300 .expect("Key pair should be present after decryption")
301 .private_key,
302 private_key
303 );
304 }
305
306 #[test]
307 fn test_load_account_from_nep6() {
308 let data = include_str!("../../../test_resources/wallet/account.json");
309 let nep6_account: NEP6Account = serde_json::from_str(data)
310 .expect("Should be able to deserialize valid NEP6Account JSON in test");
311
312 let account = nep6_account
313 .to_account()
314 .expect("Should be able to convert NEP6Account to Account in test");
315
316 assert!(!account.is_default);
317 assert!(!account.is_locked);
318 assert_eq!(
319 account.address_or_scripthash().address(),
320 TestConstants::DEFAULT_ACCOUNT_ADDRESS
321 );
322 assert_eq!(
323 account
324 .encrypted_private_key()
325 .clone()
326 .expect("Encrypted private key should be present"),
327 TestConstants::DEFAULT_ACCOUNT_ENCRYPTED_PRIVATE_KEY
328 );
329
330 assert_eq!(
331 account
332 .verification_script
333 .as_ref()
334 .expect("Verification script should be present")
335 .script(),
336 &hex::decode(TestConstants::DEFAULT_ACCOUNT_VERIFICATION_SCRIPT)
337 .expect("Should be able to decode valid verification script hex in test")
338 );
339 }
340
341 #[test]
342 fn test_load_multi_sig_account_from_nep6() {
343 let data = include_str!("../../../test_resources/wallet/multiSigAccount.json");
344 let nep6_account: NEP6Account = serde_json::from_str(data)
345 .expect("Should be able to deserialize valid NEP6Account JSON in test");
346
347 let account = nep6_account
348 .to_account()
349 .expect("Should be able to convert NEP6Account to Account in test");
350
351 assert!(!account.is_default);
352 assert!(!account.is_locked);
353 assert_eq!(
354 account.address_or_scripthash().address(),
355 TestConstants::COMMITTEE_ACCOUNT_ADDRESS
356 );
357 assert_eq!(
358 account
359 .verification_script()
360 .clone()
361 .expect("Verification script should be present")
362 .script(),
363 &hex::decode(TestConstants::COMMITTEE_ACCOUNT_VERIFICATION_SCRIPT)
364 .expect("Should be able to decode valid verification script hex in test")
365 );
366 assert_eq!(
367 account
368 .get_nr_of_participants()
369 .expect("Should be able to get number of participants"),
370 1
371 );
372 assert_eq!(
373 account
374 .get_signing_threshold()
375 .expect("Should be able to get signing threshold"),
376 1
377 );
378 }
379
380 #[test]
381 fn test_to_nep6_account_with_only_an_address() {
382 let account = Account::from_address(TestConstants::DEFAULT_ACCOUNT_ADDRESS)
383 .expect("Should be able to create account from valid address in test");
384
385 let nep6_account = account
386 .to_nep6_account()
387 .expect("Should be able to convert Account to NEP6Account in test");
388
389 assert!(nep6_account.contract().is_none());
390 assert!(!nep6_account.is_default());
391 assert!(!nep6_account.lock());
392 assert_eq!(nep6_account.address(), TestConstants::DEFAULT_ACCOUNT_ADDRESS);
393 assert_eq!(
394 nep6_account.label().clone().expect("Label should be present in test"),
395 TestConstants::DEFAULT_ACCOUNT_ADDRESS
396 );
397 assert!(nep6_account.extra().is_none());
398 }
399
400 #[test]
401 fn test_to_nep6_account_with_unecrypted_private_key() {
402 let account = Account::from_wif(TestConstants::DEFAULT_ACCOUNT_WIF)
403 .expect("Should be able to create account from valid WIF in test");
404
405 let err = account.to_nep6_account().unwrap_err();
406
407 assert_eq!(
408 err,
409 ProviderError::IllegalState(
410 "Account private key is available but not encrypted.".to_string()
411 )
412 );
413 }
414
415 #[test]
416 fn test_to_nep6_account_with_ecrypted_private_key() {
417 let mut account = Account::from_wif(TestConstants::DEFAULT_ACCOUNT_WIF)
418 .expect("Should be able to create account from valid WIF in test");
419 account
420 .encrypt_private_key("neo")
421 .expect("Should be able to encrypt private key with password in test");
422
423 let nep6_account = account
424 .to_nep6_account()
425 .expect("Should be able to convert Account to NEP6Account in test");
426
427 assert_eq!(
428 nep6_account
429 .contract()
430 .clone()
431 .expect("Contract should be present")
432 .script()
433 .clone()
434 .expect("Script should be present"),
435 TestConstants::DEFAULT_ACCOUNT_VERIFICATION_SCRIPT.to_string().to_base64()
436 );
437
438 let encrypted_key = nep6_account.key().clone().expect("Key should be present");
442 assert!(!encrypted_key.is_empty());
443 assert!(encrypted_key.starts_with("6P")); let mut account_from_nep6 = nep6_account
447 .to_account()
448 .expect("Should be able to convert NEP6Account to Account");
449 account_from_nep6
450 .decrypt_private_key("neo")
451 .expect("Should be able to decrypt with correct password");
452
453 let original_private_key =
455 hex::decode(TestConstants::DEFAULT_ACCOUNT_PRIVATE_KEY).expect("Should decode hex");
456 assert_eq!(
457 account_from_nep6
458 .key_pair
459 .as_ref()
460 .expect("Key pair should be present")
461 .private_key
462 .to_vec(),
463 original_private_key
464 );
465
466 assert!(!nep6_account.is_default());
467 assert!(!nep6_account.lock());
468 assert_eq!(nep6_account.address(), TestConstants::DEFAULT_ACCOUNT_ADDRESS);
469 assert_eq!(
470 nep6_account.label().clone().expect("Label should be present in test"),
471 TestConstants::DEFAULT_ACCOUNT_ADDRESS
472 );
473 }
474
475 #[test]
476 fn test_to_nep6_account_with_muliti_sig_account() {
477 let public_key = Secp256r1PublicKey::from_bytes(
478 &hex::decode(TestConstants::DEFAULT_ACCOUNT_PUBLIC_KEY)
479 .expect("Should be able to decode valid public key hex in test"),
480 )
481 .expect("Should be able to create public key from valid bytes in test");
482 let account = Account::multi_sig_from_public_keys(&mut vec![public_key], 1)
483 .expect("Should be able to create multi-sig account from valid public key in test");
484 let nep6_account = account
485 .to_nep6_account()
486 .expect("Should be able to convert Account to NEP6Account in test");
487
488 assert_eq!(
489 nep6_account
490 .contract()
491 .clone()
492 .expect("Contract should be present")
493 .script()
494 .clone()
495 .expect("Script should be present"),
496 TestConstants::COMMITTEE_ACCOUNT_VERIFICATION_SCRIPT.to_string().to_base64()
497 );
498 assert!(!nep6_account.is_default());
499 assert!(!nep6_account.lock());
500 assert_eq!(nep6_account.address(), TestConstants::COMMITTEE_ACCOUNT_ADDRESS);
501 assert_eq!(
502 nep6_account.label().clone().expect("Label should be present"),
503 TestConstants::COMMITTEE_ACCOUNT_ADDRESS
504 );
505 assert!(nep6_account.key().is_none());
506 assert_eq!(
507 nep6_account
508 .contract()
509 .clone()
510 .expect("Contract should be present")
511 .nep6_parameters()[0]
512 .param_name(),
513 "signature0"
514 );
515 assert_eq!(
516 nep6_account
517 .contract()
518 .clone()
519 .expect("Contract should be present")
520 .nep6_parameters()[0]
521 .param_type(),
522 &ContractParameterType::Signature
523 );
524 }
525}