From baea1b57fb064a9e7a04a6b0e8b54501d61b5cc6 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 3 Dec 2020 18:15:59 -0800 Subject: [PATCH 01/60] Update transaction params validation (#9992) * Update transaction params validation * fixup! Update transaction params validation * Update to/data error message * fixup! Update to/data error message --- app/scripts/controllers/transactions/index.js | 4 +- .../controllers/transactions/lib/util.js | 30 +++++++++++---- .../transactions/tx-controller-test.js | 7 +++- .../controllers/transactions/tx-utils-test.js | 38 ++++++++++++++++--- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 0c6f3c6ac..4a4488fc1 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -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 } diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index 758d70121..b33d3029f 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -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 } diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index f3426d5f9..0e39f791d 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -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) }) diff --git a/test/unit/app/controllers/transactions/tx-utils-test.js b/test/unit/app/controllers/transactions/tx-utils-test.js index 02c5171c7..77add3f62 100644 --- a/test/unit/app/controllers/transactions/tx-utils-test.js +++ b/test/unit/app/controllers/transactions/tx-utils-test.js @@ -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.', + }) }) }) From b7033196d28fd7d75688ddcc103549e923769e64 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 4 Dec 2020 13:47:57 -0330 Subject: [PATCH 02/60] Add timeout to `wait-until-called` (#9996) The `waitUntilCalled` utility now has a timeout. It will now throw an error if the stub is not called enough times, rather than blocking forever. The return type had to be changed to a function, so that we could throw when the timeout is triggered. I tried returning an error that rejected first, but if you don't handle the error synchronously Node.js will consider it to be an unhandled Promise rejected (even if it _is_ handled later on). I worked around this by resolving in the timeout case as well, so that there is never a "deferred" Promise exception in the timeout case. The returned function re-throws the error if it's given. That way there is never any unhandled Promise rejection. --- test/e2e/metrics.spec.js | 6 ++- test/lib/wait-until-called.js | 44 +++++++++++++++---- .../controllers/incoming-transactions-test.js | 30 ++++++------- test/unit/app/controllers/metametrics-test.js | 2 +- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/test/e2e/metrics.spec.js b/test/e2e/metrics.spec.js index 585a44e35..74c8f26d8 100644 --- a/test/e2e/metrics.spec.js +++ b/test/e2e/metrics.spec.js @@ -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') diff --git a/test/lib/wait-until-called.js b/test/lib/wait-until-called.js index 752ee7675..8252f6049 100644 --- a/test/lib/wait-until-called.js +++ b/test/lib/wait-until-called.js @@ -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 diff --git a/test/unit/app/controllers/incoming-transactions-test.js b/test/unit/app/controllers/incoming-transactions-test.js index 8401d7f24..61768cee8 100644 --- a/test/unit/app/controllers/incoming-transactions-test.js +++ b/test/unit/app/controllers/incoming-transactions-test.js @@ -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) }), diff --git a/test/unit/app/controllers/metametrics-test.js b/test/unit/app/controllers/metametrics-test.js index d43f48f4f..4e66fec4a 100644 --- a/test/unit/app/controllers/metametrics-test.js +++ b/test/unit/app/controllers/metametrics-test.js @@ -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 () { From e8cb565b48a6c84e30bd435cd490ea71a87dcb69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Dec 2020 14:38:30 -0800 Subject: [PATCH 03/60] Bump highlight.js from 10.4.0 to 10.4.1 (#10004) Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 10.4.0 to 10.4.1. - [Release notes](https://github.com/highlightjs/highlight.js/releases) - [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md) - [Commits](https://github.com/highlightjs/highlight.js/compare/10.4.0...10.4.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f328d7c05..92a25c7bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12935,9 +12935,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" From 56f80ae9a9abdcf517f969109e0dcc5fc064dd2d Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 7 Dec 2020 09:12:55 -0600 Subject: [PATCH 04/60] Fix 9988 - Don't allow more than 15% slippage (#9991) --- app/_locales/en/messages.json | 7 ++++-- ui/app/pages/swaps/build-quote/build-quote.js | 6 ++++- .../slippage-buttons/slippage-buttons.js | 22 +++++++++++++------ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 16e21905a..c0a0f82cf 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1737,7 +1737,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", @@ -1853,8 +1853,11 @@ "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", diff --git a/ui/app/pages/swaps/build-quote/build-quote.js b/ui/app/pages/swaps/build-quote/build-quote.js index 3aabcf82e..13d873b30 100644 --- a/ui/app/pages/swaps/build-quote/build-quote.js +++ b/ui/app/pages/swaps/build-quote/build-quote.js @@ -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} /> @@ -411,7 +414,8 @@ export default function BuildQuote({ disabled={ !Number(inputValue) || !selectedToToken?.address || - Number(maxSlippage) === 0 + Number(maxSlippage) === 0 || + Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE } hideCancel /> diff --git a/ui/app/pages/swaps/slippage-buttons/slippage-buttons.js b/ui/app/pages/swaps/slippage-buttons/slippage-buttons.js index fe5daaa63..293422f0f 100644 --- a/ui/app/pages/swaps/slippage-buttons/slippage-buttons.js +++ b/ui/app/pages/swaps/slippage-buttons/slippage-buttons.js @@ -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, } From 5fd6d1a8a6bc0425da6c0e3d9ab68a5b8a42f52a Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 7 Dec 2020 09:13:24 -0600 Subject: [PATCH 05/60] Fix 9906 - Prevent unwanted 'no quotes available' message when going back to build quote screen while having insufficient funds (#9994) --- ui/app/ducks/swaps/swaps.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 7f4c5df96..d09afd648 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -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) From 33b15b5c6ff0b7ce9521055be59ff5545b3945b7 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 7 Dec 2020 09:58:06 -0600 Subject: [PATCH 06/60] Prevent props error in swaps gas modal (#10001) --- .../swaps-gas-customization-modal.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.component.js b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.component.js index 53b4583dd..1347529dc 100644 --- a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.component.js +++ b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.component.js @@ -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, From 4839e31e05ce7ff28e3cbf5c59e4f302fba49727 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 7 Dec 2020 11:39:39 -0800 Subject: [PATCH 07/60] Fix Infura network chain IDs (#8629) --- .../controllers/network/createInfuraClient.js | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 0d1514d61..2a26c9b75 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -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, }) } From 26272d355739f3865faed4818053badd81983505 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 7 Dec 2020 11:40:42 -0800 Subject: [PATCH 08/60] Initialize network controller provider chainId to the appropriate default networks (#9999) --- app/scripts/controllers/network/network.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 016331bce..9dc605cf2 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -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 = { From 6795298c65ffecb61014d809269d31563252544a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:09:02 -0800 Subject: [PATCH 09/60] Remove web3 injection (#9156) * Remove web3 injection * Implement logWeb3ShimUsage --- app/scripts/inpage.js | 18 -- app/scripts/lib/enums.js | 2 +- .../createMethodMiddleware.js | 1 - .../rpc-method-middleware/handlers/index.js | 4 +- .../handlers/log-web3-shim-usage.js | 49 ++++ .../handlers/log-web3-usage.js | 63 ----- app/scripts/lib/setupWeb3.js | 251 ------------------ app/scripts/lib/web3-entities.json | 101 ------- 8 files changed, 52 insertions(+), 437 deletions(-) create mode 100644 app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js delete mode 100644 app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js delete mode 100644 app/scripts/lib/setupWeb3.js delete mode 100644 app/scripts/lib/web3-entities.json diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 8982a93f2..801539376 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -35,10 +35,6 @@ 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 */ - restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') @@ -56,17 +52,3 @@ const metamaskStream = new LocalMessageDuplexStream({ initProvider({ connectionStream: metamaskStream, }) - -// 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.`) -} diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js index d93030f2c..22b3488bc 100644 --- a/app/scripts/lib/enums.js +++ b/app/scripts/lib/enums.js @@ -23,7 +23,7 @@ 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', + LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage', PERSONAL_SIGN: 'personal_sign', WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET_LEGACY: 'metamask_watchAsset', diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index b87047cd2..fdbc265e9 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -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} */ diff --git a/app/scripts/lib/rpc-method-middleware/handlers/index.js b/app/scripts/lib/rpc-method-middleware/handlers/index.js index 74e26b675..e19278a7d 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/index.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/index.js @@ -1,5 +1,5 @@ -import logWeb3Usage from './log-web3-usage' +import logWeb3ShimUsage from './log-web3-shim-usage' import watchAsset from './watch-asset' -const handlers = [logWeb3Usage, watchAsset] +const handlers = [logWeb3ShimUsage, watchAsset] export default handlers diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js new file mode 100644 index 000000000..142e46f44 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js @@ -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} req - The JSON-RPC request object. + * @param {import('json-rpc-engine').JsonRpcResponse} 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() +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js deleted file mode 100644 index c80303223..000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js +++ /dev/null @@ -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} 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() -} diff --git a/app/scripts/lib/setupWeb3.js b/app/scripts/lib/setupWeb3.js deleted file mode 100644 index 9dc74ddb2..000000000 --- a/app/scripts/lib/setupWeb3.js +++ /dev/null @@ -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 ". - * - * @param {any} key - The key to stringify - */ -function stringifyKey(key) { - return typeof key === 'string' ? key : `typeof ${typeof key}` -} diff --git a/app/scripts/lib/web3-entities.json b/app/scripts/lib/web3-entities.json deleted file mode 100644 index 0e9b435bc..000000000 --- a/app/scripts/lib/web3-entities.json +++ /dev/null @@ -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" -} From 7349801799963d4c20ace107b55591b1ef2fb020 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 7 Dec 2020 18:46:03 -0330 Subject: [PATCH 10/60] Add SES lockdown and Sentry to all pages (#10013) When the SES lockdown was added in #9729, the lockdown and the Sentry initialization were migrated from the main bundle into separate modules, which were run as separate ` + + diff --git a/app/notification.html b/app/notification.html index 3419acdca..b55a63142 100644 --- a/app/notification.html +++ b/app/notification.html @@ -33,6 +33,9 @@
+ + + From 2bcb219501c6eabfca92fcb54f07728c62aa7e1c Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 7 Dec 2020 19:54:31 -0330 Subject: [PATCH 11/60] Deobfuscate error message (#10012) The SES lockdown added in #9729 had the effect of obfuscating our error messages. Any messages printed to the console would have the error message replaced with the string "Error #" followed by a number. The stack was also updated to point at `lockdown.cjs`, though the original stack was preserved beneath the top stack frame. Marking the `console` API as untamed seems to have fixed both issues. The original error message is now printed to the console, along with the original stack. --- app/scripts/runLockdown.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/runLockdown.js b/app/scripts/runLockdown.js index 00bc7658f..d8584c69c 100644 --- a/app/scripts/runLockdown.js +++ b/app/scripts/runLockdown.js @@ -1,6 +1,7 @@ // Freezes all intrinsics // eslint-disable-next-line no-undef,import/unambiguous lockdown({ + consoleTaming: 'unsafe', errorTaming: 'unsafe', mathTaming: 'unsafe', dateTaming: 'unsafe', From f75c3c6f6881c33c9990c85585be3d8e79b75dc6 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 8 Dec 2020 12:40:55 -0330 Subject: [PATCH 12/60] Fix unbound metrics track function (#10016) The new metrics controller has a `trackEvent` function that was being called unbound, so `this` references were undefined. It is now bound early in both places where it is passed in as a parameter. --- app/scripts/metamask-controller.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a7517c5db..b1af9cbc2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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, }) @@ -1969,7 +1971,9 @@ export default class MetamaskController extends EventEmitter { engine.push( createMethodMiddleware({ origin, - sendMetrics: this.metaMetricsController.trackEvent, + sendMetrics: this.metaMetricsController.trackEvent.bind( + this.metaMetricsController, + ), handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( this.preferencesController, ), From da5e5cd8b6c3027c8caf0f5f6de906da54b32037 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 8 Dec 2020 13:17:53 -0330 Subject: [PATCH 13/60] Reapply view quote screen designs (#9905) --- app/_locales/en/messages.json | 17 +-- app/images/down-arrow-grey.svg | 3 + ui/app/components/ui/url-icon/index.scss | 2 +- ui/app/components/ui/url-icon/url-icon.js | 5 +- ui/app/pages/swaps/build-quote/build-quote.js | 1 + .../exchange-rate-display.js | 18 ++- ui/app/pages/swaps/fee-card/fee-card.js | 62 +++++++- .../pages/swaps/fee-card/fee-card.stories.js | 17 ++- ui/app/pages/swaps/fee-card/index.scss | 86 ++++++++++- ui/app/pages/swaps/fee-card/pig-icon.js | 54 +++++++ ui/app/pages/swaps/index.scss | 2 +- .../pages/swaps/main-quote-summary/index.scss | 130 +++++++++-------- .../main-quote-summary/main-quote-summary.js | 138 ++++++++---------- .../main-quote-summary.stories.js | 44 +++--- .../pages/swaps/swaps-footer/swaps-footer.js | 5 +- ui/app/pages/swaps/swaps.util.js | 2 + ui/app/pages/swaps/view-quote/index.scss | 57 +++----- ui/app/pages/swaps/view-quote/view-quote.js | 81 ++++------ 18 files changed, 437 insertions(+), 287 deletions(-) create mode 100644 app/images/down-arrow-grey.svg create mode 100644 ui/app/pages/swaps/fee-card/pig-icon.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c0a0f82cf..b9b2497be 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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": { @@ -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,13 +1852,6 @@ "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%." }, diff --git a/app/images/down-arrow-grey.svg b/app/images/down-arrow-grey.svg new file mode 100644 index 000000000..fcdb33eec --- /dev/null +++ b/app/images/down-arrow-grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/components/ui/url-icon/index.scss b/ui/app/components/ui/url-icon/index.scss index 482f327d3..14d5524cb 100644 --- a/ui/app/components/ui/url-icon/index.scss +++ b/ui/app/components/ui/url-icon/index.scss @@ -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; } } diff --git a/ui/app/components/ui/url-icon/url-icon.js b/ui/app/components/ui/url-icon/url-icon.js index 6dcfcc06f..115eb5fe1 100644 --- a/ui/app/components/ui/url-icon/url-icon.js +++ b/ui/app/components/ui/url-icon/url-icon.js @@ -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 ( ) } @@ -18,4 +18,5 @@ UrlIcon.propTypes = { url: PropTypes.string, className: PropTypes.string, name: PropTypes.string, + fallbackClassName: PropTypes.string, } diff --git a/ui/app/pages/swaps/build-quote/build-quote.js b/ui/app/pages/swaps/build-quote/build-quote.js index 13d873b30..4714f79ad 100644 --- a/ui/app/pages/swaps/build-quote/build-quote.js +++ b/ui/app/pages/swaps/build-quote/build-quote.js @@ -418,6 +418,7 @@ export default function BuildQuote({ Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE } hideCancel + showTermsOfService /> ) diff --git a/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js b/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js index fbb268d65..e70ffd8e7 100644 --- a/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js +++ b/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js @@ -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 (
1 - {baseSymbol} + + {baseSymbol} + {comparisonSymbol} {rateToDisplay} - {ratiodSymbol} + + {ratiodSymbol} +
+
+
+ {bestQuoteText && ( +

{bestQuoteText}

+ )} +
+

+ {t('swapNQuotes', [numberOfQuotes])} +

+
+ +
+
+
+
@@ -83,26 +113,39 @@ export default function FeeCard({
{!hideTokenApprovalRow && ( -
+
{t('swapThisWillAllowApprove', [tokenApprovalTextComponent])}
-
onTokenApprovalClick()} - > - {t('swapEditLimit')} -
+
onTokenApprovalClick()} + > + {t('swapEditLimit')} +
)} +
+
+
+ {t('swapQuoteIncludesRate', [metaMaskFee])} +
+ +
+
) @@ -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, } diff --git a/ui/app/pages/swaps/fee-card/fee-card.stories.js b/ui/app/pages/swaps/fee-card/fee-card.stories.js index af57849f3..aaaf43d0f 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.stories.js +++ b/ui/app/pages/swaps/fee-card/fee-card.stories.js @@ -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" />
) @@ -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" />
) @@ -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} /> ) diff --git a/ui/app/pages/swaps/fee-card/index.scss b/ui/app/pages/swaps/fee-card/index.scss index 0a86f6ab7..d1807fc0d 100644 --- a/ui/app/pages/swaps/fee-card/index.scss +++ b/ui/app/pages/swaps/fee-card/index.scss @@ -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 { diff --git a/ui/app/pages/swaps/fee-card/pig-icon.js b/ui/app/pages/swaps/fee-card/pig-icon.js new file mode 100644 index 000000000..ef7677ae4 --- /dev/null +++ b/ui/app/pages/swaps/fee-card/pig-icon.js @@ -0,0 +1,54 @@ +import React from 'react' + +export default function PigIcon() { + return ( + + + + + + + + + + ) +} diff --git a/ui/app/pages/swaps/index.scss b/ui/app/pages/swaps/index.scss index 1346eb364..cb08d457b 100644 --- a/ui/app/pages/swaps/index.scss +++ b/ui/app/pages/swaps/index.scss @@ -71,7 +71,7 @@ font-weight: bold; color: $Black-100; - margin-top: 3px; + margin-top: -5px; } &__header { diff --git a/ui/app/pages/swaps/main-quote-summary/index.scss b/ui/app/pages/swaps/main-quote-summary/index.scss index 7993ea724..dfdc882f1 100644 --- a/ui/app/pages/swaps/main-quote-summary/index.scss +++ b/ui/app/pages/swaps/main-quote-summary/index.scss @@ -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; } } diff --git a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js index 1ae2e7d51..20aadf6fc 100644 --- a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js +++ b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js @@ -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 (
-
- -
-
- {isBestQuote && } - {isBestQuote && t('swapsBestQuote')} -
- - {t('swapsConvertToAbout', [ - - {`${sourceAmount} ${sourceSymbol}`} - , - ])} - +
+ + {formatSwapsValueForDisplay(sourceAmount)} + + + + {sourceSymbol} + +
+ +
+ + + {destinationSymbol} + +
- - {`${destinationSymbol}`} -
@@ -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" />
@@ -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, } diff --git a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js index ea267eb17..0f5ca998d 100644 --- a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js +++ b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js @@ -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 ( - - ) -} - -export const NotBestQuote = () => { - return ( - +
+ +
) } diff --git a/ui/app/pages/swaps/swaps-footer/swaps-footer.js b/ui/app/pages/swaps/swaps-footer/swaps-footer.js index 870ce028b..62a3e2a70 100644 --- a/ui/app/pages/swaps/swaps-footer/swaps-footer.js +++ b/ui/app/pages/swaps/swaps-footer/swaps-footer.js @@ -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', { diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index d3189e5b7..189ff877b 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -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, diff --git a/ui/app/pages/swaps/view-quote/index.scss b/ui/app/pages/swaps/view-quote/index.scss index 474eced60..3382befff 100644 --- a/ui/app/pages/swaps/view-quote/index.scss +++ b/ui/app/pages/swaps/view-quote/index.scss @@ -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; + } } } diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 4db97824f..1f5769f0c 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -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() { /> )}
-
+
-
- -
-
-
- {t('swapNQuotesAvailable', [Object.values(quotes).length])} - -
-
{ - allAvailableQuotesOpened() - setSelectQuotePopoverShown(true) - }} - > - {t('swapNQuotesAvailable', [Object.values(quotes).length])} - -
-
-
-

- {t('swapQuoteIncludesRate', [metaMaskFee])} -

