feat: account backup

---

Co-authored-by: @schowdhuri 
Reviewed-by: @schowdhuri
This commit is contained in:
Ahmed Bouhuolia 2022-05-26 21:04:14 +02:00 committed by GitHub
parent ad493f5147
commit 500be9a7fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 443 additions and 11 deletions

View File

@ -18,6 +18,7 @@ import { app } from "electron";
import "./security-restrictions";
import { restoreOrCreateWindow } from "@src/mainWindow";
import { registerStoreHandlers } from "@src/services/store";
import { registerHavenoHandlers } from "./services/haveno";
/**
* Prevent multiple instances
@ -61,6 +62,11 @@ app
.then(registerStoreHandlers)
.catch((e) => console.error("Failed to register store handlers:", e));
app
.whenReady()
.then(registerHavenoHandlers)
.catch((e) => console.error("Failed to register haveno handlers:", e));
/**
* Install devtools in development mode only
*/

View File

@ -0,0 +1,59 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import fsPromises from "fs/promises";
import { ipcMain, dialog } from "electron";
import type { DownloadBackupInput } from "@src/types";
import { IpcChannels } from "@src/types";
export function registerHavenoHandlers() {
ipcMain.handle(
IpcChannels.DownloadBackup,
async (_, data: DownloadBackupInput) => {
const file = await dialog.showSaveDialog({
defaultPath: "haveno-backup",
filters: [
{
extensions: ["zip"],
name: "*",
},
],
properties: ["createDirectory", "dontAddToRecent"],
});
if (!file?.filePath) {
return 0;
}
await fsPromises.writeFile(file.filePath, new Uint8Array(data.bytes));
}
);
ipcMain.handle(IpcChannels.RestoreBackup, async (): Promise<Uint8Array> => {
const files = await dialog.showOpenDialog({
filters: [
{
extensions: ["zip"],
name: "*",
},
],
properties: ["openFile", "dontAddToRecent"],
});
if (!files?.filePaths[0]) {
return new Uint8Array();
}
const zipFile = files.filePaths[0];
return new Uint8Array(await fsPromises.readFile(zipFile));
});
}

View File

