Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-address-wallet
This commit is contained in:
@@ -85,10 +85,18 @@ pub enum BackendError {
|
||||
WalletFileNotFound,
|
||||
#[error("Account ID not found in wallet")]
|
||||
NoSuchIdInWallet,
|
||||
#[error("Account ID not found in wallet login entry")]
|
||||
NoSuchIdInWalletLoginEntry,
|
||||
#[error("Account ID already found in wallet")]
|
||||
IdAlreadyExistsInWallet,
|
||||
#[error("Account ID already found in stored wallet login")]
|
||||
IdAlreadyExistsInStoredWalletLogin,
|
||||
#[error("Adding a different password to the wallet not currently supported")]
|
||||
WalletDifferentPasswordDetected,
|
||||
#[error("Unexpted multiple account entries found")]
|
||||
WalletUnexpectedMultipleAccounts,
|
||||
#[error("Unexpted mnemonic account found")]
|
||||
WalletUnexpectedMnemonicAccount,
|
||||
}
|
||||
|
||||
impl Serialize for BackendError {
|
||||
|
||||
@@ -34,21 +34,24 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(Arc::new(RwLock::new(State::default())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
mixnet::account::add_account_for_password,
|
||||
mixnet::account::connect_with_mnemonic,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::account::create_new_account,
|
||||
mixnet::account::create_new_mnemonic,
|
||||
mixnet::account::create_password,
|
||||
mixnet::account::does_password_file_exist,
|
||||
mixnet::account::get_balance,
|
||||
mixnet::account::get_validator_nymd_urls,
|
||||
mixnet::account::get_validator_api_urls,
|
||||
mixnet::account::get_validator_nymd_urls,
|
||||
mixnet::account::list_accounts_for_password,
|
||||
mixnet::account::logout,
|
||||
mixnet::account::remove_account_for_password,
|
||||
mixnet::account::remove_password,
|
||||
mixnet::account::sign_in_with_password,
|
||||
mixnet::account::switch_network,
|
||||
mixnet::account::update_validator_urls,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::admin::get_contract_settings,
|
||||
mixnet::admin::update_contract_settings,
|
||||
mixnet::bond::bond_gateway,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::error::BackendError;
|
||||
use crate::network::Network as WalletNetwork;
|
||||
use crate::nymd_client;
|
||||
use crate::state::State;
|
||||
use crate::wallet_storage::account_data::StoredLogin;
|
||||
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
|
||||
|
||||
use bip39::{Language, Mnemonic};
|
||||
@@ -113,9 +114,8 @@ pub async fn create_new_account(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_new_mnemonic() -> Result<String, BackendError> {
|
||||
let rand_mnemonic = random_mnemonic();
|
||||
Ok(rand_mnemonic.to_string())
|
||||
pub fn create_new_mnemonic() -> String {
|
||||
random_mnemonic().to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -394,16 +394,16 @@ pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
|
||||
pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendError> {
|
||||
if does_password_file_exist()? {
|
||||
return Err(BackendError::WalletFileAlreadyExists);
|
||||
}
|
||||
log::info!("Creating password");
|
||||
|
||||
let mnemonic = Mnemonic::from_str(&mnemonic)?;
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let id = wallet_storage::AccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password)
|
||||
}
|
||||
@@ -416,15 +416,70 @@ pub async fn sign_in_with_password(
|
||||
log::info!("Signing in with password");
|
||||
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let id = wallet_storage::AccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?;
|
||||
_connect_with_mnemonic(stored_account.mnemonic().clone(), state).await
|
||||
let mnemonic = match stored_account {
|
||||
StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
StoredLogin::Multiple(ref accounts) => {
|
||||
// Login using the first account in the list
|
||||
accounts
|
||||
.get_accounts()
|
||||
.next()
|
||||
.ok_or(BackendError::NoSuchIdInWalletLoginEntry)?
|
||||
.account
|
||||
.mnemonic()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_password() -> Result<(), BackendError> {
|
||||
log::info!("Removing password");
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let id = wallet_storage::AccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
wallet_storage::remove_wallet_login_information(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_account_for_password(
|
||||
mnemonic: &str,
|
||||
password: &str,
|
||||
inner_id: &str,
|
||||
) -> Result<(), BackendError> {
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::AccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let inner_id = wallet_storage::AccountId::new(inner_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
wallet_storage::append_account_to_wallet_login_information(
|
||||
mnemonic, hd_path, id, inner_id, &password,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_account_for_password(password: &str, inner_id: &str) -> Result<(), BackendError> {
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::AccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let inner_id = wallet_storage::AccountId::new(inner_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
wallet_storage::remove_account_from_wallet_login(&id, &inner_id, &password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_accounts_for_password(password: &str) -> Result<Vec<String>, BackendError> {
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::AccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
let login = wallet_storage::load_existing_wallet_login_information(&id, &password)?;
|
||||
let ids = match login {
|
||||
StoredLogin::Mnemonic(_) => vec![id.to_string()],
|
||||
StoredLogin::Multiple(ref accounts) => accounts
|
||||
.get_accounts()
|
||||
.map(|account| account.id.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
@@ -8,15 +8,16 @@ use zeroize::Zeroize;
|
||||
use crate::error::BackendError;
|
||||
|
||||
use super::encryption::EncryptedData;
|
||||
use super::password::WalletAccountId;
|
||||
use super::password::AccountId;
|
||||
use super::UserPassword;
|
||||
|
||||
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
|
||||
|
||||
/// The wallet, stored as a serialized json file.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct StoredWallet {
|
||||
version: u32,
|
||||
accounts: Vec<EncryptedAccount>,
|
||||
accounts: Vec<EncryptedLogin>,
|
||||
}
|
||||
|
||||
impl StoredWallet {
|
||||
@@ -25,16 +26,59 @@ impl StoredWallet {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub fn remove_account(&mut self, id: &WalletAccountId) -> Option<EncryptedAccount> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
pub fn add_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
|
||||
if self.get_encrypted_login(&new_login.id).is_ok() {
|
||||
return Err(BackendError::IdAlreadyExistsInWallet);
|
||||
}
|
||||
self.accounts.push(new_login);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_encrypted_login(
|
||||
&self,
|
||||
id: &AccountId,
|
||||
) -> Result<&EncryptedData<StoredLogin>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| &account.id == id)
|
||||
.map(|account| &account.account)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)
|
||||
}
|
||||
|
||||
fn get_encrypted_login_mut(
|
||||
&mut self,
|
||||
id: &AccountId,
|
||||
) -> Result<&mut EncryptedLogin, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter_mut()
|
||||
.find(|account| &account.id == id)
|
||||
//.map(|account| &mut account.account)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_encrypted_login_by_index(&self, index: usize) -> Option<&EncryptedLogin> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
|
||||
pub fn replace_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
|
||||
let login = self.get_encrypted_login_mut(&new_login.id)?;
|
||||
*login = new_login;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_encrypted_login(&mut self, id: &AccountId) -> Option<EncryptedLogin> {
|
||||
if let Some(index) = self.accounts.iter().position(|account| &account.id == id) {
|
||||
log::info!("Removing from wallet file: {id}");
|
||||
Some(self.accounts.remove(index))
|
||||
@@ -44,43 +88,15 @@ impl StoredWallet {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
|
||||
fn encrypted_account(
|
||||
pub fn decrypt_login(
|
||||
&self,
|
||||
id: &WalletAccountId,
|
||||
) -> Result<&EncryptedData<StoredAccount>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| &account.id == id)
|
||||
.map(|account| &account.account)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)
|
||||
}
|
||||
|
||||
pub fn add_encrypted_account(
|
||||
&mut self,
|
||||
new_account: EncryptedAccount,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.encrypted_account(&new_account.id).is_ok() {
|
||||
return Err(BackendError::IdAlreadyExistsInWallet);
|
||||
}
|
||||
self.accounts.push(new_account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt_account(
|
||||
&self,
|
||||
id: &WalletAccountId,
|
||||
id: &AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<StoredAccount, BackendError> {
|
||||
self.encrypted_account(id)?.decrypt_struct(password)
|
||||
) -> Result<StoredLogin, BackendError> {
|
||||
self.get_encrypted_login(id)?.decrypt_struct(password)
|
||||
}
|
||||
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredLogin>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
@@ -102,39 +118,51 @@ impl Default for StoredWallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Each entry in the stored wallet file. An id field in plaintext and an encrypted stored login.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct EncryptedAccount {
|
||||
pub id: WalletAccountId,
|
||||
pub account: EncryptedData<StoredAccount>,
|
||||
pub(crate) struct EncryptedLogin {
|
||||
pub id: AccountId,
|
||||
pub account: EncryptedData<StoredLogin>,
|
||||
}
|
||||
|
||||
// future-proofing
|
||||
/// A stored login is either a account, such as a mnemonic, or a list of multiple accounts where
|
||||
/// each has an inner id. Future proofed for having private key backed accounts.
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize)]
|
||||
#[serde(untagged)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) enum StoredAccount {
|
||||
pub(crate) enum StoredLogin {
|
||||
Mnemonic(MnemonicAccount),
|
||||
// PrivateKey(PrivateKeyAccount)
|
||||
Multiple(MultipleAccounts),
|
||||
}
|
||||
|
||||
impl StoredAccount {
|
||||
impl StoredLogin {
|
||||
pub(crate) fn new_mnemonic_backed_account(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> StoredAccount {
|
||||
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path })
|
||||
) -> Self {
|
||||
Self::Mnemonic(MnemonicAccount { mnemonic, hd_path })
|
||||
}
|
||||
|
||||
// If we add accounts backed by something that is not a mnemonic, this should probably be changed
|
||||
// to return `Option<..>`.
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_mnemonic_account(&self) -> Option<&MnemonicAccount> {
|
||||
match self {
|
||||
StoredAccount::Mnemonic(account) => account.mnemonic(),
|
||||
StoredLogin::Mnemonic(mn) => Some(mn),
|
||||
StoredLogin::Multiple(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_multiple_accounts(&self) -> Option<&MultipleAccounts> {
|
||||
match self {
|
||||
StoredLogin::Mnemonic(_) => None,
|
||||
StoredLogin::Multiple(accounts) => Some(accounts),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
/// An account backed by a unique mnemonic.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct MnemonicAccount {
|
||||
mnemonic: bip39::Mnemonic,
|
||||
#[serde(with = "display_hd_path")]
|
||||
@@ -142,14 +170,27 @@ pub(crate) struct MnemonicAccount {
|
||||
}
|
||||
|
||||
impl MnemonicAccount {
|
||||
pub(crate) fn generate_new(&self) -> MnemonicAccount {
|
||||
MnemonicAccount {
|
||||
mnemonic: bip39::Mnemonic::generate(self.mnemonic().word_count()).unwrap(),
|
||||
hd_path: self.hd_path().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
&self.mnemonic
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn hd_path(&self) -> &DerivationPath {
|
||||
&self.hd_path
|
||||
}
|
||||
|
||||
pub(crate) fn into_multiple(self, id: AccountId) -> MultipleAccounts {
|
||||
MultipleAccounts::new(WalletAccount {
|
||||
id,
|
||||
account: self.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Zeroize for MnemonicAccount {
|
||||
@@ -173,6 +214,114 @@ impl Drop for MnemonicAccount {
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiple stored accounts, each entry having an id and a data field.
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize, PartialEq, Eq)]
|
||||
pub(crate) struct MultipleAccounts {
|
||||
accounts: Vec<WalletAccount>,
|
||||
}
|
||||
|
||||
impl MultipleAccounts {
|
||||
pub(crate) fn new(account: WalletAccount) -> Self {
|
||||
MultipleAccounts {
|
||||
accounts: vec![account],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_accounts(&self) -> impl Iterator<Item = &WalletAccount> {
|
||||
self.accounts.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn get_account(&self, id: &AccountId) -> Option<&WalletAccount> {
|
||||
self.accounts.iter().find(|account| &account.id == id)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
id: AccountId,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.get_account(&id).is_some() {
|
||||
Err(BackendError::IdAlreadyExistsInStoredWalletLogin)
|
||||
} else {
|
||||
self
|
||||
.accounts
|
||||
.push(WalletAccount::new_mnemonic_backed_account(
|
||||
id, mnemonic, hd_path,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, id: &AccountId) {
|
||||
self.accounts.retain(|accounts| &accounts.id != id);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<WalletAccount>> for MultipleAccounts {
|
||||
fn from(accounts: Vec<WalletAccount>) -> MultipleAccounts {
|
||||
Self { accounts }
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry in the list of stored accounts
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize, PartialEq, Eq)]
|
||||
pub(crate) struct WalletAccount {
|
||||
pub id: AccountId,
|
||||
pub account: AccountData,
|
||||
}
|
||||
|
||||
impl WalletAccount {
|
||||
pub(crate) fn new_mnemonic_backed_account(
|
||||
id: AccountId,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
account: AccountData::new_mnemonic_backed_account(mnemonic, hd_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) enum AccountData {
|
||||
Mnemonic(MnemonicAccount),
|
||||
// PrivateKey(PrivateKeyAccount)
|
||||
}
|
||||
|
||||
impl AccountData {
|
||||
pub(crate) fn new_mnemonic_backed_account(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> AccountData {
|
||||
AccountData::Mnemonic(MnemonicAccount { mnemonic, hd_path })
|
||||
}
|
||||
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
match self {
|
||||
AccountData::Mnemonic(account) => account.mnemonic(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MnemonicAccount> for AccountData {
|
||||
fn from(mnemonic_account: MnemonicAccount) -> Self {
|
||||
AccountData::Mnemonic(mnemonic_account)
|
||||
}
|
||||
}
|
||||
|
||||
mod display_hd_path {
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) use crate::wallet_storage::password::{UserPassword, WalletAccountId};
|
||||
pub(crate) use crate::wallet_storage::password::{AccountId, UserPassword};
|
||||
|
||||
use crate::error::BackendError;
|
||||
use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME};
|
||||
use crate::wallet_storage::account_data::StoredAccount;
|
||||
use crate::wallet_storage::account_data::StoredLogin;
|
||||
use crate::wallet_storage::encryption::encrypt_struct;
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use std::fs::{self, create_dir_all, OpenOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use self::account_data::{EncryptedAccount, StoredWallet};
|
||||
use self::account_data::{EncryptedLogin, StoredWallet};
|
||||
|
||||
pub(crate) mod account_data;
|
||||
pub(crate) mod encryption;
|
||||
@@ -30,8 +30,9 @@ pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
|
||||
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
|
||||
}
|
||||
|
||||
/// Load stored wallet file
|
||||
#[allow(unused)]
|
||||
pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result<StoredWallet, BackendError> {
|
||||
pub(crate) fn load_existing_wallet() -> Result<StoredWallet, BackendError> {
|
||||
let store_dir = get_storage_directory()?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
load_existing_wallet_at_file(filepath)
|
||||
@@ -46,10 +47,12 @@ fn load_existing_wallet_at_file(filepath: PathBuf) -> Result<StoredWallet, Backe
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Load the stored wallet file and return the stored login for the given id.
|
||||
/// The returned login is either an account or list of (inner id, account) pairs.
|
||||
pub(crate) fn load_existing_wallet_login_information(
|
||||
id: &WalletAccountId,
|
||||
id: &AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<StoredAccount, BackendError> {
|
||||
) -> Result<StoredLogin, BackendError> {
|
||||
let store_dir = get_storage_directory()?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
load_existing_wallet_login_information_at_file(filepath, id, password)
|
||||
@@ -57,16 +60,19 @@ pub(crate) fn load_existing_wallet_login_information(
|
||||
|
||||
fn load_existing_wallet_login_information_at_file(
|
||||
filepath: PathBuf,
|
||||
id: &WalletAccountId,
|
||||
id: &AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<StoredAccount, BackendError> {
|
||||
load_existing_wallet_at_file(filepath)?.decrypt_account(id, password)
|
||||
) -> Result<StoredLogin, BackendError> {
|
||||
load_existing_wallet_at_file(filepath)?.decrypt_login(id, password)
|
||||
}
|
||||
|
||||
/// Encrypt `mnemonic` and store it together with `id`. It is stored at the top-level.
|
||||
/// Currently we enforce that we can only add entries with the same password as the other already
|
||||
/// existing entries. This is not unlikely to change in the future.
|
||||
pub(crate) fn store_wallet_login_information(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
id: WalletAccountId,
|
||||
id: AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
// make sure the entire directory structure exists
|
||||
@@ -81,7 +87,7 @@ fn store_wallet_login_information_at_file(
|
||||
filepath: PathBuf,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
id: WalletAccountId,
|
||||
id: AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
|
||||
@@ -94,13 +100,13 @@ fn store_wallet_login_information_at_file(
|
||||
return Err(BackendError::WalletDifferentPasswordDetected);
|
||||
}
|
||||
|
||||
let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path);
|
||||
let new_encrypted_account = EncryptedAccount {
|
||||
let new_account = StoredLogin::new_mnemonic_backed_account(mnemonic, hd_path);
|
||||
let new_encrypted_account = EncryptedLogin {
|
||||
id,
|
||||
account: encrypt_struct(&new_account, password)?,
|
||||
};
|
||||
|
||||
stored_wallet.add_encrypted_account(new_encrypted_account)?;
|
||||
stored_wallet.add_encrypted_login(new_encrypted_account)?;
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -111,16 +117,86 @@ fn store_wallet_login_information_at_file(
|
||||
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_wallet_login_information(id: &WalletAccountId) -> Result<(), BackendError> {
|
||||
/// Append an account to an already existing top-level encrypted account entry.
|
||||
/// If the existing top-level entry is just a single account, it will be converted to the first
|
||||
/// account in the list of accounts associated with the encrypted entry. The inner id for this
|
||||
/// entry will be set to the same as the outer, unencrypted, id.
|
||||
pub(crate) fn append_account_to_wallet_login_information(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
id: AccountId,
|
||||
inner_id: AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
// make sure the entire directory structure exists
|
||||
let store_dir = get_storage_directory()?;
|
||||
create_dir_all(&store_dir)?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
|
||||
append_account_to_wallet_login_information_at_file(
|
||||
filepath, mnemonic, hd_path, id, inner_id, password,
|
||||
)
|
||||
}
|
||||
|
||||
fn append_account_to_wallet_login_information_at_file(
|
||||
filepath: PathBuf,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
id: AccountId,
|
||||
inner_id: AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
|
||||
Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
|
||||
result => result?,
|
||||
};
|
||||
|
||||
let mut decrypted_login = stored_wallet.decrypt_login(&id, password)?;
|
||||
|
||||
// Since we can't clone the mnemonic, we have to perform a little dance were we add the mnemonic
|
||||
// to the inner enum payload, while also converting by swapping if necessary.
|
||||
if let StoredLogin::Multiple(ref mut accounts) = decrypted_login {
|
||||
accounts.add(inner_id, mnemonic, hd_path)?;
|
||||
} else if let StoredLogin::Mnemonic(ref mut account) = decrypted_login {
|
||||
// Move out the account by swapping, since we can't clone.
|
||||
let account = std::mem::replace(account, account.generate_new());
|
||||
// Convert the enum variant
|
||||
let mut accounts = account.into_multiple(id.clone());
|
||||
accounts.add(inner_id, mnemonic, hd_path)?;
|
||||
// Overwrite the stored login with the new enum variant
|
||||
decrypted_login = StoredLogin::Multiple(accounts);
|
||||
}
|
||||
|
||||
let encrypted_accounts = EncryptedLogin {
|
||||
id,
|
||||
account: encrypt_struct(&decrypted_login, password)?,
|
||||
};
|
||||
|
||||
stored_wallet.replace_encrypted_login(encrypted_accounts)?;
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filepath)?;
|
||||
|
||||
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
|
||||
}
|
||||
|
||||
/// Remove the entire encrypted login entry for the given `id`. This means potentially removing all
|
||||
/// associated accounts!
|
||||
/// If this was the last entry in the file, the file is removed.
|
||||
pub(crate) fn remove_wallet_login_information(id: &AccountId) -> Result<(), BackendError> {
|
||||
let store_dir = get_storage_directory()?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
remove_wallet_login_information_at_file(filepath, id)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_wallet_login_information_at_file(
|
||||
fn remove_wallet_login_information_at_file(
|
||||
filepath: PathBuf,
|
||||
id: &WalletAccountId,
|
||||
id: &AccountId,
|
||||
) -> Result<(), BackendError> {
|
||||
log::warn!("Removing wallet account with id: {id}. This includes all associated accounts!");
|
||||
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
|
||||
Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
|
||||
result => result?,
|
||||
@@ -132,7 +208,7 @@ pub(crate) fn remove_wallet_login_information_at_file(
|
||||
}
|
||||
|
||||
stored_wallet
|
||||
.remove_account(id)
|
||||
.remove_encrypted_login(id)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)?;
|
||||
|
||||
if stored_wallet.is_empty() {
|
||||
@@ -149,8 +225,76 @@ pub(crate) fn remove_wallet_login_information_at_file(
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an account from inside the encrypted login.
|
||||
/// - If the encrypted login is just a single account, abort to be on the safe side.
|
||||
/// - If it is the last associated account with that login, the encrypted login will be removed.
|
||||
/// - If this was the last encrypted login in the file, it will be removed.
|
||||
pub(crate) fn remove_account_from_wallet_login(
|
||||
id: &AccountId,
|
||||
inner_id: &AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
let store_dir = get_storage_directory()?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
remove_account_from_wallet_login_at_file(filepath, id, inner_id, password)
|
||||
}
|
||||
|
||||
fn remove_account_from_wallet_login_at_file(
|
||||
filepath: PathBuf,
|
||||
id: &AccountId,
|
||||
inner_id: &AccountId,
|
||||
password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
log::info!("Removing associated account from login account: {id}");
|
||||
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
|
||||
Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
|
||||
result => result?,
|
||||
};
|
||||
|
||||
let mut decrypted_login = stored_wallet.decrypt_login(id, password)?;
|
||||
|
||||
let is_empty = match decrypted_login {
|
||||
StoredLogin::Mnemonic(_) => {
|
||||
log::warn!("Encountered mnemonic login instead of list of accounts, aborting");
|
||||
return Err(BackendError::WalletUnexpectedMnemonicAccount);
|
||||
}
|
||||
StoredLogin::Multiple(ref mut accounts) => {
|
||||
accounts.remove(inner_id);
|
||||
accounts.is_empty()
|
||||
}
|
||||
};
|
||||
|
||||
if is_empty {
|
||||
stored_wallet
|
||||
.remove_encrypted_login(id)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)?;
|
||||
} else {
|
||||
// Replace the encrypted login with the prune one.
|
||||
let encrypted_accounts = EncryptedLogin {
|
||||
id: id.clone(),
|
||||
account: encrypt_struct(&decrypted_login, password)?,
|
||||
};
|
||||
stored_wallet.replace_encrypted_login(encrypted_accounts)?;
|
||||
}
|
||||
|
||||
if stored_wallet.is_empty() {
|
||||
log::info!("Removing file: {:#?}", filepath);
|
||||
Ok(fs::remove_file(filepath)?)
|
||||
} else {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filepath)?;
|
||||
|
||||
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::wallet_storage::account_data::WalletAccount;
|
||||
|
||||
use super::*;
|
||||
use config::defaults::COSMOS_DERIVATION_PATH;
|
||||
use std::str::FromStr;
|
||||
@@ -171,8 +315,8 @@ mod tests {
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let bad_password = UserPassword::new("bad-password".to_string());
|
||||
|
||||
let id1 = WalletAccountId::new("first".to_string());
|
||||
let id2 = WalletAccountId::new("second".to_string());
|
||||
let id1 = AccountId::new("first".to_string());
|
||||
let id2 = AccountId::new("second".to_string());
|
||||
|
||||
// Nothing was stored on the disk
|
||||
assert!(matches!(
|
||||
@@ -197,10 +341,13 @@ mod tests {
|
||||
let stored_wallet = load_existing_wallet_at_file(wallet_file.clone()).unwrap();
|
||||
assert_eq!(stored_wallet.len(), 1);
|
||||
assert_eq!(
|
||||
stored_wallet.encrypted_account_by_index(0).unwrap().id,
|
||||
WalletAccountId::new("first".to_string())
|
||||
stored_wallet.get_encrypted_login_by_index(0).unwrap().id,
|
||||
AccountId::new("first".to_string())
|
||||
);
|
||||
let encrypted_blob = &stored_wallet.encrypted_account_by_index(0).unwrap().account;
|
||||
let encrypted_blob = &stored_wallet
|
||||
.get_encrypted_login_by_index(0)
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
// some actual ciphertext was saved
|
||||
assert!(!encrypted_blob.ciphertext().is_empty());
|
||||
@@ -235,9 +382,12 @@ mod tests {
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
|
||||
|
||||
let StoredAccount::Mnemonic(ref acc) = loaded_account;
|
||||
assert_eq!(&dummy_account1, acc.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc.hd_path());
|
||||
if let StoredLogin::Mnemonic(ref acc) = loaded_account {
|
||||
assert_eq!(&dummy_account1, acc.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc.hd_path());
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
|
||||
// Can't store extra account if you use different password
|
||||
assert!(matches!(
|
||||
@@ -264,7 +414,7 @@ mod tests {
|
||||
let loaded_accounts = load_existing_wallet_at_file(wallet_file.clone()).unwrap();
|
||||
assert_eq!(2, loaded_accounts.len());
|
||||
let encrypted_blob = &loaded_accounts
|
||||
.encrypted_account_by_index(1)
|
||||
.get_encrypted_login_by_index(1)
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
@@ -275,18 +425,24 @@ mod tests {
|
||||
// first account should be unchanged
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
|
||||
let StoredAccount::Mnemonic(ref acc1) = loaded_account;
|
||||
assert_eq!(&dummy_account1, acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
if let StoredLogin::Mnemonic(ref acc1) = loaded_account {
|
||||
assert_eq!(&dummy_account1, acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password).unwrap();
|
||||
let StoredAccount::Mnemonic(ref acc2) = loaded_account;
|
||||
assert_eq!(&dummy_account2, acc2.mnemonic());
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
if let StoredLogin::Mnemonic(ref acc2) = loaded_account {
|
||||
assert_eq!(&dummy_account2, acc2.mnemonic());
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
|
||||
// Fails to delete non-existent id in the wallet
|
||||
let id3 = WalletAccountId::new("phony".to_string());
|
||||
let id3 = AccountId::new("phony".to_string());
|
||||
assert!(matches!(
|
||||
remove_wallet_login_information_at_file(wallet_file.clone(), &id3),
|
||||
Err(BackendError::NoSuchIdInWallet),
|
||||
@@ -298,9 +454,12 @@ mod tests {
|
||||
// The first account should be unchanged
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
|
||||
let StoredAccount::Mnemonic(ref acc1) = loaded_account;
|
||||
assert_eq!(&dummy_account1, acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
if let StoredLogin::Mnemonic(ref acc1) = loaded_account {
|
||||
assert_eq!(&dummy_account1, acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
|
||||
// Delete the first account
|
||||
assert!(wallet_file.exists());
|
||||
@@ -312,6 +471,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decrypt_stored_wallet() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json";
|
||||
let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET);
|
||||
|
||||
@@ -320,27 +481,109 @@ mod tests {
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let bad_password = UserPassword::new("bad-password".to_string());
|
||||
let id1 = WalletAccountId::new("first".to_string());
|
||||
let id2 = WalletAccountId::new("second".to_string());
|
||||
let id1 = AccountId::new("first".to_string());
|
||||
let id2 = AccountId::new("second".to_string());
|
||||
|
||||
assert!(!wallet.password_can_decrypt_all(&bad_password));
|
||||
assert!(wallet.password_can_decrypt_all(&password));
|
||||
|
||||
let account1 = wallet.decrypt_account(&id1, &password).unwrap();
|
||||
let account2 = wallet.decrypt_account(&id2, &password).unwrap();
|
||||
let account1 = wallet.decrypt_login(&id1, &password).unwrap();
|
||||
let account2 = wallet.decrypt_login(&id2, &password).unwrap();
|
||||
|
||||
assert!(matches!(account1, StoredLogin::Mnemonic(_)));
|
||||
assert!(matches!(account2, StoredLogin::Mnemonic(_)));
|
||||
|
||||
let expected_account1 = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap();
|
||||
let expected_account2 = bip39::Mnemonic::from_str("home mansion start quiz dress decide hint second dragon sunny juice always steak real minimum art rival skin draw total pulp foot goddess agent").unwrap();
|
||||
|
||||
assert_eq!(account1.mnemonic(), &expected_account1);
|
||||
assert_eq!(account2.mnemonic(), &expected_account2);
|
||||
assert_eq!(
|
||||
account1.as_mnemonic_account().unwrap().mnemonic(),
|
||||
&expected_account1
|
||||
);
|
||||
assert_eq!(
|
||||
account1.as_mnemonic_account().unwrap().hd_path(),
|
||||
&cosmos_hd_path,
|
||||
);
|
||||
|
||||
let StoredAccount::Mnemonic(ref mnemonic1) = account1;
|
||||
assert_eq!(mnemonic1.mnemonic(), &expected_account1);
|
||||
assert_eq!(mnemonic1.hd_path(), &cosmos_hd_path);
|
||||
assert_eq!(
|
||||
account2.as_mnemonic_account().unwrap().mnemonic(),
|
||||
&expected_account2
|
||||
);
|
||||
assert_eq!(
|
||||
account2.as_mnemonic_account().unwrap().hd_path(),
|
||||
&cosmos_hd_path,
|
||||
);
|
||||
}
|
||||
|
||||
let StoredAccount::Mnemonic(ref mnemonic2) = account2;
|
||||
assert_eq!(mnemonic2.mnemonic(), &expected_account2);
|
||||
assert_eq!(mnemonic2.hd_path(), &cosmos_hd_path);
|
||||
#[test]
|
||||
fn append_a_third_account() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
|
||||
let dummy_account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let dummy_account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let dummy_account3 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
|
||||
let password = UserPassword::new("password".to_string());
|
||||
|
||||
let id1 = AccountId::new("first".to_string());
|
||||
let id2 = AccountId::new("second".to_string());
|
||||
let id3 = AccountId::new("third".to_string());
|
||||
|
||||
store_wallet_login_information_at_file(
|
||||
wallet_file.clone(),
|
||||
dummy_account1.clone(),
|
||||
cosmos_hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
store_wallet_login_information_at_file(
|
||||
wallet_file.clone(),
|
||||
dummy_account2.clone(),
|
||||
cosmos_hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that it's there as the correct non-multiple type
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password).unwrap();
|
||||
let acc2 = loaded_account.as_mnemonic_account().unwrap();
|
||||
assert_eq!(acc2.mnemonic(), &dummy_account2);
|
||||
assert_eq!(acc2.hd_path(), &cosmos_hd_path);
|
||||
|
||||
// Add a third mnenonic grouped together with the second one
|
||||
append_account_to_wallet_login_information_at_file(
|
||||
wallet_file.clone(),
|
||||
dummy_account3.clone(),
|
||||
cosmos_hd_path.clone(),
|
||||
id2.clone(),
|
||||
id3.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that we can still load all three
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
|
||||
let acc1 = loaded_account.as_mnemonic_account().unwrap();
|
||||
assert_eq!(acc1.mnemonic(), &dummy_account1);
|
||||
assert_eq!(acc1.hd_path(), &cosmos_hd_path);
|
||||
|
||||
let loaded_accounts =
|
||||
load_existing_wallet_login_information_at_file(wallet_file, &id2, &password).unwrap();
|
||||
let accounts = loaded_accounts.as_multiple_accounts().unwrap();
|
||||
|
||||
let expected = vec![
|
||||
WalletAccount::new_mnemonic_backed_account(id2, dummy_account2, cosmos_hd_path.clone()),
|
||||
WalletAccount::new_mnemonic_backed_account(id3, dummy_account3, cosmos_hd_path),
|
||||
]
|
||||
.into();
|
||||
|
||||
assert_eq!(accounts, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,22 @@ use std::fmt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct WalletAccountId(String);
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)]
|
||||
pub(crate) struct AccountId(String);
|
||||
|
||||
impl WalletAccountId {
|
||||
pub(crate) fn new(id: String) -> WalletAccountId {
|
||||
WalletAccountId(id)
|
||||
impl AccountId {
|
||||
pub(crate) fn new(id: String) -> AccountId {
|
||||
AccountId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for WalletAccountId {
|
||||
impl AsRef<str> for AccountId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for WalletAccountId {
|
||||
impl fmt::Display for AccountId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user