import { ObservableStore } from '@metamask/obs-store'; /* eslint-disable import/first,import/order */ const Box = process.env.IN_TEST ? require('../../../development/mock-3box') : require('3box'); /* eslint-enable import/order */ import log from 'loglevel'; import { JsonRpcEngine } from 'json-rpc-engine'; import { providerFromEngine } from 'eth-json-rpc-middleware'; import Migrator from '../lib/migrator'; import migrations from '../migrations'; import createOriginMiddleware from '../lib/createOriginMiddleware'; import createMetamaskMiddleware from './network/createMetamaskMiddleware'; /* eslint-enable import/first */ const SYNC_TIMEOUT = 60 * 1000; // one minute export default class ThreeBoxController { constructor(opts = {}) { const { preferencesController, keyringController, addressBookController, version, getKeyringControllerState, trackMetaMetricsEvent, } = opts; this.preferencesController = preferencesController; this.addressBookController = addressBookController; this.keyringController = keyringController; this.provider = this._createProvider({ version, getAccounts: async ({ origin }) => { if (origin !== '3Box') { return []; } const { isUnlocked } = getKeyringControllerState(); const accounts = await this.keyringController.getAccounts(); if (isUnlocked && accounts[0]) { const appKeyAddress = await this.keyringController.getAppKeyAddress( accounts[0], 'wallet://3box.metamask.io', ); return [appKeyAddress]; } return []; }, processPersonalMessage: async (msgParams) => { const accounts = await this.keyringController.getAccounts(); return keyringController.signPersonalMessage( { ...msgParams, from: accounts[0] }, { withAppKeyOrigin: 'wallet://3box.metamask.io', }, ); }, }); this._trackMetaMetricsEvent = trackMetaMetricsEvent; const initState = { threeBoxSyncingAllowed: false, showRestorePrompt: true, threeBoxLastUpdated: 0, ...opts.initState, threeBoxAddress: null, threeBoxSynced: false, threeBoxDisabled: false, }; this.store = new ObservableStore(initState); this.registeringUpdates = false; this.lastMigration = migrations .sort((a, b) => a.version - b.version) .slice(-1)[0]; if (initState.threeBoxSyncingAllowed) { this.init(); } } async init() { const accounts = await this.keyringController.getAccounts(); this.address = accounts[0]; this._trackMetaMetricsEvent({ event: '3Box Initiated', category: '3Box', }); if (this.address && !(this.box && this.store.getState().threeBoxSynced)) { await this.new3Box(); } } async _update3Box() { try { const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState(); if (threeBoxSyncingAllowed && threeBoxSynced) { const newState = { preferences: this.preferencesController.store.getState(), addressBook: this.addressBookController.state, lastUpdated: Date.now(), lastMigration: this.lastMigration, }; await this.space.private.set( 'metamaskBackup', JSON.stringify(newState), ); await this.setShowRestorePromptToFalse(); } } catch (error) { console.error(error); } } _createProvider(providerOpts) { const metamaskMiddleware = createMetamaskMiddleware(providerOpts); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware({ origin: '3Box' })); engine.push(metamaskMiddleware); const provider = providerFromEngine(engine); return provider; } _waitForOnSyncDone() { return new Promise((resolve) => { this.box.onSyncDone(() => { log.debug('3Box box sync done'); return resolve(); }); }); } async new3Box() { const accounts = await this.keyringController.getAccounts(); this.address = await this.keyringController.getAppKeyAddress( accounts[0], 'wallet://3box.metamask.io', ); let backupExists; try { const threeBoxConfig = await Box.getConfig(this.address); backupExists = threeBoxConfig.spaces && threeBoxConfig.spaces.metamask; } catch (e) { if (e.message.match(/^Error: Invalid response \(404\)/u)) { this._trackMetaMetricsEvent({ event: '3Box Backup does not exist', category: '3Box', }); backupExists = false; } else { this._trackMetaMetricsEvent({ event: '3Box Config Error', category: '3Box', }); throw e; } } if (this.getThreeBoxSyncingState() || backupExists) { this.store.updateState({ threeBoxSynced: false }); let timedOut = false; const syncTimeout = setTimeout(() => { log.error(`3Box sync timed out after ${SYNC_TIMEOUT} ms`); timedOut = true; this.store.updateState({ threeBoxDisabled: true, threeBoxSyncingAllowed: false, }); }, SYNC_TIMEOUT); try { this.box = await Box.openBox(this.address, this.provider); await this._waitForOnSyncDone(); this.space = await this.box.openSpace('metamask', { onSyncDone: async () => { const stateUpdate = { threeBoxSynced: true, threeBoxAddress: this.address, }; if (timedOut) { log.info(`3Box sync completed after timeout; no longer disabled`); stateUpdate.threeBoxDisabled = false; } clearTimeout(syncTimeout); this.store.updateState(stateUpdate); log.debug('3Box space sync done'); this._trackMetaMetricsEvent({ event: '3Box Synced', category: '3Box', }); }, }); } catch (e) { this._trackMetaMetricsEvent({ event: '3Box Initiation Error', category: '3Box', }); console.error(e); throw e; } } } async getLastUpdated() { const res = await this.space.private.get('metamaskBackup'); const parsedRes = JSON.parse(res || '{}'); return parsedRes.lastUpdated; } async migrateBackedUpState(backedUpState) { const migrator = new Migrator({ migrations }); const { preferences, addressBook } = JSON.parse(backedUpState); const formattedStateBackup = { PreferencesController: preferences, AddressBookController: addressBook, }; const initialMigrationState = migrator.generateInitialState(formattedStateBackup); const migratedState = await migrator.migrateData(initialMigrationState); return { preferences: migratedState.data.PreferencesController, addressBook: migratedState.data.AddressBookController, }; } async restoreFromThreeBox() { const backedUpState = await this.space.private.get('metamaskBackup'); const { preferences, addressBook } = await this.migrateBackedUpState( backedUpState, ); this.store.updateState({ threeBoxLastUpdated: backedUpState.lastUpdated }); preferences && this.preferencesController.store.updateState(preferences); addressBook && this.addressBookController.update(addressBook, true); this.setShowRestorePromptToFalse(); this._trackMetaMetricsEvent({ event: '3Box Restored Data', category: '3Box', }); } turnThreeBoxSyncingOn() { this._trackMetaMetricsEvent({ event: '3Box Sync Turned On', category: '3Box', }); this._registerUpdates(); } turnThreeBoxSyncingOff() { this._trackMetaMetricsEvent({ event: '3Box Sync Turned Off', category: '3Box', }); this.box.logout(); } setShowRestorePromptToFalse() { this.store.updateState({ showRestorePrompt: false }); } setThreeBoxSyncingPermission(newThreeboxSyncingState) { if (this.store.getState().threeBoxDisabled) { return; } this.store.updateState({ threeBoxSyncingAllowed: newThreeboxSyncingState, }); if (newThreeboxSyncingState && this.box) { this.turnThreeBoxSyncingOn(); } if (!newThreeboxSyncingState && this.box) { this.turnThreeBoxSyncingOff(); } } getThreeBoxSyncingState() { return this.store.getState().threeBoxSyncingAllowed; } _registerUpdates() { if (!this.registeringUpdates) { const updatePreferences = this._update3Box.bind(this); this.preferencesController.store.subscribe(updatePreferences); const updateAddressBook = this._update3Box.bind(this); this.addressBookController.subscribe(updateAddressBook); this.registeringUpdates = true; } } }