Qortal-Hub/src/Wallets.tsx
2025-04-18 20:12:48 +02:00

585 lines
16 KiB
TypeScript

import React, { useContext, useEffect, useState } from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import Divider from '@mui/material/Divider';
import ListItemText from '@mui/material/ListItemText';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography';
import {
Box,
Button,
ButtonBase,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Input,
useTheme,
} from '@mui/material';
import { CustomButton } from './styles/App-styles';
import { useDropzone } from 'react-dropzone';
import EditIcon from '@mui/icons-material/Edit';
import { Label } from './components/Group/AddGroup';
import { Spacer } from './common/Spacer';
import { getWallets, storeWallets, walletVersion } from './background';
import { useModal } from './common/useModal';
import PhraseWallet from './utils/generateWallet/phrase-wallet';
import { decryptStoredWalletFromSeedPhrase } from './utils/decryptWallet';
import { crypto } from './constants/decryptWallet';
import { LoadingButton } from '@mui/lab';
import { PasswordField } from './components';
import { HtmlTooltip } from './ExtStates/NotAuthenticated';
import { GlobalContext } from './App';
const parsefilenameQortal = (filename) => {
return filename.startsWith('qortal_backup_') ? filename.slice(14) : filename;
};
export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
const [wallets, setWallets] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [seedValue, setSeedValue] = useState('');
const [seedName, setSeedName] = useState('');
const [seedError, setSeedError] = useState('');
const { hasSeenGettingStarted } = useContext(GlobalContext);
const [password, setPassword] = useState('');
const [isOpenSeedModal, setIsOpenSeedModal] = useState(false);
const [isLoadingEncryptSeed, setIsLoadingEncryptSeed] = useState(false);
const { isShow, onCancel, onOk, show } = useModal();
const { getRootProps, getInputProps } = useDropzone({
accept: {
'application/json': ['.json'], // Only accept JSON files
},
onDrop: async (acceptedFiles) => {
const files: any = acceptedFiles;
let importedWallets: any = [];
for (const file of files) {
try {
const fileContents = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onabort = () => reject('File reading was aborted');
reader.onerror = () => reject('File reading has failed');
reader.onload = () => {
// Resolve the promise with the reader result when reading completes
resolve(reader.result);
};
// Read the file as text
reader.readAsText(file);
});
if (typeof fileContents !== 'string') continue;
const parsedData = JSON.parse(fileContents);
importedWallets.push({ ...parsedData, filename: file?.name });
} catch (error) {
console.error(error);
}
}
let error: any = null;
let uniqueInitialMap = new Map();
// Only add a message if it doesn't already exist in the Map
importedWallets.forEach((wallet) => {
if (!wallet?.address0) return;
if (!uniqueInitialMap.has(wallet?.address0)) {
uniqueInitialMap.set(wallet?.address0, wallet);
}
});
const data = Array.from(uniqueInitialMap.values());
if (data && data?.length > 0) {
const uniqueNewWallets = data.filter(
(newWallet) =>
!wallets.some(
(existingWallet) =>
existingWallet?.address0 === newWallet?.address0
)
);
setWallets([...wallets, ...uniqueNewWallets]);
}
},
});
const updateWalletItem = (idx, wallet) => {
setWallets((prev) => {
let copyPrev = [...prev];
if (wallet === null) {
copyPrev.splice(idx, 1); // Use splice to remove the item
return copyPrev;
} else {
copyPrev[idx] = wallet; // Update the wallet at the specified index
return copyPrev;
}
});
};
const handleSetSeedValue = async () => {
try {
setIsOpenSeedModal(true);
const { seedValue, seedName, password } = await show({
message: '',
publishFee: '',
});
setIsLoadingEncryptSeed(true);
const res = await decryptStoredWalletFromSeedPhrase(seedValue);
const wallet2 = new PhraseWallet(res, walletVersion);
const wallet = await wallet2.generateSaveWalletData(
password,
crypto.kdfThreads,
() => {}
);
if (wallet?.address0) {
setWallets([
...wallets,
{
...wallet,
name: seedName,
},
]);
setIsOpenSeedModal(false);
setSeedValue('');
setSeedName('');
setPassword('');
setSeedError('');
} else {
setSeedError('Could not create account.');
}
} catch (error) {
setSeedError(error?.message || 'Could not create account.');
} finally {
setIsLoadingEncryptSeed(false);
}
};
const selectedWalletFunc = (wallet) => {
setRawWallet(wallet);
setExtState('wallet-dropped');
};
useEffect(() => {
setIsLoading(true);
getWallets()
.then((res) => {
if (res && Array.isArray(res)) {
setWallets(res);
}
setIsLoading(false);
})
.catch((error) => {
console.error(error);
setIsLoading(false);
});
}, []);
useEffect(() => {
if (!isLoading && wallets && Array.isArray(wallets)) {
storeWallets(wallets);
}
}, [wallets, isLoading]);
if (isLoading) return null;
return (
<div>
{wallets?.length === 0 || !wallets ? (
<>
<Typography>No accounts saved</Typography>
<Spacer height="75px" />
</>
) : (
<>
<Typography>Your saved accounts</Typography>
<Spacer height="30px" />
</>
)}
{rawWallet && (
<Box>
<Typography>Selected Account:</Typography>
{rawWallet?.name && <Typography>{rawWallet.name}</Typography>}
{rawWallet?.address0 && (
<Typography>{rawWallet?.address0}</Typography>
)}
</Box>
)}
{wallets?.length > 0 && (
<List
sx={{
width: '100%',
maxWidth: '500px',
maxHeight: '60vh',
overflowY: 'auto',
overflowX: 'hidden',
backgroundColor: 'rgb(30 30 32 / 70%)',
}}
>
{wallets?.map((wallet, idx) => {
return (
<>
<WalletItem
setSelectedWallet={selectedWalletFunc}
key={wallet?.address0}
wallet={wallet}
idx={idx}
updateWalletItem={updateWalletItem}
/>
<Divider variant="inset" component="li" />
</>
);
})}
</List>
)}
<Box
sx={{
display: 'flex',
gap: '10px',
alignItems: 'center',
position: wallets?.length === 0 ? 'relative' : 'fixed',
bottom: wallets?.length === 0 ? 'unset' : '20px',
right: wallets?.length === 0 ? 'unset' : '20px',
}}
>
<HtmlTooltip
disableHoverListener={hasSeenGettingStarted === true}
title={
<React.Fragment>
<Typography
color="inherit"
sx={{
fontSize: '16px',
}}
>
Already have a Qortal account? Enter your secret backup phrase
here to access it. This phrase is one of the ways to recover
your account.
</Typography>
</React.Fragment>
}
>
<CustomButton
onClick={handleSetSeedValue}
sx={{
padding: '10px',
display: 'inline',
}}
>
Add seed-phrase
</CustomButton>
</HtmlTooltip>
<HtmlTooltip
disableHoverListener={hasSeenGettingStarted === true}
title={
<React.Fragment>
<Typography
color="inherit"
sx={{
fontSize: '16px',
}}
>
Use this option to connect additional Qortal wallets you've
already made, in order to login with them afterwards. You will
need access to your backup JSON file in order to do so.
</Typography>
</React.Fragment>
}
>
<CustomButton
sx={{
padding: '10px',
}}
{...getRootProps()}
>
<input {...getInputProps()} />
Add account
</CustomButton>
</HtmlTooltip>
</Box>
<Dialog
open={isOpenSeedModal}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onKeyDown={(e) => {
if (e.key === 'Enter' && seedValue && seedName && password) {
onOk({ seedValue, seedName, password });
}
}}
>
<DialogTitle id="alert-dialog-title">
Type or paste in your seed-phrase
</DialogTitle>
<DialogContent>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Label>Name</Label>
<Input
placeholder="Name"
value={seedName}
onChange={(e) => setSeedName(e.target.value)}
/>
<Spacer height="7px" />
<Label>Seed-phrase</Label>
<PasswordField
placeholder="Seed-phrase"
id="standard-adornment-password"
value={seedValue}
onChange={(e) => setSeedValue(e.target.value)}
autoComplete="off"
sx={{
width: '100%',
}}
/>
<Spacer height="7px" />
<Label>Choose new password</Label>
<PasswordField
id="standard-adornment-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="off"
sx={{
width: '100%',
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button
disabled={isLoadingEncryptSeed}
variant="contained"
onClick={() => {
setIsOpenSeedModal(false);
setSeedValue('');
setSeedName('');
setPassword('');
setSeedError('');
}}
>
Close
</Button>
<LoadingButton
loading={isLoadingEncryptSeed}
disabled={!seedValue || !seedName || !password}
variant="contained"
onClick={() => {
if (!seedValue || !seedName || !password) return;
onOk({ seedValue, seedName, password });
}}
autoFocus
>
Add
</LoadingButton>
<Typography
sx={{
fontSize: '14px',
visibility: seedError ? 'visible' : 'hidden',
}}
>
{seedError}
</Typography>
</DialogActions>
</Dialog>
</div>
);
};
const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
const [name, setName] = useState('');
const [note, setNote] = useState('');
const [isEdit, setIsEdit] = useState(false);
const theme = useTheme();
useEffect(() => {
if (wallet?.name) {
setName(wallet.name);
}
if (wallet?.note) {
setNote(wallet.note);
}
}, [wallet]);
return (
<>
<ButtonBase
onClick={() => {
setSelectedWallet(wallet);
}}
sx={{
width: '100%',
padding: '10px',
}}
>
<ListItem
sx={{
bgcolor: theme.palette.background.default,
flexGrow: 1,
'&:hover': {
backgroundColor: theme.palette.background.paper,
transform: 'scale(1.01)',
},
transition: 'all 0.1s ease-in-out',
}}
alignItems="flex-start"
>
<ListItemAvatar>
<Avatar alt="" src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={
wallet?.name
? wallet.name
: wallet?.filename
? parsefilenameQortal(wallet?.filename)
: 'No name'
}
secondary={
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Typography
component="span"
variant="body2"
sx={{ color: theme.palette.text.primary, display: 'inline' }}
>
{wallet?.address0}
</Typography>
{wallet?.note}
<Typography
sx={{
textAlign: 'end',
marginTop: '5px',
}}
>
Login
</Typography>
</Box>
}
/>
</ListItem>
<IconButton
sx={{
alignSelf: 'flex-start',
}}
onClick={(e) => {
e.stopPropagation();
setIsEdit(true);
}}
edge="end"
aria-label="edit"
>
<EditIcon />
</IconButton>
</ButtonBase>
{isEdit && (
<Box
sx={{
padding: '8px',
}}
>
<Label>Name</Label>
<Input
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
sx={{
width: '100%',
}}
/>
<Spacer height="10px" />
<Label>Note</Label>
<Input
placeholder="Note"
value={note}
onChange={(e) => setNote(e.target.value)}
inputProps={{
maxLength: 100,
}}
sx={{
width: '100%',
}}
/>
<Spacer height="10px" />
<Box
sx={{
display: 'flex',
gap: '20px',
justifyContent: 'flex-end',
width: '100%',
}}
>
<Button
size="small"
variant="contained"
onClick={() => setIsEdit(false)}
>
Close
</Button>
<Button
sx={{
backgroundColor: 'var(--danger)',
'&:hover': {
backgroundColor: 'var(--danger)',
},
'&:focus': {
backgroundColor: 'var(--danger)',
},
}}
size="small"
variant="contained"
onClick={() => updateWalletItem(idx, null)}
>
Remove
</Button>
<Button
sx={{
backgroundColor: '#5EB049',
'&:hover': {
backgroundColor: '#5EB049',
},
'&:focus': {
backgroundColor: '#5EB049',
},
}}
size="small"
variant="contained"
onClick={() => {
updateWalletItem(idx, {
...wallet,
name,
note,
});
setIsEdit(false);
}}
>
Save
</Button>
</Box>
</Box>
)}
</>
);
};