Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3box Replacement #15243

Merged
merged 12 commits into from
Aug 9, 2022
21 changes: 21 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions app/scripts/controllers/backup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { exportAsFile } from '../../../shared/modules/export-utils';
import { prependZero } from '../../../shared/modules/string-utils';

export default class BackupController {
constructor(opts = {}) {
const {
preferencesController,
addressBookController,
trackMetaMetricsEvent,
} = opts;

this.preferencesController = preferencesController;
this.addressBookController = addressBookController;
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
}

async restoreUserData(jsonString) {
const existingPreferences = this.preferencesController.store.getState();
const { preferences, addressBook } = JSON.parse(jsonString);
if (preferences) {
preferences.identities = existingPreferences.identities;
preferences.lostIdentities = existingPreferences.lostIdentities;
preferences.selectedAddress = existingPreferences.selectedAddress;

this.preferencesController.store.updateState(preferences);
}

if (addressBook) {
this.addressBookController.update(addressBook, true);
}

if (preferences && addressBook) {
this._trackMetaMetricsEvent({
event: 'User Data Imported',
category: 'Backup',
});
}
}

async backupUserData() {
const userData = {
preferences: { ...this.preferencesController.store.getState() },
addressBook: { ...this.addressBookController.state },
};

/**
* We can remove these properties since we will won't be restoring identities from backup
*/
delete userData.preferences.identities;
delete userData.preferences.lostIdentities;
delete userData.preferences.selectedAddress;

const result = JSON.stringify(userData);

const date = new Date();

const prefixZero = (num) => prependZero(num, 2);

/*
* userData.YYYY_MM_DD_HH_mm_SS e.g userData.2022_01_13_13_45_56
* */
const userDataFileName = `MetaMaskUserData.${date.getFullYear()}_${prefixZero(
date.getMonth() + 1,
)}_${prefixZero(date.getDay())}_${prefixZero(date.getHours())}_${prefixZero(
date.getMinutes(),
)}_${prefixZero(date.getDay())}.json`;

exportAsFile(userDataFileName, result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move the exportAsFile to a utility file in the shared/ directory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, this could be called from the UI. This utility uses the DOM, so it won't work in the MV3 service worker anyway


this._trackMetaMetricsEvent({
event: 'User Data Exported',
category: 'Backup',
});

return result;
}
}
118 changes: 118 additions & 0 deletions app/scripts/controllers/backup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import BackupController from './backup';

function getMockController() {
const mcState = {
getSelectedAddress: sinon.stub().returns('0x01'),
selectedAddress: '0x01',
identities: {
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': {
address: '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B',
lastSelected: 1655380342907,
name: 'Account 3',
},
},
lostIdentities: {
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435': {
address: '0xfd59bbe569376e3d3e4430297c3c69ea93f77435',
lastSelected: 1655379648197,
name: 'Ledger 1',
},
},
update: (store) => (mcState.store = store),
};

mcState.store = {
getState: sinon.stub().returns(mcState),
updateState: (store) => (mcState.store = store),
};

return mcState;
}

const jsonData = `{"preferences":{"frequentRpcListDetail":[{"chainId":"0x539","nickname":"Localhost 8545","rpcPrefs":{},"rpcUrl":"http://localhost:8545","ticker":"ETH"},{"chainId":"0x38","nickname":"Binance Smart Chain Mainnet","rpcPrefs":{"blockExplorerUrl":"https://bscscan.com"},"rpcUrl":"https://bsc-dataseed1.binance.org","ticker":"BNB"},{"chainId":"0x61","nickname":"Binance Smart Chain Testnet","rpcPrefs":{"blockExplorerUrl":"https://testnet.bscscan.com"},"rpcUrl":"https://data-seed-prebsc-1-s1.binance.org:8545","ticker":"tBNB"},{"chainId":"0x89","nickname":"Polygon Mainnet","rpcPrefs":{"blockExplorerUrl":"https://polygonscan.com"},"rpcUrl":"https://polygon-rpc.com","ticker":"MATIC"}],"useBlockie":false,"useNonceField":false,"usePhishDetect":true,"dismissSeedBackUpReminder":false,"useTokenDetection":false,"useCollectibleDetection":false,"openSeaEnabled":false,"advancedGasFee":null,"featureFlags":{"sendHexData":true,"showIncomingTransactions":true},"knownMethodData":{},"currentLocale":"en","forgottenPassword":false,"preferences":{"hideZeroBalanceTokens":false,"showFiatInTestnets":false,"showTestNetworks":true,"useNativeCurrencyAsPrimaryCurrency":true},"ipfsGateway":"dweb.link","infuraBlocked":false,"ledgerTransportType":"webhid","theme":"light","customNetworkListEnabled":false,"textDirection":"auto"},"addressBook":{"addressBook":{"0x61":{"0x42EB768f2244C8811C63729A21A3569731535f06":{"address":"0x42EB768f2244C8811C63729A21A3569731535f06","chainId":"0x61","isEns":false,"memo":"","name":""}}}}}`;

