neo3/neo_wallets/wallet/
nep6account.rs

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/// Represents an account in the NEP-6 format.
14#[derive(Clone, Debug, Serialize, Deserialize, Getters, Setters)]
15pub struct NEP6Account {
16	/// The address of the account.
17	#[getset(get = "pub")]
18	#[serde(rename = "address")]
19	pub address: Address,
20
21	/// An optional label for the account.
22	#[getset(get = "pub")]
23	#[serde(skip_serializing_if = "Option::is_none")]
24	#[serde(rename = "label")]
25	pub label: Option<String>,
26
27	/// Indicates whether the account is set as default.
28	#[getset(get = "pub")]
29	#[serde(default)]
30	#[serde(rename = "isDefault")]
31	pub is_default: bool,
32
33	/// Indicates whether the account is locked.
34	#[getset(get = "pub")]
35	#[serde(rename = "lock")]
36	pub lock: bool,
37
38	/// An optional private key associated with the account.
39	#[getset(get = "pub")]
40	#[serde(skip_serializing_if = "Option::is_none")]
41	#[serde(rename = "key")]
42	pub key: Option<String>,
43
44	/// An optional NEP-6 contract associated with the account.
45	#[getset(get = "pub")]
46	#[serde(skip_serializing_if = "Option::is_none")]
47	#[serde(rename = "contract")]
48	pub contract: Option<NEP6Contract>,
49
50	/// An optional additional data associated with the account.
51	#[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	/// Creates a new NEP-6 account with the given parameters.
59	///
60	/// # Arguments
61	///
62	/// * `address` - The address of the account.
63	/// * `label` - An optional label for the account.
64	/// * `is_default` - Indicates whether the account is set as default.
65	/// * `lock` - Indicates whether the account is locked.
66	/// * `key` - An optional private key associated with the account.
67	/// * `contract` - An optional NEP-6 contract associated with the account.
68	/// * `extra` - An optional additional data associated with the account.
69	///
70	/// # Example
71	///
72	/// ```
73	/// use std::collections::HashMap;
74	/// use neo3::prelude::*;
75	///
76	/// let address = "example_address".to_string();
77	/// let label = Some("My Account".to_string());
78	/// let is_default = true;
79	/// let lock = false;
80	/// let key = Some("example_private_key".to_string());
81	/// let contract = None; // NEP6Contract::new() is not directly available
82	/// let extra = Some(HashMap::new());
83	///
84	/// let account = wallets::NEP6Account::new(address, label, is_default, lock, key, contract, extra);
85	/// ```
86	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	/// Converts an `Account` into a `NEP6Account`.
99	///
100	/// # Arguments
101	///
102	/// * `account` - The account to convert.
103	///
104	/// # Errors
105	///
106	/// Returns a `WalletError` if there is an issue converting the account.
107	///
108	/// # Example
109	///
110	/// ```
111	/// use neo3::prelude::*;
112	/// use neo3::neo_protocol::AccountTrait;
113	///
114	/// let account = protocol::Account::create().unwrap();
115	/// let nep6_account = wallets::NEP6Account::from_account(&account);
116	/// ```
117	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	/// Converts a `NEP6Account` into an `Account`.
166	///
167	/// # Errors
168	///
169	/// Returns a `WalletError` if there is an issue converting the account.
170	///
171	/// # Example
172	///
173	/// ```
174	/// use neo3::prelude::*;
175	/// # let nep6_account = wallets::NEP6Account::new(String::new(), None, false, false, None, None, None);
176	/// let account = nep6_account.to_account();
177	/// ```
178	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	/// Checks if two `NEP6Account` instances are equal based on their addresses.
225	///
226	/// # Example
227	///
228	/// ```
229	///
230	/// use neo3::prelude::*;
231	///
232	/// let account1 = wallets::NEP6Account::new(String::new(), None, false, false, None, None, None);
233	/// let account2 = wallets::NEP6Account::new(String::new(), None, false, false, None, None, None);
234	/// assert_eq!(account1, account2);
235	/// ```
236	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		// Create a key pair and encrypt it with current parameters
267		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		// Decrypt again
293		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		// Instead of comparing exact encrypted value, verify that:
439		// 1. There is an encrypted key
440		// 2. It can be decrypted back to the original private key
441		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")); // NEP-2 encrypted keys start with "6P"
444
445		// Verify we can decrypt it back
446		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		// Verify the decrypted private key matches the original
454		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}