@ -14,12 +14,6 @@
// limitations under the License.
// =============================================================================
import { AccountLayout } from "@templates/AccountLayout";
export function Backup() {
return (
<AccountLayout>
<h1>Account Backup</h1>
</AccountLayout>
);
export interface DownloadBackupInput {
bytes: ArrayBuffer;
}

View File

@ -16,3 +16,4 @@
export * from "./ipc";
export * from "./store";
export * from "./haveno";

View File

@ -15,6 +15,7 @@
// =============================================================================
export enum IpcChannels {
// store
GetAccountInfo = "store:accountInfo",
SetPassword = "store:accountinfo.setPassword",
ChangePassword = "store:accountinfo.changePassword",
@ -23,5 +24,10 @@ export enum IpcChannels {
GetPreferences = "store:preferences",
SetMoneroNode = "store:preferences.setMoneroNode",
// haveno
DownloadBackup = "haveno:downloadBackup",
RestoreBackup = "haveno:restoreBackup",
// others
VerifyAuthToken = "verifyAuthToken",
}

View File

@ -20,6 +20,7 @@ interface Exposed {
readonly nodeCrypto: Readonly<typeof import("./src/nodeCrypto").nodeCrypto>;
readonly versions: Readonly<typeof import("./src/versions").versions>;
readonly electronStore: Readonly<typeof import("./src/store").store>;
readonly haveno: Readonly<typeof import("./src/haveno").store>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { ipcRenderer } from "electron";
import { exposeInMainWorld } from "./exposeInMainWorld";
import type { DownloadBackupInput } from "./types";
import { IpcChannels } from "./types";
// Export for types in contracts.d.ts
export const haveno = {
downloadBackup: async (data: DownloadBackupInput): Promise<void> =>
ipcRenderer.invoke(IpcChannels.DownloadBackup, data),
getBackupData: async (): Promise<Uint8Array> =>
ipcRenderer.invoke(IpcChannels.RestoreBackup),
};
exposeInMainWorld("haveno", haveno);

View File

@ -21,3 +21,4 @@
import "./nodeCrypto";
import "./versions";
import "./store";
import "./haveno";

View File

@ -21,7 +21,7 @@ import { Home } from "@pages/Home";
import { Login } from "@pages/Login";
import { CreateAccount, Welcome } from "@pages/Onboarding";
import {
Backup,
AccountBackup,
Settings,
PaymentAccounts,
Security,
@ -57,7 +57,7 @@ export function AppRoutes() {
path={ROUTES.Backup}
element={
<ProtectedRoute>
<Backup />
<AccountBackup />
</ProtectedRoute>
}
/>

View File

@ -53,4 +53,12 @@ export enum LangKeys {
AccountWalletTitle = "account.wallet.title",
AccountWalletDesc = "account.wallet.desc",
AccountWalletPassword = "account.wallet.field.password",
AccountBackupDownloadTitle = "account.backup.download.title",
AccountBackupDownloadDesc = "account.backup.download.desc",
AccountBackupDownloadBtn = "account.backup.download.btn",
AccountBackupRestoreTitle = "account.backup.restore.title",
AccountBackupRestoreDesc = "account.backup.restore.desc",
AccountBackupRestoreBtn = "account.backup.restore.btn",
AccountBackupDownloadSuccessNotif = "account.backup.download.successNotification",
AccountBackupRestoreSuccessNotif = "account.backup.restore.successNotification",
}

View File

@ -62,6 +62,18 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountWalletDesc]:
"The Haveno wallet is permanently connected to your account. Solely saving your seed phrase is not enough to recover your account, you need to download a backup of your account, which you can download via the backup section.",
[LangKeys.AccountWalletPassword]: "Password",
[LangKeys.AccountBackupDownloadTitle]: "Download your backup file",
[LangKeys.AccountBackupDownloadDesc]:
"To be able to restore your Haveno account you need to create a backup file of your account. Keep it somewhere safe.",
[LangKeys.AccountBackupDownloadBtn]: "Download backup file",
[LangKeys.AccountBackupRestoreTitle]: "Restore an existing backup file",
[LangKeys.AccountBackupRestoreDesc]:
"When you restore an existing backup file of your Haveno account, you will lose the account youre using currently. Please use with caution.",
[LangKeys.AccountBackupRestoreBtn]: "Restore backup",
[LangKeys.AccountBackupDownloadSuccessNotif]:
"The backup has been downloaded successfully.",
[LangKeys.AccountBackupRestoreSuccessNotif]:
"The backup has been restored successfully.",
};
export default LangPackEN;

View File

@ -63,6 +63,21 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.AccountWalletDesc]:
"La billetera Haveno está permanentemente conectada a su cuenta. Solo guardar su frase inicial no es suficiente para recuperar su cuenta, necesita descargar una copia de seguridad de su cuenta, que puede descargar a través de la sección de copia de seguridad.",
[LangKeys.AccountWalletPassword]: "contraseña",
[LangKeys.AccountBackupDownloadTitle]:
"Descarga tu archivo de copia de seguridad",
[LangKeys.AccountBackupDownloadDesc]:
"Para poder restore your Haveno account you need to create a backup file of your account. Keep it somewhere safe.",
[LangKeys.AccountBackupDownloadBtn]:
"Descargar archivo de copia de seguridad",
[LangKeys.AccountBackupRestoreTitle]:
"Restaurar un archivo de copia de seguridad existente",
[LangKeys.AccountBackupRestoreDesc]:
"Cuando restaure un archivo de respaldo existente de su cuenta de Haveno, perderá la cuenta que está usando actualmente. Úselo con precaución.",
[LangKeys.AccountBackupRestoreBtn]: "Restaurar copia de seguridad",
[LangKeys.AccountBackupDownloadSuccessNotif]:
"La copia de seguridad se ha descargado correctamente.",
[LangKeys.AccountBackupRestoreSuccessNotif]:
"La copia de seguridad se ha restaurado correctamente.",
};
export default LangPackES;

View File

@ -0,0 +1,20 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export enum Notifications {
AccountRestoring = "AccountRestoring",
MoneroRestartAfterRestoring = "MoneroRestartAfterRestoring",
}

View File

@ -0,0 +1,61 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMutation } from "react-query";
import type { HavenoClient } from "haveno-ts";
import { useHavenoClient } from "./useHavenoClient";
export function useDownloadBackup() {
const client = useHavenoClient();
return useMutation(async () => {
const bytes = await getBackupData(client);
return window.haveno.downloadBackup({
bytes,
});
});
}
async function getBackupData(client: HavenoClient) {
const SIZE_INCREMENT = 4096;
let size = SIZE_INCREMENT;
let result = new ArrayBuffer(size);
let numBytes = 0;
const writableStream = new WritableStream({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
write(chunk: Array<any>) {
const len = chunk.length;
if (numBytes + len > size) {
while (numBytes + len > size) {
size += SIZE_INCREMENT;
}
const largerBuffer = new ArrayBuffer(size);
new Uint8Array(largerBuffer).set(new Uint8Array(result));
result = largerBuffer;
}
const view = new Uint8Array(result);
chunk.forEach((byte, index) => {
view[numBytes + index] = byte;
});
numBytes += len;
},
abort(err) {
console.log("Sink error:", err);
},
});
await client.backupAccount(writableStream.getWriter());
return result.slice(0, numBytes);
}

View File

