Merge remote-tracking branch 'origin/develop' into sync-master

* origin/develop:
  Add hidden tokens to store (#9320)
  @metamask/inpage-provider@^8.0.0 (#8640)
  Log persistence errors with Sentry (#10018)
  Fix SES lockdown on older browsers (#10014)
  Reapply view quote screen designs (#9905)
  Fix unbound metrics track function (#10016)
  Deobfuscate error message (#10012)
  Add SES lockdown and Sentry to all pages (#10013)
  Remove web3 injection (#9156)
  Initialize network controller provider chainId to the appropriate default networks (#9999)
  Fix Infura network chain IDs (#8629)
  Prevent props error in swaps gas modal (#10001)
  Fix 9906 - Prevent unwanted 'no quotes available' message when going back to build quote screen while having insufficient funds (#9994)
  Fix 9988 - Don't allow more than 15% slippage (#9991)
  Bump highlight.js from 10.4.0 to 10.4.1 (#10004)
  Add timeout to `wait-until-called` (#9996)
  Update transaction params validation (#9992)
feature/default_network_editable
Mark Stacey 4 years ago
commit 427268b25f
  1. 22
      app/_locales/en/messages.json
  2. 4
      app/home.html
  3. 3
      app/images/down-arrow-grey.svg
  4. 8
      app/manifest/_base.json
  5. 4
      app/notification.html
  6. 1
      app/phishing.html
  7. 1
      app/popup.html
  8. 2
      app/scripts/background.js
  9. 93
      app/scripts/contentscript.js
  10. 7
      app/scripts/controllers/detect-tokens.js
  11. 35
      app/scripts/controllers/network/createInfuraClient.js
  12. 6
      app/scripts/controllers/network/network.js
  13. 14
      app/scripts/controllers/permissions/enums.js
  14. 35
      app/scripts/controllers/permissions/index.js
  15. 1
      app/scripts/controllers/permissions/permissionsLog.js
  16. 2
      app/scripts/controllers/permissions/permissionsMethodMiddleware.js
  17. 96
      app/scripts/controllers/preferences.js
  18. 4
      app/scripts/controllers/transactions/index.js
  19. 30
      app/scripts/controllers/transactions/lib/util.js
  20. 28
      app/scripts/inpage.js
  21. 3
      app/scripts/lib/enums.js
  22. 1
      app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
  23. 46
      app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js
  24. 5
      app/scripts/lib/rpc-method-middleware/handlers/index.js
  25. 49
      app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js
  26. 63
      app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js
  27. 251
      app/scripts/lib/setupWeb3.js
  28. 101
      app/scripts/lib/web3-entities.json
  29. 190
      app/scripts/metamask-controller.js
  30. 1
      app/scripts/runLockdown.js
  31. 4
      development/build/static.js
  32. 3
      package.json
  33. 6
      test/e2e/metrics.spec.js
  34. 44
      test/lib/wait-until-called.js
  35. 47
      test/unit/app/controllers/detect-tokens-test.js
  36. 30
      test/unit/app/controllers/incoming-transactions-test.js
  37. 4
      test/unit/app/controllers/metamask-controller-test.js
  38. 2
      test/unit/app/controllers/metametrics-test.js
  39. 21
      test/unit/app/controllers/permissions/mocks.js
  40. 40
      test/unit/app/controllers/permissions/permissions-controller-test.js
  41. 2
      test/unit/app/controllers/permissions/permissions-log-controller-test.js
  42. 19
      test/unit/app/controllers/permissions/permissions-middleware-test.js
  43. 7
      test/unit/app/controllers/transactions/tx-controller-test.js
  44. 38
      test/unit/app/controllers/transactions/tx-utils-test.js
  45. 2
      ui/app/components/ui/url-icon/index.scss
  46. 5
      ui/app/components/ui/url-icon/url-icon.js
  47. 1
      ui/app/ducks/swaps/swaps.js
  48. 7
      ui/app/pages/swaps/build-quote/build-quote.js
  49. 18
      ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js
  50. 62
      ui/app/pages/swaps/fee-card/fee-card.js
  51. 17
      ui/app/pages/swaps/fee-card/fee-card.stories.js
  52. 86
      ui/app/pages/swaps/fee-card/index.scss
  53. 54
      ui/app/pages/swaps/fee-card/pig-icon.js
  54. 2
      ui/app/pages/swaps/index.scss
  55. 130
      ui/app/pages/swaps/main-quote-summary/index.scss
  56. 138
      ui/app/pages/swaps/main-quote-summary/main-quote-summary.js
  57. 44
      ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js
  58. 22
      ui/app/pages/swaps/slippage-buttons/slippage-buttons.js
  59. 5
      ui/app/pages/swaps/swaps-footer/swaps-footer.js
  60. 2
      ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.component.js
  61. 2
      ui/app/pages/swaps/swaps.util.js
  62. 57
      ui/app/pages/swaps/view-quote/index.scss
  63. 81
      ui/app/pages/swaps/view-quote/view-quote.js
  64. 30
      yarn.lock

@ -1605,6 +1605,9 @@
"message": "You need $1 more $2 to complete this swap",
"description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol."
},
"swapBetterQuoteAvailable": {
"message": "A better quote is available"
},
"swapBuildQuotePlaceHolderText": {
"message": "No tokens available matching $1",
"description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text"
@ -1701,8 +1704,8 @@
"message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
},
"swapNQuotesAvailable": {
"message": "$1 quotes available",
"swapNQuotes": {
"message": "$1 quotes",
"description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen"
},
"swapNetworkFeeSummary": {
@ -1737,7 +1740,7 @@
"message": "Quote details"
},
"swapQuoteDetailsSlippageInfo": {
"message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"max slippage\" setting."
"message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"slippage tolerance\" setting."
},
"swapQuoteIncludesRate": {
"message": "Quote includes a $1% MetaMask fee",
@ -1830,6 +1833,9 @@
"swapUnknown": {
"message": "Unknown"
},
"swapUsingBestQuote": {
"message": "Using the best quote"
},
"swapVerifyTokenExplanation": {
"message": "Multiple tokens can use the same name and symbol. Check Etherscan to verify this is the token you're looking for."
},
@ -1846,15 +1852,11 @@
"swapsAdvancedOptions": {
"message": "Advanced Options"
},
"swapsBestQuote": {
"message": "Best quote"
},
"swapsConvertToAbout": {
"message": "Convert $1 to about",
"description": "This message is part of a quote for a swap. The $1 is the amount being converted, and the amount it is being swapped for is below this message"
"swapsExcessiveSlippageWarning": {
"message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%."
},
"swapsMaxSlippage": {
"message": "Max slippage"
"message": "Slippage Tolerance"
},
"swapsNotEnoughForTx": {
"message": "Not enough $1 to complete this transaction",

@ -10,6 +10,10 @@
<body>
<div id="app-content"></div>
<div id="popover-content"></div>
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
<script src="./initSentry.js" type="text/javascript" charset="utf-8"></script>
<script src="./lockdown.cjs" type="text/javascript" charset="utf-8"></script>
<script src="./runLockdown.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui-libs.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>

@ -0,0 +1,3 @@
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.00163 15.2045C6.21347 15.1962 6.45833 15.109 6.61275 14.9637L11.6498 10.2232C11.9385 9.90194 12.0577 9.28597 11.7239 8.92693C11.3952 8.57328 10.7576 8.5838 10.4276 8.93611L6.89052 12.2693L6.89052 1.87164C6.89052 1.38076 6.49254 0.982788 6.00163 0.982788C5.51071 0.982788 5.11274 1.38076 5.11274 1.87164L5.11274 12.2693L1.57565 8.93611C1.27181 8.63281 0.611843 8.57675 0.279352 8.92693C-0.0531095 9.27702 0.0531145 9.91513 0.353459 10.2232L5.3905 14.9637C5.56288 15.126 5.76513 15.205 6.00163 15.2045Z" fill="#D6D9DC"/>
</svg>

After

Width:  |  Height:  |  Size: 636 B

@ -2,6 +2,7 @@
"author": "https://metamask.io",
"background": {
"scripts": [
"globalthis.js",
"initSentry.js",
"lockdown.cjs",
"runLockdown.js",
@ -36,7 +37,12 @@
"content_scripts": [
{
"matches": ["file://*/*", "http://*/*", "https://*/*"],
"js": ["lockdown.cjs", "runLockdown.js", "contentscript.js"],
"js": [
"globalthis.js",
"lockdown.cjs",
"runLockdown.js",
"contentscript.js"
],
"run_at": "document_start",
"all_frames": true
},

@ -33,6 +33,10 @@
<img id="loading__spinner" src="./images/spinner.gif" alt="" />
</div>
<div id="popover-content"></div>
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
<script src="./initSentry.js" type="text/javascript" charset="utf-8"></script>
<script src="./lockdown.cjs" type="text/javascript" charset="utf-8"></script>
<script src="./runLockdown.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui-libs.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>

@ -2,6 +2,7 @@
<html lang="en">
<head>
<title>Ethereum Phishing Detection - MetaMask</title>
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
<script src="./lockdown.cjs" type="text/javascript" charset="utf-8"></script>
<script src="./runLockdown.js" type="text/javascript" charset="utf-8"></script>
<script src="./phishing-detect.js"></script>

@ -10,6 +10,7 @@
<body style="width:357px; height:600px;">
<div id="app-content"></div>
<div id="popover-content"></div>
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
<script src="./initSentry.js" type="text/javascript" charset="utf-8"></script>
<script src="./lockdown.cjs" type="text/javascript" charset="utf-8"></script>
<script src="./runLockdown.js" type="text/javascript" charset="utf-8"></script>

@ -19,6 +19,7 @@ import extension from 'extensionizer'
import storeTransform from 'obs-store/lib/transform'
import asStream from 'obs-store/lib/asStream'
import PortStream from 'extension-port-stream'
import { captureException } from '@sentry/browser'
import migrations from './migrations'
import Migrator from './lib/migrator'
import ExtensionPlatform from './platforms/extension'
@ -279,6 +280,7 @@ function setupController(initState, initLangCode) {
await localStore.set(state)
} catch (err) {
// log error so we dont break the pipeline
captureException(err)
log.error('error setting state in local store:', err)
}
}

@ -16,16 +16,13 @@ const inpageContent = fs.readFileSync(
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
const inpageBundle = inpageContent + inpageSuffix
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
//
// But for now that is only Firefox
// If we create a FireFox-only code path using that API,
// MetaMask will be much faster loading and performant on Firefox.
const CONTENT_SCRIPT = 'metamask-contentscript'
const INPAGE = 'metamask-inpage'
const PROVIDER = 'metamask-provider'
if (shouldInjectProvider()) {
injectScript(inpageBundle)
start()
setupStreams()
}
/**
@ -46,15 +43,6 @@ function injectScript(content) {
}
}
/**
* Sets up the stream communication and submits site metadata
*
*/
async function start() {
await setupStreams()
await domIsReady()
}
/**
* Sets up two-way communication streams between the
* browser extension and local per-page browser context.
@ -63,10 +51,10 @@ async function start() {
async function setupStreams() {
// the transport-specific streams for communication between inpage and background
const pageStream = new LocalMessageDuplexStream({
name: 'contentscript',
target: 'inpage',
name: CONTENT_SCRIPT,
target: INPAGE,
})
const extensionPort = extension.runtime.connect({ name: 'contentscript' })
const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT })
const extensionStream = new PortStream(extensionPort)
// create and connect channel muxers
@ -79,26 +67,26 @@ async function setupStreams() {
pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
)
pump(extensionMux, extensionStream, extensionMux, (err) =>
logStreamDisconnectWarning('MetaMask Background Multiplex', err),
)
pump(extensionMux, extensionStream, extensionMux, (err) => {
logStreamDisconnectWarning('MetaMask Background Multiplex', err)
notifyInpageOfStreamFailure()
})
// forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxers('provider', pageMux, extensionMux)
forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux)
forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux)
// connect "phishing" channel to warning system
const phishingStream = extensionMux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning)
}
function forwardTrafficBetweenMuxers(channelName, muxA, muxB) {
function forwardTrafficBetweenMuxes(channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName)
const channelB = muxB.createStream(channelName)
pump(channelA, channelB, channelA, (err) =>
logStreamDisconnectWarning(
pump(channelA, channelB, channelA, (error) =>
console.debug(
`MetaMask muxed traffic for channel "${channelName}" failed.`,
err,
error,
),
)
}
@ -107,14 +95,35 @@ function forwardTrafficBetweenMuxers(channelName, muxA, muxB) {
* Error handler for page to extension stream disconnections
*
* @param {string} remoteLabel - Remote stream name
* @param {Error} err - Stream connection error
* @param {Error} error - Stream connection error
*/
function logStreamDisconnectWarning(remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) {
warningMsg += `\n${err.stack}`
}
console.warn(warningMsg)
function logStreamDisconnectWarning(remoteLabel, error) {
console.debug(
`MetaMask Contentscript: Lost connection to "${remoteLabel}".`,
error,
)
}
/**
* This function must ONLY be called in pump destruction/close callbacks.
* Notifies the inpage context that streams have failed, via window.postMessage.
* Relies on obj-multiplex and post-message-stream implementation details.
*/
function notifyInpageOfStreamFailure() {
window.postMessage(
{
target: INPAGE, // the post-message-stream "target"
data: {
// this object gets passed to obj-multiplex
name: PROVIDER, // the obj-multiplex channel name
data: {
jsonrpc: '2.0',
method: 'METAMASK_STREAM_FAILURE',
},
},
},
window.location.origin,
)
}
/**
@ -221,17 +230,3 @@ function redirectToPhishingWarning() {
href: window.location.href,
})}`
}
/**
* Returns a promise that resolves when the DOM is loaded (does not wait for images to load)
*/
async function domIsReady() {
// already loaded
if (['interactive', 'complete'].includes(document.readyState)) {
return undefined
}
// wait for load
return new Promise((resolve) =>
window.addEventListener('DOMContentLoaded', resolve, { once: true }),
)
}

@ -47,7 +47,8 @@ export default class DetectTokensController {
for (const contractAddress in contracts) {
if (
contracts[contractAddress].erc20 &&
!this.tokenAddresses.includes(contractAddress.toLowerCase())
!this.tokenAddresses.includes(contractAddress.toLowerCase()) &&
!this.hiddenTokens.includes(contractAddress.toLowerCase())
) {
tokensToDetect.push(contractAddress)
}
@ -130,10 +131,12 @@ export default class DetectTokensController {
this.tokenAddresses = currentTokens
? currentTokens.map((token) => token.address)
: []
preferences.store.subscribe(({ tokens = [] }) => {
this.hiddenTokens = preferences.store.getState().hiddenTokens
preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address
})
this.hiddenTokens = hiddenTokens
})
preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) {

@ -7,7 +7,8 @@ import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'
import createInfuraMiddleware from 'eth-json-rpc-infura'
import BlockTracker from 'eth-block-tracker'
import * as networkEnums from './enums'
import { NETWORK_TYPE_TO_ID_MAP } from './enums'
export default function createInfuraClient({ network, projectId }) {
const infuraMiddleware = createInfuraMiddleware({
@ -32,36 +33,14 @@ export default function createInfuraClient({ network, projectId }) {
}
function createNetworkAndChainIdMiddleware({ network }) {
let chainId
let netId
switch (network) {
case 'mainnet':
netId = networkEnums.MAINNET_NETWORK_ID
chainId = '0x01'
break
case 'ropsten':
netId = networkEnums.ROPSTEN_NETWORK_ID
chainId = '0x03'
break
case 'rinkeby':
netId = networkEnums.RINKEBY_NETWORK_ID
chainId = '0x04'
break
case 'kovan':
netId = networkEnums.KOVAN_NETWORK_ID
chainId = networkEnums.KOVAN_CHAIN_ID
break
case 'goerli':
netId = networkEnums.GOERLI_NETWORK_ID
chainId = '0x05'
break
default:
throw new Error(`createInfuraClient - unknown network "${network}"`)
if (!NETWORK_TYPE_TO_ID_MAP[network]) {
throw new Error(`createInfuraClient - unknown network "${network}"`)
}
const { chainId, networkId } = NETWORK_TYPE_TO_ID_MAP[network]
return createScaffoldMiddleware({
eth_chainId: chainId,
net_version: netId,
net_version: networkId,
})
}

@ -19,6 +19,8 @@ import {
MAINNET,
INFURA_PROVIDER_TYPES,
NETWORK_TYPE_TO_ID_MAP,
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from './enums'
const env = process.env.METAMASK_ENV
@ -32,9 +34,9 @@ if (process.env.IN_TEST === 'true') {
nickname: 'Localhost 8545',
}
} else if (process.env.METAMASK_DEBUG || env === 'test') {
defaultProviderConfigOpts = { type: RINKEBY }
defaultProviderConfigOpts = { type: RINKEBY, chainId: RINKEBY_CHAIN_ID }
} else {
defaultProviderConfigOpts = { type: MAINNET }
defaultProviderConfigOpts = { type: MAINNET, chainId: MAINNET_CHAIN_ID }
}
const defaultProviderConfig = {

@ -19,10 +19,15 @@ export const CAVEAT_TYPES = {
}
export const NOTIFICATION_NAMES = {
accountsChanged: 'wallet_accountsChanged',
accountsChanged: 'metamask_accountsChanged',
unlockStateChanged: 'metamask_unlockStateChanged',
chainChanged: 'metamask_chainChanged',
}
export const LOG_IGNORE_METHODS = ['wallet_sendDomainMetadata']
export const LOG_IGNORE_METHODS = [
'wallet_registerOnboarding',
'wallet_watchAsset',
]
export const LOG_METHOD_TYPES = {
restricted: 'restricted',
@ -82,8 +87,9 @@ export const SAFE_METHODS = [
'eth_submitWork',
'eth_syncing',
'eth_uninstallFilter',
'metamask_watchAsset',
'wallet_watchAsset',
'eth_getEncryptionPublicKey',
'eth_decrypt',
'metamask_watchAsset',
'wallet_watchAsset',
'metamask_getProviderState',
]

@ -28,6 +28,7 @@ export class PermissionsController {
getKeyringAccounts,
getRestrictedMethods,
getUnlockPromise,
isUnlocked,
notifyDomain,
notifyAllDomains,
preferences,
@ -47,6 +48,7 @@ export class PermissionsController {
this._notifyDomain = notifyDomain
this._notifyAllDomains = notifyAllDomains
this._showPermissionRequest = showPermissionRequest
this._isUnlocked = isUnlocked
this._restrictedMethods = getRestrictedMethods({
getKeyringAccounts: this.getKeyringAccounts.bind(this),
@ -463,21 +465,20 @@ export class PermissionsController {
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
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
// We do not share accounts when the extension is locked.
if (this._isUnlocked()) {
this._notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
params: newAccounts,
})
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
}
// NOTE:
// we don't check for accounts changing in the notifyAllDomains case,
// because the log only records when accounts were last seen,
// and the accounts only change for all domains at once when permissions
// are removed
// We don't check for accounts changing in the notifyAllDomains case,
// because the log only records when accounts were last seen, and the
// the accounts only change for all domains at once when permissions are
// removed.
}
/**
@ -508,9 +509,11 @@ export class PermissionsController {
*/
clearPermissions() {
this.permissions.clearDomains()
// It's safe to notify that no accounts are available, regardless of
// extension lock state
this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged,
result: [],
params: [],
})
}
@ -749,7 +752,3 @@ export class PermissionsController {
)
}
}
export function addInternalMethodPrefix(method) {
return WALLET_PREFIX + method
}

@ -58,6 +58,7 @@ export default class PermissionsLogController {
/**
* Updates the exposed account history for the given origin.
* Sets the 'last seen' time to Date.now() for the given accounts.
* Returns if the accounts array is empty.
*
* @param {string} origin - The origin that the accounts are exposed to.
* @param {Array<string>} accounts - The accounts.

@ -73,7 +73,7 @@ export default function createPermissionsMethodMiddleware({
// custom method for getting metadata from the requesting domain,
// sent automatically by the inpage provider when it's initialized
case 'wallet_sendDomainMetadata': {
case 'metamask_sendDomainMetadata': {
if (typeof req.domainMetadata?.name === 'string') {
addDomainMetadata(req.origin, req.domainMetadata)
}

@ -34,8 +34,10 @@ export default class PreferencesController {
const initState = {
frequentRpcListDetail: [],
accountTokens: {},
accountHiddenTokens: {},
assetImages: {},
tokens: [],
hiddenTokens: [],
suggestedTokens: {},
useBlockie: false,
useNonceField: false,
@ -191,6 +193,7 @@ export default class PreferencesController {
setAddresses(addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
@ -202,7 +205,12 @@ export default class PreferencesController {
tokens[address] = oldTokens
return tokens
}, {})
this.store.updateState({ identities, accountTokens })
const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => {
const oldHiddenTokens = oldAccountHiddenTokens[address] || {}
hiddenTokens[address] = oldHiddenTokens
return hiddenTokens
}, {})
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
}
/**
@ -212,14 +220,19 @@ export default class PreferencesController {
* @returns {string} the address that was removed
*/
removeAddress(address) {
const { identities } = this.store.getState()
const { accountTokens } = this.store.getState()
const {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState()
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`)
}
delete identities[address]
delete accountTokens[address]
this.store.updateState({ identities, accountTokens })
delete accountHiddenTokens[address]
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
// If the selected account is no longer valid,
// select an arbitrary other account:
@ -237,7 +250,11 @@ export default class PreferencesController {
*
*/
addAddresses(addresses) {
const { identities, accountTokens } = this.store.getState()
const {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState()
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) {
@ -247,9 +264,10 @@ export default class PreferencesController {
const identityCount = Object.keys(identities).length
accountTokens[address] = {}
accountHiddenTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address }
})
this.store.updateState({ identities, accountTokens })
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
}
/**
@ -346,7 +364,7 @@ export default class PreferencesController {
*/
/**
* Adds a new token to the token array, or updates the token if passed an address that already exists.
* Adds a new token to the token array and removes it from the hiddenToken array, or updates the token if passed an address that already exists.
* Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects.
* @see AddedToken {@link AddedToken}
*
@ -359,8 +377,11 @@ export default class PreferencesController {
async addToken(rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const { tokens } = this.store.getState()
const { tokens, hiddenTokens } = this.store.getState()
const assetImages = this.getAssetImages()
const updatedHiddenTokens = hiddenTokens.filter(
(tokenAddress) => tokenAddress !== rawAddress.toLowerCase(),
)
const previousEntry = tokens.find((token) => {
return token.address === address
})
@ -372,23 +393,24 @@ export default class PreferencesController {
tokens.push(newEntry)
}
assetImages[address] = image
this._updateAccountTokens(tokens, assetImages)
this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens)
return Promise.resolve(tokens)
}
/**
* Removes a specified token from the tokens array.
* Removes a specified token from the tokens array and adds it to hiddenTokens array
*
* @param {string} rawAddress - Hex address of the token contract to remove.
* @returns {Promise<array>} The new array of AddedToken objects
*
*/
removeToken(rawAddress) {
const { tokens } = this.store.getState()
const { tokens, hiddenTokens } = this.store.getState()
const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter((token) => token.address !== rawAddress)
const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()]
delete assetImages[rawAddress]
this._updateAccountTokens(updatedTokens, assetImages)
this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens)
return Promise.resolve(updatedTokens)
}
@ -643,47 +665,59 @@ export default class PreferencesController {
*/
_subscribeProviderType() {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
const { tokens, hiddenTokens } = this._getTokenRelatedStates()
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens)
})
}
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
* Updates `accountTokens`, `tokens`, `accountHiddenTokens` and `hiddenTokens` of current account and network according to it.
*
* @param {Array} tokens - Array of tokens to be updated.
* @param {array} tokens - Array of tokens to be updated.
* @param {array} assetImages - Array of assets objects related to assets added
* @param {array} hiddenTokens - Array of tokens hidden by user
*
*/
_updateAccountTokens(tokens, assetImages) {
_updateAccountTokens(tokens, assetImages, hiddenTokens) {
const {
accountTokens,
providerType,
selectedAddress,
accountHiddenTokens,
} = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens, assetImages })
accountHiddenTokens[selectedAddress][providerType] = hiddenTokens
this.store.updateState({
accountTokens,
tokens,
assetImages,
accountHiddenTokens,
hiddenTokens,
})
}
/**
* Updates `tokens` of current account and network.
* Updates `tokens` and `hiddenTokens` of current account and network.
*
* @param {string} selectedAddress - Account address to be updated with.
*
*/
_updateTokens(selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
const { tokens, hiddenTokens } = this._getTokenRelatedStates(
selectedAddress,
)
this.store.updateState({ tokens, hiddenTokens })
}
/**
* A getter for `tokens` and `accountTokens` related states.
* A getter for `tokens`, `accountTokens`, `hiddenTokens` and `accountHiddenTokens` related states.
*
* @param {string} [selectedAddress] - A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates(selectedAddress) {
const { accountTokens } = this.store.getState()
const { accountTokens, accountHiddenTokens } = this.store.getState()
if (!selectedAddress) {
// eslint-disable-next-line no-param-reassign
selectedAddress = this.store.getState().selectedAddress
@ -692,11 +726,25 @@ export default class PreferencesController {
if (!(selectedAddress in accountTokens)) {
accountTokens[selectedAddress] = {}
}
if (!(selectedAddress in accountHiddenTokens)) {
accountHiddenTokens[selectedAddress] = {}
}
if (!(providerType in accountTokens[selectedAddress])) {
accountTokens[selectedAddress][providerType] = []
}
if (!(providerType in accountHiddenTokens[selectedAddress])) {
accountHiddenTokens[selectedAddress][providerType] = []
}
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
const hiddenTokens = accountHiddenTokens[selectedAddress][providerType]
return {
tokens,
accountTokens,
hiddenTokens,
accountHiddenTokens,
providerType,
selectedAddress,
}
}
/**

@ -827,9 +827,9 @@ export default class TransactionController extends EventEmitter {
].find((methodName) => methodName === name && name.toLowerCase())
let result
if (txParams.data && tokenMethodName) {
if (data && tokenMethodName) {
result = tokenMethodName
} else if (txParams.data && !to) {
} else if (data && !to) {
result = TRANSACTION_CATEGORIES.DEPLOY_CONTRACT
}

@ -1,4 +1,5 @@
import { isValidAddress } from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors'
import { addHexPrefix } from '../../../lib/util'
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'
@ -37,19 +38,30 @@ export function normalizeTxParams(txParams, lowerCase = true) {
* @throws {Error} if the tx params contains invalid fields
*/
export function validateTxParams(txParams) {
if (!txParams || typeof txParams !== 'object' || Array.isArray(txParams)) {
throw ethErrors.rpc.invalidParams(
'Invalid transaction params: must be an object.',
)
}
if (!txParams.to && !txParams.data) {
throw ethErrors.rpc.invalidParams(
'Invalid transaction params: must specify "data" for contract deployments, or "to" (and optionally "data") for all other types of transactions.',
)
}
validateFrom(txParams)
validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(
`Invalid transaction value of ${txParams.value} not a positive number.`,
throw ethErrors.rpc.invalidParams(
`Invalid transaction value "${txParams.value}": not a positive number.`,
)
}
if (value.includes('.')) {
throw new Error(
`Invalid transaction value of ${txParams.value} number must be in wei`,
throw ethErrors.rpc.invalidParams(
`Invalid transaction value of "${txParams.value}": number must be in wei.`,
)
}
}
@ -62,10 +74,12 @@ export function validateTxParams(txParams) {
*/
export function validateFrom(txParams) {
if (!(typeof txParams.from === 'string')) {
throw new Error(`Invalid from address ${txParams.from} not a string`)
throw ethErrors.rpc.invalidParams(
`Invalid "from" address "${txParams.from}": not a string.`,
)
}
if (!isValidAddress(txParams.from)) {
throw new Error('Invalid from address')
throw ethErrors.rpc.invalidParams('Invalid "from" address.')
}
}
@ -80,10 +94,10 @@ export function validateRecipient(txParams) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
throw ethErrors.rpc.invalidParams('Invalid "to" address.')
}
} else if (txParams.to !== undefined && !isValidAddress(txParams.to)) {
throw new Error('Invalid recipient address')
throw ethErrors.rpc.invalidParams('Invalid "to" address.')
}
return txParams
}

@ -33,11 +33,7 @@ cleanContextForImports()
/* eslint-disable import/first */
import log from 'loglevel'
import LocalMessageDuplexStream from 'post-message-stream'
import { initProvider } from '@metamask/inpage-provider'
// TODO:deprecate:2020
import setupWeb3 from './lib/setupWeb3'
/* eslint-enable import/first */
import { initializeProvider } from '@metamask/inpage-provider'
restoreContextAfterImports()
@ -49,24 +45,12 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
// setup background connection
const metamaskStream = new LocalMessageDuplexStream({
name: 'inpage',
target: 'contentscript',
name: 'metamask-inpage',
target: 'metamask-contentscript',
})
initProvider({
initializeProvider({
connectionStream: metamaskStream,
logger: log,
shouldShimWeb3: true,
})
// TODO:deprecate:2020
// Setup web3
if (typeof window.web3 === 'undefined') {
// proxy web3, assign to window, and set up site auto reload
setupWeb3(log)
} else {
log.warn(`MetaMask detected another web3.
MetaMask will not work reliably with another web3 extension.
This usually happens if you have two MetaMasks installed,
or MetaMask and another web3 extension. Please remove one
and try again.`)
}

@ -23,7 +23,8 @@ const MESSAGE_TYPE = {
ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey',
ETH_SIGN: 'eth_sign',
ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
LOG_WEB3_USAGE: 'metamask_logInjectedWeb3Usage',
GET_PROVIDER_STATE: 'metamask_getProviderState',
LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage',
PERSONAL_SIGN: 'personal_sign',
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',

@ -21,7 +21,6 @@ const handlerMap = handlers.reduce((map, handler) => {
* Eventually, we'll want to extract this middleware into its own package.
*
* @param {Object} opts - The middleware options
* @param {string} opts.origin - The origin for the middleware stack
* @param {Function} opts.sendMetrics - A function for sending a metrics event
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
*/

@ -0,0 +1,46 @@
import { MESSAGE_TYPE } from '../../enums'
/**
* This RPC method gets background state relevant to the provider.
* The background sends RPC notifications on state changes, but the provider
* first requests state on initialization.
*/
const getProviderState = {
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
implementation: getProviderStateHandler,
}
export default getProviderState
/**
* @typedef {Object} ProviderStateHandlerResult
* @property {string} chainId - The current chain ID.
* @property {boolean} isUnlocked - Whether the extension is unlocked or not.
* @property {string} networkVersion - The current network ID.
*/
/**
* @typedef {Object} ProviderStateHandlerOptions
* @property {() => ProviderStateHandlerResult} getProviderState - A function that
* gets the current provider state.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<[]>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<ProviderStateHandlerResult>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {ProviderStateHandlerOptions} options
*/
async function getProviderStateHandler(
req,
res,
_next,
end,
{ getProviderState: _getProviderState },
) {
res.result = {
...(await _getProviderState(req.origin)),
}
return end()
}

@ -1,5 +1,6 @@
import logWeb3Usage from './log-web3-usage'
import getProviderState from './get-provider-state'
import logWeb3ShimUsage from './log-web3-shim-usage'
import watchAsset from './watch-asset'
const handlers = [logWeb3Usage, watchAsset]
const handlers = [getProviderState, logWeb3ShimUsage, watchAsset]
export default handlers

@ -0,0 +1,49 @@
import { MESSAGE_TYPE } from '../../enums'
/**
* This RPC method is called by the inpage provider whenever it detects the
* accessing of a non-existent property on our window.web3 shim.
* We collect this data to understand which sites are breaking due to the
* removal of our window.web3.
*/
const logWeb3ShimUsage = {
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
implementation: logWeb3ShimUsageHandler,
}
export default logWeb3ShimUsage
const recordedWeb3ShimUsage = {}
/**
* @typedef {Object} LogWeb3ShimUsageOptions
* @property {Function} sendMetrics - A function that registers a metrics event.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {LogWeb3ShimUsageOptions} options
*/
function logWeb3ShimUsageHandler(req, res, _next, end, { sendMetrics }) {
const { origin } = req
if (!recordedWeb3ShimUsage[origin]) {
recordedWeb3ShimUsage[origin] = true
sendMetrics({
event: `Website Accessed window.web3 Shim`,
category: 'inpage_provider',
eventContext: {
referrer: {
url: origin,
},
},
excludeMetaMetricsId: true,
})
}
res.result = true
return end()
}

@ -1,63 +0,0 @@
import { MESSAGE_TYPE } from '../../enums'
/**
* This RPC method is called by our inpage web3 proxy whenever window.web3 is
* accessed. We're collecting data on window.web3 usage so that we can warn
* website maintainers, and possibly our users, before we remove window.web3
* by November 16, 2020.
*/
const logWeb3Usage = {
methodNames: [MESSAGE_TYPE.LOG_WEB3_USAGE],
implementation: logWeb3UsageHandler,
}
export default logWeb3Usage
const recordedWeb3Usage = {}
/**
* @typedef {Object} LogWeb3UsageOptions
* @property {string} origin - The origin of the request.
* @property {Function} sendMetrics - A function that registers a metrics event.
*/
/**
* @typedef {Object} LogWeb3UsageParam
* @property {string} action - The action taken (get or set).
* @property {string} name - The window.web3 property name subject to the action.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<[LogWeb3UsageParam]>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {LogWeb3UsageOptions} options
*/
function logWeb3UsageHandler(req, res, _next, end, { origin, sendMetrics }) {
const { action, path } = req.params[0]
if (!recordedWeb3Usage[origin]) {
recordedWeb3Usage[origin] = {}
}
if (!recordedWeb3Usage[origin][path]) {
recordedWeb3Usage[origin][path] = true
sendMetrics(
{
event: `Website Used window.web3`,
category: 'inpage_provider',
properties: { action, web3Path: path },
referrer: {
url: origin,
},
},
{
excludeMetaMetricsId: true,
},
)
}
res.result = true
return end()
}

@ -1,251 +0,0 @@
/*global Web3*/
// TODO:deprecate:2020
// Delete this file
import web3Entitites from './web3-entities.json'
import 'web3/dist/web3.min'
const shouldLogUsage = ![
'docs.metamask.io',
'metamask.github.io',
'metamask.io',
].includes(window.location.hostname)
/**
* To understand how we arrived at this implementation, please see:
* https://github.com/ethereum/web3.js/blob/0.20.7/DOCUMENTATION.md
*/
export default function setupWeb3(log) {
// export web3 as a global, checking for usage
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
let hasBeenWarned = false
const web3 = new Web3(window.ethereum)
web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider')
}
Object.defineProperty(web3, '__isMetaMaskShim__', {
value: true,
enumerable: false,
configurable: false,
writable: false,
})
Object.defineProperty(window.ethereum, '_web3Ref', {
enumerable: false,
writable: true,
configurable: true,
value: web3.eth,
})
// Setup logging of nested property usage
if (shouldLogUsage) {
// web3 namespaces with common and uncommon dapp actions
const includedTopKeys = [
'eth',
'db',
'shh',
'net',
'personal',
'bzz',
'version',
]
// For each top-level property, create appropriate Proxy traps for all of
// their properties
includedTopKeys.forEach((topKey) => {
const applyTrapKeys = new Map()
const getTrapKeys = new Map()
Object.keys(web3[topKey]).forEach((key) => {
const path = `web3.${topKey}.${key}`
if (web3Entitites[path]) {
if (web3Entitites[path] === 'function') {
applyTrapKeys.set(key, path)
} else {
getTrapKeys.set(key, path)
}
}
})
// Create apply traps for function properties
for (const [key, path] of applyTrapKeys) {
web3[topKey][key] = new Proxy(web3[topKey][key], {
apply: (...params) => {
try {
window.ethereum.request({
method: 'metamask_logInjectedWeb3Usage',
params: [
{
action: 'apply',
path,
},
],
})
} catch (error) {
log.debug('Failed to log web3 usage.', error)
}
// Call function normally
return Reflect.apply(...params)
},
})
}
// Create get trap for non-function properties
web3[topKey] = new Proxy(web3[topKey], {
get: (web3Prop, key, ...params) => {
const name = stringifyKey(key)
if (getTrapKeys.has(name)) {
try {
window.ethereum.request({
method: 'metamask_logInjectedWeb3Usage',
params: [
{
action: 'get',
path: getTrapKeys.get(name),
},
],
})
} catch (error) {
log.debug('Failed to log web3 usage.', error)
}
}
// return value normally
return Reflect.get(web3Prop, key, ...params)
},
})
})
const topLevelFunctions = [
'isConnected',
'setProvider',
'reset',
'sha3',
'toHex',
'toAscii',
'fromAscii',
'toDecimal',
'fromDecimal',
'fromWei',
'toWei',
'toBigNumber',
'isAddress',
]
// apply-trap top-level functions
topLevelFunctions.forEach((key) => {
// This type check is probably redundant, but we've been burned before.
if (typeof web3[key] === 'function') {
web3[key] = new Proxy(web3[key], {
apply: (...params) => {
try {
window.ethereum.request({
method: 'metamask_logInjectedWeb3Usage',
params: [
{
action: 'apply',
path: `web3.${key}`,
},
],
})
} catch (error) {
log.debug('Failed to log web3 usage.', error)
}
// Call function normally
return Reflect.apply(...params)
},
})
}
})
}
const web3Proxy = new Proxy(web3, {
get: (...params) => {
// get the time of use
lastTimeUsed = Date.now()
// show warning once on web3 access
if (!hasBeenWarned) {
console.warn(
`MetaMask: We will stop injecting web3 in Q4 2020.\nPlease see this article for more information: https://medium.com/metamask/no-longer-injecting-web3-js-4a899ad6e59e`,
)
hasBeenWarned = true
}
// return value normally
return Reflect.get(...params)
},
})
Object.defineProperty(window, 'web3', {
enumerable: false,
writable: true,
configurable: true,
value: web3Proxy,
})
log.debug('MetaMask - injected web3')
window.ethereum._publicConfigStore.subscribe((state) => {
// if the auto refresh on network change is false do not
// do anything
if (!window.ethereum.autoRefreshOnNetworkChange) {
return
}
// if reload in progress, no need to check reload logic
if (reloadInProgress) {
return
}
const currentNetwork = state.networkVersion
// set the initial network
if (!lastSeenNetwork) {
lastSeenNetwork = currentNetwork
return
}
// skip reload logic if web3 not used
if (!lastTimeUsed) {
return
}
// if network did not change, exit
if (currentNetwork === lastSeenNetwork) {
return
}
// initiate page reload
reloadInProgress = true
const timeSinceUse = Date.now() - lastTimeUsed
// if web3 was recently used then delay the reloading of the page
if (timeSinceUse > 500) {
triggerReset()
} else {
setTimeout(triggerReset, 500)
}
})
}
// reload the page
function triggerReset() {
window.location.reload()
}
/**
* Returns a "stringified" key. Keys that are already strings are returned
* unchanged, and any non-string values are returned as "typeof <type>".
*
* @param {any} key - The key to stringify
*/
function stringifyKey(key) {
return typeof key === 'string' ? key : `typeof ${typeof key}`
}

@ -1,101 +0,0 @@
{
"web3.bzz.blockNetworkRead": "function",
"web3.bzz.download": "function",
"web3.bzz.get": "function",
"web3.bzz.getHive": "function",
"web3.bzz.getInfo": "function",
"web3.bzz.hive": "TRAP",
"web3.bzz.info": "TRAP",
"web3.bzz.modify": "function",
"web3.bzz.put": "function",
"web3.bzz.retrieve": "function",
"web3.bzz.store": "function",
"web3.bzz.swapEnabled": "function",
"web3.bzz.syncEnabled": "function",
"web3.bzz.upload": "function",
"web3.db.getHex": "function",
"web3.db.getString": "function",
"web3.db.putHex": "function",
"web3.db.putString": "function",
"web3.eth.accounts": "object",
"web3.eth.blockNumber": "TRAP",
"web3.eth.call": "function",
"web3.eth.coinbase": "object",
"web3.eth.compile": "object",
"web3.eth.estimateGas": "function",
"web3.eth.gasPrice": "TRAP",
"web3.eth.getAccounts": "function",
"web3.eth.getBalance": "function",
"web3.eth.getBlock": "function",
"web3.eth.getBlockNumber": "function",
"web3.eth.getBlockTransactionCount": "function",
"web3.eth.getBlockUncleCount": "function",
"web3.eth.getCode": "function",
"web3.eth.getCoinbase": "function",
"web3.eth.getCompilers": "function",
"web3.eth.getGasPrice": "function",
"web3.eth.getHashrate": "function",
"web3.eth.getMining": "function",
"web3.eth.getProtocolVersion": "function",
"web3.eth.getStorageAt": "function",
"web3.eth.getSyncing": "function",
"web3.eth.getTransaction": "function",
"web3.eth.getTransactionCount": "function",
"web3.eth.getTransactionFromBlock": "function",
"web3.eth.getTransactionReceipt": "function",
"web3.eth.getUncle": "function",
"web3.eth.getWork": "function",
"web3.eth.hashrate": "TRAP",
"web3.eth.iban": "function",
"web3.eth.mining": "TRAP",
"web3.eth.protocolVersion": "TRAP",
"web3.eth.sendIBANTransaction": "function",
"web3.eth.sendRawTransaction": "function",
"web3.eth.sendTransaction": "function",
"web3.eth.sign": "function",
"web3.eth.signTransaction": "function",
"web3.eth.submitWork": "function",
"web3.eth.syncing": "TRAP",
"web3.net.getListening": "function",
"web3.net.getPeerCount": "function",
"web3.net.listening": "TRAP",
"web3.net.peerCount": "TRAP",
"web3.personal.ecRecover": "function",
"web3.personal.getListAccounts": "function",
"web3.personal.importRawKey": "function",
"web3.personal.listAccounts": "TRAP",
"web3.personal.lockAccount": "function",
"web3.personal.newAccount": "function",
"web3.personal.sendTransaction": "function",
"web3.personal.sign": "function",
"web3.personal.unlockAccount": "function",
"web3.providers.HttpProvider": "function",
"web3.providers.IpcProvider": "function",
"web3.shh.addPrivateKey": "function",
"web3.shh.addSymKey": "function",
"web3.shh.deleteKeyPair": "function",
"web3.shh.deleteSymKey": "function",
"web3.shh.generateSymKeyFromPassword": "function",
"web3.shh.getPrivateKey": "function",
"web3.shh.getPublicKey": "function",
"web3.shh.getSymKey": "function",
"web3.shh.hasKeyPair": "function",
"web3.shh.hasSymKey": "function",
"web3.shh.info": "function",
"web3.shh.markTrustedPeer": "function",
"web3.shh.newKeyPair": "function",
"web3.shh.newSymKey": "function",
"web3.shh.post": "function",
"web3.shh.setMaxMessageSize": "function",
"web3.shh.setMinPoW": "function",
"web3.shh.version": "function",
"web3.version.api": "string",
"web3.version.ethereum": "TRAP",
"web3.version.getEthereum": "function",
"web3.version.getNetwork": "function",
"web3.version.getNode": "function",
"web3.version.getWhisper": "function",
"web3.version.network": "string",
"web3.version.node": "TRAP",
"web3.version.whisper": "TRAP"
}

@ -1,9 +1,6 @@
import EventEmitter from 'events'
import pump from 'pump'
import Dnode from 'dnode'
import ObservableStore from 'obs-store'
import asStream from 'obs-store/lib/asStream'
import { JsonRpcEngine } from 'json-rpc-engine'
import { debounce } from 'lodash'
import createEngineStream from 'json-rpc-middleware-stream/engineStream'
@ -53,6 +50,7 @@ import TokenRatesController from './controllers/token-rates'
import DetectTokensController from './controllers/detect-tokens'
import SwapsController from './controllers/swaps'
import { PermissionsController } from './controllers/permissions'
import { NOTIFICATION_NAMES } from './controllers/permissions/enums'
import getRestrictedMethods from './controllers/permissions/restrictedMethods'
import nodeify from './lib/nodeify'
import accountImporter from './account-import-strategies'
@ -219,10 +217,11 @@ export default class MetamaskController extends EventEmitter {
initState: initState.KeyringController,
encryptor: opts.encryptor || undefined,
})
this.keyringController.memStore.subscribe((s) =>
this._onKeyringControllerUpdate(s),
this.keyringController.memStore.subscribe((state) =>
this._onKeyringControllerUpdate(state),
)
this.keyringController.on('unlock', () => this.emit('unlock'))
this.keyringController.on('lock', () => this._onLock())
this.permissionsController = new PermissionsController(
{
@ -233,6 +232,7 @@ export default class MetamaskController extends EventEmitter {
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController,
),
isUnlocked: this.isUnlocked.bind(this),
notifyDomain: this.notifyConnections.bind(this),
notifyAllDomains: this.notifyAllConnections.bind(this),
preferences: this.preferencesController.store,
@ -287,7 +287,9 @@ export default class MetamaskController extends EventEmitter {
),
provider: this.provider,
blockTracker: this.blockTracker,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
getParticipateInMetrics: () =>
this.metaMetricsController.state.participateInMetaMetrics,
})
@ -346,6 +348,9 @@ export default class MetamaskController extends EventEmitter {
tokenRatesStore: this.tokenRatesController.store,
})
// ensure isClientOpenAndUnlocked is updated when memState updates
this.on('update', (memState) => this._onStateUpdate(memState))
this.store.updateStructure({
AppStateController: this.appStateController.store,
TransactionController: this.txController.store,
@ -448,38 +453,37 @@ export default class MetamaskController extends EventEmitter {
}
/**
* Constructor helper: initialize a public config store.
* This store is used to make some config info available to Dapps synchronously.
*/
createPublicConfigStore() {
// subset of state for metamask inpage provider
const publicConfigStore = new ObservableStore()
const { networkController } = this
// setup memStore subscription hooks
this.on('update', updatePublicConfigStore)
updatePublicConfigStore(this.getState())
publicConfigStore.destroy = () => {
this.removeEventListener &&
this.removeEventListener('update', updatePublicConfigStore)
}
function updatePublicConfigStore(memState) {
const chainId = networkController.getCurrentChainId()
if (memState.network !== 'loading') {
publicConfigStore.putState(selectPublicState(chainId, memState))
}
* Gets relevant state for the provider of an external origin.
*
* @param {string} origin - The origin to get the provider state for.
* @returns {Promise<{
* isUnlocked: boolean,
* networkVersion: string,
* chainId: string,
* accounts: string[],
* }>} An object with relevant state properties.
*/
async getProviderState(origin) {
return {
isUnlocked: this.isUnlocked(),
...this.getProviderNetworkState(),
accounts: await this.permissionsController.getAccounts(origin),
}
}
function selectPublicState(chainId, { isUnlocked, network }) {
return {
isUnlocked,
chainId,
networkVersion: network,
}
/**
* Gets network state relevant for external providers.
*
* @param {Object} [memState] - The MetaMask memState. If not provided,
* this function will retrieve the most recent state.
* @returns {Object} An object with relevant network state properties.
*/
getProviderNetworkState(memState) {
const { network } = memState || this.getState()
return {
chainId: this.networkController.getCurrentChainId(),
networkVersion: network,
}
return publicConfigStore
}
//=============================================================================
@ -1811,8 +1815,7 @@ export default class MetamaskController extends EventEmitter {
const mux = setupMultiplex(connectionStream)
// messages between inpage and background
this.setupProviderConnection(mux.createStream('provider'), sender)
this.setupPublicConfig(mux.createStream('publicConfig'))
this.setupProviderConnection(mux.createStream('metamask-provider'), sender)
}
/**
@ -1969,7 +1972,10 @@ export default class MetamaskController extends EventEmitter {
engine.push(
createMethodMiddleware({
origin,
sendMetrics: this.metaMetricsController.trackEvent,
getProviderState: this.getProviderState.bind(this),
sendMetrics: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind(
this.preferencesController,
),
@ -1989,29 +1995,6 @@ export default class MetamaskController extends EventEmitter {
return engine
}
/**
* A method for providing our public config info over a stream.
* This includes info we like to be synchronous if possible, like
* the current selected account, and network ID.
*
* Since synchronous methods have been deprecated in web3,
* this is a good candidate for deprecation.
*
* @param {*} outStream - The stream to provide public config over.
*/
setupPublicConfig(outStream) {
const configStore = this.createPublicConfigStore()
const configStream = asStream(configStore)
pump(configStream, outStream, (err) => {
configStore.destroy()
configStream.destroy()
if (err) {
log.error(err)
}
})
}
/**
* Adds a reference to a connection by origin. Ignores the 'metamask' origin.
* Caller must ensure that the returned id is stored such that the reference
@ -2062,37 +2045,51 @@ export default class MetamaskController extends EventEmitter {
/**
* Causes the RPC engines associated with the connections to the given origin
* to emit a notification event with the given payload.
* Does nothing if the extension is locked or the origin is unknown.
*
* The caller is responsible for ensuring that only permitted notifications
* are sent.
*
* Ignores unknown origins.
*
* @param {string} origin - The connection's origin string.
* @param {any} payload - The event payload.
*/
notifyConnections(origin, payload) {
const connections = this.connections[origin]
if (!this.isUnlocked() || !connections) {
return
}
Object.values(connections).forEach((conn) => {
conn.engine && conn.engine.emit('notification', payload)
})
if (connections) {
Object.values(connections).forEach((conn) => {
if (conn.engine) {
conn.engine.emit('notification', payload)
}
})
}
}
/**
* Causes the RPC engines associated with all connections to emit a
* notification event with the given payload.
* Does nothing if the extension is locked.
*
* @param {any} payload - The event payload.
* If the "payload" parameter is a function, the payload for each connection
* will be the return value of that function called with the connection's
* origin.
*
* The caller is responsible for ensuring that only permitted notifications
* are sent.
*
* @param {any} payload - The event payload, or payload getter function.
*/
notifyAllConnections(payload) {
if (!this.isUnlocked()) {
return
}
const getPayload =
typeof payload === 'function'
? (origin) => payload(origin)
: () => payload
Object.values(this.connections).forEach((origin) => {
Object.values(origin).forEach((conn) => {
conn.engine && conn.engine.emit('notification', payload)
if (conn.engine) {
conn.engine.emit('notification', getPayload(origin))
}
})
})
}
@ -2121,6 +2118,51 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker.syncWithAddresses(addresses)
}
/**
* Handle global unlock, triggered by KeyringController unlock.
* Notifies all connections that the extension is unlocked.
*/
_onUnlock() {
this.notifyAllConnections((origin) => {
return {
method: NOTIFICATION_NAMES.unlockStateChanged,
params: {
isUnlocked: true,
accounts: this.permissionsController.getAccounts(origin),
},
}
})
this.emit('unlock')
}
/**
* Handle global lock, triggered by KeyringController lock.
* Notifies all connections that the extension is locked.
*/
_onLock() {
this.notifyAllConnections({
method: NOTIFICATION_NAMES.unlockStateChanged,
params: {
isUnlocked: false,
},
})
this.emit('lock')
}
/**
* Handle memory state updates.
* - Ensure isClientOpenAndUnlocked is updated
* - Notifies all connections with the new provider network state
* - The external providers handle diffing the state
*/
_onStateUpdate(newState) {
this.isClientOpenAndUnlocked = newState.isUnlocked && this._isClientOpen
this.notifyAllConnections({
method: NOTIFICATION_NAMES.chainChanged,
params: this.getProviderNetworkState(newState),
})
}
// misc
/**

@ -1,6 +1,7 @@
// Freezes all intrinsics
// eslint-disable-next-line no-undef,import/unambiguous
lockdown({
consoleTaming: 'unsafe',
errorTaming: 'unsafe',
mathTaming: 'unsafe',
dateTaming: 'unsafe',

@ -44,6 +44,10 @@ const copyTargets = [
pattern: `*.html`,
dest: ``,
},
{
src: `./node_modules/globalthis/dist/browser.js`,
dest: `globalthis.js`,
},
{
src: `./node_modules/ses/dist/`,
pattern: `lockdown.cjs`,

@ -81,7 +81,7 @@
"@metamask/eth-ledger-bridge-keyring": "^0.2.6",
"@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^1.4.0",
"@metamask/inpage-provider": "^6.1.0",
"@metamask/inpage-provider": "^8.0.1",
"@metamask/jazzicon": "^2.0.0",
"@metamask/logo": "^2.5.0",
"@popperjs/core": "^2.4.0",
@ -128,6 +128,7 @@
"extensionizer": "^1.0.1",
"fast-json-patch": "^2.0.4",
"fuse.js": "^3.2.0",
"globalthis": "^1.0.1",
"human-standard-token-abi": "^2.0.0",
"json-rpc-engine": "^6.1.0",
"json-rpc-middleware-stream": "^2.1.1",

@ -28,14 +28,16 @@ describe('Segment metrics', function () {
mockSegment: true,
},
async ({ driver, segmentStub }) => {
const threeSegmentEventsReceived = waitUntilCalled(segmentStub, null, 3)
const threeSegmentEventsReceived = waitUntilCalled(segmentStub, null, {
callCount: 3,
})
await driver.navigate()
const passwordField = await driver.findElement(By.css('#password'))
await passwordField.sendKeys('correct horse battery staple')
await passwordField.sendKeys(Key.ENTER)
await threeSegmentEventsReceived
await threeSegmentEventsReceived()
assert.ok(segmentStub.called, 'Segment should receive metrics')

@ -1,22 +1,41 @@
const DEFAULT_TIMEOUT = 10000
/**
* A function that wraps a sinon stubbed function and returns a Promise
* when this stub was called.
* A function that wraps a sinon stub and returns an asynchronous function
* that resolves if the stubbed function was called enough times, or throws
* if the timeout is exceeded.
*
* The stub that has been passed in will be setup to call the wrapped function
* directly, then trigger the returned Promise to resolve.
* directly.
*
* WARNING: Any existing `callsFake` behavior will be overwritten.
*
* @param {import('sinon').stub} stub - A sinon stub of a function
* @param {unknown} [wrappedThis] - The object the stubbed function was called on, if any (i.e. the `this` value)
* @param {number} [callCount] - The number of calls to wait for. Defaults to 1.
* @returns {Promise} A Promise that resolves when the stub has been called
* @param {unknown} [wrappedThis] - The object the stubbed function was called
* on, if any (i.e. the `this` value)
* @param {Object} [options] - Optional configuration
* @param {number} [options.callCount] - The number of calls to wait for.
* @param {number|null} [options.timeout] - The timeout, in milliseconds. Pass
* in `null` to disable the timeout.
* @returns {Function} An asynchronous function that resolves when the stub is
* called enough times, or throws if the timeout is reached.
*/
function waitUntilCalled(stub, wrappedThis = null, callCount = 1) {
function waitUntilCalled(
stub,
wrappedThis = null,
{ callCount = 1, timeout = DEFAULT_TIMEOUT } = {},
) {
let numCalls = 0
let resolve
let timeoutHandle
const stubHasBeenCalled = new Promise((_resolve) => {
resolve = _resolve
if (timeout !== null) {
timeoutHandle = setTimeout(
() => resolve(new Error('Timeout exceeded')),
timeout,
)
}
})
stub.callsFake((...args) => {
try {
@ -27,12 +46,21 @@ function waitUntilCalled(stub, wrappedThis = null, callCount = 1) {
if (numCalls < callCount) {
numCalls += 1
if (numCalls === callCount) {
if (timeoutHandle) {
clearTimeout(timeoutHandle)
}
resolve()
}
}
}
})
return stubHasBeenCalled
return async () => {
const error = await stubHasBeenCalled
if (error) {
throw error
}
}
}
module.exports = waitUntilCalled

@ -85,6 +85,53 @@ describe('DetectTokensController', function () {
sandbox.assert.notCalled(stub)
})
it('should skip adding tokens listed in hiddenTokens array', async function () {
sandbox.useFakeTimers()
network.setProviderType(MAINNET)
const controller = new DetectTokensController({
preferences,
network,
keyringMemStore,
})
controller.isOpen = true
controller.isUnlocked = true
const contractAddresses = Object.keys(contracts)
const erc20ContractAddresses = contractAddresses.filter(
(contractAddress) => contracts[contractAddress].erc20 === true,
)
const existingTokenAddress = erc20ContractAddresses[0]
const existingToken = contracts[existingTokenAddress]
await preferences.addToken(
existingTokenAddress,
existingToken.symbol,
existingToken.decimals,
)
const tokenAddressToSkip = erc20ContractAddresses[1]
sandbox
.stub(controller, '_getTokenBalances')
.callsFake((tokensToDetect) =>
tokensToDetect.map((token) =>
token === tokenAddressToSkip ? new BigNumber(10) : 0,
),
)
await preferences.removeToken(tokenAddressToSkip)
await controller.detectNewTokens()
assert.deepEqual(preferences.store.getState().tokens, [
{
address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals,
symbol: existingToken.symbol,
},
])
})
it('should check and add tokens while on main network', async function () {
sandbox.useFakeTimers()
network.setProviderType(MAINNET)

@ -249,7 +249,7 @@ describe('IncomingTransactionsController', function () {
)
incomingTransactionsController.start()
await updateStateCalled
await updateStateCalled()
const actualState = incomingTransactionsController.store.getState()
const generatedTxId = actualState?.incomingTransactions?.['0xfake']?.id
@ -345,8 +345,8 @@ describe('IncomingTransactionsController', function () {
try {
await Promise.race([
updateStateCalled,
putStateCalled,
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT)
}),
@ -412,8 +412,8 @@ describe('IncomingTransactionsController', function () {
try {
await Promise.race([
updateStateCalled,
putStateCalled,
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT)
}),
@ -475,8 +475,8 @@ describe('IncomingTransactionsController', function () {
try {
await Promise.race([
updateStateCalled,
putStateCalled,
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT)
}),
@ -540,8 +540,8 @@ describe('IncomingTransactionsController', function () {
try {
await Promise.race([
updateStateCalled,
putStateCalled,
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT)
}),
@ -592,7 +592,7 @@ describe('IncomingTransactionsController', function () {
// TODO: stop skipping the first event
await subscription({ selectedAddress: MOCK_SELECTED_ADDRESS })
await subscription({ selectedAddress: NEW_MOCK_SELECTED_ADDRESS })
await updateStateCalled
await updateStateCalled()
const actualState = incomingTransactionsController.store.getState()
const generatedTxId = actualState?.incomingTransactions?.['0xfake']?.id
@ -696,8 +696,8 @@ describe('IncomingTransactionsController', function () {
try {
await Promise.race([
updateStateCalled,
putStateCalled,
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT)
}),
@ -746,7 +746,7 @@ describe('IncomingTransactionsController', function () {
ROPSTEN_CHAIN_ID,
)
await subscription(ROPSTEN)
await updateStateCalled
await updateStateCalled()
const actualState = incomingTransactionsController.store.getState()
const generatedTxId = actualState?.incomingTransactions?.['0xfake']?.id
@ -848,8 +848,8 @@ describe('IncomingTransactionsController', function () {
try {
await Promise.race([
updateStateCalled,
putStateCalled,
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT)
}),

@ -1022,7 +1022,7 @@ describe('MetaMaskController', function () {
}
streamTest.write(
{
name: 'provider',
name: 'metamask-provider',
data: message,
},
null,
@ -1061,7 +1061,7 @@ describe('MetaMaskController', function () {
}
streamTest.write(
{
name: 'provider',
name: 'metamask-provider',
data: message,
},
null,

@ -400,7 +400,7 @@ describe('MetaMetricsController', function () {
},
{ flushImmediately: true },
)
assert.doesNotReject(flushCalled)
assert.doesNotReject(flushCalled())
})
it('should throw if event or category not provided', function () {

@ -32,8 +32,6 @@ const keyringAccounts = deepFreeze([
'0xcc74c7a59194e5d9268476955650d1e285be703c',
])
const getKeyringAccounts = async () => [...keyringAccounts]
const getIdentities = () => {
return keyringAccounts.reduce((identities, address, index) => {
identities[address] = { address, name: `Account ${index}` }
@ -62,8 +60,6 @@ const getRestrictedMethods = (permController) => {
}
}
const getUnlockPromise = () => Promise.resolve()
/**
* Gets default mock constructor options for a permissions controller.
*
@ -71,10 +67,10 @@ const getUnlockPromise = () => Promise.resolve()
*/
export function getPermControllerOpts() {
return {
showPermissionRequest: noop,
getKeyringAccounts,
getUnlockPromise,
getKeyringAccounts: async () => [...keyringAccounts],
getUnlockPromise: () => Promise.resolve(),
getRestrictedMethods,
isUnlocked: () => true,
notifyDomain: noop,
notifyAllDomains: noop,
preferences: {
@ -86,6 +82,7 @@ export function getPermControllerOpts() {
},
subscribe: noop,
},
showPermissionRequest: noop,
}
}
@ -467,7 +464,7 @@ export const getters = deepFreeze({
removedAccounts: () => {
return {
method: NOTIFICATION_NAMES.accountsChanged,
result: [],
params: [],
}
},
@ -480,7 +477,7 @@ export const getters = deepFreeze({
newAccounts: (accounts) => {
return {
method: NOTIFICATION_NAMES.accountsChanged,
result: accounts,
params: accounts,
}
},
},
@ -586,17 +583,17 @@ export const getters = deepFreeze({
},
/**
* Gets a wallet_sendDomainMetadata RPC request object.
* Gets a metamask_sendDomainMetadata RPC request object.
*
* @param {string} origin - The origin of the request
* @param {Object} name - The domainMetadata name
* @param {Array<any>} [args] - Any other data for the request's domainMetadata
* @returns {Object} An RPC request object
*/
wallet_sendDomainMetadata: (origin, name, ...args) => {
metamask_sendDomainMetadata: (origin, name, ...args) => {
return {
origin,
method: 'wallet_sendDomainMetadata',
method: 'metamask_sendDomainMetadata',
domainMetadata: {
...args,
name,

@ -6,13 +6,9 @@ import sinon from 'sinon'
import {
METADATA_STORE_KEY,
METADATA_CACHE_MAX_SIZE,
WALLET_PREFIX,
} from '../../../../../app/scripts/controllers/permissions/enums'
import {
PermissionsController,
addInternalMethodPrefix,
} from '../../../../../app/scripts/controllers/permissions'
import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'
import { getRequestUserApprovalHelper, grantPermissions } from './helpers'
@ -187,7 +183,7 @@ describe('permissions controller', function () {
assert.deepEqual(
notifications[origin],
[NOTIFICATIONS.removedAccounts()],
'origin should have single wallet_accountsChanged:[] notification',
'origin should have single metamask_accountsChanged:[] notification',
)
})
@ -1325,11 +1321,18 @@ describe('permissions controller', function () {
})
it('notifyAccountsChanged records history and sends notification', async function () {
sinon.spy(permController, '_isUnlocked')
permController.notifyAccountsChanged(
DOMAINS.a.origin,
ACCOUNTS.a.permitted,
)
assert.ok(
permController._isUnlocked.calledOnce,
'_isUnlocked should have been called once',
)
assert.ok(
permController.permissionsLog.updateAccountsHistory.calledOnce,
'permissionsLog.updateAccountsHistory should have been called once',
@ -1342,6 +1345,25 @@ describe('permissions controller', function () {
)
})
it('notifyAccountsChanged does nothing if _isUnlocked returns false', async function () {
permController._isUnlocked = sinon.fake.returns(false)
permController.notifyAccountsChanged(
DOMAINS.a.origin,
ACCOUNTS.a.permitted,
)
assert.ok(
permController._isUnlocked.calledOnce,
'_isUnlocked should have been called once',
)
assert.ok(
permController.permissionsLog.updateAccountsHistory.notCalled,
'permissionsLog.updateAccountsHistory should not have been called',
)
})
it('notifyAccountsChanged throws on invalid origin', async function () {
assert.throws(
() => permController.notifyAccountsChanged(4, ACCOUNTS.a.permitted),
@ -1604,11 +1626,5 @@ describe('permissions controller', function () {
'pending approval origins should have expected item',
)
})
it('addInternalMethodPrefix', function () {
const str = 'foo'
const res = addInternalMethodPrefix(str)
assert.equal(res, WALLET_PREFIX + str, 'should prefix correctly')
})
})
})

@ -286,7 +286,7 @@ describe('permissions log', function () {
assert.equal(log.length, 0, 'log should be empty')
const res = { foo: 'bar' }
const req1 = RPC_REQUESTS.wallet_sendDomainMetadata(
const req1 = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
'foobar',
)

@ -808,7 +808,7 @@ describe('permissions middleware', function () {
})
})
describe('wallet_sendDomainMetadata', function () {
describe('metamask_sendDomainMetadata', function () {
let permController, clock
beforeEach(function () {
@ -828,7 +828,10 @@ describe('permissions middleware', function () {
DOMAINS.c.origin,
)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
)
const res = {}
await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
@ -861,7 +864,10 @@ describe('permissions middleware', function () {
extensionId,
)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
)
const res = {}
await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
@ -885,7 +891,10 @@ describe('permissions middleware', function () {
DOMAINS.c.origin,
)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
)
const res = {}
await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
@ -907,7 +916,7 @@ describe('permissions middleware', function () {
DOMAINS.c.origin,
)
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin)
const req = RPC_REQUESTS.metamask_sendDomainMetadata(DOMAINS.c.origin)
delete req.domainMetadata
const res = {}

@ -276,6 +276,7 @@ describe('Transaction Controller', function () {
describe('#addUnapprovedTransaction', function () {
const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d'
const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
let getSelectedAddress, getPermittedAccounts
beforeEach(function () {
@ -295,6 +296,7 @@ describe('Transaction Controller', function () {
it('should add an unapproved transaction and return a valid txMeta', async function () {
const txMeta = await txController.addUnapprovedTransaction({
from: selectedAddress,
to: recipientAddress,
})
assert.ok('id' in txMeta, 'should have a id')
assert.ok('time' in txMeta, 'should have a time stamp')
@ -321,7 +323,10 @@ describe('Transaction Controller', function () {
done()
})
txController
.addUnapprovedTransaction({ from: selectedAddress })
.addUnapprovedTransaction({
from: selectedAddress,
to: recipientAddress,
})
.catch(done)
})

@ -6,21 +6,47 @@ describe('txUtils', function () {
it('does not throw for positive values', function () {
const sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
value: '0x01',
}
txUtils.validateTxParams(sample)
})
it('returns error for negative values', function () {
it('throws for invalid params value', function () {
assert.throws(() => txUtils.validateTxParams(), {
message: 'Invalid transaction params: must be an object.',
})
assert.throws(() => txUtils.validateTxParams(null), {
message: 'Invalid transaction params: must be an object.',
})
assert.throws(() => txUtils.validateTxParams(true), {
message: 'Invalid transaction params: must be an object.',
})
assert.throws(() => txUtils.validateTxParams([]), {
message: 'Invalid transaction params: must be an object.',
})
})
it('throws for missing "to" and "data"', function () {
const sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01',
value: '0x01',
}
try {
txUtils.validateTxParams(sample)
} catch (err) {
assert.ok(err, 'error')
assert.throws(() => txUtils.validateTxParams(sample), {
message:
'Invalid transaction params: must specify "data" for contract deployments, or "to" (and optionally "data") for all other types of transactions.',
})
})
it('throws for negative values', function () {
const sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
value: '-0x01',
}
assert.throws(() => txUtils.validateTxParams(sample), {
message: 'Invalid transaction value "-0x01": not a positive number.',
})
})
})

@ -20,9 +20,9 @@
border-radius: 50%;
background: #bbc0c5;
flex: 0 1 auto;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding-top: 2px;
}
}

@ -3,13 +3,13 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import IconWithFallback from '../icon-with-fallback'
export default function UrlIcon({ url, className, name }) {
export default function UrlIcon({ url, className, name, fallbackClassName }) {
return (
<IconWithFallback
className={classnames('url-icon', className)}
icon={url}
name={name}
fallbackClassName="url-icon__fallback"
fallbackClassName={classnames('url-icon__fallback', fallbackClassName)}
/>
)
}
@ -18,4 +18,5 @@ UrlIcon.propTypes = {
url: PropTypes.string,
className: PropTypes.string,
name: PropTypes.string,
fallbackClassName: PropTypes.string,
}

@ -325,7 +325,6 @@ export {
export const navigateBackToBuildQuote = (history) => {
return async (dispatch) => {
// TODO: Ensure any fetch in progress is cancelled
await dispatch(resetSwapsPostFetchState())
dispatch(navigatedBackToBuildQuote())
history.push(BUILD_QUOTE_ROUTE)

@ -47,6 +47,8 @@ const fuseSearchKeys = [
{ name: 'address', weight: 0.002 },
]
const MAX_ALLOWED_SLIPPAGE = 15
export default function BuildQuote({
inputValue,
onInputChange,
@ -393,6 +395,7 @@ export default function BuildQuote({
onSelect={(newSlippage) => {
setMaxSlippage(newSlippage)
}}
maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE}
/>
</div>
</div>
@ -411,9 +414,11 @@ export default function BuildQuote({
disabled={
!Number(inputValue) ||
!selectedToToken?.address ||
Number(maxSlippage) === 0
Number(maxSlippage) === 0 ||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE
}
hideCancel
showTermsOfService
/>
</div>
)

@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import BigNumber from 'bignumber.js'
import classnames from 'classnames'
import { calcTokenAmount } from '../../../helpers/utils/token-util'
import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util'
import { formatSwapsValueForDisplay } from '../swaps.util'
export default function ExchangeRateDisplay({
primaryTokenValue,
@ -13,6 +13,7 @@ export default function ExchangeRateDisplay({
secondaryTokenDecimals = 18,
secondaryTokenSymbol,
arrowColor = 'black',
boldSymbols = true,
className,
}) {
const [showPrimaryToSecondary, setShowPrimaryToSecondary] = useState(true)
@ -57,16 +58,24 @@ export default function ExchangeRateDisplay({
} else if (new BigNumber(rate, 10).lt('0.000001', 10)) {
rateToDisplay = rate
} else {
rateToDisplay = toPrecisionWithoutTrailingZeros(rate, 9)
rateToDisplay = formatSwapsValueForDisplay(rate)
}
return (
<div className={classnames('exchange-rate-display', className)}>
<span>1</span>
<span className="exchange-rate-display__bold">{baseSymbol}</span>
<span
className={classnames({ 'exchange-rate-display__bold': boldSymbols })}
>
{baseSymbol}
</span>
<span>{comparisonSymbol}</span>
<span>{rateToDisplay}</span>
<span className="exchange-rate-display__bold">{ratiodSymbol}</span>
<span
className={classnames({ 'exchange-rate-display__bold': boldSymbols })}
>
{ratiodSymbol}
</span>
<div
className={classnames('exchange-rate-display__switch-arrows', {
'exchange-rate-display__switch-arrows-rotate': rotating,
@ -115,4 +124,5 @@ ExchangeRateDisplay.propTypes = {
secondaryTokenSymbol: PropTypes.string.isRequired,
className: PropTypes.string,
arrowColor: PropTypes.string,
boldSymbols: PropTypes.bool,
}

@ -11,11 +11,41 @@ export default function FeeCard({
tokenApprovalTextComponent,
tokenApprovalSourceTokenSymbol,
onTokenApprovalClick,
metaMaskFee,
isBestQuote,
numberOfQuotes,
onQuotesClick,
tokenConversionRate,
}) {
const t = useContext(I18nContext)
let bestQuoteText = ''
if (isBestQuote && tokenConversionRate) {
bestQuoteText = t('swapUsingBestQuote')
} else if (tokenConversionRate) {
bestQuoteText = t('swapBetterQuoteAvailable')
}
return (
<div className="fee-card">
<div className="fee-card__savings-and-quotes-header">
<div className="fee-card__savings-and-quotes-row">
{bestQuoteText && (
<p className="fee-card__savings-text">{bestQuoteText}</p>
)}
<div
className="fee-card__quote-link-container"
onClick={onQuotesClick}
>
<p className="fee-card__quote-link-text">
{t('swapNQuotes', [numberOfQuotes])}
</p>
<div className="fee-card__caret-right">
<i className="fa fa-angle-up" />
</div>
</div>
</div>
</div>
<div className="fee-card__main">
<div className="fee-card__row-header">
<div>
@ -83,26 +113,39 @@ export default function FeeCard({
</div>
</div>
{!hideTokenApprovalRow && (
<div className="fee-card__top-bordered-row">
<div className="fee-card__row-header">
<div className="fee-card__row-label">
<div className="fee-card__row-header-text">
{t('swapThisWillAllowApprove', [tokenApprovalTextComponent])}
</div>
<div
className="fee-card__link"
onClick={() => onTokenApprovalClick()}
>
{t('swapEditLimit')}
</div>
<InfoTooltip
position="top"
contentText={t('swapEnableDescription', [
tokenApprovalSourceTokenSymbol,
])}
containerClassName="fee-card__info-tooltip-container"
/>
</div>
<div
className="fee-card__link"
onClick={() => onTokenApprovalClick()}
>
{t('swapEditLimit')}
</div>
</div>
)}
<div className="fee-card__top-bordered-row">
<div className="fee-card__row-label">
<div className="fee-card__row-header-text">
{t('swapQuoteIncludesRate', [metaMaskFee])}
</div>
<InfoTooltip
position="top"
contentText={t('swapMetaMaskFeeDescription', [metaMaskFee])}
wrapperClassName="fee-card__info-tooltip-container"
/>
</div>
</div>
</div>
</div>
)
@ -122,4 +165,9 @@ FeeCard.propTypes = {
tokenApprovalTextComponent: PropTypes.node,
tokenApprovalSourceTokenSymbol: PropTypes.string,
onTokenApprovalClick: PropTypes.func,
metaMaskFee: PropTypes.string.isRequired,
isBestQuote: PropTypes.bool,
onQuotesClick: PropTypes.func.isRequired,
numberOfQuotes: PropTypes.number.isRequired,
tokenConversionRate: PropTypes.number,
}

@ -1,6 +1,6 @@
import React from 'react'
import { action } from '@storybook/addon-actions'
import { text } from '@storybook/addon-knobs'
import { text, boolean, number, object } from '@storybook/addon-knobs'
import FeeCard from './fee-card'
const tokenApprovalTextComponent = (
@ -35,6 +35,13 @@ export const WithAllProps = () => {
tokenApprovalSourceTokenSymbol="ABC"
onTokenApprovalClick={action('Clicked third row link')}
hideTokenApprovalRow={false}
metaMaskFee="0.875"
savings={object('savings 1', { total: '8.55' })}
onQuotesClick={action('Clicked quotes link')}
numberOfQuotes={number('numberOfQuotes', 6)}
isBestQuote={boolean('isBestQuote', true)}
conversionRate={300}
currentCurrency="usd"
/>
</div>
)
@ -55,6 +62,11 @@ export const WithoutThirdRow = () => {
}}
onFeeCardMaxRowClick={action('Clicked max fee row link')}
hideTokenApprovalRow
onQuotesClick={action('Clicked quotes link')}
numberOfQuotes={number('numberOfQuotes', 1)}
isBestQuote={boolean('isBestQuote', true)}
savings={object('savings 1', { total: '8.55' })}
metaMaskFee="0.875"
/>
</div>
)
@ -70,6 +82,9 @@ export const WithOnlyRequiredProps = () => {
}}
onFeeCardMaxRowClick={action('Clicked max fee row link')}
hideTokenApprovalRow
metaMaskFee="0.875"
onQuotesClick={action('Clicked quotes link')}
numberOfQuotes={2}
/>
</div>
)

@ -1,11 +1,69 @@
.fee-card {
border-radius: 8px;
border: 1px solid $Grey-100;
width: 100%;
@include H7;
&__savings-and-quotes-header {
display: flex;
position: relative;
align-items: center;
}
&__savings-and-quotes-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
left: 58px;
height: 39px;
background: $Blue-000;
border: 1px solid $Blue-500;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
border-bottom: 0;
padding-left: 8px;
padding-right: 8px;
}
&__savings-text {
@include H6;
font-weight: bold;
color: $Blue-500;
}
&__quote-link-container {
display: flex;
align-items: center;
cursor: pointer;
}
&__quote-link-text {
@include H7;
color: $Blue-500;
}
&__caret-right {
color: $Blue-500;
width: 6px;
height: 6px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 6px;
i {
transform: rotate(90deg);
}
}
&__main {
border: 1px solid $Blue-500;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
width: 100%;
max-width: 311px;
padding: 16px 16px 12px 16px;
}
@ -31,6 +89,10 @@
cursor: pointer;
}
&__row-header-text--bold {
color: $Black-100;
}
&__row,
&__top-bordered-row {
display: flex;
@ -51,7 +113,6 @@
img {
height: 10px;
width: 10px;
margin-left: 4px;
cursor: pointer;
}
}
@ -60,7 +121,12 @@
height: 10px;
width: 10px;
justify-content: center;
margin-top: 2px;
div {
// Needed to override the style property added by the react-tippy library
display: flex !important;
height: 10px;
}
}
&__info-tooltip-paragraph {
@ -111,11 +177,14 @@
margin-right: 12px;
}
&__row-header-primary,
&__row-header-primary--bold {
&__row-header-primary {
color: $Grey-500;
}
&__row-header-primary--bold {
color: $Black-100;
}
&__row-header-text--bold,
&__row-header-secondary--bold,
&__row-header-primary--bold {
@ -125,6 +194,11 @@
&__bold {
font-weight: bold;
}
&__tilde {
font-family: Roboto, Helvetica, Arial, sans-serif;
margin-right: -3.5px;
}
}
.info-tooltip {

@ -0,0 +1,54 @@
import React from 'react'
export default function PigIcon() {
return (
<svg
width="31"
height="44"
viewBox="0 0 31 44"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.86541 24.7218L1.79469 24.7925C1.02602 25.5611 0.602468 26.5834 0.602936 27.6702L1.86541 24.7218ZM1.86541 24.7218L1.93612 24.7925M1.86541 24.7218L1.93612 24.7925M1.93612 24.7925L2.34305 25.1994L2.74999 25.6063L2.82067 25.677M1.93612 24.7925L2.82067 25.677M2.82067 25.677L2.75002 25.7477M2.82067 25.677L2.75002 25.7477M2.75002 25.7477C2.23672 26.2615 1.95374 26.944 1.95374 27.6702C1.95374 28.3964 2.23672 29.0789 2.75002 29.5927L2.67928 29.6633L2.74995 29.5926C3.16102 30.0032 3.67296 30.2457 4.20842 30.3365M2.75002 25.7477L4.20842 30.3365M4.20842 30.3365V29.3801C4.20842 28.7473 4.29699 28.1236 4.44701 27.5174C5.26558 24.2082 8.15856 21.4247 11.5089 21.0552L11.5089 21.0551L11.5122 21.0549L11.5232 21.054C11.5234 21.054 11.5236 21.054 11.5238 21.054C14.4515 20.7946 17.4246 20.7241 20.3655 20.8456L20.3662 20.8456C20.4027 20.8474 20.4448 20.8518 20.4826 20.8558C20.4848 20.856 20.487 20.8562 20.4891 20.8564C21.3008 19.8016 22.4769 19.1281 23.7918 19.1281H24.3672H24.4672V19.2281V22.4872C25.6604 23.4555 26.6321 24.7942 27.2053 26.3565C27.6693 26.3858 28.1337 26.418 28.5993 26.4518L28.6 26.4519C29.8669 26.5531 30.8947 27.6691 30.8947 28.9423V33.532C30.8947 34.8021 29.8707 35.9275 28.6044 36.0438L28.6037 36.0439C28.0461 36.0912 27.4876 36.1351 26.9282 36.1755C26.2881 37.557 25.3323 38.7259 24.2059 39.5696V40.9581C24.2059 42.0522 23.3155 42.9421 22.2215 42.9421H21.2956C20.2321 42.9421 19.3615 42.1011 19.3137 41.0491C18.732 41.0644 18.149 41.0745 17.5651 41.0745C16.1032 41.0745 14.6374 41.0272 13.1767 40.9337V40.9581C13.1767 42.0522 12.2863 42.9421 11.1922 42.9421H10.2664C9.17234 42.9421 8.28244 42.0522 8.28244 40.9581V39.6399C5.89461 38.1267 4.20795 35.3776 4.20842 32.4825V31.7103C3.32631 31.6097 2.47056 31.2229 1.79516 30.5475C1.02648 29.7792 0.602947 28.757 0.602936 27.6702L4.20842 30.3365ZM11.5108 40.8072L11.5108 40.8072L11.5084 40.8069C10.8614 40.7357 10.2331 40.5697 9.63278 40.3321V40.9581C9.63278 41.307 9.91701 41.5913 10.2659 41.5913H11.1918C11.5412 41.5913 11.8254 41.307 11.8254 40.9581V40.8312C11.7948 40.8289 11.7636 40.8266 11.7324 40.8243C11.6578 40.8189 11.5824 40.8133 11.5108 40.8072ZM26.0661 27.2019C25.7283 26.1136 25.1685 25.1424 24.4672 24.3492V25.3928V25.4928H24.3672H23.2164H23.1164V25.3928V20.5711C21.7447 20.9526 20.6961 22.4798 20.6961 24.3256C20.6961 24.6853 20.7363 25.0339 20.8135 25.3705L20.8416 25.4928H20.716H19.5442H19.4607L19.4458 25.4107C19.3819 25.0577 19.3458 24.6961 19.3458 24.3256C19.3458 23.5585 19.4925 22.8324 19.7487 22.1769C19.0205 22.1527 18.291 22.138 17.5604 22.138C15.5864 22.138 13.6074 22.2251 11.6502 22.3983C10.0333 22.5788 8.51004 23.482 7.38991 24.765C6.26962 26.0482 5.55899 27.704 5.55923 29.3796V32.4825V32.5581C5.58001 34.2087 6.28668 35.835 7.38969 37.0991C8.5094 38.3822 10.031 39.2847 11.6432 39.4628C14.5132 39.7172 17.428 39.7865 20.3054 39.6673C22.5772 39.5684 24.8105 37.8103 25.8469 35.2745L25.8469 35.2744L25.9827 34.9431L26.0063 34.8856L26.0684 34.8813L26.4258 34.8565L26.4258 34.8565C27.1144 34.809 27.8016 34.7558 28.4842 34.698C29.0557 34.6452 29.5434 34.1087 29.5434 33.5316V28.9419C29.5434 28.3691 29.0595 27.8434 28.4958 27.7984C27.8886 27.7541 27.2797 27.713 26.6703 27.6762L26.6702 27.6762L26.2735 27.6519L26.2046 27.6477L26.1842 27.5818L26.0661 27.2019L26.0661 27.2019ZM22.8546 40.9581V40.3811C22.1527 40.7107 21.4138 40.9209 20.6629 40.9915C20.6803 41.325 20.9574 41.5913 21.2952 41.5913H22.221C22.5705 41.5913 22.8546 41.307 22.8546 40.9581Z"
fill="#037DD6"
stroke="#037DD6"
strokeWidth="0.2"
/>
<path
d="M15.4018 19.837C12.3221 19.837 9.81661 17.332 9.81661 14.2523C9.81661 11.1726 12.3217 8.66703 15.4018 8.66703C18.4811 8.66703 20.9866 11.1721 20.9866 14.2523C20.9866 17.3315 18.4816 19.837 15.4018 19.837ZM15.4018 10.0174C13.0673 10.0174 11.1674 11.9172 11.1674 14.2518C11.1674 16.5864 13.0673 18.4862 15.4018 18.4862C17.7364 18.4862 19.6358 16.5868 19.6358 14.2518C19.6358 11.9172 17.7369 10.0174 15.4018 10.0174Z"
fill="#037DD6"
stroke="#037DD6"
strokeWidth="0.2"
/>
<path
d="M15.4022 17.8575C13.4136 17.8575 11.7957 16.24 11.7957 14.2514C11.7957 12.2627 13.4136 10.6453 15.4022 10.6453C17.3909 10.6453 19.0083 12.2632 19.0083 14.2514C19.0083 16.2395 17.3909 17.8575 15.4022 17.8575ZM15.4022 11.9961C14.1583 11.9961 13.1465 13.0079 13.1465 14.2519C13.1465 15.4958 14.1582 16.5071 15.4022 16.5071C16.6458 16.5071 17.6575 15.4954 17.6575 14.2519C17.6575 13.0083 16.6457 11.9961 15.4022 11.9961Z"
fill="#037DD6"
stroke="#037DD6"
strokeWidth="0.2"
/>
<path
d="M14.6099 0.842163V0.742163H14.7099H15.8607H15.9607V0.842163V7.35593V7.45593H15.8607H14.7099H14.6099V7.35593V0.842163Z"
fill="#037DD6"
stroke="#037DD6"
strokeWidth="0.2"
/>
<path
d="M17.2175 4.59747V4.49747H17.3175H18.4683H18.5683V4.59747V8.34996V8.44996H18.4683H17.3175H17.2175V8.34996V4.59747Z"
fill="#037DD6"
stroke="#037DD6"
strokeWidth="0.2"
/>
<path
d="M24.4364 28.7993L24.4362 28.7993C24.3635 28.7716 24.2949 28.7581 24.2283 28.7581C23.9048 28.7581 23.6419 29.0179 23.6368 29.3397L23.6378 29.3474L23.6397 29.3614C23.6411 29.372 23.6431 29.3873 23.6447 29.4046L23.6448 29.4054C23.6735 29.7092 23.925 29.9417 24.2283 29.9417C24.5551 29.9417 24.8198 29.6769 24.8198 29.3501C24.8198 29.1005 24.6631 28.8854 24.4364 28.7993ZM23.6367 29.3501C23.6367 29.35 23.6367 29.3499 23.6367 29.3497V29.3501Z"
fill="#037DD6"
stroke="#037DD6"
/>
<path
d="M17.967 23.5994H12.8346C12.61 23.5994 12.4253 23.7836 12.4251 24.0094V24.1723C12.4251 24.3974 12.6091 24.5814 12.8342 24.5814H17.967C18.1924 24.5814 18.3766 24.397 18.3766 24.1723V24.009C18.3766 23.7836 18.1923 23.5994 17.967 23.5994Z"
fill="#037DD6"
stroke="#037DD6"
/>
</svg>
)
}

@ -71,7 +71,7 @@
font-weight: bold;
color: $Black-100;
margin-top: 3px;
margin-top: -5px;
}
&__header {

@ -1,28 +1,75 @@
.main-quote-summary {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
position: relative;
height: 196px;
max-height: 196px;
min-height: 196px;
width: 100%;
color: $white;
color: $Black-100;
&__quote-backdrop-with-top-tab,
&__quote-backdrop {
position: absolute;
box-shadow: 0 10px 39px rgba(3, 125, 214, 0.15);
border-radius: 8px;
background: #fafcff;
&__source-row,
&__destination-row {
width: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
@include H6;
color: $Grey-500;
}
&__source-row {
align-items: center;
}
&__quote-backdrop-with-top-tab {
width: 348px;
height: 215px;
&__source-row-value,
&__source-row-symbol {
// Each of these spans can be half their container width minus the space
// needed for the token icon and the span margins
max-width: calc(50% - 13px);
}
&__quote-backdrop {
width: 310px;
height: 164px;
&__source-row-value {
margin-right: 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__source-row-symbol {
margin-left: 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__destination-row {
margin-top: 6px;
}
&__destination-row-symbol {
margin-left: 5px;
color: $Black-100;
}
&__icon,
&__icon-fallback {
height: 16px;
width: 16px;
}
&__icon-fallback {
padding-top: 0;
font-size: 12px;
line-height: 16px;
}
&__down-arrow {
margin-top: 5px;
}
&__details {
@ -33,62 +80,24 @@
position: relative;
}
&__best-quote {
@include H7;
font-weight: bold;
position: relative;
display: flex;
padding-top: 6px;
letter-spacing: 0.12px;
min-height: 16px;
> span {
margin-left: 4px;
}
}
&__quote-details-top {
height: 94px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
width: 100%;
padding: 12px;
padding-top: 2px;
margin-top: 4px;
}
&__bold {
font-weight: 900;
}
&__quote-small-white {
white-space: nowrap;
width: 100%;
text-align: center;
font-size: 14px;
margin-bottom: 8px;
margin-top: 6px;
}
&__quote-large {
display: flex;
align-items: flex-end;
align-items: flex-start;
margin-top: 8px;
height: 50px;
}
&__quote-large-number {
font-size: 40px;
line-height: 32px;
margin-right: 6px;
}
&__quote-large-symbol {
display: flex;
align-items: flex-end;
font-size: 32px;
line-height: 32px;
font-size: 60px;
line-height: 48px;
}
&__quote-large-white {
@ -104,7 +113,10 @@
justify-content: center;
align-items: center;
width: 287px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
height: 42px;
margin-top: 14px;
}
&__exchange-rate-display {
color: $Grey-500;
}
}

@ -1,62 +1,33 @@
import React, { useContext } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import BigNumber from 'bignumber.js'
import classnames from 'classnames'
import { I18nContext } from '../../../contexts/i18n'
import { calcTokenAmount } from '../../../helpers/utils/token-util'
import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util'
import Tooltip from '../../../components/ui/tooltip'
import SunCheckIcon from '../../../components/ui/icon/sun-check-icon.component'
import UrlIcon from '../../../components/ui/url-icon'
import ExchangeRateDisplay from '../exchange-rate-display'
import { formatSwapsValueForDisplay } from '../swaps.util'
import QuoteBackdrop from './quote-backdrop'
function getFontSizes(fontSizeScore) {
if (fontSizeScore <= 11) {
return [40, 32]
}
if (fontSizeScore <= 16) {
return [30, 24]
}
return [24, 14]
}
function getLineHeight(fontSizeScore) {
if (fontSizeScore <= 11) {
return 32
function getFontSizesAndLineHeights(fontSizeScore) {
if (fontSizeScore <= 9) {
return [60, 48]
}
if (fontSizeScore <= 16) {
return 26
if (fontSizeScore <= 13) {
return [40, 32]
}
return 18
}
// Returns a numerical value based on the length of the two passed strings: amount and symbol.
// The returned value equals the number of digits in the amount string plus a value calculated
// from the length of the symbol string. The returned number will be passed to the getFontSizes function
// to determine the font size to apply to the amount and symbol strings when rendered. The
// desired maximum digits and letters to show in the ultimately rendered string is 20, and in
// such cases there can also be ellipsis shown and a decimal, combinding for a rendered "string"
// length of ~22. As the symbol will always have a smaller font size than the amount, the
// additive value of the symbol length to the font size score is corrected based on the total
// number of alphanumeric characters in both strings and the desired rendered length of 22.
function getFontSizeScore(amount, symbol) {
const amountLength = amount.match(/\d+/gu).join('').length
const symbolModifier = Math.min((amountLength + symbol.length) / 22, 1)
return amountLength + symbol.length * symbolModifier
return [26, 15]
}
export default function MainQuoteSummary({
isBestQuote,
sourceValue,
sourceSymbol,
sourceDecimals,
sourceIconUrl,
destinationValue,
destinationSymbol,
destinationDecimals,
destinationIconUrl,
}) {
const t = useContext(I18nContext)
const sourceAmount = toPrecisionWithoutTrailingZeros(
calcTokenAmount(sourceValue, sourceDecimals).toString(10),
12,
@ -67,43 +38,55 @@ export default function MainQuoteSummary({
)
const amountToDisplay = formatSwapsValueForDisplay(destinationAmount)
const fontSizeScore = getFontSizeScore(amountToDisplay, destinationSymbol)
const [numberFontSize, symbolFontSize] = getFontSizes(fontSizeScore)
const lineHeight = getLineHeight(fontSizeScore)
const amountDigitLength = amountToDisplay.match(/\d+/gu).join('').length
const [numberFontSize, lineHeight] = getFontSizesAndLineHeights(
amountDigitLength,
)
let ellipsedAmountToDisplay = amountToDisplay
if (fontSizeScore > 20) {
ellipsedAmountToDisplay = `${amountToDisplay.slice(
0,
amountToDisplay.length - (fontSizeScore - 20),
)}...`
if (amountDigitLength > 20) {
ellipsedAmountToDisplay = `${amountToDisplay.slice(0, 20)}...`
}
return (
<div className="main-quote-summary">
<div
className={classnames('main-quote-summary__quote-backdrop', {
'main-quote-summary__quote-backdrop-with-top-tab': isBestQuote,
})}
>
<QuoteBackdrop withTopTab={isBestQuote} />
</div>
<div className="main-quote-summary__best-quote">
{isBestQuote && <SunCheckIcon />}
<span>{isBestQuote && t('swapsBestQuote')}</span>
</div>
<div className="main-quote-summary__details">
<div className="main-quote-summary__quote-details-top">
<span className="main-quote-summary__quote-small-white">
{t('swapsConvertToAbout', [
<span
className="main-quote-summary__bold"
key="main-quote-summary-bold-1"
>
{`${sourceAmount} ${sourceSymbol}`}
</span>,
])}
</span>
<div className="main-quote-summary__source-row">
<span
className="main-quote-summary__source-row-value"
title={formatSwapsValueForDisplay(sourceAmount)}
>
{formatSwapsValueForDisplay(sourceAmount)}
</span>
<UrlIcon
url={sourceIconUrl}
className="main-quote-summary__icon"
name={sourceSymbol}
fallbackClassName="main-quote-summary__icon-fallback"
/>
<span
className="main-quote-summary__source-row-symbol"
title={sourceSymbol}
>
{sourceSymbol}
</span>
</div>
<img
className="main-quote-summary__down-arrow"
src="images/down-arrow-grey.svg"
/>
<div className="main-quote-summary__destination-row">
<UrlIcon
url={destinationIconUrl}
className="main-quote-summary__icon"
name={destinationSymbol}
fallbackClassName="main-quote-summary__icon-fallback"
/>
<span className="main-quote-summary__destination-row-symbol">
{destinationSymbol}
</span>
</div>
<div className="main-quote-summary__quote-large">
<Tooltip
interactive
@ -122,15 +105,6 @@ export default function MainQuoteSummary({
{`${ellipsedAmountToDisplay}`}
</span>
</Tooltip>
<span
className="main-quote-summary__quote-large-symbol"
style={{
fontSize: symbolFontSize,
lineHeight: `${lineHeight}px`,
}}
>
{`${destinationSymbol}`}
</span>
</div>
</div>
<div className="main-quote-summary__exchange-rate-container">
@ -141,8 +115,9 @@ export default function MainQuoteSummary({
secondaryTokenValue={destinationValue}
secondaryTokenDecimals={destinationDecimals}
secondaryTokenSymbol={destinationSymbol}
className="exchange-rate-display--white"
arrowColor="white"
arrowColor="#037DD6"
boldSymbols={false}
className="main-quote-summary__exchange-rate-display"
/>
</div>
</div>
@ -151,7 +126,6 @@ export default function MainQuoteSummary({
}
MainQuoteSummary.propTypes = {
isBestQuote: PropTypes.bool,
sourceValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(BigNumber),
@ -167,4 +141,6 @@ MainQuoteSummary.propTypes = {
PropTypes.number,
]),
destinationSymbol: PropTypes.string.isRequired,
sourceIconUrl: PropTypes.string,
destinationIconUrl: PropTypes.string,
}

@ -1,5 +1,5 @@
import React from 'react'
import { text, number, boolean } from '@storybook/addon-knobs'
import { text, number } from '@storybook/addon-knobs'
import MainQuoteSummary from './main-quote-summary'
export default {
@ -8,28 +8,24 @@ export default {
export const BestQuote = () => {
return (
<MainQuoteSummary
sourceValue={text('sourceValue', '2000000000000000000')}
sourceDecimals={number('sourceDecimals', 18)}
sourceSymbol={text('sourceSymbol', 'ETH')}
destinationValue={text('destinationValue', '200000000000000000')}
destinationDecimals={number('destinationDecimals', 18)}
destinationSymbol={text('destinationSymbol', 'ABC')}
isBestQuote={boolean('isBestQuote', true)}
/>
)
}
export const NotBestQuote = () => {
return (
<MainQuoteSummary
sourceValue={text('sourceValue', '2000000000000000000')}
sourceDecimals={number('sourceDecimals', 18)}
sourceSymbol={text('sourceSymbol', 'ETH')}
destinationValue={text('destinationValue', '200000000000000000')}
destinationDecimals={number('destinationDecimals', 18)}
destinationSymbol={text('destinationSymbol', 'ABC')}
isBestQuote={boolean('isBestQuote', false)}
/>
<div
style={{
width: '360px',
height: '224px',
border: '1px solid black',
padding: '24px',
}}
>
<MainQuoteSummary
sourceValue={text('sourceValue', '2000000000000000000')}
sourceDecimals={number('sourceDecimals', 18)}
sourceSymbol={text('sourceSymbol', 'ETH')}
destinationValue={text('destinationValue', '200000000000000000')}
destinationDecimals={number('destinationDecimals', 18)}
destinationSymbol={text('destinationSymbol', 'ABC')}
sourceIconUrl=".storybook/images/metamark.svg"
destinationIconUrl=".storybook/images/sai.svg"
/>
</div>
)
}

@ -6,7 +6,7 @@ import ButtonGroup from '../../../components/ui/button-group'
import Button from '../../../components/ui/button'
import InfoTooltip from '../../../components/ui/info-tooltip'
export default function SlippageButtons({ onSelect }) {
export default function SlippageButtons({ onSelect, maxAllowedSlippage }) {
const t = useContext(I18nContext)
const [open, setOpen] = useState(false)
const [customValue, setCustomValue] = useState('')
@ -15,12 +15,19 @@ export default function SlippageButtons({ onSelect }) {
const [inputRef, setInputRef] = useState(null)
let errorText = ''
if (customValue && Number(customValue) <= 0) {
errorText = t('swapSlippageTooLow')
} else if (customValue && Number(customValue) < 0.5) {
errorText = t('swapLowSlippageError')
} else if (customValue && Number(customValue) > 5) {
errorText = t('swapHighSlippageWarning')
if (customValue) {
if (Number(customValue) <= 0) {
errorText = t('swapSlippageTooLow')
} else if (Number(customValue) < 0.5) {
errorText = t('swapLowSlippageError')
} else if (
Number(customValue) >= 5 &&
Number(customValue) <= maxAllowedSlippage
) {
errorText = t('swapHighSlippageWarning')
} else if (Number(customValue) > maxAllowedSlippage) {
errorText = t('swapsExcessiveSlippageWarning')
}
}
const customValueText = customValue || t('swapCustom')
@ -151,4 +158,5 @@ export default function SlippageButtons({ onSelect }) {
SlippageButtons.propTypes = {
onSelect: PropTypes.func.isRequired,
maxAllowedSlippage: PropTypes.number.isRequired,
}

@ -31,7 +31,10 @@ export default function SwapsFooter({
onSubmit={onSubmit}
submitText={submitText}
submitButtonType="confirm"
footerClassName="swaps-footer__custom-page-container-footer-class"
footerClassName={classnames(
'swaps-footer__custom-page-container-footer-class',
className,
)}
footerButtonClassName={classnames(
'swaps-footer__custom-page-container-footer-button-class',
{

@ -35,7 +35,7 @@ export default class GasModalPageContainer extends Component {
disableSave: PropTypes.bool,
customGasLimitMessage: PropTypes.string,
customTotalSupplement: PropTypes.string,
usdConversionRate: PropTypes.string,
usdConversionRate: PropTypes.number,
customGasPrice: PropTypes.string,
customGasLimit: PropTypes.string,
setSwapsCustomizationModalPrice: PropTypes.func,

@ -507,6 +507,7 @@ export function quotesToRenderableData(
destinationTokenDecimals: destinationTokenInfo.decimals,
destinationTokenSymbol: destinationTokenInfo.symbol,
destinationTokenValue: formatSwapsValueForDisplay(destinationValue),
destinationIconUrl: destinationTokenInfo.iconUrl,
isBestQuote: quote.isBestQuote,
liquiditySourceKey,
feeInEth,
@ -518,6 +519,7 @@ export function quotesToRenderableData(
sourceTokenDecimals: sourceTokenInfo.decimals,
sourceTokenSymbol: sourceTokenInfo.symbol,
sourceTokenValue: sourceValue,
sourceTokenIconUrl: sourceTokenInfo.iconUrl,
ethValueOfTrade,
minimumAmountReceived,
metaMaskFee: fee,

@ -44,13 +44,13 @@
display: flex;
align-items: center;
justify-content: center;
min-height: 46px;
}
&__view-other-button,
&__view-other-button-fade {
display: flex;
align-items: center;
margin-bottom: 16px;
position: absolute;
@include H7;
@ -133,6 +133,9 @@
width: 100%;
align-items: center;
justify-content: center;
width: intrinsic; /* Safari/WebKit uses a non-standard name */
width: max-content;
max-width: 340px;
margin-top: 8px;
@media screen and (min-width: 576px) {
@ -149,57 +152,29 @@
}
&__countdown-timer-container {
@media screen and (max-width: 576px) {
margin-top: 12px;
margin-bottom: 16px;
&--thin {
margin-top: 8px;
margin-bottom: 8px;
> div {
margin-top: 0;
margin-bottom: 0;
}
}
}
@media screen and (min-width: 576px) {
&--thin {
margin-top: 6px;
}
}
width: 152px;
min-height: 32px;
display: flex;
justify-content: center;
border-radius: 42px;
background: #f2f3f4;
margin-top: 8px;
}
&__fee-card-container {
display: flex;
align-items: center;
width: 100%;
margin-top: 8px;
max-width: 311px;
margin-bottom: 8px;
@media screen and (min-width: 576px) {
margin-bottom: 0;
&--three-rows {
margin-bottom: -16px;
}
}
}
&__main-quote-summary-container {
margin-top: 24px;
@media screen and (max-width: 576px) {
margin-top: 0;
}
&--thin {
margin-top: 8px;
}
}
&__metamask-rate {
display: flex;
margin-top: 8%;
}
&__metamask-rate-text {
@ -214,5 +189,9 @@
&__thin-swaps-footer {
max-height: 82px;
@media screen and (min-width: 576px) {
height: 72px;
}
}
}

@ -73,7 +73,6 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker'
import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps'
import CountdownTimer from '../countdown-timer'
import SwapsFooter from '../swaps-footer'
import InfoTooltip from '../../../components/ui/info-tooltip'
import ViewQuotePriceDifference from './view-quote-price-difference'
export default function ViewQuote() {
@ -117,6 +116,7 @@ export default function ViewQuote() {
const tradeValue = usedQuote?.trade?.value ?? '0x0'
const { isBestQuote } = usedQuote
const fetchParamsSourceToken = fetchParams?.sourceToken
const usedGasLimit =
@ -191,9 +191,11 @@ export default function ViewQuote() {
destinationTokenDecimals,
destinationTokenSymbol,
destinationTokenValue,
destinationIconUrl,
sourceTokenDecimals,
sourceTokenSymbol,
sourceTokenValue,
sourceTokenIconUrl,
} = renderableDataForUsedQuote
const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote(
@ -499,11 +501,7 @@ export default function ViewQuote() {
/>
)}
</div>
<div
className={classnames('view-quote__countdown-timer-container', {
'view-quote__countdown-timer-container--thin': isShowingWarning,
})}
>
<div className="view-quote__countdown-timer-container">
<CountdownTimer
timeStarted={quotesLastFetched}
warningTime="0:30"
@ -511,53 +509,21 @@ export default function ViewQuote() {
labelKey="swapNewQuoteIn"
/>
</div>
<div
className={classnames('view-quote__main-quote-summary-container', {
'view-quote__main-quote-summary-container--thin': isShowingWarning,
})}
>
<MainQuoteSummary
sourceValue={calcTokenValue(sourceTokenValue, sourceTokenDecimals)}
sourceDecimals={sourceTokenDecimals}
sourceSymbol={sourceTokenSymbol}
destinationValue={calcTokenValue(
destinationTokenValue,
destinationTokenDecimals,
)}
destinationDecimals={destinationTokenDecimals}
destinationSymbol={destinationTokenSymbol}
isBestQuote={isBestQuote}
/>
</div>
<div className="view-quote__view-other-button-container">
<div className="view-quote__view-other-button">
{t('swapNQuotesAvailable', [Object.values(quotes).length])}
<i className="fa fa-arrow-right" />
</div>
<div
className="view-quote__view-other-button-fade"
onClick={() => {
allAvailableQuotesOpened()
setSelectQuotePopoverShown(true)
}}
>
{t('swapNQuotesAvailable', [Object.values(quotes).length])}
<i className="fa fa-arrow-right" />
</div>
</div>
<div className="view-quote__metamask-rate">
<p className="view-quote__metamask-rate-text">
{t('swapQuoteIncludesRate', [metaMaskFee])}
</p>
<InfoTooltip
position="top"
contentText={t('swapMetaMaskFeeDescription', [metaMaskFee])}
wrapperClassName="view-quote__metamask-rate-info-icon"
/>
</div>
<MainQuoteSummary
sourceValue={calcTokenValue(sourceTokenValue, sourceTokenDecimals)}
sourceDecimals={sourceTokenDecimals}
sourceSymbol={sourceTokenSymbol}
destinationValue={calcTokenValue(
destinationTokenValue,
destinationTokenDecimals,
)}
destinationDecimals={destinationTokenDecimals}
destinationSymbol={destinationTokenSymbol}
sourceIconUrl={sourceTokenIconUrl}
destinationIconUrl={destinationIconUrl}
/>
<div
className={classnames('view-quote__fee-card-container', {
'view-quote__fee-card-container--thin': isShowingWarning,
'view-quote__fee-card-container--three-rows':
approveTxParams && (!balanceError || warningHidden),
})}
@ -578,6 +544,18 @@ export default function ViewQuote() {
tokenApprovalTextComponent={tokenApprovalTextComponent}
tokenApprovalSourceTokenSymbol={sourceTokenSymbol}
onTokenApprovalClick={onFeeCardTokenApprovalClick}
metaMaskFee={metaMaskFee}
isBestQuote={isBestQuote}
numberOfQuotes={Object.values(quotes).length}
onQuotesClick={() => {
allAvailableQuotesOpened()
setSelectQuotePopoverShown(true)
}}
tokenConversionRate={
destinationTokenSymbol === 'ETH'
? 1
: memoizedTokenConversionRates[destinationToken.address]
}
/>
</div>
</div>
@ -595,7 +573,6 @@ export default function ViewQuote() {
onCancel={async () => await dispatch(navigateBackToBuildQuote(history))}
disabled={balanceError || gasPrice === null || gasPrice === undefined}
className={isShowingWarning && 'view-quote__thin-swaps-footer'}
showTermsOfService
showTopBorder
/>
</div>

@ -2115,20 +2115,19 @@
resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.1.0.tgz#13829d8244bbf19ea658c0b20d21a77b67de0bdd"
integrity sha512-Hggj4y0QIjDzKGTXzarhEPIQyFSB2bi2y6YLJNwaT4JmP30UB5Cj6gqoY0M4pj3QT57fzp0BUuGp7F/AUe28tw==
"@metamask/inpage-provider@^6.1.0":
version "6.3.0"
resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-6.3.0.tgz#92d965e20912c24adbf973efbd07dbf339547741"
integrity sha512-n7E06+8hWdYKmgJo84WFvgX6/BSqaOQEOMIrcbrP48LdkkZNEAChx6D8oUb2lYDQiWgahR+f20jsJoN4WmOjxw==
"@metamask/inpage-provider@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-8.0.1.tgz#67b1f0733ae7c0e0e429dc5c067ba9d2dd6d66da"
integrity sha512-dN3IpiJtaHeiPzF01UXnrQ6TxXbXbkU54kiOHuIUe9e8s7vyPzgDgN2nj84xjmIkqxL0MKY90Wcp0obFKnNj+Q==
dependencies:
eth-rpc-errors "^2.1.1"
"@metamask/safe-event-emitter" "^2.0.0"
eth-rpc-errors "^4.0.2"
fast-deep-equal "^2.0.1"
is-stream "^2.0.0"
json-rpc-engine "^5.2.0"
json-rpc-engine "^6.1.0"
json-rpc-middleware-stream "^2.1.1"
obj-multiplex "^1.0.0"
obs-store "^4.0.3"
pump "^3.0.0"
safe-event-emitter "^1.0.1"
"@metamask/jazzicon@^2.0.0":
version "2.0.0"
@ -9889,13 +9888,6 @@ eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2:
json-rpc-random-id "^1.0.0"
xtend "^4.0.1"
eth-rpc-errors@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-2.1.1.tgz#00a7d6c8a9c864a8ab7d0356be20964e5bee4b13"
integrity sha512-MY3zAa5ZF8hvgQu1HOF9agaK5GgigBRGpTJ8H0oVlE0NqMu13CW6syyjLXdeIDCGQTbUeHliU1z9dVmvMKx1Tg==
dependencies:
fast-safe-stringify "^2.0.6"
eth-rpc-errors@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10"
@ -12935,9 +12927,9 @@ hi-base32@~0.5.0:
integrity sha512-DDRmxSyoYuvjUb9EnXdoiMChBZ7ZcUVJsK5Frd3kqMhuBxvmZdnBeynAVfj7/ECbn++CekcoprvC/rprHPAtow==
highlight.js@^10.1.1, highlight.js@~10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.0.tgz#ef3ce475e5dfa7a48484260b49ea242ddab823a0"
integrity sha512-EfrUGcQ63oLJbj0J0RI9ebX6TAITbsDBLbsjr881L/X5fMO9+oadKzEF21C7R3ULKG6Gv3uoab2HiqVJa/4+oA==
version "10.4.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
history@^4.9.0:
version "4.10.1"
@ -15221,7 +15213,7 @@ json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0:
promise-to-callback "^1.0.0"
safe-event-emitter "^1.0.1"
json-rpc-engine@^5.2.0, json-rpc-engine@^5.3.0:
json-rpc-engine@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.3.0.tgz#7dc7291766b28766ebda33eb6d3f4c6301c44ff4"
integrity sha512-+diJ9s8rxB+fbJhT7ZEf8r8spaLRignLd8jTgQ/h5JSGppAHGtNMZtCoabipCaleR1B3GTGxbXBOqhaJSGmPGQ==

Loading…
Cancel
Save