Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d729081996 | |||
| 2ce1c8833f | |||
| 635ca745d9 | |||
| 4ef7fac377 | |||
| c1d136bd54 | |||
| 8ad3565f2c | |||
| 47bdf38776 | |||
| cdd883c174 | |||
| 2d82a51905 | |||
| 38c2ce9837 | |||
| a867921fdd | |||
| 423cdb1e1b | |||
| 7aeac58fd9 | |||
| 30fafa509c | |||
| c950556506 | |||
| 9a49213973 |
@@ -0,0 +1,43 @@
|
|||||||
|
name: Publish Nym binaries
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-nym:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Check the release tag starts with `nym-binaries-`
|
||||||
|
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false
|
||||||
|
uses: actions/github-script@v3
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.setFailed('Release tag did not start with nym-binaries-...')
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Build all binaries
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --workspace --release
|
||||||
|
|
||||||
|
- name: Upload to release based on tag name
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
target/release/nym-client
|
||||||
|
target/release/nym-gateway
|
||||||
|
target/release/nym-mixnode
|
||||||
|
target/release/nym-socks5-client
|
||||||
|
target/release/nym-validator-api
|
||||||
Generated
+10
@@ -3683,6 +3683,15 @@ dependencies = [
|
|||||||
"uint",
|
"uint",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cosmrs",
|
||||||
|
"cosmwasm-std",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -5999,6 +6008,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"mixnet-contract-common",
|
"mixnet-contract-common",
|
||||||
"network-defaults",
|
"network-defaults",
|
||||||
|
"primitives",
|
||||||
"prost",
|
"prost",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ members = [
|
|||||||
"common/nymsphinx/params",
|
"common/nymsphinx/params",
|
||||||
"common/nymsphinx/types",
|
"common/nymsphinx/types",
|
||||||
"common/pemstore",
|
"common/pemstore",
|
||||||
|
"common/primitives",
|
||||||
"common/socks5/proxy-helpers",
|
"common/socks5/proxy-helpers",
|
||||||
"common/socks5/requests",
|
"common/socks5/requests",
|
||||||
"common/topology",
|
"common/topology",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ url = { version = "2.2", features = ["serde"] }
|
|||||||
|
|
||||||
coconut-interface = { path = "../../coconut-interface" }
|
coconut-interface = { path = "../../coconut-interface" }
|
||||||
network-defaults = { path = "../../network-defaults" }
|
network-defaults = { path = "../../network-defaults" }
|
||||||
|
primitives = { path = "../../primitives" }
|
||||||
validator-api-requests = { path = "../../../validator-api/validator-api-requests" }
|
validator-api-requests = { path = "../../../validator-api/validator-api-requests" }
|
||||||
|
|
||||||
# required for nymd-client
|
# required for nymd-client
|
||||||
|
|||||||
@@ -369,14 +369,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
|||||||
&self,
|
&self,
|
||||||
sender_address: &AccountId,
|
sender_address: &AccountId,
|
||||||
recipient_address: &AccountId,
|
recipient_address: &AccountId,
|
||||||
amount: Vec<Coin>,
|
amount: Vec<primitives::Coin>,
|
||||||
fee: Fee,
|
fee: Fee,
|
||||||
memo: impl Into<String> + Send + 'static,
|
memo: impl Into<String> + Send + 'static,
|
||||||
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
||||||
let send_msg = MsgSend {
|
let send_msg = MsgSend {
|
||||||
from_address: sender_address.clone(),
|
from_address: sender_address.clone(),
|
||||||
to_address: recipient_address.clone(),
|
to_address: recipient_address.clone(),
|
||||||
amount,
|
amount: primitives::coin::try_into(amount)?,
|
||||||
}
|
}
|
||||||
.to_any()
|
.to_any()
|
||||||
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
|
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ pub enum NymdError {
|
|||||||
|
|
||||||
#[error("Abci query failed with code {0} - {1}")]
|
#[error("Abci query failed with code {0} - {1}")]
|
||||||
AbciError(u32, abci::Log),
|
AbciError(u32, abci::Log),
|
||||||
|
|
||||||
|
#[error("Failed to handle primitives")]
|
||||||
|
PrimitivesError(#[from] primitives::PrimitivesError)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NymdError {
|
impl NymdError {
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ impl<C> NymdClient<C> {
|
|||||||
pub async fn send(
|
pub async fn send(
|
||||||
&self,
|
&self,
|
||||||
recipient: &AccountId,
|
recipient: &AccountId,
|
||||||
amount: Vec<CosmosCoin>,
|
amount: Vec<primitives::Coin>,
|
||||||
memo: impl Into<String> + Send + 'static,
|
memo: impl Into<String> + Send + 'static,
|
||||||
) -> Result<broadcast::tx_commit::Response, NymdError>
|
) -> Result<broadcast::tx_commit::Response, NymdError>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cosmrs = { version = "0.4.1", features = ["rpc", "bip32", "cosmwasm"] }
|
||||||
|
cosmwasm-std = "1.0.0-beta3"
|
||||||
|
thiserror = "1.0"
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::error::PrimitivesError;
|
||||||
|
use core::fmt;
|
||||||
|
use cosmwasm_std::Uint128;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Common Coin type for the backend.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Coin {
|
||||||
|
pub amount: Uint128,
|
||||||
|
pub denom: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coin {
|
||||||
|
pub fn new(amount: u128, denom: impl Into<String>) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: Uint128::new(amount),
|
||||||
|
denom: denom.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Coin {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", self.amount, self.denom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<cosmrs::Coin> for Coin {
|
||||||
|
type Error = PrimitivesError;
|
||||||
|
|
||||||
|
fn try_from(cosmos_coin: cosmrs::Coin) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
amount: cosmos_coin.amount.to_string().as_str().try_into()?,
|
||||||
|
denom: cosmos_coin.denom.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Coin> for cosmrs::Coin {
|
||||||
|
type Error = PrimitivesError;
|
||||||
|
|
||||||
|
fn try_from(coin: Coin) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
denom: cosmrs::Denom::from_str(&coin.denom)?,
|
||||||
|
amount: cosmrs::Decimal::from_str(&coin.amount.to_string())?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn try_into(coins: Vec<Coin>) -> Result<Vec<cosmrs::Coin>, PrimitivesError> {
|
||||||
|
coins.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into(coins: Vec<Coin>) -> Vec<cosmwasm_std::Coin> {
|
||||||
|
coins.into_iter().map(Into::into).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<cosmwasm_std::Coin> for Coin {
|
||||||
|
fn from(cosmwasm_coin: cosmwasm_std::Coin) -> Self {
|
||||||
|
Self {
|
||||||
|
amount: cosmwasm_coin.amount,
|
||||||
|
denom: cosmwasm_coin.denom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Coin> for cosmwasm_std::Coin {
|
||||||
|
fn from(coin: Coin) -> Self {
|
||||||
|
Self {
|
||||||
|
amount: coin.amount,
|
||||||
|
denom: coin.denom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_to_and_from_cosmwasm_coin() {
|
||||||
|
let coin = Coin::new(42, "ucoin");
|
||||||
|
let cosmwasm_coin: cosmwasm_std::Coin = coin.clone().into();
|
||||||
|
assert_eq!(coin, Coin::from(cosmwasm_coin));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_to_and_from_cosmos_coin() {
|
||||||
|
let coin = Coin::new(42, "ucoin");
|
||||||
|
let cosmos_coin: cosmrs::Coin = coin.clone().try_into().unwrap();
|
||||||
|
assert_eq!(coin, Coin::try_from(cosmos_coin).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIP(JON): more tests
|
||||||
|
// Especially converting from cosmrs::Coin
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PrimitivesError {
|
||||||
|
#[error("{source}")]
|
||||||
|
CosmwasmError {
|
||||||
|
#[from]
|
||||||
|
source: cosmwasm_std::StdError,
|
||||||
|
},
|
||||||
|
#[error("{source}")]
|
||||||
|
CosmrsError {
|
||||||
|
#[from]
|
||||||
|
source: cosmrs::ErrorReport,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
pub mod coin;
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
pub use error::PrimitivesError;
|
||||||
|
pub use coin::Coin;
|
||||||
Generated
+1
@@ -2843,6 +2843,7 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"base64",
|
"base64",
|
||||||
"bip39",
|
"bip39",
|
||||||
|
"cfg-if",
|
||||||
"coconut-interface",
|
"coconut-interface",
|
||||||
"config",
|
"config",
|
||||||
"cosmrs",
|
"cosmrs",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ tauri-macros = "=1.0.0-rc.1"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bip39 = "1.0"
|
bip39 = "1.0"
|
||||||
|
cfg-if = "1.0.0"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
eyre = "0.6.5"
|
eyre = "0.6.5"
|
||||||
futures = "0.3.15"
|
futures = "0.3.15"
|
||||||
|
|||||||
@@ -77,10 +77,18 @@ pub enum BackendError {
|
|||||||
NetworkNotSupported(config::defaults::all::Network),
|
NetworkNotSupported(config::defaults::all::Network),
|
||||||
#[error("Could not access the local data storage directory")]
|
#[error("Could not access the local data storage directory")]
|
||||||
UnknownStorageDirectory,
|
UnknownStorageDirectory,
|
||||||
#[error("No nymd validator configured")]
|
|
||||||
NoNymdValidatorConfigured,
|
|
||||||
#[error("No validator API URL configured")]
|
#[error("No validator API URL configured")]
|
||||||
NoValidatorApiUrlConfigured,
|
NoValidatorApiUrlConfigured,
|
||||||
|
#[error("The wallet file already exists")]
|
||||||
|
WalletFileAlreadyExists,
|
||||||
|
#[error("The wallet file is not found")]
|
||||||
|
WalletFileNotFound,
|
||||||
|
#[error("Account ID not found in wallet")]
|
||||||
|
NoSuchIdInWallet,
|
||||||
|
#[error("Account ID already found in wallet")]
|
||||||
|
IdAlreadyExistsInWallet,
|
||||||
|
#[error("Adding a different password to the wallet not currently supported")]
|
||||||
|
WalletDifferentPasswordDetected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for BackendError {
|
impl Serialize for BackendError {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod error;
|
|||||||
mod menu;
|
mod menu;
|
||||||
mod network;
|
mod network;
|
||||||
mod operations;
|
mod operations;
|
||||||
|
mod platform_constants;
|
||||||
mod state;
|
mod state;
|
||||||
mod utils;
|
mod utils;
|
||||||
// temporarily until it is actually used
|
// temporarily until it is actually used
|
||||||
@@ -37,8 +38,11 @@ fn main() {
|
|||||||
mixnet::account::connect_with_mnemonic,
|
mixnet::account::connect_with_mnemonic,
|
||||||
mixnet::account::create_new_account,
|
mixnet::account::create_new_account,
|
||||||
mixnet::account::create_new_mnemonic,
|
mixnet::account::create_new_mnemonic,
|
||||||
|
mixnet::account::create_password,
|
||||||
|
mixnet::account::does_password_file_exist,
|
||||||
mixnet::account::get_balance,
|
mixnet::account::get_balance,
|
||||||
mixnet::account::logout,
|
mixnet::account::logout,
|
||||||
|
mixnet::account::sign_in_with_password,
|
||||||
mixnet::account::switch_network,
|
mixnet::account::switch_network,
|
||||||
mixnet::account::update_validator_urls,
|
mixnet::account::update_validator_urls,
|
||||||
mixnet::admin::get_contract_settings,
|
mixnet::admin::get_contract_settings,
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ use crate::error::BackendError;
|
|||||||
use crate::network::Network;
|
use crate::network::Network;
|
||||||
use crate::nymd_client;
|
use crate::nymd_client;
|
||||||
use crate::state::State;
|
use crate::state::State;
|
||||||
|
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
|
||||||
|
|
||||||
use bip39::{Language, Mnemonic};
|
use bip39::{Language, Mnemonic};
|
||||||
|
use config::defaults::COSMOS_DERIVATION_PATH;
|
||||||
|
use cosmrs::bip32::DerivationPath;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
@@ -227,9 +230,10 @@ async fn choose_validators(
|
|||||||
.next()
|
.next()
|
||||||
// We always have at least one hardcoded default validator
|
// We always have at least one hardcoded default validator
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!(
|
log::info!(
|
||||||
"Using default for {network}: {}, {}",
|
"Using default for {network}: {}, {}",
|
||||||
default_validator.nymd_url, default_validator.api_url,
|
default_validator.nymd_url,
|
||||||
|
default_validator.api_url,
|
||||||
);
|
);
|
||||||
default_validator
|
default_validator
|
||||||
});
|
});
|
||||||
@@ -305,9 +309,10 @@ async fn try_connect_to_validator(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
if is_validator_connection_ok(&client).await {
|
if is_validator_connection_ok(&client).await {
|
||||||
println!(
|
log::info!(
|
||||||
"Connection ok for {network}: {}, {}",
|
"Connection ok for {network}: {}, {}",
|
||||||
validator.nymd_url, validator.api_url
|
validator.nymd_url,
|
||||||
|
validator.api_url
|
||||||
);
|
);
|
||||||
Ok(Some((network, validator.clone())))
|
Ok(Some((network, validator.clone())))
|
||||||
} else {
|
} else {
|
||||||
@@ -322,3 +327,45 @@ async fn is_validator_connection_ok(client: &Client<SigningNymdClient>) -> bool
|
|||||||
Err(_) | Ok(_) => true,
|
Err(_) | Ok(_) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||||
|
log::info!("Checking wallet file");
|
||||||
|
let file = wallet_storage::wallet_login_filepath()?;
|
||||||
|
if file.exists() {
|
||||||
|
log::info!("Exists: {}", file.to_string_lossy());
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
log::info!("Does not exist: {}", file.to_string_lossy());
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
|
||||||
|
if does_password_file_exist()? {
|
||||||
|
return Err(BackendError::WalletFileAlreadyExists);
|
||||||
|
}
|
||||||
|
log::info!("Creating password");
|
||||||
|
|
||||||
|
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 password = wallet_storage::UserPassword::new(password);
|
||||||
|
wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn sign_in_with_password(
|
||||||
|
password: String,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<Account, BackendError> {
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Specify filenames and other platform specific constants to respect platform conventions, or at
|
||||||
|
// least, something popular on each respective platform.
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(target_os = "linux")] {
|
||||||
|
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
|
||||||
|
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
|
||||||
|
} else if #[cfg(taret_os = "macos")] {
|
||||||
|
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
|
||||||
|
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
|
||||||
|
} else if #[cfg(taret_os = "windows")] {
|
||||||
|
pub const STORAGE_DIR_NAME: &str = "NymWallet";
|
||||||
|
pub const WALLET_INFO_FILENAME: &str = "saved_wallet.json";
|
||||||
|
} else {
|
||||||
|
// This case is likely to be a unix-y system
|
||||||
|
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
|
||||||
|
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,96 @@ use std::fmt::Formatter;
|
|||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::error::BackendError;
|
||||||
|
|
||||||
|
use super::encryption::EncryptedData;
|
||||||
|
use super::password::WalletAccountId;
|
||||||
|
use super::UserPassword;
|
||||||
|
|
||||||
|
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub(crate) struct StoredWallet {
|
||||||
|
version: u32,
|
||||||
|
accounts: Vec<EncryptedAccount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoredWallet {
|
||||||
|
pub fn version(&self) -> u32 {
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.accounts.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.accounts.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
|
||||||
|
self.accounts.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypted_account(
|
||||||
|
&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,
|
||||||
|
password: &UserPassword,
|
||||||
|
) -> Result<StoredAccount, BackendError> {
|
||||||
|
self.encrypted_account(id)?.decrypt_struct(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
|
||||||
|
self
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|account| account.account.decrypt_struct(password))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password_can_decrypt_all(&self, password: &UserPassword) -> bool {
|
||||||
|
self.decrypt_all(password).is_ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StoredWallet {
|
||||||
|
fn default() -> Self {
|
||||||
|
StoredWallet {
|
||||||
|
version: CURRENT_WALLET_FILE_VERSION,
|
||||||
|
accounts: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub(crate) struct EncryptedAccount {
|
||||||
|
pub id: WalletAccountId,
|
||||||
|
pub account: EncryptedData<StoredAccount>,
|
||||||
|
}
|
||||||
|
|
||||||
// future-proofing
|
// future-proofing
|
||||||
#[derive(Serialize, Deserialize, Debug, Zeroize)]
|
#[derive(Serialize, Deserialize, Debug, Zeroize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@@ -24,6 +114,14 @@ impl StoredAccount {
|
|||||||
) -> StoredAccount {
|
) -> StoredAccount {
|
||||||
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path })
|
StoredAccount::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 {
|
||||||
|
match self {
|
||||||
|
StoredAccount::Mnemonic(account) => account.mnemonic(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -33,8 +131,6 @@ pub(crate) struct MnemonicAccount {
|
|||||||
hd_path: DerivationPath,
|
hd_path: DerivationPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// we only ever want to expose those getters in the test code
|
|
||||||
#[cfg(test)]
|
|
||||||
impl MnemonicAccount {
|
impl MnemonicAccount {
|
||||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||||
&self.mnemonic
|
&self.mnemonic
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub(crate) struct EncryptedData<T> {
|
|||||||
|
|
||||||
impl<T> Drop for EncryptedData<T> {
|
impl<T> Drop for EncryptedData<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.zeroize()
|
self.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
pub(crate) use crate::wallet_storage::password::{UserPassword, WalletAccountId};
|
||||||
|
|
||||||
use crate::error::BackendError;
|
use crate::error::BackendError;
|
||||||
|
use crate::operations::mixnet::account::create_new_account;
|
||||||
|
use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME};
|
||||||
use crate::wallet_storage::account_data::StoredAccount;
|
use crate::wallet_storage::account_data::StoredAccount;
|
||||||
use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData};
|
use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData};
|
||||||
use crate::wallet_storage::password::UserPassword;
|
|
||||||
use cosmrs::bip32::DerivationPath;
|
use cosmrs::bip32::DerivationPath;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::{create_dir_all, OpenOptions};
|
use std::fs::{create_dir_all, OpenOptions};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use self::account_data::{EncryptedAccount, StoredWallet};
|
||||||
|
|
||||||
pub(crate) mod account_data;
|
pub(crate) mod account_data;
|
||||||
pub(crate) mod encryption;
|
pub(crate) mod encryption;
|
||||||
|
|
||||||
mod password;
|
mod password;
|
||||||
|
|
||||||
const STORAGE_DIR_NAME: &str = "NymWallet";
|
pub(crate) const DEFAULT_WALLET_ACCOUNT_ID: &str = "default";
|
||||||
const WALLET_INFO_FILENAME: &str = "saved_wallet.json";
|
|
||||||
|
|
||||||
fn get_storage_directory() -> Result<PathBuf, BackendError> {
|
fn get_storage_directory() -> Result<PathBuf, BackendError> {
|
||||||
tauri::api::path::local_data_dir()
|
tauri::api::path::local_data_dir()
|
||||||
@@ -25,52 +32,76 @@ pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
|
|||||||
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
|
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_existing_wallet_login_information(
|
pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result<StoredWallet, BackendError> {
|
||||||
password: &UserPassword,
|
|
||||||
) -> Result<Vec<StoredAccount>, BackendError> {
|
|
||||||
let store_dir = get_storage_directory()?;
|
let store_dir = get_storage_directory()?;
|
||||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||||
|
load_existing_wallet_at_file(filepath)
|
||||||
|
}
|
||||||
|
|
||||||
load_existing_wallet_login_information_at_file(filepath, password)
|
fn load_existing_wallet_at_file(filepath: PathBuf) -> Result<StoredWallet, BackendError> {
|
||||||
|
if !filepath.exists() {
|
||||||
|
return Err(BackendError::WalletFileNotFound);
|
||||||
|
}
|
||||||
|
let file = OpenOptions::new().read(true).open(filepath)?;
|
||||||
|
let wallet: StoredWallet = serde_json::from_reader(file)?;
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_existing_wallet_login_information(
|
||||||
|
id: &WalletAccountId,
|
||||||
|
password: &UserPassword,
|
||||||
|
) -> Result<StoredAccount, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_existing_wallet_login_information_at_file(
|
fn load_existing_wallet_login_information_at_file(
|
||||||
filepath: PathBuf,
|
filepath: PathBuf,
|
||||||
|
id: &WalletAccountId,
|
||||||
password: &UserPassword,
|
password: &UserPassword,
|
||||||
) -> Result<Vec<StoredAccount>, BackendError> {
|
) -> Result<StoredAccount, BackendError> {
|
||||||
if !filepath.exists() {
|
load_existing_wallet_at_file(filepath)?.decrypt_account(id, password)
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let file = OpenOptions::new().read(true).open(filepath)?;
|
|
||||||
let encrypted_data: EncryptedData<Vec<StoredAccount>> = serde_json::from_reader(file)?;
|
|
||||||
encrypted_data.decrypt_struct(password)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn store_wallet_login_information(
|
pub(crate) fn store_wallet_login_information(
|
||||||
mnemonic: bip39::Mnemonic,
|
mnemonic: bip39::Mnemonic,
|
||||||
hd_path: DerivationPath,
|
hd_path: DerivationPath,
|
||||||
password: UserPassword,
|
id: WalletAccountId,
|
||||||
|
password: &UserPassword,
|
||||||
) -> Result<(), BackendError> {
|
) -> Result<(), BackendError> {
|
||||||
// make sure the entire directory structure exists
|
// make sure the entire directory structure exists
|
||||||
let store_dir = get_storage_directory()?;
|
let store_dir = get_storage_directory()?;
|
||||||
create_dir_all(&store_dir)?;
|
create_dir_all(&store_dir)?;
|
||||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||||
|
|
||||||
store_wallet_login_information_at_file(filepath, mnemonic, hd_path, &password)
|
store_wallet_login_information_at_file(filepath, mnemonic, hd_path, id, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_wallet_login_information_at_file(
|
fn store_wallet_login_information_at_file(
|
||||||
filepath: PathBuf,
|
filepath: PathBuf,
|
||||||
mnemonic: bip39::Mnemonic,
|
mnemonic: bip39::Mnemonic,
|
||||||
hd_path: DerivationPath,
|
hd_path: DerivationPath,
|
||||||
|
id: WalletAccountId,
|
||||||
password: &UserPassword,
|
password: &UserPassword,
|
||||||
) -> Result<(), BackendError> {
|
) -> Result<(), BackendError> {
|
||||||
let mut all_accounts =
|
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
|
||||||
load_existing_wallet_login_information_at_file(filepath.clone(), password)?;
|
Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
|
||||||
let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path);
|
result => result?,
|
||||||
all_accounts.push(new_account);
|
};
|
||||||
|
|
||||||
let encrypted = encrypt_struct(&all_accounts, password)?;
|
// Confirm that the given password also can unlock the other entries
|
||||||
|
if !stored_wallet.password_can_decrypt_all(password) {
|
||||||
|
return Err(BackendError::WalletDifferentPasswordDetected);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path);
|
||||||
|
let new_encrypted_account = EncryptedAccount {
|
||||||
|
id,
|
||||||
|
account: encrypt_struct(&new_account, password)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
stored_wallet.add_encrypted_account(new_encrypted_account)?;
|
||||||
|
|
||||||
let file = OpenOptions::new()
|
let file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
@@ -78,9 +109,7 @@ fn store_wallet_login_information_at_file(
|
|||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(filepath)?;
|
.open(filepath)?;
|
||||||
|
|
||||||
serde_json::to_writer_pretty(file, &encrypted)?;
|
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX
|
// this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX
|
||||||
@@ -91,15 +120,11 @@ fn store_wallet_login_information_at_file(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::wallet_storage::encryption::encrypt_data;
|
||||||
use config::defaults::COSMOS_DERIVATION_PATH;
|
use config::defaults::COSMOS_DERIVATION_PATH;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn read_encrypted_blob(file: PathBuf) -> EncryptedData<Vec<StoredAccount>> {
|
|
||||||
let file = OpenOptions::new().read(true).open(&file).unwrap();
|
|
||||||
serde_json::from_reader(file).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// I'm not 100% sure how to feel about having to touch the file system at all
|
// I'm not 100% sure how to feel about having to touch the file system at all
|
||||||
#[test]
|
#[test]
|
||||||
fn storing_wallet_information() {
|
fn storing_wallet_information() {
|
||||||
@@ -114,29 +139,36 @@ mod tests {
|
|||||||
let password = UserPassword::new("password".to_string());
|
let password = UserPassword::new("password".to_string());
|
||||||
let bad_password = UserPassword::new("bad-password".to_string());
|
let bad_password = UserPassword::new("bad-password".to_string());
|
||||||
|
|
||||||
// nothing was stored on the disk, so regardless of password used, there will be no error, but
|
let id1 = WalletAccountId::new("first".to_string());
|
||||||
// returned list will be empty
|
let id2 = WalletAccountId::new("second".to_string());
|
||||||
assert!(
|
|
||||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &password)
|
|
||||||
.unwrap()
|
|
||||||
.is_empty()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password)
|
|
||||||
.unwrap()
|
|
||||||
.is_empty()
|
|
||||||
);
|
|
||||||
|
|
||||||
// store the first account
|
// Nothing was stored on the disk
|
||||||
|
assert!(matches!(
|
||||||
|
load_existing_wallet_at_file(wallet_file.clone()),
|
||||||
|
Err(BackendError::WalletFileNotFound),
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password),
|
||||||
|
Err(BackendError::WalletFileNotFound),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Store the first account
|
||||||
store_wallet_login_information_at_file(
|
store_wallet_login_information_at_file(
|
||||||
wallet_file.clone(),
|
wallet_file.clone(),
|
||||||
dummy_account1.clone(),
|
dummy_account1.clone(),
|
||||||
cosmos_hd_path.clone(),
|
cosmos_hd_path.clone(),
|
||||||
|
id1.clone(),
|
||||||
&password,
|
&password,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let encrypted_blob = read_encrypted_blob(wallet_file.clone());
|
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())
|
||||||
|
);
|
||||||
|
let encrypted_blob = &stored_wallet.encrypted_account_by_index(0).unwrap().account;
|
||||||
|
|
||||||
// some actual ciphertext was saved
|
// some actual ciphertext was saved
|
||||||
assert!(!encrypted_blob.ciphertext().is_empty());
|
assert!(!encrypted_blob.ciphertext().is_empty());
|
||||||
@@ -146,53 +178,78 @@ mod tests {
|
|||||||
let original_salt = encrypted_blob.salt().to_vec();
|
let original_salt = encrypted_blob.salt().to_vec();
|
||||||
|
|
||||||
// trying to load it with wrong password now fails
|
// trying to load it with wrong password now fails
|
||||||
assert!(
|
assert!(matches!(
|
||||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password).is_err()
|
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &bad_password),
|
||||||
);
|
Err(BackendError::DecryptionError),
|
||||||
|
));
|
||||||
|
// and with the wrong id also fails
|
||||||
|
assert!(matches!(
|
||||||
|
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password),
|
||||||
|
Err(BackendError::NoSuchIdInWallet),
|
||||||
|
));
|
||||||
|
|
||||||
let loaded_accounts =
|
// and storing the same id again fails
|
||||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &password).unwrap();
|
assert!(matches!(
|
||||||
println!("{:?}", loaded_accounts);
|
store_wallet_login_information_at_file(
|
||||||
assert_eq!(1, loaded_accounts.len());
|
wallet_file.clone(),
|
||||||
|
dummy_account1.clone(),
|
||||||
|
cosmos_hd_path.clone(),
|
||||||
|
id1.clone(),
|
||||||
|
&password,
|
||||||
|
),
|
||||||
|
Err(BackendError::IdAlreadyExistsInWallet),
|
||||||
|
));
|
||||||
|
|
||||||
let StoredAccount::Mnemonic(acc) = &loaded_accounts[0];
|
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!(&dummy_account1, acc.mnemonic());
|
||||||
assert_eq!(&cosmos_hd_path, acc.hd_path());
|
assert_eq!(&cosmos_hd_path, acc.hd_path());
|
||||||
|
|
||||||
// can't store extra account if you use different password
|
// Can't store extra account if you use different password
|
||||||
assert!(store_wallet_login_information_at_file(
|
assert!(matches!(
|
||||||
wallet_file.clone(),
|
store_wallet_login_information_at_file(
|
||||||
dummy_account2.clone(),
|
wallet_file.clone(),
|
||||||
cosmos_hd_path.clone(),
|
dummy_account2.clone(),
|
||||||
&bad_password,
|
cosmos_hd_path.clone(),
|
||||||
)
|
id2.clone(),
|
||||||
.is_err());
|
&bad_password
|
||||||
|
),
|
||||||
|
Err(BackendError::WalletDifferentPasswordDetected),
|
||||||
|
));
|
||||||
|
|
||||||
// add extra account properly now
|
// add extra account properly now
|
||||||
store_wallet_login_information_at_file(
|
store_wallet_login_information_at_file(
|
||||||
wallet_file.clone(),
|
wallet_file.clone(),
|
||||||
dummy_account2.clone(),
|
dummy_account2.clone(),
|
||||||
different_hd_path.clone(),
|
different_hd_path.clone(),
|
||||||
|
id2.clone(),
|
||||||
&password,
|
&password,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let encrypted_blob = read_encrypted_blob(wallet_file.clone());
|
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)
|
||||||
|
.unwrap()
|
||||||
|
.account;
|
||||||
|
|
||||||
// fresh IV and salt are used
|
// fresh IV and salt are used
|
||||||
assert_ne!(original_iv, encrypted_blob.iv());
|
assert_ne!(original_iv, encrypted_blob.iv());
|
||||||
assert_ne!(original_salt, encrypted_blob.salt());
|
assert_ne!(original_salt, encrypted_blob.salt());
|
||||||
|
|
||||||
let loaded_accounts =
|
|
||||||
load_existing_wallet_login_information_at_file(wallet_file, &password).unwrap();
|
|
||||||
assert_eq!(2, loaded_accounts.len());
|
|
||||||
|
|
||||||
// first account should be unchanged
|
// first account should be unchanged
|
||||||
let StoredAccount::Mnemonic(acc1) = &loaded_accounts[0];
|
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!(&dummy_account1, acc1.mnemonic());
|
||||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||||
|
|
||||||
let StoredAccount::Mnemonic(acc2) = &loaded_accounts[1];
|
let loaded_account =
|
||||||
|
load_existing_wallet_login_information_at_file(wallet_file, &id2, &password).unwrap();
|
||||||
|
let StoredAccount::Mnemonic(ref acc2) = loaded_account;
|
||||||
assert_eq!(&dummy_account2, acc2.mnemonic());
|
assert_eq!(&dummy_account2, acc2.mnemonic());
|
||||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct WalletAccountId(String);
|
||||||
|
|
||||||
|
impl WalletAccountId {
|
||||||
|
pub(crate) fn new(id: String) -> WalletAccountId {
|
||||||
|
WalletAccountId(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for WalletAccountId {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// simple wrapper for String that will get zeroized on drop
|
// simple wrapper for String that will get zeroized on drop
|
||||||
#[derive(Zeroize)]
|
#[derive(Zeroize)]
|
||||||
#[zeroize(drop)]
|
#[zeroize(drop)]
|
||||||
|
|||||||
Reference in New Issue
Block a user