-
Notifications
You must be signed in to change notification settings - Fork 5k
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
3box Replacement #15243
Changes from all commits
11d4261
88ba0c4
847233e
1415e62
c543b8a
edfcd3d
37f428f
61d0fa3
13b92fe
5e3bece
f50126f
73bdbdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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); | ||
|
||
this._trackMetaMetricsEvent({ | ||
event: 'User Data Exported', | ||
category: 'Backup', | ||
}); | ||
|
||
return result; | ||
} | ||
} |
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, | ||
); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
}); |
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); | ||
} | ||
} |
There was a problem hiding this comment.
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 theshared/
directoryThere was a problem hiding this comment.
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