@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { hideNotification, showNotification } from "@mantine/notifications";
import { useMutation } from "react-query";
import { useNavigate } from "react-router-dom";
import { MoneroNodeSettings } from "haveno-ts";
import { useHavenoClient } from "./useHavenoClient";
import { Notifications } from "@constants/notifications";
import { deleteSession } from "@utils/session";
import { ROUTES } from "@constants/routes";
export function useRestoreBackup() {
const client = useHavenoClient();
const navigate = useNavigate();
return useMutation(
async () => {
const bytes = await window.haveno.getBackupData();
if (!bytes.length) {
return;
}
showNotification({
id: Notifications.AccountRestoring,
title: "Account Restoring.",
message: "Account is restoring from the file.",
loading: true,
});
await client.deleteAccount();
await client.restoreAccount(bytes);
hideNotification(Notifications.AccountRestoring);
showNotification({
id: Notifications.MoneroRestartAfterRestoring,
title: "Monero restarting.",
message:
"The account has been restored, now the Monero node restarting.",
loading: true,
});
deleteSession();
navigate(ROUTES.Login);
if (await client.isMoneroNodeRunning()) {
await client.stopMoneroNode();
}
try {
await client.startMoneroNode(new MoneroNodeSettings());
} catch (ex) {
console.log(ex);
throw new Error("Failed to start the monero node");
}
hideNotification(Notifications.MoneroRestartAfterRestoring);
},
{
onError: (err: Error) => {
hideNotification(Notifications.AccountRestoring);
hideNotification(Notifications.MoneroRestartAfterRestoring);
showNotification({
color: "red",
message: err?.message ?? "Unable to restore backup",
title: "Something went wrong",
});
},
}
);
}

View File

@ -0,0 +1,135 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { FormattedMessage, useIntl } from "react-intl";
import { showNotification } from "@mantine/notifications";
import { Box, createStyles, Stack } from "@mantine/core";
import { Button } from "@atoms/Buttons";
import { BodyText, Heading } from "@atoms/Typography";
import { LangKeys } from "@constants/lang";
import { useDownloadBackup } from "@hooks/haveno/useDownloadBackup";
import { useRestoreBackup } from "@hooks/haveno/useRestoreBackup";
import { AccountLayout } from "@templates/AccountLayout";
export function AccountBackup() {
const { classes } = useStyles();
const intl = useIntl();
const { mutateAsync: downloadBackup, isLoading: isDownloading } =
useDownloadBackup();
const { mutateAsync: restoreBackup, isLoading: isRestoring } =
useRestoreBackup();
const handleDownloadBtnClick = () => {
downloadBackup().then(() => {
showNotification({
color: "green",
message: intl.formatMessage({
id: LangKeys.AccountBackupDownloadSuccessNotif,
defaultMessage: "The backup downloaded successfully.",
}),
});
});
};
const handleRestoreBtnClick = () => {
restoreBackup().then(() => {
showNotification({
color: "green",
message: intl.formatMessage({
id: LangKeys.AccountBackupRestoreSuccessNotif,
defaultMessage: "The backup restored successfully.",
}),
});
});
};
return (
<AccountLayout>
<Stack spacing={40}>
<Box>
<Heading
className={classes.title}
order={3}
stringId={LangKeys.AccountBackupDownloadTitle}
>
Download your backup file
</Heading>
<BodyText
stringId={LangKeys.AccountBackupDownloadDesc}
className={classes.desc}
>
To be able to restore your Haveno account you need to create a
backup file of your account. Keep it somewhere safe.
</BodyText>
<Button
disabled={isRestoring}
loading={isDownloading}
loaderPosition="right"
onClick={handleDownloadBtnClick}
>
<FormattedMessage
id={LangKeys.AccountBackupDownloadBtn}
defaultMessage="Download backup file"
/>
</Button>
</Box>
<Box>
<Heading
order={3}
stringId={LangKeys.AccountBackupRestoreTitle}
className={classes.title}
>
Restore an existing backup file
</Heading>
<BodyText
stringId={LangKeys.AccountBackupRestoreDesc}
className={classes.desc}
>
When you restore an existing backup file of your Haveno account, you
will lose the account youre using currently. Please use with
caution.
</BodyText>
<Button
disabled={true}
loading={isRestoring}
loaderPosition="right"
flavor="neutral"
onClick={handleRestoreBtnClick}
>
<FormattedMessage
id={LangKeys.AccountBackupRestoreBtn}
defaultMessage="Restore backup"
/>
</Button>
</Box>
</Stack>
</AccountLayout>
);
}
const useStyles = createStyles((theme) => ({
title: {
marginBottom: theme.spacing.md,
},
desc: {
marginBottom: theme.spacing.lg,
},
}));

View File

@ -16,8 +16,8 @@
export * from "./AddPaymentAccount";
export * from "./PaymentMethods";
export * from "./Backup";
export * from "./Settings";
export * from "./PaymentAccounts";
export * from "./Security";
export * from "./Wallet";
export * from "./AccountBackup";