added seedphrase

This commit is contained in:
PhilReact 2024-11-22 23:51:57 +02:00
parent c3eb1fd50a
commit af631df00d
13 changed files with 1027 additions and 33 deletions

49
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "qortal-go",
"version": "0.3.2",
"version": "0.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qortal-go",
"version": "0.3.2",
"version": "0.3.3",
"dependencies": {
"@capacitor/android": "^6.1.2",
"@capacitor/app": "^6.0.1",
@ -53,6 +53,7 @@
"file-saver": "^2.0.5",
"html-to-text": "^9.0.5",
"jssha": "3.3.1",
"lit": "^3.2.1",
"lodash": "^4.17.21",
"mime": "^4.0.4",
"moment": "^2.30.1",
@ -3029,6 +3030,19 @@
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ=="
},
"node_modules/@lit/reactive-element": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0"
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
@ -4757,8 +4771,7 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
@ -8200,6 +8213,34 @@
"uc.micro": "^2.0.0"
}
},
"node_modules/lit": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.1.0",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-element": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.0.4",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-html": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/local-pkg": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",

View File

@ -37,10 +37,10 @@
"@tiptap/extension-color": "^2.5.9",
"@tiptap/extension-highlight": "^2.6.6",
"@tiptap/extension-image": "^2.6.6",
"@tiptap/extension-mention": "^2.9.1",
"@tiptap/extension-placeholder": "^2.6.2",
"@tiptap/extension-text-style": "^2.5.9",
"@tiptap/extension-underline": "^2.6.6",
"@tiptap/extension-mention": "^2.9.1",
"@tiptap/pm": "^2.5.9",
"@tiptap/react": "^2.5.9",
"@tiptap/starter-kit": "^2.5.9",
@ -55,7 +55,9 @@
"dompurify": "^3.1.6",
"emoji-picker-react": "^4.12.0",
"file-saver": "^2.0.5",
"html-to-text": "^9.0.5",
"jssha": "3.3.1",
"lit": "^3.2.1",
"lodash": "^4.17.21",
"mime": "^4.0.4",
"moment": "^2.30.1",
@ -78,11 +80,10 @@
"short-unique-id": "^5.2.0",
"slate": "^0.103.0",
"slate-react": "^0.109.0",
"tippy.js": "^6.3.7",
"tiptap-extension-resize-image": "^1.1.8",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0",
"html-to-text": "^9.0.5",
"tippy.js": "^6.3.7"
"vite-plugin-wasm": "^3.3.0"
},
"devDependencies": {
"@testing-library/dom": "^10.3.0",

View File

@ -77,7 +77,7 @@ display: flex;
border: 1px solid var(--50-white, rgba(255, 255, 255, 0.5));
justify-content: space-between;
align-items: center;
width: 140px;
width: auto;
height: 25px;
padding: 5px 15px 5px 15px;
gap: 5px;

View File

@ -43,10 +43,13 @@ import Success from "./assets/svgs/Success.svg";
import Info from "./assets/svgs/Info.svg";
import CloseIcon from "@mui/icons-material/Close";
import { FilePicker } from '@capawesome/capacitor-file-picker';
import './utils/seedPhrase/RandomSentenceGenerator';
import {
createAccount,
generateRandomSentence,
saveFileToDisk,
saveSeedPhraseToDisk,
} from "./utils/generateWallet/generateWallet";
import { kdf } from "./deps/kdf";
import { generateSaveWalletData } from "./utils/generateWallet/storeWallet";
@ -116,6 +119,7 @@ import { NotAuthenticated, manifestData } from "./ExtStates/NotAuthenticated";
import { openIndexedDB, showSaveFilePicker } from "./components/Apps/useQortalMessageListener";
import { fileToBase64 } from "./utils/fileReading";
import { handleGetFileFromIndexedDB } from "./utils/indexedDB";
import { Wallets } from "./Wallets";
type extStates =
@ -132,7 +136,8 @@ type extStates =
| "wallet-dropped"
| "web-app-request-buy-order"
| "buy-order-submitted"
| "group";
| "group"
| "wallets";
interface MyContextInterface {
txList: any[];
@ -436,7 +441,19 @@ function App() {
const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom);
const { toggleFullScreen } = useAppFullScreen(setFullScreen);
const generatorRef = useRef(null)
const exportSeedphrase = async ()=> {
try {
const seedPhrase = generatorRef.current.parsedString
saveSeedPhraseToDisk(seedPhrase)
await showInfo({
message: `Your seed phrase was saved to INTERNAL storage, in the document folder. Keep that file secure.`,
})
} catch (error) {
}
}
useEffect(() => {
// Attach a global event listener for double-click
const handleDoubleClick = () => {
@ -992,7 +1009,7 @@ function App() {
res();
}, 250);
});
const res = await createAccount();
const res = await createAccount(generatorRef.current.parsedString);
const wallet = await res.generateSaveWalletData(
walletToBeDownloadedPassword,
crypto.kdfThreads,
@ -2282,7 +2299,35 @@ function App() {
</CustomButton>
</>
)}
{rawWallet && extState === "wallet-dropped" && (
{extState === "wallets" && (
<>
<Spacer height="22px" />
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "flex-start",
paddingLeft: "22px",
boxSizing: "border-box",
}}
>
<img
style={{
cursor: "pointer",
}}
onClick={() => {
setRawWallet(null);
setExtstate("not-authenticated");
logoutFunc();
}}
src={Return}
/>
</Box>
<Wallets setRawWallet={setRawWallet} setExtState={setExtstate} rawWallet={rawWallet} />
</>
)}
{rawWallet && extState === "wallet-dropped" && (
<>
<Spacer height="22px" />
<Box
@ -2300,8 +2345,8 @@ function App() {
}}
onClick={() => {
setRawWallet(null);
setExtstate("not-authenticated");
logoutFunc()
setExtstate("wallets");
logoutFunc();
}}
src={Return}
/>
@ -2322,9 +2367,11 @@ function App() {
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
alignItems: "center",
}}
>
<Typography>{rawWallet?.name ? rawWallet?.name : rawWallet?.address0}</Typography>
<Spacer height="10px" />
<TextP
sx={{
textAlign: "start",
@ -2492,6 +2539,40 @@ await showInfo({
Set up your Qortal account
</TextP>
<Spacer height="14px" />
<Box sx={{
display: 'flex',
maxWidth: '100%',
justifyContent: 'center',
padding: '10px'
}}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
maxWidth: '400px',
alignItems: 'center',
gap: '10px'
}}>
<Typography sx={{
fontSize: '14px'
}}>Your seedphrase</Typography>
<Typography sx={{
fontSize: '12px'
}}>Only shown once! Please copy and keep safe!</Typography>
<random-sentence-generator
ref={generatorRef}
template="adverb verb noun adjective noun adverb verb noun adjective noun adjective verbed adjective noun"
></random-sentence-generator>
</Box>
</Box>
<CustomButton sx={{
padding: '7px',
fontSize: '12px'
}} onClick={exportSeedphrase}>
Export Seedphrase
</CustomButton>
<Spacer height="14px" />
<CustomLabel htmlFor="standard-adornment-password">
Wallet Password
</CustomLabel>

View File

@ -261,15 +261,12 @@ export const NotAuthenticated = ({
display: "flex",
gap: "10px",
alignItems: "center",
marginLeft: "28px",
}}
>
<CustomButton onClick={handleFilePick}>
Authenticate
<CustomButton onClick={()=> setExtstate('wallets')}>
Wallets
</CustomButton>
<Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip>
</Box>
<Spacer height="6px" />
@ -277,8 +274,7 @@ export const NotAuthenticated = ({
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
marginLeft: "28px",
alignItems: "center"
}}
>
<CustomButton
@ -289,12 +285,7 @@ export const NotAuthenticated = ({
Create account
</CustomButton>
<img
src={Info}
style={{
visibility: "hidden",
}}
/>
</Box>
<Spacer height="15px" />

586
src/Wallets.tsx Normal file
View File

@ -0,0 +1,586 @@
import React, { useEffect, useRef, 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,
} from "@mui/material";
import { CustomButton } from "./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 { FilePicker } from '@capawesome/capacitor-file-picker';
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 [password, setPassword] = useState("");
const [isOpenSeedModal, setIsOpenSeedModal] = useState(false);
const [isLoadingEncryptSeed, setIsLoadingEncryptSeed] = useState(false);
const { isShow, onCancel, onOk, show } = useModal();
const handleFilePick = async () => {
try {
const resultPermission = await FilePicker.checkPermissions();
// Open the file picker to select a JSON file
const result = await FilePicker.pickFiles({
types: ['application/json'], // Restrict to JSON files
multiple: true, // Allow only one file
readData: true,
});
if (result.files.length > 0) {
let importedWallets: any = [];
for (const file of result.files) {
try {
const decodedData = atob(file.data); // `atob` decodes Base64 to a string
const parsedFile = JSON.parse(decodedData);
// Validate required fields
const requiredFields = [
"address0",
"salt",
"iv",
"version",
"encryptedSeed",
"mac",
"kdfThreads",
];
for (const field of requiredFields) {
if (!(field in parsedFile)) throw new Error(`${field} not found in JSON`);
}
// Set the state with parsed wallet data
importedWallets.push({ ...parsedFile, filename: file?.name });
} catch (error) {
console.error(error);
}
}
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]);
}
} else {
console.log("No file selected.");
}
} catch (error) {
console.error("Error picking JSON file:", error);
}
};
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 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 wallet.");
}
} catch (error) {
setSeedError(error?.message || "Could not create wallet.");
} 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>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{wallets?.length === 0 || !wallets ? (
<>
<Typography>No wallets saved</Typography>
</>
) : (
<>
<Typography>Your saved wallets</Typography>
</>
)}
</Box>
<Spacer height="10px" />
{rawWallet && (
<Box>
<Typography>Selected Wallet:</Typography>
{rawWallet?.name && <Typography>{rawWallet.name}</Typography>}
{rawWallet?.address0 && (
<Typography>{rawWallet?.address0}</Typography>
)}
</Box>
)}
{wallets?.length > 0 && (
<List
sx={{
width: "100%",
maxWidth: "500px",
bgcolor: "background.paper",
maxHeight: "60vh",
overflow: "auto",
}}
>
{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",
flexDirection: wallets?.length === 0 ? "column" : "row",
}}
>
<CustomButton
onClick={handleSetSeedValue}
sx={{
padding: "10px",
}}
>
Add seed-phrase
</CustomButton>
<CustomButton
sx={{
padding: "10px",
}}
onClick={handleFilePick}
>
Add wallets
</CustomButton>
</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>
<Input
placeholder="Seed-phrase"
value={seedValue}
onChange={(e) => setSeedValue(e.target.value)}
/>
<Spacer height="7px" />
<Label>Choose new password</Label>
<PasswordField
id="standard-adornment-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="off"
/>
</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);
useEffect(() => {
if (wallet?.name) {
setName(wallet.name);
}
if (wallet?.note) {
setNote(wallet.note);
}
}, [wallet]);
return (
<>
<ButtonBase
onClick={() => {
setSelectedWallet(wallet);
}}
sx={{
width: "100%",
}}
>
<ListItem
secondaryAction={
<IconButton
onClick={(e) => {
e.stopPropagation();
setIsEdit(true);
}}
edge="end"
aria-label="edit"
>
<EditIcon
sx={{
color: "white",
}}
/>
</IconButton>
}
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: "text.primary", display: "inline" }}
>
{wallet?.address0}
</Typography>
{wallet?.note}
</Box>
}
/>
</ListItem>
</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(--unread)",
"&:hover": {
backgroundColor: "var(--unread)",
},
"&:focus": {
backgroundColor: "var(--unread)",
},
}}
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>
)}
</>
);
};

