Merge remote-tracking branch 'origin/develop' into Version-v8.0.0

* origin/develop: (35 commits)
  Delete unused InfuraController & tests (#8773)
  Permissions: Do not display HTTP/HTTPS URL schemes for unique hosts (#8768)
  Refactor confirm approve page (#8757)
  blocklisted -> blocked
  Update app/scripts/contentscript.js
  blacklist -> blocklist; whitelist -> safelist
  replace blacklist with blocklist
  Delete unused transaction history test state (#8769)
  fix-formatting-of-gif (#8767)
  Order accounts on connect page (#8762)
  add gif for loading dev build (#8766)
  Bump websocket-extensions from 0.1.3 to 0.1.4 (#8759)
  Fix prop type mismatch (#8754)
  use grid template to position list item (#8753)
  Fix account menu entry for imported accounts (#8747)
  Fix permissions connect close and redirect behavior (#8751)
  Refactor `TokenBalance` component (#8752)
  Fix 'Remove account' in Account Options menu (#8748)
  move activation logic into token rates controller (#8744)
  asset outdated warning inline on full screen (#8734)
  ...
feature/default_network_editable
Mark Stacey 5 years ago
commit 3604c3519c
  1. 4
      app/_locales/en/messages.json
  2. 7
      app/scripts/background.js
  3. 16
      app/scripts/contentscript.js
  4. 39
      app/scripts/controllers/infura.js
  5. 71
      app/scripts/controllers/permissions/index.js
  6. 28
      app/scripts/controllers/permissions/methodMiddleware.js
  7. 31
      app/scripts/controllers/token-rates.js
  8. 4
      app/scripts/controllers/transactions/index.js
  9. 19
      app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js
  10. 19
      app/scripts/controllers/transactions/lib/recipient-blocklist-checker.js
  11. 4
      app/scripts/controllers/transactions/lib/recipient-blocklist.js
  12. 3
      app/scripts/lib/decrypt-message-manager.js
  13. 3
      app/scripts/lib/encryption-public-key-manager.js
  14. 9
      app/scripts/lib/enums.js
  15. 3
      app/scripts/lib/message-manager.js
  16. 3
      app/scripts/lib/personal-message-manager.js
  17. 4
      app/scripts/lib/typed-message-manager.js
  18. 48
      app/scripts/metamask-controller.js
  19. 2
      app/scripts/phishing-detect.js
  20. 8
      app/scripts/ui.js
  21. 2
      docs/add-to-chrome.md
  22. BIN
      docs/load-dev-build-chrome.gif
  23. 2100
      test/data/mock-tx-history.json
  24. 2809
      test/data/v17-long-history.json
  25. 2
      test/e2e/address-book.spec.js
  26. 14
      test/e2e/from-import-ui.spec.js
  27. 12
      test/e2e/metamask-ui.spec.js
  28. 2
      test/e2e/signature-request.spec.js
  29. 6
      test/e2e/threebox.spec.js
  30. 66
      test/unit/app/controllers/infura-controller-test.js
  31. 4
      test/unit/app/controllers/metamask-controller-test.js
  32. 55
      test/unit/app/controllers/permissions/mocks.js
  33. 271
      test/unit/app/controllers/permissions/permissions-controller-test.js
  34. 52
      test/unit/app/controllers/permissions/permissions-log-controller-test.js
  35. 161
      test/unit/app/controllers/permissions/permissions-middleware-test.js
  36. 5
      test/unit/app/controllers/token-rates-controller.js
  37. 14
      test/unit/app/controllers/transactions/recipient-blocklist-checker-test.js
  38. 4
      test/unit/app/controllers/transactions/tx-state-history-helpers-test.js
  39. 37
      ui/app/components/app/account-menu/account-menu.component.js
  40. 4
      ui/app/components/app/account-menu/account-menu.container.js
  41. 1
      ui/app/components/app/account-menu/index.scss
  42. 13
      ui/app/components/app/account-menu/tests/account-menu.test.js
  43. 53
      ui/app/components/app/asset-list-item/asset-list-item.js
  44. 35
      ui/app/components/app/asset-list-item/asset-list-item.scss
  45. 31
      ui/app/components/app/asset-list/asset-list.js
  46. 13
      ui/app/components/app/asset-list/asset-list.scss
  47. 6
      ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js
  48. 19
      ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js
  49. 27
      ui/app/components/app/connected-sites-list/connected-sites-list.component.js
  50. 2
      ui/app/components/app/index.scss
  51. 3
      ui/app/components/app/menu-bar/account-options-menu.js
  52. 2
      ui/app/components/app/modals/hide-token-confirmation-modal.js
  53. 12
      ui/app/components/app/signature-request-original/signature-request-original.component.js
  54. 7
      ui/app/components/app/signature-request-original/signature-request-original.container.js
  55. 7
      ui/app/components/app/signature-request/signature-request.container.js
  56. 2
      ui/app/components/app/token-cell/index.js
  57. 111
      ui/app/components/app/token-cell/token-cell.component.js
  58. 14
      ui/app/components/app/token-cell/token-cell.container.js
  59. 88
      ui/app/components/app/token-cell/token-cell.js
  60. 92
      ui/app/components/app/token-cell/token-cell.scss
  61. 10
      ui/app/components/app/token-cell/token-cell.test.js
  62. 2
      ui/app/components/app/token-list/index.js
  63. 148
      ui/app/components/app/token-list/token-list.component.js
  64. 21
      ui/app/components/app/token-list/token-list.container.js
  65. 66
      ui/app/components/app/token-list/token-list.js
  66. 1
      ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js
  67. 5
      ui/app/components/app/transaction-list-item/index.scss
  68. 2
      ui/app/components/app/transaction-list/index.scss
  69. 3
      ui/app/components/app/wallet-overview/token-overview.js
  70. 3
      ui/app/components/ui/currency-display/currency-display.component.js
  71. 55
      ui/app/components/ui/list-item/index.scss
  72. 29
      ui/app/components/ui/list-item/list-item.component.js
  73. 6
      ui/app/components/ui/tabs/tabs.component.js
  74. 26
      ui/app/components/ui/toggle-button/index.scss
  75. 9
      ui/app/components/ui/toggle-button/toggle-button.component.js
  76. 2
      ui/app/components/ui/token-balance/index.js
  77. 23
      ui/app/components/ui/token-balance/token-balance.component.js
  78. 16
      ui/app/components/ui/token-balance/token-balance.container.js
  79. 30
      ui/app/components/ui/token-balance/token-balance.js
  80. 1
      ui/app/helpers/higher-order-components/with-token-tracker/index.js
  81. 44
      ui/app/helpers/higher-order-components/with-token-tracker/tests/with-token-tracker.component.test.js
  82. 101
      ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js
  83. 5
      ui/app/helpers/utils/transactions.util.js
  84. 12
      ui/app/helpers/utils/util.js
  85. 88
      ui/app/hooks/useTokenTracker.js
  86. 2
      ui/app/pages/add-token/add-token.component.js
  87. 2
      ui/app/pages/asset/asset.scss
  88. 6
      ui/app/pages/asset/components/asset-breadcrumb.js
  89. 115
      ui/app/pages/confirm-approve/confirm-approve.component.js
  90. 113
      ui/app/pages/confirm-approve/confirm-approve.container.js
  91. 141
      ui/app/pages/confirm-approve/confirm-approve.js
  92. 2
      ui/app/pages/confirm-approve/index.js
  93. 5
      ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
  94. 3
      ui/app/pages/confirm-transaction/conf-tx.js
  95. 2
      ui/app/pages/connected-accounts/connected-accounts.component.js
  96. 2
      ui/app/pages/connected-sites/connected-sites.component.js
  97. 2
      ui/app/pages/connected-sites/connected-sites.container.js
  98. 25
      ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
  99. 5
      ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
  100. 2
      ui/app/pages/home/home.component.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,4 +1,8 @@
{ {
"acceptTermsOfUse": {
"message": "I have read and agree to the $1",
"description": "$1 is the `terms` message"
},
"eth_accounts": { "eth_accounts": {
"message": "View the addresses of your permitted accounts (required)", "message": "View the addresses of your permitted accounts (required)",
"description": "The description for the `eth_accounts` permission" "description": "The description for the `eth_accounts` permission"

@ -134,7 +134,6 @@ initialize().catch(log.error)
* @property {string} currentCurrency - A string identifying the user's preferred display currency, for use in showing conversion rates. * @property {string} currentCurrency - A string identifying the user's preferred display currency, for use in showing conversion rates.
* @property {number} conversionRate - A number representing the current exchange rate from the user's preferred currency to Ether. * @property {number} conversionRate - A number representing the current exchange rate from the user's preferred currency to Ether.
* @property {number} conversionDate - A unix epoch date (ms) for the time the current conversion rate was last retrieved. * @property {number} conversionDate - A unix epoch date (ms) for the time the current conversion rate was last retrieved.
* @property {Object} infuraNetworkStatus - An object of infura network status checks.
* @property {boolean} forgottenPassword - Returns true if the user has initiated the password recovery screen, is recovering from seed phrase. * @property {boolean} forgottenPassword - Returns true if the user has initiated the password recovery screen, is recovering from seed phrase.
*/ */
@ -319,7 +318,7 @@ function setupController (initState, initLangCode) {
[ENVIRONMENT_TYPE_FULLSCREEN]: true, [ENVIRONMENT_TYPE_FULLSCREEN]: true,
} }
const metamaskBlacklistedPorts = [ const metamaskBlockedPorts = [
'trezor-connect', 'trezor-connect',
] ]
@ -343,7 +342,7 @@ function setupController (initState, initLangCode) {
const processName = remotePort.name const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName] const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
if (metamaskBlacklistedPorts.includes(remotePort.name)) { if (metamaskBlockedPorts.includes(remotePort.name)) {
return false return false
} }
@ -384,7 +383,7 @@ function setupController (initState, initLangCode) {
if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) { if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) {
const tabId = remotePort.sender.tab.id const tabId = remotePort.sender.tab.id
const url = new URL(remotePort.sender.url) const url = new URL(remotePort.sender.url)
const origin = url.hostname const { origin } = url
remotePort.onMessage.addListener((msg) => { remotePort.onMessage.addListener((msg) => {
if (msg.data && msg.data.method === 'eth_requestAccounts') { if (msg.data && msg.data.method === 'eth_requestAccounts') {

@ -127,7 +127,7 @@ function logStreamDisconnectWarning (remoteLabel, err) {
*/ */
function shouldInjectProvider () { function shouldInjectProvider () {
return doctypeCheck() && suffixCheck() && return doctypeCheck() && suffixCheck() &&
documentElementCheck() && !blacklistedDomainCheck() documentElementCheck() && !blockedDomainCheck()
} }
/** /**
@ -181,12 +181,12 @@ function documentElementCheck () {
} }
/** /**
* Checks if the current domain is blacklisted * Checks if the current domain is blocked
* *
* @returns {boolean} {@code true} - if the current domain is blacklisted * @returns {boolean} {@code true} - if the current domain is blocked
*/ */
function blacklistedDomainCheck () { function blockedDomainCheck () {
const blacklistedDomains = [ const blockedDomains = [
'uscourts.gov', 'uscourts.gov',
'dropbox.com', 'dropbox.com',
'webbyawards.com', 'webbyawards.com',
@ -200,9 +200,9 @@ function blacklistedDomainCheck () {
] ]
const currentUrl = window.location.href const currentUrl = window.location.href
let currentRegex let currentRegex
for (let i = 0; i < blacklistedDomains.length; i++) { for (let i = 0; i < blockedDomains.length; i++) {
const blacklistedDomain = blacklistedDomains[i].replace('.', '\\.') const blockedDomain = blockedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blacklistedDomain}).)*$`) currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`)
if (!currentRegex.test(currentUrl)) { if (!currentRegex.test(currentUrl)) {
return true return true
} }

@ -1,39 +0,0 @@
import ObservableStore from 'obs-store'
import log from 'loglevel'
// every ten minutes
const POLLING_INTERVAL = 10 * 60 * 1000
export default class InfuraController {
constructor (opts = {}) {
const initState = Object.assign({
infuraNetworkStatus: {},
}, opts.initState)
this.store = new ObservableStore(initState)
}
//
// PUBLIC METHODS
//
// Responsible for retrieving the status of Infura's nodes. Can return either
// ok, degraded, or down.
async checkInfuraNetworkStatus () {
const response = await window.fetch('https://api.infura.io/v1/status/metamask')
const parsedResponse = await response.json()
this.store.updateState({
infuraNetworkStatus: parsedResponse,
})
return parsedResponse
}
scheduleInfuraNetworkCheck () {
if (this.conversionInterval) {
clearInterval(this.conversionInterval)
}
this.conversionInterval = setInterval(() => {
this.checkInfuraNetworkStatus().catch(log.warn)
}, POLLING_INTERVAL)
}
}

@ -47,7 +47,7 @@ export class PermissionsController {
this.getKeyringAccounts = getKeyringAccounts this.getKeyringAccounts = getKeyringAccounts
this._getUnlockPromise = getUnlockPromise this._getUnlockPromise = getUnlockPromise
this._notifyDomain = notifyDomain this._notifyDomain = notifyDomain
this.notifyAllDomains = notifyAllDomains this._notifyAllDomains = notifyAllDomains
this._showPermissionRequest = showPermissionRequest this._showPermissionRequest = showPermissionRequest
this._restrictedMethods = getRestrictedMethods({ this._restrictedMethods = getRestrictedMethods({
@ -95,6 +95,7 @@ export class PermissionsController {
getAccounts: this.getAccounts.bind(this, origin), getAccounts: this.getAccounts.bind(this, origin),
getUnlockPromise: () => this._getUnlockPromise(true), getUnlockPromise: () => this._getUnlockPromise(true),
hasPermission: this.hasPermission.bind(this, origin), hasPermission: this.hasPermission.bind(this, origin),
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind( requestAccountsPermission: this._requestPermissions.bind(
this, { origin }, { eth_accounts: {} }, this, { origin }, { eth_accounts: {} },
), ),
@ -196,6 +197,7 @@ export class PermissionsController {
* User approval callback. Resolves the Promise for the permissions request * User approval callback. Resolves the Promise for the permissions request
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions. * waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
* The request will be rejected if finalizePermissionsRequest fails. * The request will be rejected if finalizePermissionsRequest fails.
* Idempotent for a given request id.
* *
* @param {Object} approved - The request object approved by the user * @param {Object} approved - The request object approved by the user
* @param {Array} accounts - The accounts to expose, if any * @param {Array} accounts - The accounts to expose, if any
@ -206,7 +208,7 @@ export class PermissionsController {
const approval = this.pendingApprovals.get(id) const approval = this.pendingApprovals.get(id)
if (!approval) { if (!approval) {
log.error(`Permissions request with id '${id}' not found`) log.debug(`Permissions request with id '${id}' not found`)
return return
} }
@ -241,6 +243,7 @@ export class PermissionsController {
/** /**
* User rejection callback. Rejects the Promise for the permissions request * User rejection callback. Rejects the Promise for the permissions request
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions. * waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
* Idempotent for a given id.
* *
* @param {string} id - The id of the request rejected by the user * @param {string} id - The id of the request rejected by the user
*/ */
@ -248,7 +251,7 @@ export class PermissionsController {
const approval = this.pendingApprovals.get(id) const approval = this.pendingApprovals.get(id)
if (!approval) { if (!approval) {
log.error(`Permissions request with id '${id}' not found`) log.debug(`Permissions request with id '${id}' not found`)
return return
} }
@ -289,10 +292,7 @@ export class PermissionsController {
const permittedAccounts = await this.getAccounts(origin) const permittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, { this.notifyAccountsChanged(origin, permittedAccounts)
method: NOTIFICATION_NAMES.accountsChanged,
result: permittedAccounts,
})
} }
/** /**
@ -338,10 +338,7 @@ export class PermissionsController {
newPermittedAccounts = await this.getAccounts(origin) newPermittedAccounts = await this.getAccounts(origin)
} }
this.notifyDomain(origin, { this.notifyAccountsChanged(origin, newPermittedAccounts)
method: NOTIFICATION_NAMES.accountsChanged,
result: newPermittedAccounts,
})
} }
/** /**
@ -410,21 +407,34 @@ export class PermissionsController {
}) })
} }
notifyDomain (origin, payload) { /**
* Notify a domain that its permitted accounts have changed.
* Also updates the accounts history log.
*
* @param {string} origin - The origin of the domain to notify.
* @param {Array<string>} newAccounts - The currently permitted accounts.
*/
notifyAccountsChanged (origin, newAccounts) {
if (typeof origin !== 'string' || !origin) {
throw new Error(`Invalid origin: '${origin}'`)
}
if (!Array.isArray(newAccounts)) {
throw new Error('Invalid accounts', newAccounts)
}
this._notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: newAccounts,
})
// if the accounts changed from the perspective of the dapp, // if the accounts changed from the perspective of the dapp,
// update "last seen" time for the origin and account(s) // update "last seen" time for the origin and account(s)
// exception: no accounts -> no times to update // exception: no accounts -> no times to update
if (
payload.method === NOTIFICATION_NAMES.accountsChanged &&
Array.isArray(payload.result)
) {
this.permissionsLog.updateAccountsHistory( this.permissionsLog.updateAccountsHistory(
origin, payload.result origin, newAccounts
) )
}
this._notifyDomain(origin, payload)
// NOTE: // NOTE:
// we don't check for accounts changing in the notifyAllDomains case, // we don't check for accounts changing in the notifyAllDomains case,
@ -438,7 +448,8 @@ export class PermissionsController {
* Should only be called after confirming that the permissions exist, to * Should only be called after confirming that the permissions exist, to
* avoid sending unnecessary notifications. * avoid sending unnecessary notifications.
* *
* @param {Object} domains { origin: [permissions] } * @param {Object} domains { origin: [permissions] } - The map of domain
* origins to permissions to remove.
*/ */
removePermissionsFor (domains) { removePermissionsFor (domains) {
@ -449,10 +460,7 @@ export class PermissionsController {
perms.map((methodName) => { perms.map((methodName) => {
if (methodName === 'eth_accounts') { if (methodName === 'eth_accounts') {
this.notifyDomain( this.notifyAccountsChanged(origin, [])
origin,
{ method: NOTIFICATION_NAMES.accountsChanged, result: [] }
)
} }
return { parentCapability: methodName } return { parentCapability: methodName }
@ -466,7 +474,7 @@ export class PermissionsController {
*/ */
clearPermissions () { clearPermissions () {
this.permissions.clearDomains() this.permissions.clearDomains()
this.notifyAllDomains({ this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged, method: NOTIFICATION_NAMES.accountsChanged,
result: [], result: [],
}) })
@ -505,6 +513,11 @@ export class PermissionsController {
...metadata, ...metadata,
lastUpdated: Date.now(), lastUpdated: Date.now(),
} }
if (!newMetadataState[origin].extensionId && !newMetadataState[origin].host) {
newMetadataState[origin].host = new URL(origin).host
}
this._pendingSiteMetadata.add(origin) this._pendingSiteMetadata.add(origin)
this._setDomainMetadata(newMetadataState) this._setDomainMetadata(newMetadataState)
} }
@ -583,6 +596,7 @@ export class PermissionsController {
* @param {string} account - The newly selected account's address. * @param {string} account - The newly selected account's address.
*/ */
async _handleAccountSelected (account) { async _handleAccountSelected (account) {
if (typeof account !== 'string') { if (typeof account !== 'string') {
throw new Error('Selected account should be a non-empty string.') throw new Error('Selected account should be a non-empty string.')
} }
@ -618,10 +632,7 @@ export class PermissionsController {
async _handleConnectedAccountSelected (origin) { async _handleConnectedAccountSelected (origin) {
const permittedAccounts = await this.getAccounts(origin) const permittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, { this.notifyAccountsChanged(origin, permittedAccounts)
method: NOTIFICATION_NAMES.accountsChanged,
result: permittedAccounts,
})
} }
/** /**

@ -9,6 +9,7 @@ export default function createMethodMiddleware ({
getAccounts, getAccounts,
getUnlockPromise, getUnlockPromise,
hasPermission, hasPermission,
notifyAccountsChanged,
requestAccountsPermission, requestAccountsPermission,
}) { }) {
@ -16,6 +17,8 @@ export default function createMethodMiddleware ({
return createAsyncMiddleware(async (req, res, next) => { return createAsyncMiddleware(async (req, res, next) => {
let responseHandler
switch (req.method) { switch (req.method) {
// Intercepting eth_accounts requests for backwards compatibility: // Intercepting eth_accounts requests for backwards compatibility:
@ -81,10 +84,33 @@ export default function createMethodMiddleware ({
res.result = true res.result = true
return return
// register return handler to send accountsChanged notification
case 'wallet_requestPermissions':
if ('eth_accounts' in req.params?.[0]) {
responseHandler = async () => {
if (Array.isArray(res.result)) {
for (const permission of res.result) {
if (permission.parentCapability === 'eth_accounts') {
notifyAccountsChanged(await getAccounts())
}
}
}
}
}
break
default: default:
break break
} }
next() // when this promise resolves, the response is on its way back
await next()
if (responseHandler) {
responseHandler()
}
}) })
} }

@ -17,11 +17,10 @@ export default class TokenRatesController {
* *
* @param {Object} [config] - Options to configure controller * @param {Object} [config] - Options to configure controller
*/ */
constructor ({ interval = DEFAULT_INTERVAL, currency, preferences } = {}) { constructor ({ currency, preferences } = {}) {
this.store = new ObservableStore() this.store = new ObservableStore()
this.currency = currency this.currency = currency
this.preferences = preferences this.preferences = preferences
this.interval = interval
} }
/** /**
@ -50,19 +49,6 @@ export default class TokenRatesController {
this.store.putState({ contractExchangeRates }) this.store.putState({ contractExchangeRates })
} }
/**
* @type {Number}
*/
set interval (interval) {
this._handle && clearInterval(this._handle)
if (!interval) {
return
}
this._handle = setInterval(() => {
this.updateExchangeRates()
}, interval)
}
/** /**
* @type {Object} * @type {Object}
*/ */
@ -85,4 +71,19 @@ export default class TokenRatesController {
this._tokens = tokens this._tokens = tokens
this.updateExchangeRates() this.updateExchangeRates()
} }
start (interval = DEFAULT_INTERVAL) {
this._handle && clearInterval(this._handle)
if (!interval) {
return
}
this._handle = setInterval(() => {
this.updateExchangeRates()
}, interval)
this.updateExchangeRates()
}
stop () {
this._handle && clearInterval(this._handle)
}
} }

@ -25,7 +25,7 @@ import NonceTracker from 'nonce-tracker'
import * as txUtils from './lib/util' import * as txUtils from './lib/util'
import cleanErrorStack from '../../lib/cleanErrorStack' import cleanErrorStack from '../../lib/cleanErrorStack'
import log from 'loglevel' import log from 'loglevel'
import { throwIfAccountIsBlacklisted } from './lib/recipient-blacklist-checker' import { throwIfAccountIsBlocked } from './lib/recipient-blocklist-checker'
import { import {
TRANSACTION_TYPE_CANCEL, TRANSACTION_TYPE_CANCEL,
@ -241,7 +241,7 @@ export default class TransactionController extends EventEmitter {
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
try { try {
throwIfAccountIsBlacklisted(txMeta.metamaskNetworkId, normalizedTxParams.to) throwIfAccountIsBlocked(txMeta.metamaskNetworkId, normalizedTxParams.to)
txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse) txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse)
} catch (error) { } catch (error) {
log.warn(error) log.warn(error)

@ -1,19 +0,0 @@
import blacklist from './recipient-blacklist'
/**
* Checks if a specified account on a specified network is blacklisted
* @param {number} networkId
* @param {string} account
* @throws {Error} if the account is blacklisted on mainnet
*/
export function throwIfAccountIsBlacklisted (networkId, account) {
const mainnetId = 1
if (networkId !== mainnetId) {
return
}
const accountToCheck = account.toLowerCase()
if (blacklist.includes(accountToCheck)) {
throw new Error('Recipient is a public account')
}
}

@ -0,0 +1,19 @@
import blocklist from './recipient-blocklist'
/**
* Checks if a specified account on a specified network is blocked
* @param {number} networkId
* @param {string} account
* @throws {Error} if the account is blocked on mainnet
*/
export function throwIfAccountIsBlocked (networkId, account) {
const mainnetId = 1
if (networkId !== mainnetId) {
return
}
const accountToCheck = account.toLowerCase()
if (blocklist.includes(accountToCheck)) {
throw new Error('Recipient is a public account')
}
}

@ -1,4 +1,4 @@
const blacklist = [ const blocklist = [
// IDEX phisher // IDEX phisher
'0x9bcb0A9d99d815Bb87ee3191b1399b1Bcc46dc77', '0x9bcb0A9d99d815Bb87ee3191b1399b1Bcc46dc77',
// Ganache default seed phrases // Ganache default seed phrases
@ -14,4 +14,4 @@ const blacklist = [
'0x5aeda56215b167893e80b4fe645ba6d5bab767de', '0x5aeda56215b167893e80b4fe645ba6d5bab767de',
] ]
export default blacklist export default blocklist

@ -3,6 +3,7 @@ import ObservableStore from 'obs-store'
import ethUtil from 'ethereumjs-util' import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors' import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id' import createId from './random-id'
import { MESSAGE_TYPE } from './enums'
const hexRe = /^[0-9A-Fa-f]+$/g const hexRe = /^[0-9A-Fa-f]+$/g
import log from 'loglevel' import log from 'loglevel'
@ -124,7 +125,7 @@ export default class DecryptMessageManager extends EventEmitter {
msgParams: msgParams, msgParams: msgParams,
time: time, time: time,
status: 'unapproved', status: 'unapproved',
type: 'eth_decrypt', type: MESSAGE_TYPE.ETH_DECRYPT,
} }
this.addMsg(msgData) this.addMsg(msgData)

@ -3,6 +3,7 @@ import ObservableStore from 'obs-store'
import { ethErrors } from 'eth-json-rpc-errors' import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id' import createId from './random-id'
import log from 'loglevel' import log from 'loglevel'
import { MESSAGE_TYPE } from './enums'
/** /**
* Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when * Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when
@ -114,7 +115,7 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
msgParams: address, msgParams: address,
time: time, time: time,
status: 'unapproved', status: 'unapproved',
type: 'eth_getEncryptionPublicKey', type: MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY,
} }
if (req) { if (req) {

@ -9,11 +9,20 @@ const PLATFORM_EDGE = 'Edge'
const PLATFORM_FIREFOX = 'Firefox' const PLATFORM_FIREFOX = 'Firefox'
const PLATFORM_OPERA = 'Opera' const PLATFORM_OPERA = 'Opera'
const MESSAGE_TYPE = {
ETH_DECRYPT: 'eth_decrypt',
ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey',
ETH_SIGN: 'eth_sign',
ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
PERSONAL_SIGN: 'personal_sign',
}
export { export {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
MESSAGE_TYPE,
PLATFORM_BRAVE, PLATFORM_BRAVE,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,

@ -3,6 +3,7 @@ import ObservableStore from 'obs-store'
import ethUtil from 'ethereumjs-util' import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors' import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id' import createId from './random-id'
import { MESSAGE_TYPE } from './enums'
/** /**
* Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for * Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for
@ -116,7 +117,7 @@ export default class MessageManager extends EventEmitter {
msgParams: msgParams, msgParams: msgParams,
time: time, time: time,
status: 'unapproved', status: 'unapproved',
type: 'eth_sign', type: MESSAGE_TYPE.ETH_SIGN,
} }
this.addMsg(msgData) this.addMsg(msgData)

@ -3,6 +3,7 @@ import ObservableStore from 'obs-store'
import ethUtil from 'ethereumjs-util' import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors' import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id' import createId from './random-id'
import { MESSAGE_TYPE } from './enums'
const hexRe = /^[0-9A-Fa-f]+$/g const hexRe = /^[0-9A-Fa-f]+$/g
import log from 'loglevel' import log from 'loglevel'
@ -125,7 +126,7 @@ export default class PersonalMessageManager extends EventEmitter {
msgParams: msgParams, msgParams: msgParams,
time: time, time: time,
status: 'unapproved', status: 'unapproved',
type: 'personal_sign', type: MESSAGE_TYPE.PERSONAL_SIGN,
} }
this.addMsg(msgData) this.addMsg(msgData)

@ -6,7 +6,7 @@ import { ethErrors } from 'eth-json-rpc-errors'
import sigUtil from 'eth-sig-util' import sigUtil from 'eth-sig-util'
import log from 'loglevel' import log from 'loglevel'
import jsonschema from 'jsonschema' 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 * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
* signature for an eth_signTypedData call is requested. * signature for an eth_signTypedData call is requested.
@ -118,7 +118,7 @@ export default class TypedMessageManager extends EventEmitter {
msgParams: msgParams, msgParams: msgParams,
time: time, time: time,
status: 'unapproved', status: 'unapproved',
type: 'eth_signTypedData', type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA,
} }
this.addMsg(msgData) this.addMsg(msgData)

@ -29,7 +29,6 @@ import EnsController from './controllers/ens'
import NetworkController from './controllers/network' import NetworkController from './controllers/network'
import PreferencesController from './controllers/preferences' import PreferencesController from './controllers/preferences'
import AppStateController from './controllers/app-state' import AppStateController from './controllers/app-state'
import InfuraController from './controllers/infura'
import CachedBalancesController from './controllers/cached-balances' import CachedBalancesController from './controllers/cached-balances'
import AlertController from './controllers/alert' import AlertController from './controllers/alert'
import OnboardingController from './controllers/onboarding' import OnboardingController from './controllers/onboarding'
@ -128,11 +127,6 @@ export default class MetamaskController extends EventEmitter {
this.currencyRateController = new CurrencyRateController(undefined, initState.CurrencyController) this.currencyRateController = new CurrencyRateController(undefined, initState.CurrencyController)
this.infuraController = new InfuraController({
initState: initState.InfuraController,
})
this.infuraController.scheduleInfuraNetworkCheck()
this.phishingController = new PhishingController() this.phishingController = new PhishingController()
// now we can initialize the RPC provider, which other controllers require // now we can initialize the RPC provider, which other controllers require
@ -170,9 +164,11 @@ export default class MetamaskController extends EventEmitter {
if (activeControllerConnections > 0) { if (activeControllerConnections > 0) {
this.accountTracker.start() this.accountTracker.start()
this.incomingTransactionsController.start() this.incomingTransactionsController.start()
this.tokenRatesController.start()
} else { } else {
this.accountTracker.stop() this.accountTracker.stop()
this.incomingTransactionsController.stop() this.incomingTransactionsController.stop()
this.tokenRatesController.stop()
} }
}) })
@ -281,10 +277,6 @@ export default class MetamaskController extends EventEmitter {
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager() this.encryptionPublicKeyManager = new EncryptionPublicKeyManager()
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
// ensure isClientOpenAndUnlocked is updated when memState updates
this.on('update', (memState) => {
this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen
})
this.store.updateStructure({ this.store.updateStructure({
AppStateController: this.appStateController.store, AppStateController: this.appStateController.store,
@ -294,7 +286,6 @@ export default class MetamaskController extends EventEmitter {
AddressBookController: this.addressBookController, AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController, CurrencyController: this.currencyRateController,
NetworkController: this.networkController.store, NetworkController: this.networkController.store,
InfuraController: this.infuraController.store,
CachedBalancesController: this.cachedBalancesController.store, CachedBalancesController: this.cachedBalancesController.store,
AlertController: this.alertController.store, AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store, OnboardingController: this.onboardingController.store,
@ -320,7 +311,6 @@ export default class MetamaskController extends EventEmitter {
PreferencesController: this.preferencesController.store, PreferencesController: this.preferencesController.store,
AddressBookController: this.addressBookController, AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController, CurrencyController: this.currencyRateController,
InfuraController: this.infuraController.store,
AlertController: this.alertController.store, AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store, OnboardingController: this.onboardingController.store,
IncomingTransactionsController: this.incomingTransactionsController.store, IncomingTransactionsController: this.incomingTransactionsController.store,
@ -459,6 +449,9 @@ export default class MetamaskController extends EventEmitter {
markPasswordForgotten: this.markPasswordForgotten.bind(this), markPasswordForgotten: this.markPasswordForgotten.bind(this),
unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this), unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
buyEth: this.buyEth.bind(this), buyEth: this.buyEth.bind(this),
safelistPhishingDomain: this.safelistPhishingDomain.bind(this),
getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()),
getOpenMetamaskTabsIds: (cb) => cb(null, this.getOpenMetamaskTabsIds()),
// primary HD keyring management // primary HD keyring management
addNewAccount: nodeify(this.addNewAccount, this), addNewAccount: nodeify(this.addNewAccount, this),
@ -496,9 +489,6 @@ export default class MetamaskController extends EventEmitter {
completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController), completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController),
addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController), addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
// AddressController // AddressController
setAddressBook: nodeify(this.addressBookController.set, this.addressBookController), setAddressBook: nodeify(this.addressBookController.set, this.addressBookController),
removeFromAddressBook: this.addressBookController.delete.bind(this.addressBookController), removeFromAddressBook: this.addressBookController.delete.bind(this.addressBookController),
@ -574,9 +564,6 @@ export default class MetamaskController extends EventEmitter {
addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController), addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController),
removePermittedAccount: nodeify(permissionsController.removePermittedAccount, permissionsController), removePermittedAccount: nodeify(permissionsController.removePermittedAccount, permissionsController),
requestAccountsPermission: nodeify(permissionsController.requestAccountsPermission, permissionsController), requestAccountsPermission: nodeify(permissionsController.requestAccountsPermission, permissionsController),
getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()),
getOpenMetamaskTabsIds: (cb) => cb(null, this.getOpenMetamaskTabsIds()),
} }
} }
@ -1448,7 +1435,7 @@ export default class MetamaskController extends EventEmitter {
setupUntrustedCommunication (connectionStream, sender) { setupUntrustedCommunication (connectionStream, sender) {
const { usePhishDetect } = this.preferencesController.store.getState() const { usePhishDetect } = this.preferencesController.store.getState()
const hostname = (new URL(sender.url)).hostname const hostname = (new URL(sender.url)).hostname
// Check if new connection is blacklisted if phishing detection is on // Check if new connection is blocked if phishing detection is on
if (usePhishDetect && this.phishingController.test(hostname)) { if (usePhishDetect && this.phishingController.test(hostname)) {
log.debug('MetaMask - sending phishing warning for', hostname) log.debug('MetaMask - sending phishing warning for', hostname)
this.sendPhishingWarning(connectionStream, hostname) this.sendPhishingWarning(connectionStream, hostname)
@ -1487,7 +1474,7 @@ export default class MetamaskController extends EventEmitter {
* @private * @private
* @param {*} connectionStream - The duplex stream to the per-page script, * @param {*} connectionStream - The duplex stream to the per-page script,
* for sending the reload attempt to. * for sending the reload attempt to.
* @param {string} hostname - The URL that triggered the suspicion. * @param {string} hostname - The hostname that triggered the suspicion.
*/ */
sendPhishingWarning (connectionStream, hostname) { sendPhishingWarning (connectionStream, hostname) {
const mux = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
@ -1538,7 +1525,7 @@ export default class MetamaskController extends EventEmitter {
setupProviderConnection (outStream, sender, isInternal) { setupProviderConnection (outStream, sender, isInternal) {
const origin = isInternal const origin = isInternal
? 'metamask' ? 'metamask'
: (new URL(sender.url)).hostname : (new URL(sender.url)).origin
let extensionId let extensionId
if (sender.id !== extension.runtime.id) { if (sender.id !== extension.runtime.id) {
extensionId = sender.id extensionId = sender.id
@ -1577,7 +1564,7 @@ export default class MetamaskController extends EventEmitter {
/** /**
* A method for creating a provider that is safely restricted for the requesting domain. * A method for creating a provider that is safely restricted for the requesting domain.
* @param {Object} options - Provider engine options * @param {Object} options - Provider engine options
* @param {string} options.origin - The hostname of the sender * @param {string} options.origin - The origin of the sender
* @param {string} options.location - The full URL of the sender * @param {string} options.location - The full URL of the sender
* @param {extensionId} [options.extensionId] - The extension ID of the sender, if the sender is an external extension * @param {extensionId} [options.extensionId] - The extension ID of the sender, if the sender is an external extension
* @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab * @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab
@ -2032,20 +2019,9 @@ export default class MetamaskController extends EventEmitter {
*/ */
set isClientOpen (open) { set isClientOpen (open) {
this._isClientOpen = open this._isClientOpen = open
this.isClientOpenAndUnlocked = this.isUnlocked() && open
this.detectTokensController.isOpen = open this.detectTokensController.isOpen = open
} }
/**
* A method for activating the retrieval of price data,
* which should only be fetched when the UI is visible.
* @private
* @param {boolean} active - True if price data should be getting fetched.
*/
set isClientOpenAndUnlocked (active) {
this.tokenRatesController.isActive = active
}
/** /**
* Creates RPC engine middleware for processing eth_signTypedData requests * Creates RPC engine middleware for processing eth_signTypedData requests
* *
@ -2056,10 +2032,10 @@ export default class MetamaskController extends EventEmitter {
*/ */
/** /**
* Adds a domain to the PhishingController whitelist * Adds a domain to the PhishingController safelist
* @param {string} hostname - the domain to whitelist * @param {string} hostname - the domain to safelist
*/ */
whitelistPhishingDomain (hostname) { safelistPhishingDomain (hostname) {
return this.phishingController.bypass(hostname) return this.phishingController.bypass(hostname)
} }

@ -27,7 +27,7 @@ function start () {
const continueLink = document.getElementById('unsafe-continue') const continueLink = document.getElementById('unsafe-continue')
continueLink.addEventListener('click', () => { continueLink.addEventListener('click', () => {
metaMaskController.whitelistPhishingDomain(suspect.hostname) metaMaskController.safelistPhishingDomain(suspect.hostname)
window.location.href = suspect.href window.location.href = suspect.href
}) })
}) })

@ -21,7 +21,6 @@ import { EventEmitter } from 'events'
import Dnode from 'dnode' import Dnode from 'dnode'
import Eth from 'ethjs' import Eth from 'ethjs'
import EthQuery from 'eth-query' import EthQuery from 'eth-query'
import urlUtil from 'url'
import launchMetaMaskUi from '../../ui' import launchMetaMaskUi from '../../ui'
import StreamProvider from 'web3-stream-provider' import StreamProvider from 'web3-stream-provider'
import { setupMultiplex } from './lib/stream-utils.js' import { setupMultiplex } from './lib/stream-utils.js'
@ -95,10 +94,9 @@ async function queryCurrentActiveTab (windowType) {
extension.tabs.query({ active: true, currentWindow: true }, (tabs) => { extension.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const [activeTab] = tabs const [activeTab] = tabs
const { title, url } = activeTab const { title, url } = activeTab
const { hostname: origin, protocol } = url ? urlUtil.parse(url) : {} const { origin, protocol } = url ? new URL(url) : {}
resolve({
title, origin, protocol, url, resolve({ title, origin, protocol, url })
})
}) })
}) })
} }

@ -1,5 +1,7 @@
## Add Custom Build to Chrome ## Add Custom Build to Chrome
![Load dev build](./load-dev-build-chrome.gif)
* Open `Settings` > `Extensions`. * Open `Settings` > `Extensions`.
* Check "Developer mode". * Check "Developer mode".
* Alternatively, use the URL `chrome://extensions/` in your address bar * Alternatively, use the URL `chrome://extensions/` in your address bar

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -151,7 +151,7 @@ describe('MetaMask', function () {
}) })
it('balance renders', async function () { it('balance renders', async function () {
const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .asset-list__primary-amount')) const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
await driver.wait(until.elementTextMatches(balance, /25\s*ETH/)) await driver.wait(until.elementTextMatches(balance, /25\s*ETH/))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
}) })

@ -3,7 +3,6 @@ const webdriver = require('selenium-webdriver')
const { By, Key, until } = webdriver const { By, Key, until } = webdriver
const { const {
tinyDelayMs,
regularDelayMs, regularDelayMs,
largeDelayMs, largeDelayMs,
} = require('./helpers') } = require('./helpers')
@ -278,7 +277,7 @@ describe('Using MetaMask with an existing account', function () {
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
}) })
it('should open the remove account modal', async function () { it('should see new account in account menu', async function () {
const accountName = await driver.findElement(By.css('.selected-account__name')) const accountName = await driver.findElement(By.css('.selected-account__name'))
assert.equal(await accountName.getText(), 'Account 5') assert.equal(await accountName.getText(), 'Account 5')
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
@ -289,8 +288,13 @@ describe('Using MetaMask with an existing account', function () {
const accountListItems = await driver.findElements(By.css('.account-menu__account')) const accountListItems = await driver.findElements(By.css('.account-menu__account'))
assert.equal(accountListItems.length, 5) assert.equal(accountListItems.length, 5)
await driver.clickElement(By.css('.account-menu__account:last-of-type > .remove-account-icon')) await driver.clickPoint(By.css('.account-menu__icon'), 0, 0)
await driver.delay(tinyDelayMs) })
it('should open the remove account modal', async function () {
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
await driver.clickElement(By.css('[data-testid="account-options-menu__remove-account"]'))
await driver.findElement(By.css('.confirm-remove-account__account')) await driver.findElement(By.css('.confirm-remove-account__account'))
}) })
@ -304,6 +308,8 @@ describe('Using MetaMask with an existing account', function () {
assert.equal(await accountName.getText(), 'Account 1') assert.equal(await accountName.getText(), 'Account 1')
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
await driver.clickElement(By.css('.account-menu__icon'))
const accountListItems = await driver.findElements(By.css('.account-menu__account')) const accountListItems = await driver.findElements(By.css('.account-menu__account'))
assert.equal(accountListItems.length, 4) assert.equal(accountListItems.length, 4)
}) })

@ -207,7 +207,7 @@ describe('MetaMask', function () {
}) })
it('balance renders', async function () { it('balance renders', async function () {
const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .asset-list__primary-amount')) const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
await driver.wait(until.elementTextMatches(balance, /100\s*ETH/)) await driver.wait(until.elementTextMatches(balance, /100\s*ETH/))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
}) })
@ -472,10 +472,8 @@ describe('MetaMask', function () {
const txValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency')) const txValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency'))
await txValue.click() await txValue.click()
const popoverCloseButton = await driver.findClickableElement(By.css('.popover-header__button')) const popoverCloseButton = await driver.findClickableElement(By.css('.popover-header__button'))
const txGasPrices = await driver.findElements(By.css('.transaction-breakdown__value')) const txGasPrice = await driver.findElement(By.css('[data-testid="transaction-breakdown__gas-price"]'))
const txGasPriceLabels = await driver.findElements(By.css('.transaction-breakdown-row__title')) await driver.wait(until.elementTextMatches(txGasPrice, /^10$/), 10000)
await driver.wait(until.elementTextMatches(txGasPrices[4], /^10$/), 10000)
assert(txGasPriceLabels[2])
await popoverCloseButton.click() await popoverCloseButton.click()
}) })
}) })
@ -1223,8 +1221,7 @@ describe('MetaMask', function () {
const confirmHideModal = await driver.findElement(By.css('span .modal')) const confirmHideModal = await driver.findElement(By.css('span .modal'))
const byHideTokenConfirmationButton = By.css('.hide-token-confirmation__button') await driver.clickElement(By.css('[data-testid="hide-token-confirmation__hide"]'))
await driver.clickElement(byHideTokenConfirmationButton)
await driver.wait(until.stalenessOf(confirmHideModal)) await driver.wait(until.stalenessOf(confirmHideModal))
}) })
@ -1232,7 +1229,6 @@ describe('MetaMask', function () {
describe('Add existing token using search', function () { describe('Add existing token using search', function () {
it('clicks on the Add Token button', async function () { it('clicks on the Add Token button', async function () {
await driver.clickElement(By.css('[data-testid="asset__back"]'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Add Token')]`)) await driver.clickElement(By.xpath(`//button[contains(text(), 'Add Token')]`))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
}) })

@ -107,7 +107,7 @@ describe('MetaMask', function () {
const address = content[1] const address = content[1]
assert.equal(await title.getText(), 'Signature Request') assert.equal(await title.getText(), 'Signature Request')
assert.equal(await name.getText(), 'Ether Mail') assert.equal(await name.getText(), 'Ether Mail')
assert.equal(await origin.getText(), '127.0.0.1') assert.equal(await origin.getText(), 'http://127.0.0.1:8080')
assert.equal(await address.getText(), publicAddress.slice(0, 8) + '...' + publicAddress.slice(publicAddress.length - 8)) assert.equal(await address.getText(), publicAddress.slice(0, 8) + '...' + publicAddress.slice(publicAddress.length - 8))
}) })

@ -96,7 +96,7 @@ describe('MetaMask', function () {
}) })
it('balance renders', async function () { it('balance renders', async function () {
const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .asset-list__primary-amount')) const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
await driver.wait(until.elementTextMatches(balance, /25\s*ETH/)) await driver.wait(until.elementTextMatches(balance, /25\s*ETH/))
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
}) })
@ -202,7 +202,7 @@ describe('MetaMask', function () {
}) })
it('balance renders', async function () { it('balance renders', async function () {
const balance = await driver2.findElement(By.css('[data-testid="wallet-balance"] .asset-list__primary-amount')) const balance = await driver2.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
await driver2.wait(until.elementTextMatches(balance, /25\s*ETH/)) await driver2.wait(until.elementTextMatches(balance, /25\s*ETH/))
await driver2.delay(regularDelayMs) await driver2.delay(regularDelayMs)
}) })
@ -223,7 +223,7 @@ describe('MetaMask', function () {
it('finds the blockies toggle turned on', async function () { it('finds the blockies toggle turned on', async function () {
await driver.delay(regularDelayMs) await driver.delay(regularDelayMs)
const toggleLabel = await driver.findElement(By.css('.toggle-button__status-label')) const toggleLabel = await driver.findElement(By.css('.toggle-button__status'))
const toggleLabelText = await toggleLabel.getText() const toggleLabelText = await toggleLabel.getText()
assert.equal(toggleLabelText, 'ON') assert.equal(toggleLabelText, 'ON')
}) })

@ -1,66 +0,0 @@
import assert from 'assert'
import sinon from 'sinon'
import InfuraController from '../../../../app/scripts/controllers/infura'
describe('infura-controller', function () {
let infuraController, networkStatus
const response = { 'mainnet': 'degraded', 'ropsten': 'ok', 'kovan': 'ok', 'rinkeby': 'down', 'goerli': 'ok' }
describe('Network status queries', function () {
before(async function () {
infuraController = new InfuraController()
sinon.stub(infuraController, 'checkInfuraNetworkStatus').resolves(response)
networkStatus = await infuraController.checkInfuraNetworkStatus()
})
describe('Mainnet', function () {
it('should have Mainnet', function () {
assert.equal(Object.keys(networkStatus)[0], 'mainnet')
})
it('should have a value for Mainnet status', function () {
assert.equal(networkStatus.mainnet, 'degraded')
})
})
describe('Ropsten', function () {
it('should have Ropsten', function () {
assert.equal(Object.keys(networkStatus)[1], 'ropsten')
})
it('should have a value for Ropsten status', function () {
assert.equal(networkStatus.ropsten, 'ok')
})
})
describe('Kovan', function () {
it('should have Kovan', function () {
assert.equal(Object.keys(networkStatus)[2], 'kovan')
})
it('should have a value for Kovan status', function () {
assert.equal(networkStatus.kovan, 'ok')
})
})
describe('Rinkeby', function () {
it('should have Rinkeby', function () {
assert.equal(Object.keys(networkStatus)[3], 'rinkeby')
})
it('should have a value for Rinkeby status', function () {
assert.equal(networkStatus.rinkeby, 'down')
})
})
describe('Goerli', function () {
it('should have Goerli', function () {
assert.equal(Object.keys(networkStatus)[4], 'goerli')
})
it('should have a value for Goerli status', function () {
assert.equal(networkStatus.goerli, 'ok')
})
})
})
})

@ -824,7 +824,7 @@ describe('MetaMaskController', function () {
'mock tx params', 'mock tx params',
{ {
...message, ...message,
origin: 'mycrypto.com', origin: 'http://mycrypto.com',
tabId: 456, tabId: 456,
}, },
] ]
@ -865,7 +865,7 @@ describe('MetaMaskController', function () {
'mock tx params', 'mock tx params',
{ {
...message, ...message,
origin: 'mycrypto.com', origin: 'http://mycrypto.com',
}, },
] ]
) )

@ -151,10 +151,10 @@ export const getNotifyAllDomains = (notifications = {}) => (notification) => {
* - e.g. permissions, caveats, and permission requests * - e.g. permissions, caveats, and permission requests
*/ */
const ORIGINS = { const DOMAINS = {
a: 'foo.xyz', a: { origin: 'https://foo.xyz', host: 'foo.xyz' },
b: 'bar.abc', b: { origin: 'https://bar.abc', host: 'bar.abc' },
c: 'baz.def', c: { origin: 'https://baz.def', host: 'baz.def' },
} }
const PERM_NAMES = { const PERM_NAMES = {
@ -446,6 +446,19 @@ export const getters = deepFreeze({
} }
}, },
}, },
notifyAccountsChanged: {
invalidOrigin: (origin) => {
return {
message: `Invalid origin: '${origin}'`,
}
},
invalidAccounts: () => {
return {
message: 'Invalid accounts',
}
},
},
}, },
/** /**
@ -477,18 +490,6 @@ export const getters = deepFreeze({
result: accounts, result: accounts,
} }
}, },
/**
* Gets a test notification that doesn't occur in practice.
*
* @returns {Object} A notification with the 'test_notification' method name
*/
test: () => {
return {
method: 'test_notification',
result: true,
}
},
}, },
/** /**
@ -629,7 +630,7 @@ export const constants = deepFreeze({
c: '3', c: '3',
}, },
ORIGINS: { ...ORIGINS }, DOMAINS: { ...DOMAINS },
ACCOUNTS: { ...ACCOUNTS }, ACCOUNTS: { ...ACCOUNTS },
@ -647,7 +648,7 @@ export const constants = deepFreeze({
case1: [ case1: [
{ {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 1, lastApproved: 1,
accounts: { accounts: {
@ -659,7 +660,7 @@ export const constants = deepFreeze({
}, },
}, },
{ {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 2, lastApproved: 2,
accounts: { accounts: {
@ -674,7 +675,7 @@ export const constants = deepFreeze({
case2: [ case2: [
{ {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 1, lastApproved: 1,
accounts: {}, accounts: {},
@ -685,10 +686,10 @@ export const constants = deepFreeze({
case3: [ case3: [
{ {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 1 }, [PERM_NAMES.test_method]: { lastApproved: 1 },
}, },
[ORIGINS.b]: { [DOMAINS.b.origin]: {
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 1, lastApproved: 1,
accounts: { accounts: {
@ -696,7 +697,7 @@ export const constants = deepFreeze({
}, },
}, },
}, },
[ORIGINS.c]: { [DOMAINS.c.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 1 }, [PERM_NAMES.test_method]: { lastApproved: 1 },
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 1, lastApproved: 1,
@ -707,10 +708,10 @@ export const constants = deepFreeze({
}, },
}, },
{ {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 2 }, [PERM_NAMES.test_method]: { lastApproved: 2 },
}, },
[ORIGINS.b]: { [DOMAINS.b.origin]: {
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 1, lastApproved: 1,
accounts: { accounts: {
@ -718,7 +719,7 @@ export const constants = deepFreeze({
}, },
}, },
}, },
[ORIGINS.c]: { [DOMAINS.c.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 1 }, [PERM_NAMES.test_method]: { lastApproved: 1 },
[PERM_NAMES.eth_accounts]: { [PERM_NAMES.eth_accounts]: {
lastApproved: 2, lastApproved: 2,
@ -733,7 +734,7 @@ export const constants = deepFreeze({
case4: [ case4: [
{ {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
[PERM_NAMES.test_method]: { [PERM_NAMES.test_method]: {
lastApproved: 1, lastApproved: 1,
}, },

@ -37,15 +37,15 @@ const {
ALL_ACCOUNTS, ALL_ACCOUNTS,
ACCOUNTS, ACCOUNTS,
DUMMY_ACCOUNT, DUMMY_ACCOUNT,
ORIGINS, DOMAINS,
PERM_NAMES, PERM_NAMES,
REQUEST_IDS, REQUEST_IDS,
EXTRA_ACCOUNT, EXTRA_ACCOUNT,
} = constants } = constants
const initNotifications = () => { const initNotifications = () => {
return Object.values(ORIGINS).reduce((acc, domain) => { return Object.values(DOMAINS).reduce((acc, domain) => {
acc[domain] = [] acc[domain.origin] = []
return acc return acc
}, {}) }, {})
} }
@ -73,19 +73,19 @@ describe('permissions controller', function () {
beforeEach(function () { beforeEach(function () {
permController = initPermController() permController = initPermController()
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
) )
}) })
it('gets permitted accounts for permitted origins', async function () { it('gets permitted accounts for permitted origins', async function () {
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
const bAccounts = await permController.getAccounts(ORIGINS.b) const bAccounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary], aAccounts, [ACCOUNTS.a.primary],
@ -98,7 +98,7 @@ describe('permissions controller', function () {
}) })
it('does not get accounts for unpermitted origins', async function () { it('does not get accounts for unpermitted origins', async function () {
const cAccounts = await permController.getAccounts(ORIGINS.c) const cAccounts = await permController.getAccounts(DOMAINS.c.origin)
assert.deepEqual(cAccounts, [], 'origin should have no accounts') assert.deepEqual(cAccounts, [], 'origin should have no accounts')
}) })
@ -114,29 +114,29 @@ describe('permissions controller', function () {
const permController = initPermController() const permController = initPermController()
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.test_method() PERMS.finalizedRequests.test_method()
) )
assert.ok( assert.ok(
permController.hasPermission(ORIGINS.a, 'eth_accounts'), permController.hasPermission(DOMAINS.a.origin, 'eth_accounts'),
'should return true for granted permission' 'should return true for granted permission'
) )
assert.ok( assert.ok(
permController.hasPermission(ORIGINS.b, 'test_method'), permController.hasPermission(DOMAINS.b.origin, 'test_method'),
'should return true for granted permission' 'should return true for granted permission'
) )
assert.ok( assert.ok(
!permController.hasPermission(ORIGINS.a, 'test_method'), !permController.hasPermission(DOMAINS.a.origin, 'test_method'),
'should return false for non-granted permission' 'should return false for non-granted permission'
) )
assert.ok( assert.ok(
!permController.hasPermission(ORIGINS.b, 'eth_accounts'), !permController.hasPermission(DOMAINS.b.origin, 'eth_accounts'),
'should return true for non-granted permission' 'should return true for non-granted permission'
) )
@ -145,7 +145,7 @@ describe('permissions controller', function () {
'should return false for unknown origin' 'should return false for unknown origin'
) )
assert.ok( assert.ok(
!permController.hasPermission(ORIGINS.b, 'foo'), !permController.hasPermission(DOMAINS.b.origin, 'foo'),
'should return false for unknown permission' 'should return false for unknown permission'
) )
}) })
@ -159,21 +159,21 @@ describe('permissions controller', function () {
const permController = initPermController(notifications) const permController = initPermController(notifications)
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.c, permController, DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted)
) )
let aAccounts = await permController.getAccounts(ORIGINS.a) let aAccounts = await permController.getAccounts(DOMAINS.a.origin)
let bAccounts = await permController.getAccounts(ORIGINS.b) let bAccounts = await permController.getAccounts(DOMAINS.b.origin)
let cAccounts = await permController.getAccounts(ORIGINS.c) let cAccounts = await permController.getAccounts(DOMAINS.c.origin)
assert.deepEqual( assert.deepEqual(
@ -199,9 +199,9 @@ describe('permissions controller', function () {
) )
}) })
aAccounts = await permController.getAccounts(ORIGINS.a) aAccounts = await permController.getAccounts(DOMAINS.a.origin)
bAccounts = await permController.getAccounts(ORIGINS.b) bAccounts = await permController.getAccounts(DOMAINS.b.origin)
cAccounts = await permController.getAccounts(ORIGINS.c) cAccounts = await permController.getAccounts(DOMAINS.c.origin)
assert.deepEqual(aAccounts, [], 'first origin should have no accounts') assert.deepEqual(aAccounts, [], 'first origin should have no accounts')
assert.deepEqual(bAccounts, [], 'second origin should have no accounts') assert.deepEqual(bAccounts, [], 'second origin should have no accounts')
@ -230,19 +230,19 @@ describe('permissions controller', function () {
notifications = initNotifications() notifications = initNotifications()
permController = initPermController(notifications) permController = initPermController(notifications)
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
) )
}) })
it('removes permissions for multiple domains', async function () { it('removes permissions for multiple domains', async function () {
let aAccounts = await permController.getAccounts(ORIGINS.a) let aAccounts = await permController.getAccounts(DOMAINS.a.origin)
let bAccounts = await permController.getAccounts(ORIGINS.b) let bAccounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary], aAccounts, [ACCOUNTS.a.primary],
@ -254,22 +254,22 @@ describe('permissions controller', function () {
) )
permController.removePermissionsFor({ permController.removePermissionsFor({
[ORIGINS.a]: [PERM_NAMES.eth_accounts], [DOMAINS.a.origin]: [PERM_NAMES.eth_accounts],
[ORIGINS.b]: [PERM_NAMES.eth_accounts], [DOMAINS.b.origin]: [PERM_NAMES.eth_accounts],
}) })
aAccounts = await permController.getAccounts(ORIGINS.a) aAccounts = await permController.getAccounts(DOMAINS.a.origin)
bAccounts = await permController.getAccounts(ORIGINS.b) bAccounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual(aAccounts, [], 'first origin should have no accounts') assert.deepEqual(aAccounts, [], 'first origin should have no accounts')
assert.deepEqual(bAccounts, [], 'second origin should have no accounts') assert.deepEqual(bAccounts, [], 'second origin should have no accounts')
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.a], [NOTIFICATIONS.removedAccounts()], notifications[DOMAINS.a.origin], [NOTIFICATIONS.removedAccounts()],
'first origin should have correct notification' 'first origin should have correct notification'
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b], [NOTIFICATIONS.removedAccounts()], notifications[DOMAINS.b.origin], [NOTIFICATIONS.removedAccounts()],
'second origin should have correct notification' 'second origin should have correct notification'
) )
@ -282,10 +282,10 @@ describe('permissions controller', function () {
it('only removes targeted permissions from single domain', async function () { it('only removes targeted permissions from single domain', async function () {
grantPermissions( grantPermissions(
permController, ORIGINS.b, PERMS.finalizedRequests.test_method() permController, DOMAINS.b.origin, PERMS.finalizedRequests.test_method()
) )
let bPermissions = permController.permissions.getPermissionsForDomain(ORIGINS.b) let bPermissions = permController.permissions.getPermissionsForDomain(DOMAINS.b.origin)
assert.ok( assert.ok(
( (
@ -297,10 +297,10 @@ describe('permissions controller', function () {
) )
permController.removePermissionsFor({ permController.removePermissionsFor({
[ORIGINS.b]: [PERM_NAMES.test_method], [DOMAINS.b.origin]: [PERM_NAMES.test_method],
}) })
bPermissions = permController.permissions.getPermissionsForDomain(ORIGINS.b) bPermissions = permController.permissions.getPermissionsForDomain(DOMAINS.b.origin)
assert.ok( assert.ok(
( (
@ -314,11 +314,11 @@ describe('permissions controller', function () {
it('removes permissions for a single domain, without affecting another', async function () { it('removes permissions for a single domain, without affecting another', async function () {
permController.removePermissionsFor({ permController.removePermissionsFor({
[ORIGINS.b]: [PERM_NAMES.eth_accounts], [DOMAINS.b.origin]: [PERM_NAMES.eth_accounts],
}) })
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
const bAccounts = await permController.getAccounts(ORIGINS.b) const bAccounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary], aAccounts, [ACCOUNTS.a.primary],
@ -327,16 +327,16 @@ describe('permissions controller', function () {
assert.deepEqual(bAccounts, [], 'second origin should have no accounts') assert.deepEqual(bAccounts, [], 'second origin should have no accounts')
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.a], [], notifications[DOMAINS.a.origin], [],
'first origin should have no notifications' 'first origin should have no notifications'
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b], [NOTIFICATIONS.removedAccounts()], notifications[DOMAINS.b.origin], [NOTIFICATIONS.removedAccounts()],
'second origin should have correct notification' 'second origin should have correct notification'
) )
assert.deepEqual( assert.deepEqual(
Object.keys(permController.permissions.getDomains()), [ORIGINS.a], Object.keys(permController.permissions.getDomains()), [DOMAINS.a.origin],
'only first origin should remain' 'only first origin should remain'
) )
}) })
@ -345,16 +345,16 @@ describe('permissions controller', function () {
// it knows nothing of this origin // it knows nothing of this origin
permController.removePermissionsFor({ permController.removePermissionsFor({
[ORIGINS.c]: [PERM_NAMES.eth_accounts], [DOMAINS.c.origin]: [PERM_NAMES.eth_accounts],
}) })
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.c], [NOTIFICATIONS.removedAccounts()], notifications[DOMAINS.c.origin], [NOTIFICATIONS.removedAccounts()],
'unknown origin should have notification' 'unknown origin should have notification'
) )
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
const bAccounts = await permController.getAccounts(ORIGINS.b) const bAccounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary], aAccounts, [ACCOUNTS.a.primary],
@ -367,7 +367,7 @@ describe('permissions controller', function () {
assert.deepEqual( assert.deepEqual(
Object.keys(permController.permissions.getDomains()), Object.keys(permController.permissions.getDomains()),
[ORIGINS.a, ORIGINS.b], [DOMAINS.a.origin, DOMAINS.b.origin],
'should have correct domains' 'should have correct domains'
) )
}) })
@ -380,11 +380,11 @@ describe('permissions controller', function () {
beforeEach(function () { beforeEach(function () {
permController = initPermController() permController = initPermController()
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
) )
}) })
@ -470,18 +470,18 @@ describe('permissions controller', function () {
notifications = initNotifications() notifications = initNotifications()
permController = initPermController(notifications) permController = initPermController(notifications)
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
) )
}) })
it('should throw if account is not a string', async function () { it('should throw if account is not a string', async function () {
await assert.rejects( await assert.rejects(
() => permController.addPermittedAccount(ORIGINS.a, {}), () => permController.addPermittedAccount(DOMAINS.a.origin, {}),
ERRORS.validatePermittedAccounts.nonKeyringAccount({}), ERRORS.validatePermittedAccounts.nonKeyringAccount({}),
'should throw on non-string account param' 'should throw on non-string account param'
) )
@ -489,7 +489,7 @@ describe('permissions controller', function () {
it('should throw if given account is not in keyring', async function () { it('should throw if given account is not in keyring', async function () {
await assert.rejects( await assert.rejects(
() => permController.addPermittedAccount(ORIGINS.a, DUMMY_ACCOUNT), () => permController.addPermittedAccount(DOMAINS.a.origin, DUMMY_ACCOUNT),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT), ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account' 'should throw on non-keyring account'
) )
@ -505,7 +505,7 @@ describe('permissions controller', function () {
it('should throw if origin lacks any permissions', async function () { it('should throw if origin lacks any permissions', async function () {
await assert.rejects( await assert.rejects(
() => permController.addPermittedAccount(ORIGINS.c, EXTRA_ACCOUNT), () => permController.addPermittedAccount(DOMAINS.c.origin, EXTRA_ACCOUNT),
ERRORS.addPermittedAccount.invalidOrigin(), ERRORS.addPermittedAccount.invalidOrigin(),
'should throw on origin without permissions' 'should throw on origin without permissions'
) )
@ -513,12 +513,12 @@ describe('permissions controller', function () {
it('should throw if origin lacks eth_accounts permission', async function () { it('should throw if origin lacks eth_accounts permission', async function () {
grantPermissions( grantPermissions(
permController, ORIGINS.c, permController, DOMAINS.c.origin,
PERMS.finalizedRequests.test_method() PERMS.finalizedRequests.test_method()
) )
await assert.rejects( await assert.rejects(
() => permController.addPermittedAccount(ORIGINS.c, EXTRA_ACCOUNT), () => permController.addPermittedAccount(DOMAINS.c.origin, EXTRA_ACCOUNT),
ERRORS.addPermittedAccount.noEthAccountsPermission(), ERRORS.addPermittedAccount.noEthAccountsPermission(),
'should throw on origin without eth_accounts permission' 'should throw on origin without eth_accounts permission'
) )
@ -526,16 +526,16 @@ describe('permissions controller', function () {
it('should throw if account is already permitted', async function () { it('should throw if account is already permitted', async function () {
await assert.rejects( await assert.rejects(
() => permController.addPermittedAccount(ORIGINS.a, ACCOUNTS.a.permitted[0]), () => permController.addPermittedAccount(DOMAINS.a.origin, ACCOUNTS.a.permitted[0]),
ERRORS.addPermittedAccount.alreadyPermitted(), ERRORS.addPermittedAccount.alreadyPermitted(),
'should throw if account is already permitted' 'should throw if account is already permitted'
) )
}) })
it('should successfully add permitted account', async function () { it('should successfully add permitted account', async function () {
await permController.addPermittedAccount(ORIGINS.a, EXTRA_ACCOUNT) await permController.addPermittedAccount(DOMAINS.a.origin, EXTRA_ACCOUNT)
const accounts = await permController._getPermittedAccounts(ORIGINS.a) const accounts = await permController._getPermittedAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
accounts, [...ACCOUNTS.a.permitted, EXTRA_ACCOUNT], accounts, [...ACCOUNTS.a.permitted, EXTRA_ACCOUNT],
@ -543,7 +543,7 @@ describe('permissions controller', function () {
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.a][0], notifications[DOMAINS.a.origin][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]), NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'origin should have correct notification' 'origin should have correct notification'
) )
@ -557,18 +557,18 @@ describe('permissions controller', function () {
notifications = initNotifications() notifications = initNotifications()
permController = initPermController(notifications) permController = initPermController(notifications)
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted)
) )
}) })
it('should throw if account is not a string', async function () { it('should throw if account is not a string', async function () {
await assert.rejects( await assert.rejects(
() => permController.removePermittedAccount(ORIGINS.a, {}), () => permController.removePermittedAccount(DOMAINS.a.origin, {}),
ERRORS.validatePermittedAccounts.nonKeyringAccount({}), ERRORS.validatePermittedAccounts.nonKeyringAccount({}),
'should throw on non-string account param' 'should throw on non-string account param'
) )
@ -576,7 +576,7 @@ describe('permissions controller', function () {
it('should throw if given account is not in keyring', async function () { it('should throw if given account is not in keyring', async function () {
await assert.rejects( await assert.rejects(
() => permController.removePermittedAccount(ORIGINS.a, DUMMY_ACCOUNT), () => permController.removePermittedAccount(DOMAINS.a.origin, DUMMY_ACCOUNT),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT), ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account' 'should throw on non-keyring account'
) )
@ -592,7 +592,7 @@ describe('permissions controller', function () {
it('should throw if origin lacks any permissions', async function () { it('should throw if origin lacks any permissions', async function () {
await assert.rejects( await assert.rejects(
() => permController.removePermittedAccount(ORIGINS.c, EXTRA_ACCOUNT), () => permController.removePermittedAccount(DOMAINS.c.origin, EXTRA_ACCOUNT),
ERRORS.removePermittedAccount.invalidOrigin(), ERRORS.removePermittedAccount.invalidOrigin(),
'should throw on origin without permissions' 'should throw on origin without permissions'
) )
@ -600,12 +600,12 @@ describe('permissions controller', function () {
it('should throw if origin lacks eth_accounts permission', async function () { it('should throw if origin lacks eth_accounts permission', async function () {
grantPermissions( grantPermissions(
permController, ORIGINS.c, permController, DOMAINS.c.origin,
PERMS.finalizedRequests.test_method() PERMS.finalizedRequests.test_method()
) )
await assert.rejects( await assert.rejects(
() => permController.removePermittedAccount(ORIGINS.c, EXTRA_ACCOUNT), () => permController.removePermittedAccount(DOMAINS.c.origin, EXTRA_ACCOUNT),
ERRORS.removePermittedAccount.noEthAccountsPermission(), ERRORS.removePermittedAccount.noEthAccountsPermission(),
'should throw on origin without eth_accounts permission' 'should throw on origin without eth_accounts permission'
) )
@ -613,16 +613,16 @@ describe('permissions controller', function () {
it('should throw if account is not permitted', async function () { it('should throw if account is not permitted', async function () {
await assert.rejects( await assert.rejects(
() => permController.removePermittedAccount(ORIGINS.b, ACCOUNTS.c.permitted[0]), () => permController.removePermittedAccount(DOMAINS.b.origin, ACCOUNTS.c.permitted[0]),
ERRORS.removePermittedAccount.notPermitted(), ERRORS.removePermittedAccount.notPermitted(),
'should throw if account is not permitted' 'should throw if account is not permitted'
) )
}) })
it('should successfully remove permitted account', async function () { it('should successfully remove permitted account', async function () {
await permController.removePermittedAccount(ORIGINS.a, ACCOUNTS.a.permitted[1]) await permController.removePermittedAccount(DOMAINS.a.origin, ACCOUNTS.a.permitted[1])
const accounts = await permController._getPermittedAccounts(ORIGINS.a) const accounts = await permController._getPermittedAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
accounts, ACCOUNTS.a.permitted.filter((acc) => acc !== ACCOUNTS.a.permitted[1]), accounts, ACCOUNTS.a.permitted.filter((acc) => acc !== ACCOUNTS.a.permitted[1]),
@ -630,16 +630,16 @@ describe('permissions controller', function () {
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.a][0], notifications[DOMAINS.a.origin][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]), NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'origin should have correct notification' 'origin should have correct notification'
) )
}) })
it('should remove eth_accounts permission if removing only permitted account', async function () { it('should remove eth_accounts permission if removing only permitted account', async function () {
await permController.removePermittedAccount(ORIGINS.b, ACCOUNTS.b.permitted[0]) await permController.removePermittedAccount(DOMAINS.b.origin, ACCOUNTS.b.permitted[0])
const accounts = await permController.getAccounts(ORIGINS.b) const accounts = await permController.getAccounts(DOMAINS.b.origin)
assert.deepEqual( assert.deepEqual(
accounts, [], accounts, [],
@ -647,13 +647,13 @@ describe('permissions controller', function () {
) )
const permission = await permController.permissions.getPermission( const permission = await permController.permissions.getPermission(
ORIGINS.b, PERM_NAMES.eth_accounts DOMAINS.b.origin, PERM_NAMES.eth_accounts
) )
assert.equal(permission, undefined, 'origin should not have eth_accounts permission') assert.equal(permission, undefined, 'origin should not have eth_accounts permission')
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b][0], notifications[DOMAINS.b.origin][0],
NOTIFICATIONS.removedAccounts(), NOTIFICATIONS.removedAccounts(),
'origin should have correct notification' 'origin should have correct notification'
) )
@ -744,11 +744,11 @@ describe('permissions controller', function () {
preferences, preferences,
}) })
grantPermissions( grantPermissions(
permController, ORIGINS.b, permController, DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts([...ACCOUNTS.a.permitted, EXTRA_ACCOUNT]) PERMS.finalizedRequests.eth_accounts([...ACCOUNTS.a.permitted, EXTRA_ACCOUNT])
) )
grantPermissions( grantPermissions(
permController, ORIGINS.c, permController, DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
}) })
@ -774,11 +774,11 @@ describe('permissions controller', function () {
await onPreferencesUpdate({ selectedAddress: DUMMY_ACCOUNT }) await onPreferencesUpdate({ selectedAddress: DUMMY_ACCOUNT })
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b], [], notifications[DOMAINS.b.origin], [],
'should not have emitted notification' 'should not have emitted notification'
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.c], [], notifications[DOMAINS.c.origin], [],
'should not have emitted notification' 'should not have emitted notification'
) )
}) })
@ -792,12 +792,12 @@ describe('permissions controller', function () {
await onPreferencesUpdate({ selectedAddress: ACCOUNTS.a.permitted[0] }) await onPreferencesUpdate({ selectedAddress: ACCOUNTS.a.permitted[0] })
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b], notifications[DOMAINS.b.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary])], [NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary])],
'should not have emitted notification' 'should not have emitted notification'
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.c], notifications[DOMAINS.c.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary])], [NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary])],
'should not have emitted notification' 'should not have emitted notification'
) )
@ -812,12 +812,12 @@ describe('permissions controller', function () {
await onPreferencesUpdate({ selectedAddress: EXTRA_ACCOUNT }) await onPreferencesUpdate({ selectedAddress: EXTRA_ACCOUNT })
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b], notifications[DOMAINS.b.origin],
[NOTIFICATIONS.newAccounts([EXTRA_ACCOUNT])], [NOTIFICATIONS.newAccounts([EXTRA_ACCOUNT])],
'should have emitted notification' 'should have emitted notification'
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.c], [], notifications[DOMAINS.c.origin], [],
'should not have emitted notification' 'should not have emitted notification'
) )
}) })
@ -831,12 +831,12 @@ describe('permissions controller', function () {
await onPreferencesUpdate({ selectedAddress: ACCOUNTS.a.permitted[1] }) await onPreferencesUpdate({ selectedAddress: ACCOUNTS.a.permitted[1] })
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.b], notifications[DOMAINS.b.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.a.permitted[1]])], [NOTIFICATIONS.newAccounts([ACCOUNTS.a.permitted[1]])],
'should have emitted notification' 'should have emitted notification'
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.c], notifications[DOMAINS.c.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.c.primary])], [NOTIFICATIONS.newAccounts([ACCOUNTS.c.primary])],
'should have emitted notification' 'should have emitted notification'
) )
@ -1113,7 +1113,7 @@ describe('permissions controller', function () {
let middleware let middleware
assert.doesNotThrow( assert.doesNotThrow(
() => { () => {
middleware = permController.createMiddleware({ origin: ORIGINS.a }) middleware = permController.createMiddleware({ origin: DOMAINS.a.origin })
}, },
'should not throw' 'should not throw'
) )
@ -1137,7 +1137,7 @@ describe('permissions controller', function () {
assert.doesNotThrow( assert.doesNotThrow(
() => { () => {
middleware = permController.createMiddleware({ middleware = permController.createMiddleware({
origin: ORIGINS.a, origin: DOMAINS.a.origin,
extensionId, extensionId,
}) })
}, },
@ -1157,13 +1157,13 @@ describe('permissions controller', function () {
const metadataStore = permController.store.getState()[METADATA_STORE_KEY] const metadataStore = permController.store.getState()[METADATA_STORE_KEY]
assert.deepEqual( assert.deepEqual(
metadataStore[ORIGINS.a], { extensionId, lastUpdated: 1 }, metadataStore[DOMAINS.a.origin], { extensionId, lastUpdated: 1 },
'metadata should be stored' 'metadata should be stored'
) )
}) })
}) })
describe('notifyDomain', function () { describe('notifyAccountsChanged', function () {
let notifications, permController let notifications, permController
@ -1173,11 +1173,11 @@ describe('permissions controller', function () {
sinon.spy(permController.permissionsLog, 'updateAccountsHistory') sinon.spy(permController.permissionsLog, 'updateAccountsHistory')
}) })
it('notifyDomain handles accountsChanged', async function () { it('notifyAccountsChanged records history and sends notification', async function () {
permController.notifyDomain( permController.notifyAccountsChanged(
ORIGINS.a, DOMAINS.a.origin,
NOTIFICATIONS.newAccounts(ACCOUNTS.a.permitted), ACCOUNTS.a.permitted,
) )
assert.ok( assert.ok(
@ -1186,25 +1186,51 @@ describe('permissions controller', function () {
) )
assert.deepEqual( assert.deepEqual(
notifications[ORIGINS.a], notifications[DOMAINS.a.origin],
[ NOTIFICATIONS.newAccounts(ACCOUNTS.a.permitted) ], [ NOTIFICATIONS.newAccounts(ACCOUNTS.a.permitted) ],
'origin should have correct notification' 'origin should have correct notification'
) )
}) })
it('notifyDomain handles notifications other than accountsChanged', async function () { it('notifyAccountsChanged throws on invalid origin', async function () {
permController.notifyDomain(ORIGINS.a, NOTIFICATIONS.test()) assert.throws(
() => permController.notifyAccountsChanged(
4,
ACCOUNTS.a.permitted,
),
ERRORS.notifyAccountsChanged.invalidOrigin(4),
'should throw expected error for non-string origin'
)
assert.ok( assert.throws(
permController.permissionsLog.updateAccountsHistory.notCalled, () => permController.notifyAccountsChanged(
'permissionsLog.updateAccountsHistory should not have been called' '',
ACCOUNTS.a.permitted,
),
ERRORS.notifyAccountsChanged.invalidOrigin(''),
'should throw expected error for empty string origin'
) )
})
assert.deepEqual( it('notifyAccountsChanged throws on invalid accounts', async function () {
notifications[ORIGINS.a],
[ NOTIFICATIONS.test() ], assert.throws(
'origin should have correct notification' () => permController.notifyAccountsChanged(
DOMAINS.a.origin,
4,
),
ERRORS.notifyAccountsChanged.invalidAccounts(),
'should throw expected error for truthy non-array accounts'
)
assert.throws(
() => permController.notifyAccountsChanged(
DOMAINS.a.origin,
null,
),
ERRORS.notifyAccountsChanged.invalidAccounts(),
'should throw expected error for falsy non-array accounts'
) )
}) })
}) })
@ -1236,13 +1262,13 @@ describe('permissions controller', function () {
permController.store.getState = sinon.fake.returns({ permController.store.getState = sinon.fake.returns({
[METADATA_STORE_KEY]: { [METADATA_STORE_KEY]: {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
foo: 'bar', foo: 'bar',
}, },
}, },
}) })
permController.addDomainMetadata(ORIGINS.b, { foo: 'bar' }) permController.addDomainMetadata(DOMAINS.b.origin, { foo: 'bar' })
assert.ok( assert.ok(
permController.store.getState.called, permController.store.getState.called,
@ -1255,11 +1281,12 @@ describe('permissions controller', function () {
assert.deepEqual( assert.deepEqual(
permController._setDomainMetadata.lastCall.args, permController._setDomainMetadata.lastCall.args,
[{ [{
[ORIGINS.a]: { [DOMAINS.a.origin]: {
foo: 'bar', foo: 'bar',
}, },
[ORIGINS.b]: { [DOMAINS.b.origin]: {
foo: 'bar', foo: 'bar',
host: DOMAINS.b.host,
lastUpdated: 1, lastUpdated: 1,
}, },
}] }]
@ -1270,16 +1297,16 @@ describe('permissions controller', function () {
permController.store.getState = sinon.fake.returns({ permController.store.getState = sinon.fake.returns({
[METADATA_STORE_KEY]: { [METADATA_STORE_KEY]: {
[ORIGINS.a]: { [DOMAINS.a.origin]: {
foo: 'bar', foo: 'bar',
}, },
[ORIGINS.b]: { [DOMAINS.b.origin]: {
bar: 'baz', bar: 'baz',
}, },
}, },
}) })
permController.addDomainMetadata(ORIGINS.b, { foo: 'bar' }) permController.addDomainMetadata(DOMAINS.b.origin, { foo: 'bar' })
assert.ok( assert.ok(
permController.store.getState.called, permController.store.getState.called,
@ -1292,12 +1319,13 @@ describe('permissions controller', function () {
assert.deepEqual( assert.deepEqual(
permController._setDomainMetadata.lastCall.args, permController._setDomainMetadata.lastCall.args,
[{ [{
[ORIGINS.a]: { [DOMAINS.a.origin]: {
foo: 'bar', foo: 'bar',
}, },
[ORIGINS.b]: { [DOMAINS.b.origin]: {
foo: 'bar', foo: 'bar',
bar: 'baz', bar: 'baz',
host: DOMAINS.b.host,
lastUpdated: 1, lastUpdated: 1,
}, },
}] }]
@ -1321,7 +1349,7 @@ describe('permissions controller', function () {
permController._pendingSiteMetadata.add(origin) permController._pendingSiteMetadata.add(origin)
}) })
permController.addDomainMetadata(ORIGINS.a, { foo: 'bar' }) permController.addDomainMetadata(DOMAINS.a.origin, { foo: 'bar' })
assert.ok( assert.ok(
permController.store.getState.called, permController.store.getState.called,
@ -1330,8 +1358,9 @@ describe('permissions controller', function () {
const expectedMetadata = { const expectedMetadata = {
...mockMetadata, ...mockMetadata,
[ORIGINS.a]: { [DOMAINS.a.origin]: {
foo: 'bar', foo: 'bar',
host: DOMAINS.a.host,
lastUpdated: 1, lastUpdated: 1,
}, },
} }
@ -1359,12 +1388,12 @@ describe('permissions controller', function () {
it('trims domain metadata for domains without permissions', function () { it('trims domain metadata for domains without permissions', function () {
const metadataArg = { const metadataArg = {
[ORIGINS.a]: {}, [DOMAINS.a.origin]: {},
[ORIGINS.b]: {}, [DOMAINS.b.origin]: {},
} }
permController.permissions.getDomains = sinon.fake.returns({ permController.permissions.getDomains = sinon.fake.returns({
[ORIGINS.a]: {}, [DOMAINS.a.origin]: {},
}) })
const metadataResult = permController._trimDomainMetadata(metadataArg) const metadataResult = permController._trimDomainMetadata(metadataArg)
@ -1376,7 +1405,7 @@ describe('permissions controller', function () {
assert.deepEqual( assert.deepEqual(
metadataResult, metadataResult,
{ {
[ORIGINS.a]: {}, [DOMAINS.a.origin]: {},
}, },
'should have produced expected state' 'should have produced expected state'
) )
@ -1404,7 +1433,7 @@ describe('permissions controller', function () {
it('_addPendingApproval: should throw if adding origin twice', function () { it('_addPendingApproval: should throw if adding origin twice', function () {
const id = nanoid() const id = nanoid()
const origin = ORIGINS.a const origin = DOMAINS.a
permController._addPendingApproval(id, origin, noop, noop) permController._addPendingApproval(id, origin, noop, noop)

@ -29,7 +29,7 @@ const {
const { const {
ACCOUNTS, ACCOUNTS,
EXPECTED_HISTORIES, EXPECTED_HISTORIES,
ORIGINS, DOMAINS,
PERM_NAMES, PERM_NAMES,
REQUEST_IDS, REQUEST_IDS,
RESTRICTED_METHODS, RESTRICTED_METHODS,
@ -86,7 +86,7 @@ describe('permissions log', function () {
// test_method, success // test_method, success
req = RPC_REQUESTS.test_method(ORIGINS.a) req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
req.id = REQUEST_IDS.a req.id = REQUEST_IDS.a
res = { foo: 'bar' } res = { foo: 'bar' }
@ -103,7 +103,7 @@ describe('permissions log', function () {
// eth_accounts, failure // eth_accounts, failure
req = RPC_REQUESTS.eth_accounts(ORIGINS.b) req = RPC_REQUESTS.eth_accounts(DOMAINS.b.origin)
req.id = REQUEST_IDS.b req.id = REQUEST_IDS.b
res = { error: new Error('Unauthorized.') } res = { error: new Error('Unauthorized.') }
@ -120,7 +120,7 @@ describe('permissions log', function () {
// eth_requestAccounts, success // eth_requestAccounts, success
req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.c) req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin)
req.id = REQUEST_IDS.c req.id = REQUEST_IDS.c
res = { result: ACCOUNTS.c.permitted } res = { result: ACCOUNTS.c.permitted }
@ -137,7 +137,7 @@ describe('permissions log', function () {
// test_method, no response // test_method, no response
req = RPC_REQUESTS.test_method(ORIGINS.a) req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
req.id = REQUEST_IDS.a req.id = REQUEST_IDS.a
res = null res = null
@ -170,7 +170,7 @@ describe('permissions log', function () {
const id2 = nanoid() const id2 = nanoid()
const id3 = nanoid() const id3 = nanoid()
const req = RPC_REQUESTS.test_method(ORIGINS.a) const req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
// get make requests // get make requests
req.id = id1 req.id = id1
@ -230,7 +230,7 @@ describe('permissions log', function () {
it('handles a lack of response', function () { it('handles a lack of response', function () {
let req = RPC_REQUESTS.test_method(ORIGINS.a) let req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
req.id = REQUEST_IDS.a req.id = REQUEST_IDS.a
let res = { foo: 'bar' } let res = { foo: 'bar' }
@ -247,7 +247,7 @@ describe('permissions log', function () {
) )
// next request should be handled as normal // next request should be handled as normal
req = RPC_REQUESTS.eth_accounts(ORIGINS.b) req = RPC_REQUESTS.eth_accounts(DOMAINS.b.origin)
req.id = REQUEST_IDS.b req.id = REQUEST_IDS.b
res = { result: ACCOUNTS.b.permitted } res = { result: ACCOUNTS.b.permitted }
@ -272,9 +272,9 @@ describe('permissions log', function () {
assert.equal(log.length, 0, 'log should be empty') assert.equal(log.length, 0, 'log should be empty')
const res = { foo: 'bar' } const res = { foo: 'bar' }
const req1 = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, 'foobar') const req1 = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, 'foobar')
const req2 = RPC_REQUESTS.custom(ORIGINS.b, 'eth_getBlockNumber') const req2 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'eth_getBlockNumber')
const req3 = RPC_REQUESTS.custom(ORIGINS.b, 'net_version') const req3 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'net_version')
logMiddleware(req1, res) logMiddleware(req1, res)
logMiddleware(req2, res) logMiddleware(req2, res)
@ -286,7 +286,7 @@ describe('permissions log', function () {
it('enforces log limit', function () { it('enforces log limit', function () {
const req = RPC_REQUESTS.test_method(ORIGINS.a) const req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
const res = { foo: 'bar' } const res = { foo: 'bar' }
// max out log // max out log
@ -352,7 +352,7 @@ describe('permissions log', function () {
let permHistory let permHistory
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
) )
const res = { result: [ PERMS.granted.test_method() ] } const res = { result: [ PERMS.granted.test_method() ] }
@ -371,7 +371,7 @@ describe('permissions log', function () {
'history should have single origin' 'history should have single origin'
) )
assert.ok( assert.ok(
Boolean(permHistory[ORIGINS.a]), Boolean(permHistory[DOMAINS.a.origin]),
'history should have expected origin' 'history should have expected origin'
) )
}) })
@ -379,7 +379,7 @@ describe('permissions log', function () {
it('ignores malformed permissions requests', function () { it('ignores malformed permissions requests', function () {
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
) )
delete req.params delete req.params
const res = { result: [ PERMS.granted.test_method() ] } const res = { result: [ PERMS.granted.test_method() ] }
@ -395,7 +395,7 @@ describe('permissions log', function () {
let permHistory let permHistory
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res = { const res = {
result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted) ], result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted) ],
@ -433,7 +433,7 @@ describe('permissions log', function () {
it('handles eth_accounts response without caveats', async function () { it('handles eth_accounts response without caveats', async function () {
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res = { const res = {
result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted) ], result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted) ],
@ -453,7 +453,7 @@ describe('permissions log', function () {
it('handles extra caveats for eth_accounts', async function () { it('handles extra caveats for eth_accounts', async function () {
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res = { const res = {
result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted) ], result: [ PERMS.granted.eth_accounts(ACCOUNTS.a.permitted) ],
@ -476,7 +476,7 @@ describe('permissions log', function () {
it('handles unrequested permissions on the response', async function () { it('handles unrequested permissions on the response', async function () {
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res = { const res = {
result: [ result: [
@ -499,7 +499,7 @@ describe('permissions log', function () {
it('does not update history if no new permissions are approved', async function () { it('does not update history if no new permissions are approved', async function () {
let req = RPC_REQUESTS.requestPermission( let req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
) )
let res = { let res = {
result: [ result: [
@ -522,7 +522,7 @@ describe('permissions log', function () {
clock.tick(1) clock.tick(1)
req = RPC_REQUESTS.requestPermission( req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
res = { res = {
result: [ result: [
@ -553,7 +553,7 @@ describe('permissions log', function () {
// first origin // first origin
round1.push({ round1.push({
req: RPC_REQUESTS.requestPermission( req: RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
), ),
res: { res: {
result: [ PERMS.granted.test_method() ], result: [ PERMS.granted.test_method() ],
@ -563,7 +563,7 @@ describe('permissions log', function () {
// second origin // second origin
round1.push({ round1.push({
req: RPC_REQUESTS.requestPermission( req: RPC_REQUESTS.requestPermission(
ORIGINS.b, PERM_NAMES.eth_accounts DOMAINS.b.origin, PERM_NAMES.eth_accounts
), ),
res: { res: {
result: [ PERMS.granted.eth_accounts(ACCOUNTS.b.permitted) ], result: [ PERMS.granted.eth_accounts(ACCOUNTS.b.permitted) ],
@ -572,7 +572,7 @@ describe('permissions log', function () {
// third origin // third origin
round1.push({ round1.push({
req: RPC_REQUESTS.requestPermissions(ORIGINS.c, { req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, {
[PERM_NAMES.test_method]: {}, [PERM_NAMES.test_method]: {},
[PERM_NAMES.eth_accounts]: {}, [PERM_NAMES.eth_accounts]: {},
}), }),
@ -611,7 +611,7 @@ describe('permissions log', function () {
// first origin // first origin
round2.push({ round2.push({
req: RPC_REQUESTS.requestPermission( req: RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
), ),
res: { res: {
result: [ PERMS.granted.test_method() ], result: [ PERMS.granted.test_method() ],
@ -622,7 +622,7 @@ describe('permissions log', function () {
// third origin // third origin
round2.push({ round2.push({
req: RPC_REQUESTS.requestPermissions(ORIGINS.c, { req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, {
[PERM_NAMES.eth_accounts]: {}, [PERM_NAMES.eth_accounts]: {},
}), }),
res: { res: {

@ -1,5 +1,5 @@
import { strict as assert } from 'assert' import { strict as assert } from 'assert'
import { useFakeTimers } from 'sinon' import sinon from 'sinon'
import { import {
METADATA_STORE_KEY, METADATA_STORE_KEY,
@ -30,7 +30,7 @@ const {
const { const {
ACCOUNTS, ACCOUNTS,
ORIGINS, DOMAINS,
PERM_NAMES, PERM_NAMES,
} = constants } = constants
@ -58,14 +58,15 @@ describe('permissions middleware', function () {
beforeEach(function () { beforeEach(function () {
permController = initPermController() permController = initPermController()
permController.notifyAccountsChanged = sinon.fake()
}) })
it('grants permissions on user approval', async function () { it('grants permissions on user approval', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res = {} const res = {}
@ -98,25 +99,32 @@ describe('permissions middleware', function () {
validatePermission( validatePermission(
res.result[0], res.result[0],
PERM_NAMES.eth_accounts, PERM_NAMES.eth_accounts,
ORIGINS.a, DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted) CAVEATS.eth_accounts(ACCOUNTS.a.permitted)
) )
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary], aAccounts, [ACCOUNTS.a.primary],
'origin should have correct accounts' 'origin should have correct accounts'
) )
assert.ok(
permController.notifyAccountsChanged.calledOnceWith(
DOMAINS.a.origin, aAccounts,
),
'expected notification call should have been made'
)
}) })
it('handles serial approved requests that overwrite existing permissions', async function () { it('handles serial approved requests that overwrite existing permissions', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
// create first request // create first request
const req1 = RPC_REQUESTS.requestPermission( const req1 = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res1 = {} const res1 = {}
@ -147,16 +155,23 @@ describe('permissions middleware', function () {
validatePermission( validatePermission(
res1.result[0], res1.result[0],
PERM_NAMES.eth_accounts, PERM_NAMES.eth_accounts,
ORIGINS.a, DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted) CAVEATS.eth_accounts(ACCOUNTS.a.permitted)
) )
const accounts1 = await permController.getAccounts(ORIGINS.a) const accounts1 = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
accounts1, [ACCOUNTS.a.primary], accounts1, [ACCOUNTS.a.primary],
'origin should have correct accounts' 'origin should have correct accounts'
) )
assert.ok(
permController.notifyAccountsChanged.calledOnceWith(
DOMAINS.a.origin, accounts1,
),
'expected notification call should have been made'
)
// create second request // create second request
const requestedPerms2 = { const requestedPerms2 = {
@ -165,7 +180,7 @@ describe('permissions middleware', function () {
} }
const req2 = RPC_REQUESTS.requestPermissions( const req2 = RPC_REQUESTS.requestPermissions(
ORIGINS.a, { ...requestedPerms2 } DOMAINS.a.origin, { ...requestedPerms2 }
) )
const res2 = {} const res2 = {}
@ -196,29 +211,41 @@ describe('permissions middleware', function () {
validatePermission( validatePermission(
res2.result[0], res2.result[0],
PERM_NAMES.eth_accounts, PERM_NAMES.eth_accounts,
ORIGINS.a, DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.b.permitted) CAVEATS.eth_accounts(ACCOUNTS.b.permitted)
) )
validatePermission( validatePermission(
res2.result[1], res2.result[1],
PERM_NAMES.test_method, PERM_NAMES.test_method,
ORIGINS.a, DOMAINS.a.origin,
) )
const accounts2 = await permController.getAccounts(ORIGINS.a) const accounts2 = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
accounts2, [ACCOUNTS.b.primary], accounts2, [ACCOUNTS.b.primary],
'origin should have correct accounts' 'origin should have correct accounts'
) )
assert.equal(
permController.notifyAccountsChanged.callCount, 2,
'should have called notification method 2 times in total'
)
assert.ok(
permController.notifyAccountsChanged.lastCall.calledWith(
DOMAINS.a.origin, accounts2,
),
'expected notification call should have been made'
)
}) })
it('rejects permissions on user rejection', async function () { it('rejects permissions on user rejection', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.requestPermission( const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts DOMAINS.a.origin, PERM_NAMES.eth_accounts
) )
const res = {} const res = {}
@ -248,18 +275,23 @@ describe('permissions middleware', function () {
'response should have expected error and no result' 'response should have expected error and no result'
) )
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [], 'origin should have have correct accounts' aAccounts, [], 'origin should have have correct accounts'
) )
assert.ok(
permController.notifyAccountsChanged.notCalled,
'should not have called notification method'
)
}) })
it('rejects requests with unknown permissions', async function () { it('rejects requests with unknown permissions', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.requestPermissions( const req = RPC_REQUESTS.requestPermissions(
ORIGINS.a, { DOMAINS.a.origin, {
...PERMS.requests.does_not_exist(), ...PERMS.requests.does_not_exist(),
...PERMS.requests.test_method(), ...PERMS.requests.test_method(),
} }
@ -288,6 +320,11 @@ describe('permissions middleware', function () {
), ),
'response should have expected error and no result' 'response should have expected error and no result'
) )
assert.ok(
permController.notifyAccountsChanged.notCalled,
'should not have called notification method'
)
}) })
it('accepts only a single pending permissions request per origin', async function () { it('accepts only a single pending permissions request per origin', async function () {
@ -296,13 +333,13 @@ describe('permissions middleware', function () {
// two middlewares for two origins // two middlewares for two origins
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const bMiddleware = getPermissionsMiddleware(permController, ORIGINS.b) const bMiddleware = getPermissionsMiddleware(permController, DOMAINS.b.origin)
// create and start processing first request for first origin // create and start processing first request for first origin
const reqA1 = RPC_REQUESTS.requestPermission( const reqA1 = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
) )
const resA1 = {} const resA1 = {}
@ -314,7 +351,7 @@ describe('permissions middleware', function () {
// create and start processing first request for second origin // create and start processing first request for second origin
const reqB1 = RPC_REQUESTS.requestPermission( const reqB1 = RPC_REQUESTS.requestPermission(
ORIGINS.b, PERM_NAMES.test_method DOMAINS.b.origin, PERM_NAMES.test_method
) )
const resB1 = {} const resB1 = {}
@ -332,7 +369,7 @@ describe('permissions middleware', function () {
// which should throw // which should throw
const reqA2 = RPC_REQUESTS.requestPermission( const reqA2 = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method DOMAINS.a.origin, PERM_NAMES.test_method
) )
const resA2 = {} const resA2 = {}
@ -402,9 +439,9 @@ describe('permissions middleware', function () {
it('prevents restricted method access for unpermitted domain', async function () { it('prevents restricted method access for unpermitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.test_method(ORIGINS.a) const req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
const res = {} const res = {}
const expectedError = ERRORS.rpcCap.unauthorized() const expectedError = ERRORS.rpcCap.unauthorized()
@ -426,11 +463,11 @@ describe('permissions middleware', function () {
it('allows restricted method access for permitted domain', async function () { it('allows restricted method access for permitted domain', async function () {
const bMiddleware = getPermissionsMiddleware(permController, ORIGINS.b) const bMiddleware = getPermissionsMiddleware(permController, DOMAINS.b.origin)
grantPermissions(permController, ORIGINS.b, PERMS.finalizedRequests.test_method()) grantPermissions(permController, DOMAINS.b.origin, PERMS.finalizedRequests.test_method())
const req = RPC_REQUESTS.test_method(ORIGINS.b, true) const req = RPC_REQUESTS.test_method(DOMAINS.b.origin, true)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -455,9 +492,9 @@ describe('permissions middleware', function () {
it('returns empty array for non-permitted domain', async function () { it('returns empty array for non-permitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.eth_accounts(ORIGINS.a) const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -477,14 +514,14 @@ describe('permissions middleware', function () {
it('returns correct accounts for permitted domain', async function () { it('returns correct accounts for permitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
grantPermissions( grantPermissions(
permController, ORIGINS.a, permController, DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
) )
const req = RPC_REQUESTS.eth_accounts(ORIGINS.a) const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -515,9 +552,9 @@ describe('permissions middleware', function () {
const userApprovalPromise = getUserApprovalPromise(permController) const userApprovalPromise = getUserApprovalPromise(permController)
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.a) const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin)
const res = {} const res = {}
const pendingApproval = assert.doesNotReject( const pendingApproval = assert.doesNotReject(
@ -540,7 +577,7 @@ describe('permissions middleware', function () {
// wait for permission to be granted // wait for permission to be granted
await pendingApproval await pendingApproval
const perms = permController.permissions.getPermissionsForDomain(ORIGINS.a) const perms = permController.permissions.getPermissionsForDomain(DOMAINS.a.origin)
assert.equal( assert.equal(
perms.length, 1, perms.length, 1,
@ -550,7 +587,7 @@ describe('permissions middleware', function () {
validatePermission( validatePermission(
perms[0], perms[0],
PERM_NAMES.eth_accounts, PERM_NAMES.eth_accounts,
ORIGINS.a, DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted) CAVEATS.eth_accounts(ACCOUNTS.a.permitted)
) )
@ -566,7 +603,7 @@ describe('permissions middleware', function () {
) )
// we should also be able to get the accounts independently // we should also be able to get the accounts independently
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary], 'origin should have have correct accounts' aAccounts, [ACCOUNTS.a.primary], 'origin should have have correct accounts'
) )
@ -576,9 +613,9 @@ describe('permissions middleware', function () {
const userApprovalPromise = getUserApprovalPromise(permController) const userApprovalPromise = getUserApprovalPromise(permController)
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a) const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.a) const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin)
const res = {} const res = {}
const expectedError = ERRORS.rejectPermissionsRequest.rejection() const expectedError = ERRORS.rejectPermissionsRequest.rejection()
@ -609,7 +646,7 @@ describe('permissions middleware', function () {
'response should have expected error and no result' 'response should have expected error and no result'
) )
const aAccounts = await permController.getAccounts(ORIGINS.a) const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual( assert.deepEqual(
aAccounts, [], 'origin should have have correct accounts' aAccounts, [], 'origin should have have correct accounts'
) )
@ -617,14 +654,14 @@ describe('permissions middleware', function () {
it('directly returns accounts for permitted domain', async function () { it('directly returns accounts for permitted domain', async function () {
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c) const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
grantPermissions( grantPermissions(
permController, ORIGINS.c, permController, DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted)
) )
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.c) const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -651,14 +688,14 @@ describe('permissions middleware', function () {
permController.getUnlockPromise = () => unlockPromise permController.getUnlockPromise = () => unlockPromise
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c) const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
grantPermissions( grantPermissions(
permController, ORIGINS.c, permController, DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted) PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted)
) )
const req = RPC_REQUESTS.eth_requestAccounts(ORIGINS.c) const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin)
const res = {} const res = {}
// this will block until we resolve the unlock Promise // this will block until we resolve the unlock Promise
@ -695,7 +732,7 @@ describe('permissions middleware', function () {
beforeEach(function () { beforeEach(function () {
permController = initPermController() permController = initPermController()
clock = useFakeTimers(1) clock = sinon.useFakeTimers(1)
}) })
afterEach(function () { afterEach(function () {
@ -706,9 +743,9 @@ describe('permissions middleware', function () {
const name = 'BAZ' const name = 'BAZ'
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c) const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, name) const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -722,7 +759,13 @@ describe('permissions middleware', function () {
assert.deepEqual( assert.deepEqual(
metadataStore, metadataStore,
{ [ORIGINS.c]: { name, lastUpdated: 1 } }, {
[DOMAINS.c.origin]: {
name,
host: DOMAINS.c.host,
lastUpdated: 1,
},
},
'metadata should have been added to store' 'metadata should have been added to store'
) )
}) })
@ -733,9 +776,9 @@ describe('permissions middleware', function () {
const name = 'BAZ' const name = 'BAZ'
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c, extensionId) const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin, extensionId)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, name) const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -749,7 +792,7 @@ describe('permissions middleware', function () {
assert.deepEqual( assert.deepEqual(
metadataStore, metadataStore,
{ [ORIGINS.c]: { name, extensionId, lastUpdated: 1 } }, { [DOMAINS.c.origin]: { name, extensionId, lastUpdated: 1 } },
'metadata should have been added to store' 'metadata should have been added to store'
) )
}) })
@ -758,9 +801,9 @@ describe('permissions middleware', function () {
const name = null const name = null
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c) const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c, name) const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
const res = {} const res = {}
await assert.doesNotReject( await assert.doesNotReject(
@ -780,9 +823,9 @@ describe('permissions middleware', function () {
it('should not record domain metadata if no metadata', async function () { it('should not record domain metadata if no metadata', async function () {
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c) const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(ORIGINS.c) const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin)
delete req.domainMetadata delete req.domainMetadata
const res = {} const res = {}

@ -13,8 +13,11 @@ describe('TokenRatesController', function () {
it('should poll on correct interval', async function () { it('should poll on correct interval', async function () {
const stub = sinon.stub(global, 'setInterval') const stub = sinon.stub(global, 'setInterval')
new TokenRatesController({ interval: 1337 }) // eslint-disable-line no-new const rateController = new TokenRatesController() // eslint-disable-line no-new
rateController.start(1337)
assert.strictEqual(stub.getCall(0).args[1], 1337) assert.strictEqual(stub.getCall(0).args[1], 1337)
stub.restore() stub.restore()
rateController.stop()
}) })
}) })

@ -1,9 +1,9 @@
import { strict as assert } from 'assert' import { strict as assert } from 'assert'
import { throwIfAccountIsBlacklisted } from '../../../../../app/scripts/controllers/transactions/lib/recipient-blacklist-checker' import { throwIfAccountIsBlocked } from '../../../../../app/scripts/controllers/transactions/lib/recipient-blocklist-checker'
import { ROPSTEN_NETWORK_ID, RINKEBY_NETWORK_ID, KOVAN_NETWORK_ID, GOERLI_NETWORK_ID } from '../../../../../app/scripts/controllers/network/enums' import { ROPSTEN_NETWORK_ID, RINKEBY_NETWORK_ID, KOVAN_NETWORK_ID, GOERLI_NETWORK_ID } from '../../../../../app/scripts/controllers/network/enums'
describe('Recipient Blacklist Checker', function () { describe('Recipient Blocklist Checker', function () {
describe('#throwIfAccountIsBlacklisted', function () { describe('#throwIfAccountIsBlocked', function () {
// Accounts from Ganache's original default seed phrase // Accounts from Ganache's original default seed phrase
const publicAccounts = [ const publicAccounts = [
'0x627306090abab3a6e1400e9345bc60c78a8bef57', '0x627306090abab3a6e1400e9345bc60c78a8bef57',
@ -22,7 +22,7 @@ describe('Recipient Blacklist Checker', function () {
const networks = [ROPSTEN_NETWORK_ID, RINKEBY_NETWORK_ID, KOVAN_NETWORK_ID, GOERLI_NETWORK_ID] const networks = [ROPSTEN_NETWORK_ID, RINKEBY_NETWORK_ID, KOVAN_NETWORK_ID, GOERLI_NETWORK_ID]
for (const networkId of networks) { for (const networkId of networks) {
for (const account of publicAccounts) { for (const account of publicAccounts) {
assert.doesNotThrow(() => throwIfAccountIsBlacklisted(networkId, account)) assert.doesNotThrow(() => throwIfAccountIsBlocked(networkId, account))
} }
} }
}) })
@ -30,7 +30,7 @@ describe('Recipient Blacklist Checker', function () {
it('fails on mainnet', function () { it('fails on mainnet', function () {
for (const account of publicAccounts) { for (const account of publicAccounts) {
assert.throws( assert.throws(
() => throwIfAccountIsBlacklisted(1, account), () => throwIfAccountIsBlocked(1, account),
{ message: 'Recipient is a public account' }, { message: 'Recipient is a public account' },
) )
} }
@ -38,14 +38,14 @@ describe('Recipient Blacklist Checker', function () {
it('fails for public account - uppercase', function () { it('fails for public account - uppercase', function () {
assert.throws( assert.throws(
() => throwIfAccountIsBlacklisted(1, '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2'), () => throwIfAccountIsBlocked(1, '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2'),
{ message: 'Recipient is a public account' }, { message: 'Recipient is a public account' },
) )
}) })
it('fails for public account - lowercase', function () { it('fails for public account - lowercase', function () {
assert.throws( assert.throws(
() => throwIfAccountIsBlacklisted(1, '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2'), () => throwIfAccountIsBlocked(1, '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2'),
{ message: 'Recipient is a public account' }, { message: 'Recipient is a public account' },
) )
}) })

@ -5,7 +5,7 @@ import {
replayHistory, replayHistory,
generateHistoryEntry, generateHistoryEntry,
} from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helpers' } from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helpers'
import testVault from '../../../../data/v17-long-history.json' import testData from '../../../../data/mock-tx-history.json'
describe('Transaction state history helper', function () { describe('Transaction state history helper', function () {
describe('#snapshotFromTxMeta', function () { describe('#snapshotFromTxMeta', function () {
@ -33,7 +33,7 @@ describe('Transaction state history helper', function () {
describe('#migrateFromSnapshotsToDiffs', function () { describe('#migrateFromSnapshotsToDiffs', function () {
it('migrates history to diffs and can recover original values', function () { it('migrates history to diffs and can recover original values', function () {
testVault.data.TransactionController.transactions.forEach((tx) => { testData.TransactionsController.transactions.forEach((tx) => {
const newHistory = migrateFromSnapshotsToDiffs(tx.history) const newHistory = migrateFromSnapshotsToDiffs(tx.history)
newHistory.forEach((newEntry, index) => { newHistory.forEach((newEntry, index) => {
if (index === 0) { if (index === 0) {

@ -7,7 +7,6 @@ import InputAdornment from '@material-ui/core/InputAdornment'
import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu' import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util' import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import Tooltip from '../../ui/tooltip'
import Identicon from '../../ui/identicon' import Identicon from '../../ui/identicon'
import IconWithFallBack from '../../ui/icon-with-fallback' import IconWithFallBack from '../../ui/icon-with-fallback'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
@ -38,7 +37,6 @@ export default class AccountMenu extends Component {
lockMetamask: PropTypes.func, lockMetamask: PropTypes.func,
selectedAddress: PropTypes.string, selectedAddress: PropTypes.string,
showAccountDetail: PropTypes.func, showAccountDetail: PropTypes.func,
showRemoveAccountConfirmationModal: PropTypes.func,
toggleAccountMenu: PropTypes.func, toggleAccountMenu: PropTypes.func,
addressConnectedDomainMap: PropTypes.object, addressConnectedDomainMap: PropTypes.object,
originOfCurrentTab: PropTypes.string, originOfCurrentTab: PropTypes.string,
@ -176,6 +174,7 @@ export default class AccountMenu extends Component {
type={PRIMARY} type={PRIMARY}
/> />
</div> </div>
{ this.renderKeyringType(keyring) }
{ iconAndNameForOpenDomain { iconAndNameForOpenDomain
? ( ? (
<div className="account-menu__icon-list"> <div className="account-menu__icon-list">
@ -184,45 +183,11 @@ export default class AccountMenu extends Component {
) )
: null : null
} }
{ this.renderKeyringType(keyring) }
{ this.renderRemoveAccount(keyring, identity) }
</div> </div>
) )
}) })
} }
renderRemoveAccount (keyring, identity) {
const { t } = this.context
// Sometimes keyrings aren't loaded yet
if (!keyring) {
return null
}
// Any account that's not from the HD wallet Keyring can be removed
const { type } = keyring
const isRemovable = type !== 'HD Key Tree'
return isRemovable && (
<Tooltip
title={t('removeAccount')}
position="bottom"
>
<a
className="remove-account-icon"
onClick={(e) => this.removeAccount(e, identity)}
/>
</Tooltip>
)
}
removeAccount (e, identity) {
e.preventDefault()
e.stopPropagation()
const { showRemoveAccountConfirmationModal } = this.props
showRemoveAccountConfirmationModal(identity)
}
renderKeyringType (keyring) { renderKeyringType (keyring) {
const { t } = this.context const { t } = this.context

@ -7,7 +7,6 @@ import {
hideSidebar, hideSidebar,
lockMetamask, lockMetamask,
hideWarning, hideWarning,
showModal,
} from '../../../store/actions' } from '../../../store/actions'
import { import {
getAddressConnectedDomainMap, getAddressConnectedDomainMap,
@ -54,9 +53,6 @@ function mapDispatchToProps (dispatch) {
dispatch(hideSidebar()) dispatch(hideSidebar())
dispatch(toggleAccountMenu()) dispatch(toggleAccountMenu())
}, },
showRemoveAccountConfirmationModal: (identity) => {
return dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
},
} }
} }

@ -78,6 +78,7 @@
.keyring-label { .keyring-label {
margin-top: 5px; margin-top: 5px;
margin-right: 10px;
background-color: $dusty-gray; background-color: $dusty-gray;
color: $black; color: $black;
font-weight: normal; font-weight: normal;

@ -99,19 +99,6 @@ describe('Account Menu', function () {
const importedAccount = wrapper.find('.keyring-label.allcaps') const importedAccount = wrapper.find('.keyring-label.allcaps')
assert.equal(importedAccount.text(), 'imported') assert.equal(importedAccount.text(), 'imported')
}) })
it('remove account', function () {
const removeAccount = wrapper.find('.remove-account-icon')
removeAccount.simulate('click', {
preventDefault: () => {},
stopPropagation: () => {},
})
assert(props.showRemoveAccountConfirmationModal.calledOnce)
assert.deepEqual(props.showRemoveAccountConfirmationModal.getCall(0).args[0],
{ address: '0xImportedAddress', balance: '0x0', name: 'Imported Account 1' }
)
})
}) })
describe('Log Out', function () { describe('Log Out', function () {

@ -2,9 +2,12 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import Identicon from '../../ui/identicon' import Identicon from '../../ui/identicon'
import ListItem from '../../ui/list-item'
import Tooltip from '../../ui/tooltip-v2'
import InfoIcon from '../../ui/icon/info-icon.component'
const AssetListItem = ({ const AssetListItem = ({
children,
className, className,
'data-testid': dataTestId, 'data-testid': dataTestId,
iconClassName, iconClassName,
@ -12,32 +15,54 @@ const AssetListItem = ({
tokenAddress, tokenAddress,
tokenImage, tokenImage,
warning, warning,
primary,
secondary,
}) => { }) => {
const titleIcon = warning
? (
<Tooltip
wrapperClassName="asset-list-item__warning-tooltip"
interactive
position="bottom"
html={warning}
>
<InfoIcon severity="warning" />
</Tooltip>
)
: null
const midContent = warning
? (
<>
<InfoIcon severity="warning" />
<div className="asset-list-item__warning">{warning}</div>
</>
)
: null
return ( return (
<div <ListItem
className={classnames('asset-list-item__container', className)} className={classnames('asset-list-item', className)}
data-testid={dataTestId} data-testid={dataTestId}
title={primary}
titleIcon={titleIcon}
subtitle={secondary}
onClick={onClick} onClick={onClick}
> icon={(
<Identicon <Identicon
className={iconClassName} className={iconClassName}
diameter={32} diameter={32}
address={tokenAddress} address={tokenAddress}
image={tokenImage} image={tokenImage}
/> />
<div )}
className="asset-list-item__balance" midContent={midContent}
> rightContent={<i className="fas fa-chevron-right asset-list-item__chevron-right" />}
{ children } />
</div>
{ warning }
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
</div>
) )
} }
AssetListItem.propTypes = { AssetListItem.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string, className: PropTypes.string,
'data-testid': PropTypes.string, 'data-testid': PropTypes.string,
iconClassName: PropTypes.string, iconClassName: PropTypes.string,
@ -45,6 +70,8 @@ AssetListItem.propTypes = {
tokenAddress: PropTypes.string, tokenAddress: PropTypes.string,
tokenImage: PropTypes.string, tokenImage: PropTypes.string,
warning: PropTypes.node, warning: PropTypes.node,
primary: PropTypes.string,
secondary: PropTypes.string,
} }
AssetListItem.defaultProps = { AssetListItem.defaultProps = {

@ -1,26 +1,29 @@
.asset-list-item { .asset-list-item {
&__container { &__chevron-right {
display: flex; color: $Grey-500;
padding: 24px 16px; }
align-items: center;
border-top: 1px solid $mercury;
border-bottom: 1px solid $mercury;
cursor: pointer;
&:hover { .list-item__right-content {
background-color: $Grey-000; align-self: center;
} }
.list-item__subheading {
margin-top: 6px;
font-size: 14px;
} }
&__balance { &__warning {
display: flex;
flex-direction: column;
margin-left: 15px;
flex: 1; flex: 1;
min-width: 0; margin-left: 8px;
} }
&__chevron-right { @media (min-width: 576px) {
color: $Grey-500; &__warning-tooltip {
display: none;
}
.list-item__mid-content {
display: flex;
}
} }
} }

@ -6,11 +6,11 @@ import AddTokenButton from '../add-token-button'
import TokenList from '../token-list' import TokenList from '../token-list'
import { ADD_TOKEN_ROUTE } from '../../../helpers/constants/routes' import { ADD_TOKEN_ROUTE } from '../../../helpers/constants/routes'
import AssetListItem from '../asset-list-item' import AssetListItem from '../asset-list-item'
import CurrencyDisplay from '../../ui/currency-display'
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
import { useMetricEvent } from '../../../hooks/useMetricEvent' import { useMetricEvent } from '../../../hooks/useMetricEvent'
import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency' import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'
import { getCurrentAccountWithSendEtherInfo, getNativeCurrency, getShouldShowFiat } from '../../../selectors' import { getCurrentAccountWithSendEtherInfo, getNativeCurrency, getShouldShowFiat } from '../../../selectors'
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'
const AssetList = ({ onClickAsset }) => { const AssetList = ({ onClickAsset }) => {
const history = useHistory() const history = useHistory()
@ -41,29 +41,24 @@ const AssetList = ({ onClickAsset }) => {
numberOfDecimals: secondaryNumberOfDecimals, numberOfDecimals: secondaryNumberOfDecimals,
} = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }) } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 })
const [primaryCurrencyDisplay] = useCurrencyDisplay(
selectedAccountBalance,
{ numberOfDecimals: primaryNumberOfDecimals, currency: primaryCurrency }
)
const [secondaryCurrencyDisplay] = useCurrencyDisplay(
selectedAccountBalance,
{ numberOfDecimals: secondaryNumberOfDecimals, currency: secondaryCurrency }
)
return ( return (
<> <>
<AssetListItem <AssetListItem
onClick={() => onClickAsset(nativeCurrency)} onClick={() => onClickAsset(nativeCurrency)}
data-testid="wallet-balance" data-testid="wallet-balance"
> primary={primaryCurrencyDisplay}
<CurrencyDisplay secondary={showFiat ? secondaryCurrencyDisplay : undefined}
className="asset-list__primary-amount"
currency={primaryCurrency}
numberOfDecimals={primaryNumberOfDecimals}
value={selectedAccountBalance}
/>
{
showFiat && (
<CurrencyDisplay
className="asset-list__secondary-amount"
currency={secondaryCurrency}
numberOfDecimals={secondaryNumberOfDecimals}
value={selectedAccountBalance}
/> />
)
}
</AssetListItem>
<TokenList <TokenList
onTokenClick={(tokenAddress) => { onTokenClick={(tokenAddress) => {
onClickAsset(tokenAddress) onClickAsset(tokenAddress)

@ -1,13 +0,0 @@
.asset-list {
&__primary-amount {
color: $Black-100;
font-size: 16px;
height: 16px;
}
&__secondary-amount {
color: $Grey-500;
margin-top: 6px;
font-size: 14px;
}
}

@ -42,9 +42,15 @@ export default class ConnectedAccountsListItem extends PureComponent {
<p> <p>
<strong className="connected-accounts-list__account-name">{name}</strong> <strong className="connected-accounts-list__account-name">{name}</strong>
</p> </p>
{
status
? (
<p className="connected-accounts-list__account-status"> <p className="connected-accounts-list__account-status">
{status} {status}
</p> </p>
)
: null
}
</div> </div>
</div> </div>
{options} {options}

@ -24,7 +24,7 @@ export default class ConnectedAccountsList extends PureComponent {
connectedAccounts: PropTypes.arrayOf(PropTypes.shape({ connectedAccounts: PropTypes.arrayOf(PropTypes.shape({
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
lastActive: PropTypes.number.isRequired, lastActive: PropTypes.number,
})).isRequired, })).isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({ permissions: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired, key: PropTypes.string.isRequired,
@ -97,12 +97,21 @@ export default class ConnectedAccountsList extends PureComponent {
<> <>
<main className="connected-accounts-list"> <main className="connected-accounts-list">
{this.renderUnconnectedAccount()} {this.renderUnconnectedAccount()}
{connectedAccounts.map(({ address, name, lastActive }, index) => ( {
connectedAccounts.map(({ address, name, lastActive }, index) => {
let status
if (index === 0) {
status = t('primary')
} else if (lastActive) {
status = `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}`
}
return (
<ConnectedAccountsListItem <ConnectedAccountsListItem
key={address} key={address}
address={address} address={address}
name={`${name} (…${address.substr(-4, 4)})`} name={`${name} (…${address.substr(-4, 4)})`}
status={index === 0 ? t('primary') : `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}`} status={status}
options={( options={(
<ConnectedAccountsListOptions <ConnectedAccountsListOptions
onHideOptions={this.hideAccountOptions} onHideOptions={this.hideAccountOptions}
@ -128,7 +137,9 @@ export default class ConnectedAccountsList extends PureComponent {
</ConnectedAccountsListOptions> </ConnectedAccountsListOptions>
)} )}
/> />
))} )
})
}
</main> </main>
<ConnectedAccountsListPermissions permissions={permissions} /> <ConnectedAccountsListPermissions permissions={permissions} />
</> </>

@ -1,6 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import IconWithFallBack from '../../ui/icon-with-fallback' import IconWithFallBack from '../../ui/icon-with-fallback'
import { stripHttpSchemes } from '../../../helpers/utils/util'
export default class ConnectedSitesList extends Component { export default class ConnectedSitesList extends Component {
static contextTypes = { static contextTypes = {
@ -11,9 +12,11 @@ export default class ConnectedSitesList extends Component {
connectedDomains: PropTypes.arrayOf(PropTypes.shape({ connectedDomains: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
icon: PropTypes.string, icon: PropTypes.string,
key: PropTypes.string, origin: PropTypes.string,
host: PropTypes.string,
})).isRequired, })).isRequired,
onDisconnect: PropTypes.func.isRequired, onDisconnect: PropTypes.func.isRequired,
domainHostCount: PropTypes.objectOf(PropTypes.number).isRequired,
} }
render () { render () {
@ -23,25 +26,31 @@ export default class ConnectedSitesList extends Component {
return ( return (
<main className="connected-sites-list__content-rows"> <main className="connected-sites-list__content-rows">
{ connectedDomains.map((domain) => ( { connectedDomains.map((domain) => (
<div key={domain.key} className="connected-sites-list__content-row"> <div key={domain.origin} className="connected-sites-list__content-row">
<div className="connected-sites-list__domain-info"> <div className="connected-sites-list__domain-info">
<IconWithFallBack icon={domain.icon} name={domain.name} /> <IconWithFallBack icon={domain.icon} name={domain.name} />
<span className="connected-sites-list__domain-name" title={domain.extensionId || domain.key}> <span className="connected-sites-list__domain-name" title={domain.extensionId || domain.origin}>
{ {this.getDomainDisplayName(domain)}
domain.extensionId
? t('externalExtension')
: domain.key
}
</span> </span>
</div> </div>
<i <i
className="fas fa-trash-alt connected-sites-list__trash" className="fas fa-trash-alt connected-sites-list__trash"
title={t('disconnect')} title={t('disconnect')}
onClick={() => onDisconnect(domain.key)} onClick={() => onDisconnect(domain.origin)}
/> />
</div> </div>
)) } )) }
</main> </main>
) )
} }
getDomainDisplayName (domain) {
if (domain.extensionId) {
return this.context.t('externalExtension')
}
return this.props.domainHostCount[domain.host] > 1
? domain.origin
: stripHttpSchemes(domain.origin)
}
} }

@ -6,8 +6,6 @@
@import 'app-header/index'; @import 'app-header/index';
@import 'asset-list/asset-list';
@import 'asset-list-item/asset-list-item'; @import 'asset-list-item/asset-list-item';
@import '../ui/breadcrumbs/index'; @import '../ui/breadcrumbs/index';

@ -117,8 +117,9 @@ export default function AccountOptionsMenu ({ anchorElement, onClose }) {
isRemovable isRemovable
? ( ? (
<MenuItem <MenuItem
data-testid="account-options-menu__remove-account"
onClick={() => { onClick={() => {
dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', selectedIdentity })) dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity: selectedIdentity }))
onClose() onClose()
}} }}
iconClassName="fas fa-trash-alt" iconClassName="fas fa-trash-alt"

@ -65,12 +65,14 @@ class HideTokenConfirmationModal extends Component {
<div className="hide-token-confirmation__buttons"> <div className="hide-token-confirmation__buttons">
<button <button
className="btn-default hide-token-confirmation__button btn--large" className="btn-default hide-token-confirmation__button btn--large"
data-testid="hide-token-confirmation__cancel"
onClick={() => hideModal()} onClick={() => hideModal()}
> >
{this.context.t('cancel')} {this.context.t('cancel')}
</button> </button>
<button <button
className="btn-secondary hide-token-confirmation__button btn--large" className="btn-secondary hide-token-confirmation__button btn--large"
data-testid="hide-token-confirmation__hide"
onClick={() => hideToken(address)} onClick={() => hideToken(address)}
> >
{this.context.t('hide')} {this.context.t('hide')}

@ -4,7 +4,7 @@ import ethUtil from 'ethereumjs-util'
import classnames from 'classnames' import classnames from 'classnames'
import { ObjectInspector } from 'react-inspector' import { ObjectInspector } from 'react-inspector'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' import { ENVIRONMENT_TYPE_NOTIFICATION, MESSAGE_TYPE } from '../../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util' import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import Identicon from '../../ui/identicon' import Identicon from '../../ui/identicon'
import AccountListItem from '../../../pages/send/account-list-item/account-list-item.component' import AccountListItem from '../../../pages/send/account-list-item/account-list-item.component'
@ -208,11 +208,11 @@ export default class SignatureRequestOriginal extends Component {
const { txData } = this.props const { txData } = this.props
const { type, msgParams: { data } } = txData const { type, msgParams: { data } } = txData
if (type === 'personal_sign') { if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }] rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }]
} else if (type === 'eth_signTypedData') { } else if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
rows = data rows = data
} else if (type === 'eth_sign') { } else if (type === MESSAGE_TYPE.ETH_SIGN) {
rows = [{ name: this.context.t('message'), value: data }] rows = [{ name: this.context.t('message'), value: data }]
notice = this.context.t('signNotice') notice = this.context.t('signNotice')
} }
@ -223,12 +223,12 @@ export default class SignatureRequestOriginal extends Component {
{ this.renderRequestInfo() } { this.renderRequestInfo() }
<div <div
className={classnames('request-signature__notice', { className={classnames('request-signature__notice', {
'request-signature__warning': type === 'eth_sign', 'request-signature__warning': type === MESSAGE_TYPE.ETH_SIGN,
})} })}
> >
{ notice } { notice }
{ {
type === 'eth_sign' type === MESSAGE_TYPE.ETH_SIGN
? ( ? (
<span <span
className="request-signature__help-link" className="request-signature__help-link"

@ -2,6 +2,7 @@ import { connect } from 'react-redux'
import { compose } from 'redux' import { compose } from 'redux'
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import { MESSAGE_TYPE } from '../../../../../app/scripts/lib/enums'
import { goHome } from '../../../store/actions' import { goHome } from '../../../store/actions'
import { import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
@ -50,13 +51,13 @@ function mergeProps (stateProps, dispatchProps, ownProps) {
let cancel let cancel
let sign let sign
if (type === 'personal_sign') { if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
cancel = cancelPersonalMessage cancel = cancelPersonalMessage
sign = signPersonalMessage sign = signPersonalMessage
} else if (type === 'eth_signTypedData') { } else if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
cancel = cancelTypedMessage cancel = cancelTypedMessage
sign = signTypedMessage sign = signTypedMessage
} else if (type === 'eth_sign') { } else if (type === MESSAGE_TYPE.ETH_SIGN) {
cancel = cancelMessage cancel = cancelMessage
sign = signMessage sign = signMessage
} }

@ -5,6 +5,7 @@ import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
} from '../../../selectors' } from '../../../selectors'
import { getAccountByAddress } from '../../../helpers/utils/util' import { getAccountByAddress } from '../../../helpers/utils/util'
import { MESSAGE_TYPE } from '../../../../../app/scripts/lib/enums'
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
@ -38,13 +39,13 @@ function mergeProps (stateProps, dispatchProps, ownProps) {
let cancel let cancel
let sign let sign
if (type === 'personal_sign') { if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
cancel = cancelPersonalMessage cancel = cancelPersonalMessage
sign = signPersonalMessage sign = signPersonalMessage
} else if (type === 'eth_signTypedData') { } else if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
cancel = cancelTypedMessage cancel = cancelTypedMessage
sign = signTypedMessage sign = signTypedMessage
} else if (type === 'eth_sign') { } else if (type === MESSAGE_TYPE.ETH_SIGN) {
cancel = cancelMessage cancel = cancelMessage
sign = signMessage sign = signMessage
} }

@ -1 +1 @@
export { default } from './token-cell.container' export { default } from './token-cell'

@ -1,111 +0,0 @@
import classnames from 'classnames'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util'
import Tooltip from '../../ui/tooltip-v2'
import { I18nContext } from '../../../contexts/i18n'
import AssetListItem from '../asset-list-item'
export default class TokenCell extends Component {
static contextType = I18nContext
static propTypes = {
address: PropTypes.string,
outdatedBalance: PropTypes.bool,
symbol: PropTypes.string,
string: PropTypes.string,
contractExchangeRates: PropTypes.object,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
image: PropTypes.string,
onClick: PropTypes.func.isRequired,
userAddress: PropTypes.string.isRequired,
}
static defaultProps = {
outdatedBalance: false,
}
render () {
const t = this.context
const {
address,
symbol,
string,
contractExchangeRates,
conversionRate,
onClick,
currentCurrency,
image,
outdatedBalance,
userAddress,
} = this.props
let currentTokenToFiatRate
let currentTokenInFiat
let formattedFiat = ''
if (contractExchangeRates[address]) {
currentTokenToFiatRate = multiplyCurrencies(
contractExchangeRates[address],
conversionRate
)
currentTokenInFiat = conversionUtil(string, {
fromNumericBase: 'dec',
fromCurrency: symbol,
toCurrency: currentCurrency.toUpperCase(),
numberOfDecimals: 2,
conversionRate: currentTokenToFiatRate,
})
formattedFiat = currentTokenInFiat.toString() === '0'
? ''
: `${currentTokenInFiat} ${currentCurrency.toUpperCase()}`
}
const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
const warning = outdatedBalance
? (
<Tooltip
interactive
position="bottom"
html={(
<div className="token-cell__outdated-tooltip">
{ t('troubleTokenBalances') }
<a
href={`https://ethplorer.io/address/${userAddress}`}
rel="noopener noreferrer"
target="_blank"
style={{ color: '#F7861C' }}
>
{ t('here') }
</a>
</div>
)}
>
<i className={classnames(['fa', 'fa-exclamation-circle', 'token-cell__outdated-icon'])} />
</Tooltip>
)
: null
return (
<AssetListItem
className={classnames('token-cell', { 'token-cell--outdated': outdatedBalance })}
iconClassName="token-cell__icon"
onClick={onClick.bind(null, address)}
tokenAddress={address}
tokenImage={image}
warning={warning}
>
<div className="token-cell__balance-wrapper">
<div className="token-cell__token-balance">{string || 0}</div>
<div className="token-cell__token-symbol">{symbol}</div>
{showFiat && (
<div className="token-cell__fiat-amount">
{formattedFiat}
</div>
)}
</div>
</AssetListItem>
)
}
}

@ -1,14 +0,0 @@
import { connect } from 'react-redux'
import TokenCell from './token-cell.component'
import { getSelectedAddress } from '../../../selectors'
function mapStateToProps (state) {
return {
contractExchangeRates: state.metamask.contractExchangeRates,
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
userAddress: getSelectedAddress(state),
}
}
export default connect(mapStateToProps)(TokenCell)

@ -0,0 +1,88 @@
import classnames from 'classnames'
import PropTypes from 'prop-types'
import React from 'react'
import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util'
import AssetListItem from '../asset-list-item'
import { useSelector } from 'react-redux'
import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getSelectedAddress } from '../../../selectors'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'
export default function TokenCell ({ address, outdatedBalance, symbol, string, image, onClick }) {
const contractExchangeRates = useSelector(getTokenExchangeRates)
const conversionRate = useSelector(getConversionRate)
const currentCurrency = useSelector(getCurrentCurrency)
const userAddress = useSelector(getSelectedAddress)
const t = useI18nContext()
let currentTokenToFiatRate
let currentTokenInFiat
let formattedFiat = ''
// if the conversionRate is 0 eg: currently unknown
// or the contract exchange rate is currently unknown
// the effective currentTokenToFiatRate is 0 and erroneous.
// Skipping this entire block will result in fiat not being
// shown to the user, instead of a fiat value of 0 for a non-zero
// token amount.
if (conversionRate > 0 && contractExchangeRates[address]) {
currentTokenToFiatRate = multiplyCurrencies(
contractExchangeRates[address],
conversionRate
)
currentTokenInFiat = conversionUtil(string, {
fromNumericBase: 'dec',
fromCurrency: symbol,
toCurrency: currentCurrency.toUpperCase(),
numberOfDecimals: 2,
conversionRate: currentTokenToFiatRate,
})
formattedFiat = `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}`
}
const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
const warning = outdatedBalance
? (
<span>
{ t('troubleTokenBalances') }
<a
href={`https://ethplorer.io/address/${userAddress}`}
rel="noopener noreferrer"
target="_blank"
style={{ color: '#F7861C' }}
>
{ t('here') }
</a>
</span>
)
: null
return (
<AssetListItem
className={classnames('token-cell', { 'token-cell--outdated': outdatedBalance })}
iconClassName="token-cell__icon"
onClick={onClick.bind(null, address)}
tokenAddress={address}
tokenImage={image}
warning={warning}
primary={`${string || 0} ${symbol}`}
secondary={showFiat ? formattedFiat : undefined}
/>
)
}
TokenCell.propTypes = {
address: PropTypes.string,
outdatedBalance: PropTypes.bool,
symbol: PropTypes.string,
string: PropTypes.string,
image: PropTypes.string,
onClick: PropTypes.func.isRequired,
}
TokenCell.defaultProps = {
outdatedBalance: false,
}

@ -1,95 +1,5 @@
$wallet-balance-breakpoint: 890px;
$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})";
.token-cell { .token-cell {
position: relative; &--outdated .list-item__heading {
&__token-balance {
margin-right: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
max-width: 100%;
}
&__token-balance, &__token-symbol {
font-size: 16px;
flex: 0 0 auto;
color: $Black-100;
}
&__fiat-amount {
margin-top: 6px;
font-size: 14px;
width: 100%;
text-transform: uppercase;
color: $Grey-500; color: $Grey-500;
} }
&--outdated &__icon {
opacity: 0.5
}
&--outdated &__balance-wrapper {
opacity: 0.5
}
&__balance-wrapper {
flex: 1;
flex-flow: row wrap;
display: flex;
min-width: 0;
}
&__outdated-icon {
color: $warning-yellow;
display: block;
padding: 0 10px;
}
&__outdated-tooltip {
width: 260px;
}
}
.token-menu-dropdown {
width: 80%;
position: absolute;
top: 52px;
right: 25px;
z-index: 2000;
@media #{$wallet-balance-breakpoint-range} {
right: 18px;
}
&__close-area {
position: fixed;
top: 0;
left: 0;
z-index: 2100;
width: 100%;
height: 100%;
cursor: default;
}
&__container {
padding: 16px;
z-index: 2200;
position: relative;
}
&__options {
display: flex;
flex-direction: column;
justify-content: center;
}
&__option {
color: $white;
font-family: Roboto;
font-size: 16px;
line-height: 21px;
text-align: center;
}
} }

@ -59,16 +59,12 @@ describe('Token Cell', function () {
assert.equal(wrapper.find(Identicon).prop('image'), './test-image') assert.equal(wrapper.find(Identicon).prop('image'), './test-image')
}) })
it('renders token balance', function () { it('renders token balance and symbol', function () {
assert.equal(wrapper.find('.token-cell__token-balance').text(), '5.000') assert.equal(wrapper.find('.list-item__heading').text(), '5.000 TEST ')
})
it('renders token symbol', function () {
assert.equal(wrapper.find('.token-cell__token-symbol').text(), 'TEST')
}) })
it('renders converted fiat amount', function () { it('renders converted fiat amount', function () {
assert.equal(wrapper.find('.token-cell__fiat-amount').text(), '0.52 USD') assert.equal(wrapper.find('.list-item__subheading').text(), '$0.52 USD')
}) })
it('calls onClick when clicked', function () { it('calls onClick when clicked', function () {

@ -1 +1 @@
export { default } from './token-list.container' export { default } from './token-list'

@ -1,148 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from '@metamask/eth-token-tracker'
import { isEqual } from 'lodash'
import contracts from 'eth-contract-metadata'
import { I18nContext } from '../../../contexts/i18n'
import TokenCell from '../token-cell'
const defaultTokens = []
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
class TokenList extends Component {
static contextType = I18nContext
static propTypes = {
assetImages: PropTypes.object.isRequired,
network: PropTypes.string.isRequired,
onTokenClick: PropTypes.func.isRequired,
tokens: PropTypes.array.isRequired,
userAddress: PropTypes.string.isRequired,
}
constructor () {
super()
this.state = {
error: null,
tokensLoading: false,
tokensWithBalances: [],
}
}
constructTokenTracker () {
const { network, tokens, userAddress } = this.props
if (!tokens || !tokens.length) {
this.setState({
tokensLoading: false,
tokensWithBalances: [],
})
return
}
this.setState({ tokensLoading: true })
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
return
}
const updateBalances = (tokensWithBalances) => {
this.setState({
error: null,
tokensLoading: false,
tokensWithBalances,
})
}
const showError = (error) => {
this.setState({
error,
tokensLoading: false,
})
}
this.tokenTracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: tokens,
pollingInterval: 8000,
})
this.tokenTracker.on('update', updateBalances)
this.tokenTracker.on('error', showError)
this.tokenTracker.updateBalances()
}
stopTokenTracker () {
if (this.tokenTracker) {
this.tokenTracker.stop()
this.tokenTracker.removeAllListeners('update')
this.tokenTracker.removeAllListeners('error')
}
}
componentDidMount () {
this.constructTokenTracker()
}
componentDidUpdate (prevProps) {
const { network, tokens, userAddress } = this.props
if (
isEqual(tokens, prevProps.tokens) &&
userAddress === prevProps.userAddress &&
network === prevProps.network
) {
return
}
this.stopTokenTracker()
this.constructTokenTracker()
}
componentWillUnmount () {
this.stopTokenTracker()
}
render () {
const t = this.context
const { error, tokensLoading, tokensWithBalances } = this.state
const { assetImages, network, onTokenClick } = this.props
if (network === 'loading' || tokensLoading) {
return (
<div
style={{
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
}}
>
{t('loadingTokens')}
</div>
)
}
return (
<div>
{tokensWithBalances.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address]
return (
<TokenCell
key={index}
{...tokenData}
outdatedBalance={Boolean(error)}
onClick={onTokenClick}
/>
)
})}
</div>
)
}
}
export default TokenList

@ -1,21 +0,0 @@
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { getSelectedAddress } from '../../../selectors'
import TokenList from './token-list.component'
function mapStateToProps (state) {
return {
network: state.metamask.network,
tokens: state.metamask.tokens,
userAddress: getSelectedAddress(state),
assetImages: state.metamask.assetImages,
}
}
const TokenListContainer = connect(mapStateToProps)(TokenList)
TokenListContainer.propTypes = {
onTokenClick: PropTypes.func.isRequired,
}
export default TokenListContainer

@ -0,0 +1,66 @@
import React from 'react'
import PropTypes from 'prop-types'
import contracts from 'eth-contract-metadata'
import { isEqual } from 'lodash'
import TokenCell from '../token-cell'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { useTokenTracker } from '../../../hooks/useTokenTracker'
import { useSelector } from 'react-redux'
import { getAssetImages } from '../../../selectors'
import { getTokens } from '../../../ducks/metamask/metamask'
const defaultTokens = []
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
export default function TokenList ({ onTokenClick }) {
const t = useI18nContext()
const assetImages = useSelector(getAssetImages)
// use `isEqual` comparison function because the token array is serialized
// from the background so it has a new reference with each background update,
// even if the tokens haven't changed
const tokens = useSelector(getTokens, isEqual)
const { loading, error, tokensWithBalances } = useTokenTracker(tokens)
if (loading) {
return (
<div
style={{
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
}}
>
{t('loadingTokens')}
</div>
)
}
return (
<div>
{tokensWithBalances.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address]
return (
<TokenCell
key={index}
{...tokenData}
outdatedBalance={Boolean(error)}
onClick={onTokenClick}
/>
)
})}
</div>
)
}
TokenList.propTypes = {
onTokenClick: PropTypes.func.isRequired,
}

@ -85,6 +85,7 @@ export default class TransactionBreakdown extends PureComponent {
? ( ? (
<CurrencyDisplay <CurrencyDisplay
className="transaction-breakdown__value" className="transaction-breakdown__value"
data-testid="transaction-breakdown__gas-price"
currency={nativeCurrency} currency={nativeCurrency}
denomination={GWEI} denomination={GWEI}
value={gasPrice} value={gasPrice}

@ -10,6 +10,8 @@
} }
&__secondary-currency { &__secondary-currency {
font-size: 12px;
margin-top: 4px;
color: $Grey-500; color: $Grey-500;
} }
@ -46,5 +48,8 @@
white-space: nowrap; white-space: nowrap;
line-height: 1rem; line-height: 1rem;
} }
&:empty {
padding-top: 0;
}
} }
} }

@ -34,7 +34,7 @@
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
padding-top: 8px; padding-top: 24px;
} }
&__empty-text { &__empty-text {

@ -31,9 +31,8 @@ const TokenOverview = ({ className, token }) => {
balance={( balance={(
<div className="token-overview__balance"> <div className="token-overview__balance">
<TokenBalance <TokenBalance
token={token}
withSymbol
className="token-overview__primary-balance" className="token-overview__primary-balance"
token={token}
/> />
</div> </div>
)} )}

@ -7,6 +7,7 @@ import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'
export default function CurrencyDisplay ({ export default function CurrencyDisplay ({
value, value,
displayValue, displayValue,
'data-testid': dataTestId,
style, style,
className, className,
prefix, prefix,
@ -30,6 +31,7 @@ export default function CurrencyDisplay ({
return ( return (
<div <div
className={classnames('currency-display-component', className)} className={classnames('currency-display-component', className)}
data-testid={dataTestId}
style={style} style={style}
title={(!hideTitle && title) || null} title={(!hideTitle && title) || null}
> >
@ -49,6 +51,7 @@ export default function CurrencyDisplay ({
CurrencyDisplay.propTypes = { CurrencyDisplay.propTypes = {
className: PropTypes.string, className: PropTypes.string,
currency: PropTypes.string, currency: PropTypes.string,
'data-testid': PropTypes.string,
denomination: PropTypes.oneOf([GWEI]), denomination: PropTypes.oneOf([GWEI]),
displayValue: PropTypes.string, displayValue: PropTypes.string,
hideLabel: PropTypes.bool, hideLabel: PropTypes.bool,

@ -8,23 +8,28 @@
border-top: 1px solid $mercury; border-top: 1px solid $mercury;
border-bottom: 1px solid $mercury; border-bottom: 1px solid $mercury;
color: $Black-100; color: $Black-100;
display: grid;
grid-template-columns: 0fr repeat(11, 1fr);
grid-template-areas:
'icon head head head head head head head right right right right'
'icon sub sub sub sub sub sub sub right right right right'
'. actions actions actions actions actions actions actions right right right right';
align-items: start;
display: flex; &__icon {
justify-content: flex-start; grid-area: icon;
align-items: stretch; align-self: center;
> * {
&__icon > * { margin: 0 16px 0 0;
margin: 8px 14px 0 0;
} }
&__col {
align-self: flex-start;
&-main {
flex-grow: 1;
} }
&__actions {
grid-area: actions;
} }
&__heading { &__heading {
grid-area: head;
font-size: 16px; font-size: 16px;
line-height: 160%; line-height: 160%;
position: relative; position: relative;
@ -32,7 +37,6 @@
&-wrap { &-wrap {
display: inline-block; display: inline-block;
position: absolute; position: absolute;
top: 2px;
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-left: 8px; margin-left: 8px;
@ -40,13 +44,38 @@
} }
&__subheading { &__subheading {
grid-area: sub;
font-size: 12px; font-size: 12px;
line-height: 14px; line-height: 14px;
color: $Grey-500; color: $Grey-500;
margin-top: 4px;
&:empty {
display: none;
}
}
&__mid-content {
grid-area: mid;
font-size: 12px;
color: $Grey-500;
} }
&__right-content { &__right-content {
margin: 0 0 0 auto; grid-area: right;
text-align: right; text-align: right;
align-items: flex-end;
}
@media (max-width: 575px) {
&__mid-content {
display: none;
}
}
@media (min-width: 576px) {
grid-template-areas:
'icon head head head head mid mid mid mid right right right'
'icon sub sub sub sub mid mid mid mid right right right'
'. actions actions actions actions mid mid mid mid right right right';
} }
} }

@ -2,17 +2,28 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
export default function ListItem ({ title, subtitle, onClick, subtitleStatus, children, titleIcon, icon, rightContent, className }) { export default function ListItem ({
title,
subtitle,
onClick,
subtitleStatus,
children,
titleIcon,
icon,
rightContent,
midContent,
className,
'data-testid': dataTestId,
}) {
const primaryClassName = classnames('list-item', className) const primaryClassName = classnames('list-item', className)
return ( return (
<div className={primaryClassName} onClick={onClick}> <div className={primaryClassName} onClick={onClick} data-testid={dataTestId}>
{icon && ( {icon && (
<div className="list-item__col list-item__icon"> <div className="list-item__icon">
{icon} {icon}
</div> </div>
)} )}
<div className="list-item__col list-item__col-main">
<h2 className="list-item__heading"> <h2 className="list-item__heading">
{ title } {titleIcon && ( { title } {titleIcon && (
<span className="list-item__heading-wrap"> <span className="list-item__heading-wrap">
@ -24,13 +35,17 @@ export default function ListItem ({ title, subtitle, onClick, subtitleStatus, ch
{subtitleStatus}{subtitle} {subtitleStatus}{subtitle}
</h3> </h3>
{children && ( {children && (
<div className="list-item__more"> <div className="list-item__actions">
{ children } { children }
</div> </div>
)} )}
{midContent && (
<div className="list-item__mid-content">
{midContent}
</div> </div>
)}
{rightContent && ( {rightContent && (
<div className="list-item__col list-item__right-content"> <div className="list-item__right-content">
{rightContent} {rightContent}
</div> </div>
)} )}
@ -46,6 +61,8 @@ ListItem.propTypes = {
children: PropTypes.node, children: PropTypes.node,
icon: PropTypes.node, icon: PropTypes.node,
rightContent: PropTypes.node, rightContent: PropTypes.node,
midContent: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
onClick: PropTypes.func, onClick: PropTypes.func,
'data-testid': PropTypes.string,
} }

@ -1,16 +1,19 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class Tabs extends Component { export default class Tabs extends Component {
static defaultProps = { static defaultProps = {
defaultActiveTabName: null, defaultActiveTabName: null,
onTabClick: null, onTabClick: null,
tabsClassName: undefined,
} }
static propTypes = { static propTypes = {
defaultActiveTabName: PropTypes.string, defaultActiveTabName: PropTypes.string,
onTabClick: PropTypes.func, onTabClick: PropTypes.func,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
tabsClassName: PropTypes.string,
} }
state = { state = {
@ -62,9 +65,10 @@ export default class Tabs extends Component {
} }
render () { render () {
const { tabsClassName } = this.props
return ( return (
<div className="tabs"> <div className="tabs">
<ul className="tabs__list"> <ul className={classnames('tabs__list', tabsClassName)}>
{ this.renderTabs() } { this.renderTabs() }
</ul> </ul>
<div className="tabs__content"> <div className="tabs__content">

@ -1,7 +1,8 @@
.toggle-button { .toggle-button {
display: flex; display: flex;
$self: &;
&__status-label { &__status {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
@ -10,5 +11,28 @@
display: flex; display: flex;
align-items: center; align-items: center;
text-transform: uppercase; text-transform: uppercase;
display: grid;
}
&__label-off, &__label-on {
grid-area: 1 / 1 / 1 / 1;
}
&__label-off {
visibility: hidden;
}
&__label-on {
visibility: visible;
}
&--off {
#{ $self }__label-off {
visibility: visible;
}
#{ $self }__label-on {
visibility: hidden;
}
} }
} }

@ -48,8 +48,10 @@ const colors = {
const ToggleButton = (props) => { const ToggleButton = (props) => {
const { value, onToggle, offLabel, onLabel } = props const { value, onToggle, offLabel, onLabel } = props
const modifier = value ? 'on' : 'off'
return ( return (
<div className="toggle-button"> <div className={`toggle-button toggle-button--${modifier}`}>
<ReactToggleButton <ReactToggleButton
value={value} value={value}
onToggle={onToggle} onToggle={onToggle}
@ -60,7 +62,10 @@ const ToggleButton = (props) => {
thumbAnimateRange={[3, 18]} thumbAnimateRange={[3, 18]}
colors={colors} colors={colors}
/> />
<div className="toggle-button__status-label">{ value ? onLabel : offLabel }</div> <div className="toggle-button__status">
<span className="toggle-button__label-off">{offLabel}</span>
<span className="toggle-button__label-on">{onLabel}</span>
</div>
</div> </div>
) )
} }

@ -1 +1 @@
export { default } from './token-balance.container' export { default } from './token-balance'

@ -1,23 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../currency-display'
export default class TokenBalance extends PureComponent {
static propTypes = {
string: PropTypes.string,
symbol: PropTypes.string,
className: PropTypes.string,
}
render () {
const { className, string, symbol } = this.props
return (
<CurrencyDisplay
className={className}
displayValue={string}
suffix={symbol}
/>
)
}
}

@ -1,16 +0,0 @@
import { connect } from 'react-redux'
import { compose } from 'redux'
import withTokenTracker from '../../../helpers/higher-order-components/with-token-tracker'
import TokenBalance from './token-balance.component'
import { getSelectedAddress } from '../../../selectors'
const mapStateToProps = (state) => {
return {
userAddress: getSelectedAddress(state),
}
}
export default compose(
connect(mapStateToProps),
withTokenTracker
)(TokenBalance)

@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../currency-display'
import { useTokenTracker } from '../../../hooks/useTokenTracker'
export default function TokenBalance ({ className, token }) {
const { tokensWithBalances } = useTokenTracker([token])
const { string, symbol } = tokensWithBalances[0] || {}
return (
<CurrencyDisplay
className={className}
displayValue={string || ''}
suffix={symbol || ''}
/>
)
}
TokenBalance.propTypes = {
className: PropTypes.string,
token: PropTypes.shape({
address: PropTypes.string.isRequired,
decimals: PropTypes.number,
symbol: PropTypes.string,
}).isRequired,
}
TokenBalance.defaultProps = {
className: undefined,
}

@ -1 +0,0 @@
export { default } from './with-token-tracker.component'

@ -1,44 +0,0 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import withTokenTracker from '../with-token-tracker.component'
import TokenBalance from '../../../../components/ui/token-balance/token-balance.component'
// import sinon from 'sinon'
import TokenTracker from '@metamask/eth-token-tracker'
const { createTestProviderTools } = require('../../../../../../test/stub/provider')
const provider = createTestProviderTools({ scaffold: {} }).provider
describe('WithTokenTracker HOC', function () {
let wrapper
beforeEach(function () {
const TokenTracker = withTokenTracker(TokenBalance)
wrapper = shallow(
<TokenTracker
userAddress="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
token={
{
address: 'test',
}
}
/>
)
})
it('#setError', function () {
wrapper.instance().setError('test')
assert.equal(wrapper.props().error, 'test')
})
it('#updateBalance', function () {
wrapper.instance().tracker = new TokenTracker({
provider,
})
wrapper.instance().updateBalance([{ string: 'test string', symbol: 'test symbol' }])
assert.equal(wrapper.props().string, 'test string')
assert.equal(wrapper.props().symbol, 'test symbol')
})
})

@ -1,101 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from '@metamask/eth-token-tracker'
export default function withTokenTracker (WrappedComponent) {
return class TokenTrackerWrappedComponent extends Component {
static propTypes = {
userAddress: PropTypes.string.isRequired,
token: PropTypes.object.isRequired,
}
state = {
string: '',
symbol: '',
balance: '',
error: null,
}
tracker = null
componentDidMount () {
this.createFreshTokenTracker()
}
componentDidUpdate (prevProps) {
const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props
const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) {
return
}
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) {
return
}
this.createFreshTokenTracker()
}
componentWillUnmount () {
this.removeListeners()
}
createFreshTokenTracker () {
this.removeListeners()
if (!global.ethereumProvider) {
return
}
const { userAddress, token } = this.props
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: [token],
pollingInterval: 8000,
})
this.tracker.on('update', this.updateBalance)
this.tracker.on('error', this.setError)
this.tracker.updateBalances()
.then(() => this.updateBalance(this.tracker.serialize()))
.catch((error) => this.setState({ error: error.message }))
}
setError = (error) => {
this.setState({ error })
}
updateBalance = (tokens = []) => {
if (!this.tracker.running) {
return
}
const [{ string, symbol, balance }] = tokens
this.setState({ string, symbol, error: null, balance })
}
removeListeners () {
if (this.tracker) {
this.tracker.stop()
this.tracker.removeListener('update', this.updateBalance)
this.tracker.removeListener('error', this.setError)
}
}
render () {
const { balance, string, symbol, error } = this.state
return (
<WrappedComponent
{ ...this.props }
string={string}
symbol={symbol}
tokenTrackerBalance={balance}
error={error}
/>
)
}
}
}

@ -6,6 +6,7 @@ import {
TRANSACTION_TYPE_CANCEL, TRANSACTION_TYPE_CANCEL,
TRANSACTION_STATUS_CONFIRMED, TRANSACTION_STATUS_CONFIRMED,
} from '../../../../app/scripts/controllers/transactions/enums' } from '../../../../app/scripts/controllers/transactions/enums'
import { MESSAGE_TYPE } from '../../../../app/scripts/lib/enums'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import fetchWithCache from './fetch-with-cache' import fetchWithCache from './fetch-with-cache'
@ -137,9 +138,9 @@ export function getTransactionActionKey (transaction) {
} }
if (msgParams) { if (msgParams) {
if (type === 'eth_decrypt') { if (type === MESSAGE_TYPE.ETH_DECRYPT) {
return DECRYPT_REQUEST_KEY return DECRYPT_REQUEST_KEY
} else if (type === 'eth_getEncryptionPublicKey') { } else if (type === MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY) {
return ENCRYPTION_PUBLIC_KEY_REQUEST_KEY return ENCRYPTION_PUBLIC_KEY_REQUEST_KEY
} else { } else {
return SIGNATURE_REQUEST_KEY return SIGNATURE_REQUEST_KEY

@ -297,3 +297,15 @@ export function isValidAddressHead (address) {
export function getAccountByAddress (accounts = [], targetAddress) { export function getAccountByAddress (accounts = [], targetAddress) {
return accounts.find(({ address }) => address === targetAddress) return accounts.find(({ address }) => address === targetAddress)
} }
/**
* Strips the following schemes from URL strings:
* - http
* - https
*
* @param {string} urlString - The URL string to strip the scheme from.
* @returns {string} The URL string, without the scheme, if it was stripped.
*/
export function stripHttpSchemes (urlString) {
return urlString.replace(/^https?:\/\//u, '')
}

@ -0,0 +1,88 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import TokenTracker from '@metamask/eth-token-tracker'
import { useSelector } from 'react-redux'
import { getCurrentNetwork, getSelectedAddress } from '../selectors'
export function useTokenTracker (tokens) {
const network = useSelector(getCurrentNetwork)
const userAddress = useSelector(getSelectedAddress)
const [loading, setLoading] = useState(() => tokens?.length >= 0)
const [tokensWithBalances, setTokensWithBalances] = useState([])
const [error, setError] = useState(null)
const tokenTracker = useRef(null)
const updateBalances = useCallback((tokensWithBalances) => {
setTokensWithBalances(tokensWithBalances)
setLoading(false)
setError(null)
}, [])
const showError = useCallback((error) => {
setError(error)
setLoading(false)
}, [])
const teardownTracker = useCallback(() => {
if (tokenTracker.current) {
tokenTracker.current.stop()
tokenTracker.current.removeAllListeners('update')
tokenTracker.current.removeAllListeners('error')
tokenTracker.current = null
}
}, [])
const buildTracker = useCallback((address, tokenList) => {
// clear out previous tracker, if it exists.
teardownTracker()
tokenTracker.current = new TokenTracker({
userAddress: address,
provider: global.ethereumProvider,
tokens: tokenList,
pollingInterval: 8000,
})
tokenTracker.current.on('update', updateBalances)
tokenTracker.current.on('error', showError)
tokenTracker.current.updateBalances()
}, [updateBalances, showError, teardownTracker])
// Effect to remove the tracker when the component is removed from DOM
// Do not overload this effect with additional dependencies. teardownTracker
// is the only dependency here, which itself has no dependencies and will
// never update. The lack of dependencies that change is what confirms
// that this effect only runs on mount/unmount
useEffect(() => {
return teardownTracker
}, [teardownTracker])
// Effect to set loading state and initialize tracker when values change
useEffect(() => {
// This effect will only run initially and when:
// 1. network is updated,
// 2. userAddress is changed,
// 3. token list is updated and not equal to previous list
// in any of these scenarios, we should indicate to the user that their token
// values are in the process of updating by setting loading state.
setLoading(true)
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
// If we do not have enough information to build a TokenTracker, we exit early
// When the values above change, the effect will be restarted. We also teardown
// tracker because inevitably this effect will run again momentarily.
teardownTracker()
return
}
if (tokens.length === 0) {
// sets loading state to false and token list to empty
updateBalances([])
}
buildTracker(userAddress, tokens)
}, [userAddress, network, tokens, updateBalances, buildTracker])
return { loading, tokensWithBalances, error }
}

@ -315,7 +315,7 @@ class AddToken extends Component {
title={this.context.t('addTokens')} title={this.context.t('addTokens')}
tabsComponent={this.renderTabs()} tabsComponent={this.renderTabs()}
onSubmit={() => this.handleNext()} onSubmit={() => this.handleNext()}
disabled={this.hasError() || !this.hasSelected()} disabled={Boolean(this.hasError()) || !this.hasSelected()}
onCancel={() => { onCancel={() => {
clearPendingTokens() clearPendingTokens()
history.push(mostRecentOverviewPage) history.push(mostRecentOverviewPage)

@ -19,11 +19,11 @@
.asset-breadcrumb { .asset-breadcrumb {
font-size: 14px; font-size: 14px;
color: $Black-100; color: $Black-100;
background-color: inherit;
&__chevron { &__chevron {
padding: 0 10px 0 2px; padding: 0 10px 0 2px;
font-size: 16px; font-size: 16px;
background-color: inherit;
} }
&__asset { &__asset {

@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
const AssetBreadcrumb = ({ accountName, assetName, onBack }) => { const AssetBreadcrumb = ({ accountName, assetName, onBack }) => {
return ( return (
<div className="asset-breadcrumb"> <button className="asset-breadcrumb" onClick={onBack} >
<button className="fas fa-chevron-left asset-breadcrumb__chevron" data-testid="asset__back" onClick={onBack} /> <i className="fas fa-chevron-left asset-breadcrumb__chevron" data-testid="asset__back" />
<span> <span>
{accountName} {accountName}
</span> </span>
@ -12,7 +12,7 @@ const AssetBreadcrumb = ({ accountName, assetName, onBack }) => {
<span className="asset-breadcrumb__asset"> <span className="asset-breadcrumb__asset">
{ assetName } { assetName }
</span> </span>
</div> </button>
) )
} }

@ -1,115 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmTransactionBase from '../confirm-transaction-base'
import ConfirmApproveContent from './confirm-approve-content'
import { getCustomTxParamsData } from './confirm-approve.util'
import {
calcTokenAmount,
} from '../../helpers/utils/token-util'
export default class ConfirmApprove extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
tokenAddress: PropTypes.string,
toAddress: PropTypes.string,
tokenAmount: PropTypes.string,
tokenSymbol: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
ethTransactionTotal: PropTypes.string,
contractExchangeRate: PropTypes.number,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
showEditApprovalPermissionModal: PropTypes.func,
origin: PropTypes.string,
siteImage: PropTypes.string,
tokenTrackerBalance: PropTypes.string,
data: PropTypes.string,
decimals: PropTypes.number,
txData: PropTypes.object,
}
static defaultProps = {
tokenAmount: '0',
}
state = {
customPermissionAmount: '',
}
componentDidUpdate (prevProps) {
const { tokenAmount } = this.props
if (tokenAmount !== prevProps.tokenAmount) {
this.setState({ customPermissionAmount: tokenAmount })
}
}
render () {
const {
toAddress,
tokenAddress,
tokenSymbol,
tokenAmount,
showCustomizeGasModal,
showEditApprovalPermissionModal,
origin,
siteImage,
tokenTrackerBalance,
data,
decimals,
txData,
currentCurrency,
ethTransactionTotal,
fiatTransactionTotal,
...restProps
} = this.props
const { customPermissionAmount } = this.state
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}`
const tokenBalance = tokenTrackerBalance
? calcTokenAmount(tokenTrackerBalance, decimals).toString(10)
: ''
const customData = customPermissionAmount
? getCustomTxParamsData(data, { customPermissionAmount, decimals })
: null
return (
<ConfirmTransactionBase
toAddress={toAddress}
identiconAddress={tokenAddress}
showAccountInHeader
title={tokensText}
contentComponent={(
<ConfirmApproveContent
decimals={decimals}
siteImage={siteImage}
setCustomAmount={(newAmount) => {
this.setState({ customPermissionAmount: newAmount })
}}
customTokenAmount={String(customPermissionAmount)}
tokenAmount={tokenAmount}
origin={origin}
tokenSymbol={tokenSymbol}
tokenBalance={tokenBalance}
showCustomizeGasModal={() => showCustomizeGasModal(txData)}
showEditApprovalPermissionModal={showEditApprovalPermissionModal}
data={customData || data}
toAddress={toAddress}
currentCurrency={currentCurrency}
ethTransactionTotal={ethTransactionTotal}
fiatTransactionTotal={fiatTransactionTotal}
/>
)}
hideSenderToRecipient
customTxParamsData={customData}
{...restProps}
/>
)
}
}

@ -1,113 +0,0 @@
import { connect } from 'react-redux'
import { compose } from 'redux'
import { withRouter } from 'react-router-dom'
import {
contractExchangeRateSelector,
transactionFeeSelector,
} from '../../selectors'
import { getTokens } from '../../ducks/metamask/metamask'
import { showModal } from '../../store/actions'
import {
getTokenData,
} from '../../helpers/utils/transactions.util'
import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker'
import {
calcTokenAmount,
getTokenToAddress,
getTokenValue,
} from '../../helpers/utils/token-util'
import ConfirmApprove from './confirm-approve.component'
const mapStateToProps = (state, ownProps) => {
const { match: { params = {} } } = ownProps
const { id: paramsTransactionId } = params
const {
confirmTransaction,
metamask: {
currentCurrency,
conversionRate,
currentNetworkTxList,
domainMetadata = {},
selectedAddress,
},
} = state
const {
txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {},
} = confirmTransaction
const transaction = (
currentNetworkTxList.find(({ id }) => id === (Number(paramsTransactionId) ||
transactionId)) || {}
)
const {
ethTransactionTotal,
fiatTransactionTotal,
} = transactionFeeSelector(state, transaction)
const tokens = getTokens(state)
const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address)
const { decimals, symbol: tokenSymbol } = currentToken || {}
const tokenData = getTokenData(data)
const tokenValue = tokenData && getTokenValue(tokenData.params)
const toAddress = tokenData && getTokenToAddress(tokenData.params)
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toString(10)
const contractExchangeRate = contractExchangeRateSelector(state)
const { origin } = transaction
const formattedOrigin = origin
? origin[0].toUpperCase() + origin.slice(1)
: ''
const { icon: siteImage = '' } = domainMetadata[origin] || {}
return {
toAddress,
tokenAddress,
tokenAmount,
currentCurrency,
conversionRate,
contractExchangeRate,
fiatTransactionTotal,
ethTransactionTotal,
tokenSymbol,
siteImage,
token: { address: tokenAddress },
userAddress: selectedAddress,
origin: formattedOrigin,
data,
decimals: Number(decimals),
txData: transaction,
}
}
const mapDispatchToProps = (dispatch) => {
return {
showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })),
showEditApprovalPermissionModal: ({
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenBalance,
tokenSymbol,
}) => dispatch(showModal({
name: 'EDIT_APPROVAL_PERMISSION',
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenBalance,
tokenSymbol,
})),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
withTokenTracker,
)(ConfirmApprove)

@ -0,0 +1,141 @@
import React, { useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useParams } from 'react-router-dom'
import ConfirmTransactionBase from '../confirm-transaction-base'
import ConfirmApproveContent from './confirm-approve-content'
import { getCustomTxParamsData } from './confirm-approve.util'
import { showModal } from '../../store/actions'
import {
getTokenData,
} from '../../helpers/utils/transactions.util'
import {
calcTokenAmount,
getTokenToAddress,
getTokenValue,
} from '../../helpers/utils/token-util'
import { useTokenTracker } from '../../hooks/useTokenTracker'
import { getTokens } from '../../ducks/metamask/metamask'
import {
transactionFeeSelector,
txDataSelector,
} from '../../selectors/confirm-transaction'
import { getCurrentCurrency, getDomainMetadata } from '../../selectors/selectors'
import { currentNetworkTxListSelector } from '../../selectors/transactions'
export default function ConfirmApprove () {
const dispatch = useDispatch()
const { id: paramsTransactionId } = useParams()
const {
id: transactionId,
txParams: {
to: tokenAddress,
data,
} = {},
} = useSelector(txDataSelector)
const currentCurrency = useSelector(getCurrentCurrency)
const currentNetworkTxList = useSelector(currentNetworkTxListSelector)
const domainMetadata = useSelector(getDomainMetadata)
const tokens = useSelector(getTokens)
const transaction = (
currentNetworkTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {}
)
const {
ethTransactionTotal,
fiatTransactionTotal,
} = useSelector((state) => transactionFeeSelector(state, transaction))
const currentToken = (tokens && tokens.find(({ address }) => tokenAddress === address)) || { address: tokenAddress }
const { tokensWithBalances } = useTokenTracker([currentToken])
const tokenTrackerBalance = tokensWithBalances[0]?.balance || ''
const tokenSymbol = currentToken?.symbol
const decimals = Number(currentToken?.decimals)
const tokenData = getTokenData(data)
const tokenValue = tokenData && getTokenValue(tokenData.params)
const toAddress = tokenData && getTokenToAddress(tokenData.params)
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toString(10)
const [customPermissionAmount, setCustomPermissionAmount] = useState('')
const previousTokenAmount = useRef(tokenAmount)
useEffect(
() => {
if (customPermissionAmount && previousTokenAmount.current !== tokenAmount) {
setCustomPermissionAmount(tokenAmount)
}
previousTokenAmount.current = tokenAmount
},
[customPermissionAmount, tokenAmount]
)
const { origin } = transaction
const formattedOrigin = origin
? origin[0].toUpperCase() + origin.slice(1)
: ''
const txData = transaction
const { icon: siteImage = '' } = domainMetadata[origin] || {}
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}`
const tokenBalance = tokenTrackerBalance
? calcTokenAmount(tokenTrackerBalance, decimals).toString(10)
: ''
const customData = customPermissionAmount
? getCustomTxParamsData(data, { customPermissionAmount, decimals })
: null
return (
<ConfirmTransactionBase
toAddress={toAddress}
identiconAddress={tokenAddress}
showAccountInHeader
title={tokensText}
contentComponent={(
<ConfirmApproveContent
decimals={decimals}
siteImage={siteImage}
setCustomAmount={setCustomPermissionAmount}
customTokenAmount={String(customPermissionAmount)}
tokenAmount={tokenAmount}
origin={formattedOrigin}
tokenSymbol={tokenSymbol}
tokenBalance={tokenBalance}
showCustomizeGasModal={() => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData }))}
showEditApprovalPermissionModal={
({
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenBalance,
tokenSymbol,
}) => dispatch(
showModal({
name: 'EDIT_APPROVAL_PERMISSION',
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenBalance,
tokenSymbol,
})
)
}
data={customData || data}
toAddress={toAddress}
currentCurrency={currentCurrency}
ethTransactionTotal={ethTransactionTotal}
fiatTransactionTotal={fiatTransactionTotal}
/>
)}
hideSenderToRecipient
customTxParamsData={customData}
/>
)
}

@ -1 +1 @@
export { default } from './confirm-approve.container' export { default } from './confirm-approve'

@ -21,6 +21,7 @@ import {
DEPLOY_CONTRACT_ACTION_KEY, DEPLOY_CONTRACT_ACTION_KEY,
SEND_ETHER_ACTION_KEY, SEND_ETHER_ACTION_KEY,
} from '../../helpers/constants/transactions' } from '../../helpers/constants/transactions'
import { MESSAGE_TYPE } from '../../../../app/scripts/lib/enums'
export default class ConfirmTransactionSwitch extends Component { export default class ConfirmTransactionSwitch extends Component {
static propTypes = { static propTypes = {
@ -74,9 +75,9 @@ export default class ConfirmTransactionSwitch extends Component {
return this.redirectToTransaction() return this.redirectToTransaction()
} else if (txData.msgParams) { } else if (txData.msgParams) {
let pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}` let pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
if (txData.type === 'eth_decrypt') { if (txData.type === MESSAGE_TYPE.ETH_DECRYPT) {
pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${DECRYPT_MESSAGE_REQUEST_PATH}` pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${DECRYPT_MESSAGE_REQUEST_PATH}`
} else if (txData.type === 'eth_getEncryptionPublicKey') { } else if (txData.type === MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY) {
pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}` pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`
} }
return <Redirect to={{ pathname }} /> return <Redirect to={{ pathname }} />

@ -11,6 +11,7 @@ import SignatureRequest from '../../components/app/signature-request'
import SignatureRequestOriginal from '../../components/app/signature-request-original' import SignatureRequestOriginal from '../../components/app/signature-request-original'
import Loading from '../../components/ui/loading-screen' import Loading from '../../components/ui/loading-screen'
import { getMostRecentOverviewPage } from '../../ducks/history/history' import { getMostRecentOverviewPage } from '../../ducks/history/history'
import { MESSAGE_TYPE } from '../../../../app/scripts/lib/enums'
function mapStateToProps (state) { function mapStateToProps (state) {
const { metamask, appState } = state const { metamask, appState } = state
@ -111,7 +112,7 @@ class ConfirmTxScreen extends Component {
signatureSelect (type, version) { signatureSelect (type, version) {
// Temporarily direct only v3 and v4 requests to new code. // Temporarily direct only v3 and v4 requests to new code.
if (type === 'eth_signTypedData' && (version === 'V3' || version === 'V4')) { if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA && (version === 'V3' || version === 'V4')) {
return SignatureRequest return SignatureRequest
} }

@ -54,7 +54,7 @@ export default class ConnectedAccounts extends PureComponent {
return ( return (
<Popover <Popover
title={isActiveTabExtension ? t('currentExtension') : activeTabOrigin} title={isActiveTabExtension ? t('currentExtension') : new URL(activeTabOrigin).host}
subtitle={connectedAccounts.length ? connectedAccountsDescription : t('connectedAccountsEmptyDescription')} subtitle={connectedAccounts.length ? connectedAccountsDescription : t('connectedAccountsEmptyDescription')}
onClose={() => history.push(mostRecentOverviewPage)} onClose={() => history.push(mostRecentOverviewPage)}
footerClassName="connected-accounts__footer" footerClassName="connected-accounts__footer"

@ -17,6 +17,7 @@ export default class ConnectedSites extends Component {
accountLabel: PropTypes.string.isRequired, accountLabel: PropTypes.string.isRequired,
closePopover: PropTypes.func.isRequired, closePopover: PropTypes.func.isRequired,
connectedDomains: PropTypes.arrayOf(PropTypes.object).isRequired, connectedDomains: PropTypes.arrayOf(PropTypes.object).isRequired,
domainHostCount: PropTypes.objectOf(PropTypes.number).isRequired,
disconnectAllAccounts: PropTypes.func.isRequired, disconnectAllAccounts: PropTypes.func.isRequired,
disconnectAccount: PropTypes.func.isRequired, disconnectAccount: PropTypes.func.isRequired,
getOpenMetamaskTabsIds: PropTypes.func.isRequired, getOpenMetamaskTabsIds: PropTypes.func.isRequired,
@ -69,6 +70,7 @@ export default class ConnectedSites extends Component {
renderConnectedSitesList () { renderConnectedSitesList () {
return ( return (
<ConnectedSitesList <ConnectedSitesList
domainHostCount={this.props.domainHostCount}
connectedDomains={this.props.connectedDomains} connectedDomains={this.props.connectedDomains}
onDisconnect={this.setPendingDisconnect} onDisconnect={this.setPendingDisconnect}
/> />

@ -11,6 +11,7 @@ import {
getCurrentAccountWithSendEtherInfo, getCurrentAccountWithSendEtherInfo,
getOriginOfCurrentTab, getOriginOfCurrentTab,
getPermissionDomains, getPermissionDomains,
getPermissionsMetadataHostCounts,
getPermittedAccountsByOrigin, getPermittedAccountsByOrigin,
getSelectedAddress, getSelectedAddress,
} from '../../selectors' } from '../../selectors'
@ -40,6 +41,7 @@ const mapStateToProps = (state) => {
accountLabel: getCurrentAccountWithSendEtherInfo(state).name, accountLabel: getCurrentAccountWithSendEtherInfo(state).name,
connectedDomains, connectedDomains,
domains: getPermissionDomains(state), domains: getPermissionDomains(state),
domainHostCount: getPermissionsMetadataHostCounts(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
permittedAccountsByOrigin, permittedAccountsByOrigin,
selectedAddress, selectedAddress,

@ -31,23 +31,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
termsChecked: false, termsChecked: false,
} }
parseSeedPhrase = (seedPhrase) => { parseSeedPhrase = (seedPhrase) => (seedPhrase || '').trim().toLowerCase().match(/\w+/gu)?.join(' ') || ''
if (!seedPhrase) {
return ''
}
const trimmed = seedPhrase.trim()
if (!trimmed) {
return ''
}
const words = trimmed.toLowerCase().match(/\w+/g)
if (!words) {
return ''
}
return words.join(' ')
}
UNSAFE_componentWillMount () { UNSAFE_componentWillMount () {
this._onBeforeUnload = () => this.context.metricsEvent({ this._onBeforeUnload = () => this.context.metricsEvent({
@ -73,7 +57,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
if (seedPhrase) { if (seedPhrase) {
const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase) const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase)
const wordCount = parsedSeedPhrase.split(new RegExp('\\s')).length const wordCount = parsedSeedPhrase.split(/\s/u).length
if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) { if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) {
seedPhraseError = this.context.t('seedPhraseReq') seedPhraseError = this.context.t('seedPhraseReq')
} else if (!validateMnemonic(parsedSeedPhrase)) { } else if (!validateMnemonic(parsedSeedPhrase)) {
@ -284,8 +268,10 @@ export default class ImportWithSeedPhrase extends PureComponent {
{termsChecked ? <i className="fa fa-check fa-2x" /> : null} {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
</div> </div>
<span id="ftf-chk1-label" className="first-time-flow__checkbox-label"> <span id="ftf-chk1-label" className="first-time-flow__checkbox-label">
I have read and agree to the&nbsp; {t('acceptTermsOfUse', [(
<a <a
onClick={(e) => e.stopPropagation()}
key="first-time-flow__link-text"
href="https://metamask.io/terms.html" href="https://metamask.io/terms.html"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -294,6 +280,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
{ t('terms') } { t('terms') }
</span> </span>
</a> </a>
)])}
</span> </span>
</div> </div>
<Button <Button

@ -204,8 +204,10 @@ export default class NewAccount extends PureComponent {
{termsChecked ? <i className="fa fa-check fa-2x" /> : null} {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
</div> </div>
<span id="ftf-chk1-label" className="first-time-flow__checkbox-label"> <span id="ftf-chk1-label" className="first-time-flow__checkbox-label">
I have read and agree to the&nbsp; {t('acceptTermsOfUse', [(
<a <a
onClick={(e) => e.stopPropagation()}
key="first-time-flow__link-text"
href="https://metamask.io/terms.html" href="https://metamask.io/terms.html"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -214,6 +216,7 @@ export default class NewAccount extends PureComponent {
{ t('terms') } { t('terms') }
</span> </span>
</a> </a>
)])}
</span> </span>
</div> </div>
<Button <Button

@ -232,7 +232,7 @@ export default class Home extends PureComponent {
<div className="home__balance-wrapper"> <div className="home__balance-wrapper">
<EthOverview /> <EthOverview />
</div> </div>
<Tabs defaultActiveTabName={defaultHomeActiveTabName} onTabClick={onTabClick}> <Tabs defaultActiveTabName={defaultHomeActiveTabName} onTabClick={onTabClick} tabsClassName="home__tabs">
<Tab <Tab
activeClassName="home__tab--active" activeClassName="home__tab--active"
className="home__tab" className="home__tab"

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save