diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 6623419df..016740d88 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -6,7 +6,6 @@ const encryptor = require('browser-passworder') const normalize = require('./lib/sig-util').normalize const messageManager = require('./lib/message-manager') -const IdStoreMigrator = require('./lib/idStore-migrator') const BN = ethUtil.BN // Keyrings: @@ -40,11 +39,6 @@ module.exports = class KeyringController extends EventEmitter { this._unconfMsgCbs = {} this.getNetwork = opts.getNetwork - - // TEMPORARY UNTIL FULL DEPRECATION: - this.idStoreMigrator = new IdStoreMigrator({ - configManager: this.configManager, - }) } // Set Store @@ -107,7 +101,6 @@ module.exports = class KeyringController extends EventEmitter { conversionDate: this.configManager.getConversionDate(), keyringTypes: this.keyringTypes.map(krt => krt.type), identities: this.identities, - lostAccounts: this.configManager.getLostAccounts(), } } @@ -215,10 +208,7 @@ module.exports = class KeyringController extends EventEmitter { // Temporarily also migrates any old-style vaults first, as well. // (Pre MetaMask 3.0.0) submitPassword (password) { - return this.migrateOldVaultIfAny(password) - .then(() => { - return this.unlockKeyrings(password) - }) + return this.unlockKeyrings(password) .then((keyrings) => { this.keyrings = keyrings return this.fullUpdate() @@ -420,41 +410,6 @@ module.exports = class KeyringController extends EventEmitter { // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS. - // Migrate Old Vault If Any - // @string password - // - // returns Promise() - // - // Temporary step used when logging in. - // Checks if old style (pre-3.0.0) Metamask Vault exists. - // If so, persists that vault in the new vault format - // with the provided password, so the other unlock steps - // may be completed without interruption. - migrateOldVaultIfAny (password) { - const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault() - if (!shouldMigrate) { - return Promise.resolve() - } - - return this.idStoreMigrator.migratedVaultForPassword(password) - .then((result) => { - this.password = password - - if (result && shouldMigrate) { - const { serialized, lostAccounts } = result - this.configManager.setLostAccounts(lostAccounts) - return this.restoreKeyring(serialized) - .then(keyring => keyring.getAccounts()) - .then((accounts) => { - this.configManager.setSelectedAccount(accounts[0]) - return this.persistAllKeyrings() - }) - } else { - return Promise.resolve() - } - }) - } - // Create First Key Tree // returns @Promise // @@ -575,6 +530,10 @@ module.exports = class KeyringController extends EventEmitter { // initializing the persisted keyrings to RAM. unlockKeyrings (password) { const encryptedVault = this.configManager.getVault() + if (!encryptedVault) { + throw new Error('Cannot unlock without a previous vault.') + } + return this.encryptor.decrypt(password, encryptedVault) .then((vault) => { this.password = password diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index c723655fa..93501c859 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -118,7 +118,7 @@ ConfigManager.prototype.setVault = function (encryptedString) { ConfigManager.prototype.getVault = function () { var data = this.getData() - return ('vault' in data) && data.vault + return data.vault } ConfigManager.prototype.getKeychains = function () { diff --git a/app/scripts/lib/idStore-migrator.js b/app/scripts/lib/idStore-migrator.js index 2e9418376..655aed0af 100644 --- a/app/scripts/lib/idStore-migrator.js +++ b/app/scripts/lib/idStore-migrator.js @@ -63,7 +63,12 @@ module.exports = class IdentityStoreMigrator { return { serialized, - lostAccounts, + lostAccounts: lostAccounts.map((address) => { + return { + address, + privateKey: this.idStore.exportAccount(address), + } + }), } }) } diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index 5c0f8d7f7..e4cbca456 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -190,7 +190,8 @@ IdentityStore.prototype.submitPassword = function (password, cb) { IdentityStore.prototype.exportAccount = function (address, cb) { var privateKey = this._idmgmt.exportPrivateKey(address) - cb(null, privateKey) + if (cb) cb(null, privateKey) + return privateKey } // private diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 30fcbcb66..a64c745ce 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -72,13 +72,13 @@ MetamaskInpageProvider.prototype.send = function (payload) { case 'eth_accounts': // read from localStorage - selectedAccount = self.publicConfigStore.get('selectedAddress') + selectedAccount = self.publicConfigStore.get('selectedAccount') result = selectedAccount ? [selectedAccount] : [] break case 'eth_coinbase': // read from localStorage - selectedAccount = self.publicConfigStore.get('selectedAddress') + selectedAccount = self.publicConfigStore.get('selectedAccount') result = selectedAccount || '0x0000000000000000000000000000000000000000' break diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4af5cd78d..5df10672a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -11,6 +11,7 @@ const ConfigManager = require('./lib/config-manager') const extension = require('./lib/extension') const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') +const IdStoreMigrator = require('./lib/idStore-migrator') module.exports = class MetamaskController { @@ -54,6 +55,11 @@ module.exports = class MetamaskController { this.checkTOSChange() this.scheduleConversionInterval() + + // TEMPORARY UNTIL FULL DEPRECATION: + this.idStoreMigrator = new IdStoreMigrator({ + configManager: this.configManager, + }) } getState () { @@ -63,7 +69,9 @@ module.exports = class MetamaskController { this.configManager.getConfig(), this.keyringController.getState(), this.txManager.getState(), - this.noticeController.getState() + this.noticeController.getState(), { + lostAccounts: this.configManager.getLostAccounts(), + } ) } @@ -83,6 +91,7 @@ module.exports = class MetamaskController { setTOSHash: this.setTOSHash.bind(this), checkTOSChange: this.checkTOSChange.bind(this), setGasMultiplier: this.setGasMultiplier.bind(this), + markAccountsFound: this.markAccountsFound.bind(this), // forward directly to keyringController createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController), @@ -90,7 +99,12 @@ module.exports = class MetamaskController { placeSeedWords: nodeify(keyringController.placeSeedWords).bind(keyringController), clearSeedWordCache: nodeify(keyringController.clearSeedWordCache).bind(keyringController), setLocked: nodeify(keyringController.setLocked).bind(keyringController), - submitPassword: nodeify(keyringController.submitPassword).bind(keyringController), + submitPassword: (password, cb) => { + this.migrateOldVaultIfAny(password) + .then(keyringController.submitPassword.bind(keyringController)) + .then((newState) => { cb(null, newState) }) + .catch((reason) => { cb(reason) }) + }, addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController), addNewAccount: nodeify(keyringController.addNewAccount).bind(keyringController), setSelectedAccount: nodeify(keyringController.setSelectedAccount).bind(keyringController), @@ -429,4 +443,66 @@ module.exports = class MetamaskController { getStateNetwork () { return this.state.network } + + markAccountsFound(cb) { + this.configManager.setLostAccounts([]) + this.sendUpdate() + cb(null, this.getState()) + } + + // Migrate Old Vault If Any + // @string password + // + // returns Promise() + // + // Temporary step used when logging in. + // Checks if old style (pre-3.0.0) Metamask Vault exists. + // If so, persists that vault in the new vault format + // with the provided password, so the other unlock steps + // may be completed without interruption. + migrateOldVaultIfAny (password) { + + if (!this.checkIfShouldMigrate()) { + return Promise.resolve(password) + } + + const keyringController = this.keyringController + + return this.idStoreMigrator.migratedVaultForPassword(password) + .then(this.restoreOldVaultAccounts.bind(this)) + .then(this.restoreOldLostAccounts.bind(this)) + .then(keyringController.persistAllKeyrings.bind(keyringController)) + .then(() => password) + } + + checkIfShouldMigrate() { + return !!this.configManager.getWallet() && !this.configManager.getVault() + } + + restoreOldVaultAccounts(migratorOutput) { + const { serialized } = migratorOutput + return this.keyringController.restoreKeyring(serialized) + .then(() => migratorOutput) + } + + restoreOldLostAccounts(migratorOutput) { + const { lostAccounts } = migratorOutput + if (lostAccounts) { + this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address)) + return this.importLostAccounts(migratorOutput) + } + return Promise.resolve(migratorOutput) + } + + // IMPORT LOST ACCOUNTS + // @Object with key lostAccounts: @Array accounts <{ address, privateKey }> + // Uses the array's private keys to create a new Simple Key Pair keychain + // and add it to the keyring controller. + importLostAccounts ({ lostAccounts }) { + const privKeys = lostAccounts.map(acct => acct.privateKey) + return this.keyringController.restoreKeyring({ + type: 'Simple Key Pair', + data: privKeys, + }) + } } diff --git a/development/states/lost-accounts.json b/development/states/lost-accounts.json new file mode 100644 index 000000000..9f1764c82 --- /dev/null +++ b/development/states/lost-accounts.json @@ -0,0 +1,91 @@ +{ + "metamask": { + "currentFiat": "USD", + "lostAccounts": [ + "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" + ], + "conversionRate": 11.06608791, + "conversionDate": 1470421024, + "isInitialized": true, + "isUnlocked": true, + "currentDomain": "example.com", + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { + "name": "Wallet 1", + "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "mayBeFauceting": false + }, + "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { + "name": "Wallet 2", + "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", + "mayBeFauceting": false + }, + "0xeb9e64b93097bc15f01f13eae97015c57ab64823": { + "name": "Wallet 3", + "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823", + "mayBeFauceting": false + }, + "0x704107d04affddd9b66ab9de3dd7b095852e9b69": { + "name": "Wallet 4", + "address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69", + "mayBeFauceting": false + } + }, + "unconfTxs": {}, + "accounts": { + "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { + "code": "0x", + "balance": "0x100000000000", + "nonce": "0x0", + "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" + }, + "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { + "code": "0x", + "nonce": "0x0", + "balance": "0x100000000000", + "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b" + }, + "0xeb9e64b93097bc15f01f13eae97015c57ab64823": { + "code": "0x", + "nonce": "0x0", + "balance": "0x100000000000", + "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823" + }, + "0x704107d04affddd9b66ab9de3dd7b095852e9b69": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69" + } + }, + "transactions": [], + "selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "network": "2", + "seedWords": null, + "isDisclaimerConfirmed": true, + "unconfMsgs": {}, + "messages": [], + "provider": { + "type": "testnet" + }, + "selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "detailView": null, + "context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" + }, + "accountDetail": { + "subview": "transactions" + }, + "currentDomain": "127.0.0.1:9966", + "transForward": true, + "isLoading": false, + "warning": null + }, + "identities": {} +} diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index 12c573db1..1811ccbd4 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -79,7 +79,7 @@ QUnit.test('agree to terms', function (assert) { var createButton = app.find('button.primary')[0] createButton.click() - return wait(1500) + return wait(1000) }).then(function() { var detail = app.find('.account-detail-section')[0] diff --git a/test/integration/lib/idStore-migrator-test.js b/test/integration/lib/idStore-migrator-test.js new file mode 100644 index 000000000..338896171 --- /dev/null +++ b/test/integration/lib/idStore-migrator-test.js @@ -0,0 +1,74 @@ +var KeyringController = require('../../../app/scripts/keyring-controller') +var ConfigManager = require('../../../app/scripts/lib/config-manager') +var IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator') + +var oldStyleVault = require('../mocks/oldVault.json') +var badStyleVault = require('../mocks/badVault.json') + +var STORAGE_KEY = 'metamask-config' +var PASSWORD = '12345678' +var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase() +var SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner' + +var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9' + +QUnit.module('Old Style Vaults', { + beforeEach: function () { + window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault) + + this.configManager = new ConfigManager({ + loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, + setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, + }) + + this.migrator = new IdStoreMigrator({ + configManager: this.configManager, + }) + } +}) + +QUnit.test('migrator:isInitialized', function (assert) { + assert.ok(this.migrator) +}) + +QUnit.test('migrator:migratedVaultForPassword', function (assert) { + var done = assert.async() + + this.migrator.migratedVaultForPassword(PASSWORD) + .then((result) => { + const { serialized, lostAccounts } = result + assert.equal(serialized.data.mnemonic, SEED, 'seed phrase recovered') + assert.equal(lostAccounts.length, 0, 'no lost accounts') + done() + }) +}) + +QUnit.module('Old Style Vaults with bad HD seed', { + beforeEach: function () { + window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault) + + this.configManager = new ConfigManager({ + loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, + setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, + }) + + this.migrator = new IdStoreMigrator({ + configManager: this.configManager, + }) + } +}) + +QUnit.test('migrator:migratedVaultForPassword', function (assert) { + var done = assert.async() + + this.migrator.migratedVaultForPassword(PASSWORD) + .then((result) => { + const { serialized, lostAccounts } = result + + assert.equal(lostAccounts.length, 1, 'one lost account') + assert.equal(lostAccounts[0].address, '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase()) + assert.ok(lostAccounts[0].privateKey, 'private key exported') + done() + }) +}) + diff --git a/test/integration/lib/keyring-controller-test.js b/test/integration/lib/keyring-controller-test.js deleted file mode 100644 index 314641841..000000000 --- a/test/integration/lib/keyring-controller-test.js +++ /dev/null @@ -1,126 +0,0 @@ -var KeyringController = require('../../../app/scripts/keyring-controller') -var ConfigManager = require('../../../app/scripts/lib/config-manager') - -var oldStyleVault = require('../mocks/oldVault.json') -var badStyleVault = require('../mocks/badVault.json') - -var STORAGE_KEY = 'metamask-config' -var PASSWORD = '12345678' -var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase() - -var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9' - - -QUnit.module('Old Style Vaults', { - beforeEach: function () { - window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault) - - this.configManager = new ConfigManager({ - loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, - setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, - }) - - this.keyringController = new KeyringController({ - configManager: this.configManager, - getNetwork: () => { return '2' }, - txManager: { - getTxList: () => [], - getUnapprovedTxList: () => [] - }, - }) - - this.ethStore = { - addAccount: () => {}, - removeAccount: () => {}, - } - - this.keyringController.setStore(this.ethStore) - } -}) - -QUnit.test('keyringController:isInitialized', function (assert) { - assert.ok(this.keyringController.getState().isInitialized) -}) - -QUnit.test('keyringController:submitPassword', function (assert) { - var done = assert.async() - - this.keyringController.submitPassword(PASSWORD) - .then((state) => { - assert.ok(state.identities[FIRST_ADDRESS]) - assert.equal(state.lostAccounts.length, 0, 'no lost accounts') - done() - }) -}) - -QUnit.test('keyringController:setLocked', function (assert) { - var done = assert.async() - var self = this - - this.keyringController.setLocked() - .then(function() { - assert.notOk(self.keyringController.password, 'password should be deallocated') - assert.deepEqual(self.keyringController.keyrings, [], 'keyrings should be deallocated') - done() - }) - .catch((reason) => { - assert.ifError(reason) - done() - }) -}) - -QUnit.module('Old Style Vaults with bad HD seed', { - beforeEach: function () { - window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault) - - this.configManager = new ConfigManager({ - loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, - setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, - }) - - this.keyringController = new KeyringController({ - configManager: this.configManager, - getNetwork: () => { return '2' }, - }) - - this.ethStore = { - addAccount: () => {}, - removeAccount: () => {}, - } - - this.keyringController.setStore(this.ethStore) - } -}) - -QUnit.test('keyringController:isInitialized', function (assert) { - assert.ok(this.keyringController.getState().isInitialized, 'vault is initialized') -}) - -QUnit.test('keyringController:submitPassword', function (assert) { - var done = assert.async() - - this.keyringController.submitPassword(PASSWORD) - .then((state) => { - assert.ok(state.identities[BAD_STYLE_FIRST_ADDRESS]) - assert.equal(state.lostAccounts.length, 1, 'one lost account') - assert.equal(state.lostAccounts[0], '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase()) - assert.deepEqual(this.configManager.getLostAccounts(), state.lostAccounts, 'persisted') - done() - }) -}) - -QUnit.test('keyringController:setLocked', function (assert) { - var done = assert.async() - var self = this - - this.keyringController.setLocked() - .then(function() { - assert.notOk(self.keyringController.password, 'password should be deallocated') - assert.deepEqual(self.keyringController.keyrings, [], 'keyrings should be deallocated') - done() - }) - .catch((reason) => { - assert.ifError(reason) - done() - }) -}) diff --git a/test/unit/idStore-migration-test.js b/test/unit/idStore-migration-test.js index c4a3f3ae4..7e91616bf 100644 --- a/test/unit/idStore-migration-test.js +++ b/test/unit/idStore-migration-test.js @@ -87,7 +87,7 @@ describe('IdentityStore to KeyringController migration', function() { keyringController.configManager.setWallet('something') const state = keyringController.getState() assert(state.isInitialized, 'old vault counted as initialized.') - assert.equal(state.lostAccounts.length, 0, 'no lost accounts') + assert(!state.lostAccounts, 'no lost accounts') }) }) }) diff --git a/ui/app/actions.js b/ui/app/actions.js index 1c32c9bb1..606460314 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -20,6 +20,7 @@ var actions = { showNotice: showNotice, CLEAR_NOTICES: 'CLEAR_NOTICES', clearNotices: clearNotices, + markAccountsFound, // intialize screen AGREE_TO_DISCLAIMER: 'AGREE_TO_DISCLAIMER', agreeToDisclaimer: agreeToDisclaimer, @@ -591,6 +592,17 @@ function clearNotices () { } } +function markAccountsFound() { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + background.markAccountsFound((err, newState) => { + dispatch(this.hideLoadingIndication()) + if (err) return dispatch(this.showWarning(err.message)) + dispatch(actions.updateMetamaskState(newState)) + }) + } +} + // // config // diff --git a/ui/app/app.js b/ui/app/app.js index 2fa6415dd..6da0c3b1d 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -16,7 +16,8 @@ const AccountDetailScreen = require('./account-detail') const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') // notice -const NoticeScreen = require('./notice') +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // other views const ConfigScreen = require('./config') const InfoScreen = require('./info') @@ -55,6 +56,8 @@ function mapStateToProps (state) { network: state.metamask.network, provider: state.metamask.provider, forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, } } @@ -366,8 +369,19 @@ App.prototype.renderPrimary = function () { } } + // notices if (!props.noActiveNotices) { - return h(NoticeScreen, {key: 'NoticeScreen'}) + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) } // show current view diff --git a/ui/app/notice.js b/ui/app/components/notice.js similarity index 81% rename from ui/app/notice.js rename to ui/app/components/notice.js index 3c2c746f2..00db734d7 100644 --- a/ui/app/notice.js +++ b/ui/app/components/notice.js @@ -2,18 +2,10 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const ReactMarkdown = require('react-markdown') -const connect = require('react-redux').connect -const actions = require('./actions') const linker = require('extension-link-enabler') const findDOMNode = require('react-dom').findDOMNode -module.exports = connect(mapStateToProps)(Notice) - -function mapStateToProps (state) { - return { - lastUnreadNotice: state.metamask.lastUnreadNotice, - } -} +module.exports = Notice inherits(Notice, Component) function Notice () { @@ -21,9 +13,8 @@ function Notice () { } Notice.prototype.render = function () { - const props = this.props - const title = props.lastUnreadNotice.title - const date = props.lastUnreadNotice.date + const { notice, onConfirm } = this.props + const { title, date, body } = notice return ( h('.flex-column.flex-center.flex-grow', [ @@ -59,6 +50,7 @@ Notice.prototype.render = function () { .markdown { overflow-x: hidden; } + .markdown h1, .markdown h2, .markdown h3 { margin: 10px 0; font-weight: bold; @@ -92,13 +84,13 @@ Notice.prototype.render = function () { }, }, [ h(ReactMarkdown, { - source: props.lastUnreadNotice.body, + source: body, skipHtml: true, }), ]), h('button', { - onClick: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + onClick: onConfirm, style: { marginTop: '18px', }, diff --git a/ui/lib/lost-accounts-notice.js b/ui/lib/lost-accounts-notice.js new file mode 100644 index 000000000..948b13db6 --- /dev/null +++ b/ui/lib/lost-accounts-notice.js @@ -0,0 +1,23 @@ +const summary = require('../app/util').addressSummary + +module.exports = function (lostAccounts) { + return { + date: new Date().toDateString(), + title: 'Account Problem Caught', + body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! + +We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. + +We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. + +Your affected accounts are: +${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} + +These accounts have been marked as "Loose" so they will be easy to recognize in the account list. + +For more information, please read [our blog post.][1] + +[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 + `, + } +}