diff options
47 files changed, 639 insertions, 129 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf23ccbe..19287f046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Current Master +## 4.7.4 Tue Jun 05 2018 + +- Add diagnostic reporting for users with multiple HD keyrings +- Throw explicit error when selected account is unset + +## 4.7.3 Mon Jun 04 2018 + +- Hide token now uses new modal +- Indicate the current selected account on the popup account view +- Reduce height of notice container in onboarding +- Fixes issue where old nicknames were kept around causing errors + ## 4.7.2 Sun Jun 03 2018 - Fix bug preventing users from logging in. Internally accounts and identities were out of sync. diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 3a664ec00..75deeaddf 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -62,6 +62,9 @@ "message": " $1以上 $2以下にして下さい。", "description": "helper for inputting hex as decimal input" }, + "blockiesIdenticon": { + "message": "Blockies Identicon を使用" + }, "borrowDharma": { "message": "Dharmaで借りる(ベータ版)" }, @@ -95,6 +98,9 @@ "confirmTransaction": { "message": "トランザクションの確認" }, + "continue": { + "message": "続行" + }, "continueToCoinbase": { "message": "Coinbaseを開く" }, @@ -359,6 +365,9 @@ "likeToAddTokens": { "message": "トークンを追加しますか?" }, + "links": { + "message": "リンク" + }, "limit": { "message": "リミット" }, @@ -371,12 +380,18 @@ "localhost": { "message": "Localhost 8545" }, + "login": { + "message": "ログイン" + }, "logout": { "message": "ログアウト" }, "loose": { "message": "外部秘密鍵" }, + "max": { + "message": "最大" + }, "mainnet": { "message": "Ethereumメインネットワーク" }, @@ -417,7 +432,7 @@ "message": "新規コントラクト" }, "newPassword": { - "message": "新規パスワード(最低8文字)" + "message": "新規パスワード(最低8文字)" }, "newRecipient": { "message": "新規受取人" @@ -453,6 +468,9 @@ "message": "または", "description": "choice between creating or importing a new account" }, + "password": { + "message": "パスワード" + }, "passwordMismatch": { "message": "パスワードが一致しません。", "description": "in password creation process, the two new password fields did not match" @@ -474,6 +492,9 @@ "popularTokens": { "message": "人気のトークン" }, + "privacyMsg": { + "message": "プライバシーポリシー" + }, "privateKey": { "message": "秘密鍵", "description": "select this type of file to use to import an account" @@ -546,6 +567,12 @@ "message": "ファイルとして保存", "description": "Account export process" }, + "search": { + "message": "検索" + }, + "searchResults": { + "message": "検索結果" + }, "selectService": { "message": "サービスを選択" }, @@ -575,7 +602,7 @@ }, "info": { "message": "情報" - }, + }, "shapeshiftBuy": { "message": "Shapeshiftで交換" }, @@ -609,6 +636,9 @@ "takesTooLong": { "message": "送信に時間がかかりますか?" }, + "terms": { + "message": "利用規約" + }, "testFaucet": { "message": "Faucetをテスト" }, @@ -619,6 +649,9 @@ "message": "ShapeShiftで $1をETHにする", "description": "system will fill in deposit type in start of message" }, + "token": { + "message": "トークン" + }, "tokenAddress": { "message": "トークンアドレス" }, @@ -690,6 +723,12 @@ "warning": { "message": "警告" }, + "welcomeBack": { + "message": "おかえりなさい!" + }, + "welcomeBeta": { + "message": "MetaMask ベータ版へようこそ!" + }, "whatsThis": { "message": "この機能について" }, diff --git a/app/manifest.json b/app/manifest.json index c1f26d2ea..e3a7fd963 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.7.2", + "version": "4.7.4", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 760868ddf..8411e3a28 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -2,6 +2,7 @@ const ObservableStore = require('obs-store') const normalizeAddress = require('eth-sig-util').normalize const extend = require('xtend') + class PreferencesController { /** @@ -28,7 +29,11 @@ class PreferencesController { featureFlags: {}, currentLocale: opts.initLangCode, identities: {}, + lostIdentities: {}, }, opts.initState) + + this.diagnostics = opts.diagnostics + this.store = new ObservableStore(initState) } // PUBLIC METHODS @@ -98,6 +103,50 @@ class PreferencesController { this.store.updateState({ identities }) } + /* + * Synchronizes identity entries with known accounts. + * Removes any unknown identities, and returns the resulting selected address. + * + * @param {Array<string>} addresses known to the vault. + * @returns {Promise<string>} selectedAddress the selected address. + */ + syncAddresses (addresses) { + let { identities, lostIdentities } = this.store.getState() + + let newlyLost = {} + Object.keys(identities).forEach((identity) => { + if (!addresses.includes(identity)) { + newlyLost[identity] = identities[identity] + delete identities[identity] + } + }) + + // Identities are no longer present. + if (Object.keys(newlyLost).length > 0) { + + // Notify our servers: + if (this.diagnostics) this.diagnostics.reportOrphans(newlyLost) + + // store lost accounts + for (let key in newlyLost) { + lostIdentities[key] = newlyLost[key] + } + } + + this.store.updateState({ identities, lostIdentities }) + this.addAddresses(addresses) + + // If the selected account is no longer valid, + // select an arbitrary other account: + let selected = this.getSelectedAddress() + if (!addresses.includes(selected)) { + selected = addresses[0] + this.setSelectedAddress(selected) + } + + return selected + } + /** * Setter for the `selectedAddress` property * @@ -198,6 +247,7 @@ class PreferencesController { * @return {Promise<string>} */ setAccountLabel (account, label) { + if (!account) throw new Error('setAccountLabel requires a valid address, got ' + String(account)) const address = normalizeAddress(account) const {identities} = this.store.getState() identities[address] = identities[address] || {} diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index aff5db984..b53947e27 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -10,6 +10,7 @@ const NonceTracker = require('./nonce-tracker') const txUtils = require('./lib/util') const cleanErrorStack = require('../../lib/cleanErrorStack') const log = require('loglevel') +const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker') /** Transaction Controller is an aggregate of sub-controllers and trackers @@ -157,8 +158,11 @@ class TransactionController extends EventEmitter { let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) - // add default tx params + try { + // check whether recipient account is blacklisted + recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) + // add default tx params txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { console.log(error) diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js new file mode 100644 index 000000000..84c6df1f0 --- /dev/null +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -0,0 +1,24 @@ +const Config = require('./recipient-blacklist-config.json') + +/** @module*/ +module.exports = { + checkAccount, +} + +/** + * Checks if a specified account on a specified network is blacklisted. + @param networkId {number} + @param account {string} +*/ +function checkAccount (networkId, account) { + + const mainnetId = 1 + if (networkId !== mainnetId) { + return + } + + const accountToCheck = account.toLowerCase() + if (Config.blacklist.includes(accountToCheck)) { + throw new Error('Recipient is a public account') + } +} diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-config.json b/app/scripts/controllers/transactions/lib/recipient-blacklist-config.json new file mode 100644 index 000000000..b348eb72e --- /dev/null +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-config.json @@ -0,0 +1,14 @@ +{ + "blacklist": [ + "0x627306090abab3a6e1400e9345bc60c78a8bef57", + "0xf17f52151ebef6c7334fad080c5704d77216b732", + "0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", + "0x821aea9a577a9b44299b9c15c88cf3087f3b5544", + "0x0d1d4e623d10f9fba5db95830f7d3839406c6af2", + "0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e", + "0x2191ef87e392377ec08e7c08eb105ef5448eced5", + "0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5", + "0x6330a553fc93768f612722bb8c2ec78ac90b3bbc", + "0x5aeda56215b167893e80b4fe645ba6d5bab767de" + ] +} diff --git a/app/scripts/lib/diagnostics-reporter.js b/app/scripts/lib/diagnostics-reporter.js new file mode 100644 index 000000000..aa4ca6e26 --- /dev/null +++ b/app/scripts/lib/diagnostics-reporter.js @@ -0,0 +1,71 @@ +class DiagnosticsReporter { + + constructor ({ firstTimeInfo, version }) { + this.firstTimeInfo = firstTimeInfo + this.version = version + } + + async reportOrphans(orphans) { + try { + return await this.submit({ + accounts: Object.keys(orphans), + metadata: { + type: 'orphans', + }, + }) + } catch (err) { + console.error('DiagnosticsReporter - "reportOrphans" encountered an error:') + console.error(err) + } + } + + async reportMultipleKeyrings(rawKeyrings) { + try { + const keyrings = await Promise.all(rawKeyrings.map(async (keyring, index) => { + return { + index, + type: keyring.type, + accounts: await keyring.getAccounts(), + } + })) + return await this.submit({ + accounts: [], + metadata: { + type: 'keyrings', + keyrings, + }, + }) + } catch (err) { + console.error('DiagnosticsReporter - "reportMultipleKeyrings" encountered an error:') + console.error(err) + } + } + + async submit (message) { + try { + // add metadata + message.metadata.version = this.version + message.metadata.firstTimeInfo = this.firstTimeInfo + return await postData(message) + } catch (err) { + console.error('DiagnosticsReporter - "submit" encountered an error:') + throw err + } + } + +} + +function postData(data) { + const uri = 'https://diagnostics.metamask.io/v1/orphanedAccounts' + return fetch(uri, { + body: JSON.stringify(data), // must match 'Content-Type' header + credentials: 'same-origin', // include, same-origin, *omit + headers: { + 'content-type': 'application/json', + }, + method: 'POST', // *GET, POST, PUT, DELETE, etc. + mode: 'cors', // no-cors, cors, *same-origin + }) +} + +module.exports = DiagnosticsReporter diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 96f976568..1bb0af5ee 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -46,6 +46,7 @@ const GWEI_BN = new BN('1000000000') const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const cleanErrorStack = require('./lib/cleanErrorStack') +const DiagnosticsReporter = require('./lib/diagnostics-reporter') const log = require('loglevel') module.exports = class MetamaskController extends EventEmitter { @@ -64,6 +65,12 @@ module.exports = class MetamaskController extends EventEmitter { const initState = opts.initState || {} this.recordFirstTimeInfo(initState) + // metamask diagnostics reporter + this.diagnostics = opts.diagnostics || new DiagnosticsReporter({ + firstTimeInfo: initState.firstTimeInfo, + version, + }) + // platform-specific api this.platform = opts.platform @@ -85,6 +92,7 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, + diagnostics: this.diagnostics, }) // currency controller @@ -356,7 +364,7 @@ module.exports = class MetamaskController extends EventEmitter { importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), // vault management - submitPassword: nodeify(keyringController.submitPassword, keyringController), + submitPassword: nodeify(this.submitPassword, this), // network management setProviderType: nodeify(networkController.setProviderType, networkController), @@ -474,6 +482,28 @@ module.exports = class MetamaskController extends EventEmitter { } } + /* + * Submits the user's password and attempts to unlock the vault. + * Also synchronizes the preferencesController, to ensure its schema + * is up to date with known accounts once the vault is decrypted. + * + * @param {string} password - The user's password + * @returns {Promise<object>} - The keyringController update. + */ + async submitPassword (password) { + await this.keyringController.submitPassword(password) + const accounts = await this.keyringController.getAccounts() + + // verify keyrings + const nonSimpleKeyrings = this.keyringController.keyrings.filter(keyring => keyring.type !== 'Simple Key Pair') + if (nonSimpleKeyrings.length > 1 && this.diagnostics) { + await this.diagnostics.reportMultipleKeyrings(nonSimpleKeyrings) + } + + await this.preferencesController.syncAddresses(accounts) + return this.keyringController.fullUpdate() + } + /** * @type Identity * @property {string} name - The account nickname. diff --git a/development/states/add-token.json b/development/states/add-token.json index 9c0f16372..84ad5dd4c 100644 --- a/development/states/add-token.json +++ b/development/states/add-token.json @@ -75,9 +75,9 @@ { "type": "HD Key Tree", "accounts": [ - "fdea65c8e26263f6d9a1b5de9555d2931a33b825", - "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "2f8d4a878cfa04a6e60d46362f5644deab66572d" + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" ] }, { diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index b51003d11..3c9caafb0 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -115,9 +115,9 @@ { "type": "HD Key Tree", "accounts": [ - "fdea65c8e26263f6d9a1b5de9555d2931a33b825", - "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "2f8d4a878cfa04a6e60d46362f5644deab66572d" + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" ] }, { diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 302e24c11..8c8b18a91 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -76,9 +76,9 @@ { "type": "HD Key Tree", "accounts": [ - "fdea65c8e26263f6d9a1b5de9555d2931a33b825", - "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "2f8d4a878cfa04a6e60d46362f5644deab66572d" + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" ] }, { diff --git a/development/states/send-edit.json b/development/states/send-edit.json index ae3098ecb..0d4c27116 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -94,9 +94,9 @@ { "type": "HD Key Tree", "accounts": [ - "fdea65c8e26263f6d9a1b5de9555d2931a33b825", - "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "2f8d4a878cfa04a6e60d46362f5644deab66572d" + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" ] }, { diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index 1297a9139..787f8023e 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -76,9 +76,9 @@ { "type": "HD Key Tree", "accounts": [ - "fdea65c8e26263f6d9a1b5de9555d2931a33b825", - "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "2f8d4a878cfa04a6e60d46362f5644deab66572d" + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" ] }, { diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index d567e3fed..ee787aaee 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -83,9 +83,9 @@ { "type": "HD Key Tree", "accounts": [ - "fdea65c8e26263f6d9a1b5de9555d2931a33b825", - "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", - "2f8d4a878cfa04a6e60d46362f5644deab66572d" + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" ] }, { diff --git a/mascara/src/app/first-time/index.css b/mascara/src/app/first-time/index.css index 25e60b84a..09e7d378d 100644 --- a/mascara/src/app/first-time/index.css +++ b/mascara/src/app/first-time/index.css @@ -123,10 +123,6 @@ width: calc(100vw - 80px); } - .unique-image { - width: auto; - } - .create-password__title, .unique-image__title, .tou__title, @@ -148,7 +144,7 @@ height: 100%; flex-direction: column; align-items: center; - justify-content: space-evenly; + justify-content: flex-start; margin-top: 12px; } @@ -181,7 +177,6 @@ margin: 0 !important; padding: 16px 20px !important; height: 30vh !important; - width: calc(100% - 48px) !important; } .backup-phrase__content-wrapper { @@ -280,6 +275,12 @@ width: 335px; } +@media only screen and (max-width: 575px) { + .unique-image__body-text { + width: initial; + } +} + .unique-image__body-text + .unique-image__body-text, .backup-phrase__body-text + @@ -294,7 +295,7 @@ border-radius: 8px; background-color: #FFFFFF; margin: 0 142px 0 0; - height: 334px; + height: 200px; overflow-y: auto; color: #757575; font-family: Roboto; @@ -679,7 +680,7 @@ button.backup-phrase__confirm-seed-option:hover { } .first-time-flow__input { - width: 350px; + max-width: 350px; } .first-time-flow__button { diff --git a/old-ui/app/components/account-dropdowns.js b/old-ui/app/components/account-dropdowns.js index 53468a1a1..262de6601 100644 --- a/old-ui/app/components/account-dropdowns.js +++ b/old-ui/app/components/account-dropdowns.js @@ -23,9 +23,10 @@ class AccountDropdowns extends Component { renderAccounts () { const { identities, selected, keyrings } = this.props + const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) - return Object.keys(identities).map((key, index) => { - const identity = identities[key] + return accountOrder.map((address, index) => { + const identity = identities[address] const isSelected = identity.address === selected const simpleAddress = identity.address.substring(2).toLowerCase() diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 4bc16e65e..0dda4609b 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -45,7 +45,7 @@ describe('MetaMaskController', function () { encryptor: { encrypt: function (password, object) { this.object = object - return Promise.resolve() + return Promise.resolve('mock-encrypted') }, decrypt: function () { return Promise.resolve(this.object) @@ -53,6 +53,9 @@ describe('MetaMaskController', function () { }, initState: clone(firstTimeState), }) + // disable diagnostics + metamaskController.diagnostics = null + // add sinon method spies sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') }) @@ -62,6 +65,31 @@ describe('MetaMaskController', function () { sandbox.restore() }) + describe('submitPassword', function () { + const password = 'password' + + beforeEach(async function () { + await metamaskController.createNewVaultAndKeychain(password) + }) + + it('removes any identities that do not correspond to known accounts.', async function () { + const fakeAddress = '0xbad0' + metamaskController.preferencesController.addAddresses([fakeAddress]) + await metamaskController.submitPassword(password) + + const identities = Object.keys(metamaskController.preferencesController.store.getState().identities) + const addresses = await metamaskController.keyringController.getAccounts() + + identities.forEach((identity) => { + assert.ok(addresses.includes(identity), `addresses should include all IDs: ${identity}`) + }) + + addresses.forEach((address) => { + assert.ok(identities.includes(address), `identities should include all Addresses: ${address}`) + }) + }) + }) + describe('#getGasPrice', function () { it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { @@ -479,7 +507,7 @@ describe('MetaMaskController', function () { it('errors when signing a message', async function () { await metamaskController.signPersonalMessage(personalMessages[0].msgParams) assert.equal(metamaskPersonalMsgs[msgId].status, 'signed') - assert.equal(metamaskPersonalMsgs[msgId].rawSig, '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b') + assert.equal(metamaskPersonalMsgs[msgId].rawSig, '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b') }) }) @@ -513,7 +541,7 @@ describe('MetaMaskController', function () { }) it('sets up controller dnode api for trusted communication', function (done) { - streamTest = createThoughStream((chunk, enc, cb) => { + streamTest = createThoughStream((chunk, enc, cb) => { assert.equal(chunk.name, 'controller') cb() done() diff --git a/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js new file mode 100644 index 000000000..56e8d50db --- /dev/null +++ b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js @@ -0,0 +1,77 @@ +const assert = require('assert') +const recipientBlackListChecker = require('../../../../../app/scripts/controllers/transactions/lib/recipient-blacklist-checker') +const { + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, +} = require('../../../../../app/scripts/controllers/network/enums') + +const KeyringController = require('eth-keyring-controller') + +describe('Recipient Blacklist Checker', function () { + + let publicAccounts + + before(async function () { + const damnedMnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' + const keyringController = new KeyringController({}) + const Keyring = keyringController.getKeyringClassForType('HD Key Tree') + const opts = { + mnemonic: damnedMnemonic, + numberOfAccounts: 10, + } + const keyring = new Keyring(opts) + publicAccounts = await keyring.getAccounts() + }) + + describe('#checkAccount', function () { + it('does not fail on test networks', function () { + let callCount = 0 + const networks = [ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE] + for (let networkId in networks) { + publicAccounts.forEach((account) => { + recipientBlackListChecker.checkAccount(networkId, account) + callCount++ + }) + } + assert.equal(callCount, 30) + }) + + it('fails on mainnet', function () { + const mainnetId = 1 + let callCount = 0 + publicAccounts.forEach((account) => { + try { + recipientBlackListChecker.checkAccount(mainnetId, account) + assert.fail('function should have thrown an error') + } catch (err) { + assert.equal(err.message, 'Recipient is a public account') + } + callCount++ + }) + assert.equal(callCount, 10) + }) + + it('fails for public account - uppercase', function () { + const mainnetId = 1 + const publicAccount = '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2' + try { + recipientBlackListChecker.checkAccount(mainnetId, publicAccount) + assert.fail('function should have thrown an error') + } catch (err) { + assert.equal(err.message, 'Recipient is a public account') + } + }) + + it('fails for public account - lowercase', async function () { + const mainnetId = 1 + const publicAccount = '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2' + try { + await recipientBlackListChecker.checkAccount(mainnetId, publicAccount) + assert.fail('function should have thrown an error') + } catch (err) { + assert.equal(err.message, 'Recipient is a public account') + } + }) + }) +}) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 1f32a0f37..9bdfe7c1a 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -185,6 +185,23 @@ describe('Transaction Controller', function () { .catch(done) }) + it('should fail if recipient is public', function (done) { + txController.networkStore = new ObservableStore(1) + txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d', to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2' }) + .catch((err) => { + if (err.message === 'Recipient is a public account') done() + else done(err) + }) + }) + + it('should not fail if recipient is public but not on mainnet', function (done) { + txController.once('newUnapprovedTx', (txMetaFromEmit) => { + assert(txMetaFromEmit, 'txMeta is falsey') + done() + }) + txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d', to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2' }) + .catch(done) + }) }) describe('#addTxGasDefaults', function () { diff --git a/ui/app/actions.js b/ui/app/actions.js index a9372d6f3..07d7bd5d1 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -550,10 +550,12 @@ function importNewAccount (strategy, args) { } dispatch(actions.hideLoadingIndication()) dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) + if (newState.selectedAddress) { + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + } return newState } } diff --git a/ui/app/app.js b/ui/app/app.js index 0e8b907df..7005adb7f 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -99,7 +99,7 @@ class App extends Component { } = this.props const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? - this.getConnectingLabel() : null + this.getConnectingLabel(loadingMessage) : null log.debug('Main ui render function') return ( @@ -210,7 +210,10 @@ class App extends Component { } } - getConnectingLabel = function () { + getConnectingLabel = function (loadingMessage) { + if (loadingMessage) { + return loadingMessage + } const { provider } = this.props const providerName = provider.type diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 7638995ea..f34631ca8 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -135,11 +135,12 @@ AccountMenu.prototype.renderAccounts = function () { showAccountDetail, } = this.props - return Object.keys(identities).map((key, index) => { - const identity = identities[key] + const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) + return accountOrder.map((address) => { + const identity = identities[address] const isSelected = identity.address === selectedAddress - const balanceValue = accounts[key] ? accounts[key].balance : '' + const balanceValue = accounts[address] ? accounts[address].balance : '' const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index e69acff63..351640f6e 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,5 +1,7 @@ @import './export-text-container/index'; +@import './selected-account/index'; + @import './info-box/index'; @import './pages/index'; diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js index 5681a3cad..edced8725 100644 --- a/ui/app/components/modals/edit-account-name-modal.js +++ b/ui/app/components/modals/edit-account-name-modal.js @@ -9,7 +9,7 @@ const { getSelectedAccount } = require('../../selectors') function mapStateToProps (state) { return { selectedAccount: getSelectedAccount(state), - identity: state.appState.modal.modalState.identity, + identity: state.appState.modal.modalState.props.identity, } } diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js index 72e9c84eb..1518fa9a0 100644 --- a/ui/app/components/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/modals/hide-token-confirmation-modal.js @@ -9,7 +9,7 @@ const Identicon = require('../identicon') function mapStateToProps (state) { return { network: state.metamask.network, - token: state.appState.modal.modalState.token, + token: state.appState.modal.modalState.props.token, } } diff --git a/ui/app/components/modals/shapeshift-deposit-tx-modal.js b/ui/app/components/modals/shapeshift-deposit-tx-modal.js index 24af5a0de..242c7b89d 100644 --- a/ui/app/components/modals/shapeshift-deposit-tx-modal.js +++ b/ui/app/components/modals/shapeshift-deposit-tx-modal.js @@ -8,7 +8,7 @@ const AccountModalContainer = require('./account-modal-container') function mapStateToProps (state) { return { - Qr: state.appState.modal.modalState.Qr, + Qr: state.appState.modal.modalState.props.Qr, } } diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js index 1f4b37b53..bcb93d401 100644 --- a/ui/app/components/pages/add-token/add-token.component.js +++ b/ui/app/components/pages/add-token/add-token.component.js @@ -231,7 +231,7 @@ class AddToken extends Component { <div className="add-token__custom-token-form"> <TextField id="custom-address" - label="Token Address" + label={this.context.t('tokenAddress')} type="text" value={customAddress} onChange={e => this.handleCustomAddressChange(e.target.value)} @@ -241,7 +241,7 @@ class AddToken extends Component { /> <TextField id="custom-symbol" - label="Token Symbol" + label={this.context.t('tokenSymbol')} type="text" value={customSymbol} onChange={e => this.handleCustomSymbolChange(e.target.value)} @@ -252,7 +252,7 @@ class AddToken extends Component { /> <TextField id="custom-decimals" - label="Decimals of Precision" + label={this.context.t('decimal')} type="number" value={customDecimals} onChange={e => this.handleCustomDecimalsChange(e.target.value)} diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js index 0164417ec..1dc2ba534 100644 --- a/ui/app/components/pages/create-account/import-account/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -82,18 +82,19 @@ class JsonImportSubview extends Component { } createNewKeychain () { + const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress } = this.props const state = this.state if (!state) { const message = this.context.t('validFileImport') - return this.props.displayWarning(message) + return displayWarning(message) } const { fileContents } = state if (!fileContents) { const message = this.context.t('needImportFile') - return this.props.displayWarning(message) + return displayWarning(message) } const passwordInput = document.getElementById('json-password-box') @@ -101,12 +102,19 @@ class JsonImportSubview extends Component { if (!password) { const message = this.context.t('needImportPassword') - return this.props.displayWarning(message) + return displayWarning(message) } - this.props.importNewJsonAccount([ fileContents, password ]) - // JS runtime requires caught rejections but failures are handled by Redux - .catch() + importNewJsonAccount([ fileContents, password ]) + .then(({ selectedAddress }) => { + if (selectedAddress) { + history.push(DEFAULT_ROUTE) + } else { + displayWarning('Error importing account.') + setSelectedAddress(firstAddress) + } + }) + .catch(err => displayWarning(err)) } } @@ -114,14 +122,17 @@ JsonImportSubview.propTypes = { error: PropTypes.string, goHome: PropTypes.func, displayWarning: PropTypes.func, + firstAddress: PropTypes.string, importNewJsonAccount: PropTypes.func, history: PropTypes.object, + setSelectedAddress: PropTypes.func, t: PropTypes.func, } const mapStateToProps = state => { return { error: state.appState.warning, + firstAddress: Object.keys(state.metamask.accounts)[0], } } @@ -130,6 +141,7 @@ const mapDispatchToProps = dispatch => { goHome: () => dispatch(actions.goHome()), displayWarning: warning => dispatch(actions.displayWarning(warning)), importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)), + setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)), } } diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js index e55a47a3c..5df3777da 100644 --- a/ui/app/components/pages/create-account/import-account/private-key.js +++ b/ui/app/components/pages/create-account/import-account/private-key.js @@ -21,6 +21,7 @@ module.exports = compose( function mapStateToProps (state) { return { error: state.appState.warning, + firstAddress: Object.keys(state.metamask.accounts)[0], } } @@ -29,7 +30,8 @@ function mapDispatchToProps (dispatch) { importNewAccount: (strategy, [ privateKey ]) => { return dispatch(actions.importNewAccount(strategy, [ privateKey ])) }, - displayWarning: () => dispatch(actions.displayWarning(null)), + displayWarning: (message) => dispatch(actions.displayWarning(message || null)), + setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)), } } @@ -40,7 +42,7 @@ function PrivateKeyImportView () { } PrivateKeyImportView.prototype.render = function () { - const { error } = this.props + const { error, displayWarning } = this.props return ( h('div.new-account-import-form__private-key', [ @@ -60,7 +62,10 @@ PrivateKeyImportView.prototype.render = function () { h('div.new-account-import-form__buttons', {}, [ h('button.btn-default.btn--large.new-account-create-form__button', { - onClick: () => this.props.history.push(DEFAULT_ROUTE), + onClick: () => { + displayWarning(null) + this.props.history.push(DEFAULT_ROUTE) + }, }, [ this.context.t('cancel'), ]), @@ -88,10 +93,16 @@ PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { PrivateKeyImportView.prototype.createNewKeychain = function () { const input = document.getElementById('private-key-box') const privateKey = input.value - const { importNewAccount, history } = this.props + const { importNewAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props importNewAccount('Private Key', [ privateKey ]) - // JS runtime requires caught rejections but failures are handled by Redux - .catch() - .then(() => history.push(DEFAULT_ROUTE)) + .then(({ selectedAddress }) => { + if (selectedAddress) { + history.push(DEFAULT_ROUTE) + } else { + displayWarning('Error importing account.') + setSelectedAddress(firstAddress) + } + }) + .catch(err => displayWarning(err)) } diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js index 475261253..6e3b93742 100644 --- a/ui/app/components/pages/create-account/index.js +++ b/ui/app/components/pages/create-account/index.js @@ -22,7 +22,9 @@ class CreateAccountPage extends Component { }), }), onClick: () => history.push(NEW_ACCOUNT_ROUTE), - }, 'Create'), + }, [ + this.context.t('create'), + ]), h('div.new-account__tabs__tab', { className: classnames('new-account__tabs__tab', { @@ -31,14 +33,16 @@ class CreateAccountPage extends Component { }), }), onClick: () => history.push(IMPORT_ACCOUNT_ROUTE), - }, 'Import'), + }, [ + this.context.t('import'), + ]), ]) } render () { return h('div.new-account', {}, [ h('div.new-account__header', [ - h('div.new-account__title', 'New Account'), + h('div.new-account__title', this.context.t('newAccount') ), this.renderTabs(), ]), h('div.new-account__form', [ @@ -62,6 +66,11 @@ class CreateAccountPage extends Component { CreateAccountPage.propTypes = { location: PropTypes.object, history: PropTypes.object, + t: PropTypes.func, +} + +CreateAccountPage.contextTypes = { + t: PropTypes.func, } const mapStateToProps = state => ({ diff --git a/ui/app/components/pages/settings/index.js b/ui/app/components/pages/settings/index.js index 384ae4b41..aee17e0e8 100644 --- a/ui/app/components/pages/settings/index.js +++ b/ui/app/components/pages/settings/index.js @@ -14,8 +14,8 @@ class Config extends Component { return h('div.settings__tabs', [ h(TabBar, { tabs: [ - { content: 'Settings', key: SETTINGS_ROUTE }, - { content: 'Info', key: INFO_ROUTE }, + { content: this.context.t('settings'), key: SETTINGS_ROUTE }, + { content: this.context.t('info'), key: INFO_ROUTE }, ], isActive: key => matchPath(location.pathname, { path: key, exact: true }), onSelect: key => history.push(key), @@ -54,6 +54,11 @@ class Config extends Component { Config.propTypes = { location: PropTypes.object, history: PropTypes.object, + t: PropTypes.func, +} + +Config.contextTypes = { + t: PropTypes.func, } module.exports = Config diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js index 8bc3897da..a1d3f9181 100644 --- a/ui/app/components/pages/unlock-page/unlock-page.component.js +++ b/ui/app/components/pages/unlock-page/unlock-page.component.js @@ -120,7 +120,7 @@ class UnlockPage extends Component { > <TextField id="password" - label="Password" + label={this.context.t('password')} type="password" value={this.state.password} onChange={event => this.handleInputChange(event)} diff --git a/ui/app/components/selected-account/index.js b/ui/app/components/selected-account/index.js new file mode 100644 index 000000000..eb342181f --- /dev/null +++ b/ui/app/components/selected-account/index.js @@ -0,0 +1,2 @@ +import SelectedAccount from './selected-account.container' +module.exports = SelectedAccount diff --git a/ui/app/components/selected-account/index.scss b/ui/app/components/selected-account/index.scss new file mode 100644 index 000000000..5339a228b --- /dev/null +++ b/ui/app/components/selected-account/index.scss @@ -0,0 +1,38 @@ +.selected-account { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 1; + + &__name { + max-width: 200px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + text-align: center; + } + + &__address { + font-size: .75rem; + color: $silver-chalice; + } + + &__clickable { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5px 15px; + border-radius: 10px; + cursor: pointer; + + &:hover { + background-color: #e8e6e8; + } + + &:active { + background-color: #d9d7da; + } + } +} diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js new file mode 100644 index 000000000..3386a4196 --- /dev/null +++ b/ui/app/components/selected-account/selected-account.component.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import copyToClipboard from 'copy-to-clipboard' + +const Tooltip = require('../tooltip-v2.js') + +const addressStripper = (address = '') => { + if (address.length < 4) { + return address + } + + return `${address.slice(0, 4)}...${address.slice(-4)}` +} + +class SelectedAccount extends Component { + state = { + copied: false, + } + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + selectedAddress: PropTypes.string, + selectedIdentity: PropTypes.object, + } + + render () { + const { t } = this.context + const { selectedAddress, selectedIdentity } = this.props + + return ( + <div className="selected-account"> + <Tooltip + position="bottom" + title={this.state.copied ? t('copiedExclamation') : t('copyToClipboard')} + > + <div + className="selected-account__clickable" + onClick={() => { + this.setState({ copied: true }) + setTimeout(() => this.setState({ copied: false }), 3000) + copyToClipboard(selectedAddress) + }} + > + <div className="selected-account__name"> + { selectedIdentity.name } + </div> + <div className="selected-account__address"> + { addressStripper(selectedAddress) } + </div> + </div> + </Tooltip> + </div> + ) + } +} + +export default SelectedAccount diff --git a/ui/app/components/selected-account/selected-account.container.js b/ui/app/components/selected-account/selected-account.container.js new file mode 100644 index 000000000..f9e061d15 --- /dev/null +++ b/ui/app/components/selected-account/selected-account.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import SelectedAccount from './selected-account.component' + +const selectors = require('../../selectors') + +const mapStateToProps = state => { + return { + selectedAddress: selectors.getSelectedAddress(state), + selectedIdentity: selectors.getSelectedIdentity(state), + } +} + +export default connect(mapStateToProps)(SelectedAccount) diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index c84117d84..4100d76a5 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -101,8 +101,8 @@ TokenCell.prototype.render = function () { h('div.token-list-item__balance-ellipsis', null, [ h('div.token-list-item__balance-wrapper', null, [ - h('h3.token-list-item__token-balance', `${string || 0} ${symbol}`), - + h('div.token-list-item__token-balance', `${string || 0}`), + h('div.token-list-item__token-symbol', symbol), showFiat && h('div.token-list-item__fiat-amount', { style: {}, }, formattedFiat), diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index 263f992c0..014497fcd 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -12,7 +12,7 @@ const { checksumAddress: toChecksumAddress } = require('../util') const BalanceComponent = require('./balance-component') const TxList = require('./tx-list') -const Identicon = require('./identicon') +const SelectedAccount = require('./selected-account') module.exports = compose( withRouter, @@ -103,7 +103,7 @@ TxView.prototype.renderButtons = function () { } TxView.prototype.render = function () { - const { selectedAddress, identity, network, isMascara } = this.props + const { isMascara } = this.props return h('div.tx-view.flex-column', { style: {}, @@ -111,10 +111,12 @@ TxView.prototype.render = function () { h('div.flex-row.phone-visible', { style: { - justifyContent: 'space-between', + justifyContent: 'center', alignItems: 'center', flex: '0 0 auto', - margin: '10px', + marginBottom: '16px', + padding: '5px', + borderBottom: '1px solid #e5e5e5', }, }, [ @@ -127,23 +129,7 @@ TxView.prototype.render = function () { onClick: () => this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar(), }), - h('.identicon-wrapper.select-none', { - style: { - marginLeft: '0.9em', - }, - }, [ - h(Identicon, { - diameter: 24, - address: selectedAddress, - network, - }), - ]), - - h('span.account-name', { - style: {}, - }, [ - identity.name, - ]), + h(SelectedAccount), !isMascara && h('div.open-in-browser', { onClick: () => global.platform.openExtensionInBrowser(), diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 3b29dacac..da142fad8 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -36,7 +36,6 @@ function mapStateToProps (state) { tokens: state.metamask.tokens, keyrings: state.metamask.keyrings, selectedAddress: selectors.getSelectedAddress(state), - selectedIdentity: selectors.getSelectedIdentity(state), selectedAccount: selectors.getSelectedAccount(state), selectedTokenAddress: state.metamask.selectedTokenAddress, } @@ -99,21 +98,24 @@ WalletView.prototype.render = function () { const { responsiveDisplayClassname, selectedAddress, - selectedIdentity, keyrings, showAccountDetailModal, sidebarOpen, hideSidebar, history, + identities, } = this.props // temporary logs + fake extra wallets // console.log('walletview, selectedAccount:', selectedAccount) const checksummedAddress = checksumAddress(selectedAddress) + if (!selectedAddress) { + throw new Error('selectedAddress should not be ' + String(selectedAddress)) + } + const keyring = keyrings.find((kr) => { - return kr.accounts.includes(selectedAddress) || - kr.accounts.includes(selectedIdentity.address) + return kr.accounts.includes(selectedAddress) }) const type = keyring.type @@ -145,7 +147,7 @@ WalletView.prototype.render = function () { h('span.account-name', { style: {}, }, [ - selectedIdentity.name, + identities[selectedAddress].name, ]), h('button.btn-clear.wallet-view__details-button.allcaps', this.context.t('details')), diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 657760ab5..96fba890c 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -116,6 +116,10 @@ &__name { color: $white; font-size: 18px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 200px; } &__balance { diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss index 69cde8a0f..09d66aedd 100644 --- a/ui/app/css/itcss/components/hero-balance.scss +++ b/ui/app/css/itcss/components/hero-balance.scss @@ -6,6 +6,7 @@ justify-content: flex-start; align-items: center; flex: 0 0 auto; + padding-top: 16px; } @media screen and (min-width: $break-large) { diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index e8de317e3..72fda372f 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -14,10 +14,17 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( min-width: 0; &__token-balance { - font-size: 1.5rem; + margin-right: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + min-width: 0; + max-width: 100%; + } + + &__token-balance, &__token-symbol { + font-size: 1.5rem; + flex: 0 0 auto; @media #{$wallet-balance-breakpoint-range} { font-size: 95%; @@ -66,7 +73,9 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( } &__balance-wrapper { - flex: 1 1 auto; + flex: 1; + flex-flow: row wrap; + display: flex; min-width: 0; } } diff --git a/ui/app/reducers.js b/ui/app/reducers.js index f155b2bf3..e3a3077d9 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -4,7 +4,6 @@ const copyToClipboard = require('copy-to-clipboard') // // Sub-Reducers take in the complete state and return their sub-state // -const reduceIdentities = require('./reducers/identities') const reduceMetamask = require('./reducers/metamask') const reduceApp = require('./reducers/app') const reduceLocale = require('./reducers/locale') @@ -22,12 +21,6 @@ function rootReducer (state, action) { } // - // Identities - // - - state.identities = reduceIdentities(state, action) - - // // MetaMask // diff --git a/ui/app/reducers/identities.js b/ui/app/reducers/identities.js deleted file mode 100644 index 341a404e7..000000000 --- a/ui/app/reducers/identities.js +++ /dev/null @@ -1,15 +0,0 @@ -const extend = require('xtend') - -module.exports = reduceIdentities - -function reduceIdentities (state, action) { - // clone + defaults - var idState = extend({ - - }, state.identities) - - switch (action.type) { - default: - return idState - } -} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 4fbe8ff11..612f256df 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -224,7 +224,7 @@ SendTransactionScreen.prototype.renderFromRow = function () { return h('div.send-v2__form-row', [ - h('div.send-v2__form-label', 'From:'), + h('div.send-v2__form-label', this.context.t('from')), h('div.send-v2__form-field', [ h(FromDropdown, { @@ -396,7 +396,7 @@ SendTransactionScreen.prototype.renderAmountRow = function () { return h('div.send-v2__form-row', [ h('div.send-v2__form-label', [ - 'Amount:', + this.context.t('amount'), this.renderErrorMessage('amount'), !errors.amount && gasTotal && h('div.send-v2__amount-max', { onClick: (event) => { diff --git a/ui/app/welcome-screen.js b/ui/app/welcome-screen.js index 2fa244d9f..63512cd50 100644 --- a/ui/app/welcome-screen.js +++ b/ui/app/welcome-screen.js @@ -14,6 +14,11 @@ class WelcomeScreen extends Component { closeWelcomeScreen: PropTypes.func.isRequired, welcomeScreenSeen: PropTypes.bool, history: PropTypes.object, + t: PropTypes.func, + } + + static contextTypes = { + t: PropTypes.func, } constructor (props) { @@ -45,16 +50,15 @@ class WelcomeScreen extends Component { height: '225', }), - h('div.welcome-screen__info__header', 'Welcome to MetaMask Beta'), + h('div.welcome-screen__info__header', this.context.t('welcomeBeta')), - h('div.welcome-screen__info__copy', 'MetaMask is a secure identity vault for Ethereum.'), + h('div.welcome-screen__info__copy', this.context.t('metamaskDescription')), - h('div.welcome-screen__info__copy', `It allows you to hold ether & tokens, - and serves as your bridge to decentralized applications.`), + h('div.welcome-screen__info__copy', this.context.t('holdEther')), h('button.welcome-screen__button', { onClick: this.initiateAccountCreation, - }, 'Continue'), + }, this.context.t('continue')), ]), |