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. 75
      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. 65
      ui/app/components/app/asset-list-item/asset-list-item.js
  44. 37
      ui/app/components/app/asset-list-item/asset-list-item.scss
  45. 33
      ui/app/components/app/asset-list/asset-list.js
  46. 13
      ui/app/components/app/asset-list/asset-list.scss
  47. 12
      ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js
  48. 71
      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. 57
      ui/app/components/ui/list-item/list-item.component.js
  73. 6
      ui/app/components/ui/tabs/tabs.component.js
  74. 28
      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. 43
      ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
  99. 23
      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": {
"message": "View the addresses of your permitted accounts (required)",
"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 {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 {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.
*/
@ -319,7 +318,7 @@ function setupController (initState, initLangCode) {
[ENVIRONMENT_TYPE_FULLSCREEN]: true,
}
const metamaskBlacklistedPorts = [
const metamaskBlockedPorts = [
'trezor-connect',
]
@ -343,7 +342,7 @@ function setupController (initState, initLangCode) {
const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
if (metamaskBlacklistedPorts.includes(remotePort.name)) {
if (metamaskBlockedPorts.includes(remotePort.name)) {
return false
}
@ -384,7 +383,7 @@ function setupController (initState, initLangCode) {
if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) {
const tabId = remotePort.sender.tab.id
const url = new URL(remotePort.sender.url)
const origin = url.hostname
const { origin } = url
remotePort.onMessage.addListener((msg) => {
if (msg.data && msg.data.method === 'eth_requestAccounts') {

@ -127,7 +127,7 @@ function logStreamDisconnectWarning (remoteLabel, err) {
*/
function shouldInjectProvider () {
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 () {
const blacklistedDomains = [
function blockedDomainCheck () {
const blockedDomains = [
'uscourts.gov',
'dropbox.com',
'webbyawards.com',
@ -200,9 +200,9 @@ function blacklistedDomainCheck () {
]
const currentUrl = window.location.href
let currentRegex
for (let i = 0; i < blacklistedDomains.length; i++) {
const blacklistedDomain = blacklistedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blacklistedDomain}).)*$`)
for (let i = 0; i < blockedDomains.length; i++) {
const blockedDomain = blockedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`)
if (!currentRegex.test(currentUrl)) {
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._getUnlockPromise = getUnlockPromise
this._notifyDomain = notifyDomain
this.notifyAllDomains = notifyAllDomains
this._notifyAllDomains = notifyAllDomains
this._showPermissionRequest = showPermissionRequest
this._restrictedMethods = getRestrictedMethods({
@ -95,6 +95,7 @@ export class PermissionsController {
getAccounts: this.getAccounts.bind(this, origin),
getUnlockPromise: () => this._getUnlockPromise(true),
hasPermission: this.hasPermission.bind(this, origin),
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind(
this, { origin }, { eth_accounts: {} },
),
@ -196,6 +197,7 @@ export class PermissionsController {
* User approval callback. Resolves the Promise for the permissions request
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
* 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 {Array} accounts - The accounts to expose, if any
@ -206,7 +208,7 @@ export class PermissionsController {
const approval = this.pendingApprovals.get(id)
if (!approval) {
log.error(`Permissions request with id '${id}' not found`)
log.debug(`Permissions request with id '${id}' not found`)
return
}
@ -241,6 +243,7 @@ export class PermissionsController {
/**
* User rejection callback. Rejects the Promise for the permissions request
* 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
*/
@ -248,7 +251,7 @@ export class PermissionsController {
const approval = this.pendingApprovals.get(id)
if (!approval) {
log.error(`Permissions request with id '${id}' not found`)
log.debug(`Permissions request with id '${id}' not found`)
return
}
@ -289,10 +292,7 @@ export class PermissionsController {
const permittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: permittedAccounts,
})
this.notifyAccountsChanged(origin, permittedAccounts)
}
/**
@ -338,10 +338,7 @@ export class PermissionsController {
newPermittedAccounts = await this.getAccounts(origin)
}
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: newPermittedAccounts,
})
this.notifyAccountsChanged(origin, 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,
// update "last seen" time for the origin and account(s)
// exception: no accounts -> no times to update
if (
payload.method === NOTIFICATION_NAMES.accountsChanged &&
Array.isArray(payload.result)
) {
this.permissionsLog.updateAccountsHistory(
origin, payload.result
)
}
this._notifyDomain(origin, payload)
this.permissionsLog.updateAccountsHistory(
origin, newAccounts
)
// NOTE:
// 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
* 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) {
@ -449,10 +460,7 @@ export class PermissionsController {
perms.map((methodName) => {
if (methodName === 'eth_accounts') {
this.notifyDomain(
origin,
{ method: NOTIFICATION_NAMES.accountsChanged, result: [] }
)
this.notifyAccountsChanged(origin, [])
}
return { parentCapability: methodName }
@ -466,7 +474,7 @@ export class PermissionsController {
*/
clearPermissions () {
this.permissions.clearDomains()
this.notifyAllDomains({
this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged,
result: [],
})
@ -505,6 +513,11 @@ export class PermissionsController {
...metadata,
lastUpdated: Date.now(),
}
if (!newMetadataState[origin].extensionId && !newMetadataState[origin].host) {
newMetadataState[origin].host = new URL(origin).host
}
this._pendingSiteMetadata.add(origin)
this._setDomainMetadata(newMetadataState)
}
@ -583,6 +596,7 @@ export class PermissionsController {
* @param {string} account - The newly selected account's address.
*/
async _handleAccountSelected (account) {
if (typeof account !== 'string') {
throw new Error('Selected account should be a non-empty string.')
}
@ -618,10 +632,7 @@ export class PermissionsController {
async _handleConnectedAccountSelected (origin) {
const permittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: permittedAccounts,
})
this.notifyAccountsChanged(origin, permittedAccounts)
}
/**

@ -9,6 +9,7 @@ export default function createMethodMiddleware ({
getAccounts,
getUnlockPromise,
hasPermission,
notifyAccountsChanged,
requestAccountsPermission,
}) {
@ -16,6 +17,8 @@ export default function createMethodMiddleware ({
return createAsyncMiddleware(async (req, res, next) => {
let responseHandler
switch (req.method) {
// Intercepting eth_accounts requests for backwards compatibility:
@ -81,10 +84,33 @@ export default function createMethodMiddleware ({
res.result = true
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:
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
*/
constructor ({ interval = DEFAULT_INTERVAL, currency, preferences } = {}) {
constructor ({ currency, preferences } = {}) {
this.store = new ObservableStore()
this.currency = currency
this.preferences = preferences
this.interval = interval
}
/**
@ -50,19 +49,6 @@ export default class TokenRatesController {
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}
*/
@ -85,4 +71,19 @@ export default class TokenRatesController {
this._tokens = tokens
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 cleanErrorStack from '../../lib/cleanErrorStack'
import log from 'loglevel'
import { throwIfAccountIsBlacklisted } from './lib/recipient-blacklist-checker'
import { throwIfAccountIsBlocked } from './lib/recipient-blocklist-checker'
import {
TRANSACTION_TYPE_CANCEL,
@ -241,7 +241,7 @@ export default class TransactionController extends EventEmitter {
this.emit('newUnapprovedTx', txMeta)
try {
throwIfAccountIsBlacklisted(txMeta.metamaskNetworkId, normalizedTxParams.to)
throwIfAccountIsBlocked(txMeta.metamaskNetworkId, normalizedTxParams.to)
txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse)
} catch (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
'0x9bcb0A9d99d815Bb87ee3191b1399b1Bcc46dc77',
// Ganache default seed phrases
@ -14,4 +14,4 @@ const blacklist = [
'0x5aeda56215b167893e80b4fe645ba6d5bab767de',
]
export default blacklist
export default blocklist

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

@ -3,6 +3,7 @@ import ObservableStore from 'obs-store'
import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id'
import log from 'loglevel'
import { MESSAGE_TYPE } from './enums'
/**
* 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,
time: time,
status: 'unapproved',
type: 'eth_getEncryptionPublicKey',
type: MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY,
}
if (req) {

@ -9,11 +9,20 @@ const PLATFORM_EDGE = 'Edge'
const PLATFORM_FIREFOX = 'Firefox'
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 {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND,
MESSAGE_TYPE,
PLATFORM_BRAVE,
PLATFORM_CHROME,
PLATFORM_EDGE,

@ -3,6 +3,7 @@ import ObservableStore from 'obs-store'
import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors'
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
@ -116,7 +117,7 @@ export default class MessageManager extends EventEmitter {
msgParams: msgParams,
time: time,
status: 'unapproved',
type: 'eth_sign',
type: MESSAGE_TYPE.ETH_SIGN,
}
this.addMsg(msgData)

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

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

@ -29,7 +29,6 @@ import EnsController from './controllers/ens'
import NetworkController from './controllers/network'
import PreferencesController from './controllers/preferences'
import AppStateController from './controllers/app-state'
import InfuraController from './controllers/infura'
import CachedBalancesController from './controllers/cached-balances'
import AlertController from './controllers/alert'
import OnboardingController from './controllers/onboarding'
@ -128,11 +127,6 @@ export default class MetamaskController extends EventEmitter {
this.currencyRateController = new CurrencyRateController(undefined, initState.CurrencyController)
this.infuraController = new InfuraController({
initState: initState.InfuraController,
})
this.infuraController.scheduleInfuraNetworkCheck()
this.phishingController = new PhishingController()
// now we can initialize the RPC provider, which other controllers require
@ -170,9 +164,11 @@ export default class MetamaskController extends EventEmitter {
if (activeControllerConnections > 0) {
this.accountTracker.start()
this.incomingTransactionsController.start()
this.tokenRatesController.start()
} else {
this.accountTracker.stop()
this.incomingTransactionsController.stop()
this.tokenRatesController.stop()
}
})
@ -281,10 +277,6 @@ export default class MetamaskController extends EventEmitter {
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager()
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({
AppStateController: this.appStateController.store,
@ -294,7 +286,6 @@ export default class MetamaskController extends EventEmitter {
AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController,
NetworkController: this.networkController.store,
InfuraController: this.infuraController.store,
CachedBalancesController: this.cachedBalancesController.store,
AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store,
@ -320,7 +311,6 @@ export default class MetamaskController extends EventEmitter {
PreferencesController: this.preferencesController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController,
InfuraController: this.infuraController.store,
AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store,
IncomingTransactionsController: this.incomingTransactionsController.store,
@ -459,6 +449,9 @@ export default class MetamaskController extends EventEmitter {
markPasswordForgotten: this.markPasswordForgotten.bind(this),
unMarkPasswordForgotten: this.unMarkPasswordForgotten.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
addNewAccount: nodeify(this.addNewAccount, this),
@ -496,9 +489,6 @@ export default class MetamaskController extends EventEmitter {
completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController),
addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
// AddressController
setAddressBook: nodeify(this.addressBookController.set, this.addressBookController),
removeFromAddressBook: this.addressBookController.delete.bind(this.addressBookController),
@ -574,9 +564,6 @@ export default class MetamaskController extends EventEmitter {
addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController),
removePermittedAccount: nodeify(permissionsController.removePermittedAccount, 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) {
const { usePhishDetect } = this.preferencesController.store.getState()
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)) {
log.debug('MetaMask - sending phishing warning for', hostname)
this.sendPhishingWarning(connectionStream, hostname)
@ -1487,7 +1474,7 @@ export default class MetamaskController extends EventEmitter {
* @private
* @param {*} connectionStream - The duplex stream to the per-page script,
* 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) {
const mux = setupMultiplex(connectionStream)
@ -1538,7 +1525,7 @@ export default class MetamaskController extends EventEmitter {
setupProviderConnection (outStream, sender, isInternal) {
const origin = isInternal
? 'metamask'
: (new URL(sender.url)).hostname
: (new URL(sender.url)).origin
let extensionId
if (sender.id !== extension.runtime.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.
* @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 {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
@ -2032,20 +2019,9 @@ export default class MetamaskController extends EventEmitter {
*/
set isClientOpen (open) {
this._isClientOpen = open
this.isClientOpenAndUnlocked = this.isUnlocked() && 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
*
@ -2056,10 +2032,10 @@ export default class MetamaskController extends EventEmitter {
*/
/**
* Adds a domain to the PhishingController whitelist
* @param {string} hostname - the domain to whitelist
* Adds a domain to the PhishingController safelist
* @param {string} hostname - the domain to safelist
*/
whitelistPhishingDomain (hostname) {
safelistPhishingDomain (hostname) {
return this.phishingController.bypass(hostname)
}

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

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

@ -1,5 +1,7 @@
## Add Custom Build to Chrome
![Load dev build](./load-dev-build-chrome.gif)
* Open `Settings` > `Extensions`.
* Check "Developer mode".
* 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 () {
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.delay(regularDelayMs)
})

@ -3,7 +3,6 @@ const webdriver = require('selenium-webdriver')
const { By, Key, until } = webdriver
const {
tinyDelayMs,
regularDelayMs,
largeDelayMs,
} = require('./helpers')
@ -278,7 +277,7 @@ describe('Using MetaMask with an existing account', function () {
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'))
assert.equal(await accountName.getText(), 'Account 5')
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'))
assert.equal(accountListItems.length, 5)
await driver.clickElement(By.css('.account-menu__account:last-of-type > .remove-account-icon'))
await driver.delay(tinyDelayMs)
await driver.clickPoint(By.css('.account-menu__icon'), 0, 0)
})
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'))
})
@ -304,6 +308,8 @@ describe('Using MetaMask with an existing account', function () {
assert.equal(await accountName.getText(), 'Account 1')
await driver.delay(regularDelayMs)
await driver.clickElement(By.css('.account-menu__icon'))
const accountListItems = await driver.findElements(By.css('.account-menu__account'))
assert.equal(accountListItems.length, 4)
})

@ -207,7 +207,7 @@ describe('MetaMask', 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.delay(regularDelayMs)
})
@ -472,10 +472,8 @@ describe('MetaMask', function () {
const txValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency'))
await txValue.click()
const popoverCloseButton = await driver.findClickableElement(By.css('.popover-header__button'))
const txGasPrices = await driver.findElements(By.css('.transaction-breakdown__value'))
const txGasPriceLabels = await driver.findElements(By.css('.transaction-breakdown-row__title'))
await driver.wait(until.elementTextMatches(txGasPrices[4], /^10$/), 10000)
assert(txGasPriceLabels[2])
const txGasPrice = await driver.findElement(By.css('[data-testid="transaction-breakdown__gas-price"]'))
await driver.wait(until.elementTextMatches(txGasPrice, /^10$/), 10000)
await popoverCloseButton.click()
})
})
@ -1223,8 +1221,7 @@ describe('MetaMask', function () {
const confirmHideModal = await driver.findElement(By.css('span .modal'))
const byHideTokenConfirmationButton = By.css('.hide-token-confirmation__button')
await driver.clickElement(byHideTokenConfirmationButton)
await driver.clickElement(By.css('[data-testid="hide-token-confirmation__hide"]'))
await driver.wait(until.stalenessOf(confirmHideModal))
})
@ -1232,7 +1229,6 @@ describe('MetaMask', function () {
describe('Add existing token using search', 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.delay(regularDelayMs)
})

@ -107,7 +107,7 @@ describe('MetaMask', function () {
const address = content[1]
assert.equal(await title.getText(), 'Signature Request')
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))
})

@ -96,7 +96,7 @@ describe('MetaMask', 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.delay(regularDelayMs)
})
@ -202,7 +202,7 @@ describe('MetaMask', 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.delay(regularDelayMs)
})
@ -223,7 +223,7 @@ describe('MetaMask', function () {
it('finds the blockies toggle turned on', async function () {
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()
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',
{
...message,
origin: 'mycrypto.com',
origin: 'http://mycrypto.com',
tabId: 456,
},
]
@ -865,7 +865,7 @@ describe('MetaMaskController', function () {
'mock tx params',
{
...message,
origin: 'mycrypto.com',
origin: 'http://mycrypto.com',
},
]
)

@ -151,10 +151,10 @@ export const getNotifyAllDomains = (notifications = {}) => (notification) => {
* - e.g. permissions, caveats, and permission requests
*/
const ORIGINS = {
a: 'foo.xyz',
b: 'bar.abc',
c: 'baz.def',
const DOMAINS = {
a: { origin: 'https://foo.xyz', host: 'foo.xyz' },
b: { origin: 'https://bar.abc', host: 'bar.abc' },
c: { origin: 'https://baz.def', host: 'baz.def' },
}
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,
}
},
/**
* 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',
},
ORIGINS: { ...ORIGINS },
DOMAINS: { ...DOMAINS },
ACCOUNTS: { ...ACCOUNTS },
@ -647,7 +648,7 @@ export const constants = deepFreeze({
case1: [
{
[ORIGINS.a]: {
[DOMAINS.a.origin]: {
[PERM_NAMES.eth_accounts]: {
lastApproved: 1,
accounts: {
@ -659,7 +660,7 @@ export const constants = deepFreeze({
},
},
{
[ORIGINS.a]: {
[DOMAINS.a.origin]: {
[PERM_NAMES.eth_accounts]: {
lastApproved: 2,
accounts: {
@ -674,7 +675,7 @@ export const constants = deepFreeze({
case2: [
{
[ORIGINS.a]: {
[DOMAINS.a.origin]: {
[PERM_NAMES.eth_accounts]: {
lastApproved: 1,
accounts: {},
@ -685,10 +686,10 @@ export const constants = deepFreeze({
case3: [
{
[ORIGINS.a]: {
[DOMAINS.a.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 1 },
},
[ORIGINS.b]: {
[DOMAINS.b.origin]: {
[PERM_NAMES.eth_accounts]: {
lastApproved: 1,
accounts: {
@ -696,7 +697,7 @@ export const constants = deepFreeze({
},
},
},
[ORIGINS.c]: {
[DOMAINS.c.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 1 },
[PERM_NAMES.eth_accounts]: {
lastApproved: 1,
@ -707,10 +708,10 @@ export const constants = deepFreeze({
},
},
{
[ORIGINS.a]: {
[DOMAINS.a.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 2 },
},
[ORIGINS.b]: {
[DOMAINS.b.origin]: {
[PERM_NAMES.eth_accounts]: {
lastApproved: 1,
accounts: {
@ -718,7 +719,7 @@ export const constants = deepFreeze({
},
},
},
[ORIGINS.c]: {
[DOMAINS.c.origin]: {
[PERM_NAMES.test_method]: { lastApproved: 1 },
[PERM_NAMES.eth_accounts]: {
lastApproved: 2,
@ -733,7 +734,7 @@ export const constants = deepFreeze({
case4: [
{
[ORIGINS.a]: {
[DOMAINS.a.origin]: {
[PERM_NAMES.test_method]: {
lastApproved: 1,
},

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

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

@ -1,5 +1,5 @@
import { strict as assert } from 'assert'
import { useFakeTimers } from 'sinon'
import sinon from 'sinon'
import {
METADATA_STORE_KEY,
@ -30,7 +30,7 @@ const {
const {
ACCOUNTS,
ORIGINS,
DOMAINS,
PERM_NAMES,
} = constants
@ -58,14 +58,15 @@ describe('permissions middleware', function () {
beforeEach(function () {
permController = initPermController()
permController.notifyAccountsChanged = sinon.fake()
})
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(
ORIGINS.a, PERM_NAMES.eth_accounts
DOMAINS.a.origin, PERM_NAMES.eth_accounts
)
const res = {}
@ -98,25 +99,32 @@ describe('permissions middleware', function () {
validatePermission(
res.result[0],
PERM_NAMES.eth_accounts,
ORIGINS.a,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted)
)
const aAccounts = await permController.getAccounts(ORIGINS.a)
const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual(
aAccounts, [ACCOUNTS.a.primary],
'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 () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
// create first request
const req1 = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts
DOMAINS.a.origin, PERM_NAMES.eth_accounts
)
const res1 = {}
@ -147,16 +155,23 @@ describe('permissions middleware', function () {
validatePermission(
res1.result[0],
PERM_NAMES.eth_accounts,
ORIGINS.a,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted)
)
const accounts1 = await permController.getAccounts(ORIGINS.a)
const accounts1 = await permController.getAccounts(DOMAINS.a.origin)
assert.deepEqual(
accounts1, [ACCOUNTS.a.primary],
'origin should have correct accounts'
)
assert.ok(
permController.notifyAccountsChanged.calledOnceWith(
DOMAINS.a.origin, accounts1,
),
'expected notification call should have been made'
)
// create second request
const requestedPerms2 = {
@ -165,7 +180,7 @@ describe('permissions middleware', function () {
}
const req2 = RPC_REQUESTS.requestPermissions(
ORIGINS.a, { ...requestedPerms2 }
DOMAINS.a.origin, { ...requestedPerms2 }
)
const res2 = {}
@ -196,29 +211,41 @@ describe('permissions middleware', function () {
validatePermission(
res2.result[0],
PERM_NAMES.eth_accounts,
ORIGINS.a,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.b.permitted)
)
validatePermission(
res2.result[1],
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(
accounts2, [ACCOUNTS.b.primary],
'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 () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.eth_accounts
DOMAINS.a.origin, PERM_NAMES.eth_accounts
)
const res = {}
@ -248,18 +275,23 @@ describe('permissions middleware', function () {
'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(
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 () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const req = RPC_REQUESTS.requestPermissions(
ORIGINS.a, {
DOMAINS.a.origin, {
...PERMS.requests.does_not_exist(),
...PERMS.requests.test_method(),
}
@ -288,6 +320,11 @@ describe('permissions middleware', function () {
),
'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 () {
@ -296,13 +333,13 @@ describe('permissions middleware', function () {
// two middlewares for two origins
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
const bMiddleware = getPermissionsMiddleware(permController, ORIGINS.b)
const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
const bMiddleware = getPermissionsMiddleware(permController, DOMAINS.b.origin)
// create and start processing first request for first origin
const reqA1 = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method
DOMAINS.a.origin, PERM_NAMES.test_method
)
const resA1 = {}
@ -314,7 +351,7 @@ describe('permissions middleware', function () {
// create and start processing first request for second origin
const reqB1 = RPC_REQUESTS.requestPermission(
ORIGINS.b, PERM_NAMES.test_method
DOMAINS.b.origin, PERM_NAMES.test_method
)
const resB1 = {}
@ -332,7 +369,7 @@ describe('permissions middleware', function () {
// which should throw
const reqA2 = RPC_REQUESTS.requestPermission(
ORIGINS.a, PERM_NAMES.test_method
DOMAINS.a.origin, PERM_NAMES.test_method
)
const resA2 = {}
@ -402,9 +439,9 @@ describe('permissions middleware', 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 expectedError = ERRORS.rpcCap.unauthorized()
@ -426,11 +463,11 @@ describe('permissions middleware', 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 = {}
await assert.doesNotReject(
@ -455,9 +492,9 @@ describe('permissions middleware', 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 = {}
await assert.doesNotReject(
@ -477,14 +514,14 @@ describe('permissions middleware', function () {
it('returns correct accounts for permitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(permController, ORIGINS.a)
const aMiddleware = getPermissionsMiddleware(permController, DOMAINS.a.origin)
grantPermissions(
permController, ORIGINS.a,
permController, DOMAINS.a.origin,
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 = {}
await assert.doesNotReject(
@ -515,9 +552,9 @@ describe('permissions middleware', function () {
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 pendingApproval = assert.doesNotReject(
@ -540,7 +577,7 @@ describe('permissions middleware', function () {
// wait for permission to be granted
await pendingApproval
const perms = permController.permissions.getPermissionsForDomain(ORIGINS.a)
const perms = permController.permissions.getPermissionsForDomain(DOMAINS.a.origin)
assert.equal(
perms.length, 1,
@ -550,7 +587,7 @@ describe('permissions middleware', function () {
validatePermission(
perms[0],
PERM_NAMES.eth_accounts,
ORIGINS.a,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted)
)
@ -566,7 +603,7 @@ describe('permissions middleware', function () {
)
// 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(
aAccounts, [ACCOUNTS.a.primary], 'origin should have have correct accounts'
)
@ -576,9 +613,9 @@ describe('permissions middleware', function () {
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 expectedError = ERRORS.rejectPermissionsRequest.rejection()
@ -609,7 +646,7 @@ describe('permissions middleware', function () {
'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(
aAccounts, [], 'origin should have have correct accounts'
)
@ -617,14 +654,14 @@ describe('permissions middleware', function () {
it('directly returns accounts for permitted domain', async function () {
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c)
const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
grantPermissions(
permController, ORIGINS.c,
permController, DOMAINS.c.origin,
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 = {}
await assert.doesNotReject(
@ -651,14 +688,14 @@ describe('permissions middleware', function () {
permController.getUnlockPromise = () => unlockPromise
const cMiddleware = getPermissionsMiddleware(permController, ORIGINS.c)
const cMiddleware = getPermissionsMiddleware(permController, DOMAINS.c.origin)
grantPermissions(
permController, ORIGINS.c,
permController, DOMAINS.c.origin,
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 = {}
// this will block until we resolve the unlock Promise
@ -695,7 +732,7 @@ describe('permissions middleware', function () {
beforeEach(function () {
permController = initPermController()
clock = useFakeTimers(1)
clock = sinon.useFakeTimers(1)
})
afterEach(function () {
@ -706,9 +743,9 @@ describe('permissions middleware', function () {
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 = {}
await assert.doesNotReject(
@ -722,7 +759,13 @@ describe('permissions middleware', function () {
assert.deepEqual(
metadataStore,
{ [ORIGINS.c]: { name, lastUpdated: 1 } },
{
[DOMAINS.c.origin]: {
name,
host: DOMAINS.c.host,
lastUpdated: 1,
},
},
'metadata should have been added to store'
)
})
@ -733,9 +776,9 @@ describe('permissions middleware', function () {
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 = {}
await assert.doesNotReject(
@ -749,7 +792,7 @@ describe('permissions middleware', function () {
assert.deepEqual(
metadataStore,
{ [ORIGINS.c]: { name, extensionId, lastUpdated: 1 } },
{ [DOMAINS.c.origin]: { name, extensionId, lastUpdated: 1 } },
'metadata should have been added to store'
)
})
@ -758,9 +801,9 @@ describe('permissions middleware', function () {
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 = {}
await assert.doesNotReject(
@ -780,9 +823,9 @@ describe('permissions middleware', 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
const res = {}

@ -13,8 +13,11 @@ describe('TokenRatesController', function () {
it('should poll on correct interval', async function () {
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)
stub.restore()
rateController.stop()
})
})

@ -1,9 +1,9 @@
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'
describe('Recipient Blacklist Checker', function () {
describe('#throwIfAccountIsBlacklisted', function () {
describe('Recipient Blocklist Checker', function () {
describe('#throwIfAccountIsBlocked', function () {
// Accounts from Ganache's original default seed phrase
const publicAccounts = [
'0x627306090abab3a6e1400e9345bc60c78a8bef57',
@ -22,7 +22,7 @@ describe('Recipient Blacklist Checker', function () {
const networks = [ROPSTEN_NETWORK_ID, RINKEBY_NETWORK_ID, KOVAN_NETWORK_ID, GOERLI_NETWORK_ID]
for (const networkId of networks) {
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 () {
for (const account of publicAccounts) {
assert.throws(
() => throwIfAccountIsBlacklisted(1, account),
() => throwIfAccountIsBlocked(1, account),
{ message: 'Recipient is a public account' },
)
}
@ -38,14 +38,14 @@ describe('Recipient Blacklist Checker', function () {
it('fails for public account - uppercase', function () {
assert.throws(
() => throwIfAccountIsBlacklisted(1, '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2'),
() => throwIfAccountIsBlocked(1, '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2'),
{ message: 'Recipient is a public account' },
)
})
it('fails for public account - lowercase', function () {
assert.throws(
() => throwIfAccountIsBlacklisted(1, '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2'),
() => throwIfAccountIsBlocked(1, '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2'),
{ message: 'Recipient is a public account' },
)
})

@ -5,7 +5,7 @@ import {
replayHistory,
generateHistoryEntry,
} 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('#snapshotFromTxMeta', function () {
@ -33,7 +33,7 @@ describe('Transaction state history helper', function () {
describe('#migrateFromSnapshotsToDiffs', 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)
newHistory.forEach((newEntry, index) => {
if (index === 0) {

@ -7,7 +7,6 @@ import InputAdornment from '@material-ui/core/InputAdornment'
import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import Tooltip from '../../ui/tooltip'
import Identicon from '../../ui/identicon'
import IconWithFallBack from '../../ui/icon-with-fallback'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
@ -38,7 +37,6 @@ export default class AccountMenu extends Component {
lockMetamask: PropTypes.func,
selectedAddress: PropTypes.string,
showAccountDetail: PropTypes.func,
showRemoveAccountConfirmationModal: PropTypes.func,
toggleAccountMenu: PropTypes.func,
addressConnectedDomainMap: PropTypes.object,
originOfCurrentTab: PropTypes.string,
@ -176,6 +174,7 @@ export default class AccountMenu extends Component {
type={PRIMARY}
/>
</div>
{ this.renderKeyringType(keyring) }
{ iconAndNameForOpenDomain
? (
<div className="account-menu__icon-list">
@ -184,45 +183,11 @@ export default class AccountMenu extends Component {
)
: null
}
{ this.renderKeyringType(keyring) }
{ this.renderRemoveAccount(keyring, identity) }
</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) {
const { t } = this.context

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

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

@ -99,19 +99,6 @@ describe('Account Menu', function () {
const importedAccount = wrapper.find('.keyring-label.allcaps')
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 () {

@ -2,9 +2,12 @@ import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
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 = ({
children,
className,
'data-testid': dataTestId,
iconClassName,
@ -12,32 +15,54 @@ const AssetListItem = ({
tokenAddress,
tokenImage,
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 (
<div
className={classnames('asset-list-item__container', className)}
<ListItem
className={classnames('asset-list-item', className)}
data-testid={dataTestId}
title={primary}
titleIcon={titleIcon}
subtitle={secondary}
onClick={onClick}
>
<Identicon
className={iconClassName}
diameter={32}
address={tokenAddress}
image={tokenImage}
/>
<div
className="asset-list-item__balance"
>
{ children }
</div>
{ warning }
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
</div>
icon={(
<Identicon
className={iconClassName}
diameter={32}
address={tokenAddress}
image={tokenImage}
/>
)}
midContent={midContent}
rightContent={<i className="fas fa-chevron-right asset-list-item__chevron-right" />}
/>
)
}
AssetListItem.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
'data-testid': PropTypes.string,
iconClassName: PropTypes.string,
@ -45,6 +70,8 @@ AssetListItem.propTypes = {
tokenAddress: PropTypes.string,
tokenImage: PropTypes.string,
warning: PropTypes.node,
primary: PropTypes.string,
secondary: PropTypes.string,
}
AssetListItem.defaultProps = {

@ -1,26 +1,29 @@
.asset-list-item {
&__container {
display: flex;
padding: 24px 16px;
align-items: center;
border-top: 1px solid $mercury;
border-bottom: 1px solid $mercury;
cursor: pointer;
&__chevron-right {
color: $Grey-500;
}
&:hover {
background-color: $Grey-000;
}
.list-item__right-content {
align-self: center;
}
.list-item__subheading {
margin-top: 6px;
font-size: 14px;
}
&__balance {
display: flex;
flex-direction: column;
margin-left: 15px;
&__warning {
flex: 1;
min-width: 0;
margin-left: 8px;
}
&__chevron-right {
color: $Grey-500;
@media (min-width: 576px) {
&__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 { ADD_TOKEN_ROUTE } from '../../../helpers/constants/routes'
import AssetListItem from '../asset-list-item'
import CurrencyDisplay from '../../ui/currency-display'
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
import { useMetricEvent } from '../../../hooks/useMetricEvent'
import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'
import { getCurrentAccountWithSendEtherInfo, getNativeCurrency, getShouldShowFiat } from '../../../selectors'
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'
const AssetList = ({ onClickAsset }) => {
const history = useHistory()
@ -41,29 +41,24 @@ const AssetList = ({ onClickAsset }) => {
numberOfDecimals: secondaryNumberOfDecimals,
} = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 })
const [primaryCurrencyDisplay] = useCurrencyDisplay(
selectedAccountBalance,
{ numberOfDecimals: primaryNumberOfDecimals, currency: primaryCurrency }
)
const [secondaryCurrencyDisplay] = useCurrencyDisplay(
selectedAccountBalance,
{ numberOfDecimals: secondaryNumberOfDecimals, currency: secondaryCurrency }
)
return (
<>
<AssetListItem
onClick={() => onClickAsset(nativeCurrency)}
data-testid="wallet-balance"
>
<CurrencyDisplay
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>
primary={primaryCurrencyDisplay}
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
/>
<TokenList
onTokenClick={(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>
<strong className="connected-accounts-list__account-name">{name}</strong>
</p>
<p className="connected-accounts-list__account-status">
{status}
</p>
{
status
? (
<p className="connected-accounts-list__account-status">
{status}
</p>
)
: null
}
</div>
</div>
{options}

@ -24,7 +24,7 @@ export default class ConnectedAccountsList extends PureComponent {
connectedAccounts: PropTypes.arrayOf(PropTypes.shape({
address: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
lastActive: PropTypes.number.isRequired,
lastActive: PropTypes.number,
})).isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
@ -97,38 +97,49 @@ export default class ConnectedAccountsList extends PureComponent {
<>
<main className="connected-accounts-list">
{this.renderUnconnectedAccount()}
{connectedAccounts.map(({ address, name, lastActive }, index) => (
<ConnectedAccountsListItem
key={address}
address={address}
name={`${name} (…${address.substr(-4, 4)})`}
status={index === 0 ? t('primary') : `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}`}
options={(
<ConnectedAccountsListOptions
onHideOptions={this.hideAccountOptions}
onShowOptions={this.showAccountOptions.bind(null, address)}
show={accountWithOptionsShown === address}
>
{
address === selectedAddress ? null : (
{
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
key={address}
address={address}
name={`${name} (…${address.substr(-4, 4)})`}
status={status}
options={(
<ConnectedAccountsListOptions
onHideOptions={this.hideAccountOptions}
onShowOptions={this.showAccountOptions.bind(null, address)}
show={accountWithOptionsShown === address}
>
{
address === selectedAddress ? null : (
<MenuItem
iconClassName="fas fa-random"
onClick={this.switchAccount}
>
{t('switchToThisAccount')}
</MenuItem>
)
}
<MenuItem
iconClassName="fas fa-random"
onClick={this.switchAccount}
iconClassName="disconnect-icon"
onClick={this.disconnectAccount}
>
{t('switchToThisAccount')}
{t('disconnectThisAccount')}
</MenuItem>
)
}
<MenuItem
iconClassName="disconnect-icon"
onClick={this.disconnectAccount}
>
{t('disconnectThisAccount')}
</MenuItem>
</ConnectedAccountsListOptions>
)}
/>
))}
</ConnectedAccountsListOptions>
)}
/>
)
})
}
</main>
<ConnectedAccountsListPermissions permissions={permissions} />
</>

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

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

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

@ -4,7 +4,7 @@ import ethUtil from 'ethereumjs-util'
import classnames from 'classnames'
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 Identicon from '../../ui/identicon'
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 { type, msgParams: { data } } = txData
if (type === 'personal_sign') {
if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
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
} else if (type === 'eth_sign') {
} else if (type === MESSAGE_TYPE.ETH_SIGN) {
rows = [{ name: this.context.t('message'), value: data }]
notice = this.context.t('signNotice')
}
@ -223,12 +223,12 @@ export default class SignatureRequestOriginal extends Component {
{ this.renderRequestInfo() }
<div
className={classnames('request-signature__notice', {
'request-signature__warning': type === 'eth_sign',
'request-signature__warning': type === MESSAGE_TYPE.ETH_SIGN,
})}
>
{ notice }
{
type === 'eth_sign'
type === MESSAGE_TYPE.ETH_SIGN
? (
<span
className="request-signature__help-link"

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

@ -5,6 +5,7 @@ import {
accountsWithSendEtherInfoSelector,
} from '../../../selectors'
import { getAccountByAddress } from '../../../helpers/utils/util'
import { MESSAGE_TYPE } from '../../../../../app/scripts/lib/enums'
function mapStateToProps (state) {
return {
@ -38,13 +39,13 @@ function mergeProps (stateProps, dispatchProps, ownProps) {
let cancel
let sign
if (type === 'personal_sign') {
if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
cancel = cancelPersonalMessage
sign = signPersonalMessage
} else if (type === 'eth_signTypedData') {
} else if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
cancel = cancelTypedMessage
sign = signTypedMessage
} else if (type === 'eth_sign') {
} else if (type === MESSAGE_TYPE.ETH_SIGN) {
cancel = cancelMessage
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 {
position: relative;
&__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;
&--outdated .list-item__heading {
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')
})
it('renders token balance', function () {
assert.equal(wrapper.find('.token-cell__token-balance').text(), '5.000')
})
it('renders token symbol', function () {
assert.equal(wrapper.find('.token-cell__token-symbol').text(), 'TEST')
it('renders token balance and symbol', function () {
assert.equal(wrapper.find('.list-item__heading').text(), '5.000 TEST ')
})
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 () {

@ -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
className="transaction-breakdown__value"
data-testid="transaction-breakdown__gas-price"
currency={nativeCurrency}
denomination={GWEI}
value={gasPrice}

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

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

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

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

@ -8,23 +8,28 @@
border-top: 1px solid $mercury;
border-bottom: 1px solid $mercury;
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;
justify-content: flex-start;
align-items: stretch;
&__icon > * {
margin: 8px 14px 0 0;
&__icon {
grid-area: icon;
align-self: center;
> * {
margin: 0 16px 0 0;
}
}
&__col {
align-self: flex-start;
&-main {
flex-grow: 1;
}
&__actions {
grid-area: actions;
}
&__heading {
grid-area: head;
font-size: 16px;
line-height: 160%;
position: relative;
@ -32,7 +37,6 @@
&-wrap {
display: inline-block;
position: absolute;
top: 2px;
width: 16px;
height: 16px;
margin-left: 8px;
@ -40,13 +44,38 @@
}
&__subheading {
grid-area: sub;
font-size: 12px;
line-height: 14px;
color: $Grey-500;
margin-top: 4px;
&:empty {
display: none;
}
}
&__mid-content {
grid-area: mid;
font-size: 12px;
color: $Grey-500;
}
&__right-content {
margin: 0 0 0 auto;
grid-area: 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,35 +2,50 @@ import React from 'react'
import PropTypes from 'prop-types'
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)
return (
<div className={primaryClassName} onClick={onClick}>
<div className={primaryClassName} onClick={onClick} data-testid={dataTestId}>
{icon && (
<div className="list-item__col list-item__icon">
<div className="list-item__icon">
{icon}
</div>
)}
<div className="list-item__col list-item__col-main">
<h2 className="list-item__heading">
{ title } {titleIcon && (
<span className="list-item__heading-wrap">
{titleIcon}
</span>
)}
</h2>
<h3 className="list-item__subheading">
{subtitleStatus}{subtitle}
</h3>
{children && (
<div className="list-item__more">
{ children }
</div>
<h2 className="list-item__heading">
{ title } {titleIcon && (
<span className="list-item__heading-wrap">
{titleIcon}
</span>
)}
</div>
</h2>
<h3 className="list-item__subheading">
{subtitleStatus}{subtitle}
</h3>
{children && (
<div className="list-item__actions">
{ children }
</div>
)}
{midContent && (
<div className="list-item__mid-content">
{midContent}
</div>
)}
{rightContent && (
<div className="list-item__col list-item__right-content">
<div className="list-item__right-content">
{rightContent}
</div>
)}
@ -46,6 +61,8 @@ ListItem.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
rightContent: PropTypes.node,
midContent: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func,
'data-testid': PropTypes.string,
}

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

@ -1,7 +1,8 @@
.toggle-button {
display: flex;
$self: &;
&__status-label {
&__status {
font-family: Roboto;
font-style: normal;
font-weight: normal;
@ -10,5 +11,28 @@
display: flex;
align-items: center;
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 { value, onToggle, offLabel, onLabel } = props
const modifier = value ? 'on' : 'off'
return (
<div className="toggle-button">
<div className={`toggle-button toggle-button--${modifier}`}>
<ReactToggleButton
value={value}
onToggle={onToggle}
@ -60,7 +62,10 @@ const ToggleButton = (props) => {
thumbAnimateRange={[3, 18]}
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>
)
}

@ -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_STATUS_CONFIRMED,
} from '../../../../app/scripts/controllers/transactions/enums'
import { MESSAGE_TYPE } from '../../../../app/scripts/lib/enums'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import fetchWithCache from './fetch-with-cache'
@ -137,9 +138,9 @@ export function getTransactionActionKey (transaction) {
}
if (msgParams) {
if (type === 'eth_decrypt') {
if (type === MESSAGE_TYPE.ETH_DECRYPT) {
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
} else {
return SIGNATURE_REQUEST_KEY

@ -297,3 +297,15 @@ export function isValidAddressHead (address) {
export function getAccountByAddress (accounts = [], 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')}
tabsComponent={this.renderTabs()}
onSubmit={() => this.handleNext()}
disabled={this.hasError() || !this.hasSelected()}
disabled={Boolean(this.hasError()) || !this.hasSelected()}
onCancel={() => {
clearPendingTokens()
history.push(mostRecentOverviewPage)

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

@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
const AssetBreadcrumb = ({ accountName, assetName, onBack }) => {
return (
<div className="asset-breadcrumb">
<button className="fas fa-chevron-left asset-breadcrumb__chevron" data-testid="asset__back" onClick={onBack} />
<button className="asset-breadcrumb" onClick={onBack} >
<i className="fas fa-chevron-left asset-breadcrumb__chevron" data-testid="asset__back" />
<span>
{accountName}
</span>
@ -12,7 +12,7 @@ const AssetBreadcrumb = ({ accountName, assetName, onBack }) => {
<span className="asset-breadcrumb__asset">
{ assetName }
</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,
SEND_ETHER_ACTION_KEY,
} from '../../helpers/constants/transactions'
import { MESSAGE_TYPE } from '../../../../app/scripts/lib/enums'
export default class ConfirmTransactionSwitch extends Component {
static propTypes = {
@ -74,9 +75,9 @@ export default class ConfirmTransactionSwitch extends Component {
return this.redirectToTransaction()
} else if (txData.msgParams) {
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}`
} 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}`
}
return <Redirect to={{ pathname }} />

@ -11,6 +11,7 @@ import SignatureRequest from '../../components/app/signature-request'
import SignatureRequestOriginal from '../../components/app/signature-request-original'
import Loading from '../../components/ui/loading-screen'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
import { MESSAGE_TYPE } from '../../../../app/scripts/lib/enums'
function mapStateToProps (state) {
const { metamask, appState } = state
@ -111,7 +112,7 @@ class ConfirmTxScreen extends Component {
signatureSelect (type, version) {
// 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
}

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

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

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

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

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

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

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

Loading…
Cancel
Save