Merge pull request #1273 from nymtech/fix/wallet-store-ids-in-state
wallet: store ids in state and require password to switch account
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- wallet: require password to switch accounts
|
||||
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
|
||||
- wallet: added support for multiple accounts ([#1265])
|
||||
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
|
||||
|
||||
Generated
+1
@@ -3178,6 +3178,7 @@ dependencies = [
|
||||
"proxy-helpers",
|
||||
"publicsuffix",
|
||||
"rand 0.7.3",
|
||||
"rocket",
|
||||
"serde",
|
||||
"socks5-requests",
|
||||
"sqlx",
|
||||
|
||||
@@ -43,13 +43,12 @@ fn main() {
|
||||
mixnet::account::does_password_file_exist,
|
||||
mixnet::account::get_balance,
|
||||
mixnet::account::list_accounts,
|
||||
mixnet::account::sign_in_decrypted_account,
|
||||
mixnet::account::logout,
|
||||
mixnet::account::remove_account_for_password,
|
||||
mixnet::account::remove_password,
|
||||
mixnet::account::show_mnemonic_for_account_in_password,
|
||||
mixnet::account::sign_in_decrypted_account,
|
||||
mixnet::account::sign_in_with_password,
|
||||
mixnet::account::sign_in_with_password_and_account_id,
|
||||
mixnet::account::switch_network,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::admin::get_contract_settings,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::error::BackendError;
|
||||
use crate::network::Network as WalletNetwork;
|
||||
use crate::network_config;
|
||||
use crate::nymd_client;
|
||||
use crate::state::State;
|
||||
use crate::state::{State, WalletAccountIds};
|
||||
use crate::wallet_storage::{self, DEFAULT_LOGIN_ID};
|
||||
|
||||
use bip39::{Language, Mnemonic};
|
||||
@@ -229,6 +229,10 @@ async fn _connect_with_mnemonic(
|
||||
};
|
||||
|
||||
// Register all the clients
|
||||
{
|
||||
let mut w_state = state.write().await;
|
||||
w_state.logout();
|
||||
}
|
||||
for client in clients {
|
||||
let network: WalletNetwork = client.network.into();
|
||||
let mut w_state = state.write().await;
|
||||
@@ -395,9 +399,9 @@ pub async fn sign_in_with_password(
|
||||
}
|
||||
|
||||
fn extract_first_mnemonic(
|
||||
stored_account: &wallet_storage::StoredLogin,
|
||||
stored_login: &wallet_storage::StoredLogin,
|
||||
) -> Result<Mnemonic, BackendError> {
|
||||
let mnemonic = match stored_account {
|
||||
let mnemonic = match stored_login {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => {
|
||||
// Login using the first account in the list
|
||||
@@ -413,6 +417,44 @@ fn extract_first_mnemonic(
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_with_password_and_account_id(
|
||||
account_id: &str,
|
||||
password: &str,
|
||||
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 login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_mnemonic(&stored_login, &account_id)?;
|
||||
let first_login_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
fn extract_mnemonic(
|
||||
stored_login: &wallet_storage::StoredLogin,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
) -> Result<Mnemonic, BackendError> {
|
||||
let mnemonic = match stored_login {
|
||||
wallet_storage::StoredLogin::Mnemonic(_) => {
|
||||
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
|
||||
}
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => accounts
|
||||
.get_account(account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone(),
|
||||
};
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_password() -> Result<(), BackendError> {
|
||||
log::info!("Removing password");
|
||||
@@ -476,11 +518,31 @@ async fn set_state_with_all_accounts(
|
||||
.collect();
|
||||
|
||||
for account in &all_accounts {
|
||||
log::trace!("account: {:?}", account);
|
||||
log::trace!("account: {:?}", account.id());
|
||||
}
|
||||
|
||||
let all_account_ids: Vec<WalletAccountIds> = all_accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
let mnemonic = account.mnemonic();
|
||||
let addresses: HashMap<WalletNetwork, cosmrs::AccountId> = WalletNetwork::iter()
|
||||
.map(|network| {
|
||||
let config_network: Network = network.into();
|
||||
(
|
||||
network,
|
||||
derive_address(mnemonic.clone(), config_network.bech32_prefix()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
WalletAccountIds {
|
||||
id: account.id().clone(),
|
||||
addresses,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut w_state = state.write().await;
|
||||
w_state.set_all_accounts(all_accounts);
|
||||
w_state.set_all_accounts(all_account_ids);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -523,16 +585,13 @@ pub async fn list_accounts(
|
||||
) -> Result<Vec<AccountEntry>, BackendError> {
|
||||
log::trace!("Listing accounts");
|
||||
let state = state.read().await;
|
||||
let network: Network = state.current_network().into();
|
||||
let prefix = network.bech32_prefix();
|
||||
let network = state.current_network();
|
||||
|
||||
let all_accounts = state
|
||||
.get_all_accounts()
|
||||
.map(|account| AccountEntry {
|
||||
id: account.id().to_string(),
|
||||
address: derive_address(account.mnemonic().clone(), prefix)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
id: account.id.to_string(),
|
||||
address: account.addresses[&network].to_string(),
|
||||
})
|
||||
.map(|account| {
|
||||
log::trace!("{:?}", account);
|
||||
@@ -552,8 +611,16 @@ pub fn show_mnemonic_for_account_in_password(
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id);
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let stored_account = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?;
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
|
||||
fn _show_mnemonic_for_account_in_password(
|
||||
login_id: &wallet_storage::LoginId,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
password: &wallet_storage::UserPassword,
|
||||
) -> Result<bip39::Mnemonic, BackendError> {
|
||||
let stored_account = wallet_storage::load_existing_login(login_id, password)?;
|
||||
let mnemonic = match stored_account {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => {
|
||||
@@ -561,36 +628,13 @@ pub fn show_mnemonic_for_account_in_password(
|
||||
log::debug!("{:?}", account);
|
||||
}
|
||||
accounts
|
||||
.get_account(&account_id)
|
||||
.get_account(account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_decrypted_account(
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<Account, BackendError> {
|
||||
log::info!("Signing in to already decrypted account: {account_id}");
|
||||
let mnemonic = {
|
||||
let state = state.read().await;
|
||||
let account = &state
|
||||
.get_all_accounts()
|
||||
.find(|a| a.id().as_ref() == account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?;
|
||||
account.mnemonic().clone()
|
||||
};
|
||||
|
||||
{
|
||||
state.write().await.logout();
|
||||
}
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::error::BackendError;
|
||||
use crate::network::Network;
|
||||
use crate::wallet_storage::account_data::WalletAccount;
|
||||
use crate::{config, network_config};
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
@@ -49,7 +48,7 @@ pub struct State {
|
||||
|
||||
// All the accounts the we get from decrypting the wallet. We hold on to these for being able to
|
||||
// switch accounts on-the-fly
|
||||
all_accounts: Vec<WalletAccount>,
|
||||
all_accounts: Vec<WalletAccountIds>,
|
||||
|
||||
/// Validators that have been fetched dynamically, probably during startup.
|
||||
fetched_validators: config::OptionalValidators,
|
||||
@@ -58,6 +57,13 @@ pub struct State {
|
||||
validator_metadata: HashMap<Url, ValidatorMetadata>,
|
||||
}
|
||||
|
||||
pub(crate) struct WalletAccountIds {
|
||||
// The wallet account id
|
||||
pub id: crate::wallet_storage::AccountId,
|
||||
// The set of corresponding network identities derived from the mnemonic
|
||||
pub addresses: HashMap<Network, cosmrs::AccountId>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn client(&self, network: Network) -> Result<&Client<SigningNymdClient>, BackendError> {
|
||||
self
|
||||
@@ -117,11 +123,11 @@ impl State {
|
||||
self.current_network
|
||||
}
|
||||
|
||||
pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec<WalletAccount>) {
|
||||
pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec<WalletAccountIds>) {
|
||||
self.all_accounts = all_accounts
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_accounts(&self) -> impl Iterator<Item = &WalletAccount> {
|
||||
pub(crate) fn get_all_accounts(&self) -> impl Iterator<Item = &WalletAccountIds> {
|
||||
self.all_accounts.iter()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,16 @@ import { useClipboard } from 'use-clipboard-copy';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { AccountAvatar } from './AccountAvatar';
|
||||
|
||||
export const AccountItem = ({ name, address }: { name: string; address: string }) => {
|
||||
const { selectedAccount, handleSelectAccount, setDialogToDisplay, setAccountMnemonic } = useContext(AccountsContext);
|
||||
export const AccountItem = ({
|
||||
name,
|
||||
address,
|
||||
onSelectAccount,
|
||||
}: {
|
||||
name: string;
|
||||
address: string;
|
||||
onSelectAccount: () => void;
|
||||
}) => {
|
||||
const { selectedAccount, setDialogToDisplay, setAccountMnemonic } = useContext(AccountsContext);
|
||||
const { copy, copied } = useClipboard({ copiedTimeout: 1000 });
|
||||
return (
|
||||
<ListItem
|
||||
@@ -13,7 +21,7 @@ export const AccountItem = ({ name, address }: { name: string; address: string }
|
||||
disableGutters
|
||||
sx={selectedAccount?.id === name ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}
|
||||
>
|
||||
<ListItemButton disableRipple onClick={() => handleSelectAccount(name)}>
|
||||
<ListItemButton disableRipple onClick={onSelectAccount}>
|
||||
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
|
||||
<AccountAvatar name={name} />
|
||||
</ListItemAvatar>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { AccountsContext, AppContext } from 'src/context';
|
||||
import { EditAccountModal } from './EditAccountModal';
|
||||
import { AddAccountModal } from './AddAccountModal';
|
||||
import { AccountsModal } from './AccountsModal';
|
||||
import { MnemonicModal } from './MnemonicModal';
|
||||
import { EditAccountModal } from './modals/EditAccountModal';
|
||||
import { AddAccountModal } from './modals/AddAccountModal';
|
||||
import { AccountsModal } from './modals/AccountsModal';
|
||||
import { MnemonicModal } from './modals/MnemonicModal';
|
||||
import { AccountOverview } from './AccountOverview';
|
||||
import { MultiAccountHowTo } from './MultiAccountHowTo';
|
||||
|
||||
|
||||
+34
-6
@@ -1,17 +1,36 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material';
|
||||
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { AccountItem } from './AccountItem';
|
||||
import { AccountItem } from '../AccountItem';
|
||||
import { ConfirmPasswordModal } from './ConfirmPasswordModal';
|
||||
|
||||
export const AccountsModal = ({ onClose }: { onClose?: () => void }) => {
|
||||
const { accounts, dialogToDisplay, setDialogToDisplay } = useContext(AccountsContext);
|
||||
export const AccountsModal = () => {
|
||||
const { accounts, dialogToDisplay, setDialogToDisplay, setError, handleSelectAccount, selectedAccount } =
|
||||
useContext(AccountsContext);
|
||||
const [accountToSwitchTo, setAccountToSwitchTo] = useState<string>();
|
||||
|
||||
const handleClose = () => {
|
||||
setDialogToDisplay(undefined);
|
||||
onClose?.();
|
||||
setError(undefined);
|
||||
setAccountToSwitchTo(undefined);
|
||||
};
|
||||
|
||||
if (accountToSwitchTo)
|
||||
return (
|
||||
<ConfirmPasswordModal
|
||||
accountName={accountToSwitchTo}
|
||||
onClose={() => {
|
||||
handleClose();
|
||||
setDialogToDisplay('Accounts');
|
||||
}}
|
||||
onConfirm={async (password) => {
|
||||
const isSuccessful = await handleSelectAccount({ password, accountName: accountToSwitchTo });
|
||||
if (isSuccessful) handleClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={dialogToDisplay === 'Accounts'} onClose={handleClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
@@ -27,7 +46,16 @@ export const AccountsModal = ({ onClose }: { onClose?: () => void }) => {
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ padding: 0 }}>
|
||||
{accounts?.map(({ id, address }) => (
|
||||
<AccountItem name={id} address={address} key={address} />
|
||||
<AccountItem
|
||||
name={id}
|
||||
address={address}
|
||||
key={address}
|
||||
onSelectAccount={() => {
|
||||
if (selectedAccount?.id !== id) {
|
||||
setAccountToSwitchTo(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
+32
-76
@@ -2,24 +2,21 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ArrowBackSharp } from '@mui/icons-material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { createMnemonic, validateMnemonic } from 'src/requests';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { PasswordInput } from 'src/components';
|
||||
import { Mnemonic } from '../Mnemonic';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { match } from 'assert';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { ConfirmPassword, Mnemonic } from 'src/components';
|
||||
import { MnemonicInput } from 'src/components/textfields';
|
||||
|
||||
const createAccountSteps = [
|
||||
'Copy and save mnemonic for your new account',
|
||||
@@ -66,27 +63,18 @@ const ImportMnemonic = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
{error && (
|
||||
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
multiline
|
||||
placeholder="Mnemonic to import"
|
||||
rows={3}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setError(undefined);
|
||||
}}
|
||||
fullWidth
|
||||
type="password"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
<MnemonicInput
|
||||
mnemonic={value}
|
||||
onUpdateMnemonic={(mnemon) => {
|
||||
onChange(mnemon);
|
||||
setError(undefined);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button
|
||||
@@ -100,7 +88,7 @@ const ImportMnemonic = ({
|
||||
Next
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -110,16 +98,16 @@ const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => {
|
||||
|
||||
const nameValidation = /^([a-zA-Z0-9\s]){1,20}$/;
|
||||
|
||||
const handleNext = (value: string) => {
|
||||
if (!nameValidation.test(value))
|
||||
[setError('Account name must contain only letters and numbers and be between 1 and 20 characters')];
|
||||
else onNext(value);
|
||||
const handleNext = (accountName: string) => {
|
||||
if (!nameValidation.test(accountName)) {
|
||||
setError('Account name must contain only letters and numbers and be between 1 and 20 characters');
|
||||
} else onNext(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
<TextField
|
||||
@@ -144,43 +132,7 @@ const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => {
|
||||
Next
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfirmPassword = ({ onConfirm }: { onConfirm: (password: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const { isLoading, error } = useContext(AccountsContext);
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<DialogContent>
|
||||
{error && (
|
||||
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
<PasswordInput
|
||||
password={value}
|
||||
onUpdatePassword={(pswrd) => setValue(pswrd)}
|
||||
label="Confirm password"
|
||||
autoFocus
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button
|
||||
disabled={!value.length || isLoading}
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => onConfirm(value)}
|
||||
endIcon={isLoading && <CircularProgress size={20} />}
|
||||
>
|
||||
Add account
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -191,24 +143,25 @@ export const AddAccountModal = () => {
|
||||
accountName: '',
|
||||
});
|
||||
|
||||
const { dialogToDisplay, setDialogToDisplay, handleAddAccount, setError } = useContext(AccountsContext);
|
||||
const { dialogToDisplay, setDialogToDisplay, handleAddAccount, setError, isLoading, error } =
|
||||
useContext(AccountsContext);
|
||||
|
||||
const generateMnemonic = async () => {
|
||||
const mnemon = await createMnemonic();
|
||||
setData((d) => ({ ...d, mnemonic: mnemon }));
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setDialogToDisplay('Accounts');
|
||||
resetState();
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
setData({ mnemonic: '', accountName: '' });
|
||||
setStep(0);
|
||||
setError(undefined);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setDialogToDisplay('Accounts');
|
||||
resetState();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogToDisplay === 'Add') generateMnemonic();
|
||||
if (dialogToDisplay === 'Accounts') resetState();
|
||||
@@ -260,6 +213,7 @@ export const AddAccountModal = () => {
|
||||
case 2:
|
||||
return (
|
||||
<ConfirmPassword
|
||||
buttonTitle="Add account"
|
||||
onConfirm={async (password) => {
|
||||
if (data.accountName && data.mnemonic) {
|
||||
try {
|
||||
@@ -271,6 +225,8 @@ export const AddAccountModal = () => {
|
||||
}
|
||||
}
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, Dialog, DialogTitle, IconButton, Typography } from '@mui/material';
|
||||
import { ArrowBack } from '@mui/icons-material';
|
||||
import { ConfirmPassword } from 'src/components/ConfirmPassword';
|
||||
import { AccountsContext } from 'src/context';
|
||||
|
||||
export const ConfirmPasswordModal = ({
|
||||
accountName,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
accountName?: string;
|
||||
onClose: () => void;
|
||||
onConfirm: (password: string) => Promise<void>;
|
||||
}) => {
|
||||
const { isLoading, error } = useContext(AccountsContext);
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Switch account</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
Confirm password
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<ConfirmPassword onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+3
-4
@@ -13,8 +13,7 @@ import {
|
||||
import { ArrowBackSharp } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { Mnemonic } from '../Mnemonic';
|
||||
import { PasswordInput } from '../textfields';
|
||||
import { PasswordInput, Mnemonic } from 'src/components';
|
||||
|
||||
export const MnemonicModal = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -26,7 +25,7 @@ export const MnemonicModal = () => {
|
||||
setDialogToDisplay,
|
||||
accountMnemonic,
|
||||
setAccountMnemonic,
|
||||
handleGetAcccountMnemonic,
|
||||
handleGetAccountMnemonic,
|
||||
error,
|
||||
setError,
|
||||
isLoading,
|
||||
@@ -86,7 +85,7 @@ export const MnemonicModal = () => {
|
||||
onClick={async () => {
|
||||
if (accountMnemonic?.accountName) {
|
||||
setError(undefined);
|
||||
await handleGetAcccountMnemonic({ password, accountName: accountMnemonic?.accountName });
|
||||
await handleGetAccountMnemonic({ password, accountName: accountMnemonic?.accountName });
|
||||
}
|
||||
}}
|
||||
endIcon={isLoading && <CircularProgress size={20} />}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, CircularProgress, DialogActions, DialogContent, Typography } from '@mui/material';
|
||||
import { PasswordInput } from './textfields';
|
||||
|
||||
export const ConfirmPassword = ({
|
||||
error,
|
||||
isLoading,
|
||||
onConfirm,
|
||||
buttonTitle,
|
||||
}: {
|
||||
error?: string;
|
||||
isLoading?: boolean;
|
||||
buttonTitle: string;
|
||||
onConfirm: (password: string) => void;
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
|
||||
<PasswordInput
|
||||
password={value}
|
||||
onUpdatePassword={(pswrd) => setValue(pswrd)}
|
||||
placeholder="Confirm password"
|
||||
autoFocus
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button
|
||||
disabled={!value.length || isLoading}
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => onConfirm(value)}
|
||||
endIcon={isLoading && <CircularProgress size={20} />}
|
||||
>
|
||||
{buttonTitle}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,21 @@
|
||||
export * from './ErrorFallback';
|
||||
export * from './CopyToClipboard';
|
||||
export * from './NymCard';
|
||||
export * from './Nav';
|
||||
export * from './NodeTypeSelector';
|
||||
export * from './RequestStatus';
|
||||
export * from './NoClientError';
|
||||
export * from './SuccessResponse';
|
||||
export * from './TransactionDetails';
|
||||
export * from './NymLogo';
|
||||
export * from './Fee';
|
||||
export * from './AppBar';
|
||||
export * from './NetworkSelector';
|
||||
export * from './ClientAddress';
|
||||
export * from './ConfirmPassword';
|
||||
export * from './CopyToClipboard';
|
||||
export * from './ErrorFallback';
|
||||
export * from './Fee';
|
||||
export * from './InfoToolTip';
|
||||
export * from './LoadingPage';
|
||||
export * from './Mnemonic';
|
||||
export * from './Nav';
|
||||
export * from './NetworkSelector';
|
||||
export * from './NoClientError';
|
||||
export * from './NodeTypeSelector';
|
||||
export * from './NymCard';
|
||||
export * from './NymLogo';
|
||||
export * from './RequestStatus';
|
||||
export * from './SuccessResponse';
|
||||
export * from './textfields';
|
||||
export * from './Title';
|
||||
export * from './TokenPoolSelector';
|
||||
export * from './LoadingPage';
|
||||
export * from './textfields';
|
||||
export * from './TransactionDetails';
|
||||
|
||||
@@ -36,10 +36,11 @@ export const MnemonicInput: React.FC<{
|
||||
export const PasswordInput: React.FC<{
|
||||
password: string;
|
||||
error?: string;
|
||||
label: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
onUpdatePassword: (password: string) => void;
|
||||
}> = ({ password, label, error, autoFocus, onUpdatePassword }) => {
|
||||
}> = ({ password, label, placeholder, error, autoFocus, onUpdatePassword }) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -47,6 +48,7 @@ export const PasswordInput: React.FC<{
|
||||
<Box>
|
||||
<TextField
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => onUpdatePassword(e.target.value)}
|
||||
|
||||
@@ -16,11 +16,11 @@ type TAccounts = {
|
||||
setAccountMnemonic: Dispatch<SetStateAction<TAccountMnemonic>>;
|
||||
handleAddAccount: (data: { accountName: string; mnemonic: string; password: string }) => void;
|
||||
setDialogToDisplay: (dialog?: TAccountsDialog) => void;
|
||||
handleSelectAccount: (accountId: string) => void;
|
||||
handleSelectAccount: (data: { accountName: string; password: string }) => Promise<boolean>;
|
||||
handleAccountToEdit: (accountId: string) => void;
|
||||
handleEditAccount: (account: AccountEntry) => void;
|
||||
handleImportAccount: (account: AccountEntry) => void;
|
||||
handleGetAcccountMnemonic: (data: { password: string; accountName: string }) => void;
|
||||
handleGetAccountMnemonic: (data: { password: string; accountName: string }) => void;
|
||||
};
|
||||
|
||||
export type TAccountsDialog = 'Accounts' | 'Add' | 'Edit' | 'Import' | 'Mnemonic';
|
||||
@@ -75,15 +75,19 @@ export const AccountsProvider: React.FC = ({ children }) => {
|
||||
const handleAccountToEdit = (accountName: string) =>
|
||||
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
|
||||
|
||||
const handleSelectAccount = async (accountName: string) => {
|
||||
if (accountName !== selectedAccount?.id) {
|
||||
await onAccountChange(accountName);
|
||||
const handleSelectAccount = async ({ accountName, password }: { accountName: string; password: string }) => {
|
||||
try {
|
||||
await onAccountChange({ accountId: accountName, password });
|
||||
const match = accounts?.find((acc) => acc.id === accountName);
|
||||
setSelectedAccount(match);
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError('Error switching account. Please check your password');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetAcccountMnemonic = async ({ password, accountName }: { password: string; accountName: string }) => {
|
||||
const handleGetAccountMnemonic = async ({ password, accountName }: { password: string; accountName: string }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const mnemonic = await showMnemonicForAccount({ password, accountName });
|
||||
@@ -124,7 +128,7 @@ export const AccountsProvider: React.FC = ({ children }) => {
|
||||
handleAccountToEdit,
|
||||
handleSelectAccount,
|
||||
handleImportAccount,
|
||||
handleGetAcccountMnemonic,
|
||||
handleGetAccountMnemonic,
|
||||
}),
|
||||
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
|
||||
)}
|
||||
|
||||
@@ -55,7 +55,7 @@ type TAppContext = {
|
||||
handleShowTerminal: () => void;
|
||||
signInWithPassword: (password: string) => void;
|
||||
logOut: () => void;
|
||||
onAccountChange: (accountId: string) => void;
|
||||
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
|
||||
};
|
||||
|
||||
export const AppContext = createContext({} as TAppContext);
|
||||
@@ -165,15 +165,15 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
enqueueSnackbar('Successfully logged out', { variant: 'success' });
|
||||
};
|
||||
|
||||
const onAccountChange = async (accountId: string) => {
|
||||
const onAccountChange = async ({ accountId, password }: { accountId: string; password: string }) => {
|
||||
if (network) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await switchAccount(accountId);
|
||||
await switchAccount({ accountId, password });
|
||||
await loadAccount(network);
|
||||
enqueueSnackbar('Account switch success', { variant: 'success', preventDuplicate: true });
|
||||
} catch (e) {
|
||||
enqueueSnackbar(`Error swtiching account: ${e}`, { variant: 'error' });
|
||||
throw new Error(`Error swtiching account: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -36,14 +36,13 @@ export const MockAccountsProvider: React.FC = ({ children }) => {
|
||||
const handleAccountToEdit = (accountName: string) =>
|
||||
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
|
||||
|
||||
const handleSelectAccount = async (accountName: string) => {
|
||||
if (accountName !== selectedAccount?.id) {
|
||||
const match = accounts?.find((acc) => acc.id === accountName);
|
||||
setSelectedAccount(match);
|
||||
}
|
||||
const handleSelectAccount = async ({ accountName }: { accountName: string; password: string }) => {
|
||||
const match = accounts?.find((acc) => acc.id === accountName);
|
||||
setSelectedAccount(match);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleGetAcccountMnemonic = async ({ accountName }: { password: string; accountName: string }) => {
|
||||
const handleGetAccountMnemonic = async ({ accountName }: { password: string; accountName: string }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const mnemonic = 'test mnemonic';
|
||||
@@ -73,7 +72,7 @@ export const MockAccountsProvider: React.FC = ({ children }) => {
|
||||
handleAccountToEdit,
|
||||
handleSelectAccount,
|
||||
handleImportAccount,
|
||||
handleGetAcccountMnemonic,
|
||||
handleGetAccountMnemonic,
|
||||
}),
|
||||
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
|
||||
)}
|
||||
|
||||
@@ -62,6 +62,6 @@ export const showMnemonicForAccount = async ({ password, accountName }: { passwo
|
||||
return res;
|
||||
};
|
||||
|
||||
export const switchAccount = async (accountId: string) => {
|
||||
await invoke('sign_in_decrypted_account', { accountId });
|
||||
export const switchAccount = async ({ accountId, password }: { accountId: string; password: string }) => {
|
||||
await invoke('sign_in_with_password_and_account_id', { accountId, password });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user