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 "./security-restrictions";
import { restoreOrCreateWindow } from "@src/mainWindow"; import { restoreOrCreateWindow } from "@src/mainWindow";
import { registerStoreHandlers } from "@src/services/store"; import { registerStoreHandlers } from "@src/services/store";
import { registerHavenoHandlers } from "./services/haveno";
/** /**
* Prevent multiple instances * Prevent multiple instances
@ -61,6 +62,11 @@ app
.then(registerStoreHandlers) .then(registerStoreHandlers)
.catch((e) => console.error("Failed to register store handlers:", e)); .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 * 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. // limitations under the License.
// ============================================================================= // =============================================================================
import { AccountLayout } from "@templates/AccountLayout"; export interface DownloadBackupInput {
bytes: ArrayBuffer;
export function Backup() {
return (
<AccountLayout>
<h1>Account Backup</h1>
</AccountLayout>
);
} }

View File

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

View File

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

View File

@ -20,6 +20,7 @@ interface Exposed {
readonly nodeCrypto: Readonly<typeof import("./src/nodeCrypto").nodeCrypto>; readonly nodeCrypto: Readonly<typeof import("./src/nodeCrypto").nodeCrypto>;
readonly versions: Readonly<typeof import("./src/versions").versions>; readonly versions: Readonly<typeof import("./src/versions").versions>;
readonly electronStore: Readonly<typeof import("./src/store").store>; 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 // 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 "./nodeCrypto";
import "./versions"; import "./versions";
import "./store"; import "./store";
import "./haveno";

View File

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

View File

@ -53,4 +53,12 @@ export enum LangKeys {
AccountWalletTitle = "account.wallet.title", AccountWalletTitle = "account.wallet.title",
AccountWalletDesc = "account.wallet.desc", AccountWalletDesc = "account.wallet.desc",
AccountWalletPassword = "account.wallet.field.password", 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]: [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.", "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.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; export default LangPackEN;

View File

@ -63,6 +63,21 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.AccountWalletDesc]: [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.", "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.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; 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 "./AddPaymentAccount";
export * from "./PaymentMethods"; export * from "./PaymentMethods";
export * from "./Backup";
export * from "./Settings"; export * from "./Settings";
export * from "./PaymentAccounts"; export * from "./PaymentAccounts";
export * from "./Security"; export * from "./Security";
export * from "./Wallet"; export * from "./Wallet";
export * from "./AccountBackup";