From 4bd6e86b370d17692da72ff91c2b28cb64a21c2c Mon Sep 17 00:00:00 2001 From: Luiz Gomes <8636507+LuizAsFight@users.noreply.github.com> Date: Sat, 28 Dec 2024 15:33:35 -0300 Subject: [PATCH] feat: add OPFS backup layer (#1742) - Closes #1743 - Closes FE-1231 --- .changeset/rotten-singers-hear.md | 5 + .../src/systems/Account/services/account.ts | 178 +++++++++++++----- .../app/src/systems/Core/utils/database.ts | 28 ++- packages/app/src/systems/Core/utils/opfs.ts | 37 ++++ 4 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 .changeset/rotten-singers-hear.md create mode 100644 packages/app/src/systems/Core/utils/opfs.ts diff --git a/.changeset/rotten-singers-hear.md b/.changeset/rotten-singers-hear.md new file mode 100644 index 0000000000..5d3cc4f69e --- /dev/null +++ b/.changeset/rotten-singers-hear.md @@ -0,0 +1,5 @@ +--- +"fuels-wallet": patch +--- + +feat: add OPFS backup diff --git a/packages/app/src/systems/Account/services/account.ts b/packages/app/src/systems/Account/services/account.ts index f8cda01791..f908ae9182 100644 --- a/packages/app/src/systems/Account/services/account.ts +++ b/packages/app/src/systems/Account/services/account.ts @@ -13,6 +13,7 @@ import type { Maybe } from '~/systems/Core/types'; import { db } from '~/systems/Core/utils/database'; import { getUniqueString } from '~/systems/Core/utils/string'; import { getTestNoDexieDbData } from '../utils/getTestNoDexieDbData'; +import { readFromOPFS } from '~/systems/Core/utils/opfs'; export type AccountInputs = { addAccount: { @@ -215,6 +216,7 @@ export class AccountService { allVaults, backupNetworks, allNetworks, + opfsBackupData, ] = await Promise.all([ chromeStorage.accounts.getAll(), db.accounts.toArray(), @@ -222,61 +224,97 @@ export class AccountService { db.vaults.toArray(), chromeStorage.networks.getAll(), db.networks.toArray(), + readFromOPFS(), ]); + const chromeStorageBackupData = { + accounts: backupAccounts, + vaults: backupVaults, + networks: backupNetworks, + }; + // if there is no accounts, means the user lost it. try recovering it const needsAccRecovery = - allAccounts?.length === 0 && backupAccounts?.length > 0; + allAccounts?.length === 0 && + (chromeStorageBackupData.accounts?.length > 0 || + opfsBackupData?.accounts?.length > 0); const needsVaultRecovery = - allVaults?.length === 0 && backupVaults?.length > 0; + allVaults?.length === 0 && + (chromeStorageBackupData.vaults?.length > 0 || + opfsBackupData?.vaults?.length > 0); const needsNetworkRecovery = - allNetworks?.length === 0 && backupNetworks?.length > 0; + allNetworks?.length === 0 && + (chromeStorageBackupData.networks?.length > 0 || + opfsBackupData?.networks?.length > 0); const needsRecovery = needsAccRecovery || needsVaultRecovery || needsNetworkRecovery; return { - backupAccounts, - backupVaults, - backupNetworks, needsRecovery, needsAccRecovery, needsVaultRecovery, needsNetworkRecovery, + chromeStorageBackupData, + opfsBackupData, }; } static async recoverWallet() { - const { - backupAccounts, - backupVaults, - backupNetworks, - needsRecovery, - needsAccRecovery, - needsVaultRecovery, - needsNetworkRecovery, - } = await AccountService.fetchRecoveryState(); + const { chromeStorageBackupData, needsRecovery, opfsBackupData } = + await AccountService.fetchRecoveryState(); if (needsRecovery) { (async () => { // biome-ignore lint/suspicious/noExplicitAny: const dataToLog: any = {}; try { - dataToLog.backupAccounts = JSON.stringify( - backupAccounts?.map((account) => account?.data?.address) || [] - ); - dataToLog.backupNetworks = JSON.stringify(backupNetworks || []); + dataToLog.chromeStorageBackupData = { + ...chromeStorageBackupData, + accounts: + chromeStorageBackupData.accounts?.map( + (account) => account?.data?.address + ) || [], + vaults: chromeStorageBackupData.vaults?.length || 0, + }; // try getting data from indexedDB (outside of dexie) to check if it's also corrupted const testNoDexieDbData = await getTestNoDexieDbData(); dataToLog.testNoDexieDbData = testNoDexieDbData; } catch (_) {} + try { + dataToLog.ofpsBackupupData = { + ...opfsBackupData, + accounts: + opfsBackupData.accounts?.map( + // biome-ignore lint/suspicious/noExplicitAny: + (account: any) => account?.address + ) || [], + vaults: opfsBackupData.vaults?.length || 0, + }; + } catch (_) {} - Sentry.captureException( - 'Disaster on DB. Start recovering accounts / vaults / networks', - { - extra: dataToLog, - tags: { manual: true }, - } - ); + const hasOPFSBackup = + !!opfsBackupData?.accounts?.length || + !!opfsBackupData?.vaults?.length || + !!opfsBackupData?.networks?.length; + const hasChromeStorageBackup = + !!chromeStorageBackupData.accounts?.length || + !!chromeStorageBackupData.vaults?.length || + !!chromeStorageBackupData.networks?.length; + let sentryMsg = 'DB is cleaned. '; + if (!hasOPFSBackup && !hasChromeStorageBackup) { + sentryMsg += 'No backup found. '; + } + if (hasOPFSBackup) { + sentryMsg += 'OPFS backup is found. Recovering...'; + } + if (hasChromeStorageBackup) { + sentryMsg += 'Chrome Storage backup is found. Recovering...'; + } + + Sentry.captureException(sentryMsg, { + extra: dataToLog, + tags: { manual: true }, + }); })(); await db.transaction( @@ -285,36 +323,84 @@ export class AccountService { db.vaults, db.networks, async () => { - if (needsAccRecovery) { - let isCurrentFlag = true; - console.log('recovering accounts', backupAccounts); - for (const account of backupAccounts) { + console.log('opfsBackupData', opfsBackupData); + console.log('chromeStorageBackupData', chromeStorageBackupData); + // accounts recovery + // biome-ignore lint/suspicious/noExplicitAny: + async function recoverAccounts(accounts: any) { + await db.accounts.clear(); + for (const account of accounts) { // in case of recovery, the first account will be the current - if (account.key && account.data.address) { - await db.accounts.add({ - ...account.data, - isCurrent: isCurrentFlag, - }); - isCurrentFlag = false; + if (account.address) { + await db.accounts.add(account); } } } - if (needsVaultRecovery) { - console.log('recovering vaults', backupVaults); - for (const vault of backupVaults) { - if (vault.key && vault.data) { - await db.vaults.add(vault.data); + // vaults recovery + // biome-ignore lint/suspicious/noExplicitAny: + async function recoverVaults(vaults: any) { + await db.vaults.clear(); + for (const vault of vaults) { + if (vault.key) { + await db.vaults.add(vault); } } } - if (needsNetworkRecovery) { - console.log('recovering networks', backupNetworks); - for (const network of backupNetworks) { - if (network.key && network.data.id) { - await db.networks.add(network.data); + // networks recovery + // biome-ignore lint/suspicious/noExplicitAny: + async function recoverNetworks(networks: any) { + await db.networks.clear(); + for (const network of networks) { + if (network.url) { + await db.networks.add(network); } } } + + if (opfsBackupData?.accounts?.length) { + console.log( + 'recovering accounts from OPFS', + opfsBackupData.accounts + ); + await recoverAccounts(opfsBackupData.accounts); + } else if (chromeStorageBackupData.accounts?.length) { + console.log( + 'recovering accounts from Chrome Storage', + chromeStorageBackupData.accounts + ); + await recoverAccounts( + chromeStorageBackupData.accounts?.map((account) => account.data) + ); + } + + if (opfsBackupData?.vaults?.length) { + console.log('recovering vaults from OPFS', opfsBackupData.vaults); + await recoverVaults(opfsBackupData.vaults); + } else if (chromeStorageBackupData.vaults?.length) { + console.log( + 'recovering vaults from Chrome Storage', + chromeStorageBackupData.vaults + ); + await recoverVaults( + chromeStorageBackupData.vaults?.map((vault) => vault.data) + ); + } + + if (opfsBackupData?.networks?.length) { + console.log( + 'recovering networks from OPFS', + opfsBackupData.networks + ); + await recoverNetworks(opfsBackupData.networks); + } else if (chromeStorageBackupData.networks?.length) { + console.log( + 'recovering networks from Chrome Storage', + chromeStorageBackupData.networks + ); + await recoverNetworks( + chromeStorageBackupData.networks?.map((network) => network.data) + ); + } } ); } diff --git a/packages/app/src/systems/Core/utils/database.ts b/packages/app/src/systems/Core/utils/database.ts index 9fb11e111d..2dbe4e384f 100644 --- a/packages/app/src/systems/Core/utils/database.ts +++ b/packages/app/src/systems/Core/utils/database.ts @@ -17,6 +17,7 @@ import { applyDbVersioning } from './databaseVersioning'; import { createParallelDb } from '~/systems/Core/utils/databaseNoDexie'; import { IS_LOGGED_KEY } from '~/config'; import { Storage } from '~/systems/Core/utils/storage'; +import { saveToOPFS } from './opfs'; type FailureEvents = Extract; export type FuelCachedAsset = AssetData & @@ -47,6 +48,18 @@ export class FuelDB extends Dexie { this.on('close', () => this.restart('close')); } + async syncDbToOPFS() { + const accounts = await this.accounts.toArray(); + const vaults = await this.vaults.toArray(); + const networks = await this.networks.toArray(); + const backupData = { + accounts, + vaults, + networks, + }; + await saveToOPFS(backupData); + } + async syncDbToChromeStorage() { const accounts = await this.accounts.toArray(); const vaults = await this.vaults.toArray(); @@ -55,23 +68,24 @@ export class FuelDB extends Dexie { // @TODO: this is a temporary solution to avoid the storage accounts of being wrong and // users losing funds in case of no backup // if has account, save to chrome storage - if (accounts.length) { + if (accounts.length && vaults.length && networks.length) { + console.log('saving data to chrome storage', { + accounts, + vaults, + networks, + }); for (const account of accounts) { await chromeStorage.accounts.set({ key: account.address, data: account, }); } - } - if (vaults.length) { for (const vault of vaults) { await chromeStorage.vaults.set({ key: vault.key, data: vault, }); } - } - if (networks.length) { for (const network of networks) { await chromeStorage.networks.set({ key: network.id || '', @@ -93,6 +107,10 @@ export class FuelDB extends Dexie { (() => this.syncDbToChromeStorage())(); } catch (_) {} + try { + (() => this.syncDbToOPFS())(); + } catch (_) {} + try { (async () => { const accounts = await this.accounts.toArray(); diff --git a/packages/app/src/systems/Core/utils/opfs.ts b/packages/app/src/systems/Core/utils/opfs.ts new file mode 100644 index 0000000000..8c73864fd2 --- /dev/null +++ b/packages/app/src/systems/Core/utils/opfs.ts @@ -0,0 +1,37 @@ +async function initOPFS() { + const root = await navigator?.storage?.getDirectory(); + return root; +} + +// biome-ignore lint/suspicious/noExplicitAny: +export async function saveToOPFS(data: any) { + if ( + !data.accounts?.length || + !data.vaults?.length || + !data.networks?.length + ) { + return; + } + + const root = await initOPFS(); + if (!root) return; + console.log('saving data to opfs', data); + const fileHandle = await root.getFileHandle('backup.json', { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(JSON.stringify(data)); + await writable.close(); +} + +export async function readFromOPFS() { + const root = await initOPFS(); + if (!root) return; + try { + const fileHandle = await root.getFileHandle('backup.json'); + const file = await fileHandle.getFile(); + const text = await file.text(); + return JSON.parse(text); + } catch (_) { + // Create empty backup file if it doesn't exist + return {}; + } +}