diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 60c283a29..76e3fafaa 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -909,10 +909,13 @@
"message": "Message"
},
"metaMaskConnectStatusParagraphOne": {
- "message": "This is the new MetaMask Connect status indicator. From here you can easily see and manage sites you’ve connected to with your MetaMask wallet."
+ "message": "You now have more control over your account connections in MetaMask."
},
"metaMaskConnectStatusParagraphTwo": {
- "message": "Click the Connect status to see your connected sites and their permissions."
+ "message": "The connection status button shows if the website you’re visiting is connected to your currently selected account."
+ },
+ "metaMaskConnectStatusParagraphThree": {
+ "message": "Click it to manage your connected accounts."
},
"metamaskDescription": {
"message": "Connecting you to Ethereum and the Decentralized Web."
diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js
index 12dc74a70..efd994c10 100644
--- a/app/scripts/controllers/permissions/enums.js
+++ b/app/scripts/controllers/permissions/enums.js
@@ -75,6 +75,7 @@ export const SAFE_METHODS = [
'eth_sendTransaction',
'eth_sign',
'personal_sign',
+ 'personal_ecRecover',
'eth_signTypedData',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js
index c77d70dd4..da58bf6f3 100644
--- a/app/scripts/controllers/transactions/lib/util.js
+++ b/app/scripts/controllers/transactions/lib/util.js
@@ -1,8 +1,8 @@
import { addHexPrefix, isValidAddress } from 'ethereumjs-util'
const normalizers = {
- from: (from, lowerCase = true) => (lowerCase ? addHexPrefix(from).toLowerCase() : addHexPrefix(from)),
- to: (to, lowerCase = true) => (lowerCase ? addHexPrefix(to).toLowerCase() : addHexPrefix(to)),
+ from: (from) => addHexPrefix(from),
+ to: (to, lowerCase) => (lowerCase ? addHexPrefix(to).toLowerCase() : addHexPrefix(to)),
nonce: (nonce) => addHexPrefix(nonce),
value: (value) => addHexPrefix(value),
data: (data) => addHexPrefix(data),
@@ -12,11 +12,12 @@ const normalizers = {
/**
* Normalizes the given txParams
- * @param {Object} txParams - the tx params
- * @param {boolean} lowerCase - whether to return the addresses lower cased
+ * @param {Object} txParams - The transaction params
+ * @param {boolean} [lowerCase] - Whether to lowercase the 'to' address.
+ * Default: true
* @returns {Object} the normalized tx params
*/
-export function normalizeTxParams (txParams, lowerCase) {
+export function normalizeTxParams (txParams, lowerCase = true) {
// apply only keys in the normalizers
const normalizedTxParams = {}
for (const key in normalizers) {
diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js
index d56421fda..304246565 100644
--- a/app/scripts/controllers/transactions/pending-tx-tracker.js
+++ b/app/scripts/controllers/transactions/pending-tx-tracker.js
@@ -211,7 +211,7 @@ export default class PendingTransactionTracker extends EventEmitter {
const { hash: txHash, txParams: { nonce, from } } = txMeta
const networkNextNonce = await this.query.getTransactionCount(from)
- if (parseInt(nonce, 16) >= parseInt(networkNextNonce, 16)) {
+ if (parseInt(nonce, 16) >= networkNextNonce.toNumber()) {
return false
}
diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js
index 0ae12442b..f568c62db 100644
--- a/app/scripts/lib/encryption-public-key-manager.js
+++ b/app/scripts/lib/encryption-public-key-manager.js
@@ -283,5 +283,4 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
this.memStore.updateState({ unapprovedEncryptionPublicKeyMsgs, unapprovedEncryptionPublicKeyMsgCount })
this.emit('updateBadge')
}
-
}
diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js
index 147180ffd..49e71e59f 100644
--- a/app/scripts/lib/typed-message-manager.js
+++ b/app/scripts/lib/typed-message-manager.js
@@ -4,9 +4,11 @@ import createId from './random-id'
import assert from 'assert'
import { ethErrors } from 'eth-json-rpc-errors'
import sigUtil from 'eth-sig-util'
+import { isValidAddress } from 'ethereumjs-util'
import log from 'loglevel'
import jsonschema from 'jsonschema'
import { MESSAGE_TYPE } from './enums'
+
/**
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
* signature for an eth_signTypedData call is requested.
@@ -102,14 +104,15 @@ export default class TypedMessageManager extends EventEmitter {
*
*/
addUnapprovedMessage (msgParams, req, version) {
+
msgParams.version = version
- this.validateParams(msgParams)
- // add origin from request
if (req) {
msgParams.origin = req.origin
}
+ this.validateParams(msgParams)
log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
+
// create txData obj with parameters and meta data
const time = (new Date()).getTime()
const msgId = createId()
@@ -134,37 +137,38 @@ export default class TypedMessageManager extends EventEmitter {
*
*/
validateParams (params) {
+
+ assert.ok(params && typeof params === 'object', 'Params must be an object.')
+ assert.ok('data' in params, 'Params must include a "data" field.')
+ assert.ok('from' in params, 'Params must include a "from" field.')
+ assert.ok(
+ typeof params.from === 'string' && isValidAddress(params.from),
+ '"from" field must be a valid, lowercase, hexadecimal Ethereum address string.'
+ )
+
switch (params.version) {
case 'V1':
- assert.equal(typeof params, 'object', 'Params should ben an object.')
- assert.ok('data' in params, 'Params must include a data field.')
- assert.ok('from' in params, 'Params must include a from field.')
- assert.ok(Array.isArray(params.data), 'Data should be an array.')
- assert.equal(typeof params.from, 'string', 'From field must be a string.')
+ assert.ok(Array.isArray(params.data), '"params.data" must be an array.')
assert.doesNotThrow(() => {
sigUtil.typedSignatureHash(params.data)
- }, 'Expected EIP712 typed data')
+ }, 'Signing data must be valid EIP-712 typed data.')
break
case 'V3':
case 'V4':
+ assert.equal(typeof params.data, 'string', '"params.data" must be a string.')
let data
- assert.equal(typeof params, 'object', 'Params should be an object.')
- assert.ok('data' in params, 'Params must include a data field.')
- assert.ok('from' in params, 'Params must include a from field.')
- assert.equal(typeof params.from, 'string', 'From field must be a string.')
- assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.')
assert.doesNotThrow(() => {
data = JSON.parse(params.data)
- }, 'Data must be passed as a valid JSON string.')
+ }, '"data" must be a valid JSON string.')
const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA)
assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`)
- assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.')
+ assert.equal(validation.errors.length, 0, 'Signing data must conform to EIP-712 schema. See https://git.io/fNtcx.')
const chainId = data.domain.chainId
const activeChainId = parseInt(this.networkController.getNetworkState())
- chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`)
+ chainId && assert.equal(chainId, activeChainId, `Provided chainId "${chainId}" must match the active chainId "${activeChainId}"`)
break
default:
- assert.fail(`Unknown params.version ${params.version}`)
+ assert.fail(`Unknown typed data version "${params.version}"`)
}
}
diff --git a/package.json b/package.json
index 7901bbbaf..1a68dfdb9 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,7 @@
"@formatjs/intl-relativetimeformat": "^5.2.6",
"@fortawesome/fontawesome-free": "^5.13.0",
"@material-ui/core": "1.0.0",
- "@metamask/controllers": "^2.0.0",
+ "@metamask/controllers": "^2.0.1",
"@metamask/eth-ledger-bridge-keyring": "^0.2.6",
"@metamask/eth-token-tracker": "^2.0.0",
"@metamask/etherscan-link": "^1.1.0",
@@ -106,7 +106,7 @@
"eth-json-rpc-errors": "^2.0.2",
"eth-json-rpc-filters": "^4.1.1",
"eth-json-rpc-infura": "^4.0.2",
- "eth-json-rpc-middleware": "^4.4.1",
+ "eth-json-rpc-middleware": "^5.0.0",
"eth-keyring-controller": "^6.0.0",
"eth-method-registry": "^1.2.0",
"eth-phishing-detect": "^1.1.4",
@@ -260,7 +260,7 @@
"mocha": "^7.2.0",
"nock": "^9.0.14",
"node-fetch": "^2.6.0",
- "node-sass": "^4.12.0",
+ "node-sass": "^4.14.1",
"nyc": "^15.0.0",
"polyfill-crypto.getrandomvalues": "^1.0.0",
"proxyquire": "^2.1.3",
diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js
index 0969af53f..7e98e4227 100644
--- a/test/e2e/metamask-responsive-ui.spec.js
+++ b/test/e2e/metamask-responsive-ui.spec.js
@@ -119,7 +119,7 @@ describe('MetaMask', function () {
it('show account details dropdown menu', async function () {
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
const options = await driver.findElements(By.css('.account-options-menu .menu-item'))
- assert.equal(options.length, 4) // HD Wallet type does not have to show the Remove Account option
+ assert.equal(options.length, 3) // HD Wallet type does not have to show the Remove Account option
// click outside of menu to dismiss
// account menu button chosen because the menu never covers it.
await driver.clickPoint(By.css('.account-menu__icon'), 0, 0)
diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js
index 262202419..4628aabc0 100644
--- a/test/unit/app/controllers/metamask-controller-test.js
+++ b/test/unit/app/controllers/metamask-controller-test.js
@@ -35,9 +35,33 @@ const ExtensionizerMock = {
},
}
+let loggerMiddlewareMock
+const initializeMockMiddlewareLog = () => {
+ loggerMiddlewareMock = {
+ requests: [],
+ responses: [],
+ }
+}
+const tearDownMockMiddlewareLog = () => {
+ loggerMiddlewareMock = undefined
+}
+
+const createLoggerMiddlewareMock = () => (req, res, next) => {
+ if (loggerMiddlewareMock) {
+ loggerMiddlewareMock.requests.push(req)
+ next((cb) => {
+ loggerMiddlewareMock.responses.push(res)
+ cb()
+ })
+ } else {
+ next()
+ }
+}
+
const MetaMaskController = proxyquire('../../../../app/scripts/metamask-controller', {
'./controllers/threebox': { default: ThreeBoxControllerMock },
'extensionizer': ExtensionizerMock,
+ './lib/createLoggerMiddleware': { default: createLoggerMiddlewareMock },
}).default
const currentNetworkId = 42
@@ -96,7 +120,6 @@ describe('MetaMaskController', function () {
// add sinon method spies
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
- sandbox.spy(metamaskController.txController, 'newUnapprovedTransaction')
})
afterEach(function () {
@@ -776,6 +799,17 @@ describe('MetaMaskController', function () {
})
describe('#setupUntrustedCommunication', function () {
+
+ const mockTxParams = { from: TEST_ADDRESS }
+
+ beforeEach(function () {
+ initializeMockMiddlewareLog()
+ })
+
+ after(function () {
+ tearDownMockMiddlewareLog()
+ })
+
it('sets up phishing stream for untrusted communication', async function () {
const phishingMessageSender = {
url: 'http://myethereumwalletntw.com',
@@ -815,7 +849,7 @@ describe('MetaMaskController', function () {
const message = {
id: 1999133338649204,
jsonrpc: '2.0',
- params: ['mock tx params'],
+ params: [{ ...mockTxParams }],
method: 'eth_sendTransaction',
}
streamTest.write({
@@ -824,15 +858,12 @@ describe('MetaMaskController', function () {
}, null, () => {
setTimeout(() => {
assert.deepStrictEqual(
- metamaskController.txController.newUnapprovedTransaction.getCall(0).args,
- [
- 'mock tx params',
- {
- ...message,
- origin: 'http://mycrypto.com',
- tabId: 456,
- },
- ]
+ loggerMiddlewareMock.requests[0],
+ {
+ ...message,
+ origin: 'http://mycrypto.com',
+ tabId: 456,
+ },
)
done()
})
@@ -856,7 +887,7 @@ describe('MetaMaskController', function () {
const message = {
id: 1999133338649204,
jsonrpc: '2.0',
- params: ['mock tx params'],
+ params: [{ ...mockTxParams }],
method: 'eth_sendTransaction',
}
streamTest.write({
@@ -865,14 +896,11 @@ describe('MetaMaskController', function () {
}, null, () => {
setTimeout(() => {
assert.deepStrictEqual(
- metamaskController.txController.newUnapprovedTransaction.getCall(0).args,
- [
- 'mock tx params',
- {
- ...message,
- origin: 'http://mycrypto.com',
- },
- ]
+ loggerMiddlewareMock.requests[0],
+ {
+ ...message,
+ origin: 'http://mycrypto.com',
+ },
)
done()
})
diff --git a/test/unit/app/controllers/transactions/pending-tx-tracker-test.js b/test/unit/app/controllers/transactions/pending-tx-tracker-test.js
index e2b68c775..facc0bb12 100644
--- a/test/unit/app/controllers/transactions/pending-tx-tracker-test.js
+++ b/test/unit/app/controllers/transactions/pending-tx-tracker-test.js
@@ -1,5 +1,6 @@
import sinon from 'sinon'
import { strict as assert } from 'assert'
+import BN from 'bn.js'
import PendingTransactionTracker from '../../../../../app/scripts/controllers/transactions/pending-tx-tracker'
describe('PendingTransactionTracker', function () {
@@ -311,10 +312,11 @@ describe('PendingTransactionTracker', function () {
describe('#_checkIfTxWasDropped', function () {
it('should return true when the given nonce is lower than the network nonce', async function () {
+ const nonceBN = new BN(2)
const pendingTxTracker = new PendingTransactionTracker({
query: {
getTransactionReceipt: sinon.stub(),
- getTransactionCount: sinon.stub().resolves('0x02'),
+ getTransactionCount: sinon.stub().resolves(nonceBN),
},
nonceTracker: {
getGlobalLock: sinon.stub().resolves({
@@ -343,10 +345,11 @@ describe('PendingTransactionTracker', function () {
})
it('should return false when the given nonce is the network nonce', async function () {
+ const nonceBN = new BN(1)
const pendingTxTracker = new PendingTransactionTracker({
query: {
getTransactionReceipt: sinon.stub(),
- getTransactionCount: sinon.stub().resolves('0x01'),
+ getTransactionCount: sinon.stub().resolves(nonceBN),
},
nonceTracker: {
getGlobalLock: sinon.stub().resolves({
@@ -487,10 +490,11 @@ describe('PendingTransactionTracker', function () {
history: [{}],
rawTx: '0xf86c808504a817c80082471d',
}
+ const nonceBN = new BN(2)
const pendingTxTracker = new PendingTransactionTracker({
query: {
getTransactionReceipt: sinon.stub().rejects(),
- getTransactionCount: sinon.stub().resolves('0x02'),
+ getTransactionCount: sinon.stub().resolves(nonceBN),
},
nonceTracker: {
getGlobalLock: sinon.stub().resolves({
@@ -647,6 +651,7 @@ describe('PendingTransactionTracker', function () {
})
it("should emit 'tx:dropped' with the txMetas id only after the fourth call", async function () {
+ const nonceBN = new BN(2)
const txMeta = {
id: 1,
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb',
@@ -662,7 +667,7 @@ describe('PendingTransactionTracker', function () {
const pendingTxTracker = new PendingTransactionTracker({
query: {
getTransactionReceipt: sinon.stub().resolves(null),
- getTransactionCount: sinon.stub().resolves('0x02'),
+ getTransactionCount: sinon.stub().resolves(nonceBN),
},
nonceTracker: {
getGlobalLock: sinon.stub().resolves({
diff --git a/ui/app/components/app/asset-list-item/asset-list-item.js b/ui/app/components/app/asset-list-item/asset-list-item.js
index 8c0848abe..3737698d1 100644
--- a/ui/app/components/app/asset-list-item/asset-list-item.js
+++ b/ui/app/components/app/asset-list-item/asset-list-item.js
@@ -97,7 +97,7 @@ const AssetListItem = ({
data-testid={dataTestId}
title={primary}
titleIcon={titleIcon}
- subtitle={secondary}
+ subtitle={
{secondary}
}
onClick={onClick}
icon={(
{permissions.map(({ key: permissionName }) => (
- {t(permissionName)}
+
+
))}
diff --git a/ui/app/components/app/connected-accounts-permissions/index.scss b/ui/app/components/app/connected-accounts-permissions/index.scss
index 838e727dc..712d0b431 100644
--- a/ui/app/components/app/connected-accounts-permissions/index.scss
+++ b/ui/app/components/app/connected-accounts-permissions/index.scss
@@ -41,13 +41,11 @@
&__list-item {
display: flex;
+ }
- i {
- display: block;
- padding-right: 8px;
- font-size: 18px;
- color: $Grey-800;
- }
+ & &__checkbox {
+ margin: 0 8px 0 0;
+ font-size: 18px;
}
&__list-container {
diff --git a/ui/app/components/app/menu-bar/account-options-menu.js b/ui/app/components/app/menu-bar/account-options-menu.js
index c87416cdb..9edf908d4 100644
--- a/ui/app/components/app/menu-bar/account-options-menu.js
+++ b/ui/app/components/app/menu-bar/account-options-menu.js
@@ -10,6 +10,8 @@ import genAccountLink from '../../../../lib/account-link'
import { getCurrentKeyring, getCurrentNetwork, getRpcPrefsForCurrentProvider, getSelectedIdentity } from '../../../selectors'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { useMetricEvent } from '../../../hooks/useMetricEvent'
+import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
+import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../app/scripts/lib/enums'
export default function AccountOptionsMenu ({ anchorElement, onClose }) {
const t = useI18nContext()
@@ -58,16 +60,22 @@ export default function AccountOptionsMenu ({ anchorElement, onClose }) {
className="account-options-menu"
onHide={onClose}
>
-
+ {
+ getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN
+ ? null
+ : (
+
+ )
+ }