View File

@ -782,6 +782,23 @@ export async function getSaveWallet() {
}
}
export async function getWallets() {
const res = await getData<any>("wallets").catch(() => null);
if (res) {
return res;
} else {
return null
}
}
export async function storeWallets(wallets) {
storeData("wallets", wallets)
.catch((error) => {
console.error(error)
});
}
export async function clearAllNotifications() {
try {

View File

@ -45,6 +45,8 @@ body {
.image-container {
position: relative;
height: 100px !important;
width: 100px !important;
}
.image-container img {
@ -56,6 +58,7 @@ body {
.image-container .hover-image {
opacity: 0;
}
.image-container:hover .hover-image {
@ -64,6 +67,12 @@ body {
.image-container:hover .base-image {
opacity: 0;
}
.image-container .base-image {
height: 100px !important;
width: auto !important;
}
::-webkit-scrollbar-track {

View File

@ -21,4 +21,14 @@ export const decryptStoredWallet = async (password, wallet) => {
}
const decryptedBytes = AES_CBC.decrypt(encryptedSeedBytes, encryptionKey, false, iv)
return decryptedBytes
}
export const decryptStoredWalletFromSeedPhrase = async (password) => {
console.log('p')
const threads = doInitWorkers(crypto.kdfThreads)
const salt = new Uint8Array(void 0)
const seed = await kdf(password, salt, threads)
return seed
}

View File

@ -73,8 +73,8 @@ export function generateRandomSentence(template = 'adverb verb noun adjective no
return parse(template);
}
export const createAccount = async()=> {
const generatedSeedPhrase = generateRandomSentence()
export const createAccount = async(generatedSeedPhrase)=> {
if(!generatedSeedPhrase) throw new Error('No generated seed-phrase')
const threads = doInitWorkers(crypto.kdfThreads)
const seed = await kdf(generatedSeedPhrase, void 0, threads)
@ -96,4 +96,17 @@ export const createAccount = async()=> {
encoding: Encoding.UTF8,
});
};
};
export const saveSeedPhraseToDisk = async (data) => {
const fileName = "qortal_seedphrase.txt"
await Filesystem.writeFile({
path: fileName,
data,
directory: Directory.Documents, // Save in the Documents folder
encoding: Encoding.UTF8,
});
}

View File

@ -0,0 +1,173 @@
// Author: irontiga <irontiga@gmail.com>
import { html, LitElement, css } from 'lit'
import * as WORDLISTS from './wordList'
class RandomSentenceGenerator extends LitElement {
static get properties() {
return {
template: { type: String, attribute: 'template' },
parsedString: { type: String },
fetchedWordlistCount: { type: Number, value: 0 },
capitalize: { type: Boolean },
partsOfSpeechMap: { type: Object },
templateEntropy: { type: Number, reflect: true, attribute: 'template-entropy' },
maxWordLength: { type: Number, attribute: 'max-word-length' }
}
}
constructor() {
super()
this.template = 'adjective noun verb adverb.'
this.maxWordLength = 0
this.parsedString = ''
this.fetchedWordlistCount = 0
this.capitalize = true
this.partsOfSpeechMap = {
'noun': 'nouns',
'adverb': 'adverbs',
'adv': 'adverbs',
'verb': 'verbs',
'interjection': 'interjections',
'adjective': 'adjectives',
'adj': 'adjectives',
'verbed': 'verbed'
}
this.partsOfSpeech = Object.keys(this.partsOfSpeechMap)
this._wordlists = WORDLISTS
}
static styles = css`
div {
text-align: center;
width: 90%;
background-color: #1f2023;
border-radius: 5px;
padding: 10px;
}
`;
render() {
return html`
<div>${this.parsedString}</div>
`
}
firstUpdated() {
// ...
}
updated(changedProperties) {
let regen = false
if (changedProperties.has('template')) {
regen = true
}
if (changedProperties.has('maxWordLength')) {
console.dir(this.maxWordLength)
if (this.maxWordLength) {
const wl = { ...this._wordlists }
for (const partOfSpeech in this._wordlists) {
if (Array.isArray(this._wordlists[partOfSpeech])) {
wl[partOfSpeech] = this._wordlists[partOfSpeech].filter(word => word.length <= this.maxWordLength)
}
}
this._wordlists = wl
}
regen = true
}
if (regen) this.generate()
}
_RNG(entropy) {
if (entropy > 1074) {
throw new Error('Javascript can not handle that much entropy!')
}
let randNum = 0
const crypto = window.crypto || window.msCrypto
if (crypto) {
const entropy256 = Math.ceil(entropy / 8)
let buffer = new Uint8Array(entropy256)
crypto.getRandomValues(buffer)
randNum = buffer.reduce((num, value) => {
return num * value
}, 1) / Math.pow(256, entropy256)
} else {
console.warn('Secure RNG not found. Using Math.random')
randNum = Math.random()
}
return randNum
}
setRNG(fn) {
this._RNG = fn
}
_captitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
getWord(partOfSpeech) {
const words = this._wordlists[this.partsOfSpeechMap[partOfSpeech]]
const requiredEntropy = Math.log(words.length) / Math.log(2)
const index = this._RNG(requiredEntropy) * words.length
return {
word: words[Math.round(index)],
entropy: words.length
}
}
generate() {
this.parsedString = this.parse(this.template)
}
parse(template) {
const split = template.split(/[\s]/g)
let entropy = 1
const final = split.map(word => {
const lower = word.toLowerCase()
this.partsOfSpeech.some(partOfSpeech => {
const partOfSpeechIndex = lower.indexOf(partOfSpeech) // Check it exists
const nextChar = word.charAt(partOfSpeech.length)
if (partOfSpeechIndex === 0 && !(nextChar && (nextChar.match(/[a-zA-Z]/g) != null))) {
const replacement = this.getWord(partOfSpeech)
word = replacement.word + word.slice(partOfSpeech.length) // Append the rest of the "word" (punctuation)
entropy = entropy * replacement.entropy
return true
}
})
return word
})
this.templateEntropy = Math.floor(Math.log(entropy) / Math.log(8))
return final.join(' ')
}
}
window.customElements.define('random-sentence-generator', RandomSentenceGenerator)
export default RandomSentenceGenerator

View File

@ -0,0 +1,40 @@
export const EXCEPTIONS = {
'are': 'were',
'eat': 'ate',
'go': 'went',
'have': 'had',
'inherit': 'inherited',
'is': 'was',
'run': 'ran',
'sit': 'sat',
'visit': 'visited'
}
export const getPastTense = (verb, exceptions = EXCEPTIONS) => {
if (exceptions[verb]) {
return exceptions[verb]
}
if ((/e$/i).test(verb)) {
return verb + 'd'
}
if ((/[aeiou]c$/i).test(verb)) {
return verb + 'ked'
}
// for american english only
if ((/el$/i).test(verb)) {
return verb + 'ed'
}
if ((/[aeio][aeiou][dlmnprst]$/).test(verb)) {
return verb + 'ed'
}
if ((/[aeiou][bdglmnprst]$/i).test(verb)) {
return verb.replace(/(.+[aeiou])([bdglmnprst])/, '$1$2$2ed')
}
return verb + 'ed'
}

File diff suppressed because one or more lines are too long