- -
+
{ + allAvailableQuotesOpened() + setSelectQuotePopoverShown(true) + }} + tokenConversionRate={ + destinationTokenSymbol === 'ETH' + ? 1 + : memoizedTokenConversionRates[destinationToken.address] + } />
@@ -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 /> From d13aabde2307af821b93ecb1b3221b07112e48b2 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 8 Dec 2020 15:08:31 -0330 Subject: [PATCH 14/60] Fix SES lockdown on older browsers (#10014) On older browsers that don't support `globalThis`[1], the SES lockdown throws an error. The `globalthis` shim has been added to all pages, to the background process, and to the `contentscript`. This should prevent the error on older browsers. [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis#Browser_compatibility --- app/home.html | 1 + app/manifest/_base.json | 8 +++++++- app/notification.html | 1 + app/phishing.html | 1 + app/popup.html | 1 + development/build/static.js | 4 ++++ package.json | 1 + 7 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/home.html b/app/home.html index 4979ccb3b..072eddea9 100644 --- a/app/home.html +++ b/app/home.html @@ -10,6 +10,7 @@
+ diff --git a/app/manifest/_base.json b/app/manifest/_base.json index e27c3285f..8b2e971dd 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -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 }, diff --git a/app/notification.html b/app/notification.html index b55a63142..4f424e3c0 100644 --- a/app/notification.html +++ b/app/notification.html @@ -33,6 +33,7 @@
+ diff --git a/app/phishing.html b/app/phishing.html index 1c913db2e..59ea3ac71 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -2,6 +2,7 @@ Ethereum Phishing Detection - MetaMask + diff --git a/app/popup.html b/app/popup.html index 4d29f6153..e73f3e4d2 100644 --- a/app/popup.html +++ b/app/popup.html @@ -10,6 +10,7 @@
+ diff --git a/development/build/static.js b/development/build/static.js index 142f290d1..1fe5dc959 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -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`, diff --git a/package.json b/package.json index f11903fbb..7260707b6 100644 --- a/package.json +++ b/package.json @@ -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", From 55e5f5513ce2526d18f72acc2297a041a1eb2098 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 8 Dec 2020 16:17:51 -0330 Subject: [PATCH 15/60] Log persistence errors with Sentry (#10018) Failures to persist state are now logged in Sentry. Previously they were only logged to the background console. --- app/scripts/background.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/scripts/background.js b/app/scripts/background.js index fa1e48a10..92f55319a 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -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) } } From 3bf94164ac08a15e4bed59ed61609cc75a38ef78 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 8 Dec 2020 11:48:47 -0800 Subject: [PATCH 16/60] @metamask/inpage-provider@^8.0.0 (#8640) * @metamask/inpage-provider@^8.0.0 * Replace public config store with JSON-RPC notifications * Encapsulate notification permissioning in permissions controller * Update prefix of certain internal RPC methods and notifications * Add accounts to getProviderState * Send accounts with isUnlocked notification (#10007) * Rename provider streams, notify provider of stream failures (#10006) --- app/scripts/contentscript.js | 93 +++++---- app/scripts/controllers/permissions/enums.js | 14 +- app/scripts/controllers/permissions/index.js | 35 ++-- .../controllers/permissions/permissionsLog.js | 1 + .../permissionsMethodMiddleware.js | 2 +- app/scripts/inpage.js | 10 +- app/scripts/lib/enums.js | 1 + .../handlers/get-provider-state.js | 46 +++++ .../rpc-method-middleware/handlers/index.js | 3 +- app/scripts/metamask-controller.js | 182 +++++++++++------- package.json | 2 +- .../controllers/metamask-controller-test.js | 4 +- .../unit/app/controllers/permissions/mocks.js | 21 +- .../permissions-controller-test.js | 40 ++-- .../permissions-log-controller-test.js | 2 +- .../permissions-middleware-test.js | 19 +- yarn.lock | 24 +-- 17 files changed, 301 insertions(+), 198 deletions(-) create mode 100644 app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 05d82c538..5f5f25f9d 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -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 }), - ) -} diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index 56a87fdf5..d5ca52e30 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -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', ] diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 2e3c12775..454bd8e56 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -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 -} diff --git a/app/scripts/controllers/permissions/permissionsLog.js b/app/scripts/controllers/permissions/permissionsLog.js index 6bb142993..e1a37f0c8 100644 --- a/app/scripts/controllers/permissions/permissionsLog.js +++ b/app/scripts/controllers/permissions/permissionsLog.js @@ -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} accounts - The accounts. diff --git a/app/scripts/controllers/permissions/permissionsMethodMiddleware.js b/app/scripts/controllers/permissions/permissionsMethodMiddleware.js index f2eec6cf4..de9009f13 100644 --- a/app/scripts/controllers/permissions/permissionsMethodMiddleware.js +++ b/app/scripts/controllers/permissions/permissionsMethodMiddleware.js @@ -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) } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 801539376..c7619f77e 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -33,7 +33,7 @@ cleanContextForImports() /* eslint-disable import/first */ import log from 'loglevel' import LocalMessageDuplexStream from 'post-message-stream' -import { initProvider } from '@metamask/inpage-provider' +import { initializeProvider } from '@metamask/inpage-provider' restoreContextAfterImports() @@ -45,10 +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, }) diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js index 22b3488bc..a6b7a99de 100644 --- a/app/scripts/lib/enums.js +++ b/app/scripts/lib/enums.js @@ -23,6 +23,7 @@ const MESSAGE_TYPE = { ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey', ETH_SIGN: 'eth_sign', ETH_SIGN_TYPED_DATA: 'eth_signTypedData', + GET_PROVIDER_STATE: 'metamask_getProviderState', LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage', PERSONAL_SIGN: 'personal_sign', WATCH_ASSET: 'wallet_watchAsset', diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js new file mode 100644 index 000000000..32b3c18c6 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js @@ -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} 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() +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/index.js b/app/scripts/lib/rpc-method-middleware/handlers/index.js index e19278a7d..41011778a 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/index.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/index.js @@ -1,5 +1,6 @@ +import getProviderState from './get-provider-state' import logWeb3ShimUsage from './log-web3-shim-usage' import watchAsset from './watch-asset' -const handlers = [logWeb3ShimUsage, watchAsset] +const handlers = [getProviderState, logWeb3ShimUsage, watchAsset] export default handlers diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b1af9cbc2..0e976a994 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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, @@ -348,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, @@ -450,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 } //============================================================================= @@ -1813,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) } /** @@ -1971,6 +1972,7 @@ export default class MetamaskController extends EventEmitter { engine.push( createMethodMiddleware({ origin, + getProviderState: this.getProviderState.bind(this), sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), @@ -1993,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 @@ -2066,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)) + } }) }) } @@ -2125,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 /** diff --git a/package.json b/package.json index 7260707b6..519fae646 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 99bd3f957..2e1c4021d 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -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, diff --git a/test/unit/app/controllers/permissions/mocks.js b/test/unit/app/controllers/permissions/mocks.js index 9040f6b12..d6b93dd68 100644 --- a/test/unit/app/controllers/permissions/mocks.js +++ b/test/unit/app/controllers/permissions/mocks.js @@ -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} [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, diff --git a/test/unit/app/controllers/permissions/permissions-controller-test.js b/test/unit/app/controllers/permissions/permissions-controller-test.js index 5f79bb443..434ae6bd3 100644 --- a/test/unit/app/controllers/permissions/permissions-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-controller-test.js @@ -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') - }) }) }) diff --git a/test/unit/app/controllers/permissions/permissions-log-controller-test.js b/test/unit/app/controllers/permissions/permissions-log-controller-test.js index ac498a0e0..101f898b9 100644 --- a/test/unit/app/controllers/permissions/permissions-log-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-log-controller-test.js @@ -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', ) diff --git a/test/unit/app/controllers/permissions/permissions-middleware-test.js b/test/unit/app/controllers/permissions/permissions-middleware-test.js index 2888d3da5..ff4e832c9 100644 --- a/test/unit/app/controllers/permissions/permissions-middleware-test.js +++ b/test/unit/app/controllers/permissions/permissions-middleware-test.js @@ -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 = {} diff --git a/yarn.lock b/yarn.lock index 92a25c7bc..da2e567eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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== From e4a77e1dc37555812bc7ec9fd488e9c450a2776f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20=C5=81ucka?= Date: Tue, 8 Dec 2020 21:38:00 +0100 Subject: [PATCH 17/60] Add hidden tokens to store (#9320) From a behavioral standpoint this PR fixes the issue with tracking, and persisting, tokens that the user hides. Whether we can/should optimize this to prevent duplicates of the accountHiddenTokens and hiddenToken is a point of contention, but it acts similiarly to how we track tokens and accountTokens. Also to note, for tokens under a custom network there is no way to distinguish two different custom network sets of hidden tokens, they are all under the `rpc` property, same as accountTokens. --- app/scripts/controllers/detect-tokens.js | 7 +- app/scripts/controllers/preferences.js | 96 ++++++++++++++----- .../app/controllers/detect-tokens-test.js | 47 +++++++++ 3 files changed, 124 insertions(+), 26 deletions(-) diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 1fda6dccc..df821335c 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -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) { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index bfba2835b..12da861f6 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -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} 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.} 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, + } } /** diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js index 82be8808a..d54c55154 100644 --- a/test/unit/app/controllers/detect-tokens-test.js +++ b/test/unit/app/controllers/detect-tokens-test.js @@ -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) From f4914ec177b770314dcc5a080e2c66928adc1a73 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 9 Dec 2020 01:40:13 -0330 Subject: [PATCH 18/60] Revert "Revert "Remove redundant babelify (#9945)"" This reverts commit 7696de2e4e217231c197639035890d3d1dd96136. --- development/build/scripts.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/development/build/scripts.js b/development/build/scripts.js index d1a2559a0..52bedaf32 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -326,14 +326,6 @@ function createScriptTasks({ browserPlatforms, livereload }) { let bundler = browserify(browserifyOpts) .transform('babelify') - // Transpile any dependencies using the object spread/rest operator - // because it is incompatible with `esprima`, which is used by `envify` - // See https://github.com/jquery/esprima/issues/1927 - .transform('babelify', { - only: ['./**/node_modules/libp2p'], - global: true, - plugins: ['@babel/plugin-proposal-object-rest-spread'], - }) .transform('brfs') if (opts.buildLib) { From 0b0b91196d36ec23b13498b56b44fd77f99a3be7 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 9 Dec 2020 01:40:22 -0330 Subject: [PATCH 19/60] Revert "Revert "Add SES lockdown to extension webapp (#9729)"" This reverts commit d783966065c37abea24a407478f0a574cdcb8345. --- app/manifest/_base.json | 10 +- app/phishing.html | 2 + app/popup.html | 3 + app/scripts/background.js | 8 +- app/scripts/initSentry.js | 7 ++ app/scripts/lib/freezeGlobals.js | 37 ------ app/scripts/runLockdown.js | 7 ++ app/scripts/ui.js | 14 +-- development/build/index.js | 5 - development/build/scripts.js | 12 +- development/build/static.js | 10 ++ development/build/task.js | 6 +- package.json | 7 +- test/unit-global/frozenPromise.js | 8 +- test/unit-global/globalPatch.js | 2 + yarn.lock | 182 +++++++++++++----------------- 16 files changed, 143 insertions(+), 177 deletions(-) create mode 100644 app/scripts/initSentry.js delete mode 100644 app/scripts/lib/freezeGlobals.js create mode 100644 app/scripts/runLockdown.js create mode 100644 test/unit-global/globalPatch.js diff --git a/app/manifest/_base.json b/app/manifest/_base.json index e7fcbdf37..3192abeb4 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -1,7 +1,13 @@ { "author": "https://metamask.io", "background": { - "scripts": ["bg-libs.js", "background.js"], + "scripts": [ + "initSentry.js", + "lockdown.cjs", + "runLockdown.js", + "bg-libs.js", + "background.js" + ], "persistent": true }, "browser_action": { @@ -30,7 +36,7 @@ "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], - "js": ["contentscript.js"], + "js": ["lockdown.cjs", "runLockdown.js", "contentscript.js"], "run_at": "document_start", "all_frames": true }, diff --git a/app/phishing.html b/app/phishing.html index 583a50140..1c913db2e 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -2,6 +2,8 @@ Ethereum Phishing Detection - MetaMask + + diff --git a/app/popup.html b/app/popup.html index 0c3d70407..4d29f6153 100644 --- a/app/popup.html +++ b/app/popup.html @@ -10,6 +10,9 @@
+ + + diff --git a/app/scripts/background.js b/app/scripts/background.js index c57710bd2..fa1e48a10 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -3,7 +3,6 @@ */ // these need to run before anything else /* eslint-disable import/first,import/order */ -import './lib/freezeGlobals' import setupFetchDebugging from './lib/setupFetchDebugging' /* eslint-enable import/order */ @@ -29,7 +28,6 @@ import createStreamSink from './lib/createStreamSink' import NotificationManager from './lib/notification-manager' import MetamaskController from './metamask-controller' import rawFirstTimeState from './first-time-state' -import setupSentry from './lib/setupSentry' import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code' import getObjStructure from './lib/getObjStructure' import setupEnsIpfsResolver from './lib/ens-ipfs/setup' @@ -41,18 +39,16 @@ import { } from './lib/enums' /* eslint-enable import/first */ +const { sentry } = global const firstTimeState = { ...rawFirstTimeState } log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') const platform = new ExtensionPlatform() + const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager -// setup sentry error reporting -const release = platform.getVersion() -const sentry = setupSentry({ release }) - let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} diff --git a/app/scripts/initSentry.js b/app/scripts/initSentry.js new file mode 100644 index 000000000..70ea7b936 --- /dev/null +++ b/app/scripts/initSentry.js @@ -0,0 +1,7 @@ +import setupSentry from './lib/setupSentry' + +// setup sentry error reporting +global.sentry = setupSentry({ + release: process.env.METAMASK_VERSION, + getState: () => global.getSentryState?.() || {}, +}) diff --git a/app/scripts/lib/freezeGlobals.js b/app/scripts/lib/freezeGlobals.js deleted file mode 100644 index 08b201bee..000000000 --- a/app/scripts/lib/freezeGlobals.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Freezes the Promise global and prevents its reassignment. - */ -import deepFreeze from 'deep-freeze-strict' - -if (process.env.IN_TEST !== 'true' && process.env.METAMASK_ENV !== 'test') { - freeze(global, 'Promise') -} - -/** - * Makes a key:value pair on a target object immutable, with limitations. - * The key cannot be reassigned or deleted, and the value is recursively frozen - * using Object.freeze. - * - * Because of JavaScript language limitations, this is does not mean that the - * value is completely immutable. It is, however, better than nothing. - * - * @param {Object} target - The target object to freeze a property on. - * @param {string} key - The key to freeze. - * @param {any} [value] - The value to freeze, if different from the existing value on the target. - * @param {boolean} [enumerable=true] - If given a value, whether the property is enumerable. - */ -function freeze(target, key, value, enumerable = true) { - const opts = { - configurable: false, - writable: false, - } - - if (value === undefined) { - target[key] = deepFreeze(target[key]) - } else { - opts.value = deepFreeze(value) - opts.enumerable = enumerable - } - - Object.defineProperty(target, key, opts) -} diff --git a/app/scripts/runLockdown.js b/app/scripts/runLockdown.js new file mode 100644 index 000000000..00bc7658f --- /dev/null +++ b/app/scripts/runLockdown.js @@ -0,0 +1,7 @@ +// Freezes all intrinsics +// eslint-disable-next-line no-undef,import/unambiguous +lockdown({ + errorTaming: 'unsafe', + mathTaming: 'unsafe', + dateTaming: 'unsafe', +}) diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 5b5932a50..f1f4a7b78 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -1,13 +1,9 @@ -// this must run before anything else -import './lib/freezeGlobals' - // polyfills import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' import '@formatjs/intl-relativetimeformat/polyfill' import { EventEmitter } from 'events' import PortStream from 'extension-port-stream' - import extension from 'extensionizer' import Dnode from 'dnode' @@ -16,9 +12,8 @@ import EthQuery from 'eth-query' import StreamProvider from 'web3-stream-provider' import log from 'loglevel' import launchMetaMaskUi from '../../ui' -import { setupMultiplex } from './lib/stream-utils' -import setupSentry from './lib/setupSentry' import ExtensionPlatform from './platforms/extension' +import { setupMultiplex } from './lib/stream-utils' import { ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_POPUP, @@ -31,13 +26,6 @@ async function start() { // create platform global global.platform = new ExtensionPlatform() - // setup sentry error reporting - const release = global.platform.getVersion() - setupSentry({ - release, - getState: () => window.getSentryState?.() || {}, - }) - // identify window type (popup, notification) const windowType = getEnvironmentType() diff --git a/development/build/index.js b/development/build/index.js index cc75d8398..facf87b61 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -3,11 +3,6 @@ // // run any task with "yarn build ${taskName}" // -global.globalThis = global // eslint-disable-line node/no-unsupported-features/es-builtins -require('lavamoat-core/lib/ses.umd.js') - -lockdown() // eslint-disable-line no-undef - const livereload = require('gulp-livereload') const { createTask, diff --git a/development/build/scripts.js b/development/build/scripts.js index 52bedaf32..2f6b577e4 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -9,7 +9,7 @@ const log = require('fancy-log') const { assign } = require('lodash') const watchify = require('watchify') const browserify = require('browserify') -const envify = require('envify/custom') +const envify = require('loose-envify/custom') const sourcemaps = require('gulp-sourcemaps') const sesify = require('sesify') const terser = require('gulp-terser-js') @@ -22,6 +22,8 @@ const conf = require('rc')('metamask', { SEGMENT_LEGACY_WRITE_KEY: process.env.SEGMENT_LEGACY_WRITE_KEY, }) +const baseManifest = require('../../app/manifest/_base.json') + const packageJSON = require('../../package.json') const { createTask, @@ -96,7 +98,12 @@ function createScriptTasks({ browserPlatforms, livereload }) { } function createTasksForBuildJsExtension({ taskPrefix, devMode, testing }) { - const standardBundles = ['background', 'ui', 'phishing-detect'] + const standardBundles = [ + 'background', + 'ui', + 'phishing-detect', + 'initSentry', + ] const standardSubtasks = standardBundles.map((filename) => { return createTask( @@ -349,6 +356,7 @@ function createScriptTasks({ browserPlatforms, livereload }) { envify({ METAMASK_DEBUG: opts.devMode, METAMASK_ENVIRONMENT: environment, + METAMASK_VERSION: baseManifest.version, METAMETRICS_PROJECT_ID: process.env.METAMETRICS_PROJECT_ID, NODE_ENV: opts.devMode ? 'development' : 'production', IN_TEST: opts.testing ? 'true' : false, diff --git a/development/build/static.js b/development/build/static.js index f00d53d25..142f290d1 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -44,6 +44,16 @@ const copyTargets = [ pattern: `*.html`, dest: ``, }, + { + src: `./node_modules/ses/dist/`, + pattern: `lockdown.cjs`, + dest: ``, + }, + { + src: `./app/scripts/`, + pattern: `runLockdown.js`, + dest: ``, + }, ] const languageTags = new Set() diff --git a/development/build/task.js b/development/build/task.js index 553a06956..1ed252413 100644 --- a/development/build/task.js +++ b/development/build/task.js @@ -68,7 +68,9 @@ function runInChildProcess(task) { ) } return instrumentForTaskStats(taskName, async () => { - const childProcess = spawn('yarn', ['build', taskName, '--skip-stats']) + const childProcess = spawn('yarn', ['build', taskName, '--skip-stats'], { + env: process.env, + }) // forward logs to main process // skip the first stdout event (announcing the process command) childProcess.stdout.once('data', () => { @@ -85,7 +87,7 @@ function runInChildProcess(task) { if (errCode !== 0) { reject( new Error( - `MetaMask build: runInChildProcess for task "${taskName}" encountered an error`, + `MetaMask build: runInChildProcess for task "${taskName}" encountered an error ${errCode}`, ), ) return diff --git a/package.json b/package.json index d93ce8230..f11903fbb 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,9 @@ "**/knex/minimist": "^1.2.5", "**/optimist/minimist": "^1.2.5", "**/socketcluster/minimist": "^1.2.5", + "**/redux/symbol-observable": "^2.0.3", + "**/redux-devtools-instrument/symbol-observable": "^2.0.3", + "**/rxjs/symbol-observable": "^2.0.3", "3box/ipfs/ipld-zcash/zcash-bitcore-lib/lodash": "^4.17.19", "3box/ipfs/ipld-zcash/zcash-bitcore-lib/elliptic": "^6.5.3", "3box/**/libp2p-crypto/node-forge": "^0.10.0", @@ -216,7 +219,6 @@ "css-loader": "^2.1.1", "del": "^3.0.0", "deps-dump": "^1.1.0", - "envify": "^4.1.0", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", "eslint": "^7.7.0", @@ -252,8 +254,8 @@ "gulp-zip": "^4.0.0", "jsdom": "^11.2.0", "koa": "^2.7.0", - "lavamoat-core": "^6.1.0", "lockfile-lint": "^4.0.0", + "loose-envify": "^1.4.0", "mocha": "^7.2.0", "nock": "^9.0.14", "node-fetch": "^2.6.1", @@ -276,6 +278,7 @@ "sass-loader": "^7.0.1", "selenium-webdriver": "^4.0.0-alpha.5", "serve-handler": "^6.1.2", + "ses": "0.11.0", "sesify": "^4.2.1", "sesify-viz": "^3.0.10", "sinon": "^9.0.0", diff --git a/test/unit-global/frozenPromise.js b/test/unit-global/frozenPromise.js index 6583e1edf..1a1ca18b5 100644 --- a/test/unit-global/frozenPromise.js +++ b/test/unit-global/frozenPromise.js @@ -1,6 +1,8 @@ -import '../../app/scripts/lib/freezeGlobals' - -import assert from 'assert' +// Should occur before anything else +import './globalPatch' +import 'ses/dist/lockdown.cjs' +import '../../app/scripts/runLockdown' +import assert from 'assert' /* eslint-disable-line import/first,import/order */ describe('Promise global is immutable', function () { it('throws when reassinging promise (syntax 1)', function () { diff --git a/test/unit-global/globalPatch.js b/test/unit-global/globalPatch.js new file mode 100644 index 000000000..89d392454 --- /dev/null +++ b/test/unit-global/globalPatch.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/unambiguous,node/no-unsupported-features/es-builtins +global.globalThis = global diff --git a/yarn.lock b/yarn.lock index c78a9c956..f328d7c05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,6 +47,21 @@ did-resolver "0.0.6" ipfs-did-document "^1.2.3" +"@agoric/babel-standalone@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@agoric/babel-standalone/-/babel-standalone-7.9.5.tgz#1ca0c17844924199d31e49d6b67e8b2a629b8599" + integrity sha512-1Aa23oPuRi4kywUyZODo8zey9Gq2NpD2xUnNvgJLoT8orMQRlVOtvbG3JeHq5sjJERlF/q6csg4/P8t8/5IABA== + +"@agoric/make-hardener@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@agoric/make-hardener/-/make-hardener-0.1.1.tgz#9b887da47aeec6637d9db4f0a92a4e740b8262bb" + integrity sha512-3emNc+yWJoFK5JMLoEFPs6rCzkntWQKxpR4gt3jaZYLKoUG4LrTmID3XNe8y40B6SJ3k/wLPodKa0ToQGlhrwQ== + +"@agoric/transform-module@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@agoric/transform-module/-/transform-module-0.4.1.tgz#9fb152364faf372e1bda535cb4ef89717724f57c" + integrity sha512-4TJJHXeXAWu1FCA7yXCAZmhBNoGTB/BEAe2pv+J2X8W/mJTr9b395OkDCSRMpzvmSshLfBx6wT0D7dqWIWEC1w== + "@babel/code-frame@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -104,7 +119,7 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.1", "@babel/generator@^7.12.5": +"@babel/generator@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== @@ -332,12 +347,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.1", "@babel/parser@^7.7.5": - version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd" - integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw== - -"@babel/parser@^7.12.1", "@babel/parser@^7.12.7": +"@babel/parser@^7.12.7", "@babel/parser@^7.7.5": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== @@ -1064,22 +1074,7 @@ "@babel/parser" "^7.12.7" "@babel/types" "^7.12.7" -"@babel/traverse@^7.10.1", "@babel/traverse@^7.12.1", "@babel/traverse@^7.7.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.1.tgz#941395e0c5cc86d5d3e75caa095d3924526f0c1e" - integrity sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.1" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.12.1" - "@babel/types" "^7.12.1" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9": +"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9", "@babel/traverse@^7.7.4": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f" integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw== @@ -4412,7 +4407,7 @@ async-iterator-to-stream@^1.1.0: dependencies: readable-stream "^3.0.5" -async-limiter@~1.0.0: +async-limiter@^1.0.0, async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== @@ -5436,10 +5431,10 @@ base64-arraybuffer@0.1.5: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64-js@^1.0.2, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== base64id@1.0.0: version "1.0.0" @@ -6233,12 +6228,12 @@ buffer@^4.3.0: isarray "^1.0.0" buffer@^5.0.5, buffer@^5.2.1: - version "5.4.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" - integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" + base64-js "^1.3.1" + ieee754 "^1.1.13" buffer@~5.2.1: version "5.2.1" @@ -7833,11 +7828,6 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -cytoplasm@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/cytoplasm/-/cytoplasm-3.3.1.tgz#6d10099e536b12c642e8375b47cbffac66656a4b" - integrity sha512-38swm2Lr7cmTh0KRYtiUOCT5PT2TPp31Lpp3wcAIm3iGT4KdGGewyTydkFhBEDrKik/umgmG2hVoG5A1fKQY+g== - d64@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/d64/-/d64-1.0.0.tgz#4002a87e850cbfc9f9d9706b60fca613a3336e90" @@ -9025,14 +9015,6 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== -envify@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/envify/-/envify-4.1.0.tgz#f39ad3db9d6801b4e6b478b61028d3f0b6819f7e" - integrity sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw== - dependencies: - esprima "^4.0.0" - through "~2.3.4" - enzyme-adapter-react-16@^1.15.1: version "1.15.1" resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" @@ -13277,10 +13259,10 @@ idna-uts46@^1.0.1: dependencies: punycode "^2.1.0" -ieee754@^1.1.4, ieee754@^1.1.8: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== +ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.1.8: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== iferr@^0.1.5: version "0.1.5" @@ -15779,25 +15761,6 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" -lavamoat-core@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/lavamoat-core/-/lavamoat-core-6.2.0.tgz#ff1e39c0407f4bf5b94847c5dfaf5b09f28ac77a" - integrity sha512-sY0ddwWou2WGnpEFG/8CGguo6XKq2hc5etIDQ47TYUEJB9BaWGuWulekcrTsTQ0f7EN5r0Y2QtV5ZxxM0fMm8w== - dependencies: - cytoplasm "^3.3.1" - fromentries "^1.2.0" - json-stable-stringify "^1.0.1" - lavamoat-tofu "^5.1.1" - resolve "^1.15.1" - -lavamoat-tofu@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lavamoat-tofu/-/lavamoat-tofu-5.1.1.tgz#67d3c5775b587dd439c426f1cd86650b10443760" - integrity sha512-LIZTifbbTATA5UClVGI7YGMTOjUXjrVCBOz0DaeUicldLonLlwG81/53/vTIfPM1xV/KA85MIzGPHz4pDdiVkA== - dependencies: - "@babel/parser" "^7.10.1" - "@babel/traverse" "^7.10.1" - lazy-cache@^0.2.3: version "0.2.7" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" @@ -17973,9 +17936,9 @@ nano-json-stream-parser@^0.1.2: integrity sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18= nanoid@^2.0.0, nanoid@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.6.tgz#0665418f692e54cf44f34d4010761f3240a03314" - integrity sha512-2NDzpiuEy3+H0AVtdt8LoFi7PnqkOnIzYmJQp7xsEU6VexLluHQwKREuiz57XaQC5006seIadPrIZJhyS2n7aw== + version "2.1.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" + integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== nanoid@^3.1.12: version "3.1.16" @@ -21624,12 +21587,12 @@ redux-devtools-core@^0.2.1: remotedev-serialize "^0.1.8" redux-devtools-instrument@^1.9.4: - version "1.9.6" - resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz#6b412595f74b9d48cfd4ecc13e585b1588ed6e7e" - integrity sha512-MwvY4cLEB2tIfWWBzrUR02UM9qRG2i7daNzywRvabOSVdvAY7s9BxSwMmVRH1Y/7QWjplNtOwgT0apKhHg2Qew== + version "1.10.0" + resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.10.0.tgz#036caf79fa1e5f25ec4bae38a9af4f08c69e323a" + integrity sha512-X8JRBCzX2ADSMp+iiV7YQ8uoTNyEm0VPFPd4T854coz6lvRiBrFSqAr9YAS2n8Kzxx8CJQotR0QF9wsMM+3DvA== dependencies: - lodash "^4.2.0" - symbol-observable "^1.0.2" + lodash "^4.17.19" + symbol-observable "^1.2.0" redux-mock-store@^1.5.4: version "1.5.4" @@ -21915,9 +21878,9 @@ remote-redux-devtools@^0.5.16: socketcluster-client "^14.2.1" remotedev-serialize@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/remotedev-serialize/-/remotedev-serialize-0.1.8.tgz#c99cb184e7f71a906162abc404be8ce33810205f" - integrity sha512-3YG/FDcOmiK22bl5oMRM8RRnbGrFEuPGjbcDG+z2xi5aQaNQNZ8lqoRnZTwXVfaZtutXuiAQOgPRrogzQk8edg== + version "0.1.9" + resolved "https://registry.yarnpkg.com/remotedev-serialize/-/remotedev-serialize-0.1.9.tgz#5e67e05cbca75d408d769d057dc59d0f56cd2c43" + integrity sha512-5tFdZg9mSaAWTv6xmQ7HtHjKMLSFQFExEZOtJe10PLsv1wb7cy7kYHtBvTYRro27/3fRGEcQBRNKSaixOpb69w== dependencies: jsan "^3.1.13" @@ -22181,7 +22144,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: +resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: version "1.18.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== @@ -22304,9 +22267,9 @@ rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3: bn.js "^4.11.1" rn-host-detect@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.1.5.tgz#fbecb982b73932f34529e97932b9a63e58d8deb6" - integrity sha512-ufk2dFT3QeP9HyZ/xTuMtW27KnFy815CYitJMqQm+pgG3ZAtHBsrU8nXizNKkqXGy3bQmhEoloVbrfbvMJMqkg== + version "1.2.0" + resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0" + integrity sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A== roarr@^2.15.3: version "2.15.3" @@ -22535,6 +22498,11 @@ sc-errors@^1.4.1: resolved "https://registry.yarnpkg.com/sc-errors/-/sc-errors-1.4.1.tgz#53e80030fe647e133d73b51eaa7d2b0f7591fd5b" integrity sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ== +sc-errors@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/sc-errors/-/sc-errors-2.0.1.tgz#3af2d934dfd82116279a4b2c1552c1e021ddcb03" + integrity sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ== + sc-formatter@^3.0.1, sc-formatter@^3.0.2, sc-formatter@~3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/sc-formatter/-/sc-formatter-3.0.2.tgz#9abdb14e71873ce7157714d3002477bbdb33c4e6" @@ -22848,6 +22816,15 @@ servify@^0.1.12: request "^2.79.0" xhr "^2.3.3" +ses@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/ses/-/ses-0.11.0.tgz#1e470112ed320d169f0b850525858129c0be0881" + integrity sha512-3HH+23C4bijk9VegfiP+cBMqkGim/TMsj/DK5nh/pJFiNrCMfi5euvVluIV66ry202+uckg7nXKrgrEcBwU8SA== + dependencies: + "@agoric/babel-standalone" "^7.9.5" + "@agoric/make-hardener" "^0.1.0" + "@agoric/transform-module" "^0.4.1" + sesify-tofu@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/sesify-tofu/-/sesify-tofu-2.0.4.tgz#b31d4c8d67ea2d61e9c5be4948f085a849f3e632" @@ -23222,9 +23199,9 @@ socket.io@^2.1.1: socket.io-parser "~3.3.0" socketcluster-client@^14.2.1: - version "14.2.2" - resolved "https://registry.yarnpkg.com/socketcluster-client/-/socketcluster-client-14.2.2.tgz#60b31318abe6828ba7233f5a9a32540263fd23b6" - integrity sha512-vofmFcTaHaIf+MqAR0OZS7e30X4jxbDPJl+taCe8kLGJ5rVOrKeuU0sGyHyHyqW87AIR6jqc4KODl4WQJ4SsAA== + version "14.3.1" + resolved "https://registry.yarnpkg.com/socketcluster-client/-/socketcluster-client-14.3.1.tgz#bfc3591c0cad2668e7b3512a102f3844f5f2e84d" + integrity sha512-Sd/T0K/9UlqTfz+HUuFq90dshA5OBJPQbdkRzGtcKIOm52fkdsBTt0FYpiuzzxv5VrU7PWpRm6KIfNXyPwlLpw== dependencies: buffer "^5.2.1" clone "2.1.1" @@ -23232,10 +23209,10 @@ socketcluster-client@^14.2.1: linked-list "0.1.0" querystring "0.2.0" sc-channel "^1.2.0" - sc-errors "^1.4.1" + sc-errors "^2.0.1" sc-formatter "^3.0.1" uuid "3.2.1" - ws "5.1.1" + ws "7.1.0" socketcluster-server@^14.3.2: version "14.4.0" @@ -24230,15 +24207,10 @@ swarm-js@^0.1.40: tar "^4.0.2" xhr-request "^1.0.1" -symbol-observable@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" - integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= - -symbol-observable@^1.0.2, symbol-observable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== +symbol-observable@1.0.1, symbol-observable@^1.2.0, symbol-observable@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-2.0.3.tgz#5b521d3d07a43c351055fa43b8355b62d33fd16a" + integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA== symbol-tree@^3.2.1: version "3.2.2" @@ -26449,13 +26421,6 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" - integrity sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw== - dependencies: - async-limiter "~1.0.0" - ws@6.1.2: version "6.1.2" resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" @@ -26463,6 +26428,13 @@ ws@6.1.2: dependencies: async-limiter "~1.0.0" +ws@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.0.tgz#0395646c6fcc3ac56abf61ce1a42039637a6bd98" + integrity sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g== + dependencies: + async-limiter "^1.0.0" + ws@7.2.3, ws@^7: version "7.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46" From f386e4ce4b198ab20dc761b256e6f7b03c0fc42d Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 9 Dec 2020 12:04:11 -0330 Subject: [PATCH 20/60] Rename `lockdown.cjs` to `lockdown.js` (#10026) When you load an extension `.zip` file in Firefox, it fails to load scripts with the `.cjs` file extension. However, it works if you load the extension via the `manifest.json` file instead. After renaming the `lockdown.cjs` file to `lockdown.js`, it works in Firefox in all cases, regardless whether it's loaded by manifest or by `.zip`. --- app/home.html | 2 +- app/manifest/_base.json | 4 ++-- app/notification.html | 2 +- app/phishing.html | 2 +- app/popup.html | 2 +- development/build/static.js | 5 ++--- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/home.html b/app/home.html index 072eddea9..d952983c9 100644 --- a/app/home.html +++ b/app/home.html @@ -12,7 +12,7 @@
- + diff --git a/app/manifest/_base.json b/app/manifest/_base.json index b9b650071..0fe5a017e 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -4,7 +4,7 @@ "scripts": [ "globalthis.js", "initSentry.js", - "lockdown.cjs", + "lockdown.js", "runLockdown.js", "bg-libs.js", "background.js" @@ -39,7 +39,7 @@ "matches": ["file://*/*", "http://*/*", "https://*/*"], "js": [ "globalthis.js", - "lockdown.cjs", + "lockdown.js", "runLockdown.js", "contentscript.js" ], diff --git a/app/notification.html b/app/notification.html index 4f424e3c0..8d82c41b2 100644 --- a/app/notification.html +++ b/app/notification.html @@ -35,7 +35,7 @@
- + diff --git a/app/phishing.html b/app/phishing.html index 59ea3ac71..7bbc11dab 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -3,7 +3,7 @@ Ethereum Phishing Detection - MetaMask - + diff --git a/app/popup.html b/app/popup.html index e73f3e4d2..d16257992 100644 --- a/app/popup.html +++ b/app/popup.html @@ -12,7 +12,7 @@
- + diff --git a/development/build/static.js b/development/build/static.js index 1fe5dc959..ee3341643 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -49,9 +49,8 @@ const copyTargets = [ dest: `globalthis.js`, }, { - src: `./node_modules/ses/dist/`, - pattern: `lockdown.cjs`, - dest: ``, + src: `./node_modules/ses/dist/lockdown.cjs`, + dest: `lockdown.js`, }, { src: `./app/scripts/`, From 8ab523011514eb2cf888f341ab9d603ec0319610 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 9 Dec 2020 15:40:33 -0330 Subject: [PATCH 21/60] Update `tweetnacl` dependencies (#10028) The `eth_decrypt` used to fail on Firefox with a recursion error. Updating these `tweetnacl` dependencies seemed to have fixed the issue the last time I tested this. When I tried to reproduce the failure today, it failed due to a different reason, both before and after this update. But nonetheless, it still seems like a good idea to update. These newer versions have no breaking changes and contain important bug fixes. --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index da2e567eb..223a57c4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24914,9 +24914,9 @@ tunnel@^0.0.6: integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== tweetnacl-util@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz#4576c1cee5e2d63d207fee52f1ba02819480bc75" - integrity sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU= + version "0.15.1" + resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" + integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" @@ -24924,9 +24924,9 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= tweetnacl@^1.0.0, tweetnacl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.1.tgz#2594d42da73cd036bd0d2a54683dd35a6b55ca17" - integrity sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A== + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" From 6304ce15a285897765baba5b2b2dc893926e040a Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 9 Dec 2020 17:02:38 -0330 Subject: [PATCH 22/60] Fix TokenList component name (#10030) The TokenList component on the `add-token` page had the name `InfoBox`, which doesn't seem applicable. It has been renamed to `TokenList`, to match the module filename and the component name we use elsewhere. --- ui/app/pages/add-token/token-list/token-list.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/add-token/token-list/token-list.component.js b/ui/app/pages/add-token/token-list/token-list.component.js index 83e9c352d..54006136e 100644 --- a/ui/app/pages/add-token/token-list/token-list.component.js +++ b/ui/app/pages/add-token/token-list/token-list.component.js @@ -4,7 +4,7 @@ import classnames from 'classnames' import { checkExistingAddresses } from '../../../helpers/utils/util' import TokenListPlaceholder from './token-list-placeholder' -export default class InfoBox extends Component { +export default class TokenList extends Component { static contextTypes = { t: PropTypes.func, } From 4587e984f5b1758cc5cd26df8d47d7347485d65b Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 10 Dec 2020 01:59:47 -0330 Subject: [PATCH 23/60] Increase Chrome minimum version (#10019) The Chrome minimum version has been increased from v58 to v63. We found that we had very few users on versions below v63, and v62 is incompatible with our SES lockdown dependency. This also makes us compatible with Object rest/spread syntax, so we might not have to transpile that anymore. I'll revisit that separately. --- app/manifest/chrome.json | 2 +- babel.config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/manifest/chrome.json b/app/manifest/chrome.json index 908d430c9..281c847a4 100644 --- a/app/manifest/chrome.json +++ b/app/manifest/chrome.json @@ -3,5 +3,5 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "58" + "minimum_chrome_version": "63" } diff --git a/babel.config.js b/babel.config.js index 9f3af9523..f340966ab 100644 --- a/babel.config.js +++ b/babel.config.js @@ -6,7 +6,7 @@ module.exports = function (api) { '@babel/preset-env', { targets: { - browsers: ['chrome >= 58', 'firefox >= 56.2'], + browsers: ['chrome >= 63', 'firefox >= 56.2'], }, }, ], From 86fba2dac1b924370c09bc6577d4041fd6082518 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 10 Dec 2020 14:03:04 -0330 Subject: [PATCH 24/60] Fix contentscript injection failure on Firefox 56 (#10034) On Firefox 56 and Waterfox Classic, our `runLockdown.js` script throws an error. This is fine on the HTML pages, as the next script tags still get run without issue (though they don't benefit from the SES lockdown sadly). But in the `contentscript`, an exception thrown here appears to halt the execution of subsequent scripts. To prevent the `contentscript` from crashing completely, lockdown errors are now caught and logged. They are also logged to Sentry on the pages where Sentry is setup. --- .eslintrc.js | 1 + app/scripts/runLockdown.js | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 184f6ce89..445ee523c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -195,6 +195,7 @@ module.exports = { 'babel.config.js', 'nyc.config.js', 'stylelint.config.js', + 'app/scripts/runLockdown.js', 'development/**/*.js', 'test/e2e/**/*.js', 'test/lib/wait-until-called.js', diff --git a/app/scripts/runLockdown.js b/app/scripts/runLockdown.js index d8584c69c..210005967 100644 --- a/app/scripts/runLockdown.js +++ b/app/scripts/runLockdown.js @@ -1,8 +1,19 @@ // Freezes all intrinsics -// eslint-disable-next-line no-undef,import/unambiguous -lockdown({ - consoleTaming: 'unsafe', - errorTaming: 'unsafe', - mathTaming: 'unsafe', - dateTaming: 'unsafe', -}) +try { + // eslint-disable-next-line no-undef,import/unambiguous + lockdown({ + consoleTaming: 'unsafe', + errorTaming: 'unsafe', + mathTaming: 'unsafe', + dateTaming: 'unsafe', + }) +} catch (error) { + // If the `lockdown` call throws an exception, it interferes with the + // contentscript injection on some versions of Firefox. The error is + // caught and logged here so that the contentscript still gets injected. + // This affects Firefox v56 and Waterfox Classic + console.error('Lockdown failed:', error) + if (window.sentry && window.sentry.captureException) { + window.sentry.captureException(error) + } +} From 4350a1422ea11da297f2adafb62b63eb2ab66d84 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 10 Dec 2020 14:28:19 -0330 Subject: [PATCH 25/60] Fix token validation in Send flow (#10045) Additional validation was added in #9907 to ensure that the "Known contract address" warning was shown when sending tokens to another token address after switching assets on the Send screen. Unfortunately this change had the unintended side-effect of preventing _all_ token sends after switching assets, so long as the recipient was not an internal address. The problem is that the `validate` function expects to be passed the address of the token send recipient in the case where a token is selected. Instead the token address was being passed to the validate function. The `query` state is now used, which should always contain the recipient address. This is the same state used in the only other place the `validate` function is called. --- ui/app/pages/send/send.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/send/send.component.js b/ui/app/pages/send/send.component.js index 4171f0611..31540e312 100644 --- a/ui/app/pages/send/send.component.js +++ b/ui/app/pages/send/send.component.js @@ -156,7 +156,7 @@ export default class SendTransactionScreen extends Component { if (sendTokenAddress && prevTokenAddress !== sendTokenAddress) { this.updateSendToken() - this.validate(sendTokenAddress) + this.validate(this.state.query) updateGas = true } From 07c5527b1e0fd710fed6c81afde1eaabe2a075fc Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 10 Dec 2020 12:02:57 -0600 Subject: [PATCH 26/60] Use consistent font size for modal top right Close links (#10000) --- .../app/gas-customization/gas-modal-page-container/index.scss | 1 + ui/app/pages/send/send.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss index fd09c01ee..be2dcc3ed 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss @@ -35,6 +35,7 @@ color: #4eade7; position: absolute; + font-size: 0.75rem; top: 4px; right: 16px; cursor: pointer; diff --git a/ui/app/pages/send/send.scss b/ui/app/pages/send/send.scss index decf358cf..e416b41b2 100644 --- a/ui/app/pages/send/send.scss +++ b/ui/app/pages/send/send.scss @@ -21,6 +21,7 @@ position: absolute; right: 1rem; width: min-content; + font-size: 0.75rem; } } From db004d4486fac0d5f3de4ff5f6b786ba5340e3c6 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 10 Dec 2020 12:02:21 -0800 Subject: [PATCH 27/60] Refactor home notification (#10046) Co-authored-by: Brad Decker --- .../home-notification.component.js | 153 +++++++----------- .../app/home-notification/index.scss | 47 ++---- 2 files changed, 69 insertions(+), 131 deletions(-) diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js index 2bb89eafe..ef91cbce8 100644 --- a/ui/app/components/app/home-notification/home-notification.component.js +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -1,101 +1,66 @@ -import React, { PureComponent } from 'react' +import React from 'react' import classnames from 'classnames' -import { Tooltip as ReactTippy } from 'react-tippy' import PropTypes from 'prop-types' import Button from '../../ui/button' +import Tooltip from '../../ui/tooltip' -export default class HomeNotification extends PureComponent { - static contextTypes = { - metricsEvent: PropTypes.func, - } - - static defaultProps = { - onAccept: null, - ignoreText: null, - onIgnore: null, - infoText: null, - } - - static propTypes = { - acceptText: PropTypes.node.isRequired, - onAccept: PropTypes.func, - ignoreText: PropTypes.node, - onIgnore: PropTypes.func, - descriptionText: PropTypes.node.isRequired, - infoText: PropTypes.node, - classNames: PropTypes.array, - } - - handleAccept = () => { - this.props.onAccept() - } - - handleIgnore = () => { - this.props.onIgnore() - } - - render() { - const { - descriptionText, - acceptText, - onAccept, - ignoreText, - onIgnore, - infoText, - classNames = [], - } = this.props - - return ( -
-
-
- -
{descriptionText}
-
- {infoText ? ( - {infoText}

- } - offset={-36} - distance={36} - animation="none" - position="top" - arrow - theme="tippy-tooltip-home" - > - -
- ) : null} -
-
- {onAccept && acceptText ? ( - - ) : null} - {onIgnore && ignoreText ? ( - - ) : null} +const HomeNotification = ({ + acceptText, + classNames = [], + descriptionText, + ignoreText, + infoText, + onAccept, + onIgnore, +}) => { + return ( +
+
+
+
{descriptionText}
+ {infoText ? ( + + + + ) : null} +
+
+ {onAccept && acceptText ? ( + + ) : null} + {onIgnore && ignoreText ? ( + + ) : null}
- ) - } +
+ ) } + +HomeNotification.propTypes = { + acceptText: PropTypes.node, + classNames: PropTypes.array, + descriptionText: PropTypes.node.isRequired, + ignoreText: PropTypes.node, + infoText: PropTypes.node, + onAccept: PropTypes.func, + onIgnore: PropTypes.func, +} + +export default HomeNotification diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss index 452681ac2..da2643a2a 100644 --- a/ui/app/components/app/home-notification/index.scss +++ b/ui/app/components/app/home-notification/index.scss @@ -1,14 +1,7 @@ -.tippy-tooltip { - // This looks weird, but its repeating the class name - // using interpolation for higher specificity. - &#{&}-home-theme { - background: rgba(36, 41, 46, 0.9); - color: $white; - border-radius: 8px; - } -} - .home-notification { + display: flex; + flex-flow: column; + justify-content: space-between; background: rgba(36, 41, 46, 0.9); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); border-radius: 8px; @@ -19,31 +12,20 @@ min-width: 472px; } - display: flex; - flex-flow: column; - justify-content: space-between; - - &__header-container { + &__content-container { display: flex; } - &__header { + &__content { display: flex; align-items: center; justify-content: space-between; } - &__icon { - height: 16px; - align-self: center; - } - &__text { @include H7; color: $white; - margin-left: 10px; - margin-right: 8px; } .fa-info-circle { @@ -102,23 +84,14 @@ &__buttons { display: flex; width: 100%; - margin-top: 10px; - justify-content: flex-start; + padding-top: 10px; + align-items: center; + justify-content: space-between; flex-direction: row-reverse; } -} -.home-notification-tooltip { - &__tooltip-container { + &__tooltip-wrapper { display: flex; - } - - &__content { - @include H7; - - color: $white; - text-align: left; - display: inline-block; - width: 200px; + margin-left: 10px; } } From 54e9c53b27823452b359e07030c41ffaf8c73974 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 10 Dec 2020 15:40:29 -0800 Subject: [PATCH 28/60] Add web3 shim usage notification (#10039) * Add web3 shim usage alert background state and logic * Cleanup alert background state, constants * Implement web3 shim usage notification and settings * nodeify alert controller background hooks * Remove svg icon, again * Tweak alert controller initialization * Add support article URL * Un-thunk alert UI "actions" * Delete connect.svg file (unused) --- app/_locales/en/messages.json | 15 +++- app/images/icons/connect.svg | 1 - app/scripts/controllers/alert.js | 68 +++++++++++++++---- .../handlers/log-web3-shim-usage.js | 18 +++-- app/scripts/metamask-controller.js | 20 ++++-- shared/constants/alerts.js | 18 +++++ .../home-notification.component.js | 43 +++++++++++- .../app/home-notification/index.scss | 36 ++++++++++ ui/app/ducks/alerts/invalid-custom-network.js | 2 +- ui/app/ducks/alerts/unconnected-account.js | 4 +- ui/app/ducks/index.js | 2 +- ui/app/ducks/metamask/metamask.js | 8 +-- ui/app/pages/home/home.component.js | 37 +++++++++- ui/app/pages/home/home.container.js | 28 +++++++- .../pages/settings/alerts-tab/alerts-tab.js | 11 +-- ui/app/selectors/permissions.js | 7 ++ ui/app/selectors/selectors.js | 4 ++ ui/app/store/actions.js | 10 +-- ui/index.js | 2 +- 19 files changed, 288 insertions(+), 46 deletions(-) delete mode 100644 app/images/icons/connect.svg create mode 100644 shared/constants/alerts.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b9b2497be..c1c969b13 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -95,7 +95,13 @@ "message": "Browsing a website with an unconnected account selected" }, "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected Web3 site, but the currently selected account is not connected." + "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." + }, + "alertSettingsWeb3ShimUsage": { + "message": "When a website tries to use the removed window.web3 API" + }, + "alertSettingsWeb3ShimUsageDescription": { + "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." }, "alerts": { "message": "Alerts" @@ -233,6 +239,9 @@ "bytes": { "message": "Bytes" }, + "canToggleInSettings": { + "message": "You can re-enable this notification in Settings -> Alerts." + }, "cancel": { "message": "Cancel" }, @@ -2095,6 +2104,10 @@ "walletSeed": { "message": "Seed phrase" }, + "web3ShimUsageNotification": { + "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Welcome to MetaMask" }, diff --git a/app/images/icons/connect.svg b/app/images/icons/connect.svg deleted file mode 100644 index ac6577090..000000000 --- a/app/images/icons/connect.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js index a2dd3a7db..4dc215f35 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -1,4 +1,8 @@ import ObservableStore from 'obs-store' +import { + TOGGLEABLE_ALERT_TYPES, + WEB3_SHIM_USAGE_ALERT_STATES, +} from '../../../shared/constants/alerts' /** * @typedef {Object} AlertControllerInitState @@ -14,14 +18,8 @@ import ObservableStore from 'obs-store' * @property {AlertControllerInitState} initState - The initial controller state */ -export const ALERT_TYPES = { - unconnectedAccount: 'unconnectedAccount', - // enumerated here but has no background state - invalidCustomNetwork: 'invalidCustomNetwork', -} - const defaultState = { - alertEnabledness: Object.keys(ALERT_TYPES).reduce( + alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( (alertEnabledness, alertType) => { alertEnabledness[alertType] = true return alertEnabledness @@ -29,11 +27,11 @@ const defaultState = { {}, ), unconnectedAccountAlertShownOrigins: {}, + web3ShimUsageOrigins: {}, } /** - * Controller responsible for maintaining - * alert related state + * Controller responsible for maintaining alert-related state. */ export default class AlertController { /** @@ -41,11 +39,13 @@ export default class AlertController { * @param {AlertControllerOptions} [opts] - Controller configuration parameters */ constructor(opts = {}) { - const { initState, preferencesStore } = opts + const { initState = {}, preferencesStore } = opts const state = { ...defaultState, - ...initState, - unconnectedAccountAlertShownOrigins: {}, + alertEnabledness: { + ...defaultState.alertEnabledness, + ...initState.alertEnabledness, + }, } this.store = new ObservableStore(state) @@ -83,4 +83,48 @@ export default class AlertController { unconnectedAccountAlertShownOrigins[origin] = true this.store.updateState({ unconnectedAccountAlertShownOrigins }) } + + /** + * Gets the web3 shim usage state for the given origin. + * + * @param {string} origin - The origin to get the web3 shim usage state for. + * @returns {undefined | 1 | 2} The web3 shim usage state for the given + * origin, or undefined. + */ + getWeb3ShimUsageState(origin) { + return this.store.getState().web3ShimUsageOrigins[origin] + } + + /** + * Sets the web3 shim usage state for the given origin to RECORDED. + * + * @param {string} origin - The origin the that used the web3 shim. + */ + setWeb3ShimUsageRecorded(origin) { + this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.RECORDED) + } + + /** + * Sets the web3 shim usage state for the given origin to DISMISSED. + * + * @param {string} origin - The origin that the web3 shim notification was + * dismissed for. + */ + setWeb3ShimUsageAlertDismissed(origin) { + this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.DISMISSED) + } + + /** + * @private + * @param {string} origin - The origin to set the state for. + * @param {number} value - The state value to set. + */ + _setWeb3ShimUsageState(origin, value) { + let { web3ShimUsageOrigins } = this.store.getState() + web3ShimUsageOrigins = { + ...web3ShimUsageOrigins, + } + web3ShimUsageOrigins[origin] = value + this.store.updateState({ web3ShimUsageOrigins }) + } } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js index 142e46f44..21e908000 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js @@ -13,11 +13,13 @@ const logWeb3ShimUsage = { } export default logWeb3ShimUsage -const recordedWeb3ShimUsage = {} - /** * @typedef {Object} LogWeb3ShimUsageOptions * @property {Function} sendMetrics - A function that registers a metrics event. + * @property {Function} getWeb3ShimUsageState - A function that gets web3 shim + * usage state for the given origin. + * @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim + * usage for a particular origin. */ /** @@ -27,10 +29,16 @@ const recordedWeb3ShimUsage = {} * @param {Function} end - The json-rpc-engine 'end' callback. * @param {LogWeb3ShimUsageOptions} options */ -function logWeb3ShimUsageHandler(req, res, _next, end, { sendMetrics }) { +function logWeb3ShimUsageHandler( + req, + res, + _next, + end, + { sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded }, +) { const { origin } = req - if (!recordedWeb3ShimUsage[origin]) { - recordedWeb3ShimUsage[origin] = true + if (getWeb3ShimUsageState(origin) === undefined) { + setWeb3ShimUsageRecorded(origin) sendMetrics({ event: `Website Accessed window.web3 Shim`, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0e976a994..85135f759 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -514,16 +514,16 @@ export default class MetamaskController extends EventEmitter { */ getApi() { const { + alertController, keyringController, + metaMetricsController, networkController, onboardingController, - alertController, permissionsController, preferencesController, + swapsController, threeBoxController, txController, - swapsController, - metaMetricsController, } = this return { @@ -706,8 +706,12 @@ export default class MetamaskController extends EventEmitter { alertController, ), setUnconnectedAccountAlertShown: nodeify( - this.alertController.setUnconnectedAccountAlertShown, - this.alertController, + alertController.setUnconnectedAccountAlertShown, + alertController, + ), + setWeb3ShimUsageAlertDismissed: nodeify( + alertController.setWeb3ShimUsageAlertDismissed, + alertController, ), // 3Box @@ -1979,6 +1983,12 @@ export default class MetamaskController extends EventEmitter { handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( this.preferencesController, ), + getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( + this.alertController, + ), + setWeb3ShimUsageRecorded: this.alertController.setWeb3ShimUsageRecorded.bind( + this.alertController, + ), }), ) // filter and subscription polyfills diff --git a/shared/constants/alerts.js b/shared/constants/alerts.js new file mode 100644 index 000000000..4b2bed147 --- /dev/null +++ b/shared/constants/alerts.js @@ -0,0 +1,18 @@ +export const ALERT_TYPES = { + unconnectedAccount: 'unconnectedAccount', + web3ShimUsage: 'web3ShimUsage', + invalidCustomNetwork: 'invalidCustomNetwork', +} + +/** + * Alerts that can be enabled or disabled by the user. + */ +export const TOGGLEABLE_ALERT_TYPES = [ + ALERT_TYPES.unconnectedAccount, + ALERT_TYPES.web3ShimUsage, +] + +export const WEB3_SHIM_USAGE_ALERT_STATES = { + RECORDED: 1, + DISMISSED: 2, +} diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js index ef91cbce8..0ae4d85f9 100644 --- a/ui/app/components/app/home-notification/home-notification.component.js +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -1,11 +1,14 @@ -import React from 'react' +import React, { useState } from 'react' import classnames from 'classnames' import PropTypes from 'prop-types' import Button from '../../ui/button' +import Checkbox from '../../ui/check-box' import Tooltip from '../../ui/tooltip' const HomeNotification = ({ acceptText, + checkboxText, + checkboxTooltipText, classNames = [], descriptionText, ignoreText, @@ -13,6 +16,17 @@ const HomeNotification = ({ onAccept, onIgnore, }) => { + const [checkboxState, setCheckBoxState] = useState(false) + + const checkboxElement = checkboxText && ( + setCheckBoxState((checked) => !checked)} + /> + ) + return (
@@ -43,11 +57,34 @@ const HomeNotification = ({ ) : null} + {checkboxText ? ( +
+ {checkboxTooltipText ? ( + + {checkboxElement} + + ) : ( + checkboxElement + )} + +
+ ) : null}
) @@ -55,6 +92,8 @@ const HomeNotification = ({ HomeNotification.propTypes = { acceptText: PropTypes.node, + checkboxText: PropTypes.node, + checkboxTooltipText: PropTypes.node, classNames: PropTypes.array, descriptionText: PropTypes.node.isRequired, ignoreText: PropTypes.node, diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss index da2643a2a..38173145b 100644 --- a/ui/app/components/app/home-notification/index.scss +++ b/ui/app/components/app/home-notification/index.scss @@ -28,10 +28,46 @@ color: $white; } + &__text-link { + @include H7; + + color: $primary-blue; + cursor: pointer; + } + .fa-info-circle { color: #6a737d; } + & &__checkbox-wrapper { + display: flex; + flex-direction: row; + align-items: center; + + @media screen and (max-width: 575px) { + width: 160px; + } + } + + & &__checkbox { + height: 13px; + width: 13px; + font-size: 16px; + cursor: pointer; + } + + & &__checkbox-label { + @include H7; + + color: $white; + margin-left: 10px; + margin-top: 1px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + cursor: pointer; + } + & &__ignore-button { border-color: #6a737d; box-sizing: border-box; diff --git a/ui/app/ducks/alerts/invalid-custom-network.js b/ui/app/ducks/alerts/invalid-custom-network.js index aa7ba3cdb..be2aa20e1 100644 --- a/ui/app/ducks/alerts/invalid-custom-network.js +++ b/ui/app/ducks/alerts/invalid-custom-network.js @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' import { ALERT_STATE } from './enums' // Constants diff --git a/ui/app/ducks/alerts/unconnected-account.js b/ui/app/ducks/alerts/unconnected-account.js index 58ca9a569..563af43d2 100644 --- a/ui/app/ducks/alerts/unconnected-account.js +++ b/ui/app/ducks/alerts/unconnected-account.js @@ -1,7 +1,7 @@ import { createSlice } from '@reduxjs/toolkit' import { captureException } from '@sentry/browser' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' import * as actionConstants from '../../store/actionConstants' import { addPermittedAccount, @@ -101,7 +101,7 @@ export const dismissAndDisableAlert = () => { return async (dispatch) => { try { await dispatch(disableAlertRequested()) - await dispatch(setAlertEnabledness(name, false)) + await setAlertEnabledness(name, false) await dispatch(disableAlertSucceeded()) } catch (error) { console.error(error) diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js index f414a0892..42fccab06 100644 --- a/ui/app/ducks/index.js +++ b/ui/app/ducks/index.js @@ -1,5 +1,5 @@ import { combineReducers } from 'redux' -import { ALERT_TYPES } from '../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../shared/constants/alerts' import metamaskReducer from './metamask/metamask' import localeMessagesReducer from './locale/locale' import sendReducer from './send/send.duck' diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index 61abf2559..24180f0d3 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -1,5 +1,5 @@ import * as actionConstants from '../../store/actionConstants' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' export default function reduceMetamask(state = {}, action) { const metamaskState = { @@ -375,12 +375,12 @@ export const getCurrentLocale = (state) => state.metamask.currentLocale export const getAlertEnabledness = (state) => state.metamask.alertEnabledness -export const getInvalidCustomNetworkAlertEnabledness = (state) => - getAlertEnabledness(state)[ALERT_TYPES.invalidCustomNetwork] - export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.unconnectedAccount] +export const getWeb3ShimUsageAlertEnabledness = (state) => + getAlertEnabledness(state)[ALERT_TYPES.web3ShimUsage] + export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 17ddeb3cf..92c8d685d 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -31,6 +31,8 @@ import { const LEARN_MORE_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360045129011-Intro-to-MetaMask-v8-extension' +const LEGACY_WEB3_URL = + 'https://metamask.zendesk.com/hc/en-us/articles/360053147012' export default class Home extends PureComponent { static contextTypes = { @@ -42,7 +44,7 @@ export default class Home extends PureComponent { forgottenPassword: PropTypes.bool, suggestedTokens: PropTypes.object, unconfirmedTransactionsCount: PropTypes.number, - shouldShowSeedPhraseReminder: PropTypes.bool, + shouldShowSeedPhraseReminder: PropTypes.bool.isRequired, isPopup: PropTypes.bool, isNotification: PropTypes.bool.isRequired, threeBoxSynced: PropTypes.bool, @@ -66,6 +68,10 @@ export default class Home extends PureComponent { swapsFetchParams: PropTypes.object, swapsEnabled: PropTypes.bool, isMainnet: PropTypes.bool, + shouldShowWeb3ShimUsageNotification: PropTypes.bool.isRequired, + setWeb3ShimUsageAlertDismissed: PropTypes.func.isRequired, + originOfCurrentTab: PropTypes.string, + disableWeb3ShimUsageAlert: PropTypes.func.isRequired, } state = { @@ -161,10 +167,39 @@ export default class Home extends PureComponent { setShowRestorePromptToFalse, showRestorePrompt, threeBoxLastUpdated, + shouldShowWeb3ShimUsageNotification, + setWeb3ShimUsageAlertDismissed, + originOfCurrentTab, + disableWeb3ShimUsageAlert, } = this.props return ( + {shouldShowWeb3ShimUsageNotification ? ( + + global.platform.openTab({ url: LEGACY_WEB3_URL }) + } + > + {t('here')} + , + ])} + ignoreText={t('dismiss')} + onIgnore={(disable) => { + setWeb3ShimUsageAlertDismissed(originOfCurrentTab) + if (disable) { + disableWeb3ShimUsageAlert() + } + }} + checkboxText={t('dontShowThisAgain')} + checkboxTooltipText={t('canToggleInSettings')} + key="home-web3ShimUsageNotification" + /> + ) : null} {shouldShowSeedPhraseReminder ? ( { @@ -58,6 +68,14 @@ const mapStateToProps = (state) => { ? firstPermissionsRequest.metadata.id : null + const originOfCurrentTab = getOriginOfCurrentTab(state) + const shouldShowWeb3ShimUsageNotification = + isPopup && + getWeb3ShimUsageAlertEnabledness(state) && + activeTabHasPermissions(state) && + getWeb3ShimUsageStateForOrigin(state, originOfCurrentTab) === + WEB3_SHIM_USAGE_ALERT_STATES.RECORDED + return { forgottenPassword, suggestedTokens, @@ -81,6 +99,8 @@ const mapStateToProps = (state) => { swapsFetchParams: swapsState.fetchParams, showAwaitingSwapScreen: swapsState.routeState === 'awaiting', isMainnet: getIsMainnet(state), + originOfCurrentTab, + shouldShowWeb3ShimUsageNotification, } } @@ -103,6 +123,10 @@ const mapDispatchToProps = (dispatch) => ({ onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)), setSwapsWelcomeMessageHasBeenShown: () => dispatch(setSwapsWelcomeMessageHasBeenShown()), + setWeb3ShimUsageAlertDismissed: (origin) => + setWeb3ShimUsageAlertDismissed(origin), + disableWeb3ShimUsageAlert: () => + setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false), }) export default compose( diff --git a/ui/app/pages/settings/alerts-tab/alerts-tab.js b/ui/app/pages/settings/alerts-tab/alerts-tab.js index 6bc6b5327..832a4ec17 100644 --- a/ui/app/pages/settings/alerts-tab/alerts-tab.js +++ b/ui/app/pages/settings/alerts-tab/alerts-tab.js @@ -1,8 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' -import { ALERT_TYPES } from '../../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../../shared/constants/alerts' import Tooltip from '../../../components/ui/tooltip' import ToggleButton from '../../../components/ui/toggle-button' import { setAlertEnabledness } from '../../../store/actions' @@ -11,7 +11,6 @@ import { useI18nContext } from '../../../hooks/useI18nContext' const AlertSettingsEntry = ({ alertId, description, title }) => { const t = useI18nContext() - const dispatch = useDispatch() const isEnabled = useSelector((state) => getAlertEnabledness(state)[alertId]) return ( @@ -27,7 +26,7 @@ const AlertSettingsEntry = ({ alertId, description, title }) => { dispatch(setAlertEnabledness(alertId, !isEnabled))} + onToggle={() => setAlertEnabledness(alertId, !isEnabled)} value={isEnabled} /> @@ -48,6 +47,10 @@ const AlertsTab = () => { title: t('alertSettingsUnconnectedAccount'), description: t('alertSettingsUnconnectedAccountDescription'), }, + [ALERT_TYPES.web3ShimUsage]: { + title: t('alertSettingsWeb3ShimUsage'), + description: t('alertSettingsWeb3ShimUsageDescription'), + }, } return ( diff --git a/ui/app/selectors/permissions.js b/ui/app/selectors/permissions.js index 776db2234..a463d09d0 100644 --- a/ui/app/selectors/permissions.js +++ b/ui/app/selectors/permissions.js @@ -244,6 +244,13 @@ export function getPermissionsForActiveTab(state) { }) } +export function activeTabHasPermissions(state) { + const { activeTab, metamask } = state + const { domains = {} } = metamask + + return Boolean(domains[activeTab.origin]?.permissions?.length > 0) +} + export function getLastConnectedInfo(state) { const { permissionsHistory = {} } = state.metamask return Object.keys(permissionsHistory).reduce((acc, origin) => { diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index d9fd7129a..5607476a8 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -356,3 +356,7 @@ export function getIpfsGateway(state) { export function getUSDConversionRate(state) { return state.metamask.usdConversionRate } + +export function getWeb3ShimUsageStateForOrigin(state, origin) { + return state.metamask.web3ShimUsageOrigins[origin] +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 474cdda67..57accb993 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -2481,16 +2481,18 @@ export function setSwapsWelcomeMessageHasBeenShown() { } } -export function setAlertEnabledness(alertId, enabledness) { - return async () => { - await promisifiedBackground.setAlertEnabledness(alertId, enabledness) - } +export async function setAlertEnabledness(alertId, enabledness) { + await promisifiedBackground.setAlertEnabledness(alertId, enabledness) } export async function setUnconnectedAccountAlertShown(origin) { await promisifiedBackground.setUnconnectedAccountAlertShown(origin) } +export async function setWeb3ShimUsageAlertDismissed(origin) { + await promisifiedBackground.setWeb3ShimUsageAlertDismissed(origin) +} + export function loadingMethodDataStarted() { return { type: actionConstants.LOADING_METHOD_DATA_STARTED, diff --git a/ui/index.js b/ui/index.js index 192165280..6c4541f17 100644 --- a/ui/index.js +++ b/ui/index.js @@ -4,7 +4,7 @@ import { clone } from 'lodash' import React from 'react' import { render } from 'react-dom' import { getEnvironmentType } from '../app/scripts/lib/util' -import { ALERT_TYPES } from '../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../shared/constants/alerts' import { SENTRY_STATE } from '../app/scripts/lib/setupSentry' import { ENVIRONMENT_TYPE_POPUP } from '../app/scripts/lib/enums' import Root from './app/pages' From 0b7579b5d8a72e9b4a98fc4a284a184c0401f380 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Dec 2020 12:03:00 -0330 Subject: [PATCH 29/60] Update Firefox version used on CI for e2e tests (#10058) The Firefox version has been updated to the latest stable version: v83. This was required to replicate production Firefox errors we saw recently. --- .circleci/scripts/firefox-install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/scripts/firefox-install b/.circleci/scripts/firefox-install index 21766467e..0c3512114 100755 --- a/.circleci/scripts/firefox-install +++ b/.circleci/scripts/firefox-install @@ -4,7 +4,7 @@ set -e set -u set -o pipefail -FIREFOX_VERSION='70.0' +FIREFOX_VERSION='83.0' FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2" FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}" FIREFOX_PATH='/opt/firefox' From 4a5a2881d0f7c6c250502c060fe20f3f653b79c1 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Dec 2020 12:03:20 -0330 Subject: [PATCH 30/60] Update `selenium-webdriver` and `geckodriver` (#10057) Update `geckodriver` to the latest version, and `selenium-webdriver` to the second-most-recent version. Updates include various dependency updates, bug fixes, and minor features. None of the updates seem to directly affect us, aside from one new feature of `selenium-webdriver` that updates the `installAddon` function to support `.zip` files, which will be used in a subsequent PR. `selenium-webdriver` was pinned one version behind latest because the latest version caused our Chrome e2e tests to fail with a mysterious error whenever `getAttribute` was called on a WebElement. --- package.json | 4 +- yarn.lock | 126 +++++++++++++++++++-------------------------------- 2 files changed, 49 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index 519fae646..9f6b5a56a 100644 --- a/package.json +++ b/package.json @@ -235,7 +235,7 @@ "fs-extra": "^8.1.0", "ganache-cli": "^6.12.1", "ganache-core": "^2.13.1", - "geckodriver": "^1.19.1", + "geckodriver": "^1.21.0", "get-port": "^5.1.0", "gulp": "^4.0.2", "gulp-autoprefixer": "^5.0.0", @@ -277,7 +277,7 @@ "remotedev-server": "^0.3.1", "resolve-url-loader": "^3.1.2", "sass-loader": "^7.0.1", - "selenium-webdriver": "^4.0.0-alpha.5", + "selenium-webdriver": "4.0.0-alpha.7", "serve-handler": "^6.1.2", "ses": "0.11.0", "sesify": "^4.2.1", diff --git a/yarn.lock b/yarn.lock index 223a57c4a..81d6dbe6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3579,10 +3579,10 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -adm-zip@0.4.11: - version "0.4.11" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" - integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== +adm-zip@0.4.16: + version "0.4.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" + integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== aes-js@3.0.0: version "3.0.0" @@ -5700,12 +5700,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.4.6: - version "3.4.6" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" - integrity sha1-AdqNgh2HgT0ViWfnQ9X+bGLPjA8= - -bluebird@^3.3.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -6730,7 +6725,7 @@ chokidar@^3.3.0, chokidar@^3.4.1: optionalDependencies: fsevents "~2.1.2" -chownr@^1.0.1, chownr@^1.1.1: +chownr@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== @@ -11776,16 +11771,16 @@ gc-stats@^1.2.1: nan "^2.13.2" node-pre-gyp "^0.13.0" -geckodriver@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.19.1.tgz#556f95fd6451b553cec89f81f81abbefce10d6e5" - integrity sha512-xWL/+eEhQ6+t98rc1c+xVM3hshDJibXtZf9WJA3sshxq4k5L1PBwfmswyBmmlKUfBr4xuC256gLVC2RxFhiCsQ== +geckodriver@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.21.0.tgz#1f04780ebfb451ffd08fa8fddc25cc26e37ac4a2" + integrity sha512-NamdJwGIWpPiafKQIvGman95BBi/SBqHddRXAnIEpFNFCFToTW0sEA0nUckMKCBNn1DVIcLfULfyFq/sTn9bkA== dependencies: - adm-zip "0.4.11" - bluebird "3.4.6" + adm-zip "0.4.16" + bluebird "3.7.2" got "5.6.0" - https-proxy-agent "3.0.0" - tar "4.4.2" + https-proxy-agent "5.0.0" + tar "6.0.2" generate-function@^2.0.0: version "2.3.1" @@ -13148,15 +13143,7 @@ https-did-resolver@^0.1.0: did-resolver "0.0.6" xmlhttprequest "^1.8.0" -https-proxy-agent@3.0.0, https-proxy-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" - integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -https-proxy-agent@^5.0.0: +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== @@ -13164,6 +13151,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" + integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + human-standard-collectible-abi@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/human-standard-collectible-abi/-/human-standard-collectible-abi-1.0.2.tgz#077bae9ed1b0b0b82bc46932104b4b499c941aa0" @@ -15461,10 +15456,10 @@ jsx-ast-utils@^2.4.1: array-includes "^3.1.1" object.assign "^4.1.0" -jszip@^3.1.5: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" - integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== +jszip@^3.2.2: + version "3.5.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" + integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -17520,7 +17515,7 @@ minipass-pipeline@^1.2.2: dependencies: minipass "^3.0.0" -minipass@^2.2.1, minipass@^2.2.4, minipass@^2.6.4: +minipass@^2.2.1, minipass@^2.6.4: version "2.6.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.6.5.tgz#1c245f9f2897f70fd4a219066261ce6c29f80b18" integrity sha512-ewSKOPFH9blOLXx0YSE+mbrNMBFPS+11a2b03QZ+P4LVrUHW/GAlqeYC7DBknDyMWkHzrzTpDhUvy7MUxqyrPA== @@ -17535,7 +17530,7 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" -minizlib@^1.1.0, minizlib@^1.2.1: +minizlib@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.2.tgz#6f0ccc82fa53e1bf2ff145f220d2da9fa6e3a166" integrity sha512-hR3At21uSrsjjDTWrbu0IMLTpnkpv8IIMFDFaoz43Tmu4LkmAXfH44vNNzpTnf+OAQQCHrb91y/wc2J4x5XgSQ== @@ -22222,7 +22217,7 @@ rework@1.0.1: convert-source-map "^0.3.3" css "^2.0.0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -22442,7 +22437,7 @@ sass-loader@^7.0.1: neo-async "^2.5.0" pify "^3.0.0" -sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.4: +sax@^1.2.1, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -22653,15 +22648,14 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= -selenium-webdriver@^4.0.0-alpha.5: - version "4.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.5.tgz#e4683b3dbf827d70df09a7e43bf02ebad20fa7c1" - integrity sha512-hktl3DSrhzM59yLhWzDGHIX9o56DvA+cVK7Dw6FcJR6qQ4CGzkaHeXQPcdrslkWMTeq0Ci9AmCxq0EMOvm2Rkg== +selenium-webdriver@4.0.0-alpha.7: + version "4.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" + integrity sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw== dependencies: - jszip "^3.1.5" - rimraf "^2.6.3" + jszip "^3.2.2" + rimraf "^2.7.1" tmp "0.0.30" - xml2js "^0.4.19" semaphore@>=1.0.1, semaphore@^1.0.3, semaphore@^1.1.0: version "1.1.0" @@ -24293,18 +24287,17 @@ tar-stream@^2.0.0, tar-stream@^2.0.1: inherits "^2.0.3" readable-stream "^3.1.1" -tar@4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.2.tgz#60685211ba46b38847b1ae7ee1a24d744a2cd462" - integrity sha512-BfkE9CciGGgDsATqkikUHrQrraBCO+ke/1f6SFAEMnxyyfN9lxC+nW1NFWMpqH865DhHIy9vQi682gk1X7friw== +tar@6.0.2, tar@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" + integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== dependencies: - chownr "^1.0.1" - fs-minipass "^1.2.5" - minipass "^2.2.4" - minizlib "^1.1.0" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.2" + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.0" + mkdirp "^1.0.3" + yallist "^4.0.0" tar@^2.0.0: version "2.2.2" @@ -24328,18 +24321,6 @@ tar@^4, tar@^4.0.2: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" - integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.0" - mkdirp "^1.0.3" - yallist "^4.0.0" - tarn@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/tarn/-/tarn-1.1.5.tgz#7be88622e951738b9fa3fb77477309242cdddc2d" @@ -26515,19 +26496,6 @@ xml-name-validator@^2.0.1: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" integrity sha1-TYuPHszTQZqjYgYb7O9RXh5VljU= -xml2js@^0.4.19: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xmlhttprequest-ssl@~1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" From d8ec5f19f6c28a4a08fb884d718cb1c882365c82 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Dec 2020 12:54:17 -0330 Subject: [PATCH 31/60] Use `.zip` for Firefox e2e tests (#10056) The Firefox e2e tests now use the `.zip` file for testing the extension. We've found this to produce more similar results to production, compared to the old method of loading the unzipped directory. Passing in a `.zip` file to the Chrome driver didn't seem to work. I didn't investigate this further to see if it was possible, but I'm not sure it makes a difference on Chrome anyway. --- .circleci/config.yml | 23 ++++++++++++++++++++++ development/build/index.js | 1 + test/e2e/webdriver/chrome.js | 4 ++-- test/e2e/webdriver/firefox.js | 36 +++++++---------------------------- test/e2e/webdriver/index.js | 3 +-- 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cd6efe2a3..9774dee06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -168,10 +168,14 @@ jobs: - run: name: Move test build to 'dist-test' to avoid conflict with production build command: mv ./dist ./dist-test + - run: + name: Move test zips to 'builds-test' to avoid conflict with production build + command: mv ./builds ./builds-test - persist_to_workspace: root: . paths: - dist-test + - builds-test prep-build-test-metrics: docker: @@ -189,10 +193,14 @@ jobs: - run: name: Move test build to 'dist-test-metrics' to avoid conflict with production build command: mv ./dist ./dist-test-metrics + - run: + name: Move test zips to 'builds-test' to avoid conflict with production build + command: mv ./builds ./builds-test-metrics - persist_to_workspace: root: . paths: - dist-test-metrics + - builds-test-metrics prep-build-storybook: docker: @@ -265,6 +273,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds - run: name: test:e2e:chrome command: | @@ -287,6 +298,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test-metrics ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test-metrics ./builds - run: name: test:e2e:chrome:metrics command: | @@ -312,6 +326,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds - run: name: test:e2e:firefox command: | @@ -337,6 +354,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test-metrics ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test-metrics ./builds - run: name: test:e2e:firefox:metrics command: | @@ -359,6 +379,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds - run: name: Run page load benchmark command: yarn benchmark:chrome --out test-artifacts/chrome/benchmark/pageload.json diff --git a/development/build/index.js b/development/build/index.js index facf87b61..7c5aeb6a2 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -79,6 +79,7 @@ function defineAllTasks() { clean, styleTasks.prod, composeParallel(scriptTasks.test, staticTasks.prod, manifestTasks.test), + zip, ), ) diff --git a/test/e2e/webdriver/chrome.js b/test/e2e/webdriver/chrome.js index f68237601..75e031475 100644 --- a/test/e2e/webdriver/chrome.js +++ b/test/e2e/webdriver/chrome.js @@ -5,8 +5,8 @@ const chrome = require('selenium-webdriver/chrome') * A wrapper around a {@code WebDriver} instance exposing Chrome-specific functionality */ class ChromeDriver { - static async build({ extensionPath, responsive, port }) { - const args = [`load-extension=${extensionPath}`] + static async build({ responsive, port }) { + const args = [`load-extension=dist/chrome`] if (responsive) { args.push('--auto-open-devtools-for-tabs') } diff --git a/test/e2e/webdriver/firefox.js b/test/e2e/webdriver/firefox.js index 0ce300da9..63c037b02 100644 --- a/test/e2e/webdriver/firefox.js +++ b/test/e2e/webdriver/firefox.js @@ -3,7 +3,7 @@ const os = require('os') const path = require('path') const { Builder, By, until } = require('selenium-webdriver') const firefox = require('selenium-webdriver/firefox') -const { Command } = require('selenium-webdriver/lib/command') +const { version } = require('../../../app/manifest/_base.json') /** * The prefix for temporary Firefox profiles. All Firefox profiles used for e2e tests @@ -12,20 +12,16 @@ const { Command } = require('selenium-webdriver/lib/command') */ const TEMP_PROFILE_PATH_PREFIX = path.join(os.tmpdir(), 'MetaMask-Fx-Profile') -const GeckoDriverCommand = { - INSTALL_ADDON: 'install addon', -} - /** * A wrapper around a {@code WebDriver} instance exposing Firefox-specific functionality */ class FirefoxDriver { /** * Builds a {@link FirefoxDriver} instance - * @param {{extensionPath: string}} options - the options for the build + * @param {Object} options - the options for the build * @returns {Promise<{driver: !ThenableWebDriver, extensionUrl: string, extensionId: string}>} */ - static async build({ extensionPath, responsive, port }) { + static async build({ responsive, port }) { const templateProfile = fs.mkdtempSync(TEMP_PROFILE_PATH_PREFIX) const options = new firefox.Options().setProfile(templateProfile) const builder = new Builder() @@ -38,9 +34,9 @@ class FirefoxDriver { const driver = builder.build() const fxDriver = new FirefoxDriver(driver) - await fxDriver.init() - - const extensionId = await fxDriver.installExtension(extensionPath) + const extensionId = await fxDriver.installExtension( + `builds/metamask-firefox-${version}.zip`, + ) const internalExtensionId = await fxDriver.getInternalId() if (responsive) { @@ -62,31 +58,13 @@ class FirefoxDriver { this._driver = driver } - /** - * Initializes the driver - * @returns {Promise} - */ - async init() { - await this._driver - .getExecutor() - .defineCommand( - GeckoDriverCommand.INSTALL_ADDON, - 'POST', - '/session/:sessionId/moz/addon/install', - ) - } - /** * Installs the extension at the given path * @param {string} addonPath - the path to the unpacked extension or XPI * @returns {Promise} the extension ID */ async installExtension(addonPath) { - const cmd = new Command(GeckoDriverCommand.INSTALL_ADDON) - .setParameter('path', path.resolve(addonPath)) - .setParameter('temporary', true) - - return await this._driver.execute(cmd) + return await this._driver.installAddon(addonPath, true) } /** diff --git a/test/e2e/webdriver/index.js b/test/e2e/webdriver/index.js index 024478065..8a41aac8a 100644 --- a/test/e2e/webdriver/index.js +++ b/test/e2e/webdriver/index.js @@ -6,13 +6,12 @@ const FirefoxDriver = require('./firefox') async function buildWebDriver({ responsive, port } = {}) { const browser = process.env.SELENIUM_BROWSER - const extensionPath = `dist/${browser}` const { driver: seleniumDriver, extensionId, extensionUrl, - } = await buildBrowserWebDriver(browser, { extensionPath, responsive, port }) + } = await buildBrowserWebDriver(browser, { responsive, port }) await setupFetchMocking(seleniumDriver) const driver = new Driver(seleniumDriver, browser, extensionUrl) From caa5c7369756584f4cde928a9a0c0f13283b130d Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Dec 2020 14:07:13 -0330 Subject: [PATCH 32/60] Use CircleCI executors to simplify CI config (#10055) The CI config has been updated to use CircleCI executors. This allows us to define the container environments used in one place, and reuse these environment definitions between jobs. This should result in no functional changes. --- .circleci/config.yml | 95 ++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 57 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9774dee06..da1dbb654 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,19 @@ version: 2.1 +executors: + node-browsers: + docker: + - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + node-browsers-medium-plus: + docker: + - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + resource_class: medium+ + environment: + NODE_OPTIONS: --max_old_space_size=2048 + shellcheck: + docker: + - image: koalaman/shellcheck-alpine@sha256:35882cba254810c7de458528011e935ba2c4f3ebcb224275dfa7ebfa930ef294 + workflows: test_and_release: jobs: @@ -98,8 +112,7 @@ workflows: jobs: create_release_pull_request: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -112,8 +125,7 @@ jobs: .circleci/scripts/release-create-release-pr prep-deps: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - run: @@ -131,11 +143,7 @@ jobs: - build-artifacts prep-build: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - resource_class: medium+ - environment: - NODE_OPTIONS: --max_old_space_size=2048 + executor: node-browsers-medium-plus steps: - checkout - attach_workspace: @@ -153,11 +161,7 @@ jobs: - builds prep-build-test: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - resource_class: medium+ - environment: - NODE_OPTIONS: --max_old_space_size=2048 + executor: node-browsers-medium-plus steps: - checkout - attach_workspace: @@ -178,11 +182,7 @@ jobs: - builds-test prep-build-test-metrics: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - resource_class: medium+ - environment: - NODE_OPTIONS: --max_old_space_size=2048 + executor: node-browsers-medium-plus steps: - checkout - attach_workspace: @@ -203,8 +203,7 @@ jobs: - builds-test-metrics prep-build-storybook: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -218,8 +217,7 @@ jobs: - .out test-lint: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -232,8 +230,7 @@ jobs: command: yarn verify-locales --quiet test-lint-shellcheck: - docker: - - image: koalaman/shellcheck-alpine@sha256:35882cba254810c7de458528011e935ba2c4f3ebcb224275dfa7ebfa930ef294 + executor: shellcheck steps: - checkout - run: apk add --no-cache bash jq yarn @@ -242,8 +239,7 @@ jobs: command: ./development/shellcheck.sh test-lint-lockfile: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -253,8 +249,7 @@ jobs: command: yarn lint:lockfile test-deps: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -264,8 +259,7 @@ jobs: command: .circleci/scripts/yarn-audit test-e2e-chrome: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -289,8 +283,7 @@ jobs: destination: test-artifacts test-e2e-chrome-metrics: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -314,8 +307,7 @@ jobs: destination: test-artifacts test-e2e-firefox: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - run: @@ -342,8 +334,7 @@ jobs: destination: test-artifacts test-e2e-firefox-metrics: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - run: @@ -370,8 +361,7 @@ jobs: destination: test-artifacts benchmark: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -394,8 +384,7 @@ jobs: - test-artifacts job-publish-prerelease: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -426,8 +415,7 @@ jobs: command: ./development/metamaskbot-build-announce.js job-publish-release: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -444,8 +432,7 @@ jobs: command: .circleci/scripts/release-create-master-pr job-publish-storybook: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - add_ssh_keys: fingerprints: @@ -460,8 +447,7 @@ jobs: yarn storybook:deploy test-unit: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -475,8 +461,7 @@ jobs: - .nyc_output - coverage test-unit-global: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -486,8 +471,7 @@ jobs: command: yarn test:unit:global validate-source-maps: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -497,8 +481,7 @@ jobs: command: yarn validate-source-maps test-mozilla-lint: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -508,16 +491,14 @@ jobs: command: NODE_OPTIONS=--max_old_space_size=3072 yarn mozilla-lint all-tests-pass: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - run: name: All Tests Passed command: echo 'weew - everything passed!' coveralls-upload: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: From fcf75d6438d4f44db56c648e6b5551742ca3db9c Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Fri, 11 Dec 2020 18:40:34 +0100 Subject: [PATCH 33/60] Display boolean values when signing typed data (#10048) --- CHANGELOG.md | 1 + .../signature-request-message.component.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a1c8ff5..14bb517c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Current Develop Branch +- [#10048](https://github.com/MetaMask/metamask-extension/pull/10048): Display boolean values when signing typed data ## 8.1.8 Wed Dec 09 2020 - [#9992](https://github.com/MetaMask/metamask-extension/pull/9992): Improve transaction params validation diff --git a/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js index 27a3e2563..848f5b38b 100644 --- a/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js +++ b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js @@ -29,7 +29,7 @@ export default class SignatureRequestMessage extends PureComponent { this.renderNode(value) ) : ( - {value} + {`${value}`} )}
From b16737454e4c3601b9299f4fdf268c19a6ea2245 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Fri, 11 Dec 2020 12:03:51 -0600 Subject: [PATCH 34/60] Prevent metaMaskFee prop error in FeeCard (#10047) --- ui/app/pages/swaps/view-quote/view-quote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 1f5769f0c..1eeebb2cf 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -544,7 +544,7 @@ export default function ViewQuote() { tokenApprovalTextComponent={tokenApprovalTextComponent} tokenApprovalSourceTokenSymbol={sourceTokenSymbol} onTokenApprovalClick={onFeeCardTokenApprovalClick} - metaMaskFee={metaMaskFee} + metaMaskFee={String(metaMaskFee)} isBestQuote={isBestQuote} numberOfQuotes={Object.values(quotes).length} onQuotesClick={() => { From da1aae772b64cd900bb411fe83d229b16cb3ef2a Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Dec 2020 16:20:45 -0330 Subject: [PATCH 35/60] Remove coveralls (#10063) We don't look at coveralls very much. We might occasionally consult it to see a report on our code coverage, but that report is already generated entirely locally, and has been added to the MetaMask bot comment in #10061. --- .circleci/config.yml | 13 ------------- .gitignore | 1 - package.json | 2 -- yarn.lock | 23 +---------------------- 4 files changed, 1 insertion(+), 38 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index da1dbb654..37d10381a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -106,9 +106,6 @@ workflows: only: develop requires: - prep-build-storybook - - coveralls-upload: - requires: - - test-unit jobs: create_release_pull_request: @@ -496,13 +493,3 @@ jobs: - run: name: All Tests Passed command: echo 'weew - everything passed!' - - coveralls-upload: - executor: node-browsers - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Coveralls upload - command: yarn test:coveralls-upload diff --git a/.gitignore b/.gitignore index 6d11965cb..46064a64b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ ui/app/css/output/ notes.txt -.coveralls.yml .nyc_output .metamaskrc diff --git a/package.json b/package.json index 9f6b5a56a..540e0d262 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html", "test:coverage:strict": "nyc --check-coverage yarn test:unit:strict", "test:coverage:path": "nyc --check-coverage yarn test:unit:path", - "test:coveralls-upload": "if [ \"$COVERALLS_REPO_TOKEN\" ]; then nyc report --reporter=text-lcov | coveralls; fi", "ganache:start": "./development/run-ganache", "sentry:publish": "node ./development/sentry-publish.js", "lint": "prettier --check ./**/*.json && eslint . --ext js && yarn lint:styles", @@ -215,7 +214,6 @@ "chromedriver": "^79.0.0", "concurrently": "^5.2.0", "copy-webpack-plugin": "^6.0.3", - "coveralls": "^3.0.0", "cross-spawn": "^7.0.3", "css-loader": "^2.1.1", "del": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 81d6dbe6b..06950d7c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7486,17 +7486,6 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -coveralls@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.0.tgz#22ef730330538080d29b8c151dc9146afde88a99" - integrity sha512-ZppXR9y5PraUOrf/DzHJY6gzNUhXYE3b9D43xEXs4QYZ7/Oe0Gy0CS+IPKWFfvQFXB3RG9QduaQUFehzSpGAFw== - dependencies: - js-yaml "^3.6.1" - lcov-parse "^0.0.10" - log-driver "^1.2.5" - minimist "^1.2.0" - request "^2.79.0" - cp-file@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" @@ -15111,7 +15100,7 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@3.13.1, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.5.1, js-yaml@^3.6.1, js-yaml@^3.9.0: +js-yaml@3.13.1, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.5.1, js-yaml@^3.9.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -15797,11 +15786,6 @@ lcid@^3.0.0: dependencies: invert-kv "^3.0.0" -lcov-parse@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" - integrity sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM= - lead@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" @@ -16759,11 +16743,6 @@ lodash@4.17.20, lodash@=3.10.1, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lo resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -log-driver@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" - integrity sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY= - log-symbols@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" From 94f0588f0ebf9363374e098af2133ab9295b536a Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Dec 2020 16:21:00 -0330 Subject: [PATCH 36/60] Add HTML coverage report to MetaMask bot comment (#10061) The HTML code coverage report generated by `nyc` is now included in the MetaMask bot comment. It has been saved as an artifact on CircleCI. --- .circleci/config.yml | 3 +++ development/metamaskbot-build-announce.js | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 37d10381a..e54378e98 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -395,6 +395,9 @@ jobs: - store_artifacts: path: builds destination: builds + - store_artifacts: + path: coverage + destination: coverage - store_artifacts: path: test-artifacts destination: test-artifacts diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index fd514fa8d..1bea36d73 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -55,6 +55,9 @@ async function start() { }) .join(', ') + const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html` + const coverageLink = `Report` + // links to bundle browser builds const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/deps-viz/background/index.html` const depVizLink = `background` @@ -65,6 +68,7 @@ async function start() { const contentRows = [ `builds: ${buildLinks}`, `bundle viz: ${bundleLinks}`, + `code coverage: ${coverageLink}`, `dep viz: ${depVizLink}`, `all artifacts`, ] From 45b737fca0cb4f86114a7680cd4821b253cb6425 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Dec 2020 18:27:42 -0330 Subject: [PATCH 37/60] Bump ini from 1.3.5 to 1.3.7 (#10064) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 06950d7c6..f469feb97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13432,9 +13432,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== inline-source-map@~0.6.0: version "0.6.2" From 5e01602a011b524ba612ae1eef68b6bcd864e6f6 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 14 Dec 2020 11:46:42 -0330 Subject: [PATCH 38/60] Cache dependencies (#10065) Dependencies are now cached between builds, using a checksum of the `yarn.lock` file as the cache key. The `node_modules` directory and the `.har` file from the install are cached and restored, so that we ensure the record of the install is always preserved alongside the dependencies. The consolidation of the `collect-har-artifact` script was to make it easier to cache the `.har` file along with the dependencies. --- .circleci/config.yml | 11 +++++++---- .circleci/scripts/collect-har-artifact.sh | 5 ----- .circleci/scripts/deps-install.sh | 8 ++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) delete mode 100755 .circleci/scripts/collect-har-artifact.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index e54378e98..e0407f18b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,14 +125,17 @@ jobs: executor: node-browsers steps: - checkout + - restore_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} - run: name: Install deps command: | .circleci/scripts/deps-install.sh - - run: - name: Collect yarn install HAR logs - command: | - .circleci/scripts/collect-har-artifact.sh + - save_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + paths: + - node_modules/ + - build-artifacts/yarn-install-har/ - persist_to_workspace: root: . paths: diff --git a/.circleci/scripts/collect-har-artifact.sh b/.circleci/scripts/collect-har-artifact.sh deleted file mode 100755 index 7f6f1aa41..000000000 --- a/.circleci/scripts/collect-har-artifact.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -x - -mkdir -p build-artifacts/yarn-install-har -mv ./*.har build-artifacts/yarn-install-har/ diff --git a/.circleci/scripts/deps-install.sh b/.circleci/scripts/deps-install.sh index c8b15e29b..828d6e63a 100755 --- a/.circleci/scripts/deps-install.sh +++ b/.circleci/scripts/deps-install.sh @@ -7,6 +7,14 @@ set -e yarn --frozen-lockfile --ignore-scripts --har +# Move HAR file into directory with consistent name so that we can cache it +mkdir -p build-artifacts/yarn-install-har +har_files=(./*.har) +if [[ -f "${har_files[0]}" ]] +then + mv ./*.har build-artifacts/yarn-install-har/ +fi + # run each in subshell so directory change does not persist # scripts can be any of: # preinstall From 8f40d03299f86e13dc3a73e2dec20997af3405ec Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 14 Dec 2020 08:04:26 -0800 Subject: [PATCH 39/60] Add approval controller (#9401) This PR introduces the new approval controller to the extension codebase. We use it for the permissions controller's pending approval functionality. The approval controller sets us up for a new pattern of requesting and managing user confirmations in RPC methods. Along with the generic RPC method middleware, the approval controller will allow us to eliminate our message managers, and decouple various method handlers from our provider stack, making the implementations more portable between the extension and mobile. --- app/scripts/background.js | 9 +- app/scripts/controllers/permissions/enums.js | 2 + app/scripts/controllers/permissions/index.js | 94 ++++---------- app/scripts/metamask-controller.js | 8 +- .../unit/app/controllers/permissions/mocks.js | 9 +- .../permissions-controller-test.js | 116 ++---------------- .../permissions-middleware-test.js | 111 +++++++++-------- 7 files changed, 121 insertions(+), 228 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 92f55319a..af9fbed2f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -119,6 +119,7 @@ initialize().catch(log.error) * @property {number} unapprovedDecryptMsgCount - The number of messages in unapprovedDecryptMsgs. * @property {Object} unapprovedTypedMsgs - An object of messages pending approval, mapping a unique ID to the options. * @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs. + * @property {number} pendingApprovalCount - The number of pending request in the approval controller. * @property {string[]} keyringTypes - An array of unique keyring identifying strings, representing available strategies for creating accounts. * @property {Keyring[]} keyrings - An array of keyring descriptions, summarizing the accounts that are available for use, and what keyrings they belong to. * @property {string} selectedAddress - A lower case hex string of the currently selected address. @@ -394,7 +395,7 @@ function setupController(initState, initLangCode) { controller.decryptMessageManager.on('updateBadge', updateBadge) controller.encryptionPublicKeyManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge) - controller.permissionsController.permissions.subscribe(updateBadge) + controller.approvalController.subscribe(updateBadge) controller.appStateController.on('updateBadge', updateBadge) /** @@ -411,9 +412,7 @@ function setupController(initState, initLangCode) { unapprovedEncryptionPublicKeyMsgCount, } = controller.encryptionPublicKeyManager const { unapprovedTypedMessagesCount } = controller.typedMessageManager - const pendingPermissionRequests = Object.keys( - controller.permissionsController.permissions.state.permissionsRequests, - ).length + const pendingApprovalCount = controller.approvalController.getTotalApprovalCount() const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length const count = @@ -423,7 +422,7 @@ function setupController(initState, initLangCode) { unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + unapprovedTypedMessagesCount + - pendingPermissionRequests + + pendingApprovalCount + waitingForUnlockCount if (count) { label = String(count) diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index d5ca52e30..cfb595144 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -1,3 +1,5 @@ +export const APPROVAL_TYPE = 'wallet_requestPermissions' + export const WALLET_PREFIX = 'wallet_' export const HISTORY_STORE_KEY = 'permissionsHistory' diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 454bd8e56..b9d4ddd47 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -11,6 +11,7 @@ import PermissionsLogController from './permissionsLog' // Methods that do not require any permissions to use: import { + APPROVAL_TYPE, SAFE_METHODS, // methods that do not require any permissions to use WALLET_PREFIX, METADATA_STORE_KEY, @@ -22,9 +23,13 @@ import { CAVEAT_TYPES, } from './enums' +// instanbul ignore next +const noop = () => undefined + export class PermissionsController { constructor( { + approvals, getKeyringAccounts, getRestrictedMethods, getUnlockPromise, @@ -32,7 +37,6 @@ export class PermissionsController { notifyDomain, notifyAllDomains, preferences, - showPermissionRequest, } = {}, restoredPermissions = {}, restoredState = {}, @@ -47,7 +51,6 @@ export class PermissionsController { this._getUnlockPromise = getUnlockPromise this._notifyDomain = notifyDomain this._notifyAllDomains = notifyAllDomains - this._showPermissionRequest = showPermissionRequest this._isUnlocked = isUnlocked this._restrictedMethods = getRestrictedMethods({ @@ -58,8 +61,12 @@ export class PermissionsController { restrictedMethods: Object.keys(this._restrictedMethods), store: this.store, }) - this.pendingApprovals = new Map() - this.pendingApprovalOrigins = new Set() + + /** + * @type {import('@metamask/controllers').ApprovalController} + * @public + */ + this.approvals = approvals this._initializePermissions(restoredPermissions) this._lastSelectedAddress = preferences.getState().selectedAddress this.preferences = preferences @@ -139,7 +146,7 @@ export class PermissionsController { { origin }, req, res, - () => undefined, + noop, _end, ) @@ -193,13 +200,7 @@ export class PermissionsController { } const res = {} - this.permissions.providerMiddlewareFunction( - domain, - req, - res, - () => undefined, - _end, - ) + this.permissions.providerMiddlewareFunction(domain, req, res, noop, _end) function _end(_err) { const err = _err || res.error @@ -223,16 +224,16 @@ export class PermissionsController { */ async approvePermissionsRequest(approved, accounts) { const { id } = approved.metadata - const approval = this.pendingApprovals.get(id) - if (!approval) { - log.debug(`Permissions request with id '${id}' not found`) + if (!this.approvals.has({ id })) { + log.debug(`Permissions request with id '${id}' not found.`) return } try { if (Object.keys(approved.permissions).length === 0) { - approval.reject( + this.approvals.reject( + id, ethErrors.rpc.invalidRequest({ message: 'Must request at least one permission.', }), @@ -244,19 +245,18 @@ export class PermissionsController { approved.permissions, accounts, ) - approval.resolve(approved.permissions) + this.approvals.resolve(id, approved.permissions) } } catch (err) { // if finalization fails, reject the request - approval.reject( + this.approvals.reject( + id, ethErrors.rpc.invalidRequest({ message: err.message, data: err, }), ) } - - this._removePendingApproval(id) } /** @@ -267,15 +267,12 @@ export class PermissionsController { * @param {string} id - The id of the request rejected by the user */ async rejectPermissionsRequest(id) { - const approval = this.pendingApprovals.get(id) - - if (!approval) { - log.debug(`Permissions request with id '${id}' not found`) + if (!this.approvals.has({ id })) { + log.debug(`Permissions request with id '${id}' not found.`) return } - approval.reject(ethErrors.provider.userRejectedRequest()) - this._removePendingApproval(id) + this.approvals.reject(id, ethErrors.provider.userRejectedRequest()) } /** @@ -670,37 +667,6 @@ export class PermissionsController { this.notifyAccountsChanged(origin, permittedAccounts) } - /** - * Adds a pending approval. - * @param {string} id - The id of the pending approval. - * @param {string} origin - The origin of the pending approval. - * @param {Function} resolve - The function resolving the pending approval Promise. - * @param {Function} reject - The function rejecting the pending approval Promise. - */ - _addPendingApproval(id, origin, resolve, reject) { - if ( - this.pendingApprovalOrigins.has(origin) || - this.pendingApprovals.has(id) - ) { - throw new Error( - `Pending approval with id '${id}' or origin '${origin}' already exists.`, - ) - } - - this.pendingApprovals.set(id, { origin, resolve, reject }) - this.pendingApprovalOrigins.add(origin) - } - - /** - * Removes the pending approval with the given id. - * @param {string} id - The id of the pending approval to remove. - */ - _removePendingApproval(id) { - const { origin } = this.pendingApprovals.get(id) - this.pendingApprovalOrigins.delete(origin) - this.pendingApprovals.delete(id) - } - /** * A convenience method for retrieving a login object * or creating a new one if needed. @@ -735,16 +701,10 @@ export class PermissionsController { metadata: { id, origin }, } = req - if (this.pendingApprovalOrigins.has(origin)) { - throw ethErrors.rpc.resourceUnavailable( - 'Permissions request already pending; please wait.', - ) - } - - this._showPermissionRequest() - - return new Promise((resolve, reject) => { - this._addPendingApproval(id, origin, resolve, reject) + return this.approvals.addAndShowApprovalRequest({ + id, + origin, + type: APPROVAL_TYPE, }) }, }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 85135f759..f86bb1468 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -18,6 +18,7 @@ import nanoid from 'nanoid' import contractMap from '@metamask/contract-metadata' import { AddressBookController, + ApprovalController, CurrencyRateController, PhishingController, } from '@metamask/controllers' @@ -102,6 +103,10 @@ export default class MetamaskController extends EventEmitter { // next, we will initialize the controllers // controller initialization order matters + this.approvalController = new ApprovalController({ + showApprovalRequest: opts.showUserConfirmation, + }) + this.networkController = new NetworkController(initState.NetworkController) this.networkController.setInfuraProjectId(opts.infuraProjectId) @@ -225,6 +230,7 @@ export default class MetamaskController extends EventEmitter { this.permissionsController = new PermissionsController( { + approvals: this.approvalController, getKeyringAccounts: this.keyringController.getAccounts.bind( this.keyringController, ), @@ -393,8 +399,8 @@ export default class MetamaskController extends EventEmitter { PermissionsMetadata: this.permissionsController.store, ThreeBoxController: this.threeBoxController.store, SwapsController: this.swapsController.store, - // ENS Controller EnsController: this.ensController.store, + ApprovalController: this.approvalController, }) this.memStore.subscribe(this.sendUpdate.bind(this)) diff --git a/test/unit/app/controllers/permissions/mocks.js b/test/unit/app/controllers/permissions/mocks.js index d6b93dd68..a3e72e835 100644 --- a/test/unit/app/controllers/permissions/mocks.js +++ b/test/unit/app/controllers/permissions/mocks.js @@ -1,6 +1,8 @@ import { ethErrors, ERROR_CODES } from 'eth-json-rpc-errors' import deepFreeze from 'deep-freeze-strict' +import { ApprovalController } from '@metamask/controllers' + import _getRestrictedMethods from '../../../../../app/scripts/controllers/permissions/restrictedMethods' import { @@ -67,6 +69,9 @@ const getRestrictedMethods = (permController) => { */ export function getPermControllerOpts() { return { + approvals: new ApprovalController({ + showApprovalRequest: noop, + }), getKeyringAccounts: async () => [...keyringAccounts], getUnlockPromise: () => Promise.resolve(), getRestrictedMethods, @@ -423,9 +428,9 @@ export const getters = deepFreeze({ message: `Pending approval with id '${id}' or origin '${origin}' already exists.`, } }, - requestAlreadyPending: () => { + requestAlreadyPending: (origin) => { return { - message: 'Permissions request already pending; please wait.', + message: `Request of type 'wallet_requestPermissions' already pending for origin ${origin}. Please wait.`, } }, }, diff --git a/test/unit/app/controllers/permissions/permissions-controller-test.js b/test/unit/app/controllers/permissions/permissions-controller-test.js index 434ae6bd3..2eee261f1 100644 --- a/test/unit/app/controllers/permissions/permissions-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-controller-test.js @@ -1,6 +1,5 @@ import { strict as assert } from 'assert' import { find } from 'lodash' -import nanoid from 'nanoid' import sinon from 'sinon' import { @@ -13,7 +12,6 @@ import { PermissionsController } from '../../../../../app/scripts/controllers/pe import { getRequestUserApprovalHelper, grantPermissions } from './helpers' import { - noop, constants, getters, getNotifyDomain, @@ -49,6 +47,15 @@ const initPermController = (notifications = initNotifications()) => { } describe('permissions controller', function () { + describe('constructor', function () { + it('throws on undefined argument', function () { + assert.throws( + () => new PermissionsController(), + 'should throw on undefined argument', + ) + }) + }) + describe('getAccounts', function () { let permController @@ -1006,12 +1013,6 @@ describe('permissions controller', function () { }) it('does nothing if called on non-existing request', async function () { - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty on init', - ) - sinon.spy(permController, 'finalizePermissionsRequest') const request = PERMS.approvedRequest(REQUEST_IDS.a, null) @@ -1025,12 +1026,6 @@ describe('permissions controller', function () { permController.finalizePermissionsRequest.notCalled, 'should not call finalizePermissionRequest', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should still be empty after request', - ) }) it('rejects request with bad accounts param', async function () { @@ -1047,12 +1042,6 @@ describe('permissions controller', function () { await permController.approvePermissionsRequest(request, null) await rejectionPromise - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after rejection', - ) }) it('rejects request with no permissions', async function () { @@ -1069,12 +1058,6 @@ describe('permissions controller', function () { ACCOUNTS.a.permitted, ) await requestRejection - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after rejection', - ) }) it('approves valid request', async function () { @@ -1100,12 +1083,6 @@ describe('permissions controller', function () { PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted), 'should produce expected approved permissions', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after approval', - ) }) it('approves valid requests regardless of order', async function () { @@ -1161,12 +1138,6 @@ describe('permissions controller', function () { PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted), 'second request should produce expected approved permissions', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after approvals', - ) }) }) @@ -1179,22 +1150,14 @@ describe('permissions controller', function () { }) it('does nothing if called on non-existing request', async function () { - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty on init', + permController.approvals.add = sinon.fake.throws( + new Error('should not call add'), ) await assert.doesNotReject( permController.rejectPermissionsRequest(REQUEST_IDS.a), 'should not throw on non-existing request', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should still be empty after request', - ) }) it('rejects single existing request', async function () { @@ -1206,12 +1169,6 @@ describe('permissions controller', function () { await permController.rejectPermissionsRequest(REQUEST_IDS.a) await requestRejection - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after rejection', - ) }) it('rejects requests regardless of order', async function () { @@ -1235,12 +1192,6 @@ describe('permissions controller', function () { await requestRejection1 await requestRejection2 - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after approval', - ) }) }) @@ -1568,13 +1519,8 @@ describe('permissions controller', function () { }) describe('miscellanea and edge cases', function () { - let permController - - beforeEach(function () { - permController = initPermController() - }) - it('requestAccountsPermissionWithId calls _requestAccountsPermission with an explicit request ID', async function () { + const permController = initPermController() const _requestPermissions = sinon .stub(permController, '_requestPermissions') .resolves() @@ -1588,43 +1534,5 @@ describe('permissions controller', function () { ) _requestPermissions.restore() }) - - it('_addPendingApproval: should throw if adding origin twice', function () { - const id = nanoid() - const origin = DOMAINS.a - - permController._addPendingApproval(id, origin, noop, noop) - - const otherId = nanoid() - - assert.throws( - () => permController._addPendingApproval(otherId, origin, noop, noop), - ERRORS.pendingApprovals.duplicateOriginOrId(otherId, origin), - 'should throw expected error', - ) - - assert.equal( - permController.pendingApprovals.size, - 1, - 'pending approvals should have single entry', - ) - - assert.equal( - permController.pendingApprovalOrigins.size, - 1, - 'pending approval origins should have single item', - ) - - assert.deepEqual( - permController.pendingApprovals.get(id), - { origin, resolve: noop, reject: noop }, - 'pending approvals should have expected entry', - ) - - assert.ok( - permController.pendingApprovalOrigins.has(origin), - 'pending approval origins should have expected item', - ) - }) }) }) diff --git a/test/unit/app/controllers/permissions/permissions-middleware-test.js b/test/unit/app/controllers/permissions/permissions-middleware-test.js index ff4e832c9..9e2769ed6 100644 --- a/test/unit/app/controllers/permissions/permissions-middleware-test.js +++ b/test/unit/app/controllers/permissions/permissions-middleware-test.js @@ -18,6 +18,20 @@ const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters const { ACCOUNTS, DOMAINS, PERM_NAMES } = constants +const initPermController = () => { + return new PermissionsController({ + ...getPermControllerOpts(), + }) +} + +const createApprovalSpies = (permController) => { + sinon.spy(permController.approvals, '_add') +} + +const getNextApprovalId = (permController) => { + return permController.approvals._approvals.keys().next().value +} + const validatePermission = (perm, name, origin, caveats) => { assert.equal( name, @@ -36,12 +50,6 @@ const validatePermission = (perm, name, origin, caveats) => { } } -const initPermController = () => { - return new PermissionsController({ - ...getPermControllerOpts(), - }) -} - describe('permissions middleware', function () { describe('wallet_requestPermissions', function () { let permController @@ -52,6 +60,8 @@ describe('permissions middleware', function () { }) it('grants permissions on user approval', async function () { + createApprovalSpies(permController) + const aMiddleware = getPermissionsMiddleware( permController, DOMAINS.a.origin, @@ -72,13 +82,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) const approvedReq = PERMS.approvedRequest( id, PERMS.requests.eth_accounts(), @@ -150,7 +159,7 @@ describe('permissions middleware', function () { await userApprovalPromise - const id1 = permController.pendingApprovals.keys().next().value + const id1 = getNextApprovalId(permController) const approvedReq1 = PERMS.approvedRequest( id1, PERMS.requests.eth_accounts(), @@ -219,7 +228,7 @@ describe('permissions middleware', function () { await userApprovalPromise - const id2 = permController.pendingApprovals.keys().next().value + const id2 = getNextApprovalId(permController) const approvedReq2 = PERMS.approvedRequest(id2, { ...requestedPerms2 }) await permController.approvePermissionsRequest( @@ -275,6 +284,8 @@ describe('permissions middleware', function () { }) it('rejects permissions on user rejection', async function () { + createApprovalSpies(permController) + const aMiddleware = getPermissionsMiddleware( permController, DOMAINS.a.origin, @@ -298,13 +309,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) await permController.rejectPermissionsRequest(id) await requestRejection @@ -328,6 +338,8 @@ describe('permissions middleware', function () { }) it('rejects requests with unknown permissions', async function () { + createApprovalSpies(permController) + const aMiddleware = getPermissionsMiddleware( permController, DOMAINS.a.origin, @@ -349,10 +361,9 @@ describe('permissions middleware', function () { 'request should be rejected with correct error', ) - assert.equal( - permController.pendingApprovals.size, - 0, - 'perm controller should have no pending approvals', + assert.ok( + permController.approvals._add.notCalled, + 'no approval requests should have been added', ) assert.ok( @@ -367,7 +378,7 @@ describe('permissions middleware', function () { }) it('accepts only a single pending permissions request per origin', async function () { - const expectedError = ERRORS.pendingApprovals.requestAlreadyPending() + createApprovalSpies(permController) // two middlewares for two origins @@ -414,10 +425,9 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 2, - 'perm controller should have expected number of pending approvals', + assert.ok( + permController.approvals._add.calledTwice, + 'should have added two approval requests', ) // create and start processing second request for first origin, @@ -431,6 +441,10 @@ describe('permissions middleware', function () { userApprovalPromise = getUserApprovalPromise(permController) + const expectedError = ERRORS.pendingApprovals.requestAlreadyPending( + DOMAINS.a.origin, + ) + const requestApprovalFail = assert.rejects( aMiddleware(reqA2, resA2), expectedError, @@ -447,17 +461,20 @@ describe('permissions middleware', function () { 'response should have expected error and no result', ) - // first requests for both origins should remain - assert.equal( - permController.pendingApprovals.size, + permController.approvals._add.callCount, + 3, + 'should have attempted to create three pending approvals', + ) + assert.equal( + permController.approvals._approvals.size, 2, - 'perm controller should have expected number of pending approvals', + 'should only have created two pending approvals', ) // now, remaining pending requests should be approved without issue - for (const id of permController.pendingApprovals.keys()) { + for (const id of permController.approvals._approvals.keys()) { await permController.approvePermissionsRequest( PERMS.approvedRequest(id, PERMS.requests.test_method()), ) @@ -484,12 +501,6 @@ describe('permissions middleware', function () { 1, 'second origin should have single approved permission', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'perm controller should have expected number of pending approvals', - ) }) }) @@ -609,6 +620,8 @@ describe('permissions middleware', function () { }) it('requests accounts for unpermitted origin, and approves on user approval', async function () { + createApprovalSpies(permController) + const userApprovalPromise = getUserApprovalPromise(permController) const aMiddleware = getPermissionsMiddleware( @@ -626,13 +639,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) const approvedReq = PERMS.approvedRequest( id, PERMS.requests.eth_accounts(), @@ -685,6 +697,8 @@ describe('permissions middleware', function () { }) it('requests accounts for unpermitted origin, and rejects on user rejection', async function () { + createApprovalSpies(permController) + const userApprovalPromise = getUserApprovalPromise(permController) const aMiddleware = getPermissionsMiddleware( @@ -705,13 +719,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) await permController.rejectPermissionsRequest(id) await requestRejection @@ -788,7 +801,7 @@ describe('permissions middleware', function () { // this will reject because of the already pending request await assert.rejects( cMiddleware({ ...req }, {}), - ERRORS.eth_requestAccounts.requestAlreadyPending(), + ERRORS.eth_requestAccounts.requestAlreadyPending(DOMAINS.c.origin), ) // now unlock and let through the first request From 9a1548368f99918633307a88f21153a002713f6e Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 14 Dec 2020 11:03:59 -0600 Subject: [PATCH 40/60] Use Boolean for filters (#10066) --- app/scripts/controllers/incoming-transactions.js | 4 +--- .../gas-modal-page-container.container.js | 2 +- .../multiple-notifications.component.js | 2 +- .../components/ui/page-container/page-container.component.js | 2 +- ui/app/helpers/utils/conversions.util.js | 2 +- ui/app/hooks/useTokensToSearch.js | 4 +--- .../swaps-gas-customization-modal.container.js | 2 +- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 2f70d63cd..3cb942826 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -249,9 +249,7 @@ export default class IncomingTransactionsController { }) const incomingTxs = remoteTxs.filter( - (tx) => - tx.txParams.to && - tx.txParams.to.toLowerCase() === address.toLowerCase(), + (tx) => tx.txParams?.to?.toLowerCase() === address.toLowerCase(), ) incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1)) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index bdf987c8a..cacb39b09 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -321,7 +321,7 @@ function calcCustomGasLimit(customGasLimitInHex) { } function sumHexWEIsToRenderableEth(hexWEIs) { - const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes) + const hexWEIsSum = hexWEIs.filter(Boolean).reduce(addHexes) return formatETHFee( getValueFromWeiHex({ value: hexWEIsSum, diff --git a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js index e8dcebf13..9d16d1dfe 100644 --- a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js +++ b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js @@ -21,7 +21,7 @@ export default class MultipleNotifications extends PureComponent { const { showAll } = this.state const { children, classNames } = this.props - const childrenToRender = children.filter((child) => child) + const childrenToRender = children.filter(Boolean) if (childrenToRender.length === 0) { return null } diff --git a/ui/app/components/ui/page-container/page-container.component.js b/ui/app/components/ui/page-container/page-container.component.js index eb1280b3b..79126e454 100644 --- a/ui/app/components/ui/page-container/page-container.component.js +++ b/ui/app/components/ui/page-container/page-container.component.js @@ -67,7 +67,7 @@ export default class PageContainer extends PureComponent { renderActiveTabContent() { const { tabsComponent } = this.props let { children } = tabsComponent.props - children = children.filter((child) => child) + children = children.filter(Boolean) const { activeTabIndex } = this.state return children[activeTabIndex] diff --git a/ui/app/helpers/utils/conversions.util.js b/ui/app/helpers/utils/conversions.util.js index a61043dbc..b46dff744 100644 --- a/ui/app/helpers/utils/conversions.util.js +++ b/ui/app/helpers/utils/conversions.util.js @@ -183,7 +183,7 @@ export function addHexes(aHexWEI, bHexWEI) { } export function sumHexWEIs(hexWEIs) { - return hexWEIs.filter((n) => n).reduce(addHexes) + return hexWEIs.filter(Boolean).reduce(addHexes) } export function sumHexWEIsToUnformattedFiat( diff --git a/ui/app/hooks/useTokensToSearch.js b/ui/app/hooks/useTokensToSearch.js index 8df33e55e..a4e0e94f4 100644 --- a/ui/app/hooks/useTokensToSearch.js +++ b/ui/app/hooks/useTokensToSearch.js @@ -145,9 +145,7 @@ export function useTokensToSearch({ return new BigNumber(rawFiat || 0).gt(secondRawFiat || 0) ? -1 : 1 }, ) - tokensToSearchBuckets.top = tokensToSearchBuckets.top.filter( - (token) => token, - ) + tokensToSearchBuckets.top = tokensToSearchBuckets.top.filter(Boolean) return [ ...tokensToSearchBuckets.owned, ...tokensToSearchBuckets.top, diff --git a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js index d123eb3d6..deea5c4b0 100644 --- a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js +++ b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js @@ -159,7 +159,7 @@ export default connect( )(SwapsGasCustomizationModalComponent) function sumHexWEIsToRenderableEth(hexWEIs) { - const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes) + const hexWEIsSum = hexWEIs.filter(Boolean).reduce(addHexes) return formatETHFee( getValueFromWeiHex({ value: hexWEIsSum, From 69df19f1957dda85457b3c32aaf2544b086e0de4 Mon Sep 17 00:00:00 2001 From: Etienne Dusseault Date: Tue, 15 Dec 2020 03:17:13 +0800 Subject: [PATCH 41/60] Disable console in contentscript (#10040) * Maintain console logging in dev mode Co-authored-by: kumavis Co-authored-by: Erik Marks Co-authored-by: Mark Stacey --- app/manifest/_base.json | 1 + app/scripts/contentscript.js | 10 +++++----- app/scripts/disable-console.js | 9 +++++++++ development/build/scripts.js | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 app/scripts/disable-console.js diff --git a/app/manifest/_base.json b/app/manifest/_base.json index 5274261b8..416e6918a 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -38,6 +38,7 @@ { "matches": ["file://*/*", "http://*/*", "https://*/*"], "js": [ + "disable-console.js", "globalthis.js", "lockdown.js", "runLockdown.js", diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 5f5f25f9d..7e55faec4 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -38,8 +38,8 @@ function injectScript(content) { scriptTag.textContent = content container.insertBefore(scriptTag, container.children[0]) container.removeChild(scriptTag) - } catch (e) { - console.error('MetaMask provider injection failed.', e) + } catch (error) { + console.error('MetaMask: Provider injection failed.', error) } } @@ -85,7 +85,7 @@ function forwardTrafficBetweenMuxes(channelName, muxA, muxB) { const channelB = muxB.createStream(channelName) pump(channelA, channelB, channelA, (error) => console.debug( - `MetaMask muxed traffic for channel "${channelName}" failed.`, + `MetaMask: Muxed traffic for channel "${channelName}" failed.`, error, ), ) @@ -99,7 +99,7 @@ function forwardTrafficBetweenMuxes(channelName, muxA, muxB) { */ function logStreamDisconnectWarning(remoteLabel, error) { console.debug( - `MetaMask Contentscript: Lost connection to "${remoteLabel}".`, + `MetaMask: Content script lost connection to "${remoteLabel}".`, error, ) } @@ -223,7 +223,7 @@ function blockedDomainCheck() { * Redirects the current page to a phishing information page */ function redirectToPhishingWarning() { - console.log('MetaMask - routing to Phishing Warning component') + console.debug('MetaMask: Routing to Phishing Warning component.') const extensionURL = extension.runtime.getURL('phishing.html') window.location.href = `${extensionURL}#${querystring.stringify({ hostname: window.location.hostname, diff --git a/app/scripts/disable-console.js b/app/scripts/disable-console.js new file mode 100644 index 000000000..bb7ce8e24 --- /dev/null +++ b/app/scripts/disable-console.js @@ -0,0 +1,9 @@ +// Disable console.log in contentscript to prevent SES/lockdown logging to external page +// eslint-disable-next-line import/unambiguous +if ( + !(typeof process !== 'undefined' && process.env.METAMASK_DEBUG) && + typeof console !== undefined +) { + console.log = () => undefined + console.info = () => undefined +} diff --git a/development/build/scripts.js b/development/build/scripts.js index 2f6b577e4..9236dbb7e 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -115,6 +115,7 @@ function createScriptTasks({ browserPlatforms, livereload }) { }), ) }) + // inpage must be built before contentscript // because inpage bundle result is included inside contentscript const contentscriptSubtask = createTask( @@ -122,6 +123,12 @@ function createScriptTasks({ browserPlatforms, livereload }) { createTaskForBuildJsExtensionContentscript({ devMode, testing }), ) + // this can run whenever + const disableConsoleSubtask = createTask( + `${taskPrefix}:disable-console`, + createTaskForBuildJsExtensionDisableConsole({ devMode }), + ) + // task for initiating livereload const initiateLiveReload = async () => { if (devMode) { @@ -142,6 +149,7 @@ function createScriptTasks({ browserPlatforms, livereload }) { const allSubtasks = [ ...standardSubtasks, contentscriptSubtask, + disableConsoleSubtask, ].map((subtask) => runInChildProcess(subtask)) // const allSubtasks = [...standardSubtasks, contentscriptSubtask].map(subtask => (subtask)) // make a parent task that runs each task in a child thread @@ -165,6 +173,16 @@ function createScriptTasks({ browserPlatforms, livereload }) { }) } + function createTaskForBuildJsExtensionDisableConsole({ devMode }) { + const filename = 'disable-console' + return bundleTask({ + label: filename, + filename: `${filename}.js`, + filepath: `./app/scripts/${filename}.js`, + devMode, + }) + } + function createTaskForBuildJsExtensionContentscript({ devMode, testing }) { const inpage = 'inpage' const contentscript = 'contentscript' From 3cabe7525f7f7c65ce24a2080337a910de605fd4 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Mon, 14 Dec 2020 15:54:33 -0600 Subject: [PATCH 42/60] fix metametrics option tracking (#10071) --- .../metametrics-opt-in/metametrics-opt-in.component.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js index 2b34662ce..0bafcf01a 100644 --- a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js +++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js @@ -105,7 +105,10 @@ export default class MetaMetricsOptIn extends Component { await setParticipateInMetaMetrics(false) try { - if (participateInMetaMetrics === true) { + if ( + participateInMetaMetrics === null || + participateInMetaMetrics === true + ) { await metricsEvent({ eventOpts: { category: 'Onboarding', @@ -128,7 +131,10 @@ export default class MetaMetricsOptIn extends Component { ) try { const metrics = [] - if (participateInMetaMetrics === false) { + if ( + participateInMetaMetrics === null || + participateInMetaMetrics === false + ) { metrics.push( metricsEvent({ eventOpts: { From eeee8852cdedbde42dfd1837831ea58ddcfb7993 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 14 Dec 2020 15:21:15 -0800 Subject: [PATCH 43/60] Add eth_getProof to safe methods (#10070) `eth_getProof` is an unpermissioned, read-only RPC method for getting account-related Merkle proofs, specified here: https://eips.ethereum.org/EIPS/eip-1186 It's been supported by major Ethereum clients, and Infura, for some time. By adding it to the safe methods list, we enable this method for our users. --- app/scripts/controllers/permissions/enums.js | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index cfb595144..510fad1b3 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -39,15 +39,11 @@ export const LOG_METHOD_TYPES = { export const LOG_LIMIT = 100 export const SAFE_METHODS = [ - 'web3_sha3', - 'web3_clientVersion', - 'net_listening', - 'net_peerCount', - 'net_version', 'eth_blockNumber', 'eth_call', 'eth_chainId', 'eth_coinbase', + 'eth_decrypt', 'eth_estimateGas', 'eth_gasPrice', 'eth_getBalance', @@ -56,9 +52,11 @@ export const SAFE_METHODS = [ 'eth_getBlockTransactionCountByHash', 'eth_getBlockTransactionCountByNumber', 'eth_getCode', + 'eth_getEncryptionPublicKey', 'eth_getFilterChanges', 'eth_getFilterLogs', 'eth_getLogs', + 'eth_getProof', 'eth_getStorageAt', 'eth_getTransactionByBlockHashAndIndex', 'eth_getTransactionByBlockNumberAndIndex', @@ -79,8 +77,6 @@ export const SAFE_METHODS = [ 'eth_sendRawTransaction', 'eth_sendTransaction', 'eth_sign', - 'personal_sign', - 'personal_ecRecover', 'eth_signTypedData', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -89,9 +85,14 @@ export const SAFE_METHODS = [ 'eth_submitWork', 'eth_syncing', 'eth_uninstallFilter', - 'eth_getEncryptionPublicKey', - 'eth_decrypt', + 'metamask_getProviderState', 'metamask_watchAsset', + 'net_listening', + 'net_peerCount', + 'net_version', + 'personal_ecRecover', + 'personal_sign', 'wallet_watchAsset', - 'metamask_getProviderState', + 'web3_clientVersion', + 'web3_sha3', ] From 1b19f5e7ad411b97e4bfffd22012e20851f91bb8 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Tue, 15 Dec 2020 11:16:51 -0600 Subject: [PATCH 44/60] Fix 9874 - Improve gas maximum estimation (#10043) --- ui/app/ducks/swaps/swaps.js | 17 +++++++---------- ui/app/helpers/utils/conversions.util.js | 11 ----------- ui/app/pages/swaps/swaps.util.js | 5 +++++ ui/app/pages/swaps/view-quote/view-quote.js | 14 ++++++-------- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index d09afd648..dd7fab0a1 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -39,7 +39,6 @@ import { calcGasTotal } from '../../pages/send/send.utils' import { decimalToHex, getValueFromWeiHex, - hexMax, decGWEIToHexWEI, hexToDecimal, hexWEIToDecGWEI, @@ -67,6 +66,8 @@ const GAS_PRICES_LOADING_STATES = { COMPLETED: 'COMPLETED', } +export const FALLBACK_GAS_MULTIPLIER = 1.5 + const initialState = { aggregatorMetadata: null, approveTxId: null, @@ -593,20 +594,16 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { const usedQuote = getUsedQuote(state) const usedTradeTxParams = usedQuote.trade - const estimatedGasLimit = new BigNumber( - usedQuote?.gasEstimate || decimalToHex(usedQuote?.averageGas || 0), - 16, - ) + const estimatedGasLimit = new BigNumber(usedQuote?.gasEstimate || `0x0`, 16) const estimatedGasLimitWithMultiplier = estimatedGasLimit - .times(1.4, 10) + .times(usedQuote?.gasMultiplier || FALLBACK_GAS_MULTIPLIER, 10) .round(0) .toString(16) const maxGasLimit = customSwapsGas || - hexMax( - `0x${decimalToHex(usedQuote?.maxGas || 0)}`, - estimatedGasLimitWithMultiplier, - ) + (usedQuote?.gasEstimate + ? estimatedGasLimitWithMultiplier + : usedQuote?.maxGas) const usedGasPrice = getUsedSwapsGasPrice(state) usedTradeTxParams.gas = maxGasLimit diff --git a/ui/app/helpers/utils/conversions.util.js b/ui/app/helpers/utils/conversions.util.js index b46dff744..9e0f97681 100644 --- a/ui/app/helpers/utils/conversions.util.js +++ b/ui/app/helpers/utils/conversions.util.js @@ -1,4 +1,3 @@ -import BigNumber from 'bignumber.js' import { ETH, GWEI, WEI } from '../constants/common' import { addHexPrefix } from '../../../../app/scripts/lib/util' import { @@ -163,16 +162,6 @@ export function hexWEIToDecETH(hexWEI) { }) } -export function hexMax(...hexNumbers) { - let max = hexNumbers[0] - hexNumbers.slice(1).forEach((hexNumber) => { - if (new BigNumber(hexNumber, 16).gt(max, 16)) { - max = hexNumber - } - }) - return max -} - export function addHexes(aHexWEI, bHexWEI) { return addCurrencies(aHexWEI, bHexWEI, { aBase: 16, diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index 189ff877b..e5248a76e 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -112,6 +112,11 @@ const QUOTE_VALIDATORS = [ property: 'maxGas', type: 'number', }, + { + property: 'gasEstimate', + type: 'number|undefined', + validator: (gasEstimate) => gasEstimate === undefined || gasEstimate > 0, + }, ] const TOKEN_VALIDATORS = [ diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 1eeebb2cf..3c5f7b96a 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -12,6 +12,7 @@ import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken' import { MetaMetricsContext } from '../../../contexts/metametrics.new' import FeeCard from '../fee-card' import { + FALLBACK_GAS_MULTIPLIER, getQuotes, getSelectedQuote, getApproveTxParams, @@ -57,7 +58,6 @@ import { } from '../../../helpers/utils/token-util' import { decimalToHex, - hexMax, hexToDecimal, getValueFromWeiHex, } from '../../../helpers/utils/conversions.util' @@ -123,18 +123,16 @@ export default function ViewQuote() { usedQuote?.gasEstimateWithRefund || `0x${decimalToHex(usedQuote?.averageGas || 0)}` - const gasLimitForMax = - usedQuote?.gasEstimate || `0x${decimalToHex(usedQuote?.averageGas || 0)}` + const gasLimitForMax = usedQuote?.gasEstimate || `0x0` const usedGasLimitWithMultiplier = new BigNumber(gasLimitForMax, 16) - .times(1.4, 10) + .times(usedQuote?.gasMultiplier || FALLBACK_GAS_MULTIPLIER, 10) .round(0) .toString(16) - const nonCustomMaxGasLimit = hexMax( - `0x${decimalToHex(usedQuote?.maxGas || 0)}`, - usedGasLimitWithMultiplier, - ) + const nonCustomMaxGasLimit = usedQuote?.gasEstimate + ? usedGasLimitWithMultiplier + : `0x${decimalToHex(usedQuote?.maxGas || 0)}` const maxGasLimit = customMaxGas || nonCustomMaxGasLimit const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice) From 6c637bba9c4c21e2db895507afdd8baee234f54a Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 15 Dec 2020 16:51:13 -0330 Subject: [PATCH 45/60] Fix `fetch-with-cache` handling of interwoven requests (#10079) A data race was introduced in #9919 when the old synchronous storage API was replaced with an async storage API. The problem arises when `fetchWithCache` is called a second time while it's still processing another call. In this case, the `cachedFetch` object can become stale while blocked waiting for a fetch response, and result in a cache being overwritten unintentionally. See this example (options omitted for simplicity, and assuming an empty initial cache): ``` await Promise.all([ fetchWithCache('https://metamask.io/foo'), fetchWithCache('https://metamask.io/bar'), ] ``` The order of events could be as follows: 1. Empty cache retrieved for `/foo` route 2. Empty cache retrieved for `/bar` route 3. Call made to `/foo` route 4. Call made to `/bar` route 5. `/foo` response is added to the empty cache object retrieved in step 1, then is saved in the cache. 6. `/bar` response is added to the empty cache object retrieved in step 2, then is saved in the cache. In step 6, the cache object saved would not contain the `/foo` response set in step 5. As a result, `/foo` would never be cached. This problem was resolved by embedding the URL being cached directly in the cache key. This prevents simultaneous responses from overwriting each others caches. Technically a data race still exists when handing simultaneous responses to the same route, but the result would be that the last call to finish would overwrite the previous. This seems acceptable. --- ui/app/helpers/utils/fetch-with-cache.js | 9 ++-- ui/app/helpers/utils/fetch-with-cache.test.js | 51 ++++++++++++++++--- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/ui/app/helpers/utils/fetch-with-cache.js b/ui/app/helpers/utils/fetch-with-cache.js index 351ee4ab9..ca2bbd7f0 100644 --- a/ui/app/helpers/utils/fetch-with-cache.js +++ b/ui/app/helpers/utils/fetch-with-cache.js @@ -16,7 +16,6 @@ const fetchWithCache = async ( fetchOptions.headers = new window.Headers(fetchOptions.headers) } if ( - fetchOptions.headers && fetchOptions.headers.has('Content-Type') && fetchOptions.headers.get('Content-Type') !== 'application/json' ) { @@ -24,8 +23,8 @@ const fetchWithCache = async ( } const currentTime = Date.now() - const cachedFetch = (await getStorageItem('cachedFetch')) || {} - const { cachedResponse, cachedTime } = cachedFetch[url] || {} + const cacheKey = `cachedFetch:${url}` + const { cachedResponse, cachedTime } = (await getStorageItem(cacheKey)) || {} if (cachedResponse && currentTime - cachedTime < cacheRefreshTime) { return cachedResponse } @@ -48,8 +47,8 @@ const fetchWithCache = async ( cachedResponse: responseJson, cachedTime: currentTime, } - cachedFetch[url] = cacheEntry - await setStorageItem('cachedFetch', cachedFetch) + + await setStorageItem(cacheKey, cacheEntry) return responseJson } diff --git a/ui/app/helpers/utils/fetch-with-cache.test.js b/ui/app/helpers/utils/fetch-with-cache.test.js index 43336a85e..3c98cadba 100644 --- a/ui/app/helpers/utils/fetch-with-cache.test.js +++ b/ui/app/helpers/utils/fetch-with-cache.test.js @@ -37,10 +37,8 @@ describe('Fetch with cache', function () { .reply(200, '{"average": 2}') fakeStorage.getStorageItem.returns({ - 'https://fetchwithcache.metamask.io/price': { - cachedResponse: { average: 1 }, - cachedTime: Date.now(), - }, + cachedResponse: { average: 1 }, + cachedTime: Date.now(), }) const response = await fetchWithCache( @@ -57,10 +55,8 @@ describe('Fetch with cache', function () { .reply(200, '{"average": 3}') fakeStorage.getStorageItem.returns({ - 'https://fetchwithcache.metamask.io/cached': { - cachedResponse: { average: 1 }, - cachedTime: Date.now() - 1000, - }, + cachedResponse: { average: 1 }, + cachedTime: Date.now() - 1000, }) const response = await fetchWithCache( @@ -135,4 +131,43 @@ describe('Fetch with cache', function () { { message: 'fetchWithCache only supports JSON responses' }, ) }) + + it('should correctly cache responses from interwoven requests', async function () { + nock('https://fetchwithcache.metamask.io') + .get('/foo') + .reply(200, '{"average": 9}') + nock('https://fetchwithcache.metamask.io') + .get('/bar') + .reply(200, '{"average": 9}') + + const testCache = {} + fakeStorage.getStorageItem.callsFake((key) => testCache[key]) + fakeStorage.setStorageItem.callsFake((key, value) => { + testCache[key] = value + }) + + await Promise.all([ + fetchWithCache( + 'https://fetchwithcache.metamask.io/foo', + {}, + { cacheRefreshTime: 123 }, + ), + fetchWithCache( + 'https://fetchwithcache.metamask.io/bar', + {}, + { cacheRefreshTime: 123 }, + ), + ]) + + assert.deepStrictEqual( + testCache['cachedFetch:https://fetchwithcache.metamask.io/foo'] + .cachedResponse, + { average: 9 }, + ) + assert.deepStrictEqual( + testCache['cachedFetch:https://fetchwithcache.metamask.io/bar'] + .cachedResponse, + { average: 9 }, + ) + }) }) From 88525ec392ca226496ffa28bd5ce49535de424a3 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Tue, 15 Dec 2020 14:24:22 -0600 Subject: [PATCH 46/60] Fetch swap quote refresh time from API (#10069) --- app/scripts/controllers/swaps.js | 45 ++++++++++++++++--- test/unit/app/controllers/swaps-test.js | 25 +++++++++-- ui/app/ducks/swaps/swaps.js | 3 ++ .../swaps/countdown-timer/countdown-timer.js | 15 ++++--- ui/app/pages/swaps/swaps.util.js | 33 +++++++++++--- ui/app/pages/swaps/view-quote/view-quote.js | 17 +++++-- 6 files changed, 114 insertions(+), 24 deletions(-) diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index c7691dffe..e230b8d63 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -17,6 +17,7 @@ import { import { fetchTradesInfo as defaultFetchTradesInfo, fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness, + fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime, } from '../../../ui/app/pages/swaps/swaps.util' const METASWAP_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c' @@ -28,6 +29,14 @@ const MAX_GAS_LIMIT = 2500000 // 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is. const POLL_COUNT_LIMIT = 3 +// If for any reason the MetaSwap API fails to provide a refresh time, +// provide a reasonable fallback to avoid further errors +const FALLBACK_QUOTE_REFRESH_TIME = 60000 + +// This is the amount of time to wait, after successfully fetching quotes +// and their gas estimates, before fetching for new quotes +const QUOTE_POLLING_DIFFERENCE_INTERVAL = 10 * 1000 + function calculateGasEstimateWithRefund( maxGas = MAX_GAS_LIMIT, estimatedRefund = 0, @@ -42,9 +51,6 @@ function calculateGasEstimateWithRefund( return gasEstimateWithRefund } -// This is the amount of time to wait, after successfully fetching quotes and their gas estimates, before fetching for new quotes -const QUOTE_POLLING_INTERVAL = 50 * 1000 - const initialState = { swapsState: { quotes: {}, @@ -61,6 +67,7 @@ const initialState = { topAggId: null, routeState: '', swapsFeatureIsLive: false, + swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, }, } @@ -73,6 +80,7 @@ export default class SwapsController { tokenRatesStore, fetchTradesInfo = defaultFetchTradesInfo, fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness, + fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, }) { this.store = new ObservableStore({ swapsState: { ...initialState.swapsState }, @@ -80,6 +88,7 @@ export default class SwapsController { this._fetchTradesInfo = fetchTradesInfo this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness + this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime this.getBufferedGasLimit = getBufferedGasLimit this.tokenRatesStore = tokenRatesStore @@ -101,11 +110,31 @@ export default class SwapsController { this._setupSwapsLivenessFetching() } + // Sets the refresh rate for quote updates from the MetaSwap API + async _setSwapsQuoteRefreshTime() { + // Default to fallback time unless API returns valid response + let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME + try { + swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime() + } catch (e) { + console.error('Request for swaps quote refresh time failed: ', e) + } + + const { swapsState } = this.store.getState() + this.store.updateState({ + swapsState: { ...swapsState, swapsQuoteRefreshTime }, + }) + } + // Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough // that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called it, receives fetch that parameters are stored in // state. These stored parameters are used on subsequent calls made during polling. // Note: we stop polling after 3 requests, until new quotes are explicitly asked for. The logic that enforces that maximum is in the body of fetchAndSetQuotes pollForNewQuotes() { + const { + swapsState: { swapsQuoteRefreshTime }, + } = this.store.getState() + this.pollingTimeout = setTimeout(() => { const { swapsState } = this.store.getState() this.fetchAndSetQuotes( @@ -113,7 +142,7 @@ export default class SwapsController { swapsState.fetchParams?.metaData, true, ) - }, QUOTE_POLLING_INTERVAL) + }, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL) } stopPollingForQuotes() { @@ -128,7 +157,6 @@ export default class SwapsController { if (!fetchParams) { return null } - // Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params. if (!isPolledRequest) { this.pollCount = 0 @@ -144,7 +172,10 @@ export default class SwapsController { const indexOfCurrentCall = this.indexOfNewestCallInFlight + 1 this.indexOfNewestCallInFlight = indexOfCurrentCall - let newQuotes = await this._fetchTradesInfo(fetchParams) + let [newQuotes] = await Promise.all([ + this._fetchTradesInfo(fetchParams), + this._setSwapsQuoteRefreshTime(), + ]) newQuotes = mapValues(newQuotes, (quote) => ({ ...quote, @@ -422,6 +453,7 @@ export default class SwapsController { tokens: swapsState.tokens, fetchParams: swapsState.fetchParams, swapsFeatureIsLive: swapsState.swapsFeatureIsLive, + swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, }, }) clearTimeout(this.pollingTimeout) @@ -435,6 +467,7 @@ export default class SwapsController { ...initialState.swapsState, tokens: swapsState.tokens, swapsFeatureIsLive: swapsState.swapsFeatureIsLive, + swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, }, }) clearTimeout(this.pollingTimeout) diff --git a/test/unit/app/controllers/swaps-test.js b/test/unit/app/controllers/swaps-test.js index 775c53d9f..6aa3e0476 100644 --- a/test/unit/app/controllers/swaps-test.js +++ b/test/unit/app/controllers/swaps-test.js @@ -121,12 +121,14 @@ const EMPTY_INIT_STATE = { topAggId: null, routeState: '', swapsFeatureIsLive: false, + swapsQuoteRefreshTime: 60000, }, } const sandbox = sinon.createSandbox() const fetchTradesInfoStub = sandbox.stub() const fetchSwapsFeatureLivenessStub = sandbox.stub() +const fetchSwapsQuoteRefreshTimeStub = sandbox.stub() describe('SwapsController', function () { let provider @@ -140,6 +142,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, }) } @@ -639,9 +642,9 @@ describe('SwapsController', function () { const quotes = await swapsController.fetchAndSetQuotes(undefined) assert.strictEqual(quotes, null) }) - it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () { fetchTradesInfoStub.resolves(getMockQuotes()) + fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()) // Make it so approval is not required sandbox @@ -682,9 +685,9 @@ describe('SwapsController', function () { true, ) }) - it('performs the allowance check', async function () { fetchTradesInfoStub.resolves(getMockQuotes()) + fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()) // Make it so approval is not required const allowanceStub = sandbox @@ -707,6 +710,7 @@ describe('SwapsController', function () { it('gets the gas limit if approval is required', async function () { fetchTradesInfoStub.resolves(MOCK_QUOTES_APPROVAL_REQUIRED) + fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()) // Ensure approval is required sandbox @@ -732,6 +736,7 @@ describe('SwapsController', function () { it('marks the best quote', async function () { fetchTradesInfoStub.resolves(getMockQuotes()) + fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()) // Make it so approval is not required sandbox @@ -762,6 +767,7 @@ describe('SwapsController', function () { } const quotes = { ...getMockQuotes(), [bestAggId]: bestQuote } fetchTradesInfoStub.resolves(quotes) + fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()) // Make it so approval is not required sandbox @@ -779,6 +785,7 @@ describe('SwapsController', function () { it('does not mark as best quote if no conversion rate exists for destination token', async function () { fetchTradesInfoStub.resolves(getMockQuotes()) + fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()) // Make it so approval is not required sandbox @@ -805,6 +812,7 @@ describe('SwapsController', function () { assert.deepStrictEqual(swapsState, { ...EMPTY_INIT_STATE.swapsState, tokens: old.tokens, + swapsQuoteRefreshTime: old.swapsQuoteRefreshTime, }) }) @@ -850,8 +858,14 @@ describe('SwapsController', function () { const tokens = 'test' const fetchParams = 'test' const swapsFeatureIsLive = false + const swapsQuoteRefreshTime = 0 swapsController.store.updateState({ - swapsState: { tokens, fetchParams, swapsFeatureIsLive }, + swapsState: { + tokens, + fetchParams, + swapsFeatureIsLive, + swapsQuoteRefreshTime, + }, }) swapsController.resetPostFetchState() @@ -862,6 +876,7 @@ describe('SwapsController', function () { tokens, fetchParams, swapsFeatureIsLive, + swapsQuoteRefreshTime, }) }) }) @@ -1615,3 +1630,7 @@ function getTopQuoteAndSavingsBaseExpectedResults() { }, } } + +function getMockQuoteRefreshTime() { + return 45000 +} diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index dd7fab0a1..f6d36c204 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -224,6 +224,9 @@ const getSwapsState = (state) => state.metamask.swapsState export const getSwapsFeatureLiveness = (state) => state.metamask.swapsState.swapsFeatureIsLive +export const getSwapsQuoteRefreshTime = (state) => + state.metamask.swapsState.swapsQuoteRefreshTime + export const getBackgroundSwapRouteState = (state) => state.metamask.swapsState.routeState diff --git a/ui/app/pages/swaps/countdown-timer/countdown-timer.js b/ui/app/pages/swaps/countdown-timer/countdown-timer.js index 9a7362536..393a51849 100644 --- a/ui/app/pages/swaps/countdown-timer/countdown-timer.js +++ b/ui/app/pages/swaps/countdown-timer/countdown-timer.js @@ -1,11 +1,11 @@ import React, { useState, useEffect, useContext, useRef } from 'react' +import { useSelector } from 'react-redux' import PropTypes from 'prop-types' import classnames from 'classnames' import { Duration } from 'luxon' import { I18nContext } from '../../../contexts/i18n' import InfoTooltip from '../../../components/ui/info-tooltip' - -const TIMER_BASE = 60000 +import { getSwapsQuoteRefreshTime } from '../../../ducks/swaps/swaps' // Return the mm:ss start time of the countdown timer. // If time has elapsed between `timeStarted` the time current time, @@ -31,7 +31,7 @@ function timeBelowWarningTime(timer, warningTime) { export default function CountdownTimer({ timeStarted, timeOnly, - timerBase = TIMER_BASE, + timerBase, warningTime, labelKey, infoTooltipLabelKey, @@ -40,9 +40,12 @@ export default function CountdownTimer({ const intervalRef = useRef() const initialTimeStartedRef = useRef() + const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime) + const timerStart = Number(timerBase) || swapsQuoteRefreshTime + const [currentTime, setCurrentTime] = useState(() => Date.now()) const [timer, setTimer] = useState(() => - getNewTimer(currentTime, timeStarted, timerBase), + getNewTimer(currentTime, timeStarted, timerStart), ) useEffect(() => { @@ -67,14 +70,14 @@ export default function CountdownTimer({ initialTimeStartedRef.current = timeStarted const newCurrentTime = Date.now() setCurrentTime(newCurrentTime) - setTimer(getNewTimer(newCurrentTime, timeStarted, timerBase)) + setTimer(getNewTimer(newCurrentTime, timeStarted, timerStart)) clearInterval(intervalRef.current) intervalRef.current = setInterval(() => { setTimer(decreaseTimerByOne) }, 1000) } - }, [timeStarted, timer, timerBase]) + }, [timeStarted, timer, timerStart]) const formattedTimer = Duration.fromMillis(timer).toFormat('m:ss') let time diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index e5248a76e..81876d947 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -24,20 +24,24 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH = const CACHE_REFRESH_ONE_HOUR = 3600000 +const METASWAP_API_HOST = 'https://api.metaswap.codefi.network' + const getBaseApi = function (type) { switch (type) { case 'trade': - return `https://api.metaswap.codefi.network/trades?` + return `${METASWAP_API_HOST}/trades?` case 'tokens': - return `https://api.metaswap.codefi.network/tokens` + return `${METASWAP_API_HOST}/tokens` case 'topAssets': - return `https://api.metaswap.codefi.network/topAssets` + return `${METASWAP_API_HOST}/topAssets` case 'featureFlag': - return `https://api.metaswap.codefi.network/featureFlag` + return `${METASWAP_API_HOST}/featureFlag` case 'aggregatorMetadata': - return `https://api.metaswap.codefi.network/aggregatorMetadata` + return `${METASWAP_API_HOST}/aggregatorMetadata` case 'gasPrices': - return `https://api.metaswap.codefi.network/gasPrices` + return `${METASWAP_API_HOST}/gasPrices` + case 'refreshTime': + return `${METASWAP_API_HOST}/quoteRefreshRate` default: throw new Error('getBaseApi requires an api call type') } @@ -328,6 +332,23 @@ export async function fetchSwapsFeatureLiveness() { return status?.active } +export async function fetchSwapsQuoteRefreshTime() { + const response = await fetchWithCache( + getBaseApi('refreshTime'), + { method: 'GET' }, + { cacheRefreshTime: 600000 }, + ) + + // We presently use milliseconds in the UI + if (typeof response?.seconds === 'number' && response.seconds > 0) { + return response.seconds * 1000 + } + + throw new Error( + `MetaMask - refreshTime provided invalid response: ${response}`, + ) +} + export async function fetchTokenPrice(address) { const query = `contract_addresses=${address}&vs_currencies=eth` diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 3c5f7b96a..bdceee91f 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -28,6 +28,7 @@ import { signAndSendTransactions, getBackgroundSwapRouteState, swapsQuoteSelected, + getSwapsQuoteRefreshTime, } from '../../../ducks/swaps/swaps' import { conversionRateSelector, @@ -114,6 +115,7 @@ export default function ViewQuote() { const topQuote = useSelector(getTopQuote) const usedQuote = selectedQuote || topQuote const tradeValue = usedQuote?.trade?.value ?? '0x0' + const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime) const { isBestQuote } = usedQuote @@ -265,14 +267,23 @@ export default function ViewQuote() { useEffect(() => { const currentTime = Date.now() const timeSinceLastFetched = currentTime - quotesLastFetched - if (timeSinceLastFetched > 60000 && !dispatchedSafeRefetch) { + if ( + timeSinceLastFetched > swapsQuoteRefreshTime && + !dispatchedSafeRefetch + ) { setDispatchedSafeRefetch(true) dispatch(safeRefetchQuotes()) - } else if (timeSinceLastFetched > 60000) { + } else if (timeSinceLastFetched > swapsQuoteRefreshTime) { dispatch(setSwapsErrorKey(QUOTES_EXPIRED_ERROR)) history.push(SWAPS_ERROR_ROUTE) } - }, [quotesLastFetched, dispatchedSafeRefetch, dispatch, history]) + }, [ + quotesLastFetched, + dispatchedSafeRefetch, + dispatch, + history, + swapsQuoteRefreshTime, + ]) useEffect(() => { if (!originalApproveAmount && approveAmount) { From ce70c867746232da65e66bc483b50e27d956e3ab Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Wed, 16 Dec 2020 09:48:42 -0600 Subject: [PATCH 47/60] set last provider when switching to a customRPC (#10084) --- ui/app/components/app/dropdowns/network-dropdown.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index e55f77afc..a2493049d 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -40,6 +40,9 @@ function mapDispatchToProps(dispatch) { setProviderType: (type) => { dispatch(actions.setProviderType(type)) }, + setPreviousProvider: (type) => { + dispatch(actions.setPreviousProvider(type)) + }, setRpcTarget: (target, chainId, ticker, nickname) => { dispatch(actions.setRpcTarget(target, chainId, ticker, nickname)) }, @@ -82,6 +85,7 @@ class NetworkDropdown extends Component { setRpcTarget: PropTypes.func.isRequired, hideNetworkDropdown: PropTypes.func.isRequired, setNetworksTabAddMode: PropTypes.func.isRequired, + setPreviousProvider: PropTypes.func.isRequired, setSelectedSettingsRpcUrl: PropTypes.func.isRequired, frequentRpcListDetail: PropTypes.array.isRequired, networkDropdownOpen: PropTypes.bool.isRequired, @@ -112,6 +116,10 @@ class NetworkDropdown extends Component { } renderCustomRpcList(rpcListDetail, provider) { + const { + provider: { type: providerType }, + setPreviousProvider, + } = this.props const reversedRpcListDetail = rpcListDetail.slice().reverse() return reversedRpcListDetail.map((entry) => { @@ -125,6 +133,7 @@ class NetworkDropdown extends Component { closeMenu={() => this.props.hideNetworkDropdown()} onClick={() => { if (isPrefixedFormattedHexString(chainId)) { + setPreviousProvider(providerType) this.props.setRpcTarget(rpcUrl, chainId, ticker, nickname) } else { this.props.displayInvalidCustomNetworkAlert(nickname || rpcUrl) From e05be40d92c39be9041788e662e1d48b51edee1c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 16 Dec 2020 13:14:49 -0800 Subject: [PATCH 48/60] @metamask/obs-store@5.0.0 (#10092) --- app/scripts/background.js | 7 +++---- app/scripts/controllers/alert.js | 2 +- app/scripts/controllers/app-state.js | 2 +- app/scripts/controllers/cached-balances.js | 2 +- app/scripts/controllers/ens/index.js | 2 +- app/scripts/controllers/incoming-transactions.js | 2 +- app/scripts/controllers/metametrics.js | 2 +- app/scripts/controllers/network/network.js | 3 +-- app/scripts/controllers/onboarding.js | 2 +- app/scripts/controllers/permissions/index.js | 2 +- app/scripts/controllers/preferences.js | 2 +- app/scripts/controllers/swaps.js | 2 +- app/scripts/controllers/threebox.js | 2 +- app/scripts/controllers/token-rates.js | 2 +- app/scripts/controllers/transactions/index.js | 2 +- app/scripts/controllers/transactions/tx-state-manager.js | 2 +- app/scripts/lib/ComposableObservableStore.js | 2 +- app/scripts/lib/account-tracker.js | 2 +- app/scripts/lib/decrypt-message-manager.js | 2 +- app/scripts/lib/encryption-public-key-manager.js | 2 +- app/scripts/lib/message-manager.js | 2 +- app/scripts/lib/personal-message-manager.js | 2 +- app/scripts/lib/typed-message-manager.js | 2 +- package.json | 2 +- test/unit/app/ComposableObservableStore.js | 2 +- test/unit/app/controllers/detect-tokens-test.js | 2 +- test/unit/app/controllers/ens-controller-test.js | 2 +- .../permissions/permissions-log-controller-test.js | 2 +- test/unit/app/controllers/preferences-controller-test.js | 2 +- test/unit/app/controllers/swaps-test.js | 2 +- test/unit/app/controllers/token-rates-controller.js | 2 +- .../app/controllers/transactions/tx-controller-test.js | 2 +- yarn.lock | 9 +++++++++ 33 files changed, 43 insertions(+), 36 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index af9fbed2f..bba1acc4e 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -16,8 +16,7 @@ import pump from 'pump' import debounce from 'debounce-stream' import log from 'loglevel' import extension from 'extensionizer' -import storeTransform from 'obs-store/lib/transform' -import asStream from 'obs-store/lib/asStream' +import { storeAsStream, storeTransformStream } from '@metamask/obs-store' import PortStream from 'extension-port-stream' import { captureException } from '@sentry/browser' import migrations from './migrations' @@ -250,9 +249,9 @@ function setupController(initState, initLangCode) { // setup state persistence pump( - asStream(controller.store), + storeAsStream(controller.store), debounce(1000), - storeTransform(versionifyData), + storeTransformStream(versionifyData), createStreamSink(persistData), (error) => { log.error('MetaMask - Persistence pipeline failed', error) diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js index 4dc215f35..a58276d88 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { TOGGLEABLE_ALERT_TYPES, WEB3_SHIM_USAGE_ALERT_STATES, diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 738a0ef85..207ce96e4 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' export default class AppStateController extends EventEmitter { /** diff --git a/app/scripts/controllers/cached-balances.js b/app/scripts/controllers/cached-balances.js index 6e7e4eace..16ef40e88 100644 --- a/app/scripts/controllers/cached-balances.js +++ b/app/scripts/controllers/cached-balances.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' /** * @typedef {Object} CachedBalancesOptions diff --git a/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js index 766aeeb88..08647b12f 100644 --- a/app/scripts/controllers/ens/index.js +++ b/app/scripts/controllers/ens/index.js @@ -1,6 +1,6 @@ import punycode from 'punycode/punycode' import ethUtil from 'ethereumjs-util' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import Ens from './ens' diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 3cb942826..58a059dd2 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import BN from 'bn.js' import createId from '../lib/random-id' diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 23e6dfcf2..c995ea491 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -1,5 +1,5 @@ import { merge, omit } from 'lodash' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { bufferToHex, sha3 } from 'ethereumjs-util' import { ENVIRONMENT_TYPE_BACKGROUND } from '../lib/enums' import { diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 9dc605cf2..dc7b4a648 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,7 +1,6 @@ import assert from 'assert' import EventEmitter from 'events' -import ObservableStore from 'obs-store' -import ComposedStore from 'obs-store/lib/composed' +import { ComposedStore, ObservableStore } from '@metamask/obs-store' import { JsonRpcEngine } from 'json-rpc-engine' import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine' import log from 'loglevel' diff --git a/app/scripts/controllers/onboarding.js b/app/scripts/controllers/onboarding.js index 71424a1e4..011edbfc3 100644 --- a/app/scripts/controllers/onboarding.js +++ b/app/scripts/controllers/onboarding.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' /** diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index b9d4ddd47..c4ffd14fb 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -1,6 +1,6 @@ import nanoid from 'nanoid' import { JsonRpcEngine } from 'json-rpc-engine' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import { CapabilitiesController as RpcCap } from 'rpc-cap' import { ethErrors } from 'eth-json-rpc-errors' diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 12da861f6..7445be65b 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ethErrors } from 'eth-json-rpc-errors' import { normalize as normalizeAddress } from 'eth-sig-util' import { isValidAddress } from 'ethereumjs-util' diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index e230b8d63..0f599bb07 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -1,7 +1,7 @@ import { ethers } from 'ethers' import log from 'loglevel' import BigNumber from 'bignumber.js' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { mapValues, cloneDeep } from 'lodash' import abi from 'human-standard-token-abi' import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util' diff --git a/app/scripts/controllers/threebox.js b/app/scripts/controllers/threebox.js index ec73e2651..d7503b589 100644 --- a/app/scripts/controllers/threebox.js +++ b/app/scripts/controllers/threebox.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' /* eslint-disable import/first,import/order */ const Box = process.env.IN_TEST diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index b943c540f..9011b1d01 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import { normalize as normalizeAddress } from 'eth-sig-util' import ethUtil from 'ethereumjs-util' diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 4a4488fc1..670fce56f 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1,5 +1,5 @@ import EventEmitter from 'safe-event-emitter' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import Transaction from 'ethereumjs-tx' import EthQuery from 'ethjs-query' diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 456a1db98..0c94bd270 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'safe-event-emitter' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import createId from '../../lib/random-id' import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction' diff --git a/app/scripts/lib/ComposableObservableStore.js b/app/scripts/lib/ComposableObservableStore.js index a943b31b1..3661259bd 100644 --- a/app/scripts/lib/ComposableObservableStore.js +++ b/app/scripts/lib/ComposableObservableStore.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' /** * An ObservableStore that can composes a flat diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index a22d56760..850c39291 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -9,7 +9,7 @@ import EthQuery from 'eth-query' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import pify from 'pify' import Web3 from 'web3' diff --git a/app/scripts/lib/decrypt-message-manager.js b/app/scripts/lib/decrypt-message-manager.js index dc73b80f8..fc8ce03ce 100644 --- a/app/scripts/lib/decrypt-message-manager.js +++ b/app/scripts/lib/decrypt-message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import { ethErrors } from 'eth-json-rpc-errors' import log from 'loglevel' diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js index 5079c8be1..d563cae0b 100644 --- a/app/scripts/lib/encryption-public-key-manager.js +++ b/app/scripts/lib/encryption-public-key-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ethErrors } from 'eth-json-rpc-errors' import log from 'loglevel' import createId from './random-id' diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 6f6a208e2..872f97b0d 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import { ethErrors } from 'eth-json-rpc-errors' import createId from './random-id' diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 4e8b66de7..52d20588f 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import { ethErrors } from 'eth-json-rpc-errors' import log from 'loglevel' diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index c4f5fd54a..6bcfe846b 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -1,6 +1,6 @@ import EventEmitter from 'events' import assert from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ethErrors } from 'eth-json-rpc-errors' import { typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util' import { isValidAddress } from 'ethereumjs-util' diff --git a/package.json b/package.json index 540e0d262..0642a8909 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@metamask/inpage-provider": "^8.0.1", "@metamask/jazzicon": "^2.0.0", "@metamask/logo": "^2.5.0", + "@metamask/obs-store": "^5.0.0", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.3.2", "@sentry/browser": "^5.26.0", @@ -140,7 +141,6 @@ "nanoid": "^2.1.6", "nonce-tracker": "^1.0.0", "obj-multiplex": "^1.0.0", - "obs-store": "^4.0.3", "pify": "^5.0.0", "post-message-stream": "^3.0.0", "promise-to-callback": "^1.0.0", diff --git a/test/unit/app/ComposableObservableStore.js b/test/unit/app/ComposableObservableStore.js index 961be1dc0..205e0a59b 100644 --- a/test/unit/app/ComposableObservableStore.js +++ b/test/unit/app/ComposableObservableStore.js @@ -1,5 +1,5 @@ import assert from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ComposableObservableStore from '../../../app/scripts/lib/ComposableObservableStore' describe('ComposableObservableStore', function () { diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js index d54c55154..b7ab22ed6 100644 --- a/test/unit/app/controllers/detect-tokens-test.js +++ b/test/unit/app/controllers/detect-tokens-test.js @@ -1,6 +1,6 @@ import assert from 'assert' import sinon from 'sinon' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import contracts from '@metamask/contract-metadata' import BigNumber from 'bignumber.js' diff --git a/test/unit/app/controllers/ens-controller-test.js b/test/unit/app/controllers/ens-controller-test.js index 0fbaf33ac..dd116e69f 100644 --- a/test/unit/app/controllers/ens-controller-test.js +++ b/test/unit/app/controllers/ens-controller-test.js @@ -1,6 +1,6 @@ import assert from 'assert' import sinon from 'sinon' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import EnsController from '../../../../app/scripts/controllers/ens' const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/test/unit/app/controllers/permissions/permissions-log-controller-test.js b/test/unit/app/controllers/permissions/permissions-log-controller-test.js index 101f898b9..d98db8453 100644 --- a/test/unit/app/controllers/permissions/permissions-log-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-log-controller-test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import nanoid from 'nanoid' import { useFakeTimers } from 'sinon' diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 6a831c3e7..065d35782 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -1,5 +1,5 @@ import assert from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import sinon from 'sinon' import PreferencesController from '../../../../app/scripts/controllers/preferences' diff --git a/test/unit/app/controllers/swaps-test.js b/test/unit/app/controllers/swaps-test.js index 6aa3e0476..38f948088 100644 --- a/test/unit/app/controllers/swaps-test.js +++ b/test/unit/app/controllers/swaps-test.js @@ -4,7 +4,7 @@ import sinon from 'sinon' import { ethers } from 'ethers' import { mapValues } from 'lodash' import BigNumber from 'bignumber.js' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ROPSTEN_NETWORK_ID, MAINNET_NETWORK_ID, diff --git a/test/unit/app/controllers/token-rates-controller.js b/test/unit/app/controllers/token-rates-controller.js index 1f342abe5..58e0009a1 100644 --- a/test/unit/app/controllers/token-rates-controller.js +++ b/test/unit/app/controllers/token-rates-controller.js @@ -1,6 +1,6 @@ import assert from 'assert' import sinon from 'sinon' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import TokenRatesController from '../../../../app/scripts/controllers/token-rates' describe('TokenRatesController', function () { diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 0e39f791d..e37c44a47 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -2,7 +2,7 @@ import { strict as assert } from 'assert' import EventEmitter from 'events' import ethUtil from 'ethereumjs-util' import EthTx from 'ethereumjs-tx' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import sinon from 'sinon' import TransactionController from '../../../../../app/scripts/controllers/transactions' diff --git a/yarn.lock b/yarn.lock index f469feb97..0cff65506 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2145,6 +2145,15 @@ gl-mat4 "1.1.4" gl-vec3 "1.0.3" +"@metamask/obs-store@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-5.0.0.tgz#cf2213874c62186a8a4c80f0c402af62a0a877a9" + integrity sha512-9Az33hULCDVUVpBzNAdjPPgNTfs6paC38LYZv4xazt03mJt4jwWFFTP79TtF4grXfiAtUs/UZOktW4Vp+r37rw== + dependencies: + "@metamask/safe-event-emitter" "^2.0.0" + readable-stream "^2.2.2" + through2 "^2.0.3" + "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" From bba2b9646dc25e152d2de2af4f15ce9a1b24a732 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 17 Dec 2020 12:06:29 -0330 Subject: [PATCH 49/60] Update `@metamask/controllers` to v5.1.0 (#10096) This update comes with a breaking change to the Approval controller. It now requires a `defaultApprovalType` parameter. I don't think we have any use for a default approval type, but I've added a "NO_TYPE" one for now because it's a strict requirement. We should consider making this parameter optional in the future, for cases like this where it's not needed. This update will hopefully address some caching issues we've been seeing with our phishing configuration. See here for more details: https://github.com/MetaMask/controllers/pull/297 --- app/scripts/metamask-controller.js | 1 + package.json | 2 +- test/unit/app/controllers/permissions/mocks.js | 1 + yarn.lock | 15 +++++++-------- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f86bb1468..9df882bf5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -105,6 +105,7 @@ export default class MetamaskController extends EventEmitter { this.approvalController = new ApprovalController({ showApprovalRequest: opts.showUserConfirmation, + defaultApprovalType: 'NO_TYPE', }) this.networkController = new NetworkController(initState.NetworkController) diff --git a/package.json b/package.json index 0642a8909..b8d30ae3e 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@fortawesome/fontawesome-free": "^5.13.0", "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.19.0", - "@metamask/controllers": "^4.2.0", + "@metamask/controllers": "^5.1.0", "@metamask/eth-ledger-bridge-keyring": "^0.2.6", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^1.4.0", diff --git a/test/unit/app/controllers/permissions/mocks.js b/test/unit/app/controllers/permissions/mocks.js index a3e72e835..48937faf2 100644 --- a/test/unit/app/controllers/permissions/mocks.js +++ b/test/unit/app/controllers/permissions/mocks.js @@ -71,6 +71,7 @@ export function getPermControllerOpts() { return { approvals: new ApprovalController({ showApprovalRequest: noop, + defaultApprovalType: 'NO_TYPE', }), getKeyringAccounts: async () => [...keyringAccounts], getUnlockPromise: () => Promise.resolve(), diff --git a/yarn.lock b/yarn.lock index 0cff65506..467ede1b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2047,13 +2047,13 @@ web3 "^0.20.7" web3-provider-engine "^16.0.1" -"@metamask/controllers@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-4.2.0.tgz#ee304fa26aa1d291e21e7969dffee0977b149cbf" - integrity sha512-NPpBFP6x2YPEdJhjP5jC9E/WHJ+FJNWH7wf8he5/dfeY0J9hlXgkRv6vrvYI3wo6YvcgPy3IF1EOA0rR4jNbeg== +"@metamask/controllers@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-5.1.0.tgz#02c1957295bcb6db1655a716d165665d170e7f34" + integrity sha512-4piqkIrpphe+9nEy68WH+yBw9wsXZyCMVeBZeRtliVHAJFXUdz+KZDUi/R1Y+568JBzqAvsOtOzbUIU4btD3Fw== dependencies: + "@metamask/contract-metadata" "^1.19.0" await-semaphore "^0.1.3" - eth-contract-metadata "^1.11.0" eth-ens-namehash "^2.0.8" eth-json-rpc-infura "^5.1.0" eth-keyring-controller "^6.1.0" @@ -2063,14 +2063,13 @@ eth-rpc-errors "^4.0.0" eth-sig-util "^3.0.0" ethereumjs-util "^6.1.0" - ethereumjs-wallet "0.6.0" + ethereumjs-wallet "^0.6.4" ethjs-query "^0.3.8" human-standard-collectible-abi "^1.0.2" human-standard-token-abi "^2.0.0" isomorphic-fetch "^3.0.0" jsonschema "^1.2.4" nanoid "^3.1.12" - percentile "^1.2.1" single-call-balance-checker-abi "^1.0.0" uuid "^3.3.2" web3 "^0.20.7" @@ -10244,7 +10243,7 @@ ethereumjs-wallet@0.6.0: utf8 "^2.1.1" uuid "^2.0.1" -ethereumjs-wallet@0.6.5, ethereumjs-wallet@^0.6.0: +ethereumjs-wallet@0.6.5, ethereumjs-wallet@^0.6.0, ethereumjs-wallet@^0.6.4: version "0.6.5" resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.5.tgz#685e9091645cee230ad125c007658833991ed474" integrity sha512-MDwjwB9VQVnpp/Dc1XzA6J1a3wgHQ4hSvA1uWNatdpOrtCbPVuQSKSyRnjLvS0a+KKMw2pvQ9Ybqpb3+eW8oNA== From d2b6376c3d076531c20b3739b00301cc65607aca Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 17 Dec 2020 15:39:01 -0330 Subject: [PATCH 50/60] Skip reporting of successive persistence failures (#10099) Failure to persist state will now only report to Sentry if the last attempt to save state succeeded. This ensures that if anyone is stuck in a state where state can't be saved (e.g. low disk space), we aren't flooded with repeated errors on Sentry. --- app/scripts/background.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index bba1acc4e..1f8badfda 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -268,6 +268,8 @@ function setupController(initState, initLangCode) { return versionedData } + let dataPersistenceFailing = false + async function persistData(state) { if (!state) { throw new Error('MetaMask - updated state is missing') @@ -278,9 +280,15 @@ function setupController(initState, initLangCode) { if (localStore.isSupported) { try { await localStore.set(state) + if (dataPersistenceFailing) { + dataPersistenceFailing = false + } } catch (err) { // log error so we dont break the pipeline - captureException(err) + if (!dataPersistenceFailing) { + dataPersistenceFailing = true + captureException(err) + } log.error('error setting state in local store:', err) } } From 4b766fa538319270f0dfac2b0f46a55123bd195c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 17 Dec 2020 21:34:43 -0800 Subject: [PATCH 51/60] Tighten up loading indication logic (#10103) Ensures that `hideLoadingIndication` is always called in all actions that call `showLoadingIndication`. It's unclear how many of these actions were failing to hide the loading indication, because other actions superset `hideLoadingIndication`. At the very least, `updateTransaction` was probably failing to hide the loading indication in the error case. This PR also refactors a lot of actions to call `hideLoadingIndication` once in `finally` blocks as opposed to multiple times across `try` and `catch` blocks. We avoided making changes to functions using `Promise` methods, because `Promise.finally` is not supported by Waterfox, and it's not properly transpiled by Babel. --- test/unit/ui/app/actions.spec.js | 22 +-- ui/app/store/actions.js | 231 +++++++++++++++++-------------- 2 files changed, 143 insertions(+), 110 deletions(-) diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index 09ae2acba..5e9ad3909 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -218,8 +218,8 @@ describe('Actions', function () { const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] submitPasswordSpy = sinon.stub(background, 'verifySeedPhrase') @@ -368,8 +368,8 @@ describe('Actions', function () { type: 'SHOW_LOADING_INDICATION', value: 'This may take a while, please be patient.', }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] importAccountWithStrategySpy = sinon.stub( @@ -428,6 +428,7 @@ describe('Actions', function () { const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] try { @@ -465,6 +466,7 @@ describe('Actions', function () { const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] try { @@ -507,6 +509,7 @@ describe('Actions', function () { value: 'Looking for your Ledger...', }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] try { @@ -545,6 +548,7 @@ describe('Actions', function () { const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ] @@ -581,8 +585,8 @@ describe('Actions', function () { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] await store.dispatch(actions.setCurrentCurrency()) @@ -623,8 +627,8 @@ describe('Actions', function () { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] signMessageSpy = sinon.stub(background, 'signMessage') @@ -675,8 +679,8 @@ describe('Actions', function () { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] signPersonalMessageSpy = sinon.stub(background, 'signPersonalMessage') @@ -765,8 +769,8 @@ describe('Actions', function () { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] try { @@ -1005,8 +1009,8 @@ describe('Actions', function () { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] await store.dispatch(actions.setSelectedAddress()) @@ -1044,8 +1048,8 @@ describe('Actions', function () { }) const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] await store.dispatch(actions.showAccountDetail()) @@ -1426,8 +1430,8 @@ describe('Actions', function () { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, - { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, ] setCurrentLocaleSpy = sinon.stub(background, 'setCurrentLocale') setCurrentLocaleSpy.callsFake((_, callback) => { diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 57accb993..93c13f6ba 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -121,12 +121,12 @@ export function createNewVaultAndGetSeedPhrase(password) { try { await createNewVault(password) const seedWords = await verifySeedPhrase() - dispatch(hideLoadingIndication()) return seedWords } catch (error) { - dispatch(hideLoadingIndication()) dispatch(displayWarning(error.message)) throw new Error(error.message) + } finally { + dispatch(hideLoadingIndication()) } } } @@ -139,12 +139,12 @@ export function unlockAndGetSeedPhrase(password) { await submitPassword(password) const seedWords = await verifySeedPhrase() await forceUpdateMetamaskState(dispatch) - dispatch(hideLoadingIndication()) return seedWords } catch (error) { - dispatch(hideLoadingIndication()) dispatch(displayWarning(error.message)) throw new Error(error.message) + } finally { + dispatch(hideLoadingIndication()) } } } @@ -209,12 +209,12 @@ export function requestRevealSeedWords(password) { try { await verifyPassword(password) const seedWords = await verifySeedPhrase() - dispatch(hideLoadingIndication()) return seedWords } catch (error) { - dispatch(hideLoadingIndication()) dispatch(displayWarning(error.message)) throw new Error(error.message) + } finally { + dispatch(hideLoadingIndication()) } } } @@ -306,11 +306,12 @@ export function importNewAccount(strategy, args) { log.debug(`background.getState`) newState = await promisifiedBackground.getState() } catch (err) { - dispatch(hideLoadingIndication()) dispatch(displayWarning(err.message)) throw err + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch(updateMetamaskState(newState)) if (newState.selectedAddress) { dispatch({ @@ -335,11 +336,13 @@ export function addNewAccount() { } catch (error) { dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } + const newAccountAddress = Object.keys(newIdentities).find( (address) => !oldIdentities[address], ) - dispatch(hideLoadingIndication()) await forceUpdateMetamaskState(dispatch) return newAccountAddress } @@ -360,9 +363,10 @@ export function checkHardwareStatus(deviceName, hdPath) { log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) await forceUpdateMetamaskState(dispatch) return unlocked } @@ -378,9 +382,10 @@ export function forgetDevice(deviceName) { log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) await forceUpdateMetamaskState(dispatch) } } @@ -403,10 +408,11 @@ export function connectHardware(deviceName, page, hdPath) { log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) - await forceUpdateMetamaskState(dispatch) + await forceUpdateMetamaskState(dispatch) return accounts } } @@ -421,6 +427,7 @@ export function unlockHardwareWalletAccount(index, deviceName, hdPath) { deviceName, hdPath, (err) => { + dispatch(hideLoadingIndication()) if (err) { log.error(err) dispatch(displayWarning(err.message)) @@ -428,7 +435,6 @@ export function unlockHardwareWalletAccount(index, deviceName, hdPath) { return } - dispatch(hideLoadingIndication()) resolve() }, ) @@ -454,12 +460,13 @@ export function setCurrentCurrency(currencyCode) { try { data = await promisifiedBackground.setCurrentCurrency(currencyCode) } catch (error) { - dispatch(hideLoadingIndication()) log.error(error.stack) dispatch(displayWarning(error.message)) return + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch({ type: actionConstants.SET_CURRENT_FIAT, value: { @@ -480,12 +487,13 @@ export function signMsg(msgData) { try { newState = await promisifiedBackground.signMessage(msgData) } catch (error) { - dispatch(hideLoadingIndication()) log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.metamaskId)) dispatch(closeCurrentNotificationWindow()) @@ -503,12 +511,13 @@ export function signPersonalMsg(msgData) { try { newState = await promisifiedBackground.signPersonalMessage(msgData) } catch (error) { - dispatch(hideLoadingIndication()) log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.metamaskId)) dispatch(closeCurrentNotificationWindow()) @@ -547,12 +556,13 @@ export function decryptMsg(decryptedMsgData) { try { newState = await promisifiedBackground.decryptMessage(decryptedMsgData) } catch (error) { - dispatch(hideLoadingIndication()) log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch(updateMetamaskState(newState)) dispatch(completedTx(decryptedMsgData.metamaskId)) dispatch(closeCurrentNotificationWindow()) @@ -570,12 +580,13 @@ export function encryptionPublicKeyMsg(msgData) { try { newState = await promisifiedBackground.encryptionPublicKey(msgData) } catch (error) { - dispatch(hideLoadingIndication()) log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.metamaskId)) dispatch(closeCurrentNotificationWindow()) @@ -593,12 +604,13 @@ export function signTypedMsg(msgData) { try { newState = await promisifiedBackground.signTypedMessage(msgData) } catch (error) { - dispatch(hideLoadingIndication()) log.error(error) dispatch(displayWarning(error.message)) throw error + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.metamaskId)) dispatch(closeCurrentNotificationWindow()) @@ -788,14 +800,19 @@ export function updateSendEnsResolutionError(errorMessage) { } export function signTokenTx(tokenAddress, toAddress, amount, txData) { - return (dispatch) => { + return async (dispatch) => { dispatch(showLoadingIndication()) - const token = global.eth.contract(abi).at(tokenAddress) - token.transfer(toAddress, addHexPrefix(amount), txData).catch((err) => { + + try { + const token = global.eth.contract(abi).at(tokenAddress) + const txPromise = token.transfer(toAddress, addHexPrefix(amount), txData) + dispatch(showConfTxPage()) dispatch(hideLoadingIndication()) - dispatch(displayWarning(err.message)) - }) - dispatch(showConfTxPage()) + await txPromise + } catch (error) { + dispatch(hideLoadingIndication()) + dispatch(displayWarning(error.message)) + } } } @@ -815,30 +832,29 @@ const updateMetamaskStateFromBackground = () => { } export function updateTransaction(txData, dontShowLoadingIndicator) { - return (dispatch) => { + return async (dispatch) => { !dontShowLoadingIndicator && dispatch(showLoadingIndication()) - return new Promise((resolve, reject) => { - background.updateTransaction(txData, (err) => { - dispatch(updateTransactionParams(txData.id, txData.txParams)) - if (err) { - dispatch(txError(err)) - dispatch(goHome()) - log.error(err.message) - reject(err) - return - } + try { + await promisifiedBackground.updateTransaction(txData) + } catch (error) { + dispatch(updateTransactionParams(txData.id, txData.txParams)) + dispatch(hideLoadingIndication()) + dispatch(txError(error)) + dispatch(goHome()) + log.error(error.message) + throw error + } - resolve(txData) - }) - }) - .then(() => updateMetamaskStateFromBackground()) - .then((newState) => dispatch(updateMetamaskState(newState))) - .then(() => { - dispatch(showConfTxPage({ id: txData.id })) - dispatch(hideLoadingIndication()) - return txData - }) + try { + dispatch(updateTransactionParams(txData.id, txData.txParams)) + const newState = await updateMetamaskStateFromBackground() + dispatch(updateMetamaskState(newState)) + dispatch(showConfTxPage({ id: txData.id })) + return txData + } finally { + dispatch(hideLoadingIndication()) + } } } @@ -950,6 +966,7 @@ export function cancelMsg(msgData) { } finally { dispatch(hideLoadingIndication()) } + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.id)) dispatch(closeCurrentNotificationWindow()) @@ -967,6 +984,7 @@ export function cancelPersonalMsg(msgData) { } finally { dispatch(hideLoadingIndication()) } + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.id)) dispatch(closeCurrentNotificationWindow()) @@ -984,6 +1002,7 @@ export function cancelDecryptMsg(msgData) { } finally { dispatch(hideLoadingIndication()) } + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.id)) dispatch(closeCurrentNotificationWindow()) @@ -1003,6 +1022,7 @@ export function cancelEncryptionPublicKeyMsg(msgData) { } finally { dispatch(hideLoadingIndication()) } + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.id)) dispatch(closeCurrentNotificationWindow()) @@ -1020,6 +1040,7 @@ export function cancelTypedMsg(msgData) { } finally { dispatch(hideLoadingIndication()) } + dispatch(updateMetamaskState(newState)) dispatch(completedTx(msgData.id)) dispatch(closeCurrentNotificationWindow()) @@ -1031,9 +1052,9 @@ export function cancelTx(txData) { return (dispatch) => { dispatch(showLoadingIndication()) return new Promise((resolve, reject) => { - background.cancelTransaction(txData.id, (err) => { - if (err) { - reject(err) + background.cancelTransaction(txData.id, (error) => { + if (error) { + reject(error) return } @@ -1050,6 +1071,10 @@ export function cancelTx(txData) { return txData }) + .catch((error) => { + dispatch(hideLoadingIndication()) + throw error + }) } } @@ -1061,34 +1086,38 @@ export function cancelTx(txData) { export function cancelTxs(txDataList) { return async (dispatch) => { dispatch(showLoadingIndication()) - const txIds = txDataList.map(({ id }) => id) - const cancellations = txIds.map( - (id) => - new Promise((resolve, reject) => { - background.cancelTransaction(id, (err) => { - if (err) { - reject(err) - return - } - - resolve() - }) - }), - ) - await Promise.all(cancellations) - const newState = await updateMetamaskStateFromBackground() - dispatch(updateMetamaskState(newState)) - dispatch(clearSend()) + try { + const txIds = txDataList.map(({ id }) => id) + const cancellations = txIds.map( + (id) => + new Promise((resolve, reject) => { + background.cancelTransaction(id, (err) => { + if (err) { + reject(err) + return + } + + resolve() + }) + }), + ) - txIds.forEach((id) => { - dispatch(completedTx(id)) - }) + await Promise.all(cancellations) - dispatch(hideLoadingIndication()) + const newState = await updateMetamaskStateFromBackground() + dispatch(updateMetamaskState(newState)) + dispatch(clearSend()) - if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { - global.platform.closeCurrentWindow() + txIds.forEach((id) => { + dispatch(completedTx(id)) + }) + } finally { + if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { + global.platform.closeCurrentWindow() + } else { + dispatch(hideLoadingIndication()) + } } } } @@ -1235,11 +1264,11 @@ export function setSelectedAddress(address) { try { await _setSelectedAddress(dispatch, address) } catch (error) { - dispatch(hideLoadingIndication()) dispatch(displayWarning(error.message)) return + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) } } @@ -1270,11 +1299,12 @@ export function showAccountDetail(address) { try { await _setSelectedAddress(dispatch, address) } catch (error) { - dispatch(hideLoadingIndication()) dispatch(displayWarning(error.message)) return + } finally { + dispatch(hideLoadingIndication()) } - dispatch(hideLoadingIndication()) + dispatch({ type: actionConstants.SHOW_ACCOUNT_DETAIL, value: address, @@ -2024,13 +2054,13 @@ export function setCompletedOnboarding() { try { await promisifiedBackground.completeOnboarding() + dispatch(completeOnboarding()) } catch (err) { dispatch(displayWarning(err.message)) throw err + } finally { + dispatch(hideLoadingIndication()) } - - dispatch(completeOnboarding()) - dispatch(hideLoadingIndication()) } } @@ -2179,20 +2209,19 @@ export function setIpfsGateway(val) { export function updateCurrentLocale(key) { return async (dispatch) => { dispatch(showLoadingIndication()) - await loadRelativeTimeFormatLocaleData(key) - return fetchLocale(key).then((localeMessages) => { - log.debug(`background.setCurrentLocale`) - background.setCurrentLocale(key, (err, textDirection) => { - if (err) { - dispatch(hideLoadingIndication()) - dispatch(displayWarning(err.message)) - return - } - switchDirection(textDirection) - dispatch(setCurrentLocale(key, localeMessages)) - dispatch(hideLoadingIndication()) - }) - }) + + try { + await loadRelativeTimeFormatLocaleData(key) + const localeMessages = await fetchLocale(key) + const textDirection = await promisifiedBackground.setCurrentLocale(key) + await switchDirection(textDirection) + dispatch(setCurrentLocale(key, localeMessages)) + } catch (error) { + dispatch(displayWarning(error.message)) + return + } finally { + dispatch(hideLoadingIndication()) + } } } From d4d3d6a52a36c7d2764430efb4da06330608c113 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Fri, 18 Dec 2020 09:58:42 -0600 Subject: [PATCH 52/60] Remove unnecessary swaps footer space when in dropdown mode (#10100) --- ui/app/pages/swaps/view-quote/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/swaps/view-quote/index.scss b/ui/app/pages/swaps/view-quote/index.scss index 3382befff..397748c2a 100644 --- a/ui/app/pages/swaps/view-quote/index.scss +++ b/ui/app/pages/swaps/view-quote/index.scss @@ -17,7 +17,7 @@ @media screen and (max-width: 576px) { overflow-y: auto; - max-height: 388px; + max-height: 428px; } } From 5681634ba2458dcc7863ab811e6f03befe8ba16b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Dec 2020 10:14:48 -0600 Subject: [PATCH 53/60] Bump @metamask/contract-metadata from 1.19.0 to 1.20.0 (#10104) Bumps [@metamask/contract-metadata](https://github.com/MetaMask/contract-metadata) from 1.19.0 to 1.20.0. - [Release notes](https://github.com/MetaMask/contract-metadata/releases) - [Commits](https://github.com/MetaMask/contract-metadata/compare/v1.19.0...v1.20.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 467ede1b3..9d619fbff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2015,9 +2015,9 @@ react-is "^16.8.0" "@metamask/contract-metadata@^1.19.0": - version "1.19.0" - resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.19.0.tgz#2f074bce7ab7ffd0d20e3905b1936da0749a0473" - integrity sha512-TklMuz7ZbFJ2Zc6C7I+9qL3J9J+4prs5Ok5MJzoxD/57Iq6espzArhpI275elVCFF9ci8IMvach1kH8+F04/hA== + version "1.20.0" + resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.20.0.tgz#8a127ca08edeb2c19b2f725b439b1630926c8aa9" + integrity sha512-Htb2yUwmAe4GrABpqahNDpYR3ErcCcaavF6tKamqvyymICeywi6HV0y/A7rbe86gLy5UW6wJG9yP4pCpQxs4RQ== "@metamask/controllers@^3.1.0": version "3.2.0" From 889ca62723b53516d28c9307f766ca4e065cc126 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sat, 19 Dec 2020 16:58:49 -0330 Subject: [PATCH 54/60] Use late-bound noop function when disabling console (#10110) The `disable-console` script introduced in #10040 used an arrow- function no-op function to replace `console.log` and `console.info`. This replacement function was early-bound to the `this` context of the `disable-console` script, because that's how arrow functions work. This violates an assumption baked into Sentry, which also replaces the `console` functions. It wraps them in a function it uses to track console logs as breadcrumbs. This wrapper function blows up for some reason if the "original" `console` function is early-bound to a `this` value of `undefined`. This resulted in various UI freezes. One example is during onboarding, when using Firefox with Enhanced Tracking Protection set in "strict" mode. After submitting a password in the 'Create wallet' flow, the Sentry `console` wrapper would throw and leave the user stuck on the loading screen. By replacing the no-op arrow function with a no-op function declaration, the problem has been resolved. Relates to #10097 --- app/scripts/disable-console.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/scripts/disable-console.js b/app/scripts/disable-console.js index bb7ce8e24..7416b36f5 100644 --- a/app/scripts/disable-console.js +++ b/app/scripts/disable-console.js @@ -4,6 +4,10 @@ if ( !(typeof process !== 'undefined' && process.env.METAMASK_DEBUG) && typeof console !== undefined ) { - console.log = () => undefined - console.info = () => undefined + console.log = noop + console.info = noop +} + +function noop() { + return undefined } From 2f6f8966bb92acf765a5e49ecde330f5123606f1 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 21 Dec 2020 12:07:32 -0800 Subject: [PATCH 55/60] @metamask/contract-metadata@1.20.0 (#10116) --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b8d30ae3e..2282a5013 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@formatjs/intl-relativetimeformat": "^5.2.6", "@fortawesome/fontawesome-free": "^5.13.0", "@material-ui/core": "^4.11.0", - "@metamask/contract-metadata": "^1.19.0", + "@metamask/contract-metadata": "^1.20.0", "@metamask/controllers": "^5.1.0", "@metamask/eth-ledger-bridge-keyring": "^0.2.6", "@metamask/eth-token-tracker": "^3.0.1", diff --git a/yarn.lock b/yarn.lock index 9d619fbff..4a308380e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2014,7 +2014,7 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@metamask/contract-metadata@^1.19.0": +"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.20.0": version "1.20.0" resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.20.0.tgz#8a127ca08edeb2c19b2f725b439b1630926c8aa9" integrity sha512-Htb2yUwmAe4GrABpqahNDpYR3ErcCcaavF6tKamqvyymICeywi6HV0y/A7rbe86gLy5UW6wJG9yP4pCpQxs4RQ== From 64adcae08deda6b9c5dba3c7e550c2ce32fadde6 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Tue, 22 Dec 2020 21:54:49 -0600 Subject: [PATCH 56/60] throw a new wrapped error instead of default one from segment (#10118) --- app/scripts/controllers/metametrics.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index c995ea491..3ae74cbf8 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -265,7 +265,13 @@ export default class MetaMetricsController { return new Promise((resolve, reject) => { const callback = (err) => { if (err) { - return reject(err) + // The error that segment gives us has some manipulation done to it + // that seemingly breaks with lockdown enabled. Creating a new error + // here prevents the system from freezing when the network request to + // segment fails for any reason. + const safeError = new Error(err.message) + safeError.stack = err.stack + return reject(safeError) } return resolve() } From c42087d04434b5d70a899f1c1a32ba0a5f0f9a1c Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 23 Dec 2020 10:15:07 -0600 Subject: [PATCH 57/60] Use destructured signal (#10115) --- app/scripts/lib/fetch-with-timeout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/fetch-with-timeout.js b/app/scripts/lib/fetch-with-timeout.js index bca8d652d..a9e895b12 100644 --- a/app/scripts/lib/fetch-with-timeout.js +++ b/app/scripts/lib/fetch-with-timeout.js @@ -1,10 +1,10 @@ const fetchWithTimeout = ({ timeout = 120000 } = {}) => { return async function _fetch(url, opts) { const abortController = new window.AbortController() - const abortSignal = abortController.signal + const { signal } = abortController const f = window.fetch(url, { ...opts, - signal: abortSignal, + signal, }) const timer = setTimeout(() => abortController.abort(), timeout) From 869124c4c92c093194cca3e59e0ff8eca7f5375a Mon Sep 17 00:00:00 2001 From: ivigamberdiev Date: Fri, 25 Dec 2020 21:51:47 +0300 Subject: [PATCH 58/60] Fix network settings Kovan block explorer link (#10117) --- ui/app/pages/settings/networks-tab/networks-tab.constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js index 4c0ab5462..0271abcfc 100644 --- a/ui/app/pages/settings/networks-tab/networks-tab.constants.js +++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js @@ -55,7 +55,7 @@ const defaultNetworksData = [ rpcUrl: `https://kovan.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, chainId: KOVAN_CHAIN_ID, ticker: 'ETH', - blockExplorerUrl: 'https://etherscan.io', + blockExplorerUrl: 'https://kovan.etherscan.io', }, ] From d55f5794478f50c0d282a1ee28598324346614bf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 4 Jan 2021 09:45:54 -0800 Subject: [PATCH 59/60] Fix useTransactionDisplayData unit tests (#10134) New year, new problems. It's working as expected, but we had hard-coded some 2020 date values, and `formatDateWithYearContext` adds the year to its output formatted date if the date is not from the current year. --- .../hooks/tests/useTransactionDisplayData.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/app/hooks/tests/useTransactionDisplayData.test.js b/ui/app/hooks/tests/useTransactionDisplayData.test.js index 00b2f5bb8..64c6a0366 100644 --- a/ui/app/hooks/tests/useTransactionDisplayData.test.js +++ b/ui/app/hooks/tests/useTransactionDisplayData.test.js @@ -30,7 +30,7 @@ const expectedResults = [ category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0xffe5...1a97', subtitleContainsOrigin: false, - date: 'May 12', + date: 'May 12, 2020', primaryCurrency: '-1 ETH', senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', recipientAddress: '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', @@ -44,7 +44,7 @@ const expectedResults = [ category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0x0ccc...8848', subtitleContainsOrigin: false, - date: 'May 12', + date: 'May 12, 2020', primaryCurrency: '-2 ETH', senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', recipientAddress: '0x0ccc8aeeaf5ce790f3b448325981a143fdef8848', @@ -57,7 +57,7 @@ const expectedResults = [ category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0xffe5...1a97', subtitleContainsOrigin: false, - date: 'May 12', + date: 'May 12, 2020', primaryCurrency: '-2 ETH', senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', recipientAddress: '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', @@ -70,7 +70,7 @@ const expectedResults = [ category: TRANSACTION_GROUP_CATEGORIES.RECEIVE, subtitle: 'From: 0x31b9...4523', subtitleContainsOrigin: false, - date: 'May 12', + date: 'May 12, 2020', primaryCurrency: '18.75 ETH', senderAddress: '0x31b98d14007bdee637298086988a0bbd31184523', recipientAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', @@ -83,7 +83,7 @@ const expectedResults = [ category: TRANSACTION_GROUP_CATEGORIES.RECEIVE, subtitle: 'From: 0x9eca...a149', subtitleContainsOrigin: false, - date: 'May 8', + date: 'May 8, 2020', primaryCurrency: '0 ETH', senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', recipientAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', @@ -96,7 +96,7 @@ const expectedResults = [ category: TRANSACTION_GROUP_CATEGORIES.RECEIVE, subtitle: 'From: 0xee01...febb', subtitleContainsOrigin: false, - date: 'May 24', + date: 'May 24, 2020', primaryCurrency: '1 ETH', senderAddress: '0xee014609ef9e09776ac5fe00bdbfef57bcdefebb', recipientAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', @@ -109,7 +109,7 @@ const expectedResults = [ category: TRANSACTION_CATEGORIES.SWAP, subtitle: '', subtitleContainsOrigin: false, - date: 'May 12', + date: 'May 12, 2020', primaryCurrency: '+1 ABC', senderAddress: '0xee014609ef9e09776ac5fe00bdbfef57bcdefebb', recipientAddress: '0xabca64466f257793eaa52fcfff5066894b76a149', From 29579061010818c4f79b01c977e914a7c991a983 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 4 Jan 2021 14:43:44 -0330 Subject: [PATCH 60/60] Ensure that gas for swap tx submitted at same time as approval is in hex (#10135) --- ui/app/ducks/swaps/swaps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index f6d36c204..876bd50ec 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -606,7 +606,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { customSwapsGas || (usedQuote?.gasEstimate ? estimatedGasLimitWithMultiplier - : usedQuote?.maxGas) + : `0x${decimalToHex(usedQuote?.maxGas || 0)}`) const usedGasPrice = getUsedSwapsGasPrice(state) usedTradeTxParams.gas = maxGasLimit