describe('BackupController', function () {
const getBackupController = () => {
return new BackupController({
preferencesController: getMockController(),
addressBookController: getMockController(),
trackMetaMetricsEvent: sinon.stub(),
});
};

describe('constructor', function () {
it('should setup correctly', async function () {
const backupController = getBackupController();
const selectedAddress =
backupController.preferencesController.getSelectedAddress();
assert.equal(selectedAddress, '0x01');
});

it('should restore backup', async function () {
const backupController = getBackupController();
backupController.restoreUserData(jsonData);
// check Preferences backup
assert.equal(
backupController.preferencesController.store.frequentRpcListDetail[0]
.chainId,
'0x539',
);
assert.equal(
backupController.preferencesController.store.frequentRpcListDetail[1]
.chainId,
'0x38',
);
// make sure identities are not lost after restore
assert.equal(
backupController.preferencesController.store.identities[
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
].lastSelected,
1655380342907,
);
assert.equal(
backupController.preferencesController.store.identities[
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
].name,
'Account 3',
);
assert.equal(
backupController.preferencesController.store.lostIdentities[
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
].lastSelected,
1655379648197,
);
assert.equal(
backupController.preferencesController.store.lostIdentities[
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
].name,
'Ledger 1',
);
// make sure selected address is not lost after restore
assert.equal(
backupController.preferencesController.store.selectedAddress,
'0x01',
);
// check address book backup
assert.equal(
backupController.addressBookController.store.addressBook['0x61'][
'0x42EB768f2244C8811C63729A21A3569731535f06'
].chainId,
'0x61',
);
assert.equal(
backupController.addressBookController.store.addressBook['0x61'][
'0x42EB768f2244C8811C63729A21A3569731535f06'
].address,
'0x42EB768f2244C8811C63729A21A3569731535f06',
);
assert.equal(
backupController.addressBookController.store.addressBook['0x61'][
'0x42EB768f2244C8811C63729A21A3569731535f06'
].isEns,
false,
);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add restoreUserData and backupUserData tests to ensure that we get expected outcomes in each case, since those are the bulk of the work with this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi David,

I can't seem to find a way to mock window functions in sinon for backup so I spoke with @PeterYinusa and we agreed to do an e2e test instead.

I've however added a unit test for restore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The e2e suggestion was made to ensure we have some coverage, but is not a replacement for a unit test.

});
16 changes: 16 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import CachedBalancesController from './controllers/cached-balances';
import AlertController from './controllers/alert';
import OnboardingController from './controllers/onboarding';
import ThreeBoxController from './controllers/threebox';
import BackupController from './controllers/backup';
import IncomingTransactionsController from './controllers/incoming-transactions';
import MessageManager, { normalizeMsgData } from './lib/message-manager';
import DecryptMessageManager from './lib/decrypt-message-manager';
Expand Down Expand Up @@ -797,6 +798,14 @@ export default class MetamaskController extends EventEmitter {
),
});

this.backupController = new BackupController({
preferencesController: this.preferencesController,
addressBookController: this.addressBookController,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});

this.txController = new TransactionController({
initState:
initState.TransactionController || initState.TransactionManager,
Expand Down Expand Up @@ -1047,6 +1056,7 @@ export default class MetamaskController extends EventEmitter {
PermissionLogController: this.permissionLogController.store,
SubjectMetadataController: this.subjectMetadataController,
ThreeBoxController: this.threeBoxController.store,
BackupController: this.backupController,
AnnouncementController: this.announcementController,
GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController,
Expand Down Expand Up @@ -1085,6 +1095,7 @@ export default class MetamaskController extends EventEmitter {
PermissionLogController: this.permissionLogController.store,
SubjectMetadataController: this.subjectMetadataController,
ThreeBoxController: this.threeBoxController.store,
BackupController: this.backupController,
SwapsController: this.swapsController.store,
EnsController: this.ensController.store,
ApprovalController: this.approvalController,
Expand Down Expand Up @@ -1519,6 +1530,7 @@ export default class MetamaskController extends EventEmitter {
smartTransactionsController,
txController,
assetsContractController,
backupController,
} = this;

return {
Expand Down Expand Up @@ -1960,6 +1972,10 @@ export default class MetamaskController extends EventEmitter {
removePollingTokenFromAppState:
appStateController.removePollingToken.bind(appStateController),

// BackupController
backupUserData: backupController.backupUserData.bind(backupController),
restoreUserData: backupController.restoreUserData.bind(backupController),

// DetectTokenController
detectNewTokens: detectTokensController.detectNewTokens.bind(
detectTokensController,
Expand Down
19 changes: 19 additions & 0 deletions shared/modules/export-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getRandomFileName } from '../../ui/helpers/utils/util';

export function exportAsFile(filename, data, type = 'text/csv') {
// eslint-disable-next-line no-param-reassign
filename = filename || getRandomFileName();
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
const blob = new window.Blob([data], { type });
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename);
} else {
const elem = window.document.createElement('a');
elem.target = '_blank';
elem.href = window.URL.createObjectURL(blob);
elem.download = filename;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
4 changes: 4 additions & 0 deletions shared/modules/string-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export function isEqualCaseInsensitive(value1, value2) {
}
return value1.toLowerCase() === value2.toLowerCase();
}

export function prependZero(num, maxLength) {
return num.toString().padStart(maxLength, '0');
}
7 changes: 7 additions & 0 deletions test/e2e/helpers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const path = require('path');
const { promises: fs } = require('fs');
const BigNumber = require('bignumber.js');
const mockttp = require('mockttp');
const createStaticServer = require('../../development/create-static-server');
Expand All @@ -17,6 +18,11 @@ const largeDelayMs = regularDelayMs * 2;
const veryLargeDelayMs = largeDelayMs * 2;
const dappBasePort = 8080;

const createDownloadFolder = async (downloadsFolder) => {
await fs.rm(downloadsFolder, { recursive: true, force: true });
await fs.mkdir(downloadsFolder, { recursive: true });
};

const convertToHexValue = (val) => `0x${new BigNumber(val, 10).toString(16)}`;

async function withFixtures(options, testSuite) {
Expand Down Expand Up @@ -330,4 +336,5 @@ module.exports = {
connectDappWithExtensionPopup,
completeImportSRPOnboardingFlow,
completeImportSRPOnboardingFlowWordByWord,
createDownloadFolder,
};
Loading