diff --git a/package.json b/package.json index 3e8823d0..b346c40a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "lottie-web": "^5.7.8", "marina-provider": "^2.0.0", "moment": "^2.29.4", + "octokit": "^2.0.14", "path-browserify": "^1.0.1", "postcss": "^7.0.35", "qrcode.react": "^1.0.1", diff --git a/src/application/account.ts b/src/application/account.ts index 8f93ce87..6634f371 100644 --- a/src/application/account.ts +++ b/src/application/account.ts @@ -19,6 +19,7 @@ import { Contract } from '@ionio-lang/ionio'; import type { ZKPInterface } from 'liquidjs-lib/src/confidential'; import { h2b } from './utils'; import type { ChainSource } from '../domain/chainsource'; +import type { RestorationJSON, RestorationJSONDictionary } from '../domain/backup'; export const MainAccountLegacy = 'mainAccountLegacy'; export const MainAccount = 'mainAccount'; @@ -44,16 +45,6 @@ type AccountOpts = { type contractName = string; -export type RestorationJSON = { - accountName: string; - artifacts: Record; - pathToArguments: Record; -}; - -export type RestorationJSONDictionary = { - [network: string]: RestorationJSON[]; -}; - export function makeAccountXPub(seed: Buffer, basePath: string) { return bip32.fromSeed(seed).derivePath(basePath).neutered().toBase58(); } diff --git a/src/application/backup.ts b/src/application/backup.ts new file mode 100644 index 00000000..3f9174b4 --- /dev/null +++ b/src/application/backup.ts @@ -0,0 +1,69 @@ +import type { NetworkString } from 'marina-provider'; +import type { BackupConfig, BackupService } from '../domain/backup'; +import { BackupServiceType } from '../domain/backup'; +import type { AppRepository, WalletRepository } from '../domain/repository'; +import { BrowserSyncBackup } from '../port/browser-sync-backup-service'; +import { AccountFactory } from './account'; +import { GithubBackupService, isBackupGithubServiceConfig } from '../port/github-backup-service'; + +export function makeBackupService(config: BackupConfig): BackupService { + switch (config.type) { + case BackupServiceType.BROWSER_SYNC: + return new BrowserSyncBackup(); + case BackupServiceType.GITHUB: + if (!isBackupGithubServiceConfig(config)) + throw new Error('Invalid backup service configuration for Github'); + return new GithubBackupService(config); + default: + throw new Error('Invalid backup service configuration'); + } +} + +function isNetworkString(str: string): str is NetworkString { + return str === 'liquid' || str === 'testnet' || str === 'regtest'; +} + +export async function loadFromBackupServices( + appRepository: AppRepository, + walletRepository: WalletRepository, + backupServices: BackupService[] +) { + const backupData = await Promise.all(backupServices.map((service) => service.load())); + + const chainSourceLiquid = await appRepository.getChainSource('liquid'); + const chainSourceTestnet = await appRepository.getChainSource('testnet'); + const chainSourceRegtest = await appRepository.getChainSource('regtest'); + const chainSource = (network: string) => + network === 'liquid' + ? chainSourceLiquid + : network === 'testnet' + ? chainSourceTestnet + : chainSourceRegtest; + + try { + const accountFactory = await AccountFactory.create(walletRepository); + + for (const { ionioAccountsRestorationDictionary } of backupData) { + for (const [network, restorations] of Object.entries(ionioAccountsRestorationDictionary)) { + if (!isNetworkString(network)) continue; + const chain = chainSource(network); + if (!chain) continue; + + for (const restoration of restorations) { + try { + const account = await accountFactory.make(network, restoration.accountName); + await account.restoreFromJSON(chain, restoration); + } catch (e) { + console.error(e); + } + } + } + } + } finally { + await Promise.all([ + chainSourceLiquid?.close(), + chainSourceTestnet?.close(), + chainSourceRegtest?.close(), + ]).catch(console.error); + } +} diff --git a/src/background/background-script.ts b/src/background/background-script.ts index 708a5fe1..fa3a78ba 100644 --- a/src/background/background-script.ts +++ b/src/background/background-script.ts @@ -25,6 +25,7 @@ import { BlockHeadersAPI } from '../infrastructure/storage/blockheaders-reposito import type { ChainSource } from '../domain/chainsource'; import { WalletRepositoryUnblinder } from '../application/unblinder'; import { Transaction } from 'liquidjs-lib'; +import { BackupSyncer } from './backup-syncer'; // manifest v2 needs BrowserAction, v3 needs action const action = Browser.browserAction ?? Browser.action; @@ -42,7 +43,7 @@ const backgroundPort = getBackgroundPortImplementation(); const walletRepository = new WalletStorageAPI(); const appRepository = new AppStorageAPI(); -const assetRepository = new AssetStorageAPI(walletRepository); +const assetRepository = new AssetStorageAPI(); const taxiRepository = new TaxiStorageAPI(assetRepository, appRepository); const blockHeadersRepository = new BlockHeadersAPI(); @@ -59,6 +60,7 @@ const subscriberService = new SubscriberService( blockHeadersRepository ); const taxiService = new TaxiUpdater(taxiRepository, appRepository, assetRepository); +const backupSyncerService = new BackupSyncer(appRepository, walletRepository); let started = false; @@ -77,11 +79,17 @@ async function startBackgroundServices() { if (started) return; started = true; await walletRepository.unlockUtxos(); // unlock all utxos at startup - await Promise.allSettled([ + const results = await Promise.allSettled([ updaterService.start(), subscriberService.start(), Promise.resolve(taxiService.start()), + backupSyncerService.start(), ]); + results.forEach((result) => { + if (result.status === 'rejected') { + console.error(result.reason); + } + }); } async function restoreTask(restoreMessage: RestoreMessage): Promise { @@ -116,7 +124,17 @@ async function restoreTask(restoreMessage: RestoreMessage): Promise { async function stopBackgroundServices() { started = false; - await Promise.allSettled([updaterService.stop(), subscriberService.stop(), taxiService.stop()]); + const results = await Promise.allSettled([ + updaterService.stop(), + subscriberService.stop(), + taxiService.stop(), + backupSyncerService.stop(), + ]); + results.forEach((result) => { + if (result.status === 'rejected') { + console.error(result.reason); + } + }); } /** diff --git a/src/background/backup-syncer.ts b/src/background/backup-syncer.ts new file mode 100644 index 00000000..ef576137 --- /dev/null +++ b/src/background/backup-syncer.ts @@ -0,0 +1,95 @@ +import { AccountType, isIonioScriptDetails } from 'marina-provider'; +import Browser from 'webextension-polyfill'; +import { AccountFactory } from '../application/account'; +import { loadFromBackupServices, makeBackupService } from '../application/backup'; +import type { RestorationJSONDictionary } from '../domain/backup'; +import type { AppRepository, WalletRepository } from '../domain/repository'; + +export class BackupSyncer { + static ALARM = 'backup-syncer'; + + private closeFn: () => Promise = () => Promise.resolve(); + + constructor(private appRepository: AppRepository, private walletRepository: WalletRepository) {} + + private async loadBackupData() { + const backupConfigs = await this.appRepository.getBackupServiceConfigs(); + const backupServices = backupConfigs.map(makeBackupService); + await loadFromBackupServices(this.appRepository, this.walletRepository, backupServices); + } + + private async saveBackupData() { + const restoration: RestorationJSONDictionary = { + liquid: [], + testnet: [], + regtest: [], + }; + const allAccounts = await this.walletRepository.getAccountDetails(); + const ionioAccounts = Object.values(allAccounts).filter( + ({ type }) => type === AccountType.Ionio + ); + const factory = await AccountFactory.create(this.walletRepository); + + for (const details of ionioAccounts) { + for (const net of details.accountNetworks) { + const account = await factory.make(net, details.accountID); + const restorationJSON = await account.restorationJSON(); + restoration[net].push(restorationJSON); + } + } + + const backupConfigs = await this.appRepository.getBackupServiceConfigs(); + const backupServices = backupConfigs.map(makeBackupService); + const results = await Promise.allSettled([ + ...backupServices.map((service) => + service.save({ ionioAccountsRestorationDictionary: restoration }) + ), + ]); + + for (const result of results) { + if (result.status === 'rejected') { + console.error(result.reason); + } + } + } + + async start() { + this.closeFn = () => Promise.resolve(); + const closeFns: (() => void | Promise)[] = []; + + // set up onNewScript & onNetworkChanged callbacks triggering backup saves + closeFns.push( + this.walletRepository.onNewScript(async (_, scriptDetails) => { + if (isIonioScriptDetails(scriptDetails)) { + await this.saveBackupData(); + } + }) + ); + + closeFns.push( + this.appRepository.onNetworkChanged(async () => { + await this.saveBackupData(); + }) + ); + + // set up an alarm triggering backup loads + Browser.alarms.create(BackupSyncer.ALARM, { periodInMinutes: 10 }); + Browser.alarms.onAlarm.addListener(async (alarm: Browser.Alarms.Alarm) => { + if (alarm.name !== BackupSyncer.ALARM) return; + await this.loadBackupData(); + }); + closeFns.push(async () => { + await Browser.alarms.clear(BackupSyncer.ALARM); + }); + + this.closeFn = async () => { + await Promise.all(closeFns.map((fn) => Promise.resolve(fn()))); + }; + await this.loadBackupData(); // load backup data on start + } + + async stop() { + await this.saveBackupData(); // save backup data on stop + await this.closeFn(); + } +} diff --git a/src/content/marina/marinaBroker.ts b/src/content/marina/marinaBroker.ts index c22ab5c7..de164ebd 100644 --- a/src/content/marina/marinaBroker.ts +++ b/src/content/marina/marinaBroker.ts @@ -82,7 +82,7 @@ export default class MarinaBroker extends Broker { this.hostname = hostname; this.walletRepository = new WalletStorageAPI(); this.appRepository = new AppStorageAPI(); - this.assetRepository = new AssetStorageAPI(this.walletRepository); + this.assetRepository = new AssetStorageAPI(); this.taxiRepository = new TaxiStorageAPI(this.assetRepository, this.appRepository); this.popupsRepository = new PopupsStorageAPI(); } diff --git a/src/domain/backup.ts b/src/domain/backup.ts new file mode 100644 index 00000000..d3085426 --- /dev/null +++ b/src/domain/backup.ts @@ -0,0 +1,38 @@ +import type { Argument, Artifact } from '@ionio-lang/ionio'; + +type contractName = string; + +export type RestorationJSON = { + accountName: string; + artifacts: Record; + pathToArguments: Record; +}; + +export type RestorationJSONDictionary = { + [network: string]: RestorationJSON[]; +}; + +// attach a version number for later updates +export type BackupDataVersion = 0; + +export interface BackupData { + version: BackupDataVersion; + ionioAccountsRestorationDictionary: RestorationJSONDictionary; +} + +export interface BackupService { + save(data: Partial): Promise; + load(): Promise; + delete(): Promise; + initialize(): Promise; +} + +export enum BackupServiceType { + BROWSER_SYNC = 'browser-sync', + GITHUB = 'github', +} + +export interface BackupConfig { + ID: string; // Unique ID for the backup service + type: BackupServiceType; +} diff --git a/src/domain/repository.ts b/src/domain/repository.ts index 747b61ed..c1e81ddc 100644 --- a/src/domain/repository.ts +++ b/src/domain/repository.ts @@ -13,7 +13,6 @@ import type { UnblindingData, CoinSelection, TxDetails, UnblindedOutput } from ' import Browser from 'webextension-polyfill'; import type { Encrypted } from './encryption'; import { encrypt } from './encryption'; -import type { RestorationJSONDictionary } from '../application/account'; import { Account, MainAccount, @@ -24,6 +23,7 @@ import { import { mnemonicToSeed } from 'bip39'; import { SLIP77Factory } from 'slip77'; import type { BlockHeader, ChainSource } from './chainsource'; +import type { BackupConfig, RestorationJSONDictionary } from './backup'; export interface AppStatus { isMnemonicVerified: boolean; @@ -73,6 +73,10 @@ export interface AppRepository { onNetworkChanged: EventEmitter<[NetworkString]>; onIsAuthenticatedChanged: EventEmitter<[authenticated: boolean]>; + addBackupServiceConfig(...config: BackupConfig[]): Promise; + removeBackupServiceConfig(ID: BackupConfig['ID']): Promise; + getBackupServiceConfigs(): Promise; + /** loaders **/ restorerLoader: Loader; updaterLoader: Loader; @@ -182,6 +186,8 @@ export interface OnboardingRepository { setOnboardingPasswordAndMnemonic(password: string, mnemonic: string): Promise; setRestorationJSONDictionary(json: RestorationJSONDictionary): Promise; getRestorationJSONDictionary(): Promise; + setBackupServicesConfiguration(configs: BackupConfig[]): Promise; + getBackupServicesConfiguration(): Promise; setIsFromPopupFlow(mnemonicToBackup: string): Promise; flush(): Promise; // flush all data } diff --git a/src/extension/components/github-backup-form.tsx b/src/extension/components/github-backup-form.tsx new file mode 100644 index 00000000..26f4d0f5 --- /dev/null +++ b/src/extension/components/github-backup-form.tsx @@ -0,0 +1,51 @@ +import * as Yup from 'yup'; +import type { FormikProps } from 'formik'; +import { withFormik } from 'formik'; +import Input from './input'; +import Button from './button'; + +interface FormProps { + onSubmit: (githubToken: string) => void; +} + +interface FormValues { + githubToken: string; +} + +const Form = (props: FormikProps) => { + return ( +
+ +
+ +
+
+ ); +}; + +const GithubBackupForm = withFormik({ + mapPropsToValues: () => ({ + githubToken: '', + }), + validationSchema: Yup.object().shape({ + githubToken: Yup.string().required('Required'), + }), + handleSubmit: (values, { props }) => { + props.onSubmit(values.githubToken); + }, +})(Form); + +export default GithubBackupForm; diff --git a/src/extension/components/input.tsx b/src/extension/components/input.tsx index 94dc9baa..8e1bb454 100644 --- a/src/extension/components/input.tsx +++ b/src/extension/components/input.tsx @@ -11,7 +11,7 @@ interface InputProps extends FormikProps { } const MarinaInputClasses = - 'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full sm:w-2/5 rounded-md z-10'; + 'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full rounded-md z-10'; /** * Generic Formik Input diff --git a/src/extension/components/ionio-restoration-form.tsx b/src/extension/components/ionio-restoration-form.tsx index ffea65f0..02c999ee 100644 --- a/src/extension/components/ionio-restoration-form.tsx +++ b/src/extension/components/ionio-restoration-form.tsx @@ -1,10 +1,10 @@ import type { FormikProps } from 'formik'; import { withFormik } from 'formik'; -import type { RestorationJSONDictionary } from '../../application/account'; import { checkRestorationDictionary } from '../../application/account'; import Button from './button'; import Input from './input'; import * as Yup from 'yup'; +import type { RestorationJSONDictionary } from '../../domain/backup'; interface FormProps { onSubmit: (dict: RestorationJSONDictionary, password: string) => Promise; diff --git a/src/extension/components/modal.tsx b/src/extension/components/modal.tsx index acbd8ac3..36c4751b 100644 --- a/src/extension/components/modal.tsx +++ b/src/extension/components/modal.tsx @@ -1,14 +1,16 @@ import React, { useRef } from 'react'; import ButtonIcon from './button-icon'; import useOnClickOutside from '../hooks/use-onclick-outside'; +import classNames from 'classnames'; export interface ModalProps { children: React.ReactNode; isOpen: boolean; onClose: () => any; + withoutMinHeight?: boolean; } -const Modal: React.FC = ({ isOpen, onClose, children }) => { +const Modal: React.FC = ({ isOpen, onClose, children, withoutMinHeight = false }) => { const ref = useRef(null); useOnClickOutside(ref, onClose); @@ -19,7 +21,10 @@ const Modal: React.FC = ({ isOpen, onClose, children }) => { return (
{children}
diff --git a/src/extension/components/restoration-backup-form.tsx b/src/extension/components/restoration-backup-form.tsx new file mode 100644 index 00000000..e036a7bb --- /dev/null +++ b/src/extension/components/restoration-backup-form.tsx @@ -0,0 +1,235 @@ +import type { AccountID } from 'marina-provider'; +import type { ChangeEvent } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { checkRestorationDictionary } from '../../application/account'; +import type { BackupConfig, RestorationJSONDictionary } from '../../domain/backup'; +import { BackupServiceType } from '../../domain/backup'; +import { BrowserSyncBackup } from '../../port/browser-sync-backup-service'; +import { extractErrorMessage } from '../utility/error'; +import Button from './button'; +import { Spinner } from './spinner'; +import Modal from './modal'; +import type { GithubBackupServiceConfig } from '../../port/github-backup-service'; +import { GithubBackupService } from '../../port/github-backup-service'; +import GithubBackupForm from './github-backup-form'; + +export type BackupFormValues = { + restoration: RestorationJSONDictionary; + backupServicesConfigs: BackupConfig[]; +}; + +export type RestorationBackupFormProps = { + onSubmit: (values: BackupFormValues) => void; +}; + +export const RestorationBackupForm: React.FC = ({ onSubmit }) => { + const hiddenFileInputRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [restorationError, setRestorationError] = useState(); + const [values, setValues] = useState({ + restoration: {}, + backupServicesConfigs: [], + }); + const [loadingText, setLoadingText] = useState(); + + // github config + const [githubConfigModalOpen, setGithubConfigModalOpen] = useState(false); + + useEffect(() => { + onSubmit(values); + }, [values]); + + const handleFileChange = async (e: ChangeEvent) => { + try { + setIsLoading(true); + setLoadingText('Restoring from file...'); + setRestorationError(undefined); + const strContent = await e.target.files![0].text(); + const restoration = JSON.parse(strContent); + if (!checkRestorationDictionary(restoration)) throw new Error('invalid restoration file'); + const newValues = { + ...values, + restoration, + }; + setValues(newValues); + } catch (e) { + console.error(e); + setRestorationError(extractErrorMessage(e)); + } finally { + setIsLoading(false); + setLoadingText(undefined); + } + }; + + const handleRestoreFromBrowserSync = async () => { + setIsLoading(true); + setLoadingText('Restoring from Browser Sync...'); + setRestorationError(undefined); + try { + const browserSyncBackupService = new BrowserSyncBackup(); + await browserSyncBackupService.initialize(); + const { ionioAccountsRestorationDictionary } = await browserSyncBackupService.load(); + setValues({ + ...values, + restoration: ionioAccountsRestorationDictionary, + backupServicesConfigs: [ + ...values.backupServicesConfigs, + { ID: 'browser-sync', type: BackupServiceType.BROWSER_SYNC }, + ], + }); + } catch (e) { + console.error(e); + setRestorationError(extractErrorMessage(e)); + } finally { + setIsLoading(false); + setLoadingText(undefined); + } + }; + + const handleRestoreFromGithub = async (token: string) => { + setIsLoading(true); + setLoadingText('Restoring from Github...'); + setRestorationError(undefined); + try { + const githubBackupService = new GithubBackupService({ githubAccessToken: token }); + await githubBackupService.initialize(); + const { ionioAccountsRestorationDictionary } = await githubBackupService.load(); + setValues({ + ...values, + restoration: ionioAccountsRestorationDictionary, + backupServicesConfigs: [ + ...values.backupServicesConfigs, + { + ID: 'github', + type: BackupServiceType.GITHUB, + githubAccessToken: token, + } as GithubBackupServiceConfig, + ], + }); + } catch (e) { + console.error(e); + setRestorationError(extractErrorMessage(e)); + } finally { + setIsLoading(false); + setLoadingText(undefined); + } + }; + + return ( +
+ {Object.keys(values.restoration).length === 0 && values.backupServicesConfigs.length === 0 ? ( +
+ {!isLoading ? ( +
+ + + + + +
+ ) : ( + <> + +

{loadingText || 'Loading...'}

+ + )} +
+ ) : ( +
+
{ + setRestorationError(undefined); + setLoadingText(undefined); + setValues({ + restoration: {}, + backupServicesConfigs: [], + }); + setIsLoading(false); + }} + className="absolute top-1 right-0.5" + > + +
+ {Object.keys(values.restoration).length > 0 ? ( + <> +

Successfully restored

+

+ Number of accounts:{' '} + { + Object.entries(values.restoration).reduce( + (acc, [_, restorations]) => + new Set([...acc, ...restorations.map((r) => r.accountName)]), + new Set() + ).size + }{' '} +

+ + ) : ( +

Successfully loaded, no backup found

+ )} + {values.backupServicesConfigs.length > 0 && ( +

Backup service will be enabled

+ )} +
+ )} + + {restorationError &&

{restorationError}

} + setGithubConfigModalOpen(false)} + > +
+

Link Marina with Github

+
+ { + setGithubConfigModalOpen(false); + handleRestoreFromGithub(token).catch(console.error); + }} + /> +
+
+ ); +}; diff --git a/src/extension/context/storage-context.tsx b/src/extension/context/storage-context.tsx index 534017aa..0729ccc7 100644 --- a/src/extension/context/storage-context.tsx +++ b/src/extension/context/storage-context.tsx @@ -21,7 +21,7 @@ import { useToastContext } from './toast-context'; const walletRepository = new WalletStorageAPI(); const appRepository = new AppStorageAPI(); -const assetRepository = new AssetStorageAPI(walletRepository); +const assetRepository = new AssetStorageAPI(); const taxiRepository = new TaxiStorageAPI(assetRepository, appRepository); const onboardingRepository = new OnboardingStorageAPI(); const sendFlowRepository = new SendFlowStorageAPI(); diff --git a/src/extension/onboarding/end-of-flow/index.tsx b/src/extension/onboarding/end-of-flow/index.tsx index 3b5adbdc..cf0b5f34 100644 --- a/src/extension/onboarding/end-of-flow/index.tsx +++ b/src/extension/onboarding/end-of-flow/index.tsx @@ -18,7 +18,6 @@ import type { NetworkString } from 'marina-provider'; import { AccountType } from 'marina-provider'; import { mnemonicToSeed } from 'bip39'; import { initWalletRepository } from '../../../domain/repository'; -import type { ChainSource } from '../../../domain/chainsource'; import { useStorageContext } from '../../context/storage-context'; import { UpdaterService } from '../../../application/updater'; import { Spinner } from '../../components/spinner'; @@ -52,7 +51,6 @@ const EndOfFlowOnboarding: React.FC = () => { try { const onboardingMnemonic = await onboardingRepository.getOnboardingMnemonic(); const onboardingPassword = await onboardingRepository.getOnboardingPassword(); - if (!onboardingMnemonic || !onboardingPassword) { throw new Error('onboarding Mnemonic or password not found'); } @@ -60,6 +58,8 @@ const EndOfFlowOnboarding: React.FC = () => { setErrorMsg(undefined); checkPassword(onboardingPassword); + const backupServicesConfigs = await onboardingRepository.getBackupServicesConfiguration(); + await appRepository.addBackupServiceConfig(...(backupServicesConfigs ?? [])); await initWalletRepository(walletRepository, onboardingMnemonic, onboardingPassword); await (Browser.browserAction ?? Browser.action).setPopup({ popup: 'popup.html' }); await appRepository.updateStatus({ isOnboardingCompleted: true }); @@ -137,33 +137,28 @@ const EndOfFlowOnboarding: React.FC = () => { }); } - // we already opened the Liquid chain source - let chainSourceRegtest: ChainSource | null = null; // restore the accounts const factory = await AccountFactory.create(walletRepository); for (const [network, restorations] of Object.entries(restoration)) { - let chainSource = undefined; - if (network === 'liquid') chainSource = liquidChainSource; - else if (network === 'testnet') chainSource = testnetChainSource; - else if (network === 'regtest') { - if (!chainSourceRegtest) { - chainSourceRegtest = await appRepository.getChainSource('regtest'); - if (!chainSourceRegtest) { - throw new Error('Chain source not found for regtest network'); - } + if (restorations.length === 0) continue; + try { + let chainSource = undefined; + if (network === 'liquid') chainSource = liquidChainSource; + else if (network === 'testnet') chainSource = testnetChainSource; + else if (network === 'regtest') { + continue; } - chainSource = chainSourceRegtest; - } - if (!chainSource) throw new Error(`Chain source not found for ${network} network`); + if (!chainSource) throw new Error(`Chain source not found for ${network} network`); - for (const restoration of restorations) { - const account = await factory.make(network as NetworkString, restoration.accountName); - await account.restoreFromJSON(chainSource, restoration); + for (const restoration of restorations) { + const account = await factory.make(network as NetworkString, restoration.accountName); + await account.restoreFromJSON(chainSource, restoration); + } + } catch { + console.warn(`Failed to restore ${network} account(s) from JSON file(s)`); + continue; } } - - // close the chain source if opened - await chainSourceRegtest?.close(); } await testnetChainSource.close(); await liquidChainSource.close(); diff --git a/src/extension/onboarding/onboarding-form.tsx b/src/extension/onboarding/onboarding-form.tsx index 5490c009..aef4a6bb 100644 --- a/src/extension/onboarding/onboarding-form.tsx +++ b/src/extension/onboarding/onboarding-form.tsx @@ -38,7 +38,7 @@ const OnboardingFormView = (props: FormikProps) => { const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props; return ( -
+