mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-04-27 13:27:52 +00:00
added seedphrase
This commit is contained in:
parent
c3eb1fd50a
commit
af631df00d
49
package-lock.json
generated
49
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
95
src/App.tsx
95
src/App.tsx
@ -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>
|
||||
|
@ -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
586
src/Wallets.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
});
|
||||
|
||||
}
|
173
src/utils/seedPhrase/RandomSentenceGenerator.ts
Normal file
173
src/utils/seedPhrase/RandomSentenceGenerator.ts
Normal 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
|
40
src/utils/seedPhrase/verb-past-tense.ts
Normal file
40
src/utils/seedPhrase/verb-past-tense.ts
Normal 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'
|
||||
}
|
32
src/utils/seedPhrase/wordList.ts
Normal file
32
src/utils/seedPhrase/wordList.ts
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user