From afb578886134663506320e7462935d3431512a9a Mon Sep 17 00:00:00 2001 From: Csaba Solya Date: Wed, 30 May 2018 15:53:18 +0200 Subject: initial implementation --- app/scripts/controllers/transactions/index.js | 6 +++- .../lib/recipient-blacklist-checker.js | 36 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index aff5db984..3d6f5beb5 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 public + await 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..f6fbee678 --- /dev/null +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -0,0 +1,36 @@ +const KeyringController = require('eth-keyring-controller') + +/** @module*/ +module.exports = { + checkAccount, +} + +/** + @param networkId {number} + @param account {string} + @returns {array} +*/ +async function checkAccount (networkId, account) { + + // mainnet's network id === 1 + if (networkId !== 1) { + return + } + + 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 accountToCheck = account.toLowerCase() + const keyring = new Keyring(opts) + const damnedAccounts = await keyring.getAccounts() + for (let i = 0; i < damnedAccounts.length; i++) { + if (damnedAccounts[i].toLowerCase() === accountToCheck) { + throw new Error('this is a public account') + } + } +} \ No newline at end of file -- cgit From 6affd8f9492e04cdc81007e4f5390e4faa56499d Mon Sep 17 00:00:00 2001 From: Csaba Solya Date: Wed, 30 May 2018 16:24:40 +0200 Subject: adding transaction controller tests --- .../transactions/lib/recipient-blacklist-checker.js | 2 +- .../app/controllers/transactions/tx-controller-test.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js index f6fbee678..c52e58863 100644 --- a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -30,7 +30,7 @@ async function checkAccount (networkId, account) { const damnedAccounts = await keyring.getAccounts() for (let i = 0; i < damnedAccounts.length; i++) { if (damnedAccounts[i].toLowerCase() === accountToCheck) { - throw new Error('this is a public account') + throw new Error('Recipient is a public account') } } } \ No newline at end of file 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 () { -- cgit From cf73581c0e1a90371fb23eb05318ce39027325b5 Mon Sep 17 00:00:00 2001 From: Csaba Solya Date: Wed, 30 May 2018 17:38:27 +0200 Subject: adding tests for recipient blacklist checker --- app/scripts/controllers/transactions/index.js | 4 +- .../lib/recipient-blacklist-checker.js | 6 +- .../recipient-blacklist-checker-test.js | 78 ++++++++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 3d6f5beb5..16f7291d6 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -10,7 +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') +const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker') /** Transaction Controller is an aggregate of sub-controllers and trackers @@ -161,7 +161,7 @@ class TransactionController extends EventEmitter { try { // check whether recipient account is public - await recipientBlackListChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) + await recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) // add default tx params txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js index c52e58863..414302d12 100644 --- a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -12,8 +12,8 @@ module.exports = { */ async function checkAccount (networkId, account) { - // mainnet's network id === 1 - if (networkId !== 1) { + const mainnetId = 1 + if (networkId !== mainnetId) { return } @@ -33,4 +33,4 @@ async function checkAccount (networkId, account) { throw new Error('Recipient is a public account') } } -} \ No newline at end of file +} 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..b55894684 --- /dev/null +++ b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js @@ -0,0 +1,78 @@ +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', async function () { + let callCount = 0 + const networks = [ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE] + for (let networkId in networks) { + await Promise.all(publicAccounts.map(async (account) => { + await recipientBlackListChecker.checkAccount(networkId, account) + callCount++ + }) + ) + } + assert.equal(callCount, 30) + }) + + it('fails on mainnet', async function () { + const mainnetId = 1 + let callCount = 0 + await Promise.all(publicAccounts.map(async (account) => { + try { + await 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', 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') + } + }) + + 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') + } + }) + }) +}) -- cgit From 3e489ea16506569950c10fc3636071075b2495e8 Mon Sep 17 00:00:00 2001 From: Csaba Solya Date: Wed, 30 May 2018 17:42:41 +0200 Subject: fix documentation --- app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js index 414302d12..d44c1ddc1 100644 --- a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -6,9 +6,9 @@ module.exports = { } /** + * Checks if a specified account on a specified network is blacklisted. @param networkId {number} @param account {string} - @returns {array} */ async function checkAccount (networkId, account) { -- cgit From 1dda0c646940179bec6e886117a8ecf3f0f7ab48 Mon Sep 17 00:00:00 2001 From: Csaba Solya Date: Wed, 30 May 2018 21:15:59 +0200 Subject: remove generating blocked accounts and use a config file instead --- app/scripts/controllers/transactions/index.js | 4 ++-- .../transactions/lib/recipient-blacklist-checker.js | 20 ++++---------------- .../lib/recipient-blacklist-config.json | 14 ++++++++++++++ .../recipient-blacklist-checker-test.js | 21 ++++++++++----------- 4 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 app/scripts/controllers/transactions/lib/recipient-blacklist-config.json diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 16f7291d6..b53947e27 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -160,8 +160,8 @@ class TransactionController extends EventEmitter { this.emit('newUnapprovedTx', txMeta) try { - // check whether recipient account is public - await recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) + // check whether recipient account is blacklisted + recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) // add default tx params txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js index d44c1ddc1..84c6df1f0 100644 --- a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -1,4 +1,4 @@ -const KeyringController = require('eth-keyring-controller') +const Config = require('./recipient-blacklist-config.json') /** @module*/ module.exports = { @@ -10,27 +10,15 @@ module.exports = { @param networkId {number} @param account {string} */ -async function checkAccount (networkId, account) { +function checkAccount (networkId, account) { const mainnetId = 1 if (networkId !== mainnetId) { return } - 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 accountToCheck = account.toLowerCase() - const keyring = new Keyring(opts) - const damnedAccounts = await keyring.getAccounts() - for (let i = 0; i < damnedAccounts.length; i++) { - if (damnedAccounts[i].toLowerCase() === accountToCheck) { - throw new Error('Recipient is a public account') - } + 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/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js index b55894684..56e8d50db 100644 --- a/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js +++ b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js @@ -25,39 +25,38 @@ describe('Recipient Blacklist Checker', function () { }) describe('#checkAccount', function () { - it('does not fail on test networks', async 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) { - await Promise.all(publicAccounts.map(async (account) => { - await recipientBlackListChecker.checkAccount(networkId, account) - callCount++ + publicAccounts.forEach((account) => { + recipientBlackListChecker.checkAccount(networkId, account) + callCount++ }) - ) } assert.equal(callCount, 30) }) - it('fails on mainnet', async function () { + it('fails on mainnet', function () { const mainnetId = 1 let callCount = 0 - await Promise.all(publicAccounts.map(async (account) => { + publicAccounts.forEach((account) => { try { - await recipientBlackListChecker.checkAccount(mainnetId, account) + 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', async function () { + it('fails for public account - uppercase', function () { const mainnetId = 1 const publicAccount = '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2' try { - await recipientBlackListChecker.checkAccount(mainnetId, publicAccount) + recipientBlackListChecker.checkAccount(mainnetId, publicAccount) assert.fail('function should have thrown an error') } catch (err) { assert.equal(err.message, 'Recipient is a public account') -- cgit From 1c3d2aa18b85ddb83734d6afdbb1111ba0791229 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 30 May 2018 16:43:45 -0230 Subject: Importing account by json and private key shows error and does not change account if no selectedAddress comes after import. --- ui/app/actions.js | 10 ++++++---- ui/app/app.js | 7 +++++-- .../pages/create-account/import-account/json.js | 23 +++++++++++++++++----- .../create-account/import-account/private-key.js | 22 ++++++++++++++++----- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 649f740e9..ed3c147b8 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -544,10 +544,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/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js index 0a3314b2a..3dabad68e 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,20 @@ 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 + importNewJsonAccount([ fileContents, password ]) + // JS runtime requires caught rejections but failures are handled by Redux .catch() + .then(({ selectedAddress }) => { + if (selectedAddress) { + history.push(DEFAULT_ROUTE) + } else { + displayWarning('Error importing account.') + setSelectedAddress(firstAddress) + } + }) } } @@ -114,14 +123,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 +142,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 df7ac910a..e71e47647 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-secondary--lg.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,17 @@ 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) + } + }) } -- cgit From 0f3480a97f2924de899e49a095ef24b9fa5506f1 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 30 May 2018 21:33:40 -0230 Subject: Fix then-catch + error handling in import-account --- ui/app/components/pages/create-account/import-account/json.js | 3 +-- ui/app/components/pages/create-account/import-account/private-key.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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 3dabad68e..23b7f0afd 100644 --- a/ui/app/components/pages/create-account/import-account/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -106,8 +106,6 @@ class JsonImportSubview extends Component { } importNewJsonAccount([ fileContents, password ]) - // JS runtime requires caught rejections but failures are handled by Redux - .catch() .then(({ selectedAddress }) => { if (selectedAddress) { history.push(DEFAULT_ROUTE) @@ -116,6 +114,7 @@ class JsonImportSubview extends Component { setSelectedAddress(firstAddress) } }) + .catch(err => displayWarning(err)) } } 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 e71e47647..81cef09f9 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 @@ -96,8 +96,6 @@ PrivateKeyImportView.prototype.createNewKeychain = function () { 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(({ selectedAddress }) => { if (selectedAddress) { history.push(DEFAULT_ROUTE) @@ -106,4 +104,5 @@ PrivateKeyImportView.prototype.createNewKeychain = function () { setSelectedAddress(firstAddress) } }) + .catch(err => displayWarning(err)) } -- cgit From 12a7fc40161e3cd6dd8714f67744a905017b99d9 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 4 Jun 2018 16:11:19 -0700 Subject: 4.7.3 --- CHANGELOG.md | 6 +++++- app/manifest.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c2abd4d..7aaa2abb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## Current Master -- Fixes issue where old nicknames were kept around causing errors. +## 4.7.3 Mon Jun 04 2018 + +- 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 diff --git a/app/manifest.json b/app/manifest.json index c1f26d2ea..383b71ce3 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.3", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", -- cgit From c12b02b4af75d6d706d0b8cd9aeb07f18aa3037a Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 4 Jun 2018 16:16:18 -0700 Subject: changelog - add note on new modal --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aaa2abb5..719033da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 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 -- cgit From f73feccf5af76312f9b0a65c42b2bd0877dfba7d Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 5 Jun 2018 13:22:48 -0700 Subject: 4.7.4 --- CHANGELOG.md | 5 +++++ app/manifest.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719033da0..19287f046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 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 diff --git a/app/manifest.json b/app/manifest.json index 383b71ce3..e3a7fd963 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.7.3", + "version": "4.7.4", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", -- cgit From da938ded5c055845885d1e95ba1c74df3f92e71d Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 5 Jun 2018 13:31:34 -0700 Subject: test - metamask-controller - disable diagnostics --- test/unit/app/controllers/metamask-controller-test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 266c3f258..0dda4609b 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -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') }) -- cgit From 3cc85c219ef565267093de80e709e53d57b38d4e Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 5 Jun 2018 14:06:56 -0700 Subject: Add account type assertion to PreferencesController#setAccountLabel --- app/scripts/controllers/preferences.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index a5d8cc27b..8411e3a28 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -247,6 +247,7 @@ class PreferencesController { * @return {Promise} */ 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] || {} -- cgit From 8b449b325d96ad28c346d87729c1ebd230b8d38e Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 4 Jun 2018 14:11:50 -0700 Subject: Remove unused identities reducer from UI --- ui/app/reducers.js | 7 ------- ui/app/reducers/identities.js | 15 --------------- 2 files changed, 22 deletions(-) delete mode 100644 ui/app/reducers/identities.js 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') @@ -21,12 +20,6 @@ function rootReducer (state, action) { return action.value } - // - // 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 - } -} -- cgit From d9d09f953b9500b783a164b4aba85efcbd7ddbe2 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 4 Jun 2018 15:03:03 -0700 Subject: Render the accounts in keyring order --- ui/app/components/account-menu/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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() -- cgit From df6d03cefd1917c377f658fa8423654957ca6440 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 5 Jun 2018 11:39:51 -0700 Subject: Fix account list order for the old UI --- old-ui/app/components/account-dropdowns.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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() -- cgit From 6ee96854904d149c00b629f5025fda1bf5c60070 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 5 Jun 2018 19:48:42 -0700 Subject: Fix mock addresses used for integration tests --- development/states/add-token.json | 6 +++--- development/states/confirm-sig-requests.json | 6 +++--- development/states/currency-localization.json | 6 +++--- development/states/send-edit.json | 6 +++--- development/states/send-new-ui.json | 6 +++--- development/states/tx-list-items.json | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) 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" ] }, { -- cgit From ccd4884db112a5440e7f482f644e6729e638dc49 Mon Sep 17 00:00:00 2001 From: 03-26 <37808790+03-26@users.noreply.github.com> Date: Thu, 7 Jun 2018 03:38:57 +0900 Subject: i18n - ja improvements --- app/_locales/ja/messages.json | 43 +++++++++++++++++++++- .../pages/add-token/add-token.component.js | 6 +-- ui/app/components/pages/create-account/index.js | 15 ++++++-- ui/app/components/pages/settings/index.js | 9 ++++- .../pages/unlock-page/unlock-page.component.js | 2 +- ui/app/send-v2.js | 4 +- ui/app/welcome-screen.js | 14 ++++--- 7 files changed, 75 insertions(+), 18 deletions(-) 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/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 {
this.handleCustomAddressChange(e.target.value)} @@ -241,7 +241,7 @@ class AddToken extends Component { /> this.handleCustomSymbolChange(e.target.value)} @@ -252,7 +252,7 @@ class AddToken extends Component { /> this.handleCustomDecimalsChange(e.target.value)} 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 { > this.handleInputChange(event)} 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')), ]), -- cgit