diff --git a/public/locales/en.json b/public/locales/en.json index 3a0965d..073f807 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -579,7 +579,7 @@ }, "import": { - "description": "Paste the backup code (or import from a file below) and enter the corresponding master password.", + "description": "Paste the backup code (or import from a file below) and enter the corresponding master password. Backups from KristWeb v1 are also supported.", "masterPasswordPlaceholder": "Master password", "masterPasswordRequired": "Master password is required.", @@ -591,6 +591,8 @@ "fileErrorTitle": "Import error", "fileErrorNotText": "The imported file must be a text file.", + "overwriteCheckboxLabel": "Update existing wallet labels if there are conflicts", + "modalTitle": "Import backup", "modalButton": "Import", @@ -599,12 +601,6 @@ "detectedFormatKristWebV2": "KristWeb v2", "detectedFormatInvalid": "Invalid!", - "treeHeaderWallets": "Wallets", - "treeHeaderFriends": "Friends (Address Book)", - - "treeWallet": "Wallet {{id}}", - "treeFriend": "Friend {{id}}", - "decodeErrors": { "atob": "The backup could not be decoded as it is not valid base64!", "json": "The backup could not be decoded as it is not valid JSON!", @@ -622,6 +618,7 @@ "successSkipped": "A wallet with the same address ({{address}}) and settings already exists, so it was skipped.", "successUpdated": "A wallet with the same address ({{address}}) already exists. Its label was updated to \"{{label}}\"", "successSkippedNoOverwrite": "A wallet with the same address ({{address}}) already exists, and you chose not to overwrite the label, so it was skipped.", + "successImportSkipped": "A wallet with the same address ({{address}}) was already imported, so it was skipped.", "warningSyncNode": "This wallet had a custom sync node, which is not supported in KristWeb v2. The sync node was skipped.", "warningIcon": "This wallet had a custom icon, which is not supported in KristWeb v2. The icon was skipped.", @@ -640,6 +637,8 @@ "errorPasswordMissing": "This wallet is missing a password!", "errorPrivateKeyMissing": "This wallet is missing a private key!", "errorMasterKeyMissing": "This wallet is missing a master key!", + "errorPrivateKeyMismatch": "This wallet's password did not map to its stored private key!", + "errorMasterKeyMismatch": "This wallet's password did not map to its stored master key!", "errorLimitReached": "You reached the wallet limit. You currently cannot add any more wallets.", "errorUnknown": "An unknown error occurred. See console for details." }, @@ -665,7 +664,13 @@ "warnings_plural": "There were <1>{{count, number}} warning while importing your backup.", "errors": "There was <1>{{count, number}} error while importing your backup.", "errors_plural": "There were <1>{{count, number}} errors while importing your backup.", - "errorsAndWarnings": "There were <1>{{errors, number}} error(s) and <3>{{warnings, number}} warning(s) while importing your backup." + "errorsAndWarnings": "There were <1>{{errors, number}} error(s) and <3>{{warnings, number}} warning(s) while importing your backup.", + + "treeHeaderWallets": "Wallets", + "treeHeaderFriends": "Friends (Address Book)", + + "treeWallet": "Wallet {{id}}", + "treeFriend": "Friend {{id}}" } } } diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index a46058f..8adfb4f 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -207,7 +207,7 @@ wallet: WalletNew, password: string, save: boolean -): Promise { +): Promise { // Calculate the privatekey for the given wallet format const privatekey = await applyWalletFormat(wallet.format || "kristwallet", password, wallet.username); const address = await makeV2Address(addressPrefix, privatekey); @@ -239,6 +239,8 @@ store.dispatch(actions.addWallet(newWallet)); syncWallet(newWallet); + + return newWallet; } /** diff --git a/src/pages/backup/BackupResultsTree.less b/src/pages/backup/BackupResultsTree.less new file mode 100644 index 0000000..cbaa146 --- /dev/null +++ b/src/pages/backup/BackupResultsTree.less @@ -0,0 +1,20 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +@import (reference) "../../App.less"; + +.backup-results-tree { + .backup-result-icon { + margin-right: @padding-xs; + } + + // Map the different result type icons to their appropriate colours + .anticon.backup-result-success { color: @kw-green; } + .anticon.backup-result-warning { color: @kw-orange; } + .anticon.backup-result-error { color: @kw-red; } + + // Make the non-leaf nodes bold, so the tree is more readable + .ant-tree-treenode:not(.backup-results-tree-message) .ant-tree-title { + font-weight: 500; + } +} diff --git a/src/pages/backup/BackupResultsTree.tsx b/src/pages/backup/BackupResultsTree.tsx index 9b2cf78..338ac00 100644 --- a/src/pages/backup/BackupResultsTree.tsx +++ b/src/pages/backup/BackupResultsTree.tsx @@ -1,11 +1,20 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import React from "react"; +import React, { useMemo } from "react"; import { Tree, Typography } from "antd"; +import { DataNode } from "antd/lib/tree"; import { CheckCircleOutlined, WarningOutlined, ExclamationCircleOutlined } from "@ant-design/icons"; -import { BackupResults, ResultType } from "./backupResults"; +import { i18n } from "i18next"; +import { useTranslation, TFunction } from "react-i18next"; +import { translateError } from "@utils/i18n"; + +import { + BackupResults, ResultType, MessageType, TranslatedMessage +} from "./backupResults"; + +import "./BackupResultsTree.less"; const { Paragraph } = Typography; @@ -13,17 +22,116 @@ results: BackupResults; } +const CLEAN_ID_RE = /^(?:[Ww]allet\d*|[Ff]riend\d*)-/; + +/** Maps the different types of message results (success, warning, error) + * to icons. */ +function getMessageIcon(type: ResultType) { + switch (type) { + case "success": + return ; + case "warning": + return ; + case "error": + return ; + } +} + +/** Maps the different types of messages to a properly rendered ReactNode. */ +function getMessageTitle( + t: TFunction, i18n: i18n, + message?: MessageType, error?: Error +): React.ReactNode { + // If there was an error, translate it if possible and render it. + if (error) { + return translateError(t, error); + } + + // If there's no error, show the message instead + if (typeof message === "string") { + // If the message is a string, translate it if possible, otherwise, render + // it directly. + if (i18n.exists(message)) return t(message); + else return message; + } else if (message && typeof message === "object" && (message as TranslatedMessage).key) { + // If the message is a TranslatedMessage, translate it and substitute the + // arguments in + const msg = (message as TranslatedMessage); + return t(msg.key, msg.args); + } else if (message) { + // It's probably a ReactNode, render it directly + return message; + } + + // Shouldn't happen, but there was neither a message nor an error. + return null; +} + +/** Converts the backup results into a tree of messages, grouped by wallet + * and friend UUID. */ +function getTreeData( + t: TFunction, i18n: i18n, + results: BackupResults +): DataNode[] { + // Add the wallet messages data + const walletData: DataNode[] = []; + + for (const id in results.messages.wallets) { + // The IDs are the keys of the backup, which may begin with prefixes like + // "Wallet-"; remove those for cleanliness + const cleanID = id.replace(CLEAN_ID_RE, ""); + const messages = results.messages.wallets[id]; + const messageNodes: DataNode[] = []; + + for (let i = 0; i < messages.length; i++) { + const { type, message, error } = messages[i]; + const icon = getMessageIcon(type); + const title = getMessageTitle(t, i18n, message, error); + + messageNodes.push({ + key: `wallets-${cleanID}-${i}`, + title, + icon, + isLeaf: true, + className: "backup-results-tree-message" + }); + } + + walletData.push({ + key: `wallets-${cleanID}`, + title: t("import.results.treeWallet", { id: cleanID }), + children: messageNodes + }); + } + + // TODO: Add the friends data + + return [{ + key: "wallets", + title: t("import.results.treeHeaderWallets"), + children: walletData + }]; +} + export function BackupResultsTree({ results }: Props): JSX.Element { + const { t, i18n } = useTranslation(); + + const treeData = useMemo(() => + getTreeData(t, i18n, results), [t, i18n, results]); + return <> {/* Results summary */} {/* Results tree */} ; } diff --git a/src/pages/backup/ImportBackupModal.tsx b/src/pages/backup/ImportBackupModal.tsx index f3eba72..efb8eec 100644 --- a/src/pages/backup/ImportBackupModal.tsx +++ b/src/pages/backup/ImportBackupModal.tsx @@ -2,7 +2,7 @@ // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React, { useState, Dispatch, SetStateAction } from "react"; -import { Modal, Form, FormInstance, Input, Button, Typography, notification } from "antd"; +import { Modal, Form, FormInstance, Input, Checkbox, Button, Typography, notification } from "antd"; import { useTranslation } from "react-i18next"; import { translateError } from "@utils/i18n"; @@ -24,6 +24,7 @@ interface FormValues { masterPassword: string; code: string; + overwrite: boolean; } interface Props { @@ -102,7 +103,7 @@ const values = await form.validateFields(); - const { masterPassword, code } = values; + const { masterPassword, code, overwrite } = values; if (!masterPassword || !code) return; try { @@ -116,7 +117,7 @@ setMasterPasswordError(undefined); // Perform the import - const results = await backupImport(backup, masterPassword); + const results = await backupImport(backup, masterPassword, !overwrite); setResults(results); } catch (err) { if (err.message === "import.masterPasswordRequired" @@ -137,6 +138,11 @@ visible={visible} destroyOnClose + // Grow the modal when there are results. This not only helps make it look + // better, but also prevents the user from accidentally double clicking + // the 'Import' button and immediately closing the results. + width={results ? 768 : undefined} + onCancel={closeModal} // Handle showing just an 'OK' button on the results screen, or all three @@ -231,7 +237,8 @@ initialValues={{ masterPassword: "", - code: "" + code: "", + overwrite: true }} onValuesChange={onValuesChange} @@ -283,5 +290,16 @@ placeholder={t("import.textareaPlaceholder")} /> + + {/* Overwrite checkbox */} + + + {t("import.overwriteCheckboxLabel")} + + ; } diff --git a/src/pages/backup/backupImport.ts b/src/pages/backup/backupImport.ts new file mode 100644 index 0000000..0e7817c --- /dev/null +++ b/src/pages/backup/backupImport.ts @@ -0,0 +1,129 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { store } from "@app"; + +import { TranslatedError } from "@utils/i18n"; + +import { aesGcmDecrypt } from "@utils/crypto"; +import { decryptCryptoJS } from "@utils/CryptoJS"; + +import { Backup, BackupFormatType, isBackupKristWebV1 } from "./backupFormats"; +import { BackupResults } from "./backupResults"; +import { importV1Wallet } from "./backupImportV1"; + +import Debug from "debug"; +const debug = Debug("kristweb:backup-import"); + +/** + * Attempts to decrypt a given value using the appropriate function for the + * backup format, with the `masterPassword` as the password. Returns `false` if + * decryption failed for any reason. + */ +export async function backupDecryptValue( + format: BackupFormatType, + masterPassword: string, + value: string +): Promise { + try { + switch (format) { + // KristWeb v1 used Crypto.JS for its cryptography (SubtleCrypto was not + // yet widespread enough), which uses its own funky key derivation + // algorithm. Use our polyfill for it. + // For more info, see `utils/CryptoJS.ts`. + case BackupFormatType.KRISTWEB_V1: + return await decryptCryptoJS(value, masterPassword); + + // KristWeb v2 simply uses WebCrypto/SubtleCrypto. + // For more info, see `utils/crypto.ts`. + case BackupFormatType.KRISTWEB_V2: + return await aesGcmDecrypt(value, masterPassword); + } + } catch (err) { + debug("failed to decrypt backup value '%s'", value); + console.error(err); + return false; + } +} + +/** + * Attempts to decrypt a backup by verifying its salt and tester. If the + * decryption fails (due to an incorrect master password), it will throw an + * exception. + */ +export async function backupVerifyPassword( + backup: Backup, + masterPassword: string +): Promise { + // These were already verified to exist and be the correct type by + // the decodeBackup function. + const { salt, tester } = backup; + + if (!masterPassword) + throw new TranslatedError("import.masterPasswordRequired"); + + // Attempt to decrypt the tester with the given password. backupDecryptValue + // will return `false` if the decryption failed for any reason. + const testerDec = await backupDecryptValue(backup.type, masterPassword, tester); + + // Verify that the decrypted tester is equal to the salt, if not, the + // provided master password is incorrect. + if (testerDec === false || testerDec !== salt) + throw new TranslatedError("import.masterPasswordIncorrect"); +} + +/** + * Performs the backup import, logging any messages necessary. The process is + * as cautious as possible. + */ +export async function backupImport( + backup: Backup, + masterPassword: string, + noOverwrite: boolean +): Promise { + // It is assumed at this point that the backup was already successfully + // decoded, and the master password was verified to be correct. + debug("beginning import (format: %s)", backup.type, backup); + + // Fetch the current set of wallets from the Redux store, to ensure the limit + // isn't reached, and to handle duplication checking. + const existingWallets = store.getState().wallets.wallets; + // Fetch other useful state from the Redux store + const appSyncNode = store.getState().node.syncNode; + const addressPrefix = store.getState().node.currency.address_prefix; + + // The results instance to keep track of logged messages, etc. + const results = new BackupResults(); + + // Attempt to add the wallets + if (isBackupKristWebV1(backup)) { + // Import wallets from a KristWeb v1 backup + for (const uuid in backup.wallets) { + if (!uuid || !uuid.startsWith("Wallet-")) { + // Not a wallet + debug("skipping v1 wallet key %s", uuid); + continue; + } + + const rawWallet = backup.wallets[uuid]; + debug("importing v1 wallet uuid %s: %o", uuid, rawWallet); + + try { + await importV1Wallet( + existingWallets, appSyncNode, addressPrefix, + backup, masterPassword, noOverwrite, + uuid, rawWallet, + results + ); + } catch (err) { + debug("error importing v1 wallet", err); + results.addErrorMessage("wallets", uuid, undefined, err); + } + } + } else { + debug("WTF: unsupported backup format %s", backup.type); + } + + debug("import finished, final results:", results); + return results; +} diff --git a/src/pages/backup/backupImport.tsx b/src/pages/backup/backupImport.tsx deleted file mode 100644 index ebde259..0000000 --- a/src/pages/backup/backupImport.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2020-2021 Drew Lemmy -// This file is part of KristWeb 2 under GPL-3.0. -// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { TranslatedError } from "@utils/i18n"; - -import { aesGcmDecrypt } from "@utils/crypto"; -import { decryptCryptoJS } from "@utils/CryptoJS"; - -import { Backup, BackupFormatType, isBackupKristWebV1 } from "./backupFormats"; -import { BackupResults } from "./backupResults"; -import { importV1Wallet } from "./backupImportV1"; - -import Debug from "debug"; -const debug = Debug("kristweb:backup-import"); - -/** - * Attempts to decrypt a given value using the appropriate function for the - * backup format, with the `masterPassword` as the password. Returns `false` if - * decryption failed for any reason. - */ -export async function backupDecryptValue( - format: BackupFormatType, - masterPassword: string, - value: string -): Promise { - try { - switch (format) { - // KristWeb v1 used Crypto.JS for its cryptography (SubtleCrypto was not - // yet widespread enough), which uses its own funky key derivation - // algorithm. Use our polyfill for it. - // For more info, see `utils/CryptoJS.ts`. - case BackupFormatType.KRISTWEB_V1: - return await decryptCryptoJS(value, masterPassword); - - // KristWeb v2 simply uses WebCrypto/SubtleCrypto. - // For more info, see `utils/crypto.ts`. - case BackupFormatType.KRISTWEB_V2: - return await aesGcmDecrypt(value, masterPassword); - } - } catch (err) { - debug("failed to decrypt backup value '%s'", value); - console.error(err); - return false; - } -} - -/** - * Attempts to decrypt a backup by verifying its salt and tester. If the - * decryption fails (due to an incorrect master password), it will throw an - * exception. - */ -export async function backupVerifyPassword( - backup: Backup, - masterPassword: string -): Promise { - // These were already verified to exist and be the correct type by - // the decodeBackup function. - const { salt, tester } = backup; - - if (!masterPassword) - throw new TranslatedError("import.masterPasswordRequired"); - - // Attempt to decrypt the tester with the given password. backupDecryptValue - // will return `false` if the decryption failed for any reason. - const testerDec = await backupDecryptValue(backup.type, masterPassword, tester); - - // Verify that the decrypted tester is equal to the salt, if not, the - // provided master password is incorrect. - if (testerDec === false || testerDec !== salt) - throw new TranslatedError("import.masterPasswordIncorrect"); -} - -/** - * Performs the backup import, logging any messages necessary. The process is - * as cautious as possible. - */ -export async function backupImport( - backup: Backup, - masterPassword: string -): Promise { - // It is assumed at this point that the backup was already successfully - // decoded, and the master password was verified to be correct. - debug("beginning import (format: %s)", backup.type, backup); - - // The results instance to keep track of logged messages, etc. - const results = new BackupResults(); - - // Attempt to add the wallets - if (isBackupKristWebV1(backup)) { - // Import wallets from a KristWeb v1 backup - for (const uuid in backup.wallets) { - if (!uuid || !uuid.startsWith("Wallet-")) { - // Not a wallet - debug("skipping v1 wallet key %s", uuid); - continue; - } - - const rawWallet = backup.wallets[uuid]; - debug("importing v1 wallet uuid %s: %o", uuid, rawWallet); - - try { - await importV1Wallet(backup, masterPassword, uuid, rawWallet, results); - } catch (err) { - debug("error importing v1 wallet", err); - results.addErrorMessage("wallets", uuid, undefined, err); - } - } - } else { - debug("WTF: unsupported backup format %s", backup.type); - } - - debug("import finished, final results:", results); - return results; -} diff --git a/src/pages/backup/backupImportV1.ts b/src/pages/backup/backupImportV1.ts index b1155b5..c161e36 100644 --- a/src/pages/backup/backupImportV1.ts +++ b/src/pages/backup/backupImportV1.ts @@ -1,15 +1,17 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { store } from "@app"; - import { BackupKristWebV1, KristWebV1Wallet } from "./backupFormats"; import { BackupWalletError, BackupResults, MessageType } from "./backupResults"; import { backupDecryptValue } from "./backupImport"; +import { WalletMap } from "@reducers/WalletsReducer"; import { - WALLET_FORMATS, ADVANCED_FORMATS, WalletFormatName, formatNeedsUsername + WALLET_FORMATS, ADVANCED_FORMATS, WalletFormatName, formatNeedsUsername, + applyWalletFormat } from "@wallets/WalletFormat"; +import { ADDRESS_LIST_LIMIT } from "@wallets/Wallet"; +import { makeV2Address } from "@krist/AddressAlgo"; import { isPlainObject, memoize } from "lodash-es"; import to from "await-to-js"; @@ -28,10 +30,19 @@ * Imports a single wallet in the KristWeb v1 format. */ export async function importV1Wallet( + // Things regarding the app's existing state + existingWallets: WalletMap, + appSyncNode: string, + addressPrefix: string, + + // Things related to the backup backup: BackupKristWebV1, masterPassword: string, + noOverwrite: boolean, + uuid: string, rawWallet: string, // The encrypted wallet + results: BackupResults ): Promise { const { type } = backup; @@ -44,7 +55,11 @@ const importWarnings: MessageType[] = []; const importWarn = (msg: MessageType) => { debug("v1 wallet %s added import warning:", uuid, msg); - importWarnings.push(msg); + + // Prepend the i18n key if it was just a string + importWarnings.push(typeof msg === "string" + ? "import.walletMessages." + msg + : msg); }; /** Asserts that `val` is a truthy string */ @@ -102,9 +117,9 @@ // OPTIONAL PROPERTY VALIDATION // --------------------------------------------------------------------------- // Check if the wallet was using a custom sync node (ignoring http/https) - const appSyncNode = _cleanSyncNode(store.getState().node.syncNode); + const cleanAppSyncNode = _cleanSyncNode(appSyncNode); const { syncNode } = wallet; - if (syncNode && _cleanSyncNode(syncNode) !== appSyncNode) + if (syncNode && _cleanSyncNode(syncNode) !== cleanAppSyncNode) importWarn("warningSyncNode"); // Check if the wallet was using a custom icon @@ -118,4 +133,58 @@ // Check if the wallet is using an advanced (unsupported) format if (ADVANCED_FORMATS.includes(format)) importWarn({ key: "import.walletMessages.warningAdvancedFormat", args: { format }}); + + // --------------------------------------------------------------------------- + // WALLET IMPORT PREPARATION/VALIDATION + // --------------------------------------------------------------------------- + // Calculate the address in advance, to check for existing wallets + const privatekey = await applyWalletFormat(format || "kristwallet", password, username); + const address = await makeV2Address(addressPrefix, privatekey); + + // Check that our calculated privatekey is actually equal to the stored + // masterkey. In practice these should never be different. + if (privatekey !== masterkey) + throw new BackupWalletError("errorMasterKeyMismatch"); + + // Check if a wallet already exists, either in the Redux store, or our list of + // imported wallets during this backup import + const existingWallet = Object.values(existingWallets) + .find(w => w.address === address); + const existingImportWallet = results.importedWallets + .find(w => w.address === address); + + // Skip with no additional checks or updates if this wallet was already + // handled by this backup import + if (existingImportWallet) + return success({ key: "import.walletMessages.successImportSkipped", args: { address }}); + + // --------------------------------------------------------------------------- + // WALLET IMPORT + // --------------------------------------------------------------------------- + // Handle duplicate wallets + if (existingWallet) { + // The wallet already exists in the Redux store, so determine if we need to + // update its label (only if it was determined to be valid) + if (labelValid && existingWallet.label !== label!.trim()) { + if (noOverwrite) { + // The user didn't want to overwrite the wallet, so skip it + return success({ key: "import.walletMessages.successSkippedNoOverwrite", args: { address }}); + } else { + // TODO: Edit the wallet + } + } else { + return success({ key: "import.walletMessages.successSkipped", args: { address }}); + } + } + + // No existing wallet to update/skip, so now check if we can add it without + // going over the wallet limit + const currentWalletCount = + Object.keys(existingWallets).length + results.importedWallets.length; + if (currentWalletCount >= ADDRESS_LIST_LIMIT) + throw new BackupWalletError("errorLimitReached"); + + // Now that we're actually importing the wallet, push any warnings that may + // have been generated + importWarnings.forEach(warn); } diff --git a/src/pages/backup/backupResults.ts b/src/pages/backup/backupResults.ts index 4192e75..5f9028d 100644 --- a/src/pages/backup/backupResults.ts +++ b/src/pages/backup/backupResults.ts @@ -3,6 +3,10 @@ // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React from "react"; +import { TranslatedError } from "@utils/i18n"; + +import { Wallet } from "@wallets/Wallet"; + import Debug from "debug"; const debug = Debug("kristweb:backup-results"); @@ -17,14 +21,19 @@ export class BackupResults { /** Number of new wallets that were added as a result of this import. */ - private newWallets = 0; + public newWallets = 0; /** Number of wallets from the backup that were skipped (not imported). */ - private skippedWallets = 0; + public skippedWallets = 0; + + /** Array of wallets that were successfully imported, used to handle + * duplication checking (since the Redux state isn't guaranteed to be up to + * date). */ + public importedWallets: Wallet[] = []; /** For both wallets and friends, a map of wallet/friend UUIDs containing * all the messages (success, warning, error). */ - private messages: { + public messages: { wallets: Record; friends: Record; } = { @@ -58,16 +67,6 @@ public addErrorMessage(src: MessageSource, uuid: string, message?: MessageType, error?: Error): void { this.addMessage(src, uuid, { type: "error", message, error }); } - - /** Increments the new wallets counter. */ - public incrNewWallets(): void { - this.newWallets++; - } - - /** Increments the skipped wallets counter. */ - public incrSkippedWallets(): void { - this.skippedWallets++; - } } export interface BackupMessage { @@ -76,7 +75,7 @@ message?: MessageType; } -export class BackupError extends Error { +export class BackupError extends TranslatedError { constructor(message: string) { super(message); } }