Skip to content

Commit 4bd6e86

Browse files
authored
feat: add OPFS backup layer (#1742)
- Closes #1743 - Closes FE-1231
1 parent 197d175 commit 4bd6e86

File tree

4 files changed

+197
-51
lines changed

4 files changed

+197
-51
lines changed

.changeset/rotten-singers-hear.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"fuels-wallet": patch
3+
---
4+
5+
feat: add OPFS backup

packages/app/src/systems/Account/services/account.ts

+132-46
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { Maybe } from '~/systems/Core/types';
1313
import { db } from '~/systems/Core/utils/database';
1414
import { getUniqueString } from '~/systems/Core/utils/string';
1515
import { getTestNoDexieDbData } from '../utils/getTestNoDexieDbData';
16+
import { readFromOPFS } from '~/systems/Core/utils/opfs';
1617

1718
export type AccountInputs = {
1819
addAccount: {
@@ -215,68 +216,105 @@ export class AccountService {
215216
allVaults,
216217
backupNetworks,
217218
allNetworks,
219+
opfsBackupData,
218220
] = await Promise.all([
219221
chromeStorage.accounts.getAll(),
220222
db.accounts.toArray(),
221223
chromeStorage.vaults.getAll(),
222224
db.vaults.toArray(),
223225
chromeStorage.networks.getAll(),
224226
db.networks.toArray(),
227+
readFromOPFS(),
225228
]);
226229

230+
const chromeStorageBackupData = {
231+
accounts: backupAccounts,
232+
vaults: backupVaults,
233+
networks: backupNetworks,
234+
};
235+
227236
// if there is no accounts, means the user lost it. try recovering it
228237
const needsAccRecovery =
229-
allAccounts?.length === 0 && backupAccounts?.length > 0;
238+
allAccounts?.length === 0 &&
239+
(chromeStorageBackupData.accounts?.length > 0 ||
240+
opfsBackupData?.accounts?.length > 0);
230241
const needsVaultRecovery =
231-
allVaults?.length === 0 && backupVaults?.length > 0;
242+
allVaults?.length === 0 &&
243+
(chromeStorageBackupData.vaults?.length > 0 ||
244+
opfsBackupData?.vaults?.length > 0);
232245
const needsNetworkRecovery =
233-
allNetworks?.length === 0 && backupNetworks?.length > 0;
246+
allNetworks?.length === 0 &&
247+
(chromeStorageBackupData.networks?.length > 0 ||
248+
opfsBackupData?.networks?.length > 0);
234249
const needsRecovery =
235250
needsAccRecovery || needsVaultRecovery || needsNetworkRecovery;
236251

237252
return {
238-
backupAccounts,
239-
backupVaults,
240-
backupNetworks,
241253
needsRecovery,
242254
needsAccRecovery,
243255
needsVaultRecovery,
244256
needsNetworkRecovery,
257+
chromeStorageBackupData,
258+
opfsBackupData,
245259
};
246260
}
247261

248262
static async recoverWallet() {
249-
const {
250-
backupAccounts,
251-
backupVaults,
252-
backupNetworks,
253-
needsRecovery,
254-
needsAccRecovery,
255-
needsVaultRecovery,
256-
needsNetworkRecovery,
257-
} = await AccountService.fetchRecoveryState();
263+
const { chromeStorageBackupData, needsRecovery, opfsBackupData } =
264+
await AccountService.fetchRecoveryState();
258265

259266
if (needsRecovery) {
260267
(async () => {
261268
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
262269
const dataToLog: any = {};
263270
try {
264-
dataToLog.backupAccounts = JSON.stringify(
265-
backupAccounts?.map((account) => account?.data?.address) || []
266-
);
267-
dataToLog.backupNetworks = JSON.stringify(backupNetworks || []);
271+
dataToLog.chromeStorageBackupData = {
272+
...chromeStorageBackupData,
273+
accounts:
274+
chromeStorageBackupData.accounts?.map(
275+
(account) => account?.data?.address
276+
) || [],
277+
vaults: chromeStorageBackupData.vaults?.length || 0,
278+
};
268279
// try getting data from indexedDB (outside of dexie) to check if it's also corrupted
269280
const testNoDexieDbData = await getTestNoDexieDbData();
270281
dataToLog.testNoDexieDbData = testNoDexieDbData;
271282
} catch (_) {}
283+
try {
284+
dataToLog.ofpsBackupupData = {
285+
...opfsBackupData,
286+
accounts:
287+
opfsBackupData.accounts?.map(
288+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
289+
(account: any) => account?.address
290+
) || [],
291+
vaults: opfsBackupData.vaults?.length || 0,
292+
};
293+
} catch (_) {}
272294

273-
Sentry.captureException(
274-
'Disaster on DB. Start recovering accounts / vaults / networks',
275-
{
276-
extra: dataToLog,
277-
tags: { manual: true },
278-
}
279-
);
295+
const hasOPFSBackup =
296+
!!opfsBackupData?.accounts?.length ||
297+
!!opfsBackupData?.vaults?.length ||
298+
!!opfsBackupData?.networks?.length;
299+
const hasChromeStorageBackup =
300+
!!chromeStorageBackupData.accounts?.length ||
301+
!!chromeStorageBackupData.vaults?.length ||
302+
!!chromeStorageBackupData.networks?.length;
303+
let sentryMsg = 'DB is cleaned. ';
304+
if (!hasOPFSBackup && !hasChromeStorageBackup) {
305+
sentryMsg += 'No backup found. ';
306+
}
307+
if (hasOPFSBackup) {
308+
sentryMsg += 'OPFS backup is found. Recovering...';
309+
}
310+
if (hasChromeStorageBackup) {
311+
sentryMsg += 'Chrome Storage backup is found. Recovering...';
312+
}
313+
314+
Sentry.captureException(sentryMsg, {
315+
extra: dataToLog,
316+
tags: { manual: true },
317+
});
280318
})();
281319

282320
await db.transaction(
@@ -285,36 +323,84 @@ export class AccountService {
285323
db.vaults,
286324
db.networks,
287325
async () => {
288-
if (needsAccRecovery) {
289-
let isCurrentFlag = true;
290-
console.log('recovering accounts', backupAccounts);
291-
for (const account of backupAccounts) {
326+
console.log('opfsBackupData', opfsBackupData);
327+
console.log('chromeStorageBackupData', chromeStorageBackupData);
328+
// accounts recovery
329+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
330+
async function recoverAccounts(accounts: any) {
331+
await db.accounts.clear();
332+
for (const account of accounts) {
292333
// in case of recovery, the first account will be the current
293-
if (account.key && account.data.address) {
294-
await db.accounts.add({
295-
...account.data,
296-
isCurrent: isCurrentFlag,
297-
});
298-
isCurrentFlag = false;
334+
if (account.address) {
335+
await db.accounts.add(account);
299336
}
300337
}
301338
}
302-
if (needsVaultRecovery) {
303-
console.log('recovering vaults', backupVaults);
304-
for (const vault of backupVaults) {
305-
if (vault.key && vault.data) {
306-
await db.vaults.add(vault.data);
339+
// vaults recovery
340+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
341+
async function recoverVaults(vaults: any) {
342+
await db.vaults.clear();
343+
for (const vault of vaults) {
344+
if (vault.key) {
345+
await db.vaults.add(vault);
307346
}
308347
}
309348
}
310-
if (needsNetworkRecovery) {
311-
console.log('recovering networks', backupNetworks);
312-
for (const network of backupNetworks) {
313-
if (network.key && network.data.id) {
314-
await db.networks.add(network.data);
349+
// networks recovery
350+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
351+
async function recoverNetworks(networks: any) {
352+
await db.networks.clear();
353+
for (const network of networks) {
354+
if (network.url) {
355+
await db.networks.add(network);
315356
}
316357
}
317358
}
359+
360+
if (opfsBackupData?.accounts?.length) {
361+
console.log(
362+
'recovering accounts from OPFS',
363+
opfsBackupData.accounts
364+
);
365+
await recoverAccounts(opfsBackupData.accounts);
366+
} else if (chromeStorageBackupData.accounts?.length) {
367+
console.log(
368+
'recovering accounts from Chrome Storage',
369+
chromeStorageBackupData.accounts
370+
);
371+
await recoverAccounts(
372+
chromeStorageBackupData.accounts?.map((account) => account.data)
373+
);
374+
}
375+
376+
if (opfsBackupData?.vaults?.length) {
377+
console.log('recovering vaults from OPFS', opfsBackupData.vaults);
378+
await recoverVaults(opfsBackupData.vaults);
379+
} else if (chromeStorageBackupData.vaults?.length) {
380+
console.log(
381+
'recovering vaults from Chrome Storage',
382+
chromeStorageBackupData.vaults
383+
);
384+
await recoverVaults(
385+
chromeStorageBackupData.vaults?.map((vault) => vault.data)
386+
);
387+
}
388+
389+
if (opfsBackupData?.networks?.length) {
390+
console.log(
391+
'recovering networks from OPFS',
392+
opfsBackupData.networks
393+
);
394+
await recoverNetworks(opfsBackupData.networks);
395+
} else if (chromeStorageBackupData.networks?.length) {
396+
console.log(
397+
'recovering networks from Chrome Storage',
398+
chromeStorageBackupData.networks
399+
);
400+
await recoverNetworks(
401+
chromeStorageBackupData.networks?.map((network) => network.data)
402+
);
403+
}
318404
}
319405
);
320406
}

packages/app/src/systems/Core/utils/database.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { applyDbVersioning } from './databaseVersioning';
1717
import { createParallelDb } from '~/systems/Core/utils/databaseNoDexie';
1818
import { IS_LOGGED_KEY } from '~/config';
1919
import { Storage } from '~/systems/Core/utils/storage';
20+
import { saveToOPFS } from './opfs';
2021

2122
type FailureEvents = Extract<keyof DbEvents, 'close' | 'blocked'>;
2223
export type FuelCachedAsset = AssetData &
@@ -47,6 +48,18 @@ export class FuelDB extends Dexie {
4748
this.on('close', () => this.restart('close'));
4849
}
4950

51+
async syncDbToOPFS() {
52+
const accounts = await this.accounts.toArray();
53+
const vaults = await this.vaults.toArray();
54+
const networks = await this.networks.toArray();
55+
const backupData = {
56+
accounts,
57+
vaults,
58+
networks,
59+
};
60+
await saveToOPFS(backupData);
61+
}
62+
5063
async syncDbToChromeStorage() {
5164
const accounts = await this.accounts.toArray();
5265
const vaults = await this.vaults.toArray();
@@ -55,23 +68,24 @@ export class FuelDB extends Dexie {
5568
// @TODO: this is a temporary solution to avoid the storage accounts of being wrong and
5669
// users losing funds in case of no backup
5770
// if has account, save to chrome storage
58-
if (accounts.length) {
71+
if (accounts.length && vaults.length && networks.length) {
72+
console.log('saving data to chrome storage', {
73+
accounts,
74+
vaults,
75+
networks,
76+
});
5977
for (const account of accounts) {
6078
await chromeStorage.accounts.set({
6179
key: account.address,
6280
data: account,
6381
});
6482
}
65-
}
66-
if (vaults.length) {
6783
for (const vault of vaults) {
6884
await chromeStorage.vaults.set({
6985
key: vault.key,
7086
data: vault,
7187
});
7288
}
73-
}
74-
if (networks.length) {
7589
for (const network of networks) {
7690
await chromeStorage.networks.set({
7791
key: network.id || '',
@@ -93,6 +107,10 @@ export class FuelDB extends Dexie {
93107
(() => this.syncDbToChromeStorage())();
94108
} catch (_) {}
95109

110+
try {
111+
(() => this.syncDbToOPFS())();
112+
} catch (_) {}
113+
96114
try {
97115
(async () => {
98116
const accounts = await this.accounts.toArray();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
async function initOPFS() {
2+
const root = await navigator?.storage?.getDirectory();
3+
return root;
4+
}
5+
6+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
7+
export async function saveToOPFS(data: any) {
8+
if (
9+
!data.accounts?.length ||
10+
!data.vaults?.length ||
11+
!data.networks?.length
12+
) {
13+
return;
14+
}
15+
16+
const root = await initOPFS();
17+
if (!root) return;
18+
console.log('saving data to opfs', data);
19+
const fileHandle = await root.getFileHandle('backup.json', { create: true });
20+
const writable = await fileHandle.createWritable();
21+
await writable.write(JSON.stringify(data));
22+
await writable.close();
23+
}
24+
25+
export async function readFromOPFS() {
26+
const root = await initOPFS();
27+
if (!root) return;
28+
try {
29+
const fileHandle = await root.getFileHandle('backup.json');
30+
const file = await fileHandle.getFile();
31+
const text = await file.text();
32+
return JSON.parse(text);
33+
} catch (_) {
34+
// Create empty backup file if it doesn't exist
35+
return {};
36+
}
37+
}

0 commit comments

Comments
 (0)