From 94c705c5ad7762f7bf5ff440e7ac33829a7d8856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 7 Apr 2021 10:53:25 -0400 Subject: [PATCH 01/51] Improvement/assets by chainid (#2441) * migrations * add custom rpc assets * addpackage * bumpcontroller --- app/store/migrations.js | 58 +++++++++++++++++++++++++++++++- metamask-controllers-v6.2.2.tgz | Bin 0 -> 121709 bytes package.json | 2 +- yarn.lock | 18 +++++++--- 4 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 metamask-controllers-v6.2.2.tgz diff --git a/app/store/migrations.js b/app/store/migrations.js index 6d13e2369e5..34b676ad182 100644 --- a/app/store/migrations.js +++ b/app/store/migrations.js @@ -72,7 +72,63 @@ export const migrations = { }; } return state; + }, + 4: state => { + const { allCollectibleContracts, allCollectibles, allTokens } = state.engine.backgroundState.AssetsController; + const { frequentRpcList } = state.engine.backgroundState.PreferencesController; + + const newAllCollectibleContracts = {}; + const newAllCollectibles = {}; + const newAllTokens = {}; + + Object.keys(allTokens).forEach(address => { + newAllTokens[address] = {}; + Object.keys(allTokens[address]).forEach(networkType => { + if (NetworksChainId[networkType]) { + newAllTokens[address][NetworksChainId[networkType]] = allTokens[address][networkType]; + } else { + frequentRpcList.forEach(({ chainId }) => { + newAllTokens[address][chainId] = allTokens[address][networkType]; + }); + } + }); + }); + + Object.keys(allCollectibles).forEach(address => { + newAllCollectibles[address] = {}; + Object.keys(allCollectibles[address]).forEach(networkType => { + if (NetworksChainId[networkType]) { + newAllCollectibles[address][NetworksChainId[networkType]] = allCollectibles[address][networkType]; + } else { + frequentRpcList.forEach(({ chainId }) => { + newAllCollectibles[address][chainId] = allCollectibles[address][networkType]; + }); + } + }); + }); + + Object.keys(allCollectibleContracts).forEach(address => { + newAllCollectibleContracts[address] = {}; + Object.keys(allCollectibleContracts[address]).forEach(networkType => { + if (NetworksChainId[networkType]) { + newAllCollectibleContracts[address][NetworksChainId[networkType]] = + allCollectibleContracts[address][networkType]; + } else { + frequentRpcList.forEach(({ chainId }) => { + newAllCollectibleContracts[address][chainId] = allCollectibleContracts[address][networkType]; + }); + } + }); + }); + + state.engine.backgroundState.AssetsController = { + ...state.engine.backgroundState.AssetsController, + allTokens: newAllTokens, + allCollectibles: newAllCollectibles, + allCollectibleContracts: newAllCollectibleContracts + }; + return state; } }; -export const version = 3; +export const version = 4; diff --git a/metamask-controllers-v6.2.2.tgz b/metamask-controllers-v6.2.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..990b678f8c1ec8958bd58b6b20a8b91e4ab998e5 GIT binary patch literal 121709 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

R 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 z z*R?nZ8ry<7J|K423U;37`4QWwMJ{i&p7FdYb!L%Hahi9R@rk1XMk#kVD{su;2WKng zIlwW=B)|A%QC!Mg7<)SmtzNE6JR>2O9%H zvG-b#xZJGsn(E|ceAjg2+I4O2vq_%g8lhXI;tAd`VVV5?0EGg^F zW;eWUkI=9@|24Dq4G|z-Yich@mo@pLl)oC0>*sM+iyXW2{nVVZ!AyiJ;FT6XyhohU zLYpMyW5YB*n7a`>?FSr< z47V=wF> Date: Wed, 7 Apr 2021 22:42:59 +0100 Subject: [PATCH 03/51] Fix/gas estimations (#2408) * initial implementation * Only display advanced * Approve transaction gas estimation * Fix custom gas input * Hide transaction actions for non-mainnet * Fix typo * Fix gas estimations for dapp transactions * Typo: Chain id as prop * Add test * Improve test * Fix Transactions UI test * Move to chainId * Added fallback to network gas price for mainnet basic estimates error * Update tests * Move code to util & fix custom gas component & timeout * Remove unused getBasicGasEstimatesWithHardcodedFallback --- app/components/UI/CustomGas/index.js | 93 ++++++++------ app/components/UI/CustomGas/index.test.js | 3 +- app/components/UI/Swaps/utils/useGasPrice.js | 4 +- app/components/UI/TransactionEditor/index.js | 13 +- .../UI/TransactionEditor/index.test.js | 3 +- app/components/UI/Transactions/index.test.js | 5 + .../Views/ApproveView/Approve/index.js | 11 +- app/components/Views/SendFlow/Amount/index.js | 25 +--- .../Views/SendFlow/Confirm/index.js | 47 +++---- app/util/custom-gas.js | 116 ++++++++++++++---- app/util/networks.js | 9 ++ app/util/number.js | 108 +++++++++++++++- ios/Podfile.lock | 4 +- 13 files changed, 315 insertions(+), 126 deletions(-) diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index 5dc55cfb55c..b7a9f72ea0b 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -13,6 +13,7 @@ import { safeToChecksumAddress } from '../../../util/address'; import Radio from '../Radio'; import StyledButton from '../../UI/StyledButton'; import Device from '../../../util/Device'; +import { isMainnetByChainId } from '../../../util/networks'; const styles = StyleSheet.create({ root: { @@ -312,7 +313,11 @@ class CustomGas extends PureComponent { /** * review or edit */ - toAdvancedFrom: PropTypes.string + toAdvancedFrom: PropTypes.string, + /** + * Current network chain id + */ + chainId: PropTypes.string }; state = { @@ -334,7 +339,10 @@ class CustomGas extends PureComponent { const { gas, gasPrice, toggleAdvancedCustomGas } = this.props; const warningSufficientFunds = this.hasSufficientFunds(gas, gasPrice); const { ticker } = this.props; - if (ticker && ticker !== 'ETH') toggleAdvancedCustomGas(true); + if (this.onlyAdvanced()) { + toggleAdvancedCustomGas(true); + } + //Applies ISF error if present before any gas modifications this.setState({ warningSufficientFunds, advancedCustomGas: ticker && ticker !== 'ETH' }); }; @@ -463,17 +471,19 @@ class CustomGas extends PureComponent { const warningSufficientFunds = this.hasSufficientFunds(customGasLimitBN, gasPriceBNWei); let warningGasPrice; let warningGasPriceHigh = ''; - if (parseInt(gasPrice) < parseInt(this.props.basicGasEstimates.safeLowGwei)) - warningGasPrice = strings('transaction.low_gas_price'); - //Warning should be displayed when the gas fee is 1.5 times higher than the fast rate - if (parseInt(gasPrice) > parseInt(this.props.basicGasEstimates.fastGwei) * 1.5) { - const currentGasPrice = getRenderableFiatGasFee( - gasPrice, - this.props.conversionRate, - this.props.currentCurrency, - customGasLimitBN - ); - warningGasPriceHigh = strings('transaction.high_gas_price', { currentGasPrice }); + if (this.props.basicGasEstimates) { + if (parseInt(gasPrice) < parseInt(this.props.basicGasEstimates.safeLowGwei)) + warningGasPrice = strings('transaction.low_gas_price'); + //Warning should be displayed when the gas fee is 1.5 times higher than the fast rate + if (parseInt(gasPrice) > parseInt(this.props.basicGasEstimates.fastGwei) * 1.5) { + const currentGasPrice = getRenderableFiatGasFee( + gasPrice, + this.props.conversionRate, + this.props.currentCurrency, + customGasLimitBN + ); + warningGasPriceHigh = strings('transaction.high_gas_price', { currentGasPrice }); + } } if (!value || value === '' || !isDecimal(value) || value <= 0) warningGasPrice = strings('transaction.invalid_gas_price'); @@ -491,13 +501,7 @@ class CustomGas extends PureComponent { //Handle gas fee selection when save button is pressed instead of everytime a change is made, otherwise cannot switch back to review mode if there is an error saveCustomGasSelection = () => { const { gasSpeedSelected, customGasLimit, customGasPrice } = this.state; - const { - review, - gas, - handleGasFeeSelection, - advancedCustomGas, - basicGasEstimates: { fastGwei, averageGwei, safeLowGwei } - } = this.props; + const { review, gas, handleGasFeeSelection, advancedCustomGas } = this.props; if (advancedCustomGas) { handleGasFeeSelection( new BN(customGasLimit), @@ -509,6 +513,9 @@ class CustomGas extends PureComponent { ); } else { const mode = { mode: gasSpeedSelected }; + const { + basicGasEstimates: { fastGwei, averageGwei, safeLowGwei } + } = this.props; const noGasWarning = ''; if (gasSpeedSelected === 'slow') handleGasFeeSelection(gas, apiEstimateModifiedToWEI(safeLowGwei), noGasWarning, mode); @@ -636,7 +643,7 @@ class CustomGas extends PureComponent { @@ -693,6 +700,14 @@ class CustomGas extends PureComponent { !this.state.gasInputHeight && this.setState({ gasInputHeight: event.nativeEvent.layout.height }); }; + onlyAdvanced = () => { + const { chainId, basicGasEstimates } = this.props; + const isNotMainnet = !isMainnetByChainId(chainId); + // Check if either no basicGasEstimates were provided or less than 3 options were provided (for example, only the average gas price) + const noBasicGasEstimates = !basicGasEstimates || Object.keys(basicGasEstimates).length < 3; + return isNotMainnet || noBasicGasEstimates; + }; + render = () => { const { warningGasLimit, warningGasPrice, warningSufficientFunds } = this.state; const { @@ -722,23 +737,26 @@ class CustomGas extends PureComponent { {strings('transaction.edit_network_fee')} - - - {strings('custom_gas.basic_options')} - - - {strings('custom_gas.advanced_options')} - - + {this.onlyAdvanced() ? null : ( + + + {strings('custom_gas.basic_options')} + + + + {strings('custom_gas.advanced_options')} + + + )} - {this.renderCustomGasSelector()} + {this.onlyAdvanced() ? null : this.renderCustomGasSelector()} {this.renderCustomGasInput()} @@ -768,7 +786,8 @@ const mapStateToProps = (state, props) => ({ conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, ticker: state.engine.backgroundState.NetworkController.provider.ticker, - transaction: props.customTransaction || getNormalizedTxState(state) + transaction: props.customTransaction || getNormalizedTxState(state), + chainId: state.engine.backgroundState.NetworkController.provider.chainId }); export default connect(mapStateToProps)(CustomGas); diff --git a/app/components/UI/CustomGas/index.test.js b/app/components/UI/CustomGas/index.test.js index 067677ec8d3..600b8329106 100644 --- a/app/components/UI/CustomGas/index.test.js +++ b/app/components/UI/CustomGas/index.test.js @@ -25,7 +25,8 @@ describe('CustomGas', () => { }, NetworkController: { provider: { - ticker: 'ETH' + ticker: 'ETH', + chainId: '1' } } } diff --git a/app/components/UI/Swaps/utils/useGasPrice.js b/app/components/UI/Swaps/utils/useGasPrice.js index f4fde19514d..9db789b12ea 100644 --- a/app/components/UI/Swaps/utils/useGasPrice.js +++ b/app/components/UI/Swaps/utils/useGasPrice.js @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { getBasicGasEstimates } from '../../../../util/custom-gas'; +import { getBasicGasEstimatesByChainId } from '../../../../util/custom-gas'; import Logger from '../../../../util/Logger'; function useGasPrice() { @@ -7,7 +7,7 @@ function useGasPrice() { const getGasPrice = useCallback(async () => { try { - const gasEstimates = await getBasicGasEstimates(); + const gasEstimates = await getBasicGasEstimatesByChainId(); setGasPrice(gasEstimates); } catch (error) { Logger.log('Swaps: Error while trying to get gas estimates', error); diff --git a/app/components/UI/TransactionEditor/index.js b/app/components/UI/TransactionEditor/index.js index 1c1fb135d69..62ac6faf8fb 100644 --- a/app/components/UI/TransactionEditor/index.js +++ b/app/components/UI/TransactionEditor/index.js @@ -10,7 +10,7 @@ import { strings } from '../../../../locales/i18n'; import { connect } from 'react-redux'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { generateTransferData, getNormalizedTxState, getTicker } from '../../../util/transactions'; -import { getBasicGasEstimates, apiEstimateModifiedToWEI } from '../../../util/custom-gas'; +import { apiEstimateModifiedToWEI, getBasicGasEstimatesByChainId } from '../../../util/custom-gas'; import { setTransactionObject } from '../../../actions/transaction'; import Engine from '../../../core/Engine'; import collectiblesTransferInformation from '../../../util/collectibles-transfer'; @@ -596,9 +596,14 @@ class TransactionEditor extends PureComponent { handleFetchBasicEstimates = async () => { this.setState({ ready: false }); - const basicGasEstimates = await getBasicGasEstimates(); - this.handleGasFeeSelection(this.props.transaction.gas, apiEstimateModifiedToWEI(basicGasEstimates.averageGwei)); - this.setState({ basicGasEstimates, ready: true }); + const basicGasEstimates = await getBasicGasEstimatesByChainId(); + if (basicGasEstimates) { + this.handleGasFeeSelection( + this.props.transaction.gas, + apiEstimateModifiedToWEI(basicGasEstimates.averageGwei) + ); + } + return this.setState({ basicGasEstimates, ready: true }); }; render = () => { diff --git a/app/components/UI/TransactionEditor/index.test.js b/app/components/UI/TransactionEditor/index.test.js index 063baf0db9f..7cd96248ac5 100644 --- a/app/components/UI/TransactionEditor/index.test.js +++ b/app/components/UI/TransactionEditor/index.test.js @@ -29,7 +29,8 @@ describe('TransactionEditor', () => { }, NetworkController: { provider: { - type: 'mainnet' + type: 'mainnet', + chainId: '1' } } } diff --git a/app/components/UI/Transactions/index.test.js b/app/components/UI/Transactions/index.test.js index f863c5bd064..e123a0b5996 100644 --- a/app/components/UI/Transactions/index.test.js +++ b/app/components/UI/Transactions/index.test.js @@ -24,6 +24,11 @@ describe('Transactions', () => { CurrencyRateController: { currentCurrency: 'USD', conversionRate: 1 + }, + NetworkController: { + provider: { + chainId: '1' + } } } }, diff --git a/app/components/Views/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js index 879111b5e25..3c5be56080d 100644 --- a/app/components/Views/ApproveView/Approve/index.js +++ b/app/components/Views/ApproveView/Approve/index.js @@ -14,7 +14,7 @@ import { setTransactionObject } from '../../../../actions/transaction'; import { util } from '@metamask/controllers'; import { isBN, renderFromWei } from '../../../../util/number'; import { getNormalizedTxState, getTicker } from '../../../../util/transactions'; -import { getBasicGasEstimates, apiEstimateModifiedToWEI } from '../../../../util/custom-gas'; +import { apiEstimateModifiedToWEI, getBasicGasEstimatesByChainId } from '../../../../util/custom-gas'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import NotificationManager from '../../../../core/NotificationManager'; import Analytics from '../../../../core/Analytics'; @@ -121,9 +121,11 @@ class Approve extends PureComponent { handleFetchBasicEstimates = async () => { this.setState({ ready: false }); - const basicGasEstimates = await getBasicGasEstimates(); - this.handleSetGasFee(this.props.transaction.gas, apiEstimateModifiedToWEI(basicGasEstimates.averageGwei)); - this.setState({ basicGasEstimates, ready: true }); + const basicGasEstimates = await getBasicGasEstimatesByChainId(); + if (basicGasEstimates) { + this.handleSetGasFee(this.props.transaction.gas, apiEstimateModifiedToWEI(basicGasEstimates.averageGwei)); + } + return this.setState({ basicGasEstimates, ready: true }); }; trackApproveEvent = event => { @@ -235,6 +237,7 @@ class Approve extends PureComponent { const { gasError, basicGasEstimates, mode, ready, over, warningGasPriceHigh } = this.state; const { transaction } = this.props; if (!transaction.id) return null; + return ( { - const { TransactionController } = Engine.context; const { transaction: { from }, transactionTo } = this.props.transactionState; - let estimation, basicGasEstimates; - try { - estimation = await TransactionController.estimateGas({ - from, - to: transactionTo - }); - } catch (e) { - estimation = { gas: TransactionTypes.CUSTOM_GAS.DEFAULT_GAS_LIMIT }; - } - try { - basicGasEstimates = await fetchBasicGasEstimates(); - } catch (error) { - basicGasEstimates = { average: 20 }; - } - const gas = hexToBN(estimation.gas); - const gasPrice = toWei(convertApiValueToGWEI(basicGasEstimates.average), 'gwei'); + const { gas, gasPrice } = await getGasPriceByChainId({ + from, + to: transactionTo + }); + return gas.mul(gasPrice); }; diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index 3f42d50dbd8..c375afcc526 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -24,7 +24,6 @@ import { weiToFiatNumber, balanceToFiatNumber, renderFiatAddition, - toWei, isDecimal, toBN } from '../../../../util/number'; @@ -33,10 +32,9 @@ import StyledButton from '../../../UI/StyledButton'; import { util } from '@metamask/controllers'; import { prepareTransaction, resetTransaction } from '../../../../actions/transaction'; import { - fetchBasicGasEstimates, - convertApiValueToGWEI, apiEstimateModifiedToWEI, - getBasicGasEstimates + getGasPriceByChainId, + getBasicGasEstimatesByChainId } from '../../../../util/custom-gas'; import Engine from '../../../../core/Engine'; import Logger from '../../../../util/Logger'; @@ -61,9 +59,6 @@ const EDIT = 'edit'; const REVIEW = 'review'; const { hexToBN, BNToHex } = util; -const { - CUSTOM_GAS: { AVERAGE_GAS, FAST_GAS, LOW_GAS } -} = TransactionTypes; const styles = StyleSheet.create({ wrapper: { @@ -377,9 +372,11 @@ class Confirm extends PureComponent { handleFetchBasicEstimates = async () => { this.setState({ ready: false }); - const basicGasEstimates = await getBasicGasEstimates(); - this.handleSetGasFee(this.props.transaction.gas, apiEstimateModifiedToWEI(basicGasEstimates.averageGwei)); - this.setState({ basicGasEstimates, ready: true }); + const basicGasEstimates = await getBasicGasEstimatesByChainId(); + if (basicGasEstimates) { + this.handleSetGasFee(this.props.transaction.gas, apiEstimateModifiedToWEI(basicGasEstimates.averageGwei)); + } + return this.setState({ basicGasEstimates, ready: true }); }; prepareTransaction = async () => { @@ -394,30 +391,14 @@ class Confirm extends PureComponent { }; estimateGas = async transaction => { - const { TransactionController } = Engine.context; const { value, data, to, from } = transaction; - let estimation; - try { - estimation = await TransactionController.estimateGas({ - value, - from, - data, - to - }); - } catch (e) { - estimation = { gas: TransactionTypes.CUSTOM_GAS.DEFAULT_GAS_LIMIT }; - } - let basicGasEstimates; - try { - basicGasEstimates = await fetchBasicGasEstimates(); - } catch (error) { - Logger.log('Error while trying to get gas limit estimates', error); - basicGasEstimates = { average: AVERAGE_GAS, safeLow: LOW_GAS, fast: FAST_GAS }; - } - return { - gas: hexToBN(estimation.gas), - gasPrice: toWei(convertApiValueToGWEI(basicGasEstimates.average), 'gwei') - }; + + return await getGasPriceByChainId({ + value, + from, + data, + to + }); }; parseTransactionData = () => { diff --git a/app/util/custom-gas.js b/app/util/custom-gas.js index 82cc62f2d3e..14ca609a9e4 100644 --- a/app/util/custom-gas.js +++ b/app/util/custom-gas.js @@ -1,8 +1,16 @@ import { BN } from 'ethereumjs-util'; -import { renderFromWei, weiToFiat, toWei } from './number'; +import { renderFromWei, weiToFiat, toWei, conversionUtil } from './number'; import { strings } from '../../locales/i18n'; import Logger from '../util/Logger'; import TransactionTypes from '../core/TransactionTypes'; +import Engine from '../core/Engine'; +import { isMainnetByChainId } from '../util/networks'; +import { util } from '@metamask/controllers'; +const { hexToBN } = util; + +export const ETH = 'ETH'; +export const GWEI = 'GWEI'; +export const WEI = 'WEI'; /** * Calculates wei value of estimate gas price in gwei @@ -103,7 +111,10 @@ export function parseWaitTime(min) { * @returns {Object} - Object containing basic estimates */ export async function fetchBasicGasEstimates() { - return await fetch(`https://api.metaswap.codefi.network/gasPrices`, { + // Timeout in 7 seconds + const timeout = 7000; + + const fetchPromise = fetch(`https://api.metaswap.codefi.network/gasPrices`, { headers: {}, referrerPolicy: 'no-referrer-when-downgrade', body: null, @@ -117,8 +128,14 @@ export async function fetchBasicGasEstimates() { safeLow: SafeGasPrice, fast: FastGasPrice }; + return basicEstimates; }); + + return Promise.race([ + fetchPromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)) + ]); } /** @@ -127,29 +144,13 @@ export async function fetchBasicGasEstimates() { * @returns {Object} - Object containing formatted wait times */ export async function getBasicGasEstimates() { - const { - CUSTOM_GAS: { AVERAGE_GAS, FAST_GAS, LOW_GAS } - } = TransactionTypes; - - let basicGasEstimates; - try { - basicGasEstimates = await fetchBasicGasEstimates(); - } catch (error) { - Logger.log('Error while trying to get gas limit estimates', error); - basicGasEstimates = { - average: AVERAGE_GAS, - safeLow: LOW_GAS, - fast: FAST_GAS - }; - } + const basicGasEstimates = await fetchBasicGasEstimates(); // Handle api failure returning same gas prices - let { average, fast, safeLow } = basicGasEstimates; + const { average, fast, safeLow } = basicGasEstimates; if (average === fast && average === safeLow) { - average = AVERAGE_GAS; - safeLow = LOW_GAS; - fast = FAST_GAS; + throw new Error('Api returned same gas prices'); } return { @@ -158,3 +159,76 @@ export async function getBasicGasEstimates() { safeLowGwei: convertApiValueToGWEI(safeLow) }; } + +export async function getGasPriceByChainId(transaction) { + const { TransactionController, NetworkController } = Engine.context; + const chainId = NetworkController.state.provider.chainId; + + let estimation, basicGasEstimates; + try { + estimation = await TransactionController.estimateGas(transaction); + basicGasEstimates = { + average: getValueFromWeiHex({ + value: estimation.gasPrice.toString(16), + numberOfDecimals: 4, + toDenomination: 'GWEI' + }) + }; + } catch (error) { + estimation = { + gas: TransactionTypes.CUSTOM_GAS.DEFAULT_GAS_LIMIT, + gasPrice: TransactionTypes.CUSTOM_GAS.AVERAGE_GAS + }; + basicGasEstimates = { + average: estimation.gasPrice + }; + Logger.log('Error while trying to get gas price from the network', error); + } + + if (isMainnetByChainId(chainId)) { + try { + basicGasEstimates = await fetchBasicGasEstimates(); + } catch (error) { + Logger.log('Error while trying to get gas limit estimates', error); + // Will use gas price from network that was fetched above + } + } + const gas = hexToBN(estimation.gas); + const gasPrice = toWei(convertApiValueToGWEI(basicGasEstimates.average), 'gwei'); + return { gas, gasPrice }; +} + +export async function getBasicGasEstimatesByChainId() { + const { NetworkController } = Engine.context; + const chainId = NetworkController.state.provider.chainId; + + if (!isMainnetByChainId(chainId)) { + return null; + } + try { + const basicGasEstimates = await getBasicGasEstimates(); + return basicGasEstimates; + } catch (e) { + return null; + } +} + +export function getValueFromWeiHex({ + value, + fromCurrency = ETH, + toCurrency, + conversionRate, + numberOfDecimals, + toDenomination +}) { + return conversionUtil(value, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency, + toCurrency, + numberOfDecimals, + fromDenomination: WEI, + toDenomination, + conversionRate + }); +} diff --git a/app/util/networks.js b/app/util/networks.js index ab859d33e33..7b019261bd1 100644 --- a/app/util/networks.js +++ b/app/util/networks.js @@ -72,6 +72,15 @@ export const getAllNetworks = () => NetworkListKeys.filter(name => name !== RPC) export const isMainNet = network => network?.provider?.type === MAINNET || network === String(1); +export const getDecimalChainId = chainId => { + if (!chainId || typeof chainId !== 'string' || !chainId.startsWith('0x')) { + return chainId; + } + return parseInt(chainId, 16).toString(10); +}; + +export const isMainnetByChainId = chainId => getDecimalChainId(String(chainId)) === String(1); + export const getNetworkName = id => NetworkListKeys.find(key => NetworkList[key].networkId === Number(id)); export function getNetworkTypeById(id) { diff --git a/app/util/number.js b/app/util/number.js index 33aed748ef5..b8f25118505 100644 --- a/app/util/number.js +++ b/app/util/number.js @@ -1,13 +1,40 @@ /** * Collection of utility functions for consistent formatting and conversion */ -import { addHexPrefix, BN } from 'ethereumjs-util'; +import { addHexPrefix, BN, stripHexPrefix } from 'ethereumjs-util'; import { utils as ethersUtils } from 'ethers'; import convert from 'ethjs-unit'; import { util } from '@metamask/controllers'; import numberToBN from 'number-to-bn'; import currencySymbols from '../util/currency-symbols.json'; - +import BigNumber from 'bignumber.js'; + +// Big Number Constants +const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000'); +const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000'); +const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1'); + +// Setter Maps +const toBigNumber = { + hex: n => new BigNumber(stripHexPrefix(n), 16), + dec: n => new BigNumber(String(n), 10), + BN: n => new BigNumber(n.toString(16), 16) +}; +const toNormalizedDenomination = { + WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER), + GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER), + ETH: bigNumber => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER) +}; +const toSpecifiedDenomination = { + WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).decimalPlaces(), + GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).decimalPlaces(9), + ETH: bigNumber => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).decimalPlaces(9) +}; +const baseChange = { + hex: n => n.toString(16), + dec: n => new BigNumber(n).toString(10), + BN: n => new BN(n.toString(16)) +}; /** * Converts a BN object to a hex string with a '0x' prefix * @@ -496,3 +523,80 @@ export function isPrefixedFormattedHexString(value) { } return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value); } + +const converter = ({ + value, + fromNumericBase, + fromDenomination, + fromCurrency, + toNumericBase, + toDenomination, + toCurrency, + numberOfDecimals, + conversionRate, + invertConversionRate, + roundDown +}) => { + let convertedValue = fromNumericBase ? toBigNumber[fromNumericBase](value) : value; + + if (fromDenomination) { + convertedValue = toNormalizedDenomination[fromDenomination](convertedValue); + } + + if (fromCurrency !== toCurrency) { + if (conversionRate === null || conversionRate === undefined) { + throw new Error( + `Converting from ${fromCurrency} to ${toCurrency} requires a conversionRate, but one was not provided` + ); + } + let rate = toBigNumber.dec(conversionRate); + if (invertConversionRate) { + rate = new BigNumber(1.0).div(conversionRate); + } + convertedValue = convertedValue.times(rate); + } + + if (toDenomination) { + convertedValue = toSpecifiedDenomination[toDenomination](convertedValue); + } + + if (numberOfDecimals) { + convertedValue = convertedValue.decimalPlaces(numberOfDecimals, BigNumber.ROUND_HALF_DOWN); + } + + if (roundDown) { + convertedValue = convertedValue.decimalPlaces(roundDown, BigNumber.ROUND_DOWN); + } + + if (toNumericBase) { + convertedValue = baseChange[toNumericBase](convertedValue); + } + return convertedValue; +}; + +export const conversionUtil = ( + value, + { + fromCurrency = null, + toCurrency = fromCurrency, + fromNumericBase, + toNumericBase, + fromDenomination, + toDenomination, + numberOfDecimals, + conversionRate, + invertConversionRate + } +) => + converter({ + fromCurrency, + toCurrency, + fromNumericBase, + toNumericBase, + fromDenomination, + toDenomination, + numberOfDecimals, + conversionRate, + invertConversionRate, + value: value || '0' + }); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5b829048f67..6e70a3a7975 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -634,7 +634,7 @@ SPEC CHECKSUMS: Branch: 49c609eb0ac0130b8491d0923a9298714731bd0d CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f - DoubleConversion: cde416483dac037923206447da6e1454df403714 + DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2 FBLazyVector: 3bb422f41b18121b71783a905c10e58606f7dc3e FBReactNativeSpec: f2c97f2529dd79c083355182cc158c9f98f4bd6e Flipper: be611d4b742d8c87fbae2ca5f44603a02539e365 @@ -645,7 +645,7 @@ SPEC CHECKSUMS: Flipper-RSocket: a3acb8812d6adf127deb0a5edae2793b97e6b641 FlipperKit: ab353d41aea8aae2ea6daaf813e67496642f3d7d Folly: b73c3869541e86821df3c387eb0af5f65addfab4 - glog: 40a13f7840415b9a77023fbcae0f1e6f43192af3 + glog: 1f3da668190260b06b429bb211bfbee5cd790c28 lottie-ios: a50d5c0160425cd4b01b852bb9578963e6d92d31 lottie-react-native: 7ca15c46249b61e3f9ffcf114cb4123e907a2156 OpenSSL-Universal: ff34003318d5e1163e9529b08470708e389ffcdd From c621de7212b9654831927cbc14562e53ef599202 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Wed, 7 Apr 2021 23:03:30 +0100 Subject: [PATCH 04/51] Analytics v2 (priority 1) (#2456) * Init analytics * Insert category * Add anonymous functionality * Add interactionmanager * Move analytics to parent on dapp transactions * Add missing gas analytics * Add all properties to anonymous params * Analytics to networks * Add metrics opt in item * Change anonymous params * Change name from log to trackEvent * Accounting for undefineds * Adding more fallbacks and updating unit tests --- app/components/UI/AccountApproval/index.js | 40 +- .../UI/AddCustomCollectible/index.js | 15 + .../__snapshots__/index.test.js.snap | 411 +++++++++++++++++- app/components/UI/AddCustomNetwork/index.js | 11 +- .../UI/AddCustomNetwork/index.test.js | 2 +- app/components/UI/AddCustomToken/index.js | 17 + .../UI/ApproveTransactionReview/index.js | 68 ++- app/components/UI/CustomGas/index.js | 33 +- app/components/UI/DrawerView/index.js | 3 +- app/components/UI/MessageSign/index.js | 22 + app/components/UI/NetworkList/index.js | 30 +- app/components/UI/OptinMetrics/index.js | 4 +- app/components/UI/PersonalSign/index.js | 22 + .../UI/SearchTokenAutocomplete/index.js | 21 + .../components/TransactionsEditionModal.js | 1 + .../__snapshots__/index.test.js.snap | 2 +- .../UI/SwitchCustomNetwork/index.test.js | 2 +- .../__snapshots__/index.test.js.snap | 11 + app/components/UI/TransactionEditor/index.js | 25 +- app/components/UI/TypedSign/index.js | 23 + app/components/UI/WatchAssetRequest/index.js | 30 +- app/components/Views/Approval/index.js | 36 +- .../Views/ApproveView/Approve/index.js | 28 +- .../__snapshots__/index.test.js.snap | 9 +- app/components/Views/BrowserTab/index.js | 5 + .../Views/SendFlow/Confirm/index.js | 31 +- .../NetworksSettings/NetworkSettings/index.js | 12 + .../RPCMethods/wallet_addEthereumChain.js | 36 +- app/util/analyticsV2.js | 95 ++++ locales/languages/en.json | 7 +- locales/languages/es.json | 6 +- locales/languages/hi-in.json | 6 +- locales/languages/id-id.json | 6 +- locales/languages/ja-jp.json | 6 +- locales/languages/ko-kr.json | 6 +- locales/languages/pt-br.json | 6 +- locales/languages/ru-ru.json | 6 +- locales/languages/tl.json | 6 +- locales/languages/vi-vn.json | 6 +- locales/languages/zh-cn.json | 6 +- 40 files changed, 1001 insertions(+), 111 deletions(-) create mode 100644 app/util/analyticsV2.js diff --git a/app/components/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js index 3b644ba7c6b..38842d930c3 100644 --- a/app/components/UI/AccountApproval/index.js +++ b/app/components/UI/AccountApproval/index.js @@ -8,10 +8,9 @@ import AccountInfoCard from '../AccountInfoCard'; import { strings } from '../../../../locales/i18n'; import { colors, fontStyles } from '../../../styles/common'; import Device from '../../../util/Device'; -import Analytics from '../../../core/Analytics'; -import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; import NotificationManager from '../../../core/NotificationManager'; - +import AnalyticsV2 from '../../../util/analyticsV2'; +import URL from 'url-parse'; const styles = StyleSheet.create({ root: { backgroundColor: colors.white, @@ -90,18 +89,31 @@ class AccountApproval extends PureComponent { /** * Whether it was a request coming through wallet connect */ - walletConnectRequest: PropTypes.bool + walletConnectRequest: PropTypes.bool, + /** + * A string representing the network chainId + */ + chainId: PropTypes.string }; state = { start: Date.now() }; + getAnalyticsParams = () => { + const { currentPageInformation, chainId, networkType } = this.props; + const url = new URL(currentPageInformation.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: networkType, + chain_id: chainId + }; + }; + componentDidMount = () => { - const params = this.getTrackingParams(); - delete params.timeOpen; InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.AUTHENTICATION_CONNECT, params); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.CONNECT_REQUEST_STARTED, this.getAnalyticsParams()); }); }; @@ -126,10 +138,7 @@ class AccountApproval extends PureComponent { */ onConfirm = () => { this.props.onConfirm(); - Analytics.trackEventWithParameters( - ANALYTICS_EVENT_OPTS.AUTHENTICATION_CONNECT_CONFIRMED, - this.getTrackingParams() - ); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.CONNECT_REQUEST_COMPLETED, this.getAnalyticsParams()); this.showWalletConnectNotification(true); }; @@ -137,10 +146,8 @@ class AccountApproval extends PureComponent { * Calls onConfirm callback and analytics to track connect canceled event */ onCancel = () => { - Analytics.trackEventWithParameters( - ANALYTICS_EVENT_OPTS.AUTHENTICATION_CONNECT_CANCELED, - this.getTrackingParams() - ); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.CONNECT_REQUEST_CANCELLED, this.getAnalyticsParams()); + this.props.onCancel(); this.showWalletConnectNotification(); }; @@ -202,7 +209,8 @@ class AccountApproval extends PureComponent { const mapStateToProps = state => ({ accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts || {}).length, tokensLength: state.engine.backgroundState.AssetsController.tokens.length, - networkType: state.engine.backgroundState.NetworkController.provider.type + networkType: state.engine.backgroundState.NetworkController.provider.type, + chainId: state.engine.backgroundState.NetworkController.provider.chainId }); export default connect(mapStateToProps)(AccountApproval); diff --git a/app/components/UI/AddCustomCollectible/index.js b/app/components/UI/AddCustomCollectible/index.js index fb538ef8435..a86952e6b8d 100644 --- a/app/components/UI/AddCustomCollectible/index.js +++ b/app/components/UI/AddCustomCollectible/index.js @@ -9,6 +9,7 @@ import ActionView from '../ActionView'; import { isSmartContractAddress } from '../../../util/transactions'; import Device from '../../../util/Device'; import { connect } from 'react-redux'; +import AnalyticsV2 from '../../../util/analyticsV2'; const styles = StyleSheet.create({ wrapper: { @@ -72,6 +73,17 @@ class AddCustomCollectible extends PureComponent { this.mounted = false; }; + getAnalyticsParams = () => { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const { address } = this.state; + return { + collectible_address: { value: address, anonymous: true }, + network_name: type, + chain_id: chainId + }; + }; + addCollectible = async () => { if (!(await this.validateCustomCollectible())) return; const isOwner = await this.validateCollectibleOwnership(); @@ -82,6 +94,9 @@ class AddCustomCollectible extends PureComponent { const { AssetsController } = Engine.context; const { address, tokenId } = this.state; AssetsController.addCollectible(address, tokenId); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.COLLECTIBLE_ADDED, this.getAnalyticsParams()); + this.props.navigation.goBack(); }; diff --git a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap index 95fd845dbd9..0890e1abbcc 100644 --- a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap @@ -1,7 +1,412 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AddCustomNetwork should render correctly 1`] = ` - - - + + + + + Allow this site to add a network? + + + This allows this network to be used within MetaMask. + + + + MetaMask does not verify custom networks or their security. + + + Learn about + + + scams and network security risks + + . + + + + + Display name + + + + + + Chain ID + + + + + + Network URL + + + + + + + + View details + + + + + + Cancel + + + Approve + + + + `; diff --git a/app/components/UI/AddCustomNetwork/index.js b/app/components/UI/AddCustomNetwork/index.js index 2d27dfea06a..29f9dcb0117 100644 --- a/app/components/UI/AddCustomNetwork/index.js +++ b/app/components/UI/AddCustomNetwork/index.js @@ -9,7 +9,6 @@ import Device from '../../../util/Device'; import Icon from 'react-native-vector-icons/FontAwesome'; import Alert from '../../Base/Alert'; import EvilIcons from 'react-native-vector-icons/EvilIcons'; -import { withNavigation } from 'react-navigation'; import Text from '../../Base/Text'; const styles = StyleSheet.create({ @@ -122,7 +121,7 @@ const styles = StyleSheet.create({ /** * Account access approval component */ -const AddCustomNetwork = ({ customNetworkInformation, currentPageInformation, navigation, onCancel, onConfirm }) => { +const AddCustomNetwork = ({ customNetworkInformation, currentPageInformation, onCancel, onConfirm }) => { const [viewDetails, setViewDetails] = useState(false); /** @@ -326,11 +325,7 @@ AddCustomNetwork.propTypes = { /** * Object containing info of the network to add */ - customNetworkInformation: PropTypes.object, - /** - * Object that represents the navigator - */ - navigation: PropTypes.object + customNetworkInformation: PropTypes.object }; -export default withNavigation(AddCustomNetwork); +export default AddCustomNetwork; diff --git a/app/components/UI/AddCustomNetwork/index.test.js b/app/components/UI/AddCustomNetwork/index.test.js index e308830dfb4..117e29f8e90 100644 --- a/app/components/UI/AddCustomNetwork/index.test.js +++ b/app/components/UI/AddCustomNetwork/index.test.js @@ -4,7 +4,7 @@ import { shallow } from 'enzyme'; describe('AddCustomNetwork', () => { it('should render correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/UI/AddCustomToken/index.js b/app/components/UI/AddCustomToken/index.js index 365b1d03791..f84627b02d1 100644 --- a/app/components/UI/AddCustomToken/index.js +++ b/app/components/UI/AddCustomToken/index.js @@ -7,6 +7,7 @@ import { strings } from '../../../../locales/i18n'; import { isValidAddress } from 'ethereumjs-util'; import ActionView from '../ActionView'; import { isSmartContractAddress } from '../../../util/transactions'; +import AnalyticsV2 from '../../../util/analyticsV2'; const styles = StyleSheet.create({ wrapper: { @@ -50,11 +51,27 @@ export default class AddCustomToken extends PureComponent { navigation: PropTypes.object }; + getAnalyticsParams = () => { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const { address, symbol } = this.state; + return { + token_address: { value: address, anonymous: true }, + token_symbol: { value: symbol, anonymous: true }, + network_name: type, + chain_id: chainId, + source: 'Custom token' + }; + }; + addToken = async () => { if (!(await this.validateCustomToken())) return; const { AssetsController } = Engine.context; const { address, symbol, decimals } = this.state; await AssetsController.addToken(address, symbol, decimals); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams()); + // Clear state before closing this.setState( { diff --git a/app/components/UI/ApproveTransactionReview/index.js b/app/components/UI/ApproveTransactionReview/index.js index 37831fc34f0..7ae908d45d4 100644 --- a/app/components/UI/ApproveTransactionReview/index.js +++ b/app/components/UI/ApproveTransactionReview/index.js @@ -26,6 +26,7 @@ import { import { showAlert } from '../../../actions/alert'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; +import AnalyticsV2 from '../../../util/analyticsV2'; import TransactionHeader from '../../UI/TransactionHeader'; import AccountInfoCard from '../../UI/AccountInfoCard'; import IonicIcon from 'react-native-vector-icons/Ionicons'; @@ -227,7 +228,11 @@ class ApproveTransactionReview extends PureComponent { /** * True if transaction is over the available funds */ - over: PropTypes.bool + over: PropTypes.bool, + /** + * Function to set analytics params + */ + onSetAnalyticsParams: PropTypes.func }; state = { @@ -276,16 +281,22 @@ class ApproveTransactionReview extends PureComponent { const totalGas = gas?.mul(gasPrice); const { name: method } = await getMethodData(data); - this.setState({ - host, - method, - originalApproveAmount: approveAmount, - tokenSymbol, - token: { symbol: tokenSymbol, decimals: tokenDecimals }, - totalGas: renderFromWei(totalGas), - totalGasFiat: weiToFiatNumber(totalGas, conversionRate), - spenderAddress - }); + this.setState( + { + host, + method, + originalApproveAmount: approveAmount, + tokenSymbol, + token: { symbol: tokenSymbol, decimals: tokenDecimals }, + totalGas: renderFromWei(totalGas), + totalGasFiat: weiToFiatNumber(totalGas, conversionRate), + spenderAddress, + encodedAmount + }, + () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.APPROVAL_STARTED, this.getAnalyticsParams()); + } + ); }; componentDidUpdate(previousProps) { @@ -306,6 +317,29 @@ class ApproveTransactionReview extends PureComponent { } } + getAnalyticsParams = () => { + const { activeTabUrl, transaction, onSetAnalyticsParams } = this.props; + const { tokenSymbol, originalApproveAmount, encodedAmount } = this.state; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const isDapp = !Object.values(AppConstants.DEEPLINKS).includes(transaction.origin); + const unlimited = encodedAmount === 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + const params = { + dapp_host_name: transaction?.origin, + dapp_url: isDapp ? activeTabUrl : undefined, + network_name: type, + chain_id: chainId, + active_currency: { value: tokenSymbol, anonymous: true }, + number_tokens_requested: { value: originalApproveAmount, anonymous: true }, + unlimited_permission_requested: unlimited, + referral_type: isDapp ? 'dapp' : transaction?.origin + }; + // Send analytics params to parent component so it's available when cancelling and confirming + onSetAnalyticsParams && onSetAnalyticsParams(params); + + return params; + }; + trackApproveEvent = event => { const { transaction, tokensLength, accountsLength, providerType } = this.props; InteractionManager.runAfterInteractions(() => { @@ -392,6 +426,7 @@ class ApproveTransactionReview extends PureComponent { const newApprovalTransaction = { ...transaction, data: approvalData }; setTransactionObject(newApprovalTransaction); this.toggleEditPermission(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.APPROVAL_PERMISSION_UPDATED, this.getAnalyticsParams()); }; renderEditPermission = () => { @@ -402,6 +437,7 @@ class ApproveTransactionReview extends PureComponent { spendLimitCustomValue, originalApproveAmount } = this.state; + return ( { + const { onConfirm } = this.props; + onConfirm && onConfirm(); + }; + gotoFaucet = () => { const mmFaucetUrl = 'https://faucet.metamask.io/'; InteractionManager.runAfterInteractions(() => { @@ -534,8 +574,8 @@ class ApproveTransactionReview extends PureComponent { confirmButtonMode="confirm" cancelText={strings('transaction.reject')} confirmText={strings('transactions.approve')} - onCancelPress={this.props.onCancel} - onConfirmPress={this.props.onConfirm} + onCancelPress={this.onCancelPress} + onConfirmPress={this.onConfirmPress} > { + const { advancedCustomGas, chainId, networkType, view, analyticsParams } = this.props; + const { gasSpeedSelected } = this.state; + return { + ...(analyticsParams || {}), + network_name: networkType, + chain_id: chainId, + function_type: { value: view, anonymous: true }, + gas_mode: { value: advancedCustomGas ? 'Advanced' : 'Basic', anonymous: true }, + speed_set: { value: advancedCustomGas ? undefined : gasSpeedSelected, anonymous: true } + }; + }; + //Handle gas fee selection when save button is pressed instead of everytime a change is made, otherwise cannot switch back to review mode if there is an error saveCustomGasSelection = () => { const { gasSpeedSelected, customGasLimit, customGasPrice } = this.state; @@ -524,7 +550,9 @@ class CustomGas extends PureComponent { if (gasSpeedSelected === 'fast') handleGasFeeSelection(gas, apiEstimateModifiedToWEI(fastGwei), noGasWarning, mode); } + review(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.GAS_FEE_CHANGED, this.getAnalyticsParams()); }; renderCustomGasSelector = () => { @@ -787,6 +815,7 @@ const mapStateToProps = (state, props) => ({ currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, ticker: state.engine.backgroundState.NetworkController.provider.ticker, transaction: props.customTransaction || getNormalizedTxState(state), + networkType: state.engine.backgroundState.NetworkController.provider.type, chainId: state.engine.backgroundState.NetworkController.provider.chainId }); diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 4b22602b96f..052ed51e878 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -41,6 +41,7 @@ import WhatsNewModal from '../WhatsNewModal'; import InvalidCustomNetworkAlert from '../InvalidCustomNetworkAlert'; import { RPC } from '../../../constants/network'; import { findBottomTabRouteNameFromNavigatorState, findRouteNameFromNavigatorState } from '../../../util/general'; +import { ANALYTICS_EVENTS_V2 } from '../../../util/analyticsV2'; const styles = StyleSheet.create({ wrapper: { @@ -497,7 +498,7 @@ class DrawerView extends PureComponent { showWallet = () => { this.props.navigation.navigate('WalletTabHome'); this.hideDrawer(); - this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_WALLET); + this.trackEvent(ANALYTICS_EVENTS_V2.WALLET_OPENED); }; goToTransactionHistory = () => { diff --git a/app/components/UI/MessageSign/index.js b/app/components/UI/MessageSign/index.js index d8d4a6c7f2c..0d5ed009e10 100644 --- a/app/components/UI/MessageSign/index.js +++ b/app/components/UI/MessageSign/index.js @@ -8,6 +8,8 @@ import ExpandedMessage from '../SignatureRequest/ExpandedMessage'; import NotificationManager from '../../../core/NotificationManager'; import { strings } from '../../../../locales/i18n'; import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect'; +import URL from 'url-parse'; +import AnalyticsV2 from '../../../util/analyticsV2'; const styles = StyleSheet.create({ expandedMessage: { @@ -59,6 +61,24 @@ export default class MessageSign extends PureComponent { truncateMessage: false }; + getAnalyticsParams = () => { + const { currentPageInformation } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const url = new URL(currentPageInformation.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + sign_type: 'eth' + }; + }; + + componentDidMount = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_STARTED, this.getAnalyticsParams()); + }; + showWalletConnectNotification = (messageParams = {}, confirmation = false) => { InteractionManager.runAfterInteractions(() => { messageParams.origin && @@ -93,11 +113,13 @@ export default class MessageSign extends PureComponent { }; cancelSignature = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.rejectMessage(); this.props.onCancel(); }; confirmSignature = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.signMessage(); this.props.onConfirm(); }; diff --git a/app/components/UI/NetworkList/index.js b/app/components/UI/NetworkList/index.js index 6bbc6d000a7..142e11087d9 100644 --- a/app/components/UI/NetworkList/index.js +++ b/app/components/UI/NetworkList/index.js @@ -7,8 +7,7 @@ import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import Networks, { getAllNetworks, isSafeChainId } from '../../../util/networks'; import { connect } from 'react-redux'; -import Analytics from '../../../core/Analytics'; -import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; +import AnalyticsV2 from '../../../util/analyticsV2'; import { MAINNET, RPC } from '../../../constants/network'; const styles = StyleSheet.create({ @@ -142,7 +141,6 @@ export class NetworkList extends PureComponent { getOtherNetworks = () => getAllNetworks().slice(1); onNetworkChange = type => { - const { provider } = this.props; requestAnimationFrame(() => { this.props.onClose(false); InteractionManager.runAfterInteractions(() => { @@ -153,9 +151,11 @@ export class NetworkList extends PureComponent { setTimeout(() => { Engine.refreshTransactionHistory(); }, 1000); - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.COMMON_SWITCHED_NETWORKS, { - 'From Network': provider.type, - 'To Network': type + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_SWITCHED, { + network_name: type, + chain_id: String(Networks[type].chainId), + source: 'Settings' }); }); }); @@ -169,7 +169,13 @@ export class NetworkList extends PureComponent { const { frequentRpcList } = this.props; const { NetworkController, CurrencyRateController } = Engine.context; const rpc = frequentRpcList.find(({ rpcUrl }) => rpcUrl === rpcTarget); - const { rpcUrl, chainId, ticker, nickname } = rpc; + const { + rpcUrl, + chainId, + ticker, + nickname, + rpcPrefs: { blockExplorerUrl } + } = rpc; // If the network does not have chainId then show invalid custom network alert const chainIdNumber = parseInt(chainId, 10); @@ -181,6 +187,16 @@ export class NetworkList extends PureComponent { CurrencyRateController.configure({ nativeCurrency: ticker }); NetworkController.setRpcTarget(rpcUrl, chainId, ticker, nickname); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_SWITCHED, { + rpc_url: rpcUrl, + chain_id: chainId, + source: 'Settings', + symbol: ticker, + block_explorer_url: blockExplorerUrl, + network_name: 'rpc' + }); + this.props.onClose(false); }; diff --git a/app/components/UI/OptinMetrics/index.js b/app/components/UI/OptinMetrics/index.js index 6461ce64200..0b9dc548b92 100644 --- a/app/components/UI/OptinMetrics/index.js +++ b/app/components/UI/OptinMetrics/index.js @@ -121,8 +121,8 @@ class OptinMetrics extends PureComponent { clearOnboardingEvents: PropTypes.func }; - actionsList = [1, 2, 3, 4, 5].map(value => ({ - action: value <= 2 ? 0 : 1, + actionsList = [1, 2, 3, 4, 5, 6].map(value => ({ + action: value <= 3 ? 0 : 1, description: strings(`privacy_policy.action_description_${value}`) })); diff --git a/app/components/UI/PersonalSign/index.js b/app/components/UI/PersonalSign/index.js index e47027b5414..3be8fd7a319 100644 --- a/app/components/UI/PersonalSign/index.js +++ b/app/components/UI/PersonalSign/index.js @@ -9,6 +9,8 @@ import { util } from '@metamask/controllers'; import NotificationManager from '../../../core/NotificationManager'; import { strings } from '../../../../locales/i18n'; import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect'; +import URL from 'url-parse'; +import AnalyticsV2 from '../../../util/analyticsV2'; const styles = StyleSheet.create({ messageText: { @@ -64,6 +66,24 @@ export default class PersonalSign extends PureComponent { truncateMessage: false }; + getAnalyticsParams = () => { + const { currentPageInformation } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const url = new URL(currentPageInformation.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + sign_type: 'personal' + }; + }; + + componentDidMount = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_STARTED, this.getAnalyticsParams()); + }; + showWalletConnectNotification = (messageParams = {}, confirmation = false) => { InteractionManager.runAfterInteractions(() => { messageParams.origin && @@ -98,11 +118,13 @@ export default class PersonalSign extends PureComponent { }; cancelSignature = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.rejectMessage(); this.props.onCancel(); }; confirmSignature = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.signMessage(); this.props.onConfirm(); }; diff --git a/app/components/UI/SearchTokenAutocomplete/index.js b/app/components/UI/SearchTokenAutocomplete/index.js index 14212559a54..3955e0901b3 100644 --- a/app/components/UI/SearchTokenAutocomplete/index.js +++ b/app/components/UI/SearchTokenAutocomplete/index.js @@ -7,6 +7,7 @@ import ActionView from '../ActionView'; import AssetSearch from '../AssetSearch'; import AssetList from '../AssetList'; import Engine from '../../../core/Engine'; +import AnalyticsV2 from '../../../util/analyticsV2'; const styles = StyleSheet.create({ wrapper: { @@ -44,10 +45,30 @@ export default class SearchTokenAutocomplete extends PureComponent { this.setState({ selectedAsset: asset }); }; + componentDidMount = () => { + this.getAnalyticsParams(); + }; + + getAnalyticsParams = () => { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const { address, symbol } = this.state.selectedAsset; + return { + token_address: { value: address, anonymous: true }, + token_symbol: { value: symbol, anonymous: true }, + network_name: type, + chain_id: chainId, + source: 'Add token dropdown' + }; + }; + addToken = async () => { const { AssetsController } = Engine.context; const { address, symbol, decimals } = this.state.selectedAsset; await AssetsController.addToken(address, symbol, decimals); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams()); + // Clear state before closing this.setState( { diff --git a/app/components/UI/Swaps/components/TransactionsEditionModal.js b/app/components/UI/Swaps/components/TransactionsEditionModal.js index 8bc85366019..a96fafb2ce4 100644 --- a/app/components/UI/Swaps/components/TransactionsEditionModal.js +++ b/app/components/UI/Swaps/components/TransactionsEditionModal.js @@ -136,6 +136,7 @@ function TransactionsEditionModal({ mode={'edit'} customTransaction={selectedQuote.trade} hideSlow + view={'Swaps'} /> )} diff --git a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap index f61fb102fa0..0e329d4cc05 100644 --- a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap @@ -96,7 +96,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` underline={false} upper={false} > - "undefined" + "" { it('should render correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/UI/TransactionEditor/__snapshots__/index.test.js.snap b/app/components/UI/TransactionEditor/__snapshots__/index.test.js.snap index 5653c3a8212..249be5f27d6 100644 --- a/app/components/UI/TransactionEditor/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionEditor/__snapshots__/index.test.js.snap @@ -29,9 +29,20 @@ exports[`TransactionEditor should render correctly 1`] = ` validate={[Function]} /> diff --git a/app/components/UI/TransactionEditor/index.js b/app/components/UI/TransactionEditor/index.js index 62ac6faf8fb..d0b7aa3c761 100644 --- a/app/components/UI/TransactionEditor/index.js +++ b/app/components/UI/TransactionEditor/index.js @@ -9,8 +9,8 @@ import { isValidAddress, toChecksumAddress, BN, addHexPrefix } from 'ethereumjs- import { strings } from '../../../../locales/i18n'; import { connect } from 'react-redux'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { generateTransferData, getNormalizedTxState, getTicker } from '../../../util/transactions'; -import { apiEstimateModifiedToWEI, getBasicGasEstimatesByChainId } from '../../../util/custom-gas'; +import { generateTransferData, getNormalizedTxState, getTicker, getActiveTabUrl } from '../../../util/transactions'; +import { getBasicGasEstimatesByChainId, apiEstimateModifiedToWEI } from '../../../util/custom-gas'; import { setTransactionObject } from '../../../actions/transaction'; import Engine from '../../../core/Engine'; import collectiblesTransferInformation from '../../../util/collectibles-transfer'; @@ -93,7 +93,11 @@ class TransactionEditor extends PureComponent { /** * Current selected ticker */ - ticker: PropTypes.string + ticker: PropTypes.string, + /** + * Active tab URL, the currently active tab url + */ + activeTabUrl: PropTypes.string }; state = { @@ -606,6 +610,16 @@ class TransactionEditor extends PureComponent { return this.setState({ basicGasEstimates, ready: true }); }; + getGasAnalyticsParams = () => { + const { transaction, activeTabUrl } = this.props; + const { selectedAsset } = transaction; + return { + dapp_host_name: transaction?.origin, + dapp_url: activeTabUrl, + active_currency: { value: selectedAsset?.symbol, anonymous: true } + }; + }; + render = () => { const { mode, transactionConfirmed, transaction, onModeChange } = this.props; const { basicGasEstimates, ready, gasError, over } = this.state; @@ -628,6 +642,8 @@ class TransactionEditor extends PureComponent { gasPrice={transaction.gasPrice} gasError={gasError} mode={mode} + view={'Transaction'} + analyticsParams={this.getGasAnalyticsParams()} /> @@ -644,7 +660,8 @@ const mapStateToProps = state => ({ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, tokens: state.engine.backgroundState.AssetsController.tokens, ticker: state.engine.backgroundState.NetworkController.provider.ticker, - transaction: getNormalizedTxState(state) + transaction: getNormalizedTxState(state), + activeTabUrl: getActiveTabUrl(state) }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/UI/TypedSign/index.js b/app/components/UI/TypedSign/index.js index aa09e788723..b850d716c9b 100644 --- a/app/components/UI/TypedSign/index.js +++ b/app/components/UI/TypedSign/index.js @@ -9,6 +9,8 @@ import Device from '../../../util/Device'; import NotificationManager from '../../../core/NotificationManager'; import { strings } from '../../../../locales/i18n'; import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect'; +import AnalyticsV2 from '../../../util/analyticsV2'; +import URL from 'url-parse'; const styles = StyleSheet.create({ messageText: { @@ -73,6 +75,25 @@ export default class TypedSign extends PureComponent { truncateMessage: false }; + getAnalyticsParams = () => { + const { currentPageInformation, messageParams } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const url = new URL(currentPageInformation.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + sign_type: 'typed', + version: messageParams?.version + }; + }; + + componentDidMount = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_STARTED, this.getAnalyticsParams()); + }; + showWalletConnectNotification = (messageParams = {}, confirmation = false) => { InteractionManager.runAfterInteractions(() => { messageParams.origin && @@ -108,11 +129,13 @@ export default class TypedSign extends PureComponent { }; cancelSignature = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.rejectMessage(); this.props.onCancel(); }; confirmSignature = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.signMessage(); this.props.onConfirm(); }; diff --git a/app/components/UI/WatchAssetRequest/index.js b/app/components/UI/WatchAssetRequest/index.js index c8283859300..6670c464d94 100644 --- a/app/components/UI/WatchAssetRequest/index.js +++ b/app/components/UI/WatchAssetRequest/index.js @@ -9,6 +9,8 @@ import { renderFromTokenMinimalUnit } from '../../../util/number'; import TokenImage from '../../UI/TokenImage'; import Device from '../../../util/Device'; import Engine from '../../../core/Engine'; +import URL from 'url-parse'; +import AnalyticsV2 from '../../../util/analyticsV2'; const styles = StyleSheet.create({ root: { @@ -96,7 +98,32 @@ class WatchAssetRequest extends PureComponent { /** * Object containing token balances in the format address => balance */ - contractBalances: PropTypes.object + contractBalances: PropTypes.object, + /** + * Object containing current page title, url, and icon href + */ + currentPageInformation: PropTypes.object + }; + + getAnalyticsParams = () => { + const { + suggestedAssetMeta: { asset }, + currentPageInformation + } = this.props; + + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + + const url = new URL(currentPageInformation?.url); + return { + token_address: asset?.address, + token_symbol: asset?.symbol, + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + source: 'Dapp suggested (watchAsset)' + }; }; componentWillUnmount = async () => { @@ -108,6 +135,7 @@ class WatchAssetRequest extends PureComponent { onConfirm = async () => { const { onConfirm, suggestedAssetMeta } = this.props; const { AssetsController } = Engine.context; + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams()); await AssetsController.acceptWatchAsset(suggestedAssetMeta.id); onConfirm && onConfirm(); }; diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js index 73bceb4ebcd..8d029e2776c 100644 --- a/app/components/Views/Approval/index.js +++ b/app/components/Views/Approval/index.js @@ -11,11 +11,12 @@ import { connect } from 'react-redux'; import NotificationManager from '../../../core/NotificationManager'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; -import { getTransactionReviewActionKey, getNormalizedTxState } from '../../../util/transactions'; +import { getTransactionReviewActionKey, getNormalizedTxState, getActiveTabUrl } from '../../../util/transactions'; import { strings } from '../../../../locales/i18n'; import { safeToChecksumAddress } from '../../../util/address'; import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect'; import Logger from '../../../util/Logger'; +import AnalyticsV2 from '../../../util/analyticsV2'; const REVIEW = 'review'; const EDIT = 'edit'; @@ -62,7 +63,15 @@ class Approval extends PureComponent { /** * Tells whether or not dApp transaction modal is visible */ - dappTransactionModalVisible: PropTypes.bool + dappTransactionModalVisible: PropTypes.bool, + /** + * Active tab URL, the currently active tab url + */ + activeTabUrl: PropTypes.string, + /** + * A string representing the network chainId + */ + chainId: PropTypes.string }; state = { @@ -93,7 +102,8 @@ class Approval extends PureComponent { const { navigation } = this.props; AppState.addEventListener('change', this.handleAppStateChange); navigation && navigation.setParams({ mode: REVIEW, dispatch: this.onModeChange }); - this.trackConfirmScreen(); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.DAPP_TRANSACTION_STARTED, this.getAnalyticsParams()); }; /** @@ -153,6 +163,19 @@ class Approval extends PureComponent { }; }; + getAnalyticsParams = () => { + const { activeTabUrl, chainId, transaction, networkType } = this.props; + const { selectedAsset } = transaction; + return { + dapp_host_name: transaction?.origin, + dapp_url: activeTabUrl, + network_name: networkType, + chain_id: chainId, + active_currency: { value: selectedAsset?.symbol, anonymous: true }, + asset_type: { value: transaction?.assetType, anonymous: true } + }; + }; + /** * Transaction state is erased, ready to create a new clean transaction */ @@ -180,6 +203,7 @@ class Approval extends PureComponent { this.props.toggleDappTransactionModal(); this.state.mode === REVIEW && this.trackOnCancel(); this.showWalletConnectNotification(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.DAPP_TRANSACTION_CANCELLED, this.getAnalyticsParams()); }; /** @@ -224,7 +248,7 @@ class Approval extends PureComponent { Logger.error(error, 'error while trying to send transaction (Approval)'); this.setState({ transactionHandled: false }); } - this.trackOnConfirm(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.DAPP_TRANSACTION_CONFIRMED, this.getAnalyticsParams()); }; /** @@ -313,7 +337,9 @@ class Approval extends PureComponent { const mapStateToProps = state => ({ transaction: getNormalizedTxState(state), transactions: state.engine.backgroundState.TransactionController.transactions, - networkType: state.engine.backgroundState.NetworkController.provider.type + networkType: state.engine.backgroundState.NetworkController.provider.type, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, + activeTabUrl: getActiveTabUrl(state) }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/Views/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js index 3c5be56080d..467167be5ea 100644 --- a/app/components/Views/ApproveView/Approve/index.js +++ b/app/components/Views/ApproveView/Approve/index.js @@ -20,6 +20,7 @@ import NotificationManager from '../../../../core/NotificationManager'; import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; import Logger from '../../../../util/Logger'; +import AnalyticsV2 from '../../../../util/analyticsV2'; const { BNToHex, hexToBN } = util; @@ -92,7 +93,8 @@ class Approve extends PureComponent { warningGasPriceHigh: undefined, ready: false, mode: REVIEW, - over: false + over: false, + analyticsParams: {} }; componentDidMount = () => { @@ -207,7 +209,7 @@ class Approve extends PureComponent { const updatedTx = { ...fullTx, transaction }; await TransactionController.updateTransaction(updatedTx); await TransactionController.approveTransaction(transaction.id); - this.trackApproveEvent(ANALYTICS_EVENT_OPTS.DAPP_APPROVE_SCREEN_APPROVE); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.APPROVAL_COMPLETED, this.state.analyticsParams); } catch (error) { Alert.alert(strings('transactions.transaction_error'), error && error.message, [{ text: 'OK' }]); Logger.error(error, 'error while trying to send transaction (Approve)'); @@ -216,7 +218,7 @@ class Approve extends PureComponent { }; onCancel = () => { - this.trackApproveEvent(ANALYTICS_EVENT_OPTS.DAPP_APPROVE_SCREEN_CANCEL); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.APPROVAL_CANCELLED, this.state.analyticsParams); this.props.toggleApproveModal(false); }; @@ -233,6 +235,20 @@ class Approve extends PureComponent { } }; + setAnalyticsParams = analyticsParams => { + this.setState({ analyticsParams }); + }; + + getGasAnalyticsParams = () => { + const { analyticsParams } = this.state; + + return { + dapp_host_name: analyticsParams?.dapp_host_name, + dapp_url: analyticsParams?.dapp_url, + active_currency: { value: analyticsParams?.active_currency, anonymous: true } + }; + }; + render = () => { const { gasError, basicGasEstimates, mode, ready, over, warningGasPriceHigh } = this.state; const { transaction } = this.props; @@ -261,6 +277,7 @@ class Approve extends PureComponent { onCancel={this.onCancel} onConfirm={this.onConfirm} over={over} + onSetAnalyticsParams={this.setAnalyticsParams} /> @@ -283,8 +302,7 @@ const mapStateToProps = state => ({ transaction: getNormalizedTxState(state), transactions: state.engine.backgroundState.TransactionController.transactions, accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts || {}).length, - tokensLength: state.engine.backgroundState.AssetsController.tokens.length, - providerType: state.engine.backgroundState.NetworkController.provider.type + tokensLength: state.engine.backgroundState.AssetsController.tokens.length }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap index 8d031f91958..20e5dcf04a1 100644 --- a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap +++ b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap @@ -306,6 +306,13 @@ exports[`Browser should render correctly 1`] = ` useNativeDriver={false} > @@ -364,7 +371,7 @@ exports[`Browser should render correctly 1`] = ` swipeThreshold={100} useNativeDriver={false} > - { onCancel={onCancelWatchAsset} onConfirm={onCancelWatchAsset} suggestedAssetMeta={suggestedAssetMeta} + currentPageInformation={{ + title: title.current, + url: getMaskedUrl(url.current), + icon: icon.current + }} /> ); diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index c375afcc526..46ed93ce157 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -54,6 +54,7 @@ import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; import { capitalize } from '../../../../util/format'; import { isMainNet, getNetworkName } from '../../../../util/networks'; +import AnalyticsV2 from '../../../../util/analyticsV2'; const EDIT = 'edit'; const REVIEW = 'review'; @@ -316,8 +317,28 @@ class Confirm extends PureComponent { over: false }; + getAnalyticsParams = () => { + const { selectedAsset } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + return { + active_currency: { value: selectedAsset?.symbol, anonymous: true }, + network_name: type, + chain_id: chainId + }; + }; + + getGasAnalyticsParams = () => { + const { selectedAsset } = this.props; + return { + active_currency: { value: selectedAsset.symbol, anonymous: true } + }; + }; + componentDidMount = async () => { // For analytics + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SEND_TRANSACTION_STARTED, this.getAnalyticsParams()); + const { navigation, providerType } = this.props; await this.handleFetchBasicEstimates(); navigation.setParams({ providerType }); @@ -638,7 +659,6 @@ class Confirm extends PureComponent { const { transactionState: { assetType }, navigation, - providerType, resetTransaction } = this.props; this.setState({ transactionConfirmed: true }); @@ -670,9 +690,10 @@ class Confirm extends PureComponent { assetType }); this.checkRemoveCollectible(); - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SEND_FLOW_CONFIRM_SEND, { - network: providerType - }); + AnalyticsV2.trackEvent( + AnalyticsV2.ANALYTICS_EVENTS.SEND_TRANSACTION_COMPLETED, + this.getAnalyticsParams() + ); resetTransaction(); navigation && navigation.dismiss(); }); @@ -760,6 +781,8 @@ class Confirm extends PureComponent { mode={mode} onPress={this.handleSetGasSpeed} gasSpeedSelected={gasSpeedSelected} + view={'SendTo (Confirm)'} + analyticsParams={this.getGasAnalyticsParams()} /> diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index f8571bb9ee9..61c28898c06 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -17,6 +17,7 @@ import { jsonRpcRequest } from '../../../../../util/jsonRpcRequest'; import Logger from '../../../../../util/Logger'; import { isPrefixedFormattedHexString } from '../../../../../util/number'; import AppConstants from '../../../../../core/AppConstants'; +import AnalyticsV2 from '../../../../../util/analyticsV2'; const styles = StyleSheet.create({ wrapper: { @@ -268,6 +269,17 @@ class NetworkSettings extends PureComponent { blockExplorerUrl }); NetworkController.setRpcTarget(url.href, decimalChainId, ticker, nickname); + + const analyticsParamsAdd = { + rpc_url: url.href, + chain_id: decimalChainId, + source: 'Settings', + symbol: ticker, + block_explorer_url: blockExplorerUrl, + network_name: 'rpc' + }; + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_ADDED, analyticsParamsAdd); + navigation.navigate('WalletView'); } }; diff --git a/app/core/RPCMethods/wallet_addEthereumChain.js b/app/core/RPCMethods/wallet_addEthereumChain.js index d33e50d1870..81dc04582aa 100644 --- a/app/core/RPCMethods/wallet_addEthereumChain.js +++ b/app/core/RPCMethods/wallet_addEthereumChain.js @@ -6,6 +6,7 @@ import Engine from '../Engine'; import { ethErrors } from 'eth-json-rpc-errors'; import { isPrefixedFormattedHexString, isSafeChainId } from '../../util/networks'; import URL from 'url-parse'; +import AnalyticsV2 from '../../util/analyticsV2'; const wallet_addEthereumChain = async ({ req, @@ -111,7 +112,19 @@ const wallet_addEthereumChain = async ({ switchCustomNetworkRequest.current = { resolve, reject }; }); - if (!switchCustomNetworkApprove) throw ethErrors.provider.userRejectedRequest(); + const analyticsParams = { + rpc_url: existingNetwork.rpcUrl, + chain_id: _chainId, + source: 'Custom Network API', + symbol: existingNetwork.ticker, + block_explorer_url: existingNetwork.blockExplorerUrl, + network_name: 'rpc' + }; + + if (!switchCustomNetworkApprove) { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_REQUEST_REJECTED, analyticsParams); + throw ethErrors.provider.userRejectedRequest(); + } CurrencyRateController.configure({ nativeCurrency: existingNetwork.ticker }); NetworkController.setRpcTarget( @@ -120,6 +133,9 @@ const wallet_addEthereumChain = async ({ existingNetwork.ticker, existingNetwork.nickname ); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_SWITCHED, analyticsParams); + res.result = null; return; } @@ -213,6 +229,17 @@ const wallet_addEthereumChain = async ({ } requestData.alert = alert; + const analyticsParamsAdd = { + rpc_url: firstValidRPCUrl, + chain_id: chainIdDecimal, + source: 'Custom Network API', + symbol: ticker, + block_explorer_url: firstValidBlockExplorerUrl, + network_name: 'rpc' + }; + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_REQUESTED, analyticsParamsAdd); + setCustomNetworkToAdd(requestData); setShowAddCustomNetworkDialog(true); @@ -220,12 +247,17 @@ const wallet_addEthereumChain = async ({ addCustomNetworkRequest.current = { resolve, reject }; }); - if (!addCustomNetworkApprove) throw ethErrors.provider.userRejectedRequest(); + if (!addCustomNetworkApprove) { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_REQUEST_REJECTED, analyticsParamsAdd); + throw ethErrors.provider.userRejectedRequest(); + } PreferencesController.addToFrequentRpcList(firstValidRPCUrl, chainIdDecimal, ticker, _chainName, { blockExplorerUrl: firstValidBlockExplorerUrl }); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.NETWORK_ADDED, analyticsParamsAdd); + InteractionManager.runAfterInteractions(() => { setCustomNetworkToSwitch(requestData); setShowSwitchCustomNetworkDialog('new'); diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js new file mode 100644 index 00000000000..11f2d54faaf --- /dev/null +++ b/app/util/analyticsV2.js @@ -0,0 +1,95 @@ +import Analytics from '../core/Analytics'; +import Logger from './Logger'; +import { InteractionManager } from 'react-native'; + +const generateOpt = name => ({ category: name }); + +export const ANALYTICS_EVENTS_V2 = { + // Approval + APPROVAL_STARTED: generateOpt('Approval Started'), + APPROVAL_COMPLETED: generateOpt('Approval Completed'), + APPROVAL_CANCELLED: generateOpt('Approval Cancelled'), + APPROVAL_PERMISSION_UPDATED: generateOpt('Approval Permission Updated'), + // Fee changed + GAS_FEE_CHANGED: generateOpt('Gas Fee Changed'), + // Dapp Transaction + DAPP_TRANSACTION_STARTED: generateOpt('Dapp Transaction Started'), + DAPP_TRANSACTION_COMPLETED: generateOpt('Dapp Transaction Completed'), + DAPP_TRANSACTION_CANCELLED: generateOpt('Dapp Transaction Cancelled'), + // Sign request + SIGN_REQUEST_STARTED: generateOpt('Sign Request Started'), + SIGN_REQUEST_COMPLETED: generateOpt('Sign Request Completed'), + SIGN_REQUEST_CANCELLED: generateOpt('Sign Request Cancelled'), + // Connect request + CONNECT_REQUEST_STARTED: generateOpt('Connect Request Started'), + CONNECT_REQUEST_COMPLETED: generateOpt('Connect Request Completed'), + CONNECT_REQUEST_CANCELLED: generateOpt('Connect Request Cancelled'), + // Wallet + WALLET_OPENED: generateOpt('Wallet Opened'), + TOKEN_ADDED: generateOpt('Token Added'), + COLLECTIBLE_ADDED: generateOpt('Collectible Added'), + // Network + NETWORK_SWITCHED: generateOpt('Network Switched'), + NETWORK_ADDED: generateOpt('Network Added'), + NETWORK_REQUESTED: generateOpt('Network Requested'), + NETWORK_REQUEST_REJECTED: generateOpt('Network Request Rejected'), + NETWORK_SWITCH_REJECTED: generateOpt('Network Switch Rejected'), + // Send transaction + SEND_TRANSACTION_STARTED: generateOpt('Send Transaction Started'), + SEND_TRANSACTION_COMPLETED: generateOpt('Send Transaction Completed') +}; + +/** + * This takes params with the following structure: + * { foo : 'this is not anonymous', bar: {value: 'this is anonymous', anonymous: true} } + * @param {String} eventName + * @param {Object} params + */ +export const trackEventV2 = (eventName, params) => { + try { + InteractionManager.runAfterInteractions(() => { + if (!params) { + Analytics.trackEvent(eventName); + } + + const userParams = {}; + const anonymousParams = {}; + + for (const key in params) { + const property = params[key]; + + if (typeof property === 'string' || property instanceof String) { + // Non-anonymous properties - add to both + userParams[key] = property; + anonymousParams[key] = property; + } else if (typeof property === 'object') { + if (property.anonymous) { + // Anonymous property - add only to anonymous params + anonymousParams[key] = property.value; + } else { + // Non-anonymous property - add to both + userParams[key] = property.value; + anonymousParams[key] = property.value; + } + } + } + + // Log all non-anonymous properties + if (Object.keys(userParams).length) { + Analytics.trackEventWithParameters(eventName, userParams); + } + + // Log all anonymous properties + if (Object.keys(anonymousParams).length) { + Analytics.trackEventWithParameters(eventName, anonymousParams, true); + } + }); + } catch (error) { + Logger.error(error, 'Error logging analytics'); + } +}; + +export default { + ANALYTICS_EVENTS: ANALYTICS_EVENTS_V2, + trackEvent: trackEventV2 +}; diff --git a/locales/languages/en.json b/locales/languages/en.json index c51c0708d17..5f5168fa389 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -328,9 +328,10 @@ "description_content_2": "MetaMask will...", "action_description_1": "Always allow you to opt-out via Settings", "action_description_2": "Send anonymized click & pageview events", - "action_description_3": "Never collect keys, addresses, transactions, balances, hashes, or any personal information", - "action_description_4": "Never collect your IP address", - "action_description_5": "Never sell data for profit. Ever!" + "action_description_3": "Send country, region, city data (not specific location)", + "action_description_4": "Never collect keys, addresses, transactions, balances, hashes, or any personal information", + "action_description_5": "Never collect your IP address", + "action_description_6": "Never sell data for profit. Ever!" }, "token": { "token_symbol": "Token Symbol", diff --git a/locales/languages/es.json b/locales/languages/es.json index f8c08c1d07a..04884442541 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -314,9 +314,9 @@ "description_content_2": "MetaMask…", "action_description_1": "Siempre le permitirá optar por no participar a través de Configuración", "action_description_2": "Enviará eventos de vistas de página y clics anónimos", - "action_description_3": "Nunca recopilará claves, direcciones, transacciones, saldos, hashes o cualquier otra información personal", - "action_description_4": "Nunca recopilará su dirección IP", - "action_description_5": "Nunca venderá datos con afán de lucro. ¡Jamás!" + "action_description_4": "Nunca recopilará claves, direcciones, transacciones, saldos, hashes o cualquier otra información personal", + "action_description_5": "Nunca recopilará su dirección IP", + "action_description_6": "Nunca venderá datos con afán de lucro. ¡Jamás!" }, "token": { "token_symbol": "Símbolo del token", diff --git a/locales/languages/hi-in.json b/locales/languages/hi-in.json index afb8c033072..f567a98a491 100644 --- a/locales/languages/hi-in.json +++ b/locales/languages/hi-in.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask निम्न चीज़ें करेगा...", "action_description_1": "हमेशा आपको सेटिंग्स के माध्यम से ऑप्ट-आउट करने की अनुमति देगा", "action_description_2": "बेनाम क्लिक और पेजव्यू ईवेंट भेजेगा", - "action_description_3": "कुंजी, पते, लेनदेन, शेषराशि, हैश या कोई भी व्यक्तिगत जानकारी कभी एकत्र नहीं करेगा", - "action_description_4": "आपका IP पता एकत्र नहीं करेगा", - "action_description_5": "लाभ के लिए डेटा कभी नहीं बेचेगा। हमेशा!" + "action_description_4": "कुंजी, पते, लेनदेन, शेषराशि, हैश या कोई भी व्यक्तिगत जानकारी कभी एकत्र नहीं करेगा", + "action_description_5": "आपका IP पता एकत्र नहीं करेगा", + "action_description_6": "लाभ के लिए डेटा कभी नहीं बेचेगा। हमेशा!" }, "token": { "token_symbol": "टोकन का प्रतीक", diff --git a/locales/languages/id-id.json b/locales/languages/id-id.json index 202b22b59db..317a524de5b 100644 --- a/locales/languages/id-id.json +++ b/locales/languages/id-id.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask akan...", "action_description_1": "Selalu izinkan Anda untuk menyisih melalui Pengaturan", "action_description_2": "Kirim kejadian pageview & klik anonim", - "action_description_3": "Jangan mengumpulkan kunci, alamat, transaksi, saldo, hash, atau informasi pribadi lainnya", - "action_description_4": "Jangan mengumpulkan alamat IP Anda", - "action_description_5": "Jangan menjual data untuk mendapatkan keuntungan. Selamanya!" + "action_description_4": "Jangan mengumpulkan kunci, alamat, transaksi, saldo, hash, atau informasi pribadi lainnya", + "action_description_5": "Jangan mengumpulkan alamat IP Anda", + "action_description_6": "Jangan menjual data untuk mendapatkan keuntungan. Selamanya!" }, "token": { "token_symbol": "Simbol Token", diff --git a/locales/languages/ja-jp.json b/locales/languages/ja-jp.json index 6cb7b5c2e8e..268607c8df7 100644 --- a/locales/languages/ja-jp.json +++ b/locales/languages/ja-jp.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask が実行する内容...", "action_description_1": "お客様がいつでも設定からオプトアウトできるようにします", "action_description_2": "匿名化されたクリック イベントとページビュー イベントを送信します", - "action_description_3": "キー、アドレス、トランザクション、残高、ハッシュなど、いかなる個人情報も収集しません", - "action_description_4": "お客様の IP アドレスを収集することはありません", - "action_description_5": "営利目的でデータを販売することは決してありません。" + "action_description_4": "キー、アドレス、トランザクション、残高、ハッシュなど、いかなる個人情報も収集しません", + "action_description_5": "お客様の IP アドレスを収集することはありません", + "action_description_6": "営利目的でデータを販売することは決してありません。" }, "token": { "token_symbol": "トークン シンボル", diff --git a/locales/languages/ko-kr.json b/locales/languages/ko-kr.json index 5450959fe90..71a333aa5cf 100644 --- a/locales/languages/ko-kr.json +++ b/locales/languages/ko-kr.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask에서는...", "action_description_1": "언제든 설정을 통해 옵트아웃이 가능합니다.", "action_description_2": "익명화된 클릭 및 페이지뷰 이벤트를 보냅니다.", - "action_description_3": "키, 주소, 거래, 잔액, 해시 또는 개인 정보는 절대 수집하지 않습니다.", - "action_description_4": "IP 주소를 수집하지 않습니다.", - "action_description_5": "수익을 위해 데이터를 판매하지 않습니다. 절대!" + "action_description_4": "키, 주소, 거래, 잔액, 해시 또는 개인 정보는 절대 수집하지 않습니다.", + "action_description_5": "IP 주소를 수집하지 않습니다.", + "action_description_6": "수익을 위해 데이터를 판매하지 않습니다. 절대!" }, "token": { "token_symbol": "토큰 기호", diff --git a/locales/languages/pt-br.json b/locales/languages/pt-br.json index 94d4a9ba48f..999173788c9 100644 --- a/locales/languages/pt-br.json +++ b/locales/languages/pt-br.json @@ -313,9 +313,9 @@ "description_content_2": "O MetaMask...", "action_description_1": "Sempre permitirá que você cancele o envio dos dados, via Configurações", "action_description_2": "Enviará eventos anonimizados de cliques e visualização de página", - "action_description_3": "Jamais coletará chaves, endereços, transações, saldos, hashes ou qualquer outra informação pessoal", - "action_description_4": "Jamais coletará seu endereço IP", - "action_description_5": "Nunca venderá dados em troca de lucro. Jamais!" + "action_description_4": "Jamais coletará chaves, endereços, transações, saldos, hashes ou qualquer outra informação pessoal", + "action_description_5": "Jamais coletará seu endereço IP", + "action_description_6": "Nunca venderá dados em troca de lucro. Jamais!" }, "token": { "token_symbol": "Símbolo do token", diff --git a/locales/languages/ru-ru.json b/locales/languages/ru-ru.json index abbaf14093b..06c33e2121c 100644 --- a/locales/languages/ru-ru.json +++ b/locales/languages/ru-ru.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask будет...", "action_description_1": "Всегда разрешать вам отказаться через настройки", "action_description_2": "Отправлять анонимизированные события кликов и просмотров страниц", - "action_description_3": "Никогда не хранить ключи, адреса, транзакции, балансы, хэши или любую персональную информацию", - "action_description_4": "Никогда не сохранять ваш IP-адрес", - "action_description_5": "Никогда не продавать данные для прибыли. Никогда!" + "action_description_4": "Никогда не хранить ключи, адреса, транзакции, балансы, хэши или любую персональную информацию", + "action_description_5": "Никогда не сохранять ваш IP-адрес", + "action_description_6": "Никогда не продавать данные для прибыли. Никогда!" }, "token": { "token_symbol": "Символ токена", diff --git a/locales/languages/tl.json b/locales/languages/tl.json index 64987e9058d..83704061166 100644 --- a/locales/languages/tl.json +++ b/locales/languages/tl.json @@ -313,9 +313,9 @@ "description_content_2": "Gagawin ng MetaMask ang sumusunod...", "action_description_1": "Palagi kang papayagang mag-opt out sa pamamagitan ng Mga Setting", "action_description_2": "Magpapadala ng mga anonymous na kaganapang pag-click at pagtingin sa page", - "action_description_3": "Huwag kailanman mangolekta ng mga key, address, transaksyon, balanse, hash, o anumang personal na impormasyon", - "action_description_4": "Huwag kailanman kolektahin ang iyong IP address", - "action_description_5": "Huwag kailanman magbenta ng data para pagkakitaan. Kahit kailan!" + "action_description_4": "Huwag kailanman mangolekta ng mga key, address, transaksyon, balanse, hash, o anumang personal na impormasyon", + "action_description_5": "Huwag kailanman kolektahin ang iyong IP address", + "action_description_6": "Huwag kailanman magbenta ng data para pagkakitaan. Kahit kailan!" }, "token": { "token_symbol": "Simbolo ng Token", diff --git a/locales/languages/vi-vn.json b/locales/languages/vi-vn.json index d5986aeeaa4..4a45d238326 100644 --- a/locales/languages/vi-vn.json +++ b/locales/languages/vi-vn.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask sẽ...", "action_description_1": "Luôn cho phép bạn chọn không tham gia thông qua phần Cài đặt", "action_description_2": "Gửi các lượt nhấp và xem trang đã được ẩn danh", - "action_description_3": "Không bao giờ thu thập mã khóa, địa chỉ, giao dịch, số dư, mã băm hoặc bất kỳ thông tin cá nhân nào", - "action_description_4": "Không bao giờ thu thập địa chỉ IP của bạn", - "action_description_5": "Không bao giờ bán dữ liệu để thu lợi. Tuyệt đối không bao giờ!" + "action_description_4": "Không bao giờ thu thập mã khóa, địa chỉ, giao dịch, số dư, mã băm hoặc bất kỳ thông tin cá nhân nào", + "action_description_5": "Không bao giờ thu thập địa chỉ IP của bạn", + "action_description_6": "Không bao giờ bán dữ liệu để thu lợi. Tuyệt đối không bao giờ!" }, "token": { "token_symbol": "Ký hiệu token", diff --git a/locales/languages/zh-cn.json b/locales/languages/zh-cn.json index 09dc8d5b315..99fb869be60 100644 --- a/locales/languages/zh-cn.json +++ b/locales/languages/zh-cn.json @@ -313,9 +313,9 @@ "description_content_2": "MetaMask...", "action_description_1": "始终允许您通过“设置”选择退出", "action_description_2": "发送匿名化点击和页面浏览事件", - "action_description_3": "决不收集密钥、地址、交易、余额、哈希或任何个人信息", - "action_description_4": "决不收集您的 IP 地址", - "action_description_5": "决不出售数据牟利。绝对不会!" + "action_description_4": "决不收集密钥、地址、交易、余额、哈希或任何个人信息", + "action_description_5": "决不收集您的 IP 地址", + "action_description_6": "决不出售数据牟利。绝对不会!" }, "token": { "token_symbol": "代币符号", From ade0934d3541460994fc92331fc83a92ecc914d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 7 Apr 2021 18:30:55 -0400 Subject: [PATCH 05/51] v2.1.0 (#2481) * bumps * changelog --- CHANGELOG.md | 22 ++++++++++++++++++++++ android/app/build.gradle | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90310404a41..a0122d10dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## Current Develop Branch + +## v2.1.0 - Apr 12 2021 +- [#2456](https://github.com/MetaMask/metamask-mobile/pull/2456): Analytics v2 (priority 1) +- [#2408](https://github.com/MetaMask/metamask-mobile/pull/2408): Fix/gas estimations +- [#2479](https://github.com/MetaMask/metamask-mobile/pull/2479): remove controllers tgz +- [#2441](https://github.com/MetaMask/metamask-mobile/pull/2441): Improvement/assets by chainid +- [#2442](https://github.com/MetaMask/metamask-mobile/pull/2442): Improvement/chain ticker +- [#2372](https://github.com/MetaMask/metamask-mobile/pull/2372): Remove instapay +- [#2467](https://github.com/MetaMask/metamask-mobile/pull/2467): Fix iOS build +- [#2084](https://github.com/MetaMask/metamask-mobile/pull/2084): Migrate from AsyncStorage to FileStorage +- [#2443](https://github.com/MetaMask/metamask-mobile/pull/2443): Update terms and privacy links +- [#2318](https://github.com/MetaMask/metamask-mobile/pull/2318): Add custom network rpc API +- [#2306](https://github.com/MetaMask/metamask-mobile/pull/2306): Feature/high gas warn +- [#2463](https://github.com/MetaMask/metamask-mobile/pull/2463): update pods +- [#2448](https://github.com/MetaMask/metamask-mobile/pull/2448): Add resolution for netmask +- [#2445](https://github.com/MetaMask/metamask-mobile/pull/2445): Add resolution for y18n +- [#2404](https://github.com/MetaMask/metamask-mobile/pull/2404): Bump react-native-branch from 5.0.0 to 5.0.1 +- [#2439](https://github.com/MetaMask/metamask-mobile/pull/2439): json-rpc-engine@6.1.0 +- [#2413](https://github.com/MetaMask/metamask-mobile/pull/2413): remove "git add" per husky warning +- [#2431](https://github.com/MetaMask/metamask-mobile/pull/2431): Update BN import + ## v2.0.1 - Mar 24 2021 - [#2430](https://github.com/MetaMask/metamask-mobile/pull/2430): Fix/send to style - [#2426](https://github.com/MetaMask/metamask-mobile/pull/2426): bugfix/allow seedphrases when locked diff --git a/android/app/build.gradle b/android/app/build.gradle index bd75d063563..c2f664d7216 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -166,8 +166,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 49 - versionName "2.0.1" + versionCode 50 + versionName "2.1.0" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index d610c89a9bf..1fdbd817742 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 605; + CURRENT_PROJECT_VERSION = 606; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -882,7 +882,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.0.1; + MARKETING_VERSION = 2.1.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 605; + CURRENT_PROJECT_VERSION = 606; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( @@ -945,7 +945,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.0.1; + MARKETING_VERSION = 2.1.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index c83ce6f9832..c6693548d73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "2.0.1", + "version": "2.1.0", "private": true, "scripts": { "watch": "./scripts/build.sh watcher watch", From c04d982928cdeaad24a1bea8219a2554e11bddd7 Mon Sep 17 00:00:00 2001 From: ricky Date: Fri, 9 Apr 2021 02:27:29 -0400 Subject: [PATCH 06/51] Add scripts for generating and verifying SHA 512 checksums (#2168) * Add scripts for generating and verifying SHA 512 checksums * Update checksum scripts * Rename to NON_EMPTY since that's what it actually is :sweat_smile: * Add checksum to prerelease * Address feedback * only gen checksum if apk exists * add shellscript * fix .sh file --- app/core/Engine.js | 4 ++-- package.json | 2 ++ scripts/build.sh | 2 ++ scripts/checksum.sh | 7 +++++++ 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100755 scripts/checksum.sh diff --git a/app/core/Engine.js b/app/core/Engine.js index c0f46a229ab..d1ba8c936ed 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -33,7 +33,7 @@ import contractMap from '@metamask/contract-metadata'; import Logger from '../util/Logger'; import { LAST_INCOMING_TX_BLOCK_INFO } from '../constants/storage'; -const EMPTY = 'EMPTY'; +const NON_EMPTY = 'NON_EMPTY'; const encryptor = new Encryptor(); let currentChainId; @@ -79,7 +79,7 @@ class Engine { new PersonalMessageManager(), new MessageManager(), new NetworkController({ - infuraProjectId: process.env.MM_INFURA_PROJECT_ID || EMPTY, + infuraProjectId: process.env.MM_INFURA_PROJECT_ID || NON_EMPTY, providerConfig: { static: { eth_sendTransaction: async (payload, next, end) => { diff --git a/package.json b/package.json index c6693548d73..61b3faa24c6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "build:announce": "node ./scripts/metamask-bot-build-announce.js", "build:android:release": "./scripts/build.sh android release", "build:android:release:e2e": "./scripts/build.sh android releaseE2E", + "build:android:checksum": "./scripts/checksum.sh", + "build:android:checksum:verify": "shasum -a 512 -c sha512sums.txt", "build:android:pre-release": "./scripts/build.sh android release --pre", "build:android:pre-release:bundle": "GENERATE_BUNDLE=true ./scripts/build.sh android release --pre", "build:ios:release": "./scripts/build.sh ios release", diff --git a/scripts/build.sh b/scripts/build.sh index 371c9d243b3..67797024d0e 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -235,6 +235,8 @@ buildAndroidRelease(){ if [ "$PRE_RELEASE" = true ] ; then # Generate sourcemaps yarn sourcemaps:android + # Generate checksum + yarn build:android:checksum fi if [ "$PRE_RELEASE" = false ] ; then diff --git a/scripts/checksum.sh b/scripts/checksum.sh new file mode 100755 index 00000000000..e5d27b93510 --- /dev/null +++ b/scripts/checksum.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +FILE=./android/app/build/outputs/apk/release/app-release.apk + +if test -f "$FILE"; then + shasum -a 512 "$FILE" > ./android/app/build/outputs/apk/release/sha512sums.txt +fi; From cba89a2852365719fc7be65e8f573876f9b1102f Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 9 Apr 2021 16:52:08 +0100 Subject: [PATCH 07/51] Fix/analytics v1 priority1 (#2487) * Fix anonymous properties * Added try catch to all analytics * Spanish translation * Bump version and changelog * Removed collectible address --- CHANGELOG.md | 1 + app/components/Nav/Main/index.js | 2 +- app/components/UI/AccountApproval/index.js | 20 +++++---- .../UI/AddCustomCollectible/index.js | 18 ++++---- app/components/UI/AddCustomToken/index.js | 24 +++++----- .../UI/ApproveTransactionReview/index.js | 44 ++++++++++--------- app/components/UI/CustomGas/index.js | 24 +++++----- app/components/UI/MessageSign/index.js | 30 +++++++------ app/components/UI/PersonalSign/index.js | 31 +++++++------ .../UI/SearchTokenAutocomplete/index.js | 24 +++++----- app/components/UI/TransactionEditor/index.js | 18 +++++--- app/components/UI/TypedSign/index.js | 32 ++++++++------ app/components/UI/WatchAssetRequest/index.js | 36 ++++++++------- app/components/Views/Approval/index.js | 24 +++++----- .../Views/ApproveView/Approve/index.js | 16 ++++--- .../Views/SendFlow/Confirm/index.js | 32 +++++++++----- .../RPCMethods/wallet_addEthereumChain.js | 6 +-- app/util/analyticsV2.js | 11 +++-- ios/MetaMask.xcodeproj/project.pbxproj | 4 +- locales/languages/es.json | 1 + 20 files changed, 229 insertions(+), 169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0122d10dfa..a1735eb3e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Develop Branch ## v2.1.0 - Apr 12 2021 +- [#2487](https://github.com/MetaMask/metamask-mobile/pull/2487): Fix/analytics v1 priority1 - [#2456](https://github.com/MetaMask/metamask-mobile/pull/2456): Analytics v2 (priority 1) - [#2408](https://github.com/MetaMask/metamask-mobile/pull/2408): Fix/gas estimations - [#2479](https://github.com/MetaMask/metamask-mobile/pull/2479): remove controllers tgz diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index ff7a77f8bde..86af6b065cc 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -128,11 +128,11 @@ const Main = props => { const onUnapprovedMessage = (messageParams, type) => { const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; delete messageParams.meta; - setSignMessage(true); setSignMessageParams(messageParams); setSignType(type); setCurrentPageTitle(currentPageTitle); setCurrentPageUrl(currentPageUrl); + setSignMessage(true); }; const connectionChangeHandler = useCallback( diff --git a/app/components/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js index 38842d930c3..a53493d3daf 100644 --- a/app/components/UI/AccountApproval/index.js +++ b/app/components/UI/AccountApproval/index.js @@ -101,14 +101,18 @@ class AccountApproval extends PureComponent { }; getAnalyticsParams = () => { - const { currentPageInformation, chainId, networkType } = this.props; - const url = new URL(currentPageInformation.url); - return { - dapp_host_name: url?.host, - dapp_url: currentPageInformation?.url, - network_name: networkType, - chain_id: chainId - }; + try { + const { currentPageInformation, chainId, networkType } = this.props; + const url = new URL(currentPageInformation?.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: networkType, + chain_id: chainId + }; + } catch (error) { + return {}; + } }; componentDidMount = () => { diff --git a/app/components/UI/AddCustomCollectible/index.js b/app/components/UI/AddCustomCollectible/index.js index a86952e6b8d..5983e6fd1e8 100644 --- a/app/components/UI/AddCustomCollectible/index.js +++ b/app/components/UI/AddCustomCollectible/index.js @@ -74,14 +74,16 @@ class AddCustomCollectible extends PureComponent { }; getAnalyticsParams = () => { - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const { address } = this.state; - return { - collectible_address: { value: address, anonymous: true }, - network_name: type, - chain_id: chainId - }; + try { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + return { + network_name: type, + chain_id: chainId + }; + } catch (error) { + return {}; + } }; addCollectible = async () => { diff --git a/app/components/UI/AddCustomToken/index.js b/app/components/UI/AddCustomToken/index.js index f84627b02d1..1e6a08cd346 100644 --- a/app/components/UI/AddCustomToken/index.js +++ b/app/components/UI/AddCustomToken/index.js @@ -52,16 +52,20 @@ export default class AddCustomToken extends PureComponent { }; getAnalyticsParams = () => { - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const { address, symbol } = this.state; - return { - token_address: { value: address, anonymous: true }, - token_symbol: { value: symbol, anonymous: true }, - network_name: type, - chain_id: chainId, - source: 'Custom token' - }; + try { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const { address, symbol } = this.state; + return { + token_address: address, + token_symbol: symbol, + network_name: type, + chain_id: chainId, + source: 'Custom token' + }; + } catch (error) { + return {}; + } }; addToken = async () => { diff --git a/app/components/UI/ApproveTransactionReview/index.js b/app/components/UI/ApproveTransactionReview/index.js index 7ae908d45d4..9c38c20684e 100644 --- a/app/components/UI/ApproveTransactionReview/index.js +++ b/app/components/UI/ApproveTransactionReview/index.js @@ -318,26 +318,30 @@ class ApproveTransactionReview extends PureComponent { } getAnalyticsParams = () => { - const { activeTabUrl, transaction, onSetAnalyticsParams } = this.props; - const { tokenSymbol, originalApproveAmount, encodedAmount } = this.state; - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const isDapp = !Object.values(AppConstants.DEEPLINKS).includes(transaction.origin); - const unlimited = encodedAmount === 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; - const params = { - dapp_host_name: transaction?.origin, - dapp_url: isDapp ? activeTabUrl : undefined, - network_name: type, - chain_id: chainId, - active_currency: { value: tokenSymbol, anonymous: true }, - number_tokens_requested: { value: originalApproveAmount, anonymous: true }, - unlimited_permission_requested: unlimited, - referral_type: isDapp ? 'dapp' : transaction?.origin - }; - // Send analytics params to parent component so it's available when cancelling and confirming - onSetAnalyticsParams && onSetAnalyticsParams(params); - - return params; + try { + const { activeTabUrl, transaction, onSetAnalyticsParams } = this.props; + const { tokenSymbol, originalApproveAmount, encodedAmount } = this.state; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const isDapp = !Object.values(AppConstants.DEEPLINKS).includes(transaction?.origin); + const unlimited = encodedAmount === 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + const params = { + dapp_host_name: transaction?.origin, + dapp_url: isDapp ? activeTabUrl : undefined, + network_name: type, + chain_id: chainId, + active_currency: { value: tokenSymbol, anonymous: true }, + number_tokens_requested: { value: originalApproveAmount, anonymous: true }, + unlimited_permission_requested: unlimited, + referral_type: isDapp ? 'dapp' : transaction?.origin + }; + // Send analytics params to parent component so it's available when cancelling and confirming + onSetAnalyticsParams && onSetAnalyticsParams(params); + + return params; + } catch (error) { + return {}; + } }; trackApproveEvent = event => { diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index ffd905440c4..781df3efc1c 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -512,16 +512,20 @@ class CustomGas extends PureComponent { }; getAnalyticsParams = () => { - const { advancedCustomGas, chainId, networkType, view, analyticsParams } = this.props; - const { gasSpeedSelected } = this.state; - return { - ...(analyticsParams || {}), - network_name: networkType, - chain_id: chainId, - function_type: { value: view, anonymous: true }, - gas_mode: { value: advancedCustomGas ? 'Advanced' : 'Basic', anonymous: true }, - speed_set: { value: advancedCustomGas ? undefined : gasSpeedSelected, anonymous: true } - }; + try { + const { advancedCustomGas, chainId, networkType, view, analyticsParams } = this.props; + const { gasSpeedSelected } = this.state; + return { + ...(analyticsParams || {}), + network_name: networkType, + chain_id: chainId, + function_type: view, + gas_mode: advancedCustomGas ? 'Advanced' : 'Basic', + speed_set: advancedCustomGas ? undefined : gasSpeedSelected + }; + } catch (error) { + return {}; + } }; //Handle gas fee selection when save button is pressed instead of everytime a change is made, otherwise cannot switch back to review mode if there is an error diff --git a/app/components/UI/MessageSign/index.js b/app/components/UI/MessageSign/index.js index 0d5ed009e10..09e15d7c66d 100644 --- a/app/components/UI/MessageSign/index.js +++ b/app/components/UI/MessageSign/index.js @@ -62,17 +62,21 @@ export default class MessageSign extends PureComponent { }; getAnalyticsParams = () => { - const { currentPageInformation } = this.props; - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const url = new URL(currentPageInformation.url); - return { - dapp_host_name: url?.host, - dapp_url: currentPageInformation?.url, - network_name: type, - chain_id: chainId, - sign_type: 'eth' - }; + try { + const { currentPageInformation } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const url = new URL(currentPageInformation?.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + sign_type: 'eth' + }; + } catch (error) { + return {}; + } }; componentDidMount = () => { @@ -113,14 +117,14 @@ export default class MessageSign extends PureComponent { }; cancelSignature = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.rejectMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.props.onCancel(); }; confirmSignature = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.signMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.props.onConfirm(); }; diff --git a/app/components/UI/PersonalSign/index.js b/app/components/UI/PersonalSign/index.js index 3be8fd7a319..91e21679a4b 100644 --- a/app/components/UI/PersonalSign/index.js +++ b/app/components/UI/PersonalSign/index.js @@ -67,17 +67,22 @@ export default class PersonalSign extends PureComponent { }; getAnalyticsParams = () => { - const { currentPageInformation } = this.props; - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const url = new URL(currentPageInformation.url); - return { - dapp_host_name: url?.host, - dapp_url: currentPageInformation?.url, - network_name: type, - chain_id: chainId, - sign_type: 'personal' - }; + try { + const { currentPageInformation } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const url = new URL(currentPageInformation?.url); + + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + sign_type: 'personal' + }; + } catch (error) { + return {}; + } }; componentDidMount = () => { @@ -118,14 +123,14 @@ export default class PersonalSign extends PureComponent { }; cancelSignature = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.rejectMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.props.onCancel(); }; confirmSignature = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.signMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.props.onConfirm(); }; diff --git a/app/components/UI/SearchTokenAutocomplete/index.js b/app/components/UI/SearchTokenAutocomplete/index.js index 3955e0901b3..83dd3a6caba 100644 --- a/app/components/UI/SearchTokenAutocomplete/index.js +++ b/app/components/UI/SearchTokenAutocomplete/index.js @@ -50,16 +50,20 @@ export default class SearchTokenAutocomplete extends PureComponent { }; getAnalyticsParams = () => { - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const { address, symbol } = this.state.selectedAsset; - return { - token_address: { value: address, anonymous: true }, - token_symbol: { value: symbol, anonymous: true }, - network_name: type, - chain_id: chainId, - source: 'Add token dropdown' - }; + try { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const { address, symbol } = this.state.selectedAsset || {}; + return { + token_address: address, + token_symbol: symbol, + network_name: type, + chain_id: chainId, + source: 'Add token dropdown' + }; + } catch (error) { + return {}; + } }; addToken = async () => { diff --git a/app/components/UI/TransactionEditor/index.js b/app/components/UI/TransactionEditor/index.js index d0b7aa3c761..dfdb3fa2a1c 100644 --- a/app/components/UI/TransactionEditor/index.js +++ b/app/components/UI/TransactionEditor/index.js @@ -611,13 +611,17 @@ class TransactionEditor extends PureComponent { }; getGasAnalyticsParams = () => { - const { transaction, activeTabUrl } = this.props; - const { selectedAsset } = transaction; - return { - dapp_host_name: transaction?.origin, - dapp_url: activeTabUrl, - active_currency: { value: selectedAsset?.symbol, anonymous: true } - }; + try { + const { transaction, activeTabUrl } = this.props; + const { selectedAsset } = transaction; + return { + dapp_host_name: transaction?.origin, + dapp_url: activeTabUrl, + active_currency: { value: selectedAsset?.symbol, anonymous: true } + }; + } catch (error) { + return {}; + } }; render = () => { diff --git a/app/components/UI/TypedSign/index.js b/app/components/UI/TypedSign/index.js index b850d716c9b..09e9b5e1425 100644 --- a/app/components/UI/TypedSign/index.js +++ b/app/components/UI/TypedSign/index.js @@ -76,18 +76,22 @@ export default class TypedSign extends PureComponent { }; getAnalyticsParams = () => { - const { currentPageInformation, messageParams } = this.props; - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - const url = new URL(currentPageInformation.url); - return { - dapp_host_name: url?.host, - dapp_url: currentPageInformation?.url, - network_name: type, - chain_id: chainId, - sign_type: 'typed', - version: messageParams?.version - }; + try { + const { currentPageInformation, messageParams } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + const url = new URL(currentPageInformation?.url); + return { + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + sign_type: 'typed', + version: messageParams?.version + }; + } catch (error) { + return {}; + } }; componentDidMount = () => { @@ -129,14 +133,14 @@ export default class TypedSign extends PureComponent { }; cancelSignature = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.rejectMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.props.onCancel(); }; confirmSignature = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.signMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.props.onConfirm(); }; diff --git a/app/components/UI/WatchAssetRequest/index.js b/app/components/UI/WatchAssetRequest/index.js index 6670c464d94..91f56f59569 100644 --- a/app/components/UI/WatchAssetRequest/index.js +++ b/app/components/UI/WatchAssetRequest/index.js @@ -106,24 +106,28 @@ class WatchAssetRequest extends PureComponent { }; getAnalyticsParams = () => { - const { - suggestedAssetMeta: { asset }, - currentPageInformation - } = this.props; + try { + const { + suggestedAssetMeta: { asset }, + currentPageInformation + } = this.props; - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; - const url = new URL(currentPageInformation?.url); - return { - token_address: asset?.address, - token_symbol: asset?.symbol, - dapp_host_name: url?.host, - dapp_url: currentPageInformation?.url, - network_name: type, - chain_id: chainId, - source: 'Dapp suggested (watchAsset)' - }; + const url = new URL(currentPageInformation?.url); + return { + token_address: asset?.address, + token_symbol: asset?.symbol, + dapp_host_name: url?.host, + dapp_url: currentPageInformation?.url, + network_name: type, + chain_id: chainId, + source: 'Dapp suggested (watchAsset)' + }; + } catch (error) { + return {}; + } }; componentWillUnmount = async () => { diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js index 8d029e2776c..7e78deaa49d 100644 --- a/app/components/Views/Approval/index.js +++ b/app/components/Views/Approval/index.js @@ -164,16 +164,20 @@ class Approval extends PureComponent { }; getAnalyticsParams = () => { - const { activeTabUrl, chainId, transaction, networkType } = this.props; - const { selectedAsset } = transaction; - return { - dapp_host_name: transaction?.origin, - dapp_url: activeTabUrl, - network_name: networkType, - chain_id: chainId, - active_currency: { value: selectedAsset?.symbol, anonymous: true }, - asset_type: { value: transaction?.assetType, anonymous: true } - }; + try { + const { activeTabUrl, chainId, transaction, networkType } = this.props; + const { selectedAsset } = transaction; + return { + dapp_host_name: transaction?.origin, + dapp_url: activeTabUrl, + network_name: networkType, + chain_id: chainId, + active_currency: { value: selectedAsset?.symbol, anonymous: true }, + asset_type: { value: transaction?.assetType, anonymous: true } + }; + } catch (error) { + return {}; + } }; /** diff --git a/app/components/Views/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js index 467167be5ea..727c3758d4b 100644 --- a/app/components/Views/ApproveView/Approve/index.js +++ b/app/components/Views/ApproveView/Approve/index.js @@ -240,13 +240,17 @@ class Approve extends PureComponent { }; getGasAnalyticsParams = () => { - const { analyticsParams } = this.state; + try { + const { analyticsParams } = this.state; - return { - dapp_host_name: analyticsParams?.dapp_host_name, - dapp_url: analyticsParams?.dapp_url, - active_currency: { value: analyticsParams?.active_currency, anonymous: true } - }; + return { + dapp_host_name: analyticsParams?.dapp_host_name, + dapp_url: analyticsParams?.dapp_url, + active_currency: { value: analyticsParams?.active_currency, anonymous: true } + }; + } catch (error) { + return {}; + } }; render = () => { diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index 46ed93ce157..bd7ab140216 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -318,21 +318,29 @@ class Confirm extends PureComponent { }; getAnalyticsParams = () => { - const { selectedAsset } = this.props; - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - return { - active_currency: { value: selectedAsset?.symbol, anonymous: true }, - network_name: type, - chain_id: chainId - }; + try { + const { selectedAsset } = this.props; + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + return { + active_currency: { value: selectedAsset?.symbol, anonymous: true }, + network_name: type, + chain_id: chainId + }; + } catch (error) { + return {}; + } }; getGasAnalyticsParams = () => { - const { selectedAsset } = this.props; - return { - active_currency: { value: selectedAsset.symbol, anonymous: true } - }; + try { + const { selectedAsset } = this.props; + return { + active_currency: { value: selectedAsset.symbol, anonymous: true } + }; + } catch (error) { + return {}; + } }; componentDidMount = async () => { diff --git a/app/core/RPCMethods/wallet_addEthereumChain.js b/app/core/RPCMethods/wallet_addEthereumChain.js index 81dc04582aa..feceb2d88af 100644 --- a/app/core/RPCMethods/wallet_addEthereumChain.js +++ b/app/core/RPCMethods/wallet_addEthereumChain.js @@ -113,11 +113,11 @@ const wallet_addEthereumChain = async ({ }); const analyticsParams = { - rpc_url: existingNetwork.rpcUrl, + rpc_url: existingNetwork?.rpcUrl, chain_id: _chainId, source: 'Custom Network API', - symbol: existingNetwork.ticker, - block_explorer_url: existingNetwork.blockExplorerUrl, + symbol: existingNetwork?.ticker, + block_explorer_url: existingNetwork?.blockExplorerUrl, network_name: 'rpc' }; diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js index 11f2d54faaf..298b9862b0e 100644 --- a/app/util/analyticsV2.js +++ b/app/util/analyticsV2.js @@ -33,7 +33,6 @@ export const ANALYTICS_EVENTS_V2 = { NETWORK_ADDED: generateOpt('Network Added'), NETWORK_REQUESTED: generateOpt('Network Requested'), NETWORK_REQUEST_REJECTED: generateOpt('Network Request Rejected'), - NETWORK_SWITCH_REJECTED: generateOpt('Network Switch Rejected'), // Send transaction SEND_TRANSACTION_STARTED: generateOpt('Send Transaction Started'), SEND_TRANSACTION_COMPLETED: generateOpt('Send Transaction Completed') @@ -58,11 +57,7 @@ export const trackEventV2 = (eventName, params) => { for (const key in params) { const property = params[key]; - if (typeof property === 'string' || property instanceof String) { - // Non-anonymous properties - add to both - userParams[key] = property; - anonymousParams[key] = property; - } else if (typeof property === 'object') { + if (typeof property === 'object') { if (property.anonymous) { // Anonymous property - add only to anonymous params anonymousParams[key] = property.value; @@ -71,6 +66,10 @@ export const trackEventV2 = (eventName, params) => { userParams[key] = property.value; anonymousParams[key] = property.value; } + } else { + // Non-anonymous properties - add to both + userParams[key] = property; + anonymousParams[key] = property; } } diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 1fdbd817742..75043cc8a1a 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 606; + CURRENT_PROJECT_VERSION = 607; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 606; + CURRENT_PROJECT_VERSION = 607; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( diff --git a/locales/languages/es.json b/locales/languages/es.json index 04884442541..c8b74a5f16d 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -314,6 +314,7 @@ "description_content_2": "MetaMask…", "action_description_1": "Siempre le permitirá optar por no participar a través de Configuración", "action_description_2": "Enviará eventos de vistas de página y clics anónimos", + "action_description_3": "Enviar país, región, ciudad (no ubicación específica)", "action_description_4": "Nunca recopilará claves, direcciones, transacciones, saldos, hashes o cualquier otra información personal", "action_description_5": "Nunca recopilará su dirección IP", "action_description_6": "Nunca venderá datos con afán de lucro. ¡Jamás!" From d2b3ea30d2f0050780f4603278f14513ff295dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 9 Apr 2021 16:11:31 -0400 Subject: [PATCH 08/51] bugfix/2488 (#2490) --- app/components/Views/SendFlow/Amount/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/Views/SendFlow/Amount/index.js b/app/components/Views/SendFlow/Amount/index.js index a55baecf28b..efbb5b5e623 100644 --- a/app/components/Views/SendFlow/Amount/index.js +++ b/app/components/Views/SendFlow/Amount/index.js @@ -397,7 +397,7 @@ class Amount extends PureComponent { this.collectibles = this.processCollectibles(); this.amountInput && this.amountInput.current && this.amountInput.current.focus(); this.onInputChange(readableValue); - this.handleSelectedAssetBalance(selectedAsset); + !selectedAsset.tokenId && this.handleSelectedAssetBalance(selectedAsset); const estimatedTotalGas = await this.estimateTransactionTotalGas(); this.setState({ @@ -733,7 +733,6 @@ class Amount extends PureComponent { handleSelectedAssetBalance = ({ address, decimals, symbol, isETH }, renderableBalance) => { const { accounts, selectedAddress, contractBalances } = this.props; let currentBalance; - if (renderableBalance) { currentBalance = `${renderableBalance} ${symbol}`; } else if (isETH) { From 4a1c60d3170be4dbc22c4a5d9ba3319b8b73cd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 9 Apr 2021 16:42:11 -0400 Subject: [PATCH 09/51] bugfix/4849 (#2491) * fixit * title * bump608 * rightcheck * a * rm? --- app/components/Views/BrowserTab/index.js | 6 +++--- app/components/Views/ResetPassword/index.js | 7 ++++--- app/components/Views/Settings/SecuritySettings/index.js | 3 ++- ios/MetaMask.xcodeproj/project.pbxproj | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 4fad1ab76fb..219a7b8a678 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -418,7 +418,7 @@ export const BrowserTab = props => { const { privacyMode, selectedAddress } = props; const isEnabled = !privacyMode || approvedHosts[hostname]; - return isEnabled ? [selectedAddress.toLowerCase()] : []; + return isEnabled && selectedAddress ? [selectedAddress] : []; }; const rpcMethods = { @@ -464,7 +464,7 @@ export const BrowserTab = props => { }); if (approved) { - res.result = [selectedAddress.toLowerCase()]; + res.result = selectedAddress ? [selectedAddress] : []; } else { throw ethErrors.provider.userRejectedRequest('User denied account authorization.'); } @@ -1961,7 +1961,7 @@ const mapStateToProps = state => ({ networkProvider: state.engine.backgroundState.NetworkController.provider, networkType: state.engine.backgroundState.NetworkController.provider.type, network: state.engine.backgroundState.NetworkController.network, - selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress.toLowerCase(), + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress?.toLowerCase(), privacyMode: state.privacy.privacyMode, searchEngine: state.settings.searchEngine, whitelist: state.browser.whitelist, diff --git a/app/components/Views/ResetPassword/index.js b/app/components/Views/ResetPassword/index.js index dfa5375b828..1a7a3a7957a 100644 --- a/app/components/Views/ResetPassword/index.js +++ b/app/components/Views/ResetPassword/index.js @@ -27,7 +27,7 @@ import Engine from '../../../core/Engine'; import Device from '../../../util/Device'; import { colors, fontStyles, baseStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; -import { getOnboardingNavbarOptions } from '../../UI/Navbar'; +import { getNavigationOptionsTitle } from '../../UI/Navbar'; import SecureKeychain from '../../../core/SecureKeychain'; import Icon from 'react-native-vector-icons/FontAwesome'; import AppConstants from '../../../core/AppConstants'; @@ -251,7 +251,8 @@ const CONFIRM_PASSWORD = 'confirm_password'; * View where users can set their password for the first time */ class ResetPassword extends PureComponent { - static navigationOptions = ({ navigation }) => getOnboardingNavbarOptions(navigation); + static navigationOptions = ({ navigation }) => + getNavigationOptionsTitle(strings('password_reset.change_password'), navigation); static propTypes = { /** @@ -451,7 +452,7 @@ class ResetPassword extends PureComponent { if (hdKeyring.accounts.includes(selectedAddress)) { PreferencesController.setSelectedAddress(selectedAddress); } else { - PreferencesController.setSelectedAddress(hdKeyring[0]); + PreferencesController.setSelectedAddress(hdKeyring.accounts[0]); } }; diff --git a/app/components/Views/Settings/SecuritySettings/index.js b/app/components/Views/Settings/SecuritySettings/index.js index 7017adb638c..8416178c9b3 100644 --- a/app/components/Views/Settings/SecuritySettings/index.js +++ b/app/components/Views/Settings/SecuritySettings/index.js @@ -228,7 +228,8 @@ class Settings extends PureComponent { seedphraseBackedUp: PropTypes.bool }; - static navigationOptions = ({ navigation }) => getNavigationOptionsTitle(strings('app_settings.back'), navigation); + static navigationOptions = ({ navigation }) => + getNavigationOptionsTitle(strings('app_settings.security_title'), navigation); state = { approvalModalVisible: false, diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 75043cc8a1a..6520dcc27e8 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 607; + CURRENT_PROJECT_VERSION = 608; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 607; + CURRENT_PROJECT_VERSION = 608; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( From a1900713971f9fb8acfbd6e9dbedb9d884fe7104 Mon Sep 17 00:00:00 2001 From: ricky Date: Mon, 12 Apr 2021 13:48:49 -0400 Subject: [PATCH 10/51] Display correct number of decimals for 'usd' fiat (#2381) --- app/core/Engine.js | 5 +++-- app/util/number.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/core/Engine.js b/app/core/Engine.js index d1ba8c936ed..8a76319905f 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -235,13 +235,14 @@ class Engine { TokenRatesController } = this.datamodel.context; const { selectedAddress } = PreferencesController.state; - const { conversionRate } = CurrencyRateController.state; + const { conversionRate, currentCurrency } = CurrencyRateController.state; const { accounts } = AccountTrackerController.state; const { tokens } = AssetsController.state; let ethFiat = 0; let tokenFiat = 0; if (accounts[selectedAddress]) { - ethFiat = weiToFiatNumber(accounts[selectedAddress].balance, conversionRate); + const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; + ethFiat = weiToFiatNumber(accounts[selectedAddress].balance, conversionRate, decimalsToShow); } if (tokens.length > 0) { const { contractBalances: tokenBalances } = TokenBalancesController.state; diff --git a/app/util/number.js b/app/util/number.js index b8f25118505..13131b7227b 100644 --- a/app/util/number.js +++ b/app/util/number.js @@ -348,7 +348,7 @@ export function weiToFiat(wei, conversionRate, currencyCode, decimalsToShow = 5) } return `0.00 ${currencyCode}`; } - decimalsToShow = currencyCode === 'usd' && 2; + decimalsToShow = (currencyCode === 'usd' && 2) || undefined; const value = weiToFiatNumber(wei, conversionRate, decimalsToShow); if (currencySymbols[currencyCode]) { return `${currencySymbols[currencyCode]}${value}`; From a25d1975824e6734ff1607eb6b0f8ae3a77e5d92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 14:44:55 -0400 Subject: [PATCH 11/51] Bump babel-eslint from 10.0.3 to 10.1.0 (#2403) * Bump babel-eslint from 10.0.3 to 10.1.0 Bumps [babel-eslint](https://github.com/babel/babel-eslint) from 10.0.3 to 10.1.0. - [Release notes](https://github.com/babel/babel-eslint/releases) - [Commits](https://github.com/babel/babel-eslint/compare/v10.0.3...v10.1.0) Signed-off-by: dependabot[bot] * Add `gasSpeedSelected` to props validation * remove optional chaining Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ricky --- app/components/UI/CustomGas/index.js | 8 +++- package.json | 2 +- yarn.lock | 70 ++-------------------------- 3 files changed, 11 insertions(+), 69 deletions(-) diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index 781df3efc1c..3103e63b99c 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -330,11 +330,15 @@ class CustomGas extends PureComponent { /** * Extra analytics params to be send with the gas analytics */ - analyticsParams: PropTypes.object + analyticsParams: PropTypes.object, + /** + * The currently selected gas speed + */ + gasSpeedSelected: PropTypes.string }; state = { - gasSpeedSelected: this?.props?.gasSpeedSelected || 'average', + gasSpeedSelected: this.props.gasSpeedSelected || 'average', customGasPrice: '10', customGasLimit: fromWei(this.props.gas, 'wei'), customGasPriceBNWei: this.props.gasPrice, diff --git a/package.json b/package.json index 61b3faa24c6..2ab6a9ba88d 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "@react-native-community/eslint-config": "^1.1.0", "assert": "1.4.1", "babel-core": "7.0.0-bridge.0", - "babel-eslint": "10.0.3", + "babel-eslint": "10.1.0", "babel-jest": "^26.6.3", "concat-cli": "4.0.0", "detox": "17.3.1", diff --git a/yarn.lock b/yarn.lock index 560079f002f..d764de87d57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" - integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== - dependencies: - "@babel/highlight" "^7.10.1" - -"@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.10.4": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== @@ -327,11 +320,6 @@ dependencies: "@babel/types" "^7.12.11" -"@babel/helper-validator-identifier@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" - integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== - "@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" @@ -365,15 +353,6 @@ "@babel/traverse" "^7.12.5" "@babel/types" "^7.12.5" -"@babel/highlight@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" - integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== - dependencies: - "@babel/helper-validator-identifier" "^7.10.1" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -383,12 +362,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.1", "@babel/parser@^7.7.0": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.1.tgz#2e142c27ca58aa2c7b119d09269b702c8bbad28c" - integrity sha512-AUTksaz3FqugBkbTZ1i+lDLG5qy8hIzCaAxEtttU6C0BtZZU9pkNZtWSVAht4EW9kl46YBiyTGMp9xTTGqViNg== - -"@babel/parser@^7.12.10", "@babel/parser@^7.12.7": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.1", "@babel/parser@^7.12.10", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== @@ -853,22 +827,7 @@ "@babel/parser" "^7.12.7" "@babel/types" "^7.12.7" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.1", "@babel/traverse@^7.7.0": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.1.tgz#bbcef3031e4152a6c0b50147f4958df54ca0dd27" - integrity sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ== - dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/generator" "^7.10.1" - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" - "@babel/parser" "^7.10.1" - "@babel/types" "^7.10.1" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.1", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.10.tgz#2d1f4041e8bf42ea099e5b2dc48d6a594c00017a" integrity sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg== @@ -883,16 +842,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.10.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.7.0": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.1.tgz#6886724d31c8022160a7db895e6731ca33483921" - integrity sha512-L2yqUOpf3tzlW9GVuipgLEcZxnO+96SzR6fjXMuxxNkIgFJ5+07mHCZ+HkHqaeZu8+3LKnNJJ1bKbjBETQAsrA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.1" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.5", "@babel/types@^7.12.7": +"@babel/types@^7.0.0", "@babel/types@^7.10.1", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.7.0": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.11.tgz#a86e4d71e30a9b6ee102590446c98662589283ce" integrity sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA== @@ -2829,18 +2779,6 @@ babel-core@7.0.0-bridge.0: resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== -babel-eslint@10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" - integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - eslint-visitor-keys "^1.0.0" - resolve "^1.12.0" - babel-eslint@10.1.0, babel-eslint@^10.0.1: version "10.1.0" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" From 91aa93207baeb099c50bdc8fff9b6ae245c1475d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 16:18:23 -0400 Subject: [PATCH 12/51] Bump eslint-plugin-prettier from 3.3.0 to 3.3.1 (#2406) Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v3.3.0...v3.3.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2ab6a9ba88d..c880e2d82c3 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "eslint": "^6.5.1", "eslint-config-react-native": "4.0.0", "eslint-plugin-import": "2.18.2", - "eslint-plugin-prettier": "^3.3.0", + "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-react": "7.16.0", "eslint-plugin-react-native": "3.7.0", "husky": "1.3.1", diff --git a/yarn.lock b/yarn.lock index d764de87d57..78e44f03057 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4888,10 +4888,10 @@ eslint-plugin-prettier@3.1.2: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-prettier@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz#61e295349a65688ffac0b7808ef0a8244bdd8d40" - integrity sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ== +eslint-plugin-prettier@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7" + integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ== dependencies: prettier-linter-helpers "^1.0.0" From 7e6fa750b48e67be92722be76e61881630db5ad0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 17:50:02 -0400 Subject: [PATCH 13/51] Bump json-rpc-middleware-stream from 2.1.1 to 3.0.0 (#2411) * Bump json-rpc-middleware-stream from 2.1.1 to 3.0.0 Bumps [json-rpc-middleware-stream](https://github.com/MetaMask/json-rpc-middleware-stream) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/MetaMask/json-rpc-middleware-stream/releases) - [Changelog](https://github.com/MetaMask/json-rpc-middleware-stream/blob/main/CHANGELOG.md) - [Commits](https://github.com/MetaMask/json-rpc-middleware-stream/commits) Signed-off-by: dependabot[bot] * Used named import for createEngineStream Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ricky --- app/core/BackgroundBridge.js | 2 +- package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/core/BackgroundBridge.js b/app/core/BackgroundBridge.js index 2a4e85ce765..42386131b41 100644 --- a/app/core/BackgroundBridge.js +++ b/app/core/BackgroundBridge.js @@ -10,8 +10,8 @@ import Engine from './Engine'; import { getAllNetworks } from '../util/networks'; import Logger from '../util/Logger'; import AppConstants from './AppConstants'; +import { createEngineStream } from 'json-rpc-middleware-stream'; -const createEngineStream = require('json-rpc-middleware-stream/engineStream'); const createFilterMiddleware = require('eth-json-rpc-filters'); const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager'); const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware'); diff --git a/package.json b/package.json index c880e2d82c3..0a638f3d8f3 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "https-browserify": "0.0.1", "is-url": "^1.2.4", "json-rpc-engine": "^6.1.0", - "json-rpc-middleware-stream": "2.1.1", + "json-rpc-middleware-stream": "3.0.0", "lottie-react-native": "git+https://github.com/MetaMask/lottie-react-native.git#7ce6a78ac4ac7b9891bc513cb3f12f8b9c9d9106", "multihashes": "0.4.14", "number-to-bn": "1.7.0", diff --git a/yarn.lock b/yarn.lock index 78e44f03057..33d3478bf96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8133,13 +8133,13 @@ json-rpc-engine@^6.1.0: "@metamask/safe-event-emitter" "^2.0.0" eth-rpc-errors "^4.0.2" -json-rpc-middleware-stream@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-2.1.1.tgz#06e5409e201e7ddeae47bef29f7059eafd4d5325" - integrity sha512-WZheufPN+/RKkjXQP3lK5tFYblqG0n+oYv5qpammwwY2vsJRB7mM4Txhr4ajzvYEZi1UkENnplrmaYiqaqafaA== +json-rpc-middleware-stream@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-3.0.0.tgz#8540331d884f36b9e0ad31054cc68ac6b5a89b52" + integrity sha512-JmZmlehE0xF3swwORpLHny/GvW3MZxCsb2uFNBrn8TOqMqivzCfz232NSDLLOtIQlrPlgyEjiYpyzyOPFOzClw== dependencies: + "@metamask/safe-event-emitter" "^2.0.0" readable-stream "^2.3.3" - safe-event-emitter "^1.0.1" json-rpc-random-id@^1.0.0, json-rpc-random-id@^1.0.1: version "1.0.1" From 67925709a5bc015c501ec23d568cd8f57e6504b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:02:32 -0400 Subject: [PATCH 14/51] Bump react-native-share from 3.3.2 to 5.2.2 (#2499) Bumps [react-native-share](https://github.com/react-native-community/react-native-share) from 3.3.2 to 5.2.2. - [Release notes](https://github.com/react-native-community/react-native-share/releases) - [Changelog](https://github.com/react-native-share/react-native-share/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-native-community/react-native-share/compare/v3.3.2...v5.2.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0a638f3d8f3..240ecb46f87 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "react-native-scrollable-tab-view": "^1.0.0", "react-native-search-api": "ombori/react-native-search-api#8/head", "react-native-sensors": "5.3.0", - "react-native-share": "^3.2.0", + "react-native-share": "^5.2.2", "react-native-splash-screen": "git+https://github.com/MetaMask/react-native-splash-screen.git", "react-native-step-indicator": "^1.0.3", "react-native-svg": "12.1.0", diff --git a/yarn.lock b/yarn.lock index 33d3478bf96..42882e2fb9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11086,10 +11086,10 @@ react-native-sensors@5.3.0: dependencies: rxjs ">= 6" -react-native-share@^3.2.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-3.3.2.tgz#06d1d3f14ba8eeb95e7e94e4db6a286e9902bd29" - integrity sha512-Pvkr62TiCX511RMPL+wvy9Fofre4HQnvUT5zzgPPN3vszP/C8lUb7cmFu/8x5U14t3JQg+xW/svNK5eKNebJKw== +react-native-share@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-5.2.2.tgz#7161fd37bc861e6a63df5d5c5fafbff10c7ff5e5" + integrity sha512-Jn92T+fXzq8ZIfiZllznFYrhDQoFUcMZ6vO0oXgQJYR5leVZuesqy8II3taWLtQzbAD5tl4Y+EaNYo7Z6TNGTw== "react-native-splash-screen@git+https://github.com/MetaMask/react-native-splash-screen.git": version "3.2.0" From 8a59e80295539aa81c48542256953d3959c9c777 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:19:10 -0400 Subject: [PATCH 15/51] Bump jest-serializer from 24.4.0 to 26.6.2 (#2501) Bumps [jest-serializer](https://github.com/facebook/jest/tree/HEAD/packages/jest-serializer) from 24.4.0 to 26.6.2. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/commits/v26.6.2/packages/jest-serializer) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 240ecb46f87..a2f5db91ad5 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,7 @@ "eslint-plugin-react-native": "3.7.0", "husky": "1.3.1", "jest": "^25.2.7", - "jest-serializer": "24.4.0", + "jest-serializer": "26.6.2", "jetifier": "^1.6.6", "lint-staged": "10.5.4", "metro": "^0.59.0", diff --git a/yarn.lock b/yarn.lock index 42882e2fb9e..b2a12ecc26d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7860,10 +7860,13 @@ jest-runtime@^25.5.4: strip-bom "^4.0.0" yargs "^15.3.1" -jest-serializer@24.4.0: - version "24.4.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.4.0.tgz#f70c5918c8ea9235ccb1276d232e459080588db3" - integrity sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q== +jest-serializer@26.6.2, jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" jest-serializer@^24.4.0, jest-serializer@^24.9.0: version "24.9.0" @@ -7877,14 +7880,6 @@ jest-serializer@^25.5.0: dependencies: graceful-fs "^4.2.4" -jest-serializer@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" - integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.4" - jest-snapshot@^25.5.1: version "25.5.1" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.5.1.tgz#1a2a576491f9961eb8d00c2e5fd479bc28e5ff7f" From 582b86dcd116acc2517d79002d3816b65c26d5af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 12:22:42 -0400 Subject: [PATCH 16/51] Bump vm-browserify from 0.0.4 to 1.1.2 (#2447) Bumps [vm-browserify](https://github.com/substack/vm-browserify) from 0.0.4 to 1.1.2. - [Release notes](https://github.com/substack/vm-browserify/releases) - [Changelog](https://github.com/browserify/vm-browserify/blob/master/CHANGELOG.md) - [Commits](https://github.com/substack/vm-browserify/compare/0.0.4...v1.1.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a2f5db91ad5..d238fe43d2b 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "url": "0.11.0", "url-parse": "1.4.4", "valid-url": "1.0.9", - "vm-browserify": "0.0.4", + "vm-browserify": "1.1.2", "web3-provider-engine": "^16.0.1", "zxcvbn": "4.4.2" }, diff --git a/yarn.lock b/yarn.lock index b2a12ecc26d..60e84cccafb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6975,7 +6975,7 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indexof@0.0.1, indexof@~0.0.1: +indexof@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= @@ -13309,12 +13309,10 @@ vlq@^1.0.0: resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== -vm-browserify@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" - integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM= - dependencies: - indexof "0.0.1" +vm-browserify@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== w3c-hr-time@^1.0.1: version "1.0.2" From 93a1b8af2ff5c831803eda12f0e6aebf52b1efc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Tue, 13 Apr 2021 13:14:24 -0400 Subject: [PATCH 17/51] rename master to main (#2493) --- .circleci/config.yml | 4 ++-- RELEASE.MD | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 29ca80eddf2..52702030dfb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -199,13 +199,13 @@ workflows: filters: branches: only: - - master + - main - develop - prep-node-deps: filters: branches: ignore: - - master + - main - develop - lint: requires: diff --git a/RELEASE.MD b/RELEASE.MD index 409647923fb..b878ee3631b 100644 --- a/RELEASE.MD +++ b/RELEASE.MD @@ -47,6 +47,6 @@ ### Once you're done with both stores: - Submit a PR with the changes -- Once it's merged create a tag on master for that version +- Once it's merged create a tag on main for that version - Go to the release pages and create a new release for that tag, including the changelog From f7c85ebff98a7ea8ff6542a50e113603bea32cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 14 Apr 2021 13:01:23 -0400 Subject: [PATCH 18/51] Bump v2.1.1 (#2521) * bump * changelog --- CHANGELOG.md | 3 +++ android/app/build.gradle | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1735eb3e1b..00eb1b23fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Develop Branch +## v2.1.1 - Apr 14 2021 +- [#2520](https://github.com/MetaMask/metamask-mobile/pull/2520): Check provider status + ## v2.1.0 - Apr 12 2021 - [#2487](https://github.com/MetaMask/metamask-mobile/pull/2487): Fix/analytics v1 priority1 - [#2456](https://github.com/MetaMask/metamask-mobile/pull/2456): Analytics v2 (priority 1) diff --git a/android/app/build.gradle b/android/app/build.gradle index c2f664d7216..e9bbe1e3344 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -166,8 +166,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 50 - versionName "2.1.0" + versionCode 51 + versionName "2.1.1" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 6520dcc27e8..362d95953a6 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 608; + CURRENT_PROJECT_VERSION = 609; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -882,7 +882,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.1.0; + MARKETING_VERSION = 2.1.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 608; + CURRENT_PROJECT_VERSION = 609; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( @@ -945,7 +945,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.1.0; + MARKETING_VERSION = 2.1.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index d238fe43d2b..940e09094e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "2.1.0", + "version": "2.1.1", "private": true, "scripts": { "watch": "./scripts/build.sh watcher watch", From 7b167b848b08d9db3fdc63d5d79fccdbfd587a21 Mon Sep 17 00:00:00 2001 From: ricky Date: Wed, 14 Apr 2021 15:22:24 -0400 Subject: [PATCH 19/51] Feature/custom nonce (#2371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add custom nonce switch * update snapshot * Use props * Get edit nonce button in place * Add edit nonce modal * Add translation strings * Add CustomNonceModal component * Add proposedNonce * undo * Update styles and translation strings * Use a hook * Save nonce to state * Add IncrementDecrementSvg component * Add comment for newly added prop * Fix width @ 80 * Add component tests * Add tests * Display warning * Update tests * Add getProposedNonce * Merge some utils and add tests * Add constants * Use object property shorthand * Fix displayWarning to account for String * Add ModalDragger * Add save and cancel * Use the TransactionController from "@metamask/controllers" * Only use custom nonce if showCustomNonce * Update snapshot * Start using Base/Text * Add colors to Base/Text component * Account for NaN values * Use Base/Text * Move increment decrement buttons down * Update tests * Add showSoftInputOnFocus={false} * Update tests * Add icon * Use link * Update tests * prep for QA * Update warning text * Use 6.2.0 controllers * Update translation strings * update snapshot * Remove TODO * Use EvilIcons * Update snapshot * Beef up tests for toLowerCaseCompare * Remove comment * update snapshot * Get nonce from network * Update snapshot * fix lint nits * fix copy * Update snapshot * Move edit nonce functionality into TransactionReviewFeeCard * Add custom nonce to dapp send * Use redux actions * Continue using actions * Set custom nonce on confirm * Update snapshot * Move nonce and proposedNonce off transaction object * Bump controllers * Only getNetworkNonce if proposedNonce is not set * fix props * delete tgz * Add getNetworkNonce to networks utility * document props * Update snapshots * Update app/components/UI/TransactionReview/TransactionReviewInformation/index.js Co-authored-by: Esteban Miño * use nonce directly instead of passing it in render Co-authored-by: Esteban Miño --- app/actions/settings/index.js | 7 + app/actions/transaction/index.js | 14 + app/components/Base/Text.js | 20 + .../__snapshots__/index.test.js.snap | 24 + .../UI/ApproveTransactionReview/index.js | 2 +- .../__snapshots__/index.test.js.snap | 12 + .../__snapshots__/index.test.js.snap | 413 ++++++++++++++++++ app/components/UI/CustomNonceModal/index.js | 224 ++++++++++ .../UI/CustomNonceModal/index.test.js | 15 + .../__snapshots__/index.test.js.snap | 8 + .../__snapshots__/TokenIcon.test.js.snap | 4 + .../TokenSelectButton.test.js.snap | 10 + .../__snapshots__/index.test.js.snap | 10 + .../__snapshots__/index.test.js.snap | 6 + .../__snapshots__/index.test.js.snap | 234 +++++----- .../TransactionReviewFeeCard/index.js | 118 +++-- .../TransactionReviewInformation/index.js | 74 +++- app/components/Views/Approval/index.js | 11 +- app/components/Views/Approval/index.test.js | 3 + .../Confirm/__snapshots__/index.test.js.snap | 131 +++++- .../Views/SendFlow/Confirm/index.js | 69 ++- .../__snapshots__/index.test.js.snap | 2 + .../__snapshots__/index.test.js.snap | 53 +++ .../Views/Settings/AdvancedSettings/index.js | 34 +- app/reducers/settings/index.js | 5 + app/reducers/transaction/index.js | 14 +- app/util/format.js | 2 - app/util/general.js | 5 + app/util/general.test.js | 27 ++ app/util/networks.js | 8 + locales/languages/en.json | 9 + 31 files changed, 1381 insertions(+), 187 deletions(-) create mode 100644 app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/CustomNonceModal/index.js create mode 100644 app/components/UI/CustomNonceModal/index.test.js delete mode 100644 app/util/format.js create mode 100644 app/util/general.test.js diff --git a/app/actions/settings/index.js b/app/actions/settings/index.js index 9f61e001997..8bec8eda1f1 100644 --- a/app/actions/settings/index.js +++ b/app/actions/settings/index.js @@ -12,6 +12,13 @@ export function setShowHexData(showHexData) { }; } +export function setShowCustomNonce(showCustomNonce) { + return { + type: 'SET_SHOW_CUSTOM_NONCE', + showCustomNonce + }; +} + export function setLockTime(lockTime) { return { type: 'SET_LOCK_TIME', diff --git a/app/actions/transaction/index.js b/app/actions/transaction/index.js index 449aea8d81f..ee514d97cc7 100644 --- a/app/actions/transaction/index.js +++ b/app/actions/transaction/index.js @@ -172,3 +172,17 @@ export function setCollectibleContractTransaction(collectible) { collectible }; } + +export function setNonce(nonce) { + return { + type: 'SET_NONCE', + nonce + }; +} + +export function setProposedNonce(proposedNonce) { + return { + type: 'SET_PROPOSED_NONCE', + proposedNonce + }; +} diff --git a/app/components/Base/Text.js b/app/components/Base/Text.js index d79aea64d18..906ba541319 100644 --- a/app/components/Base/Text.js +++ b/app/components/Base/Text.js @@ -17,6 +17,12 @@ const style = StyleSheet.create({ textAlign: 'right' }, bold: fontStyles.bold, + black: { + color: colors.black + }, + blue: { + color: colors.blue + }, green: { color: colors.green400 }, @@ -58,6 +64,8 @@ const Text = ({ right, bold, green, + black, + blue, primary, small, upper, @@ -77,6 +85,8 @@ const Text = ({ right && style.right, bold && style.bold, green && style.green, + black && style.black, + blue && style.blue, primary && style.primary, disclaimer && [style.small, style.disclaimer], small && style.small, @@ -98,6 +108,8 @@ Text.defaultProps = { right: false, bold: false, green: false, + black: false, + blue: false, primary: false, disclaimer: false, modal: false, @@ -130,6 +142,14 @@ Text.propTypes = { * Makes text green */ green: PropTypes.bool, + /** + * Makes text black + */ + black: PropTypes.bool, + /** + * Makes text blue + */ + blue: PropTypes.bool, /** * Makes text fontPrimary color */ diff --git a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap index 0890e1abbcc..19c45c50f29 100644 --- a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap @@ -16,6 +16,8 @@ exports[`AddCustomNetwork should render correctly 1`] = ` + + + + + + Edit transaction nonce + + + + + + Current suggested nonce: + + + 26 + + + + + + + + + + + + + + + Warning: You may encounter issues with future transactions if you continue. Use with caution. + + + + This is an advanced feature used to cancel or speed up any pending transactions. + + + Think of the nonce as the transaction number of an account. Every account's nonce begins with 0 for the first transaction and continues in sequential order. + + + + + + Cancel + + + Save + + + + + +`; diff --git a/app/components/UI/CustomNonceModal/index.js b/app/components/UI/CustomNonceModal/index.js new file mode 100644 index 00000000000..e4a9a12a7b7 --- /dev/null +++ b/app/components/UI/CustomNonceModal/index.js @@ -0,0 +1,224 @@ +import React from 'react'; +import { colors, fontStyles } from '../../../styles/common'; +import { strings } from '../../../../locales/i18n'; +import { StyleSheet, View, TextInput, SafeAreaView, TouchableOpacity } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import ModalDragger from '../../Base/ModalDragger'; +import Text from '../../Base/Text'; +import StyledButton from '../../UI/StyledButton'; +import Modal from 'react-native-modal'; +import PropTypes from 'prop-types'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import EvilIcons from 'react-native-vector-icons/EvilIcons'; + +const styles = StyleSheet.create({ + bottomModal: { + justifyContent: 'flex-end', + margin: 0 + }, + keyboardAwareWrapper: { + flex: 1, + justifyContent: 'flex-end' + }, + modal: { + minHeight: 200, + backgroundColor: colors.white, + borderTopLeftRadius: 20, + borderTopRightRadius: 20 + }, + modalContainer: { + margin: 24 + }, + title: { + fontSize: 14, + color: colors.black + }, + nonceInput: { + width: 80, + fontSize: 36, + ...fontStyles.bold, + color: colors.black, + textAlign: 'center', + marginHorizontal: 24 + }, + desc: { + color: colors.black, + fontSize: 12, + lineHeight: 16, + marginVertical: 10 + }, + nonceInputContainer: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'center', + marginVertical: 10 + }, + incrementDecrementNonceContainer: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'center' + }, + currentSuggested: { + fontSize: 14, + color: colors.grey500, + marginBottom: 10 + }, + nonceWarning: { + borderWidth: 1, + borderColor: colors.yellow, + backgroundColor: colors.yellow100, + padding: 16, + display: 'flex', + flexDirection: 'row', + borderRadius: 8, + marginTop: 10, + marginBottom: 16 + }, + nonceWarningText: { + color: colors.black, + fontSize: 12, + lineHeight: 16, + width: '100%', + flex: 1 + }, + descWarningContainer: { + height: 240 + }, + actionRow: { + flexDirection: 'row', + marginBottom: 15 + }, + actionButton: { + flex: 1, + marginHorizontal: 8 + }, + incrementHit: { + padding: 4 + }, + icon: { + flex: 0, + marginTop: 6, + paddingRight: 14 + }, + incrementDecrementIcon: { + color: colors.blue + } +}); + +const CustomModalNonce = ({ proposedNonce, nonceValue, close, save }) => { + const [nonce, onChangeText] = React.useState(nonceValue); + + const incrementDecrementNonce = decrement => { + let newValue = nonce; + newValue = decrement ? --newValue : ++newValue; + onChangeText(newValue > 1 ? newValue : 1); + }; + + const saveAndClose = () => { + save(nonce); + close(); + }; + + const displayWarning = String(proposedNonce) !== String(nonce); + + return ( + + + + + + + {strings('transaction.edit_transaction_nonce')} + + + + + + {strings('transaction.current_suggested_nonce')} {proposedNonce} + + + incrementDecrementNonce(true)}> + + + incrementDecrementNonce(false)} + > + + + + + {displayWarning ? ( + + + {strings('transaction.nonce_warning')} + + ) : null} + + {strings('transaction.this_is_an_advanced')} + + {strings('transaction.think_of_the_nonce')} + + + + + {strings('transaction.cancel')} + + saveAndClose(nonce)} + containerStyle={styles.actionButton} + > + {strings('transaction.save')} + + + + + + ); +}; + +CustomModalNonce.propTypes = { + proposedNonce: PropTypes.number.isRequired, + nonceValue: PropTypes.number.isRequired, + save: PropTypes.func.isRequired, + close: PropTypes.func.isRequired +}; + +export default CustomModalNonce; diff --git a/app/components/UI/CustomNonceModal/index.test.js b/app/components/UI/CustomNonceModal/index.test.js new file mode 100644 index 00000000000..6ec667ee64b --- /dev/null +++ b/app/components/UI/CustomNonceModal/index.test.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CustomNonceModal from './'; + +describe('CustomNonceModal', () => { + it('should render correctly', () => { + const proposedNonce = 26; + const customNonce = 28; + const noop = () => ({}); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap index 76937393466..71e670559c0 100644 --- a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap @@ -19,6 +19,8 @@ exports[`ReceiveRequest should render correctly 1`] = ` } > +

- - - Amount - - - - - + } + } + > + - Network fee + Amount - + + + + - - Edit + Network fee - - - + + + Edit + + + + - - - - - - + + + + + + + Total + + Amount + + + - Total - - Amount - - - - - - - + > + + + + `; diff --git a/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js b/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js index a3dab58bdf6..4a2eec462be 100644 --- a/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js @@ -16,6 +16,20 @@ const styles = StyleSheet.create({ }, over: { color: colors.red + }, + customNonce: { + marginTop: 10, + marginHorizontal: 24, + borderWidth: 1, + borderColor: colors.grey050, + borderRadius: 8, + paddingVertical: 14, + paddingHorizontal: 16, + display: 'flex', + flexDirection: 'row' + }, + nonceNumber: { + marginLeft: 'auto' } }); @@ -67,7 +81,19 @@ class TransactionReviewFeeCard extends PureComponent { /** * True if transaction is gas price is higher than the "FAST" value */ - warningGasPriceHigh: PropTypes.string + warningGasPriceHigh: PropTypes.string, + /** + * Indicates whether custom nonce should be shown in transaction editor + */ + showCustomNonce: PropTypes.bool, + /** + * Current nonce + */ + nonceValue: PropTypes.number, + /** + * Function called when editing nonce + */ + onNonceEdit: PropTypes.func }; renderIfGasEstimationReady = children => { @@ -93,8 +119,12 @@ class TransactionReviewFeeCard extends PureComponent { gasEstimationReady, edit, over, - warningGasPriceHigh + warningGasPriceHigh, + showCustomNonce, + nonceValue, + onNonceEdit } = this.props; + let amount; let networkFee; let totalAmount; @@ -111,44 +141,60 @@ class TransactionReviewFeeCard extends PureComponent { equivalentTotalAmount = totalValue; } return ( - - - - {strings('transaction.amount')} - - - {amount} - - - - + + + - {strings('transaction.gas_fee')} + {strings('transaction.amount')} + + + {amount} - - - {' '} - {strings('transaction.edit')} + + + + + {strings('transaction.gas_fee')} - - - {this.renderIfGasEstimationReady( - - {networkFee} + + + {' '} + {strings('transaction.edit')} + + + + {this.renderIfGasEstimationReady( + + {networkFee} + + )} + + + + + {strings('transaction.total')} {strings('transaction.amount')} + + {!!totalFiat && this.renderIfGasEstimationReady(totalAmount)} + + + {this.renderIfGasEstimationReady({equivalentTotalAmount})} + + + {showCustomNonce && ( + + + {strings('transaction.custom_nonce')} - )} - - - - - {strings('transaction.total')} {strings('transaction.amount')} - - {!!totalFiat && this.renderIfGasEstimationReady(totalAmount)} - - - {this.renderIfGasEstimationReady({equivalentTotalAmount})} - - + + {' '} + {strings('transaction.edit')} + + + {nonceValue} + + + )} + ); } } diff --git a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js index 16de211749c..e4b19b49e5a 100644 --- a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js @@ -19,8 +19,10 @@ import TransactionReviewFeeCard from '../TransactionReviewFeeCard'; import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; import { withNavigation } from 'react-navigation'; -import { getNetworkName, isMainNet } from '../../../../util/networks'; -import { capitalize } from '../../../../util/format'; +import { getNetworkName, getNetworkNonce, isMainNet } from '../../../../util/networks'; +import { capitalize } from '../../../../util/general'; +import CustomNonceModal from '../../../UI/CustomNonceModal'; +import { setNonce, setProposedNonce } from '../../../../actions/transaction'; const styles = StyleSheet.create({ overviewAlert: { @@ -184,13 +186,52 @@ class TransactionReviewInformation extends PureComponent { /** * Network id */ - network: PropTypes.string + network: PropTypes.string, + /** + * Indicates whether custom nonce should be shown in transaction editor + */ + showCustomNonce: PropTypes.bool, + /** + * Set transaction nonce + */ + setNonce: PropTypes.func, + /** + * Set proposed nonce (from network) + */ + setProposedNonce: PropTypes.func }; state = { toFocused: false, amountError: '', - actionKey: strings('transactions.tx_review_confirm') + actionKey: strings('transactions.tx_review_confirm'), + nonceModalVisible: false + }; + + componentDidMount = async () => { + await this.setNetworkNonce(); + }; + + setNetworkNonce = async () => { + const { setNonce, setProposedNonce, transaction } = this.props; + const proposedNonce = await getNetworkNonce(transaction); + setNonce(proposedNonce); + setProposedNonce(proposedNonce); + }; + + toggleNonceModal = () => this.setState(state => ({ nonceModalVisible: !state.nonceModalVisible })); + + renderCustomNonceModal = () => { + const { setNonce } = this.props; + const { proposedNonce, nonce } = this.props.transaction; + return ( + + ); }; getTotalFiat = (asset, totalGas, conversionRate, exchangeRate, currentCurrency, amountToken) => { @@ -305,7 +346,8 @@ class TransactionReviewInformation extends PureComponent { }; render() { - const { amountError } = this.state; + const { amountError, nonceModalVisible } = this.state; + const { nonce } = this.props.transaction; const { fiatValue, assetAmount, @@ -318,7 +360,8 @@ class TransactionReviewInformation extends PureComponent { ticker, error, over, - network + network, + showCustomNonce } = this.props; const is_main_net = isMainNet(network); const totalGas = isBN(gas) && isBN(gasPrice) ? gas.mul(gasPrice) : toBN('0x0'); @@ -333,6 +376,7 @@ class TransactionReviewInformation extends PureComponent { return ( + {nonceModalVisible && this.renderCustomNonceModal()} {!!amountError && ( @@ -370,7 +417,7 @@ class TransactionReviewInformation extends PureComponent { {warningGasPriceHigh} )} - {!over && ( + {!over && !showCustomNonce && ( {strings('transaction.view_data')} @@ -389,7 +436,16 @@ const mapStateToProps = state => ({ contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, transaction: getNormalizedTxState(state), ticker: state.engine.backgroundState.NetworkController.provider.ticker, - primaryCurrency: state.settings.primaryCurrency + primaryCurrency: state.settings.primaryCurrency, + showCustomNonce: state.settings.showCustomNonce +}); + +const mapDispatchToProps = dispatch => ({ + setNonce: nonce => dispatch(setNonce(nonce)), + setProposedNonce: nonce => dispatch(setProposedNonce(nonce)) }); -export default connect(mapStateToProps)(withNavigation(TransactionReviewInformation)); +export default connect( + mapStateToProps, + mapDispatchToProps +)(withNavigation(TransactionReviewInformation)); diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js index 7e78deaa49d..3fd0700c237 100644 --- a/app/components/Views/Approval/index.js +++ b/app/components/Views/Approval/index.js @@ -64,6 +64,11 @@ class Approval extends PureComponent { * Tells whether or not dApp transaction modal is visible */ dappTransactionModalVisible: PropTypes.bool, + /** + * Indicates whether custom nonce should be shown in transaction editor + */ + showCustomNonce: PropTypes.bool, + nonce: PropTypes.number, /** * Active tab URL, the currently active tab url */ @@ -217,9 +222,12 @@ class Approval extends PureComponent { const { TransactionController } = Engine.context; const { transactions, - transaction: { assetType, selectedAsset } + transaction: { assetType, selectedAsset }, + showCustomNonce } = this.props; let { transaction } = this.props; + const { nonce } = transaction; + if (showCustomNonce && nonce) transaction.nonce = BNToHex(nonce); try { if (assetType === 'ETH') { transaction = this.prepareTransaction(transaction); @@ -342,6 +350,7 @@ const mapStateToProps = state => ({ transaction: getNormalizedTxState(state), transactions: state.engine.backgroundState.TransactionController.transactions, networkType: state.engine.backgroundState.NetworkController.provider.type, + showCustomNonce: state.settings.showCustomNonce, chainId: state.engine.backgroundState.NetworkController.provider.chainId, activeTabUrl: getActiveTabUrl(state) }); diff --git a/app/components/Views/Approval/index.test.js b/app/components/Views/Approval/index.test.js index 4ef3bb31fdb..cd9beef6aba 100644 --- a/app/components/Views/Approval/index.test.js +++ b/app/components/Views/Approval/index.test.js @@ -9,6 +9,9 @@ const mockStore = configureMockStore(); describe('Approval', () => { it('should render correctly', () => { const initialState = { + settings: { + showCustomNonce: false + }, transaction: { value: '', data: '', diff --git a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap index 4ef738915b4..7d77f9aef48 100644 --- a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap @@ -45,6 +45,19 @@ exports[`Confirm should render correctly 1`] = ` } > Amount } + totalFiat={ + + } totalGasFiat="" - totalValue={} + totalValue={ + + } transactionValue="" /> Hex Data @@ -304,6 +401,19 @@ exports[`Confirm should render correctly 1`] = ` } > Hex Data 0x diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index bd7ab140216..4a755bd4b70 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -6,7 +6,6 @@ import { SafeAreaView, View, Alert, - Text, ScrollView, TouchableOpacity, ActivityIndicator @@ -30,7 +29,7 @@ import { import { getTicker, decodeTransferData, getNormalizedTxState } from '../../../../util/transactions'; import StyledButton from '../../../UI/StyledButton'; import { util } from '@metamask/controllers'; -import { prepareTransaction, resetTransaction } from '../../../../actions/transaction'; +import { prepareTransaction, resetTransaction, setNonce, setProposedNonce } from '../../../../actions/transaction'; import { apiEstimateModifiedToWEI, getGasPriceByChainId, @@ -39,6 +38,7 @@ import { import Engine from '../../../../core/Engine'; import Logger from '../../../../util/Logger'; import AccountList from '../../../UI/AccountList'; +import CustomNonceModal from '../../../UI/CustomNonceModal'; import AnimatedTransactionModal from '../../../UI/AnimatedTransactionModal'; import TransactionReviewFeeCard from '../../../UI/TransactionReview/TransactionReviewFeeCard'; import CustomGas from '../../../UI/CustomGas'; @@ -52,11 +52,13 @@ import IonicIcon from 'react-native-vector-icons/Ionicons'; import TransactionTypes from '../../../../core/TransactionTypes'; import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; -import { capitalize } from '../../../../util/format'; -import { isMainNet, getNetworkName } from '../../../../util/networks'; +import { capitalize } from '../../../../util/general'; +import { isMainNet, getNetworkName, getNetworkNonce } from '../../../../util/networks'; +import Text from '../../../Base/Text'; import AnalyticsV2 from '../../../../util/analyticsV2'; const EDIT = 'edit'; +const EDIT_NONCE = 'edit_nonce'; const REVIEW = 'review'; const { hexToBN, BNToHex } = util; @@ -268,6 +270,10 @@ class Confirm extends PureComponent { * Indicates whether hex data should be shown in transaction editor */ showHexData: PropTypes.bool, + /** + * Indicates whether custom nonce should be shown in transaction editor + */ + showCustomNonce: PropTypes.bool, /** * Network provider type as mainnet */ @@ -291,7 +297,15 @@ class Confirm extends PureComponent { /** * ETH or fiat, depending on user setting */ - primaryCurrency: PropTypes.string + primaryCurrency: PropTypes.string, + /** + * Set transaction nonce + */ + setNonce: PropTypes.func, + /** + * Set proposed nonce (from network) + */ + setProposedNonce: PropTypes.func }; state = { @@ -317,6 +331,13 @@ class Confirm extends PureComponent { over: false }; + setNetworkNonce = async () => { + const { setNonce, setProposedNonce, transaction } = this.props; + const proposedNonce = await getNetworkNonce(transaction); + setNonce(proposedNonce); + setProposedNonce(proposedNonce); + }; + getAnalyticsParams = () => { try { const { selectedAsset } = this.props; @@ -349,6 +370,7 @@ class Confirm extends PureComponent { const { navigation, providerType } = this.props; await this.handleFetchBasicEstimates(); + await this.setNetworkNonce(); navigation.setParams({ providerType }); this.parseTransactionData(); this.prepareTransaction(); @@ -386,8 +408,8 @@ class Confirm extends PureComponent { this.onModeChange(REVIEW); }; - edit = () => { - this.onModeChange(EDIT); + edit = MODE => { + this.onModeChange(MODE); }; onModeChange = mode => { @@ -586,13 +608,16 @@ class Confirm extends PureComponent { prepareTransactionToSend = () => { const { - transactionState: { transaction } + transactionState: { transaction }, + showCustomNonce } = this.props; const { fromSelectedAddress } = this.state; + const { nonce } = this.props.transaction; const transactionToSend = { ...transaction }; transactionToSend.gas = BNToHex(transaction.gas); transactionToSend.gasPrice = BNToHex(transaction.gasPrice); transactionToSend.from = fromSelectedAddress; + if (showCustomNonce && nonce) transactionToSend.nonce = BNToHex(nonce); return transactionToSend; }; @@ -684,7 +709,6 @@ class Confirm extends PureComponent { transaction, TransactionTypes.MMM ); - await TransactionController.approveTransaction(transactionMeta.id); await new Promise(resolve => resolve(result)); @@ -798,6 +822,19 @@ class Confirm extends PureComponent { ); }; + renderCustomNonceModal = () => { + const { setNonce } = this.props; + const { proposedNonce, nonce } = this.props.transaction; + return ( + this.review()} + save={setNonce} + /> + ); + }; + renderHexDataModal = () => { const { hexDataModalVisible } = this.state; const { data } = this.props.transactionState.transaction; @@ -869,7 +906,8 @@ class Confirm extends PureComponent { render = () => { const { transactionToName, selectedAsset, paymentRequest } = this.props.transactionState; - const { showHexData, primaryCurrency, network } = this.props; + const { showHexData, showCustomNonce, primaryCurrency, network } = this.props; + const { nonce } = this.props.transaction; const { gasEstimationReady, fromAccountBalance, @@ -946,9 +984,12 @@ class Confirm extends PureComponent { transactionValue={transactionValue} primaryCurrency={primaryCurrency} gasEstimationReady={gasEstimationReady} - edit={this.edit} + edit={() => this.edit(EDIT)} over={over} warningGasPriceHigh={warningGasPriceHigh} + showCustomNonce={showCustomNonce} + nonceValue={nonce} + onNonceEdit={() => this.edit(EDIT_NONCE)} /> {errorMessage && ( @@ -991,6 +1032,7 @@ class Confirm extends PureComponent { {this.renderFromAccountModal()} {mode === EDIT && this.renderCustomGasModal()} + {mode === EDIT_NONCE && this.renderCustomNonceModal()} {this.renderHexDataModal()} ); @@ -1007,6 +1049,7 @@ const mapStateToProps = state => ({ identities: state.engine.backgroundState.PreferencesController.identities, providerType: state.engine.backgroundState.NetworkController.provider.type, showHexData: state.settings.showHexData, + showCustomNonce: state.settings.showCustomNonce, ticker: state.engine.backgroundState.NetworkController.provider.ticker, keyrings: state.engine.backgroundState.KeyringController.keyrings, transaction: getNormalizedTxState(state), @@ -1017,7 +1060,9 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ prepareTransaction: transaction => dispatch(prepareTransaction(transaction)), - resetTransaction: () => dispatch(resetTransaction()) + resetTransaction: () => dispatch(resetTransaction()), + setNonce: nonce => dispatch(setNonce(nonce)), + setProposedNonce: nonce => dispatch(setProposedNonce(nonce)) }); export default connect( diff --git a/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap index 2d3d098a235..57413a9c970 100644 --- a/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap @@ -23,6 +23,8 @@ exports[`ErrorMessage should render correctly 1`] = ` > + + + Customize transaction nonce + + + Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously. + + + + + { - this.props.setShowHexData(showHexData); - }; - downloadStateLogs = async () => { const appName = await getApplicationName(); const appVersion = await getVersion(); @@ -222,7 +226,7 @@ class AdvancedSettings extends PureComponent { }; render = () => { - const { showHexData, ipfsGateway } = this.props; + const { showHexData, showCustomNonce, setShowHexData, setShowCustomNonce, ipfsGateway } = this.props; const { resetModalVisible, onlineIpfsGateways } = this.state; return ( @@ -281,7 +285,19 @@ class AdvancedSettings extends PureComponent { + + + + {strings('app_settings.show_custom_nonce')} + {strings('app_settings.custom_nonce_desc')} + + @@ -308,11 +324,13 @@ class AdvancedSettings extends PureComponent { const mapStateToProps = state => ({ ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway, showHexData: state.settings.showHexData, + showCustomNonce: state.settings.showCustomNonce, fullState: state }); const mapDispatchToProps = dispatch => ({ - setShowHexData: showHexData => dispatch(setShowHexData(showHexData)) + setShowHexData: showHexData => dispatch(setShowHexData(showHexData)), + setShowCustomNonce: showCustomNonce => dispatch(setShowCustomNonce(showCustomNonce)) }); export default connect( diff --git a/app/reducers/settings/index.js b/app/reducers/settings/index.js index 2245d0f4974..f269bea8a51 100644 --- a/app/reducers/settings/index.js +++ b/app/reducers/settings/index.js @@ -30,6 +30,11 @@ const settingsReducer = (state = initialState, action) => { ...state, showHexData: action.showHexData }; + case 'SET_SHOW_CUSTOM_NONCE': + return { + ...state, + showCustomNonce: action.showCustomNonce + }; case 'SET_USE_BLOCKIE_ICON': return { ...state, diff --git a/app/reducers/transaction/index.js b/app/reducers/transaction/index.js index a8e3f92d0c2..47a3ed17571 100644 --- a/app/reducers/transaction/index.js +++ b/app/reducers/transaction/index.js @@ -22,7 +22,9 @@ const initialState = { paymentRequest: undefined, readableValue: undefined, id: undefined, - type: undefined + type: undefined, + proposedNonce: undefined, + nonce: undefined }; const getAssetType = selectedAsset => { @@ -57,6 +59,16 @@ const transactionReducer = (state = initialState, action) => { selectedAsset: action.selectedAsset, assetType: action.assetType }; + case 'SET_NONCE': + return { + ...state, + nonce: action.nonce + }; + case 'SET_PROPOSED_NONCE': + return { + ...state, + proposedNonce: action.proposedNonce + }; case 'SET_RECIPIENT': return { ...state, diff --git a/app/util/format.js b/app/util/format.js deleted file mode 100644 index bc5eca37906..00000000000 --- a/app/util/format.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/prefer-default-export -export const capitalize = str => (str && str.charAt(0).toUpperCase() + str.slice(1)) || false; diff --git a/app/util/general.js b/app/util/general.js index 9703817b192..18db0cff41a 100644 --- a/app/util/general.js +++ b/app/util/general.js @@ -1,3 +1,5 @@ +export const tlc = str => String(str).toLowerCase(); + /** * Fetch that fails after timeout * @@ -31,3 +33,6 @@ export function findRouteNameFromNavigatorState({ routes }) { } return route?.routeName; } +export const capitalize = str => (str && str.charAt(0).toUpperCase() + str.slice(1)) || false; + +export const toLowerCaseCompare = (a, b) => tlc(a) === tlc(b); diff --git a/app/util/general.test.js b/app/util/general.test.js new file mode 100644 index 00000000000..94bf225f425 --- /dev/null +++ b/app/util/general.test.js @@ -0,0 +1,27 @@ +import { capitalize, tlc, toLowerCaseCompare } from './general'; + +describe('capitalize', () => { + const my_string = 'string'; + it('should capitalize a string', () => { + expect(capitalize(my_string)).toEqual('String'); + }); + it('should return false if a string is not provided', () => { + expect(capitalize(null)).toEqual(false); + }); +}); + +describe('tlc', () => { + it('should coerce a string toLowerCase', () => { + expect(tlc('aBCDefH')).toEqual('abcdefh'); + expect(tlc(NaN)).toEqual('nan'); + }); +}); + +describe('toLowerCaseCompare', () => { + it('compare two things', () => { + expect(toLowerCaseCompare('A', 'A')).toEqual(true); + expect(toLowerCaseCompare('aBCDefH', 'abcdefh')).toEqual(true); + expect(toLowerCaseCompare('A', 'B')).toEqual(false); + expect(toLowerCaseCompare('aBCDefH', 'abcdefi')).toEqual(false); + }); +}); diff --git a/app/util/networks.js b/app/util/networks.js index 7b019261bd1..2d859ac5d8d 100644 --- a/app/util/networks.js +++ b/app/util/networks.js @@ -2,6 +2,8 @@ import { colors } from '../styles/common'; import URL from 'url-parse'; import AppConstants from '../core/AppConstants'; import { MAINNET, ROPSTEN, KOVAN, RINKEBY, GOERLI, RPC } from '../../app/constants/network'; +import { util } from '@metamask/controllers'; +import Engine from '../core/Engine'; /** * List of the supported networks @@ -165,3 +167,9 @@ export function isPrefixedFormattedHexString(value) { } return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value); } + +export const getNetworkNonce = async ({ from }) => { + const { TransactionController } = Engine.context; + const networkNonce = await util.query(TransactionController.ethQuery, 'getTransactionCount', [from, 'pending']); + return parseInt(networkNonce, 16); +}; diff --git a/locales/languages/en.json b/locales/languages/en.json index 5f5168fa389..b8b55a7c22b 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -430,6 +430,8 @@ "privacy_mode_desc": "Websites must request access to view your account information.", "show_hex_data": "Show Hex Data", "show_hex_data_desc": "Select this to show the hex data field on the send screen.", + "show_custom_nonce": "Customize transaction nonce", + "custom_nonce_desc": "Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously.", "accounts_identicon_title": "Account Identicon", "accounts_identicon_desc": "Jazzicons and Blockies are two different styles of unique icons that help you identify an account at a glance.", "jazzicons": "Jazzicons", @@ -591,6 +593,7 @@ "reject": "Reject", "edit": "Edit", "cancel": "Cancel", + "save": "Save", "speedup": "Speed up", "from": "From", "gas_fee": "Network fee", @@ -598,6 +601,12 @@ "gas_fee_average": "AVERAGE", "gas_fee_slow": "SLOW", "hex_data": "Hex Data", + "custom_nonce": "Nonce", + "this_is_an_advanced": "This is an advanced feature used to cancel or speed up any pending transactions.", + "current_suggested_nonce": "Current suggested nonce:", + "edit_transaction_nonce": "Edit transaction nonce", + "think_of_the_nonce": "Think of the nonce as the transaction number of an account. Every account's nonce begins with 0 for the first transaction and continues in sequential order.", + "nonce_warning": "Warning: You may encounter issues with future transactions if you continue. Use with caution.", "review_details": "DETAILS", "review_data": "DATA", "data": "Data", From 8a9128973c19aca67d0d9b61ea6a5b79e1e75bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 14 Apr 2021 16:12:52 -0400 Subject: [PATCH 20/51] Check infura availability (#2520) * addcheck * component * componentUI * checkProviderStatus * log * finalstyle * locales * tryagain * snapshots * checkInfuraAvailability * updatecopy * handleinfura * tests * CONNECTIVITY_ISSUESlink * copy * copy * translations * e * check * test --- app/actions/infuraAvailability/index.js | 13 ++ app/components/Nav/Main/index.js | 54 ++++++- .../__snapshots__/index.test.js.snap | 109 ++++++++------ app/components/Views/OfflineMode/index.js | 138 ++++++++++-------- .../Views/OfflineMode/index.test.js | 14 +- app/core/AppConstants.js | 6 +- app/reducers/index.js | 4 +- app/reducers/infuraAvailability/index.js | 26 ++++ ios/Podfile.lock | 4 +- locales/languages/en.json | 5 +- locales/languages/es-OLD.json | 2 +- locales/languages/es.json | 2 +- locales/languages/hi-in.json | 2 +- locales/languages/id-id.json | 2 +- locales/languages/ja-jp.json | 2 +- locales/languages/ko-kr.json | 2 +- locales/languages/pt-br.json | 2 +- locales/languages/ru-ru.json | 2 +- locales/languages/vi-vn.json | 2 +- package.json | 2 +- yarn.lock | 8 +- 21 files changed, 267 insertions(+), 134 deletions(-) create mode 100644 app/actions/infuraAvailability/index.js create mode 100644 app/reducers/infuraAvailability/index.js diff --git a/app/actions/infuraAvailability/index.js b/app/actions/infuraAvailability/index.js new file mode 100644 index 00000000000..7fd1b8ec23d --- /dev/null +++ b/app/actions/infuraAvailability/index.js @@ -0,0 +1,13 @@ +import { INFURA_AVAILABILITY_BLOCKED, INFURA_AVAILABILITY_NOT_BLOCKED } from '../../reducers/infuraAvailability'; + +export function setInfuraAvailabilityBlocked() { + return { + type: INFURA_AVAILABILITY_BLOCKED + }; +} + +export function setInfuraAvailabilityNotBlocked() { + return { + type: INFURA_AVAILABILITY_NOT_BLOCKED + }; +} diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 86af6b065cc..214f63f3b32 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -63,6 +63,7 @@ import SwapsLiveness from '../../UI/Swaps/SwapsLiveness'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; import BigNumber from 'bignumber.js'; +import { setInfuraAvailabilityBlocked, setInfuraAvailabilityNotBlocked } from '../../../actions/infuraAvailability'; const styles = StyleSheet.create({ flex: { @@ -79,9 +80,8 @@ const styles = StyleSheet.create({ margin: 0 } }); - const Main = props => { - const [connected, setConnected] = useState(false); + const [connected, setConnected] = useState(true); const [forceReload, setForceReload] = useState(false); const [signMessage, setSignMessage] = useState(false); const [signMessageParams, setSignMessageParams] = useState({ data: '' }); @@ -138,14 +138,37 @@ const Main = props => { const connectionChangeHandler = useCallback( state => { // Show the modal once the status changes to offline - if (connected && !state.isConnected) { + if (connected && state && !state.isConnected) { props.navigation.navigate('OfflineModeView'); + setConnected(state.isConnected); } - setConnected(state.isConnected); }, [connected, props.navigation] ); + const checkInfuraAvailability = useCallback(async () => { + if (props.providerType !== 'rpc') { + try { + const { TransactionController } = Engine.context; + await util.query(TransactionController.ethQuery, 'blockNumber', []); + props.setInfuraAvailabilityNotBlocked(); + } catch (e) { + if (e.message === AppConstants.ERRORS.INFURA_BLOCKED_MESSAGE) { + props.navigation.navigate('OfflineModeView'); + props.setInfuraAvailabilityBlocked(); + } + } + } else { + props.setInfuraAvailabilityNotBlocked(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + props.navigation, + props.providerType, + props.setInfuraAvailabilityBlocked, + props.setInfuraAvailabilityNotBlocked + ]); + const initializeWalletConnect = () => { WalletConnect.hub.on('walletconnectSessionRequest', peerInfo => { setWalletConnectRequest(true); @@ -592,7 +615,7 @@ const Main = props => { removeNotificationById: props.removeNotificationById }); pollForIncomingTransactions(); - + checkInfuraAvailability(); removeConnectionStatusListener.current = NetInfo.addEventListener(connectionChangeHandler); }, 1000); @@ -709,7 +732,19 @@ Main.propTypes = { /** * Selected address */ - selectedAddress: PropTypes.string + selectedAddress: PropTypes.string, + /** + * Network provider type + */ + providerType: PropTypes.string, + /** + * Dispatch infura availability blocked + */ + setInfuraAvailabilityBlocked: PropTypes.func, + /** + * Dispatch infura availability not blocked + */ + setInfuraAvailabilityNotBlocked: PropTypes.func }; const mapStateToProps = state => ({ @@ -720,7 +755,8 @@ const mapStateToProps = state => ({ isPaymentRequest: state.transaction.paymentRequest, dappTransactionModalVisible: state.modals.dappTransactionModalVisible, approveModalVisible: state.modals.approveModalVisible, - swapsTransactions: state.engine.backgroundState.TransactionController.swapsTransactions || {} + swapsTransactions: state.engine.backgroundState.TransactionController.swapsTransactions || {}, + providerType: state.engine.backgroundState.NetworkController.provider.type }); const mapDispatchToProps = dispatch => ({ @@ -731,7 +767,9 @@ const mapDispatchToProps = dispatch => ({ hideCurrentNotification: () => dispatch(hideCurrentNotification()), removeNotificationById: id => dispatch(removeNotificationById(id)), toggleDappTransactionModal: (show = null) => dispatch(toggleDappTransactionModal(show)), - toggleApproveModal: show => dispatch(toggleApproveModal(show)) + toggleApproveModal: show => dispatch(toggleApproveModal(show)), + setInfuraAvailabilityBlocked: () => dispatch(setInfuraAvailabilityBlocked()), + setInfuraAvailabilityNotBlocked: () => dispatch(setInfuraAvailabilityNotBlocked()) }); export default connect( diff --git a/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap b/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap index 1542afc8c51..0305a7201aa 100644 --- a/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap +++ b/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap @@ -1,82 +1,109 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`OfflineMode should render correctly 1`] = ` - - + - You're offline - Check your internet connection and try again + Unable to connect to the blockchain host. - + + Try again - - + + `; diff --git a/app/components/Views/OfflineMode/index.js b/app/components/Views/OfflineMode/index.js index 6b9596e240e..b3d747bbcd7 100644 --- a/app/components/Views/OfflineMode/index.js +++ b/app/components/Views/OfflineMode/index.js @@ -1,100 +1,112 @@ 'use strict'; -import React, { PureComponent } from 'react'; -import { SafeAreaView, Image, Text, View, StyleSheet } from 'react-native'; +import React from 'react'; +import { SafeAreaView, Image, View, StyleSheet } from 'react-native'; +import Text from '../../Base/Text'; import NetInfo from '@react-native-community/netinfo'; -import { colors } from '../../../styles/common'; +import { baseStyles, colors, fontStyles } from '../../../styles/common'; import PropTypes from 'prop-types'; import { strings } from '../../../../locales/i18n'; import StyledButton from '../../UI/StyledButton'; import { getOfflineModalNavbar } from '../../UI/Navbar'; import AndroidBackHandler from '../AndroidBackHandler'; import Device from '../../../util/Device'; +import AppConstants from '../../../core/AppConstants'; +import { connect } from 'react-redux'; +import { getInfuraBlockedSelector } from '../../../reducers/infuraAvailability'; const styles = StyleSheet.create({ container: { - flex: 1, - backgroundColor: colors.white - }, - innerView: { flex: 1 }, frame: { width: 200, height: 200, alignSelf: 'center', - justifyContent: 'center', - marginTop: 80, - marginBottom: 10 + marginTop: 60 }, content: { - width: 300, - height: 125, - alignSelf: 'center', - justifyContent: 'center' - }, - text: { flex: 1, - fontSize: 12, - color: colors.fontPrimary, - textAlign: 'center', - justifyContent: 'center' + marginHorizontal: 18, + justifyContent: 'center', + marginVertical: 30 }, title: { - fontSize: 17, + fontSize: 18, color: colors.fontPrimary, - textAlign: 'center', - justifyContent: 'center', - marginBottom: 10 + marginBottom: 10, + ...fontStyles.bold }, - button: { - alignSelf: 'center', - width: 150, - height: 50 + text: { + fontSize: 12, + color: colors.fontPrimary, + ...fontStyles.normal + }, + buttonContainer: { + marginHorizontal: 18 } }); const astronautImage = require('../../../images/astronaut.png'); // eslint-disable-line import/no-commonjs -/** - * View that wraps the Offline mode screen - */ -export default class OfflineMode extends PureComponent { - static navigationOptions = ({ navigation }) => getOfflineModalNavbar(navigation); +const OfflineMode = ({ navigation, infuraBlocked }) => { + const netinfo = NetInfo.useNetInfo(); - static propTypes = { - /** - * Object that represents the navigator - */ - navigation: PropTypes.object + const tryAgain = () => { + if (netinfo?.isConnected) { + navigation.pop(); + } }; - goBack = () => { - this.props.navigation.goBack(); + const learnMore = () => { + navigation.navigate('SimpleWebview', { url: AppConstants.URLS.CONNECTIVITY_ISSUES }); }; - tryAgain = () => { - NetInfo.isConnected.fetch().then(isConnected => { - if (isConnected) { - this.props.navigation.pop(); - } - }); + const action = () => { + if (infuraBlocked) { + learnMore(); + } else { + tryAgain(); + } }; - render() { - return ( - - - - - {strings('offline_mode.title')} - {strings('offline_mode.text')} - - {strings('offline_mode.try_again')} - - - - {Device.isAndroid() && } + return ( + + + + + + {strings('offline_mode.title')} + + + {strings(`offline_mode.text`)} + + + + + {strings(`offline_mode.${infuraBlocked ? 'learn_more' : 'try_again'}`)} + + - ); - } -} + {Device.isAndroid() && } + + ); +}; + +OfflineMode.navigationOptions = ({ navigation }) => getOfflineModalNavbar(navigation); + +OfflineMode.propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + * Whether infura was blocked or not + */ + infuraBlocked: PropTypes.bool +}; + +const mapStateToProps = state => ({ + infuraBlocked: getInfuraBlockedSelector(state) +}); + +export default connect(mapStateToProps)(OfflineMode); diff --git a/app/components/Views/OfflineMode/index.test.js b/app/components/Views/OfflineMode/index.test.js index 53b1297aa0f..ab24f9fbf8c 100644 --- a/app/components/Views/OfflineMode/index.test.js +++ b/app/components/Views/OfflineMode/index.test.js @@ -1,10 +1,20 @@ import React from 'react'; import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; import OfflineMode from './'; +const mockStore = configureMockStore(); + describe('OfflineMode', () => { it('should render correctly', () => { - const wrapper = shallow( false }} />); - expect(wrapper).toMatchSnapshot(); + const initialState = { + infuraAvailability: { + isBlocked: false + } + }; + const wrapper = shallow( false }} />, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); }); }); diff --git a/app/core/AppConstants.js b/app/core/AppConstants.js index 48c3d4ca9ff..8e7a51578fd 100644 --- a/app/core/AppConstants.js +++ b/app/core/AppConstants.js @@ -66,6 +66,10 @@ export default { MAX_SAFE_CHAIN_ID: 4503599627370476, URLS: { TERMS_AND_CONDITIONS: 'https://consensys.net/terms-of-use/', - PRIVACY_POLICY: 'https://consensys.net/privacy-policy/' + PRIVACY_POLICY: 'https://consensys.net/privacy-policy/', + CONNECTIVITY_ISSUES: 'https://metamask.zendesk.com/hc/en-us/articles/360059386712' + }, + ERRORS: { + INFURA_BLOCKED_MESSAGE: 'EthQuery - RPC Error - This service is not available in your country' } }; diff --git a/app/reducers/index.js b/app/reducers/index.js index e8085a142b5..66484349551 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -13,6 +13,7 @@ import onboardingReducer from './onboarding'; import fiatOrders from './fiatOrders'; import swapsReducer from './swaps'; import notificationReducer from './notification'; +import infuraAvailabilityReducer from './infuraAvailability'; import { combineReducers } from 'redux'; const rootReducer = combineReducers({ @@ -30,7 +31,8 @@ const rootReducer = combineReducers({ onboarding: onboardingReducer, notification: notificationReducer, swaps: swapsReducer, - fiatOrders + fiatOrders, + infuraAvailability: infuraAvailabilityReducer }); export default rootReducer; diff --git a/app/reducers/infuraAvailability/index.js b/app/reducers/infuraAvailability/index.js new file mode 100644 index 00000000000..4079763a530 --- /dev/null +++ b/app/reducers/infuraAvailability/index.js @@ -0,0 +1,26 @@ +const initialState = { + isBlocked: false +}; + +export const INFURA_AVAILABILITY_BLOCKED = 'INFURA_AVAILABILITY_BLOCKED'; +export const INFURA_AVAILABILITY_NOT_BLOCKED = 'INFURA_AVAILABILITY_NOT_BLOCKED'; + +export const getInfuraBlockedSelector = state => state.infuraAvailability?.isBlocked; + +const infuraAvailabilityReducer = (state = initialState, action) => { + switch (action.type) { + case INFURA_AVAILABILITY_BLOCKED: + return { + ...state, + isBlocked: true + }; + case INFURA_AVAILABILITY_NOT_BLOCKED: + return { + ...state, + isBlocked: false + }; + default: + return state; + } +}; +export default infuraAvailabilityReducer; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6e70a3a7975..5b829048f67 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -634,7 +634,7 @@ SPEC CHECKSUMS: Branch: 49c609eb0ac0130b8491d0923a9298714731bd0d CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f - DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2 + DoubleConversion: cde416483dac037923206447da6e1454df403714 FBLazyVector: 3bb422f41b18121b71783a905c10e58606f7dc3e FBReactNativeSpec: f2c97f2529dd79c083355182cc158c9f98f4bd6e Flipper: be611d4b742d8c87fbae2ca5f44603a02539e365 @@ -645,7 +645,7 @@ SPEC CHECKSUMS: Flipper-RSocket: a3acb8812d6adf127deb0a5edae2793b97e6b641 FlipperKit: ab353d41aea8aae2ea6daaf813e67496642f3d7d Folly: b73c3869541e86821df3c387eb0af5f65addfab4 - glog: 1f3da668190260b06b429bb211bfbee5cd790c28 + glog: 40a13f7840415b9a77023fbcae0f1e6f43192af3 lottie-ios: a50d5c0160425cd4b01b852bb9578963e6d92d31 lottie-react-native: 7ca15c46249b61e3f9ffcf114cb4123e907a2156 OpenSSL-Universal: ff34003318d5e1163e9529b08470708e389ffcdd diff --git a/locales/languages/en.json b/locales/languages/en.json index b8b55a7c22b..b52c5efde68 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1257,8 +1257,9 @@ }, "offline_mode": { "title": "You're offline", - "text": "Check your internet connection and try again", - "try_again": "Try again" + "text": "Unable to connect to the blockchain host.", + "try_again": "Try again", + "learn_more": "Learn more" }, "walletconnect_return_modal": { "title": "You're all set!", diff --git a/locales/languages/es-OLD.json b/locales/languages/es-OLD.json index 82cff406943..f4d0d25e9ae 100644 --- a/locales/languages/es-OLD.json +++ b/locales/languages/es-OLD.json @@ -1088,7 +1088,7 @@ }, "offline_mode": { "title": "Sin Conexión", - "text": "Revisa tu conexión de internet e intenta nuevamente", + "text": "No se puede conectar al host de blockchain.", "try_again": "Intentar de nuevo" }, "walletconnect_return_modal": { diff --git a/locales/languages/es.json b/locales/languages/es.json index c8b74a5f16d..d9e9c954ae1 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -1292,7 +1292,7 @@ }, "offline_mode": { "title": "Está desconectado", - "text": "Compruebe la conexión a Internet y vuelva a intentarlo", + "text": "No se puede conectar al host de blockchain.", "try_again": "Vuelva a intentarlo" }, "payment_channel_request": { diff --git a/locales/languages/hi-in.json b/locales/languages/hi-in.json index f567a98a491..f1ea43747bd 100644 --- a/locales/languages/hi-in.json +++ b/locales/languages/hi-in.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "आप ऑफ़लाइन हैं", - "text": "अपना इंटरनेट कनेक्शन जाँचें और पुनः प्रयास करें", + "text": "ब्लॉकचैन होस्ट से कनेक्ट करने में असमर्थ।", "try_again": "पुनः प्रयास करें" }, "payment_channel_request": { diff --git a/locales/languages/id-id.json b/locales/languages/id-id.json index 317a524de5b..bd98a57fc64 100644 --- a/locales/languages/id-id.json +++ b/locales/languages/id-id.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "Anda sedang offline", - "text": "Periksa koneksi internet Anda dan coba lagi", + "text": "Tidak dapat terhubung ke host blockchain.", "try_again": "Coba lagi" }, "payment_channel_request": { diff --git a/locales/languages/ja-jp.json b/locales/languages/ja-jp.json index 268607c8df7..8bade3dfb1f 100644 --- a/locales/languages/ja-jp.json +++ b/locales/languages/ja-jp.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "オフラインです", - "text": "インターネット接続を確認して、もう一度実行してください", + "text": "ブロックチェーンホストに接続できません。", "try_again": "再試行" }, "payment_channel_request": { diff --git a/locales/languages/ko-kr.json b/locales/languages/ko-kr.json index 71a333aa5cf..4ef4f20c992 100644 --- a/locales/languages/ko-kr.json +++ b/locales/languages/ko-kr.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "오프라인 상태입니다.", - "text": "인터넷 연결을 확인하고 다시 시도하세요.", + "text": "블록 체인 호스트에 연결할 수 없습니다.", "try_again": "다시 시도" }, "payment_channel_request": { diff --git a/locales/languages/pt-br.json b/locales/languages/pt-br.json index 999173788c9..f47d2e8f816 100644 --- a/locales/languages/pt-br.json +++ b/locales/languages/pt-br.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "Você está offline", - "text": "Verifique a conexão com a internet e tente novamente", + "text": "Não foi possível conectar ao host blockchain.", "try_again": "Tente novamente" }, "payment_channel_request": { diff --git a/locales/languages/ru-ru.json b/locales/languages/ru-ru.json index 06c33e2121c..15144420bfb 100644 --- a/locales/languages/ru-ru.json +++ b/locales/languages/ru-ru.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "Вы не в сети", - "text": "Проверьте подключение к интернету и попробуйте еще раз", + "text": "Невозможно подключиться к хосту блокчейна.", "try_again": "Попробуйте еще раз" }, "payment_channel_request": { diff --git a/locales/languages/vi-vn.json b/locales/languages/vi-vn.json index 4a45d238326..22bf24e6d65 100644 --- a/locales/languages/vi-vn.json +++ b/locales/languages/vi-vn.json @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "Bạn đang không kết nối mạng", - "text": "Hãy kiểm tra kết nối internet của bạn và thử lại", + "text": "Không thể kết nối với máy chủ lưu trữ chuỗi khối.", "try_again": "Thử lại" }, "payment_channel_request": { diff --git a/package.json b/package.json index 940e09094e7..92acaf32395 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@react-native-community/clipboard": "^1.2.2", "@react-native-community/cookies": "^4.0.1", "@react-native-community/masked-view": "^0.1.10", - "@react-native-community/netinfo": "4.1.5", + "@react-native-community/netinfo": "6.0.0", "@react-native-community/viewpager": "^3.3.0", "@rnhooks/keyboard": "^0.0.3", "@sentry/integrations": "5.13.0", diff --git a/yarn.lock b/yarn.lock index 60e84cccafb..5c95b5c6ca9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1790,10 +1790,10 @@ resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.10.tgz#5dda643e19e587793bc2034dd9bf7398ad43d401" integrity sha512-rk4sWFsmtOw8oyx8SD3KSvawwaK7gRBSEIy2TAwURyGt+3TizssXP1r8nx3zY+R7v2vYYHXZ+k2/GULAT/bcaQ== -"@react-native-community/netinfo@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-4.1.5.tgz#4bb44842db6a1a18f00a0f061b0e3dcc638f67dd" - integrity sha512-lagdZr9UiVAccNXYfTEj+aUcPCx9ykbMe9puffeIyF3JsRuMmlu3BjHYx1klUHX7wNRmFNC8qVP0puxUt1sZ0A== +"@react-native-community/netinfo@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-6.0.0.tgz#2a4d7190b508dd0c2293656c9c1aa068f6f60a71" + integrity sha512-Z9M8VGcF2IZVOo2x+oUStvpCW/8HjIRi4+iQCu5n+PhC7OqCQX58KYAzdBr///alIfRXiu6oMb+lK+rXQH1FvQ== "@react-native-community/viewpager@^2.0.1": version "2.0.2" From b0cfdeeaebbb12c08268c3f4f2fc8172ccb3d989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 14 Apr 2021 18:08:17 -0400 Subject: [PATCH 21/51] use contract metadata version from package (#2373) Co-authored-by: Ibrahim Taveras --- app/util/assets.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/util/assets.js b/app/util/assets.js index af2d7737a1d..a1812397dc3 100644 --- a/app/util/assets.js +++ b/app/util/assets.js @@ -1,11 +1,14 @@ +const pack = require('../../package.json'); // eslint-disable-line + /** * Utility function to return corresponding @metamask/contract-metadata logo * * @param {string} logo - Logo path from @metamask/contract-metadata */ export default function getAssetLogoPath(logo) { + const version = pack.dependencies['@metamask/contract-metadata']?.replace('^', ''); if (!logo) return; - const path = 'https://raw.githubusercontent.com/metamask/contract-metadata/v1.23.0/images/'; + const path = `https://raw.githubusercontent.com/metamask/contract-metadata/v${version}/images/`; const uri = path + logo; return uri; } From 37d13d5561a8f12a20d2d7fad9e3264d35b14bbd Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Thu, 15 Apr 2021 13:31:26 +0100 Subject: [PATCH 22/51] Fix typo (#2534) --- app/components/Views/Approval/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js index 3fd0700c237..a72d4d62c4e 100644 --- a/app/components/Views/Approval/index.js +++ b/app/components/Views/Approval/index.js @@ -260,7 +260,7 @@ class Approval extends PureComponent { Logger.error(error, 'error while trying to send transaction (Approval)'); this.setState({ transactionHandled: false }); } - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.DAPP_TRANSACTION_CONFIRMED, this.getAnalyticsParams()); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.DAPP_TRANSACTION_COMPLETED, this.getAnalyticsParams()); }; /** From 53bb1e1dc566c82f44de324f6d38931a15ae4dba Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Thu, 15 Apr 2021 17:52:28 +0100 Subject: [PATCH 23/51] Merge pull request from GHSA-3hjh-69hq-6wgp --- app/core/DeeplinkManager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/core/DeeplinkManager.js b/app/core/DeeplinkManager.js index 5e5cf90b337..34ecf3b1e82 100644 --- a/app/core/DeeplinkManager.js +++ b/app/core/DeeplinkManager.js @@ -63,7 +63,10 @@ class DeeplinkManager { txParams.to = `${target_address}`; txParams.from = `${PreferencesController.state.selectedAddress}`; txParams.value = '0x0'; - const value = Number(uint256).toString(16); + const uint256Number = Number(uint256); + if (Number.isNaN(uint256Number)) throw new Error('The parameter uint256 should be a number'); + if (!Number.isInteger(uint256Number)) throw new Error('The parameter uint256 should be an integer'); + const value = uint256Number.toString(16); txParams.data = generateApproveData({ spender: address, value }); TransactionController.addTransaction(txParams, origin); } From 4f69e2545be56046550c4e736e8dc4d0980f0531 Mon Sep 17 00:00:00 2001 From: sethkfman <10342624+sethkfman@users.noreply.github.com> Date: Thu, 15 Apr 2021 14:12:32 -0600 Subject: [PATCH 24/51] Bug/persists old account names (#2469) * added functionality to persists old account names when resetting password #2002 * added logic to check for old account/identities and persist their balances and names * added check to see if it is not an existing user * added check to see if it is a existing user * add sync utility code and test to reduce common code * remove logs --- app/components/Views/Login/index.js | 3 +- app/components/Views/ResetPassword/index.js | 10 +- app/core/Vault.js | 19 +++- app/util/sync.js | 37 +++++++ app/util/sync.test.js | 106 ++++++++++++++++++++ 5 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 app/util/sync.js create mode 100644 app/util/sync.test.js diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 9ea55a275af..4695544d0e2 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -278,7 +278,8 @@ class Login extends PureComponent { // Restore vault with user entered password await KeyringController.submitPassword(this.state.password); const encryptionLib = await AsyncStorage.getItem(ENCRYPTION_LIB); - if (encryptionLib !== ORIGINAL) { + const existingUser = await AsyncStorage.getItem(EXISTING_USER); + if (encryptionLib !== ORIGINAL && existingUser) { await recreateVaultWithSamePassword(this.state.password, this.props.selectedAddress); await AsyncStorage.setItem(ENCRYPTION_LIB, ORIGINAL); } diff --git a/app/components/Views/ResetPassword/index.js b/app/components/Views/ResetPassword/index.js index 1a7a3a7957a..0a9eb1ce249 100644 --- a/app/components/Views/ResetPassword/index.js +++ b/app/components/Views/ResetPassword/index.js @@ -37,6 +37,7 @@ import { ONBOARDING, PREVIOUS_SCREEN } from '../../../constants/navigation'; import { EXISTING_USER, TRUE, BIOMETRY_CHOICE_DISABLED } from '../../../constants/storage'; import { getPasswordStrengthWord, passwordRequirementsMet } from '../../../util/password'; import NotificationManager from '../../../core/NotificationManager'; +import { syncPrefs } from '../../../util/sync'; const styles = StyleSheet.create({ mainWrapper: { @@ -401,6 +402,7 @@ class ResetPassword extends PureComponent { const { originalPassword, password: newPassword } = this.state; const { KeyringController, PreferencesController } = Engine.context; const seedPhrase = await this.getSeedPhrase(); + const oldPrefs = PreferencesController.state; let importedAccounts = []; try { @@ -427,7 +429,6 @@ class ResetPassword extends PureComponent { const hdKeyring = KeyringController.state.keyrings[0]; const existingAccountCount = hdKeyring.accounts.length; const selectedAddress = this.props.selectedAddress; - let preferencesControllerState = PreferencesController.state; // Create previous accounts again for (let i = 0; i < existingAccountCount - 1; i++) { @@ -443,11 +444,12 @@ class ResetPassword extends PureComponent { Logger.error(e, 'error while trying to import accounts on recreate vault'); } - // Reset preferencesControllerState - preferencesControllerState = PreferencesController.state; + //Persist old account/identities names + const preferencesControllerState = PreferencesController.state; + const prefUpdates = syncPrefs(oldPrefs, preferencesControllerState); // Set preferencesControllerState again - await PreferencesController.update(preferencesControllerState); + await PreferencesController.update(prefUpdates); // Reselect previous selected account if still available if (hdKeyring.accounts.includes(selectedAddress)) { PreferencesController.setSelectedAddress(selectedAddress); diff --git a/app/core/Vault.js b/app/core/Vault.js index 471e6618d5a..6c3ef8684d1 100644 --- a/app/core/Vault.js +++ b/app/core/Vault.js @@ -1,5 +1,6 @@ import Engine from './Engine'; import Logger from '../util/Logger'; +import { syncPrefs, syncAccounts } from '../util/sync'; /** * Returns current vault seed phrase @@ -18,8 +19,10 @@ export const getSeedPhrase = async (password = '') => { * @param password - Password to recreate and set the vault with */ export const recreateVaultWithSamePassword = async (password = '', selectedAddress) => { - const { KeyringController, PreferencesController } = Engine.context; + const { KeyringController, PreferencesController, AccountTrackerController } = Engine.context; const seedPhrase = await getSeedPhrase(password); + const oldPrefs = PreferencesController.state; + const oldAccounts = AccountTrackerController.accounts; let importedAccounts = []; try { @@ -42,7 +45,6 @@ export const recreateVaultWithSamePassword = async (password = '', selectedAddre // Get props to restore vault const hdKeyring = KeyringController.state.keyrings[0]; const existingAccountCount = hdKeyring.accounts.length; - let preferencesControllerState = PreferencesController.state; // Create previous accounts again for (let i = 0; i < existingAccountCount - 1; i++) { @@ -58,11 +60,18 @@ export const recreateVaultWithSamePassword = async (password = '', selectedAddre Logger.error(e, 'error while trying to import accounts on recreate vault'); } - // Reset preferencesControllerState - preferencesControllerState = PreferencesController.state; + //Persist old account/identities names + const preferencesControllerState = PreferencesController.state; + const prefUpdates = syncPrefs(oldPrefs, preferencesControllerState); + + //Persist old account data + const accounts = AccountTrackerController.accounts; + const updateAccounts = syncAccounts(oldAccounts, accounts); // Set preferencesControllerState again - await PreferencesController.update(preferencesControllerState); + await PreferencesController.update(prefUpdates); + await AccountTrackerController.update(updateAccounts); + // Reselect previous selected account if still available if (hdKeyring.accounts.includes(selectedAddress)) { PreferencesController.setSelectedAddress(selectedAddress); diff --git a/app/util/sync.js b/app/util/sync.js new file mode 100644 index 00000000000..571599b377e --- /dev/null +++ b/app/util/sync.js @@ -0,0 +1,37 @@ +/** + * Function to persist the old account name during an new preferences update + * @param {Object} oldPrefs - old preferences object containing the account names + * @param {Object} updatedPref - preferences object that will be updated with oldPrefs + */ +export async function syncPrefs(oldPrefs, updatedPref) { + try { + Object.keys(oldPrefs.identities).forEach(ids => { + if (updatedPref.identities[ids]) { + updatedPref.identities[ids] = oldPrefs.identities[ids]; + } + }); + + return updatedPref; + } catch (err) { + return updatedPref; + } +} + +/** + * Function to persist the old account balance during an vault update + * @param {Object} oldAccounts - old account object containing the account names + * @param {Object} updatedAccounts - accounts object that will be updated with old accout balance + */ +export async function syncAccounts(oldAccounts, updatedAccounts) { + try { + Object.keys(oldAccounts).forEach(account => { + if (updatedAccounts[account]) { + updatedAccounts[account] = oldAccounts[account]; + } + }); + + return updatedAccounts; + } catch (err) { + return updatedAccounts; + } +} diff --git a/app/util/sync.test.js b/app/util/sync.test.js new file mode 100644 index 00000000000..2fd5f8dfaf9 --- /dev/null +++ b/app/util/sync.test.js @@ -0,0 +1,106 @@ +import { syncPrefs, syncAccounts } from '../util/sync'; + +const OLD_PREFS = { + accountTokens: { + '0x0942890c603273059a11a298F81cb137Be9CF704': { '0x1': [Array], '0x3': [Array] }, + '0x120bfFfa4138fD00A8025a223C350b9ffaDAD8F5': { '0x3': [Array] }, + '0x16C6C3079edE914e83B388a52fFD9255E1c3165': { '0x3': [Array] }, + '0x223367C61c38FAcbdd0b92De5aA7B742e1e5a196': { '0x1': [Array], '0x3': [Array] }, + '0x7b8C6B8363B9E7A77d279dDad49BEF2994a3bf28': { '0x3': [Array] }, + '0x9236413AfD369B2aeb5e52C048f6B30e7308f2e3': { '0x1': [Array], '0x3': [Array] }, + '0x9b07Ba86631bdb74eE2DDb5750440986DECB9e11': { '0x1': [Array], '0x3': [Array] }, + '0xE4D7f194b07B85511973f1FAAB31b8C2F1f9F344': { '0x3': [Array] } + }, + currentLocale: 'en', + featureFlags: {}, + frequentRpcList: [], + identities: { + '0x7f9f9A0e248Ef58298e911219e5B45D610C4B539': { + address: '0x7f9f9A0e248Ef58298e911219e5B45D610C4B539', + name: 'Testy Account' + } + }, + ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', + lostIdentities: {}, + selectedAddress: '0x7f9f9A0e248Ef58298e911219e5B45D610C4B539', + tokens: [] +}; +const OLD_ACCOUNTS = { + '0x0942890c603273059a11a298F81cb137Be9CF704': { balance: '0x365369025dd23000' }, + '0x120bfFfa4138fD00A8025a223C350b9ffaDAD8F5': { balance: '0x0' }, + '0x16C6C3079edE914e83B388a52fFD9255E1c3165': { balance: '0x0' }, + '0x223367C61c38FAcbdd0b92De5aA7B742e1e5a196': { balance: '0x1bf5ef59d293408b' }, + '0x7b8C6B8363B9E7A77d279dDad49BEF2994a3bf28': { balance: '0x0' }, + '0x9236413AfD369B2aeb5e52C048f6B30e7308f2e3': { balance: '0x0' }, + '0x9b07Ba86631bdb74eE2DDb5750440986DECB9e11': { balance: '0xe8d4a51000' }, + '0xE4D7f194b07B85511973f1FAAB31b8C2F1f9F344': { balance: '0x0' } +}; +const NEW_PREFS = { + accountTokens: { + '0x0942890c603273059a11a298F81cb137Be9CF704': { '0x1': [Array], '0x3': [Array] }, + '0x120bfFfa4138fD00A8025a223C350b9ffaDAD8F5': { '0x3': [Array] }, + '0x16C6C3079edE914e83B388a52fFD9255E1c3165': { '0x3': [Array] }, + '0x223367C61c38FAcbdd0b92De5aA7B742e1e5a196': { '0x1': [Array], '0x3': [Array] }, + '0x7b8C6B8363B9E7A77d279dDad49BEF2994a3bf28': { '0x3': [Array] }, + '0x9236413AfD369B2aeb5e52C048f6B30e7308f2e3': { '0x1': [Array], '0x3': [Array] }, + '0x9b07Ba86631bdb74eE2DDb5750440986DECB9e11': { '0x1': [Array], '0x3': [Array] }, + '0xE4D7f194b07B85511973f1FAAB31b8C2F1f9F344': { '0x3': [Array] } + }, + currentLocale: 'en', + featureFlags: {}, + frequentRpcList: [], + identities: { + '0x7f9f9A0e248Ef58298e911219e5B45D610C4B539': { + address: '0x7f9f9A0e248Ef58298e911219e5B45D610C4B539', + name: 'Account 1' + }, + '0x7f9f9A0e248Ef58298e911219e5B45D610C4B589': { + address: '0x7f9f9A0e248Ef58298e911219e5B45D610C4B589', + name: 'Account 2' + } + }, + ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', + lostIdentities: {}, + selectedAddress: '0x7f9f9A0e248Ef58298e911219e5B45D610C4B539', + tokens: [] +}; +const NEW_ACCOUNTS = { + '0x0942890c603273059a11a298F81cb137Be9CF704': { balance: '0x0' }, + '0x120bfFfa4138fD00A8025a223C350b9ffaDAD8F5': { balance: '0x0' }, + '0x16C6C3079edE914e83B388a52fFD9255E1c3165': { balance: '0x0' }, + '0x223367C61c38FAcbdd0b92De5aA7B742e1e5a196': { balance: '0x0' }, + '0x7b8C6B8363B9E7A77d279dDad49BEF2994a3bf28': { balance: '0x0' }, + '0x9236413AfD369B2aeb5e52C048f6B30e7308f2e3': { balance: '0x0' }, + '0x9b07Ba86631bdb74eE2DDb5750440986DECB9e11': { balance: '0x0' }, + '0xE4D7f194b07B85511973f1FAAB31b8C2F1f9F344': { balance: '0x0' } +}; + +describe('Success Sync', () => { + it('should succeed sync prefs of varying lengths', async () => { + const syncedPrefs = await syncPrefs(OLD_PREFS, NEW_PREFS); + expect(Object.values(syncedPrefs.identities)[0]).toEqual(Object.values(syncedPrefs.identities)[0]); + expect(Object.values(syncedPrefs.identities)[1]).not.toBeUndefined(); + expect(Object.values(syncedPrefs.identities).length).not.toEqual(Object.values(OLD_PREFS.identities).length); + }); + it('should succeed sync accounts balances', async () => { + const syncedAccounts = await syncAccounts(OLD_ACCOUNTS, NEW_ACCOUNTS); + expect(Object.values(syncedAccounts)[0].balance).toEqual(Object.values(OLD_ACCOUNTS)[0].balance); + expect(Object.values(syncedAccounts)[3].balance).toEqual(Object.values(OLD_ACCOUNTS)[3].balance); + expect(Object.values(syncedAccounts)[6].balance).toEqual(Object.values(OLD_ACCOUNTS)[6].balance); + }); +}); + +describe('Error Syncs', () => { + it('should return undefined sync prefs', async () => { + expect(await syncPrefs(OLD_PREFS, undefined)).toEqual(undefined); + }); + it('should return new sync prefs', async () => { + expect(await syncPrefs(undefined, NEW_PREFS)).toEqual(NEW_PREFS); + }); + it('should return new sync accounts', async () => { + expect(await syncAccounts(undefined, NEW_ACCOUNTS)).toEqual(NEW_ACCOUNTS); + }); + it('should return undefined sync accounts', async () => { + expect(await syncAccounts(OLD_ACCOUNTS, undefined)).toEqual(undefined); + }); +}); From bda6776d8e73505208cdefa3b39ab7d5bddf2b17 Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 15 Apr 2021 16:23:17 -0400 Subject: [PATCH 25/51] Fix notification so it doesn't block terms + conditions (#2485) * Fix notification so it doesn't block terms + conditions * Move Animated.View outside ElevatedView --- .../UI/Notification/SimpleNotification/index.js | 11 ++++++----- app/components/Views/Onboarding/index.js | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/components/UI/Notification/SimpleNotification/index.js b/app/components/UI/Notification/SimpleNotification/index.js index 22735c06d21..bde4f6bfe56 100644 --- a/app/components/UI/Notification/SimpleNotification/index.js +++ b/app/components/UI/Notification/SimpleNotification/index.js @@ -28,17 +28,18 @@ const styles = StyleSheet.create({ function SimpleNotification({ isInBrowserView, notificationAnimated, hideCurrentNotification, currentNotification }) { return ( - - + - - + + ); } diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index d0b8964a164..d83e83674dc 100644 --- a/app/components/Views/Onboarding/index.js +++ b/app/components/Views/Onboarding/index.js @@ -624,17 +624,17 @@ class Onboarding extends PureComponent { handleSimpleNotification = () => { if (!this.props.navigation.getParam('delete', false)) return; return ( - - + + - - + + ); }; From ed3d8505c4ff5f25cc4b515d1aa67c57fd2b582e Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 15 Apr 2021 16:42:23 -0400 Subject: [PATCH 26/51] Change Send Feedback to Request a Feature and update link (#2536) --- app/components/UI/DrawerView/index.js | 7 +++++-- locales/languages/en.json | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 052ed51e878..4590ab75626 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -573,7 +573,10 @@ class DrawerView extends PureComponent { submitFeedback = () => { this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_SEND_FEEDBACK); - this.goToBrowserUrl('https://metamask.zendesk.com/hc/en-us/requests/new', strings('drawer.metamask_support')); + this.goToBrowserUrl( + 'https://community.metamask.io/c/feature-requests-ideas/', + strings('drawer.request_feature') + ); }; showHelp = () => { @@ -722,7 +725,7 @@ class DrawerView extends PureComponent { action: this.showHelp }, { - name: strings('drawer.submit_feedback'), + name: strings('drawer.request_feature'), icon: this.getFeatherIcon('message-square'), action: this.submitFeedback }, diff --git a/locales/languages/en.json b/locales/languages/en.json index b52c5efde68..5f61e4edb68 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -229,7 +229,7 @@ "coming_soon": "Coming soon...", "wallet": "Wallet", "transaction_history": "Transaction History", - "submit_feedback": "Send Feedback", + "request_feature": "Request a Feature", "submit_feedback_message": "Choose the type of feedback to send.", "submit_bug": "Bug Report", "submit_general_feedback": "General", From fd19a47a380d6ed34560a93404ff4789a45523b0 Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 15 Apr 2021 17:11:41 -0400 Subject: [PATCH 27/51] Style updates (#2375) * Fix AssetList typeface * Update BaseNotification text * Update ChoosePassword ImportFromSeed * Update styles * Update snapshots * Update snapshot * fix checkmark position * update snapshots --- .../__snapshots__/index.test.js.snap | 6 +- app/components/UI/AssetList/index.js | 3 +- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 40 ++++++------- .../__snapshots__/index.test.js.snap | 10 ++-- .../UI/Notification/BaseNotification/index.js | 3 +- .../__snapshots__/index.test.js.snap | 2 +- .../UI/Tabs/__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 4 +- .../__snapshots__/index.test.js.snap | 14 ++--- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 60 +++++++++---------- app/components/Views/ChoosePassword/index.js | 39 ++++++------ .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 59 +++++++++++++----- app/components/Views/ImportFromSeed/index.js | 29 +++++---- app/components/Views/Login/index.js | 10 +++- .../__snapshots__/index.test.js.snap | 24 ++++---- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- .../Amount/__snapshots__/index.test.js.snap | 5 +- app/components/Views/SendFlow/Amount/index.js | 8 ++- .../Confirm/__snapshots__/index.test.js.snap | 8 +-- .../Views/SendFlow/Confirm/index.js | 2 +- .../SendTo/__snapshots__/index.test.js.snap | 4 +- .../__snapshots__/index.test.js.snap | 4 +- .../__snapshots__/index.test.js.snap | 4 +- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- app/styles/common.js | 2 +- app/util/password.js | 2 +- 34 files changed, 207 insertions(+), 157 deletions(-) diff --git a/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap b/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap index 042bc064d6b..3b4ced5f45c 100644 --- a/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap +++ b/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap @@ -45,7 +45,7 @@ exports[`AccountInfoCard should render correctly 1`] = ` numberOfLines={1} style={ Object { - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Bold", "fontSize": 16, "fontWeight": "600", @@ -60,7 +60,7 @@ exports[`AccountInfoCard should render correctly 1`] = ` numberOfLines={1} style={ Object { - "color": "#000000", + "color": "#24292E", "flexGrow": 1, "fontFamily": "EuclidCircularB-Bold", "fontSize": 16, @@ -78,7 +78,7 @@ exports[`AccountInfoCard should render correctly 1`] = ` style={ Object { "alignSelf": "flex-start", - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "100", diff --git a/app/components/UI/AssetList/index.js b/app/components/UI/AssetList/index.js index 07c4c31b97d..dc56faa968e 100644 --- a/app/components/UI/AssetList/index.js +++ b/app/components/UI/AssetList/index.js @@ -1,10 +1,11 @@ import React, { PureComponent } from 'react'; -import { Text, View, StyleSheet } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; import { strings } from '../../../../locales/i18n'; import StyledButton from '../StyledButton'; // eslint-disable-line import/no-unresolved import AssetIcon from '../AssetIcon'; import { fontStyles } from '../../../styles/common'; +import Text from '../../Base/Text'; const styles = StyleSheet.create({ rowWrapper: { diff --git a/app/components/UI/BiometryButton/__snapshots__/index.test.js.snap b/app/components/UI/BiometryButton/__snapshots__/index.test.js.snap index 47166de2fc3..a7a712f4332 100644 --- a/app/components/UI/BiometryButton/__snapshots__/index.test.js.snap +++ b/app/components/UI/BiometryButton/__snapshots__/index.test.js.snap @@ -13,7 +13,7 @@ exports[`BiometryButton should render correctly 1`] = ` > @@ -36,7 +36,7 @@ exports[`CustomGas should render correctly 1`] = ` style={ Object { "alignSelf": "center", - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Bold", "fontSize": 14, "fontWeight": "600", @@ -85,7 +85,7 @@ exports[`CustomGas should render correctly 1`] = ` @@ -38,7 +38,7 @@ exports[`TransactionReviewData should render correctly 1`] = ` style={ Object { "alignSelf": "center", - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Bold", "fontSize": 14, "fontWeight": "600", @@ -57,7 +57,7 @@ exports[`TransactionReviewData should render correctly 1`] = ` @@ -134,13 +133,11 @@ exports[`ChoosePassword should render correctly 1`] = ` style={ Array [ Object { - "color": "#8E8E93", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", - "fontSize": 12, + "fontSize": 16, "fontWeight": "400", - "height": 20, - "marginTop": 14, - "textAlign": "left", + "marginBottom": 12, }, Object { "position": "absolute", @@ -182,13 +179,12 @@ exports[`ChoosePassword should render correctly 1`] = ` @@ -196,6 +192,7 @@ exports[`ChoosePassword should render correctly 1`] = ` @@ -249,20 +244,19 @@ exports[`ChoosePassword should render correctly 1`] = ` "alignSelf": "flex-end", "position": "absolute", "right": 17, - "top": 50, + "top": 52, } } /> @@ -282,11 +276,11 @@ exports[`ChoosePassword should render correctly 1`] = ` @@ -347,7 +341,7 @@ exports[`ChoosePassword should render correctly 1`] = ` onPress={[Function]} style={ Object { - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "400", diff --git a/app/components/Views/ChoosePassword/index.js b/app/components/Views/ChoosePassword/index.js index bc412367a4b..5601e44c747 100644 --- a/app/components/Views/ChoosePassword/index.js +++ b/app/components/Views/ChoosePassword/index.js @@ -29,7 +29,7 @@ import { SEED_PHRASE_HINTS, BIOMETRY_CHOICE_DISABLED } from '../../../constants/storage'; -import { getPasswordStrengthWord, passwordRequirementsMet } from '../../../util/password'; +import { getPasswordStrengthWord, passwordRequirementsMet, MIN_PASSWORD_LENGTH } from '../../../util/password'; import { CHOOSE_PASSWORD_STEPS } from '../../../constants/onboarding'; @@ -71,13 +71,13 @@ const styles = StyleSheet.create({ alignItems: 'center' }, title: { - fontSize: 24, + fontSize: Device.isAndroid() ? 20 : 25, marginTop: 20, marginBottom: 20, color: colors.fontPrimary, justifyContent: 'center', textAlign: 'center', - ...fontStyles.normal + ...fontStyles.bold }, subtitle: { fontSize: 16, @@ -118,6 +118,7 @@ const styles = StyleSheet.create({ textDecorationColor: colors.blue }, field: { + marginVertical: 5, position: 'relative' }, input: { @@ -144,11 +145,10 @@ const styles = StyleSheet.create({ marginBottom: 30 }, biometryLabel: { - fontSize: 14, - color: colors.fontPrimary, - position: 'absolute', - top: 0, - left: 0 + flex: 1, + fontSize: 16, + color: colors.black, + ...fontStyles.normal }, biometrySwitch: { position: 'absolute', @@ -156,11 +156,16 @@ const styles = StyleSheet.create({ right: 0 }, hintLabel: { + color: colors.black, + fontSize: 16, + marginBottom: 12, + ...fontStyles.normal + }, + passwordStrengthLabel: { height: 20, - marginTop: 14, - fontSize: 12, - color: colors.grey450, - textAlign: 'left', + marginTop: 10, + fontSize: 15, + color: colors.black, ...fontStyles.normal }, showPassword: { @@ -182,7 +187,7 @@ const styles = StyleSheet.create({ }, showMatchingPasswords: { position: 'absolute', - top: 50, + top: 52, right: 17, alignSelf: 'flex-end' } @@ -571,14 +576,14 @@ class ChoosePassword extends PureComponent { autoCapitalize="none" /> {(password !== '' && ( - + {strings('choose_password.password_strength')} {' '} {strings(`choose_password.strength_${passwordStrengthWord}`)} - )) || } + )) || } {strings('choose_password.confirm_password')} @@ -600,8 +605,8 @@ class ChoosePassword extends PureComponent { ) : null} - - {strings('choose_password.must_be_at_least', { number: 8 })} + + {strings('choose_password.must_be_at_least', { number: MIN_PASSWORD_LENGTH })} {this.renderSwitch()} diff --git a/app/components/Views/EnterPasswordSimple/__snapshots__/index.test.js.snap b/app/components/Views/EnterPasswordSimple/__snapshots__/index.test.js.snap index 3284eee2aac..0ff57e33882 100644 --- a/app/components/Views/EnterPasswordSimple/__snapshots__/index.test.js.snap +++ b/app/components/Views/EnterPasswordSimple/__snapshots__/index.test.js.snap @@ -51,7 +51,7 @@ exports[`EnterPasswordSimple should render correctly 1`] = ` @@ -191,8 +202,9 @@ exports[`ImportFromSeed should render correctly 1`] = ` @@ -281,14 +299,16 @@ exports[`ImportFromSeed should render correctly 1`] = ` style={ Object { "marginVertical": 5, + "position": "relative", } } > @@ -364,6 +390,7 @@ exports[`ImportFromSeed should render correctly 1`] = ` {hideSeedPhraseInput ? ( {strings('import_from_seed.confirm_password')} - {strings('choose_password.must_be_at_least', { number: 8 })} + {strings('choose_password.must_be_at_least', { number: MIN_PASSWORD_LENGTH })} diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 4695544d0e2..518bcaa85cf 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -85,7 +85,8 @@ const styles = StyleSheet.create({ flexDirection: 'column' }, label: { - fontSize: 14, + color: colors.black, + fontSize: 16, marginBottom: 12, ...fontStyles.normal }, @@ -114,11 +115,17 @@ const styles = StyleSheet.create({ biometryLabel: { flex: 1, fontSize: 16, + color: colors.black, ...fontStyles.normal }, biometrySwitch: { flex: 0 }, + input: { + ...fontStyles.normal, + fontSize: 16, + paddingTop: 2 + }, cant: { width: 280, alignSelf: 'center', @@ -478,6 +485,7 @@ class Login extends PureComponent { {strings('login.type_delete')} @@ -417,7 +417,7 @@ exports[`Confirm should render correctly 1`] = ` style={ Object { "alignItems": "center", - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Bold", "fontSize": 16, "fontWeight": "600", diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index 4a755bd4b70..bc15e4c73e6 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -87,7 +87,7 @@ const styles = StyleSheet.create({ marginVertical: 3 }, textAmount: { - fontFamily: 'Roboto-Light', + ...fontStyles.normal, fontWeight: fontStyles.light.fontWeight, color: colors.black, fontSize: 44, diff --git a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap index 7c5e3703cb9..d6855203729 100644 --- a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap @@ -162,7 +162,7 @@ exports[`SendTo should render correctly 1`] = ` { switch (strength) { case 0: From 86771f780218d01e6f4e11f9f1dc326bc69e126c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 16 Apr 2021 12:31:57 -0400 Subject: [PATCH 28/51] fix/connection change handler (#2538) * additionalchecks * rm * setConnected * false * null --- app/components/Nav/Main/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 214f63f3b32..096f0f05d88 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -137,13 +137,17 @@ const Main = props => { const connectionChangeHandler = useCallback( state => { + if (!state) return; + const { isConnected } = state; // Show the modal once the status changes to offline - if (connected && state && !state.isConnected) { + if (connected && isConnected === false) { props.navigation.navigate('OfflineModeView'); - setConnected(state.isConnected); + } + if (connected !== isConnected && isConnected !== null) { + setConnected(isConnected); } }, - [connected, props.navigation] + [connected, setConnected, props.navigation] ); const checkInfuraAvailability = useCallback(async () => { From 02fd01e42361e468b92c90211ef6a85b7a13756a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 16 Apr 2021 12:59:42 -0400 Subject: [PATCH 29/51] bump v2.1.2 (#2540) * bump * 610 --- CHANGELOG.md | 3 +++ android/app/build.gradle | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00eb1b23fea..94e009c69f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Develop Branch +## v2.1.2 - Apr 16 2021 +- [#2538](https://github.com/MetaMask/metamask-mobile/pull/2538): fix/connection change handler + ## v2.1.1 - Apr 14 2021 - [#2520](https://github.com/MetaMask/metamask-mobile/pull/2520): Check provider status diff --git a/android/app/build.gradle b/android/app/build.gradle index e9bbe1e3344..62ed6398e59 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -166,8 +166,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 51 - versionName "2.1.1" + versionCode 52 + versionName "2.1.2" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 362d95953a6..e6f7e6facd9 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 609; + CURRENT_PROJECT_VERSION = 610; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -882,7 +882,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.1.2; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 609; + CURRENT_PROJECT_VERSION = 610; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( @@ -945,7 +945,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.1.1; + MARKETING_VERSION = 2.1.2; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 92acaf32395..1dadf1f4dd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "2.1.1", + "version": "2.1.2", "private": true, "scripts": { "watch": "./scripts/build.sh watcher watch", From 194a1858b96b1f88762f8679380b09dda3c8b29e Mon Sep 17 00:00:00 2001 From: sethkfman <10342624+sethkfman@users.noreply.github.com> Date: Fri, 16 Apr 2021 12:11:49 -0600 Subject: [PATCH 30/51] Feature/tx local state logs (#2460) * added reducer to capture wallet import time and added list element to display import time * added UI level features for import wallet display in transaciton * update UI and import location * added logic to track local tx and refactored added wallet view * added code comments * updated package.json * added test controllers module * updated add transaction for deeplink * updated package and yarn * updated unit tests * updated importTime to work with controller updates * updated yarn.lock * updated unit tests * updated snapshots * snapshot update * updated contoller related code for latest change * update package.json with latest controller version * updated yarn * update package.json with develop * updated yarn file to develops * PR feedback * snapshot updated * return null if element cannot be rendered * refactored account added time * added tmethods to determine the time when a transaction should be flagged to dispaly the added account time --- app/components/UI/Swaps/QuotesView.js | 7 +- app/components/UI/TransactionElement/index.js | 149 ++++++++++++++---- .../UI/TransactionElement/index.test.js | 4 + app/components/UI/Transactions/index.js | 13 +- app/components/UI/Transactions/index.test.js | 4 + app/components/Views/Asset/index.js | 19 +++ app/components/Views/Send/index.js | 6 +- .../Views/SendFlow/Confirm/index.js | 5 +- .../Views/TransactionsView/index.js | 27 +++- app/core/DeeplinkManager.js | 3 +- app/core/Engine.js | 6 +- app/core/WalletConnect.js | 4 +- app/util/transactions.js | 11 ++ locales/languages/en.json | 6 +- yarn.lock | 2 +- 15 files changed, 214 insertions(+), 52 deletions(-) diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index 124c82db1b8..e50bc23b3c7 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -8,6 +8,7 @@ import FAIcon from 'react-native-vector-icons/FontAwesome'; import BigNumber from 'bignumber.js'; import { NavigationContext } from 'react-navigation'; import { swapsUtils, util } from '@estebanmino/controllers'; +import { WalletDevice } from '@metamask/controllers/'; import { BNToHex, @@ -552,7 +553,8 @@ function SwapsQuotesView({ try { const { transactionMeta } = await TransactionController.addTransaction( approvalTransaction, - process.env.MM_FOX_CODE + process.env.MM_FOX_CODE, + WalletDevice.MM_MOBILE ); approvalTransactionMetaId = transactionMeta.id; newSwapsTransactions[transactionMeta.id] = { @@ -571,7 +573,8 @@ function SwapsQuotesView({ try { const { transactionMeta } = await TransactionController.addTransaction( selectedQuote.trade, - process.env.MM_FOX_CODE + process.env.MM_FOX_CODE, + WalletDevice.MM_MOBILE ); updateSwapsTransactions(transactionMeta, approvalTransactionMetaId, newSwapsTransactions); } catch (e) { diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index 79e61ac7e16..c08e11a7506 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { TouchableHighlight, StyleSheet, Image } from 'react-native'; -import { colors } from '../../../styles/common'; +import { TouchableOpacity, TouchableHighlight, StyleSheet, Image, Text, View } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import FAIcon from 'react-native-vector-icons/FontAwesome'; import { strings } from '../../../../locales/i18n'; import { toDateFormat } from '../../../util/date'; import TransactionDetails from './TransactionDetails'; @@ -14,6 +15,7 @@ import { TRANSACTION_TYPES } from '../../../util/transactions'; import ListItem from '../../Base/ListItem'; import StatusText from '../../Base/StatusText'; import DetailsModal from '../../Base/DetailsModal'; +import { WalletDevice } from '@metamask/controllers/'; const styles = StyleSheet.create({ row: { @@ -38,6 +40,26 @@ const styles = StyleSheet.create({ icon: { width: 28, height: 28 + }, + summaryWrapper: { + padding: 15 + }, + fromDeviceText: { + color: colors.fontSecondary, + fontSize: 14, + marginBottom: 10, + ...fontStyles.normal + }, + importText: { + color: colors.fontSecondary, + fontSize: 14, + ...fontStyles.bold, + alignContent: 'center' + }, + importRowBody: { + alignItems: 'center', + backgroundColor: colors.grey000, + paddingTop: 10 } }); @@ -71,6 +93,10 @@ class TransactionElement extends PureComponent { * String of selected address */ selectedAddress: PropTypes.string, + /** + /* Identities object required to get import time name + */ + identities: PropTypes.object, /** * Current element of the list index */ @@ -96,6 +122,7 @@ class TransactionElement extends PureComponent { cancelIsOpen: false, speedUpIsOpen: false, detailsModalVisible: false, + importModalVisible: false, transactionGas: { gasBN: undefined, gasPriceBN: undefined, gasTotal: undefined }, transactionElement: undefined, transactionDetails: undefined @@ -124,6 +151,14 @@ class TransactionElement extends PureComponent { this.setState({ detailsModalVisible: true }); }; + onPressImportWalletTip = () => { + this.setState({ importModalVisible: true }); + }; + + onCloseImportWalletModal = () => { + this.setState({ importModalVisible: false }); + }; + onCloseDetailsModal = () => { this.setState({ detailsModalVisible: false }); }; @@ -133,8 +168,36 @@ class TransactionElement extends PureComponent { const incoming = safeToChecksumAddress(tx.transaction.to) === selectedAddress; const selfSent = incoming && safeToChecksumAddress(tx.transaction.from) === selectedAddress; return `${ - (!incoming || selfSent) && tx.transaction.nonce ? `#${parseInt(tx.transaction.nonce, 16)} - ` : '' - }${toDateFormat(tx.time)}`; + (!incoming || selfSent) && tx.deviceConfirmedOn === WalletDevice.MM_MOBILE + ? `#${parseInt(tx.transaction.nonce, 16)} - ${toDateFormat(tx.time)} ${strings( + 'transactions.from_device_label' + // eslint-disable-next-line no-mixed-spaces-and-tabs + )}` + : `${toDateFormat(tx.time)} + ` + }`; + }; + + /** + * Function that evaluates tx to see if the Added Wallet label should be rendered. + * @returns Account added to wallet view + */ + renderImportTime = () => { + const { tx, identities, selectedAddress } = this.props; + if (tx.insertImportTime && identities[selectedAddress].importTime) { + return ( + <> + + + {`${strings('transactions.import_wallet_row')} `} + + + {toDateFormat(identities[selectedAddress].importTime)} + + + ); + } + return null; }; renderTxElementIcon = (transactionElement, status) => { @@ -169,33 +232,40 @@ class TransactionElement extends PureComponent { */ renderTxElement = transactionElement => { const { - tx: { status } + identities, + selectedAddress, + tx: { time, status } } = this.props; const { value, fiatValue = false, actionKey } = transactionElement; const renderTxActions = status === 'submitted' || status === 'approved'; + const accountImportTime = identities[selectedAddress].importTime; return ( - - {this.renderTxTime()} - - {this.renderTxElementIcon(transactionElement, status)} - - {actionKey} - - - {Boolean(value) && ( - - {value} - {fiatValue} - + <> + {accountImportTime > time && this.renderImportTime()} + + {this.renderTxTime()} + + {this.renderTxElementIcon(transactionElement, status)} + + {actionKey} + + + {Boolean(value) && ( + + {value} + {fiatValue} + + )} + + {!!renderTxActions && ( + + {this.renderSpeedUpButton()} + {this.renderCancelButton()} + )} - - {!!renderTxActions && ( - - {this.renderSpeedUpButton()} - {this.renderCancelButton()} - - )} - + + {accountImportTime <= time && this.renderImportTime()} + ); }; @@ -241,7 +311,7 @@ class TransactionElement extends PureComponent { render() { const { tx } = this.props; - const { detailsModalVisible, transactionElement, transactionDetails } = this.state; + const { detailsModalVisible, importModalVisible, transactionElement, transactionDetails } = this.state; if (!transactionElement || !transactionDetails) return null; return ( @@ -276,15 +346,36 @@ class TransactionElement extends PureComponent { /> + + + + + {strings('transactions.import_wallet_label')} + + + + + {strings('transactions.import_wallet_tip')} + + + ); } } const mapStateToProps = state => ({ - ticker: state.engine.backgroundState.NetworkController.provider.ticker, + identities: state.engine.backgroundState.PreferencesController.identities, primaryCurrency: state.settings.primaryCurrency, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, swapsTransactions: state.engine.backgroundState.TransactionController.swapsTransactions || {}, - swapsTokens: state.engine.backgroundState.SwapsController.tokens + swapsTokens: state.engine.backgroundState.SwapsController.tokens, + ticker: state.engine.backgroundState.NetworkController.provider.ticker }); export default connect(mapStateToProps)(TransactionElement); diff --git a/app/components/UI/TransactionElement/index.test.js b/app/components/UI/TransactionElement/index.test.js index 6f84fc1146a..a14320b5952 100644 --- a/app/components/UI/TransactionElement/index.test.js +++ b/app/components/UI/TransactionElement/index.test.js @@ -10,6 +10,10 @@ describe('TransactionElement', () => { const initialState = { engine: { backgroundState: { + PreferencesController: { + selectedAddress: '0x0', + identities: { '0xbar': { name: 'Account 1', address: '0x0', importTime: Date.now() } } + }, CurrencyRateController: { currentCurrency: 'usd', conversionRate: 0.1 diff --git a/app/components/UI/Transactions/index.js b/app/components/UI/Transactions/index.js index ab01fbb17f7..4597f431f0f 100644 --- a/app/components/UI/Transactions/index.js +++ b/app/components/UI/Transactions/index.js @@ -306,13 +306,13 @@ class Transactions extends PureComponent { if (!this.props.transactions.length) { return this.renderEmpty(); } - const { submittedTransactions, confirmedTransactions, header } = this.props; const { cancelConfirmDisabled, speedUpConfirmDisabled } = this.state; const transactions = submittedTransactions && submittedTransactions.length ? submittedTransactions.concat(confirmedTransactions) : this.props.transactions; + return ( ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, - tokens: state.engine.backgroundState.AssetsController.tokens.reduce((tokens, token) => { - tokens[token.address] = token; - return tokens; - }, {}), collectibleContracts: state.engine.backgroundState.AssetsController.collectibleContracts, contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, - thirdPartyApiMode: state.privacy.thirdPartyApiMode + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + thirdPartyApiMode: state.privacy.thirdPartyApiMode, + tokens: state.engine.backgroundState.AssetsController.tokens.reduce((tokens, token) => { + tokens[token.address] = token; + return tokens; + }, {}) }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/UI/Transactions/index.test.js b/app/components/UI/Transactions/index.test.js index e123a0b5996..cd5c80ff4fd 100644 --- a/app/components/UI/Transactions/index.test.js +++ b/app/components/UI/Transactions/index.test.js @@ -12,6 +12,10 @@ describe('Transactions', () => { const initialState = { engine: { backgroundState: { + PreferencesController: { + selectedAddress: '0x0', + identities: { '0xbar': { name: 'Account 1', address: '0x0', importTime: Date.now() } } + }, AccountTrackerController: { accounts: {} }, diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js index 0ed4e3cd97d..ecedc25af9c 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -9,6 +9,7 @@ import { getNetworkNavbarOptions } from '../../UI/Navbar'; import Engine from '../../../core/Engine'; import { safeToChecksumAddress } from '../../../util/address'; import { SWAPS_CONTRACT_ADDRESS } from '@estebanmino/controllers/dist/swaps/SwapsUtil'; +import { addAccountTimeFlagFilter } from '../../../util/transactions'; const styles = StyleSheet.create({ wrapper: { @@ -46,6 +47,10 @@ class Asset extends PureComponent { /* Selected currency */ currentCurrency: PropTypes.string, + /** + /* Identities object required to get account name + */ + identities: PropTypes.object, /** * A string that represents the selected address */ @@ -174,6 +179,8 @@ class Asset extends PureComponent { normalizeTransactions() { if (this.isNormalizing) return; + let accountAddedTimeInsertPointFound = false; + const addedAccountTime = this.props.identities[this.props.selectedAddress]?.importTime; this.isNormalizing = true; let submittedTxs = []; const newPendingTxs = []; @@ -183,6 +190,12 @@ class Asset extends PureComponent { const txs = transactions.filter(tx => { const filerResult = this.filter(tx); if (filerResult) { + tx.insertImportTime = addAccountTimeFlagFilter( + tx, + addedAccountTime, + accountAddedTimeInsertPointFound + ); + if (tx.insertImportTime) accountAddedTimeInsertPointFound = true; switch (tx.status) { case 'submitted': case 'signed': @@ -211,6 +224,11 @@ class Asset extends PureComponent { return !alreadySubmitted; }); + //if the account added insertpoint is not found add it to the last transaction + if (!accountAddedTimeInsertPointFound && txs && txs.length) { + txs[txs.length - 1].insertImportTime = true; + } + // To avoid extra re-renders we want to set the new txs only when // there's a new tx in the history or the status of one of the existing txs changed if ( @@ -295,6 +313,7 @@ const mapStateToProps = state => ({ conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities, chainId: state.engine.backgroundState.NetworkController.provider.chainId, tokens: state.engine.backgroundState.AssetsController.tokens, transactions: state.engine.backgroundState.TransactionController.transactions, diff --git a/app/components/Views/Send/index.js b/app/components/Views/Send/index.js index 5e1daf16aff..f29b80de929 100644 --- a/app/components/Views/Send/index.js +++ b/app/components/Views/Send/index.js @@ -28,6 +28,7 @@ import { isENS } from '../../../util/address'; import TransactionTypes from '../../../core/TransactionTypes'; import { MAINNET } from '../../../constants/network'; import BigNumber from 'bignumber.js'; +import { WalletDevice } from '@metamask/controllers/'; const REVIEW = 'review'; const EDIT = 'edit'; @@ -317,7 +318,6 @@ class Send extends PureComponent { if (gasPrice) { newTxMeta.gasPrice = toBN(gas); } - // TODO: We should add here support for sending tokens // or calling smart contract functions } @@ -333,7 +333,6 @@ class Send extends PureComponent { newTxMeta.from = selectedAddress; newTxMeta.transactionFromName = identities[selectedAddress].name; - this.props.setTransactionObject(newTxMeta); this.mounted && this.setState({ ready: true, transactionKey: Date.now() }); }; @@ -487,7 +486,8 @@ class Send extends PureComponent { } const { result, transactionMeta } = await TransactionController.addTransaction( transaction, - TransactionTypes.MMM + TransactionTypes.MMM, + WalletDevice.MM_MOBILE ); await TransactionController.approveTransaction(transactionMeta.id); diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index bc15e4c73e6..9cf24822b60 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -28,7 +28,7 @@ import { } from '../../../../util/number'; import { getTicker, decodeTransferData, getNormalizedTxState } from '../../../../util/transactions'; import StyledButton from '../../../UI/StyledButton'; -import { util } from '@metamask/controllers'; +import { util, WalletDevice } from '@metamask/controllers'; import { prepareTransaction, resetTransaction, setNonce, setProposedNonce } from '../../../../actions/transaction'; import { apiEstimateModifiedToWEI, @@ -707,7 +707,8 @@ class Confirm extends PureComponent { } const { result, transactionMeta } = await TransactionController.addTransaction( transaction, - TransactionTypes.MMM + TransactionTypes.MMM, + WalletDevice.MM_MOBILE ); await TransactionController.approveTransaction(transactionMeta.id); await new Promise(resolve => resolve(result)); diff --git a/app/components/Views/TransactionsView/index.js b/app/components/Views/TransactionsView/index.js index 2505791f026..32704877d8c 100644 --- a/app/components/Views/TransactionsView/index.js +++ b/app/components/Views/TransactionsView/index.js @@ -7,6 +7,7 @@ import Engine from '../../../core/Engine'; import { showAlert } from '../../../actions/alert'; import Transactions from '../../UI/Transactions'; import { safeToChecksumAddress } from '../../../util/address'; +import { addAccountTimeFlagFilter } from '../../../util/transactions'; const styles = StyleSheet.create({ wrapper: { @@ -18,6 +19,7 @@ const TransactionsView = ({ navigation, conversionRate, selectedAddress, + identities, networkType, currentCurrency, transactions, @@ -32,6 +34,10 @@ const TransactionsView = ({ const filterTransactions = useCallback(() => { const network = Engine.context.NetworkController.state.network; if (network === 'loading') return; + + let accountAddedTimeInsertPointFound = false; + const addedAccountTime = identities[selectedAddress]?.importTime; + const ethFilter = tx => { const { transaction: { from, to }, @@ -61,6 +67,10 @@ const TransactionsView = ({ const allTransactions = allTransactionsSorted.filter(tx => { const filter = ethFilter(tx); if (!filter) return false; + + tx.insertImportTime = addAccountTimeFlagFilter(tx, addedAccountTime, accountAddedTimeInsertPointFound); + if (tx.insertImportTime) accountAddedTimeInsertPointFound = true; + switch (tx.status) { case 'submitted': case 'signed': @@ -84,15 +94,19 @@ const TransactionsView = ({ return !alreadySubmitted; }); + //if the account added insertpoint is not found add it to the last transaction + if (!accountAddedTimeInsertPointFound && allTransactions && allTransactions.length) { + allTransactions[allTransactions.length - 1].insertImportTime = true; + } + setAllTransactions(allTransactions); setSubmittedTxs(submittedTxsFiltered); setConfirmedTxs(confirmedTxs); setLoading(false); - }, [transactions, selectedAddress, tokens, chainId]); + }, [transactions, identities, selectedAddress, tokens, chainId]); useEffect(() => { setLoading(true); - /* Since this screen is always mounted and computations happen on this screen everytime the user changes network using the InteractionManager will help by giving enough time for any animations/screen transactions before it starts @@ -131,8 +145,12 @@ TransactionsView.propTypes = { */ currentCurrency: PropTypes.string, /** - /* navigation object required to push new views - */ + /* Identities object required to get account name + */ + identities: PropTypes.object, + /** + /* navigation object required to push new views + */ navigation: PropTypes.object, /** * A string that represents the selected address @@ -161,6 +179,7 @@ const mapStateToProps = state => ({ currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, tokens: state.engine.backgroundState.AssetsController.tokens, + identities: state.engine.backgroundState.PreferencesController.identities, transactions: state.engine.backgroundState.TransactionController.transactions, networkType: state.engine.backgroundState.NetworkController.provider.type, chainId: state.engine.backgroundState.NetworkController.provider.chainId diff --git a/app/core/DeeplinkManager.js b/app/core/DeeplinkManager.js index 34ecf3b1e82..cb025a67fa3 100644 --- a/app/core/DeeplinkManager.js +++ b/app/core/DeeplinkManager.js @@ -10,6 +10,7 @@ import Engine from './Engine'; import { generateApproveData } from '../util/transactions'; import { strings } from '../../locales/i18n'; import { getNetworkTypeById } from '../util/networks'; +import { WalletDevice } from '@metamask/controllers/'; class DeeplinkManager { constructor(_navigation) { @@ -68,7 +69,7 @@ class DeeplinkManager { if (!Number.isInteger(uint256Number)) throw new Error('The parameter uint256 should be an integer'); const value = uint256Number.toString(16); txParams.data = generateApproveData({ spender: address, value }); - TransactionController.addTransaction(txParams, origin); + TransactionController.addTransaction(txParams, origin, WalletDevice.MM_MOBILE); } } diff --git a/app/core/Engine.js b/app/core/Engine.js index 8a76319905f..c5535ff8f29 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -15,7 +15,8 @@ import { TokenBalancesController, TokenRatesController, TransactionController, - TypedMessageManager + TypedMessageManager, + WalletDevice } from '@metamask/controllers'; import { SwapsController } from '@estebanmino/controllers'; @@ -87,7 +88,8 @@ class Engine { try { const hash = await (await TransactionController.addTransaction( payload.params[0], - payload.origin + payload.origin, + WalletDevice.MM_MOBILE )).result; end(undefined, hash); } catch (error) { diff --git a/app/core/WalletConnect.js b/app/core/WalletConnect.js index 3751a9f2757..802bcbfee8a 100644 --- a/app/core/WalletConnect.js +++ b/app/core/WalletConnect.js @@ -7,6 +7,7 @@ import { EventEmitter } from 'events'; import AsyncStorage from '@react-native-community/async-storage'; import { CLIENT_OPTIONS, WALLET_CONNECT_ORIGIN } from '../util/walletconnect'; import { WALLETCONNECT_SESSIONS } from '../constants/storage'; +import { WalletDevice } from '@metamask/controllers/'; const hub = new EventEmitter(); let connectors = []; @@ -118,7 +119,8 @@ class WalletConnect { txParams.data = payload.params[0].data; const hash = await (await TransactionController.addTransaction( txParams, - meta ? WALLET_CONNECT_ORIGIN + meta.url : undefined + meta ? WALLET_CONNECT_ORIGIN + meta.url : undefined, + WalletDevice.MM_MOBILE )).result; this.walletConnector.approveRequest({ id: payload.id, diff --git a/app/util/transactions.js b/app/util/transactions.js index 30d5243cc45..8058191ef0f 100644 --- a/app/util/transactions.js +++ b/app/util/transactions.js @@ -408,6 +408,17 @@ export function validateTransactionActionBalance(transaction, rate, accounts) { } } +/** + * Return a boolen if the transaction should be flagged to add the account added label + * + * @param {object} transaction - Transaction object get time + * @param {object} addedAccountTime - Time the account was added to the wallet + * @param {object} accountAddedTimeInsertPointFound - Flag to see if the import time was already found + */ +export function addAccountTimeFlagFilter(transaction, addedAccountTime, accountAddedTimeInsertPointFound) { + return transaction.time <= addedAccountTime && !accountAddedTimeInsertPointFound; +} + export function getNormalizedTxState(state) { return { ...state.transaction, ...state.transaction.transaction }; } diff --git a/locales/languages/en.json b/locales/languages/en.json index 5f61e4edb68..45bb14c9d99 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -822,7 +822,11 @@ "address_from_balance": "Balance:", "status": "Status", "date": "Date", - "nonce": "Nonce" + "nonce": "Nonce", + "from_device_label": "from this device", + "import_wallet_row": "Account added to this device", + "import_wallet_label": "Account Added", + "import_wallet_tip": "All future transactions made from this device will include a label \"from this device\" next to the timestamp. For transactions dated before adding the account, this history will not indicate which outgoing transactions originated from this device." }, "address_book": { "recents": "Recents", diff --git a/yarn.lock b/yarn.lock index 5c95b5c6ca9..4ff283b7705 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13911,4 +13911,4 @@ yargs@^3.30.0: zxcvbn@4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" - integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA= + integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA= \ No newline at end of file From 711c25f131d54336ba85e8be941cfdd2fede96b2 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 16 Apr 2021 19:54:38 +0100 Subject: [PATCH 31/51] v2.2.0 (#2542) * v2.2.0 * Update CHANGELOG.md --- CHANGELOG.md | 20 ++++++++++++++++++++ android/app/build.gradle | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e009c69f5..148cdfc5984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ # Changelog ## Current Develop Branch +- [#2460](https://github.com/MetaMask/metamask-mobile/pull/2460): Feature/tx local state logs +- [#2540](https://github.com/MetaMask/metamask-mobile/pull/2540): bump v2.1.2 +- [#2538](https://github.com/MetaMask/metamask-mobile/pull/2538): fix/connection change handler +- [#2375](https://github.com/MetaMask/metamask-mobile/pull/2375): Style updates +- [#2536](https://github.com/MetaMask/metamask-mobile/pull/2536): Change Send Feedback to Request a Feature and update link +- [#2485](https://github.com/MetaMask/metamask-mobile/pull/2485): Fix notification so it doesn't block terms + conditions +- [#2469](https://github.com/MetaMask/metamask-mobile/pull/2469): Bug/persists old account names +- [#2534](https://github.com/MetaMask/metamask-mobile/pull/2534): Fix typo +- [#2373](https://github.com/MetaMask/metamask-mobile/pull/2373): use contract metadata version from package +- [#2520](https://github.com/MetaMask/metamask-mobile/pull/2520): Check infura availability +- [#2371](https://github.com/MetaMask/metamask-mobile/pull/2371): Feature/custom nonce +- [#2521](https://github.com/MetaMask/metamask-mobile/pull/2521): Bump v2.1.1 +- [#2493](https://github.com/MetaMask/metamask-mobile/pull/2493): rename master to main +- [#2447](https://github.com/MetaMask/metamask-mobile/pull/2447): Bump vm-browserify from 0.0.4 to 1.1.2 +- [#2501](https://github.com/MetaMask/metamask-mobile/pull/2501): Bump jest-serializer from 24.4.0 to 26.6.2 +- [#2499](https://github.com/MetaMask/metamask-mobile/pull/2499): Bump react-native-share from 3.3.2 to 5.2.2 +- [#2411](https://github.com/MetaMask/metamask-mobile/pull/2411): Bump json-rpc-middleware-stream from 2.1.1 to 3.0.0 +- [#2406](https://github.com/MetaMask/metamask-mobile/pull/2406): Bump eslint-plugin-prettier from 3.3.0 to 3.3.1 +- [#2403](https://github.com/MetaMask/metamask-mobile/pull/2403): Bump babel-eslint from 10.0.3 to 10.1.0 +- [#2381](https://github.com/MetaMask/metamask-mobile/pull/2381): Display correct number of decimals for 'usd' fiat ## v2.1.2 - Apr 16 2021 - [#2538](https://github.com/MetaMask/metamask-mobile/pull/2538): fix/connection change handler diff --git a/android/app/build.gradle b/android/app/build.gradle index 62ed6398e59..fba1856c5f4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -166,8 +166,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 52 - versionName "2.1.2" + versionCode 53 + versionName "2.2.0" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index e6f7e6facd9..4b4f7921a83 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 610; + CURRENT_PROJECT_VERSION = 611; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -882,7 +882,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 610; + CURRENT_PROJECT_VERSION = 611; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( @@ -945,7 +945,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.2.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 1dadf1f4dd8..61a48e635d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "2.1.2", + "version": "2.2.0", "private": true, "scripts": { "watch": "./scripts/build.sh watcher watch", From 09d72b69219764ebc9bb3bdf65aa2ccea9b88240 Mon Sep 17 00:00:00 2001 From: ricky Date: Mon, 19 Apr 2021 10:11:54 -0400 Subject: [PATCH 32/51] Only get nonce from the network if the feature is enabled (#2543) --- .../TransactionReview/TransactionReviewInformation/index.js | 3 ++- app/components/Views/SendFlow/Confirm/index.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js index e4b19b49e5a..500e0393fe3 100644 --- a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js @@ -209,7 +209,8 @@ class TransactionReviewInformation extends PureComponent { }; componentDidMount = async () => { - await this.setNetworkNonce(); + const { showCustomNonce } = this.props; + showCustomNonce && (await this.setNetworkNonce()); }; setNetworkNonce = async () => { diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index 9cf24822b60..d1091e5d948 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -368,9 +368,9 @@ class Confirm extends PureComponent { // For analytics AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SEND_TRANSACTION_STARTED, this.getAnalyticsParams()); - const { navigation, providerType } = this.props; + const { showCustomNonce, navigation, providerType } = this.props; await this.handleFetchBasicEstimates(); - await this.setNetworkNonce(); + showCustomNonce && (await this.setNetworkNonce()); navigation.setParams({ providerType }); this.parseTransactionData(); this.prepareTransaction(); From 658e3b1e204c3078e30a9b3508afcafc807afda8 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Mon, 19 Apr 2021 16:43:01 +0100 Subject: [PATCH 33/51] Fix analytics try catch (#2546) * Fix analytics try catch * Move analytics on watch asset to line below * Update changelog and bump version * Fix version on changelog * Mock InteractionManager --- CHANGELOG.md | 4 +++- app/components/UI/WatchAssetRequest/index.js | 2 +- app/util/analyticsV2.js | 14 +++++++------- app/util/testSetup.js | 8 ++++++++ ios/MetaMask.xcodeproj/project.pbxproj | 4 ++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 148cdfc5984..d64ca4bd958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## Current Develop Branch +## v2.2.0 - Apr 21 2021 +- [#2546](https://github.com/MetaMask/metamask-mobile/pull/2546): Fix analytics try catch +- [#2543](https://github.com/MetaMask/metamask-mobile/pull/2543): Only get nonce from the network if the feature is enabled - [#2460](https://github.com/MetaMask/metamask-mobile/pull/2460): Feature/tx local state logs - [#2540](https://github.com/MetaMask/metamask-mobile/pull/2540): bump v2.1.2 - [#2538](https://github.com/MetaMask/metamask-mobile/pull/2538): fix/connection change handler diff --git a/app/components/UI/WatchAssetRequest/index.js b/app/components/UI/WatchAssetRequest/index.js index 91f56f59569..2651f41c88a 100644 --- a/app/components/UI/WatchAssetRequest/index.js +++ b/app/components/UI/WatchAssetRequest/index.js @@ -139,8 +139,8 @@ class WatchAssetRequest extends PureComponent { onConfirm = async () => { const { onConfirm, suggestedAssetMeta } = this.props; const { AssetsController } = Engine.context; - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams()); await AssetsController.acceptWatchAsset(suggestedAssetMeta.id); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams()); onConfirm && onConfirm(); }; diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js index 298b9862b0e..c1083b179fe 100644 --- a/app/util/analyticsV2.js +++ b/app/util/analyticsV2.js @@ -45,8 +45,8 @@ export const ANALYTICS_EVENTS_V2 = { * @param {Object} params */ export const trackEventV2 = (eventName, params) => { - try { - InteractionManager.runAfterInteractions(() => { + InteractionManager.runAfterInteractions(() => { + try { if (!params) { Analytics.trackEvent(eventName); } @@ -57,7 +57,7 @@ export const trackEventV2 = (eventName, params) => { for (const key in params) { const property = params[key]; - if (typeof property === 'object') { + if (property && typeof property === 'object') { if (property.anonymous) { // Anonymous property - add only to anonymous params anonymousParams[key] = property.value; @@ -82,10 +82,10 @@ export const trackEventV2 = (eventName, params) => { if (Object.keys(anonymousParams).length) { Analytics.trackEventWithParameters(eventName, anonymousParams, true); } - }); - } catch (error) { - Logger.error(error, 'Error logging analytics'); - } + } catch (error) { + Logger.error(error, 'Error logging analytics'); + } + }); }; export default { diff --git a/app/util/testSetup.js b/app/util/testSetup.js index 7f2d67781f7..f49f82f2be4 100644 --- a/app/util/testSetup.js +++ b/app/util/testSetup.js @@ -1,6 +1,7 @@ import Adapter from 'enzyme-adapter-react-16'; import Enzyme from 'enzyme'; import Engine from '../core/Engine'; + import NotificationManager from '../core/NotificationManager'; import { NativeModules } from 'react-native'; import mockAsyncStorage from '../../node_modules/@react-native-community/async-storage/jest/async-storage-mock'; @@ -125,3 +126,10 @@ jest.mock('react-native/Libraries/Components/Touchable/TouchableHighlight', () = jest.mock('react-native/Libraries/Components/TextInput/TextInput', () => 'TextInput'); jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); + +jest.mock('react-native/Libraries/Interaction/InteractionManager', () => ({ + runAfterInteractions: jest.fn(), + createInteractionHandle: jest.fn(), + clearInteractionHandle: jest.fn(), + setDeadline: jest.fn() +})); diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 4b4f7921a83..44bc53f912d 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 611; + CURRENT_PROJECT_VERSION = 612; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 611; + CURRENT_PROJECT_VERSION = 612; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( From 7b5a0352d9992999651018389f218aa972397b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Mon, 19 Apr 2021 16:46:11 -0400 Subject: [PATCH 34/51] bundle update (#2549) --- ios/Gemfile.lock | 80 +++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 00472fb18d5..264dfc268e1 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -4,22 +4,23 @@ GEM CFPropertyList (3.0.3) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) + artifactory (3.0.15) atomos (0.1.3) - aws-eventstream (1.1.0) - aws-partitions (1.414.0) - aws-sdk-core (3.110.0) + aws-eventstream (1.1.1) + aws-partitions (1.446.0) + aws-sdk-core (3.114.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.40.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (1.43.0) + aws-sdk-core (~> 3, >= 3.112.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.87.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-s3 (1.93.1) + aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.2) + aws-sigv4 (1.2.3) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.0.3) @@ -28,28 +29,32 @@ GEM commander-fastlane (4.4.6) highline (~> 1.7.2) declarative (0.0.20) - declarative-option (0.1.0) digest-crc (0.6.3) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.2.1) - excon (0.78.1) - faraday (1.3.0) + emoji_regex (3.2.2) + excon (0.80.1) + faraday (1.4.1) + faraday-excon (~> 1.1) faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) multipart-post (>= 1.2, < 3) - ruby2_keywords + ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) http-cookie (~> 1.0.0) - faraday-net_http (1.0.0) + faraday-excon (1.1.0) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.1.0) faraday_middleware (1.0.0) faraday (~> 1.0) - fastimage (2.2.1) - fastlane (2.171.0) + fastimage (2.2.3) + fastlane (2.180.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) + artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) @@ -70,6 +75,7 @@ GEM jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) + naturally (~> 2.2) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -92,20 +98,35 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.12) - google-cloud-core (1.5.0) + google-apis-core (0.3.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.14) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + rexml + signet (~> 0.14) + webrick + google-apis-iamcredentials_v1 (0.3.0) + google-apis-core (~> 0.1) + google-apis-storage_v1 (0.3.0) + google-apis-core (~> 0.1) + google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.4.0) + google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.0.1) - google-cloud-storage (1.29.2) + google-cloud-errors (1.1.0) + google-cloud-storage (1.31.0) addressable (~> 2.5) digest-crc (~> 0.4) - google-api-client (~> 0.33) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.14.0) + googleauth (0.16.1) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -121,25 +142,26 @@ GEM jwt (2.2.2) memoist (0.16.2) mini_magick (4.11.0) - mini_mime (1.0.2) + mini_mime (1.1.0) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) - naturally (2.2.0) + naturally (2.2.1) os (1.1.1) plist (3.6.0) public_suffix (4.0.6) rake (13.0.3) - representable (3.0.4) + representable (3.1.1) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) + rexml (3.2.5) rouge (2.0.7) - ruby2_keywords (0.0.2) + ruby2_keywords (0.0.4) rubyzip (2.3.0) security (0.1.3) - signet (0.14.0) + signet (0.15.0) addressable (~> 2.3) faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) @@ -151,6 +173,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.1) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -160,6 +183,7 @@ GEM unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) + webrick (1.7.0) word_wrap (1.0.0) xcodeproj (1.19.0) CFPropertyList (>= 2.3.3, < 4.0) From 0c294f2e4bf623f965ec454929a9db49c44267cc Mon Sep 17 00:00:00 2001 From: sethkfman <10342624+sethkfman@users.noreply.github.com> Date: Tue, 20 Apr 2021 14:22:42 -0600 Subject: [PATCH 35/51] Bug fix/sync import time (#2554) * added sort order on transactions before filtering * added import date to preferences coming from sync data of the extension * correct variable spelling --- app/components/Views/Asset/index.js | 12 ++++-------- app/core/Engine.js | 1 + 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js index ecedc25af9c..f88c527139c 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -187,9 +187,10 @@ class Asset extends PureComponent { const confirmedTxs = []; const { chainId, transactions } = this.props; if (transactions.length) { + transactions.sort((a, b) => (a.time > b.time ? -1 : b.time > a.time ? 1 : 0)); const txs = transactions.filter(tx => { - const filerResult = this.filter(tx); - if (filerResult) { + const filterResult = this.filter(tx); + if (filterResult) { tx.insertImportTime = addAccountTimeFlagFilter( tx, addedAccountTime, @@ -210,13 +211,9 @@ class Asset extends PureComponent { break; } } - return filerResult; + return filterResult; }); - txs.sort((a, b) => (a.time > b.time ? -1 : b.time > a.time ? 1 : 0)); - submittedTxs.sort((a, b) => (a.time > b.time ? -1 : b.time > a.time ? 1 : 0)); - confirmedTxs.sort((a, b) => (a.time > b.time ? -1 : b.time > a.time ? 1 : 0)); - const submittedNonces = []; submittedTxs = submittedTxs.filter(transaction => { const alreadySubmitted = submittedNonces.includes(transaction.transaction.nonce); @@ -228,7 +225,6 @@ class Asset extends PureComponent { if (!accountAddedTimeInsertPointFound && txs && txs.length) { txs[txs.length - 1].insertImportTime = true; } - // To avoid extra re-renders we want to set the new txs only when // there's a new tx in the history or the status of one of the existing txs changed if ( diff --git a/app/core/Engine.js b/app/core/Engine.js index c5535ff8f29..48746df72af 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -383,6 +383,7 @@ class Engine { const checksummedAddress = toChecksumAddress(address); if (accounts.hd.includes(checksummedAddress) || accounts.simpleKeyPair.includes(checksummedAddress)) { updatedPref.identities[checksummedAddress] = preferences.identities[address]; + updatedPref.identities[checksummedAddress].importTime = Date.now(); } }); await PreferencesController.update(updatedPref); From d519bf761a38e349ccc7879097088891a322339e Mon Sep 17 00:00:00 2001 From: ricky Date: Tue, 20 Apr 2021 16:33:24 -0400 Subject: [PATCH 36/51] Include decimalsToShow in balanceToFiatNumber (#2547) --- app/core/Engine.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/core/Engine.js b/app/core/Engine.js index 48746df72af..70508bbf8af 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -242,8 +242,8 @@ class Engine { const { tokens } = AssetsController.state; let ethFiat = 0; let tokenFiat = 0; + const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; if (accounts[selectedAddress]) { - const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; ethFiat = weiToFiatNumber(accounts[selectedAddress].balance, conversionRate, decimalsToShow); } if (tokens.length > 0) { @@ -256,7 +256,12 @@ class Engine { (item.address in tokenBalances ? renderFromTokenMinimalUnit(tokenBalances[item.address], item.decimals) : undefined); - const tokenBalanceFiat = balanceToFiatNumber(tokenBalance, conversionRate, exchangeRate); + const tokenBalanceFiat = balanceToFiatNumber( + tokenBalance, + conversionRate, + exchangeRate, + decimalsToShow + ); tokenFiat += tokenBalanceFiat; }); } From d72aa1769e9a9e8b1d97199af740f23240917f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Tue, 20 Apr 2021 17:04:56 -0400 Subject: [PATCH 37/51] v2.2.0 (#2555) * 613 * fastfile * changelog --- CHANGELOG.md | 5 +++++ ios/MetaMask.xcodeproj/project.pbxproj | 4 ++-- ios/fastlane/Fastfile | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d64ca4bd958..fb592ef25a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## v2.2.0 - Apr 21 2021 +- [#2547](https://github.com/MetaMask/metamask-mobile/pull/2547): Include decimalsToShow in balanceToFiatNumber +- [#2554](https://github.com/MetaMask/metamask-mobile/pull/2554): Bug fix/sync import time - [#2546](https://github.com/MetaMask/metamask-mobile/pull/2546): Fix analytics try catch - [#2543](https://github.com/MetaMask/metamask-mobile/pull/2543): Only get nonce from the network if the feature is enabled - [#2460](https://github.com/MetaMask/metamask-mobile/pull/2460): Feature/tx local state logs @@ -24,6 +26,9 @@ - [#2403](https://github.com/MetaMask/metamask-mobile/pull/2403): Bump babel-eslint from 10.0.3 to 10.1.0 - [#2381](https://github.com/MetaMask/metamask-mobile/pull/2381): Display correct number of decimals for 'usd' fiat +## v2.1.3 - Apr 19 2021 +- [#2548](https://github.com/MetaMask/metamask-mobile/pull/2548): Hotfix analytics try catch + ## v2.1.2 - Apr 16 2021 - [#2538](https://github.com/MetaMask/metamask-mobile/pull/2538): fix/connection change handler diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 44bc53f912d..6e7beb4b1f4 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 612; + CURRENT_PROJECT_VERSION = 613; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 612; + CURRENT_PROJECT_VERSION = 613; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d841e745d97..dc3216bec8a 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -15,6 +15,8 @@ default_platform(:ios) +ENV["DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS"] = "-t DAV" + platform :ios do desc "Submit a new Beta Build to Testflight" From 3cbee3f3ad5dc14ef0867fb82fa7a9bb8a9d3854 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Wed, 21 Apr 2021 16:15:55 +0100 Subject: [PATCH 38/51] Bump versioncode (#2558) --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index fba1856c5f4..87caea76f7b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -166,7 +166,7 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 53 + versionCode 54 versionName "2.2.0" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') From 4b5d268e921151d3742e4bcb01055168343621a4 Mon Sep 17 00:00:00 2001 From: Minh Date: Fri, 23 Apr 2021 02:22:38 +1000 Subject: [PATCH 39/51] resolve isENS without case sensitivity (#2545) Co-authored-by: ricky --- app/util/address.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/util/address.js b/app/util/address.js index 842a597adbd..ae4e5d2e9cb 100644 --- a/app/util/address.js +++ b/app/util/address.js @@ -68,7 +68,7 @@ export async function importAccountFromPrivateKey(private_key) { */ export function isENS(name) { const rec = name && name.split('.'); - if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1])) { + if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1].toLowerCase())) { return false; } return true; From 6cbea593f3f2ba045ce8f4404b687cf082412f62 Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 22 Apr 2021 12:44:12 -0400 Subject: [PATCH 40/51] Revert "resolve isENS without case sensitivity (#2545)" (#2566) This reverts commit 4b5d268e921151d3742e4bcb01055168343621a4. --- app/util/address.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/util/address.js b/app/util/address.js index ae4e5d2e9cb..842a597adbd 100644 --- a/app/util/address.js +++ b/app/util/address.js @@ -68,7 +68,7 @@ export async function importAccountFromPrivateKey(private_key) { */ export function isENS(name) { const rec = name && name.split('.'); - if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1].toLowerCase())) { + if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1])) { return false; } return true; From e49850839409ae31a856eb85b75061786ef17f0c Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 22 Apr 2021 15:00:06 -0400 Subject: [PATCH 41/51] resolve isENS without case sensitivity (#2545) (#2568) Co-authored-by: ricky Co-authored-by: Minh --- app/util/address.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/util/address.js b/app/util/address.js index 842a597adbd..ae4e5d2e9cb 100644 --- a/app/util/address.js +++ b/app/util/address.js @@ -68,7 +68,7 @@ export async function importAccountFromPrivateKey(private_key) { */ export function isENS(name) { const rec = name && name.split('.'); - if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1])) { + if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1].toLowerCase())) { return false; } return true; From b5ad243d49387c1144e8e4fb498c7b5076bc09dc Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 22 Apr 2021 15:31:05 -0400 Subject: [PATCH 42/51] Use node 14 (#2539) --- .circleci/config.yml | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 52702030dfb..2b607f1bd02 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,7 @@ jobs: prep-deps: <<: *defaults docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - checkout - run: @@ -33,7 +33,7 @@ jobs: prep-node-deps: <<: *defaults docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - checkout - restore_cache: *restore-node-cache @@ -49,7 +49,7 @@ jobs: lint: <<: *defaults docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - checkout - attach_workspace: @@ -60,7 +60,7 @@ jobs: <<: *defaults parallelism: 3 docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - checkout - attach_workspace: @@ -74,7 +74,7 @@ jobs: test-deps: <<: *defaults docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - checkout - attach_workspace: @@ -106,7 +106,7 @@ jobs: upload-coverage: <<: *defaults docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - checkout - attach_workspace: @@ -117,7 +117,7 @@ jobs: all-tests-pass: <<: *defaults docker: - - image: circleci/node:10 + - image: circleci/node:14 steps: - run: name: All Tests Passed diff --git a/package.json b/package.json index 61a48e635d0..e84ebdfab01 100644 --- a/package.json +++ b/package.json @@ -332,7 +332,7 @@ "fs": "react-native-level-fs" }, "engines": { - "node": "^10.17.0", + "node": "^14.0.0", "yarn": "^1.22.0" }, "rnpm": { From c7b93e527648f53bc141b04e8cbd8640dfa6073d Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Fri, 23 Apr 2021 11:30:14 -0400 Subject: [PATCH 43/51] Swaps: BSC Support (#2468) --- app/components/Nav/Main/index.js | 27 +++-- app/components/UI/AccountOverview/index.js | 9 +- app/components/UI/AssetOverview/index.js | 19 ++-- app/components/UI/CustomGas/index.js | 9 ++ app/components/UI/DrawerView/index.js | 10 +- .../TransactionNotification/index.js | 7 ++ app/components/UI/Swaps/QuotesView.js | 51 ++++++--- app/components/UI/Swaps/SwapsLiveness.js | 17 +-- .../UI/Swaps/components/QuotesModal.js | 9 +- .../UI/Swaps/components/TokenIcon.js | 8 +- .../UI/Swaps/components/TokenSelectModal.js | 4 +- .../components/TransactionsEditionModal.js | 13 ++- app/components/UI/Swaps/index.js | 107 ++++++++++++------ app/components/UI/Swaps/utils/index.js | 21 +++- app/components/UI/Swaps/utils/useBalance.js | 4 +- .../UI/Swaps/utils/useBlockExplorer.js | 76 +++++++++++++ app/components/UI/Tokens/index.js | 14 ++- .../TransactionDetails/index.js | 17 ++- app/components/UI/TransactionElement/index.js | 17 ++- app/components/UI/TransactionElement/utils.js | 25 ++-- app/components/UI/TransactionReview/index.js | 10 +- app/components/Views/Asset/index.js | 6 +- app/components/Views/SendFlow/Amount/index.js | 21 +++- .../Confirm/__snapshots__/index.test.js.snap | 28 ----- .../Views/SendFlow/Confirm/index.js | 11 +- app/core/Engine.js | 6 +- app/images/bnb-logo.png | Bin 0 -> 6675 bytes app/reducers/swaps/index.js | 22 +++- app/reducers/swaps/swaps.test.js | 8 +- app/util/transactions.js | 18 +-- index.js | 1 + locales/languages/en.json | 2 + package.json | 4 +- yarn.lock | 100 ++++++++++------ 34 files changed, 486 insertions(+), 215 deletions(-) create mode 100644 app/components/UI/Swaps/utils/useBlockExplorer.js create mode 100644 app/images/bnb-logo.png diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 096f0f05d88..27c3ed21e2b 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -38,7 +38,6 @@ import { decodeApproveData } from '../../../util/transactions'; import { BN } from 'ethereumjs-util'; -import { safeToChecksumAddress } from '../../../util/address'; import Logger from '../../../util/Logger'; import contractMap from '@metamask/contract-metadata'; import MessageSign from '../../UI/MessageSign'; @@ -58,7 +57,7 @@ import AccountApproval from '../../UI/AccountApproval'; import ProtectYourWalletModal from '../../UI/ProtectYourWalletModal'; import MainNavigator from './MainNavigator'; import SkipAccountSecurityModal from '../../UI/SkipAccountSecurityModal'; -import { swapsUtils, util } from '@estebanmino/controllers'; +import { swapsUtils, util } from '@metamask/swaps-controller'; import SwapsLiveness from '../../UI/Swaps/SwapsLiveness'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; @@ -310,16 +309,17 @@ const Main = props => { async transactionMeta => { if (transactionMeta.origin === TransactionTypes.MMM) return; - const to = safeToChecksumAddress(transactionMeta.transaction.to); + const to = transactionMeta.transaction.to?.toLowerCase(); const { data } = transactionMeta.transaction; // if approval data includes metaswap contract // if destination address is metaswap contract if ( - to === safeToChecksumAddress(swapsUtils.SWAPS_CONTRACT_ADDRESS) || + to === swapsUtils.getSwapsContractAddress(props.chainId) || (data && data.substr(0, 10) === APPROVE_FUNCTION_SIGNATURE && - decodeApproveData(data).spenderAddress === swapsUtils.SWAPS_CONTRACT_ADDRESS) + decodeApproveData(data).spenderAddress?.toLowerCase() === + swapsUtils.getSwapsContractAddress(props.chainId)) ) { if (transactionMeta.origin === process.env.MM_FOX_CODE) { autoSign(transactionMeta); @@ -388,6 +388,7 @@ const Main = props => { }, [ props.tokens, + props.chainId, setEtherTransaction, setTransactionObject, toggleApproveModal, @@ -568,6 +569,14 @@ const Main = props => { } }); + // unapprovedTransaction effect + useEffect(() => { + Engine.context.TransactionController.hub.on('unapprovedTransaction', onUnapprovedTransaction); + return () => { + Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', onUnapprovedTransaction); + }; + }, [onUnapprovedTransaction]); + useEffect(() => { initializeWalletConnect(); AppState.addEventListener('change', handleAppStateChange); @@ -596,8 +605,6 @@ const Main = props => { } }); - Engine.context.TransactionController.hub.on('unapprovedTransaction', onUnapprovedTransaction); - Engine.context.MessageManager.hub.on('unapprovedMessage', messageParams => onUnapprovedMessage(messageParams, 'eth') ); @@ -628,7 +635,6 @@ const Main = props => { lockManager.current.stopListening(); Engine.context.PersonalMessageManager.hub.removeAllListeners(); Engine.context.TypedMessageManager.hub.removeAllListeners(); - Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', onUnapprovedTransaction); WalletConnect.hub.removeAllListeners(); removeConnectionStatusListener.current && removeConnectionStatusListener.current(); }; @@ -737,6 +743,10 @@ Main.propTypes = { * Selected address */ selectedAddress: PropTypes.string, + /** + * Chain id + */ + chainId: PropTypes.string, /** * Network provider type */ @@ -755,6 +765,7 @@ const mapStateToProps = state => ({ lockTime: state.settings.lockTime, thirdPartyApiMode: state.privacy.thirdPartyApiMode, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, tokens: state.engine.backgroundState.AssetsController.tokens, isPaymentRequest: state.transaction.paymentRequest, dappTransactionModalVisible: state.modals.dappTransactionModalVisible, diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index 9b11b04316f..1cbfdc57956 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { ScrollView, TextInput, StyleSheet, Text, View, TouchableOpacity, InteractionManager } from 'react-native'; import Clipboard from '@react-native-community/clipboard'; -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; import { connect } from 'react-redux'; import Engine from '../../../core/Engine'; import Analytics from '../../../core/Analytics'; @@ -21,6 +21,7 @@ import { renderFiat } from '../../../util/number'; import { renderAccountName } from '../../../util/address'; import { isMainNet } from '../../../util/networks'; import { getEther } from '../../../util/transactions'; +import { isSwapsAllowed } from '../Swaps/utils'; import Identicon from '../Identicon'; import AssetActionButton from '../AssetActionButton'; @@ -257,7 +258,7 @@ class AccountOverview extends PureComponent { goToSwaps = () => this.props.navigation.navigate('Swaps', { - sourceToken: swapsUtils.ETH_SWAPS_TOKEN_ADDRESS + sourceToken: swapsUtils.NATIVE_SWAPS_TOKEN_ADDRESS }); render() { @@ -331,7 +332,7 @@ class AccountOverview extends PureComponent { )} - {fiatBalance} + {isMainNet(chainId) && {fiatBalance}} @@ -358,7 +359,7 @@ class AccountOverview extends PureComponent { {AppConstants.SWAPS.ACTIVE && ( diff --git a/app/components/UI/AssetOverview/index.js b/app/components/UI/AssetOverview/index.js index 87fd44b11df..a492a823efd 100644 --- a/app/components/UI/AssetOverview/index.js +++ b/app/components/UI/AssetOverview/index.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { InteractionManager, StyleSheet, Text, View, TouchableOpacity } from 'react-native'; import PropTypes from 'prop-types'; -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; import AssetIcon from '../AssetIcon'; import Identicon from '../Identicon'; import AssetActionButton from '../AssetActionButton'; @@ -12,9 +12,10 @@ import { toggleReceiveModal } from '../../../actions/modals'; import { connect } from 'react-redux'; import { renderFromTokenMinimalUnit, balanceToFiat, renderFromWei, weiToFiat, hexToBN } from '../../../util/number'; import { safeToChecksumAddress } from '../../../util/address'; +import { isMainNet } from '../../../util/networks'; import { getEther } from '../../../util/transactions'; import { newAssetTransaction } from '../../../actions/transaction'; -import { isMainNet } from '../../../util/networks'; +import { isSwapsAllowed } from '../Swaps/utils'; import { swapsLivenessSelector, swapsTokensObjectSelector } from '../../../reducers/swaps'; import Engine from '../../../core/Engine'; import Logger from '../../../util/Logger'; @@ -178,7 +179,7 @@ class AssetOverview extends PureComponent { goToSwaps = () => { this.props.navigation.navigate('Swaps', { - sourceToken: this.props.asset.isETH ? swapsUtils.ETH_SWAPS_TOKEN_ADDRESS : this.props.asset.address + sourceToken: this.props.asset.isETH ? swapsUtils.NATIVE_SWAPS_TOKEN_ADDRESS : this.props.asset.address }); }; @@ -249,12 +250,16 @@ class AssetOverview extends PureComponent { let balance, balanceFiat; if (isETH) { balance = renderFromWei(accounts[selectedAddress] && accounts[selectedAddress].balance); - balanceFiat = weiToFiat(hexToBN(accounts[selectedAddress].balance), conversionRate, currentCurrency); + balanceFiat = isMainNet(chainId) + ? weiToFiat(hexToBN(accounts[selectedAddress].balance), conversionRate, currentCurrency) + : null; } else { const exchangeRate = itemAddress in tokenExchangeRates ? tokenExchangeRates[itemAddress] : undefined; balance = itemAddress in tokenBalances ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], decimals) : 0; - balanceFiat = balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency); + balanceFiat = isMainNet(chainId) + ? balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency) + : null; } // choose balances depending on 'primaryCurrency' if (primaryCurrency === 'ETH') { @@ -275,7 +280,7 @@ class AssetOverview extends PureComponent { {mainBalance} - {secondaryBalance} + {secondaryBalance && {secondaryBalance}} )} @@ -303,7 +308,7 @@ class AssetOverview extends PureComponent { {AppConstants.SWAPS.ACTIVE && ( diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index 3103e63b99c..be6d5ab7a29 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -252,6 +252,10 @@ class CustomGas extends PureComponent { * Object BN containing gas price */ gasPrice: PropTypes.object, + /** + * Object BN containing mininum gas price + */ + minimumGasPrice: PropTypes.object, /** * Callback to modify parent state */ @@ -488,6 +492,11 @@ class CustomGas extends PureComponent { const warningSufficientFunds = this.hasSufficientFunds(customGasLimitBN, gasPriceBNWei); let warningGasPrice; let warningGasPriceHigh = ''; + if (this.onlyAdvanced() && this.props.minimumGasPrice) { + if (parseInt(gasPrice) < parseInt(fromWei(this.props.minimumGasPrice, 'gwei'))) { + warningGasPrice = strings('transaction.low_gas_price'); + } + } if (this.props.basicGasEstimates) { if (parseInt(gasPrice) < parseInt(this.props.basicGasEstimates.safeLowGwei)) warningGasPrice = strings('transaction.low_gas_price'); diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 4590ab75626..ebf002880fc 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -8,7 +8,7 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import FeatherIcon from 'react-native-vector-icons/Feather'; import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; import { colors, fontStyles } from '../../../styles/common'; -import { hasBlockExplorer, findBlockExplorerForRpc, getBlockExplorerName } from '../../../util/networks'; +import { hasBlockExplorer, findBlockExplorerForRpc, getBlockExplorerName, isMainNet } from '../../../util/networks'; import Identicon from '../Identicon'; import StyledButton from '../StyledButton'; import AccountList from '../AccountList'; @@ -338,6 +338,10 @@ class DrawerView extends PureComponent { * Wizard onboarding state */ wizard: PropTypes.object, + /** + * Chain Id + */ + chainId: PropTypes.string, /** * Current provider ticker */ @@ -830,6 +834,7 @@ class DrawerView extends PureComponent { selectedAddress, keyrings, currentCurrency, + chainId, ticker, seedphraseBackedUp } = this.props; @@ -876,7 +881,7 @@ class DrawerView extends PureComponent { - {fiatBalanceStr} + {isMainNet(chainId) && {fiatBalanceStr}} ({ receiveModalVisible: state.modals.receiveModalVisible, passwordSet: state.user.passwordSet, wizard: state.wizard, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, ticker: state.engine.backgroundState.NetworkController.provider.ticker, tokens: state.engine.backgroundState.AssetsController.tokens, tokenBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, diff --git a/app/components/UI/Notification/TransactionNotification/index.js b/app/components/UI/Notification/TransactionNotification/index.js index 0cff2f48f06..995928086c0 100644 --- a/app/components/UI/Notification/TransactionNotification/index.js +++ b/app/components/UI/Notification/TransactionNotification/index.js @@ -179,6 +179,7 @@ function TransactionNotification(props) { const { selectedAddress, ticker, + chainId, conversionRate, currentCurrency, exchangeRate, @@ -194,6 +195,7 @@ function TransactionNotification(props) { tx, selectedAddress, ticker, + chainId, conversionRate, currentCurrency, exchangeRate, @@ -338,6 +340,10 @@ TransactionNotification.propTypes = { * Current provider ticker */ ticker: PropTypes.string, + /** + * Current provider chainId + */ + chainId: PropTypes.string, /** * ETH to current currency conversion rate */ @@ -374,6 +380,7 @@ const mapStateToProps = state => ({ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, transactions: state.engine.backgroundState.TransactionController.transactions, ticker: state.engine.backgroundState.NetworkController.provider.ticker, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, tokens: state.engine.backgroundState.AssetsController.tokens.reduce((tokens, token) => { tokens[token.address] = token; return tokens; diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index e50bc23b3c7..d578eadd56c 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -7,7 +7,7 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI import FAIcon from 'react-native-vector-icons/FontAwesome'; import BigNumber from 'bignumber.js'; import { NavigationContext } from 'react-navigation'; -import { swapsUtils, util } from '@estebanmino/controllers'; +import { swapsUtils, util } from '@metamask/swaps-controller'; import { WalletDevice } from '@metamask/controllers/'; import { @@ -20,8 +20,9 @@ import { toWei, weiToFiat } from '../../../util/number'; +import { isMainNet } from '../../../util/networks'; import { safeToChecksumAddress } from '../../../util/address'; -import { getErrorMessage, getFetchParams, getQuotesNavigationsParams, isSwapsETH } from './utils'; +import { getErrorMessage, getFetchParams, getQuotesNavigationsParams, isSwapsNativeAsset } from './utils'; import { colors } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; @@ -49,7 +50,7 @@ import InfoModal from './components/InfoModal'; import useModalHandler from '../../Base/hooks/useModalHandler'; import useBalance from './utils/useBalance'; import useGasPrice from './utils/useGasPrice'; -import { decodeApproveData } from '../../../util/transactions'; +import { decodeApproveData, getTicker } from '../../../util/transactions'; import Logger from '../../../util/Logger'; const POLLING_INTERVAL = AppConstants.SWAPS.POLLING_INTERVAL; @@ -209,7 +210,7 @@ async function resetAndStartPolling({ slippage, sourceToken, destinationToken, s const contractExchangeRates = TokenRatesController.state.contractExchangeRates; // ff the token is not in the wallet, we'll add it if ( - destinationToken.address !== swapsUtils.ETH_SWAPS_TOKEN_ADDRESS && + !isSwapsNativeAsset(destinationToken) && !contractExchangeRates[safeToChecksumAddress(destinationToken.address)] ) { const { address, symbol, decimals } = destinationToken; @@ -253,6 +254,8 @@ function SwapsQuotesView({ selectedAddress, currentCurrency, conversionRate, + chainId, + ticker, isInPolling, quotesLastFetched, pollingCyclesLeft, @@ -280,7 +283,7 @@ function SwapsQuotesView({ const hasConversionRate = Boolean(destinationToken) && - (isSwapsETH(destinationToken) || + (isSwapsNativeAsset(destinationToken) || Boolean( Engine.context.TokenRatesController.state.contractExchangeRates?.[ safeToChecksumAddress(destinationToken.address) @@ -373,7 +376,7 @@ function SwapsQuotesView({ const hasEnoughTokenBalance = tokenBalanceBN.gte(sourceBN); const missingTokenBalance = hasEnoughTokenBalance ? null : sourceBN.minus(tokenBalanceBN); - const ethAmountBN = sourceToken.address === swapsUtils.ETH_SWAPS_TOKEN_ADDRESS ? sourceBN : new BigNumber(0); + const ethAmountBN = isSwapsNativeAsset(sourceToken) ? sourceBN : new BigNumber(0); const ethBalanceBN = new BigNumber(accounts[selectedAddress].balance); const gasBN = toWei(selectedQuoteValue?.maxEthFee || '0'); const hasEnoughEthBalance = ethBalanceBN.gte(ethAmountBN.plus(gasBN)); @@ -1022,22 +1025,23 @@ function SwapsQuotesView({ {`${strings('swaps.you_need')} `} - {!hasEnoughTokenBalance && sourceToken.address !== swapsUtils.ETH_SWAPS_TOKEN_ADDRESS + {!hasEnoughTokenBalance && !isSwapsNativeAsset(sourceToken) ? `${renderFromTokenMinimalUnit(missingTokenBalance, sourceToken.decimals)} ${ sourceToken.symbol // eslint-disable-next-line no-mixed-spaces-and-tabs } ` - : `${renderFromWei(missingEthBalance)} ETH `} + : `${renderFromWei(missingEthBalance)} ${getTicker(ticker)} `} {!hasEnoughTokenBalance ? `${strings('swaps.more_to_complete')} ` : `${strings('swaps.more_gas_to_complete')} `} - {(sourceToken.address === swapsUtils.ETH_SWAPS_TOKEN_ADDRESS || - (hasEnoughTokenBalance && !hasEnoughEthBalance)) && ( - - {strings('swaps.buy_more_eth')} - - )} + {isMainNet(chainId) && + (isSwapsNativeAsset(sourceToken) || + (hasEnoughTokenBalance && !hasEnoughEthBalance)) && ( + + {strings('swaps.buy_more_eth')} + + )} )} @@ -1209,7 +1213,7 @@ function SwapsQuotesView({ - {renderFromWei(toWei(selectedQuoteValue?.ethFee))} ETH + {renderFromWei(toWei(selectedQuoteValue?.ethFee))} {getTicker(ticker)} {` ${weiToFiat( @@ -1233,7 +1237,10 @@ function SwapsQuotesView({ - {renderFromWei(toWei(selectedQuoteValue?.maxEthFee || '0x0'))} ETH + + {renderFromWei(toWei(selectedQuoteValue?.maxEthFee || '0x0'))}{' '} + {getTicker(ticker)} + {` ${weiToFiat( toWei(selectedQuoteValue?.maxEthFee), @@ -1326,6 +1333,7 @@ function SwapsQuotesView({ destinationToken={destinationToken} selectedQuote={selectedQuoteId} showOverallValue={hasConversionRate} + ticker={getTicker(ticker)} /> ); @@ -1374,6 +1383,14 @@ SwapsQuotesView.propTypes = { * A string that represents the selected address */ selectedAddress: PropTypes.string, + /** + * Chain Id + */ + chainId: PropTypes.string, + /** + * Native asset ticker + */ + ticker: PropTypes.string, isInPolling: PropTypes.bool, quotesLastFetched: PropTypes.number, topAggId: PropTypes.string, @@ -1392,6 +1409,8 @@ SwapsQuotesView.propTypes = { const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, + ticker: state.engine.backgroundState.NetworkController.provider.ticker, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, balances: state.engine.backgroundState.TokenBalancesController.contractBalances, conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, diff --git a/app/components/UI/Swaps/SwapsLiveness.js b/app/components/UI/Swaps/SwapsLiveness.js index 27ba0a6fb07..16d3324a1d8 100644 --- a/app/components/UI/Swaps/SwapsLiveness.js +++ b/app/components/UI/Swaps/SwapsLiveness.js @@ -1,4 +1,4 @@ -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; import { useCallback, useEffect, useState } from 'react'; import { AppState } from 'react-native'; import { connect } from 'react-redux'; @@ -10,18 +10,18 @@ import useInterval from '../../hooks/useInterval'; const SWAPS_ACTIVE = AppConstants.SWAPS.ACTIVE; const POLLING_FREQUENCY = AppConstants.SWAPS.LIVENESS_POLLING_FREQUENCY; -function SwapLiveness({ isLive, setLiveness }) { +function SwapLiveness({ isLive, chainId, setLiveness }) { const [hasMountChecked, setHasMountChecked] = useState(false); const checkLiveness = useCallback(async () => { try { - const { mobile_active: liveness } = await swapsUtils.fetchSwapsFeatureLiveness(); - setLiveness(liveness); + const { mobile_active: liveness } = await swapsUtils.fetchSwapsFeatureLiveness(chainId); + setLiveness(liveness, chainId); } catch (error) { Logger.error(error, 'Swaps: error while fetching swaps liveness'); - setLiveness(false); + setLiveness(false, chainId); } - }, [setLiveness]); + }, [setLiveness, chainId]); // Check on mount useEffect(() => { @@ -62,11 +62,12 @@ function SwapLiveness({ isLive, setLiveness }) { } const mapStateToProps = state => ({ - isLive: swapsLivenessSelector(state) + isLive: swapsLivenessSelector(state), + chainId: state.engine.backgroundState.NetworkController.provider.chainId }); const mapDispatchToProps = dispatch => ({ - setLiveness: liveness => dispatch(setSwapsLiveness(liveness)) + setLiveness: (liveness, chainId) => dispatch(setSwapsLiveness(liveness, chainId)) }); export default connect( diff --git a/app/components/UI/Swaps/components/QuotesModal.js b/app/components/UI/Swaps/components/QuotesModal.js index 9078cd8ca13..514245d3497 100644 --- a/app/components/UI/Swaps/components/QuotesModal.js +++ b/app/components/UI/Swaps/components/QuotesModal.js @@ -137,7 +137,8 @@ function QuotesModal({ conversionRate, currentCurrency, quoteValues, - showOverallValue + showOverallValue, + ticker }) { const bestOverallValue = quoteValues[quotes[0].aggregator].overallValueOfQuote; const [displayDetails, setDisplayDetails] = useState(false); @@ -290,7 +291,7 @@ function QuotesModal({ {renderFromWei(toWei(selectedDetailsQuoteValues.ethFee))}{' '} - ETH + {ticker} {' '} (~ @@ -466,6 +467,10 @@ QuotesModal.propTypes = { * Currency code of the currently-active currency */ currentCurrency: PropTypes.string, + /** + * Native asset ticker + */ + ticker: PropTypes.string, quoteValues: PropTypes.object, showOverallValue: PropTypes.bool }; diff --git a/app/components/UI/Swaps/components/TokenIcon.js b/app/components/UI/Swaps/components/TokenIcon.js index d0ef90f26b9..b857ef30c93 100644 --- a/app/components/UI/Swaps/components/TokenIcon.js +++ b/app/components/UI/Swaps/components/TokenIcon.js @@ -6,8 +6,10 @@ import RemoteImage from '../../../Base/RemoteImage'; import Text from '../../../Base/Text'; import { colors } from '../../../../styles/common'; -// eslint-disable-next-line import/no-commonjs +/* eslint-disable import/no-commonjs */ const ethLogo = require('../../../../images/eth-logo.png'); +const bnbLogo = require('../../../../images/bnb-logo.png'); +/* eslint-enable import/no-commonjs */ const REGULAR_SIZE = 24; const REGULAR_RADIUS = 12; @@ -79,11 +81,11 @@ EmptyIcon.propTypes = { }; function TokenIcon({ symbol, icon, medium, big, biggest, style }) { - if (symbol === 'ETH') { + if (symbol === 'ETH' || symbol === 'BNB') { return ( ({ diff --git a/app/components/UI/Swaps/index.js b/app/components/UI/Swaps/index.js index f244c074dce..ab6b62f733a 100644 --- a/app/components/UI/Swaps/index.js +++ b/app/components/UI/Swaps/index.js @@ -15,7 +15,7 @@ import { weiToFiat } from '../../../util/number'; import { safeToChecksumAddress } from '../../../util/address'; -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; import { @@ -30,10 +30,9 @@ import Device from '../../../util/Device'; import Engine from '../../../core/Engine'; import AppConstants from '../../../core/AppConstants'; -import { getEtherscanAddressUrl } from '../../../util/etherscan'; import { strings } from '../../../../locales/i18n'; import { colors } from '../../../styles/common'; -import { setQuotesNavigationsParams, isSwapsETH } from './utils'; +import { setQuotesNavigationsParams, isSwapsNativeAsset } from './utils'; import { getSwapsAmountNavbar } from '../Navbar'; import Onboarding from './components/Onboarding'; @@ -47,6 +46,7 @@ import TokenSelectButton from './components/TokenSelectButton'; import TokenSelectModal from './components/TokenSelectModal'; import SlippageModal from './components/SlippageModal'; import useBalance from './utils/useBalance'; +import useBlockExplorer from './utils/useBlockExplorer'; import InfoModal from './components/InfoModal'; const styles = StyleSheet.create({ @@ -130,7 +130,7 @@ const styles = StyleSheet.create({ } }); -const SWAPS_ETH_ADDRESS = swapsUtils.ETH_SWAPS_TOKEN_ADDRESS; +const SWAPS_NATIVE_ADDRESS = swapsUtils.NATIVE_SWAPS_TOKEN_ADDRESS; const TOKEN_MINIMUM_SOURCES = 1; const MAX_TOP_ASSETS = 20; @@ -138,6 +138,9 @@ function SwapsAmountView({ swapsTokens, accounts, selectedAddress, + chainId, + provider, + frequentRpcList, balances, tokensWithBalance, tokensTopAssets, @@ -149,7 +152,8 @@ function SwapsAmountView({ setLiveness }) { const navigation = useContext(NavigationContext); - const initialSource = navigation.getParam('sourceToken', SWAPS_ETH_ADDRESS); + const explorer = useBlockExplorer(provider, frequentRpcList); + const initialSource = navigation.getParam('sourceToken', SWAPS_NATIVE_ADDRESS); const [amount, setAmount] = useState('0'); const [slippage, setSlippage] = useState(AppConstants.SWAPS.DEFAULT_SLIPPAGE); const [isInitialLoadingTokens, setInitialLoadingTokens] = useState(false); @@ -179,13 +183,13 @@ function SwapsAmountView({ useEffect(() => { (async () => { try { - const { mobile_active: liveness } = await swapsUtils.fetchSwapsFeatureLiveness(); - setLiveness(liveness); + const { mobile_active: liveness } = await swapsUtils.fetchSwapsFeatureLiveness(chainId); + setLiveness(liveness, chainId); if (liveness) { // Triggered when a user enters the MetaMask Swap feature InteractionManager.runAfterInteractions(() => { const parameters = { - source: initialSource === SWAPS_ETH_ADDRESS ? 'MainView' : 'TokenView', + source: initialSource === SWAPS_NATIVE_ADDRESS ? 'MainView' : 'TokenView', activeCurrency: swapsTokens?.find( token => token.address?.toLowerCase() === initialSource.toLowerCase() )?.symbol @@ -198,12 +202,12 @@ function SwapsAmountView({ } } catch (error) { Logger.error(error, 'Swaps: error while fetching swaps liveness'); - setLiveness(false); + setLiveness(false, chainId); navigation.pop(); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialSource, navigation, setLiveness]); + }, [initialSource, chainId, navigation, setLiveness]); const keypadViewRef = useRef(null); @@ -251,11 +255,13 @@ function SwapsAmountView({ }, [destinationToken]); const isTokenInBalances = - sourceToken && !isSwapsETH(sourceToken) ? safeToChecksumAddress(sourceToken.address) in balances : false; + sourceToken && !isSwapsNativeAsset(sourceToken) + ? safeToChecksumAddress(sourceToken.address) in balances + : false; useEffect(() => { (async () => { - if (sourceToken && !isSwapsETH(sourceToken) && !isTokenInBalances) { + if (sourceToken && !isSwapsNativeAsset(sourceToken) && !isTokenInBalances) { setContractBalance(null); setContractBalanceAsUnits(numberToBN(0)); const { AssetsContractController } = Engine.context; @@ -285,9 +291,9 @@ function SwapsAmountView({ const controllerBalance = useBalance(accounts, balances, selectedAddress, sourceToken); const controllerBalanceAsUnits = useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits: true }); - const balance = isSwapsETH(sourceToken) || isTokenInBalances ? controllerBalance : contractBalance; + const balance = isSwapsNativeAsset(sourceToken) || isTokenInBalances ? controllerBalance : contractBalance; const balanceAsUnits = - isSwapsETH(sourceToken) || isTokenInBalances ? controllerBalanceAsUnits : contractBalanceAsUnits; + isSwapsNativeAsset(sourceToken) || isTokenInBalances ? controllerBalanceAsUnits : contractBalanceAsUnits; const hasBalance = useMemo(() => { if (!balanceAsUnits || !sourceToken) { return false; @@ -308,7 +314,7 @@ function SwapsAmountView({ return undefined; } let balanceFiat; - if (isSwapsETH(sourceToken)) { + if (isSwapsNativeAsset(sourceToken)) { balanceFiat = weiToFiat(toTokenMinimalUnit(amount, sourceToken?.decimals), conversionRate, currentCurrency); } else { const sourceAddress = safeToChecksumAddress(sourceToken.address); @@ -319,7 +325,7 @@ function SwapsAmountView({ }, [amount, conversionRate, currentCurrency, hasInvalidDecimals, sourceToken, tokenExchangeRates]); const destinationTokenHasEnoughOcurrances = useMemo(() => { - if (!destinationToken || isSwapsETH(destinationToken)) { + if (!destinationToken || isSwapsNativeAsset(destinationToken)) { return true; } return destinationToken?.occurances > TOKEN_MINIMUM_SOURCES; @@ -330,7 +336,7 @@ function SwapsAmountView({ if (hasInvalidDecimals) { return; } - if (!isSwapsETH(sourceToken) && !isTokenInBalances && !balanceAsUnits?.isZero()) { + if (!isSwapsNativeAsset(sourceToken) && !isTokenInBalances && !balanceAsUnits?.isZero()) { const { AssetsController } = Engine.context; const { address, symbol, decimals } = sourceToken; await AssetsController.addToken(address, symbol, decimals); @@ -404,10 +410,10 @@ function SwapsAmountView({ } hideTokenVerificationModal(); navigation.navigate('Webview', { - url: getEtherscanAddressUrl('mainnet', destinationToken.address), + url: explorer.token(destinationToken.address), title: strings('swaps.verify') }); - }, [destinationToken, hideTokenVerificationModal, navigation]); + }, [explorer, destinationToken, hideTokenVerificationModal, navigation]); const handleAmountPress = useCallback(() => keypadViewRef?.current?.shake?.(), []); @@ -481,7 +487,7 @@ function SwapsAmountView({ strings('swaps.available_to_swap', { asset: `${balance} ${sourceToken.symbol}` })} - {!isSwapsETH(sourceToken) && hasBalance && ( + {!isSwapsNativeAsset(sourceToken) && hasBalance && ( {' '} {strings('swaps.use_max')} @@ -519,23 +525,33 @@ function SwapsAmountView({ dismiss={toggleDestinationModal} title={strings('swaps.convert_to')} tokens={swapsTokens} - initialTokens={[swapsUtils.ETH_SWAPS_TOKEN_OBJECT, ...tokensTopAssets.slice(0, MAX_TOP_ASSETS)]} + initialTokens={[ + swapsUtils.getNativeSwapsToken(chainId), + ...tokensTopAssets.slice(0, MAX_TOP_ASSETS) + ]} onItemPress={handleDestinationTokenPress} excludeAddresses={[sourceToken?.address]} /> - {Boolean(destinationToken) && !isSwapsETH(destinationToken) ? ( + {Boolean(destinationToken) && !isSwapsNativeAsset(destinationToken) ? ( destinationTokenHasEnoughOcurrances ? ( - + {strings('swaps.verified_on_sources', { sources: destinationToken.occurances })} {` ${strings('swaps.verify_on')} `} - - Etherscan - + {explorer.isValid ? ( + + {explorer.name} + + ) : ( + strings('swaps.a_block_explorer') + )} . @@ -548,7 +564,7 @@ function SwapsAmountView({ onInfoPress={toggleTokenVerificationModal} > {textStyle => ( - + {strings('swaps.only_verified_on', { symbol: destinationToken.symbol, @@ -557,9 +573,13 @@ function SwapsAmountView({ {`${strings('swaps.verify_address_on')} `} - - Etherscan - + {explorer.isValid ? ( + + {explorer.name} + + ) : ( + strings('swaps.a_block_explorer') + )} . @@ -617,9 +637,13 @@ function SwapsAmountView({ {strings('swaps.token_multiple')} {` ${strings('swaps.token_check')} `} - - Etherscan - + {explorer.isValid ? ( + + {explorer.name} + + ) : ( + strings('swaps.a_block_explorer') + )} {` ${strings('swaps.token_to_verify')}`} } @@ -672,6 +696,18 @@ SwapsAmountView.propTypes = { * Function to set hasOnboarded */ setHasOnboarded: PropTypes.func, + /** + * Current Network provider + */ + provider: PropTypes.object, + /** + * Chain Id + */ + chainId: PropTypes.string, + /** + * Frequent RPC list from PreferencesController + */ + frequentRpcList: PropTypes.array, /** * Function to set liveness */ @@ -686,6 +722,9 @@ const mapStateToProps = state => ({ conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, tokenExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + provider: state.engine.backgroundState.NetworkController.provider, + frequentRpcList: state.engine.backgroundState.PreferencesController.frequentRpcList, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, tokensWithBalance: swapsTokensWithBalanceSelector(state), tokensTopAssets: swapsTopAssetsSelector(state), userHasOnboarded: swapsHasOnboardedSelector(state) @@ -693,7 +732,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ setHasOnboarded: hasOnboarded => dispatch(setSwapsHasOnboarded(hasOnboarded)), - setLiveness: liveness => dispatch(setSwapsLiveness(liveness)) + setLiveness: (liveness, chainId) => dispatch(setSwapsLiveness(liveness, chainId)) }); export default connect( diff --git a/app/components/UI/Swaps/utils/index.js b/app/components/UI/Swaps/utils/index.js index 31bf2a6f10c..46baab4f9fe 100644 --- a/app/components/UI/Swaps/utils/index.js +++ b/app/components/UI/Swaps/utils/index.js @@ -1,10 +1,25 @@ import { useMemo } from 'react'; import BigNumber from 'bignumber.js'; -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; import { strings } from '../../../../../locales/i18n'; +import AppConstants from '../../../../core/AppConstants'; -export function isSwapsETH(token) { - return Boolean(token) && token?.address === swapsUtils.ETH_SWAPS_TOKEN_ADDRESS; +const { ETH_CHAIN_ID, BSC_CHAIN_ID, SWAPS_TESTNET_CHAIN_ID } = swapsUtils; + +const allowedChainIds = [ETH_CHAIN_ID, BSC_CHAIN_ID]; + +export function isSwapsAllowed(chainId) { + if (!AppConstants.SWAPS.ACTIVE) { + return false; + } + if (!AppConstants.SWAPS.ONLY_MAINNET) { + allowedChainIds.push(SWAPS_TESTNET_CHAIN_ID); + } + return allowedChainIds.includes(chainId); +} + +export function isSwapsNativeAsset(token) { + return Boolean(token) && token?.address === swapsUtils.NATIVE_SWAPS_TOKEN_ADDRESS; } /** diff --git a/app/components/UI/Swaps/utils/useBalance.js b/app/components/UI/Swaps/utils/useBalance.js index 4d4e09bfee1..d006be26a8f 100644 --- a/app/components/UI/Swaps/utils/useBalance.js +++ b/app/components/UI/Swaps/utils/useBalance.js @@ -1,6 +1,6 @@ -import { swapsUtils } from '@estebanmino/controllers'; import { useMemo } from 'react'; import numberToBN from 'number-to-bn'; +import { isSwapsNativeAsset } from '.'; import { renderFromTokenMinimalUnit, renderFromWei } from '../../../../util/number'; import { safeToChecksumAddress } from '../../../../util/address'; @@ -9,7 +9,7 @@ function useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits if (!sourceToken) { return null; } - if (sourceToken.address === swapsUtils.ETH_SWAPS_TOKEN_ADDRESS) { + if (isSwapsNativeAsset(sourceToken)) { if (asUnits) { // Controller stores balances in hex for ETH return numberToBN((accounts[selectedAddress] && accounts[selectedAddress].balance) || 0); diff --git a/app/components/UI/Swaps/utils/useBlockExplorer.js b/app/components/UI/Swaps/utils/useBlockExplorer.js new file mode 100644 index 00000000000..193c7fb1a8b --- /dev/null +++ b/app/components/UI/Swaps/utils/useBlockExplorer.js @@ -0,0 +1,76 @@ +import { useCallback, useEffect, useState } from 'react'; +import etherscanLink from '@metamask/etherscan-link'; +import { RPC } from '../../../../constants/network'; +import { findBlockExplorerForRpc, getBlockExplorerName } from '../../../../util/networks'; +import { strings } from '../../../../../locales/i18n'; + +function useBlockExplorer(provider, frequentRpcList) { + const [explorer, setExplorer] = useState({ name: '', value: null, isValid: false, isRPC: false }); + + useEffect(() => { + if (provider.type === RPC) { + try { + const blockExplorer = findBlockExplorerForRpc(provider.rpcTarget, frequentRpcList); + if (!blockExplorer) { + throw new Error('No block explorer url'); + } + const url = new URL(blockExplorer); + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error('Block explorer URL is not a valid http(s) protocol'); + } + + const name = getBlockExplorerName(blockExplorer) || strings('swaps.block_explorer'); + setExplorer({ name, value: blockExplorer, isValid: true, isRPC: true }); + } catch { + setExplorer({ name: '', value: null, isValid: false, isRPC: false }); + } + } else { + setExplorer({ name: 'Etherscan', value: provider.chainId, isValid: true, isRPC: false }); + } + }, [frequentRpcList, provider]); + + const tx = useCallback( + hash => { + if (!explorer.isValid) { + return ''; + } + + const create = explorer.isRPC ? etherscanLink.createCustomExplorerLink : etherscanLink.createExplorerLink; + return create(hash, explorer.value); + }, + [explorer] + ); + const account = useCallback( + address => { + if (!explorer.isValid) { + return ''; + } + + const create = explorer.isRPC ? etherscanLink.createCustomAccountLink : etherscanLink.createAccountLink; + return create(address, explorer.value); + }, + [explorer] + ); + const token = useCallback( + address => { + if (!explorer.isValid) { + return ''; + } + + const create = explorer.isRPC + ? etherscanLink.createCustomTokenTrackerLink + : etherscanLink.createTokenTrackerLink; + return create(address, explorer.value); + }, + [explorer] + ); + + return { + ...explorer, + tx, + account, + token + }; +} + +export default useBlockExplorer; diff --git a/app/components/UI/Tokens/index.js b/app/components/UI/Tokens/index.js index cd1f46b10d5..886a2469f3d 100644 --- a/app/components/UI/Tokens/index.js +++ b/app/components/UI/Tokens/index.js @@ -17,6 +17,7 @@ import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; import StyledButton from '../StyledButton'; import { allowedToBuy } from '../FiatOrders'; import NetworkMainAssetLogo from '../NetworkMainAssetLogo'; +import { isMainNet } from '../../../util/networks'; const styles = StyleSheet.create({ wrapper: { @@ -162,14 +163,23 @@ class Tokens extends PureComponent { ); renderItem = asset => { - const { conversionRate, currentCurrency, tokenBalances, tokenExchangeRates, primaryCurrency } = this.props; + const { + chainId, + conversionRate, + currentCurrency, + tokenBalances, + tokenExchangeRates, + primaryCurrency + } = this.props; const itemAddress = safeToChecksumAddress(asset.address); const logo = asset.logo || ((contractMap[itemAddress] && contractMap[itemAddress].logo) || undefined); const exchangeRate = itemAddress in tokenExchangeRates ? tokenExchangeRates[itemAddress] : undefined; const balance = asset.balance || (itemAddress in tokenBalances ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], asset.decimals) : 0); - const balanceFiat = asset.balanceFiat || balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency); + const balanceFiat = isMainNet(chainId) + ? asset.balanceFiat || balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency) + : null; const balanceValue = `${balance} ${asset.symbol}`; // render balances according to primary currency diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index 647bb168250..7004d8c482a 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -3,7 +3,12 @@ import PropTypes from 'prop-types'; import { TouchableOpacity, StyleSheet, View } from 'react-native'; import { colors, fontStyles } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; -import { getNetworkTypeById, findBlockExplorerForRpc, getBlockExplorerName } from '../../../../util/networks'; +import { + getNetworkTypeById, + findBlockExplorerForRpc, + getBlockExplorerName, + isMainNet +} from '../../../../util/networks'; import { getEtherscanTransactionUrl, getEtherscanBaseUrl } from '../../../../util/etherscan'; import Logger from '../../../../util/Logger'; import { connect } from 'react-redux'; @@ -61,6 +66,10 @@ class TransactionDetails extends PureComponent { /* navigation object required to push new views */ navigation: PropTypes.object, + /** + * Chain Id + */ + chainId: PropTypes.string, /** * Object representing the selected the selected network */ @@ -166,6 +175,7 @@ class TransactionDetails extends PureComponent { render = () => { const { + chainId, transactionDetails, transactionObject, transactionObject: { @@ -223,7 +233,9 @@ class TransactionDetails extends PureComponent { amount={transactionDetails.summaryAmount} fee={transactionDetails.summaryFee} totalAmount={transactionDetails.summaryTotalAmount} - secondaryTotalAmount={transactionDetails.summarySecondaryTotalAmount} + secondaryTotalAmount={ + isMainNet(chainId) ? transactionDetails.summarySecondaryTotalAmount : undefined + } gasEstimationReady transactionType={transactionDetails.transactionType} /> @@ -247,6 +259,7 @@ class TransactionDetails extends PureComponent { const mapStateToProps = state => ({ network: state.engine.backgroundState.NetworkController, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, frequentRpcList: state.engine.backgroundState.PreferencesController.frequentRpcList }); export default connect(mapStateToProps)(TransactionDetails); diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index c08e11a7506..b49481973b6 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -15,6 +15,7 @@ import { TRANSACTION_TYPES } from '../../../util/transactions'; import ListItem from '../../Base/ListItem'; import StatusText from '../../Base/StatusText'; import DetailsModal from '../../Base/DetailsModal'; +import { isMainNet } from '../../../util/networks'; import { WalletDevice } from '@metamask/controllers/'; const styles = StyleSheet.create({ @@ -114,7 +115,11 @@ class TransactionElement extends PureComponent { */ onCancelAction: PropTypes.func, swapsTransactions: PropTypes.object, - swapsTokens: PropTypes.arrayOf(PropTypes.object) + swapsTokens: PropTypes.arrayOf(PropTypes.object), + /** + * Chain Id + */ + chainId: PropTypes.string }; state = { @@ -173,7 +178,7 @@ class TransactionElement extends PureComponent { 'transactions.from_device_label' // eslint-disable-next-line no-mixed-spaces-and-tabs )}` - : `${toDateFormat(tx.time)} + : `${toDateFormat(tx.time)} ` }`; }; @@ -233,6 +238,7 @@ class TransactionElement extends PureComponent { renderTxElement = transactionElement => { const { identities, + chainId, selectedAddress, tx: { time, status } } = this.props; @@ -253,7 +259,7 @@ class TransactionElement extends PureComponent { {Boolean(value) && ( {value} - {fiatValue} + {isMainNet(chainId) && {fiatValue}} )} @@ -371,11 +377,12 @@ class TransactionElement extends PureComponent { } const mapStateToProps = state => ({ + ticker: state.engine.backgroundState.NetworkController.provider.ticker, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, identities: state.engine.backgroundState.PreferencesController.identities, primaryCurrency: state.settings.primaryCurrency, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, swapsTransactions: state.engine.backgroundState.TransactionController.swapsTransactions || {}, - swapsTokens: state.engine.backgroundState.SwapsController.tokens, - ticker: state.engine.backgroundState.NetworkController.provider.ticker + swapsTokens: state.engine.backgroundState.SwapsController.tokens }); export default connect(mapStateToProps)(TransactionElement); diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js index 88a08654d9f..8c7753326cc 100644 --- a/app/components/UI/TransactionElement/utils.js +++ b/app/components/UI/TransactionElement/utils.js @@ -23,9 +23,10 @@ import { } from '../../../util/transactions'; import contractMap from '@metamask/contract-metadata'; import { toChecksumAddress } from 'ethereumjs-util'; -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; +import { isSwapsNativeAsset } from '../Swaps/utils'; -const { ETH_SWAPS_TOKEN_ADDRESS, SWAPS_CONTRACT_ADDRESS } = swapsUtils; +const { getSwapsContractAddress } = swapsUtils; function calculateTotalGas(gas, gasPrice) { const gasBN = hexToBN(gas); @@ -604,16 +605,14 @@ function decodeSwapsTx(args) { ); } - const sourceExchangeRate = - sourceToken.address === ETH_SWAPS_TOKEN_ADDRESS - ? 1 - : contractExchangeRates[safeToChecksumAddress(sourceToken.address)]; + const sourceExchangeRate = isSwapsNativeAsset(sourceToken) + ? 1 + : contractExchangeRates[safeToChecksumAddress(sourceToken.address)]; const renderSourceTokenFiatNumber = balanceToFiatNumber(decimalSourceAmount, conversionRate, sourceExchangeRate); - const destinationExchangeRate = - destinationToken.address === ETH_SWAPS_TOKEN_ADDRESS - ? 1 - : contractExchangeRates[safeToChecksumAddress(destinationToken.address)]; + const destinationExchangeRate = isSwapsNativeAsset(destinationToken) + ? 1 + : contractExchangeRates[safeToChecksumAddress(destinationToken.address)]; const renderDestinationTokenFiatNumber = balanceToFiatNumber( decimalDestinationAmount, conversionRate, @@ -682,13 +681,13 @@ function decodeSwapsTx(args) { * currentCurrency, exchangeRate, contractExchangeRates, collectibleContracts, tokens */ export default async function decodeTransaction(args) { - const { tx, selectedAddress, ticker, swapsTransactions = {} } = args; + const { tx, selectedAddress, ticker, chainId, swapsTransactions = {} } = args; const { isTransfer } = tx || {}; - const actionKey = await getActionKey(tx, selectedAddress, ticker); + const actionKey = await getActionKey(tx, selectedAddress, ticker, chainId); let transactionElement, transactionDetails; - if (tx.transaction.to === SWAPS_CONTRACT_ADDRESS || swapsTransactions[tx.id]) { + if (tx.transaction.to?.toLowerCase() === getSwapsContractAddress(chainId) || swapsTransactions[tx.id]) { const [transactionElement, transactionDetails] = decodeSwapsTx({ ...args, actionKey }); if (transactionElement && transactionDetails) return [transactionElement, transactionDetails]; } diff --git a/app/components/UI/TransactionReview/index.js b/app/components/UI/TransactionReview/index.js index c6d6461ef81..e8c7520ee74 100644 --- a/app/components/UI/TransactionReview/index.js +++ b/app/components/UI/TransactionReview/index.js @@ -124,6 +124,10 @@ class TransactionReview extends PureComponent { * Current provider ticker */ ticker: PropTypes.string, + /** + * Chain id + */ + chainId: PropTypes.string, /** * ETH or fiat, depending on user setting */ @@ -174,14 +178,15 @@ class TransactionReview extends PureComponent { validate, transaction, transaction: { data, to }, - tokens + tokens, + chainId } = this.props; let { showHexData } = this.props; let assetAmount, conversionRate, fiatValue; showHexData = showHexData || data; const approveTransaction = data && data.substr(0, 10) === APPROVE_FUNCTION_SIGNATURE; const error = validate && (await validate()); - const actionKey = await getTransactionReviewActionKey(transaction); + const actionKey = await getTransactionReviewActionKey(transaction, chainId); if (approveTransaction) { let contract = contractMap[safeToChecksumAddress(to)]; if (!contract) { @@ -358,6 +363,7 @@ const mapStateToProps = state => ({ contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, ticker: state.engine.backgroundState.NetworkController.provider.ticker, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, showHexData: state.settings.showHexData, transaction: getNormalizedTxState(state), browser: state.browser, diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js index f88c527139c..7fed702bfcf 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -2,13 +2,14 @@ import React, { PureComponent } from 'react'; import { ActivityIndicator, InteractionManager, View, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { swapsUtils } from '@metamask/swaps-controller/'; + import { colors } from '../../../styles/common'; import AssetOverview from '../../UI/AssetOverview'; import Transactions from '../../UI/Transactions'; import { getNetworkNavbarOptions } from '../../UI/Navbar'; import Engine from '../../../core/Engine'; import { safeToChecksumAddress } from '../../../util/address'; -import { SWAPS_CONTRACT_ADDRESS } from '@estebanmino/controllers/dist/swaps/SwapsUtil'; import { addAccountTimeFlagFilter } from '../../../util/transactions'; const styles = StyleSheet.create({ @@ -168,7 +169,8 @@ class Asset extends PureComponent { if (isTransfer) return this.navAddress === transferInformation.contractAddress.toLowerCase(); if ( swapsTransactions[tx.id] && - (to?.toLowerCase() === SWAPS_CONTRACT_ADDRESS || to?.toLowerCase() === this.navAddress) + (to?.toLowerCase() === swapsUtils.getSwapsContractAddress(chainId) || + to?.toLowerCase() === this.navAddress) ) { const { destinationToken, sourceToken } = swapsTransactions[tx.id]; return destinationToken.address === this.navAddress || sourceToken.address === this.navAddress; diff --git a/app/components/Views/SendFlow/Amount/index.js b/app/components/Views/SendFlow/Amount/index.js index f2f3e27e5db..6f64eb6bf7d 100644 --- a/app/components/Views/SendFlow/Amount/index.js +++ b/app/components/Views/SendFlow/Amount/index.js @@ -52,6 +52,7 @@ import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; import dismissKeyboard from 'react-native/Libraries/Utilities/dismissKeyboard'; import NetworkMainAssetLogo from '../../../UI/NetworkMainAssetLogo'; +import { isMainNet } from '../../../../util/networks'; const { hexToBN, BNToHex } = util; @@ -330,6 +331,10 @@ class Amount extends PureComponent { * An array that represents the user tokens */ tokens: PropTypes.array, + /** + * Chain Id + */ + chainId: PropTypes.string, /** * Current provider ticker */ @@ -664,7 +669,7 @@ class Amount extends PureComponent { }; onInputChange = (inputValue, selectedAsset, useMax) => { - const { contractExchangeRates, conversionRate, currentCurrency, ticker } = this.props; + const { contractExchangeRates, conversionRate, currentCurrency, chainId, ticker } = this.props; const { internalPrimaryCurrencyIsCrypto } = this.state; let inputValueConversion, renderableInputValueConversion, hasExchangeRate, comma; // Remove spaces from input @@ -678,7 +683,7 @@ class Amount extends PureComponent { const processedInputValue = isDecimal(inputValue) ? handleWeiNumber(inputValue) : '0'; selectedAsset = selectedAsset || this.props.selectedAsset; if (selectedAsset.isETH) { - hasExchangeRate = !!conversionRate; + hasExchangeRate = isMainNet(chainId) ? !!conversionRate : false; if (internalPrimaryCurrencyIsCrypto) { inputValueConversion = `${weiToFiatNumber(toWei(processedInputValue), conversionRate)}`; renderableInputValueConversion = `${weiToFiat( @@ -692,7 +697,7 @@ class Amount extends PureComponent { } } else { const exchangeRate = contractExchangeRates[selectedAsset.address]; - hasExchangeRate = !!exchangeRate; + hasExchangeRate = isMainNet(chainId) ? !!exchangeRate : false; // If !hasExchangeRate we have to handle crypto amount if (internalPrimaryCurrencyIsCrypto || !hasExchangeRate) { inputValueConversion = `${balanceToFiatNumber(processedInputValue, conversionRate, exchangeRate)}`; @@ -766,6 +771,7 @@ class Amount extends PureComponent { renderToken = (token, index) => { const { accounts, + chainId, selectedAddress, conversionRate, currentCurrency, @@ -776,11 +782,15 @@ class Amount extends PureComponent { const { address, decimals, symbol } = token; if (token.isETH) { balance = renderFromWei(accounts[selectedAddress].balance); - balanceFiat = weiToFiat(hexToBN(accounts[selectedAddress].balance), conversionRate, currentCurrency); + balanceFiat = isMainNet(chainId) + ? weiToFiat(hexToBN(accounts[selectedAddress].balance), conversionRate, currentCurrency) + : null; } else { balance = renderFromTokenMinimalUnit(contractBalances[address], decimals); const exchangeRate = contractExchangeRates[address]; - balanceFiat = balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency); + balanceFiat = isMainNet(chainId) + ? balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency) + : null; } return ( ({ providerType: state.engine.backgroundState.NetworkController.provider.type, primaryCurrency: state.settings.primaryCurrency, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, ticker: state.engine.backgroundState.NetworkController.provider.ticker, tokens: state.engine.backgroundState.AssetsController.tokens, transactionState: ownProps.transaction || state.transaction, diff --git a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap index c91387b3fb9..30049b78f87 100644 --- a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap @@ -101,34 +101,6 @@ exports[`Confirm should render correctly 1`] = ` underline={false} upper={false} /> - { const { transactionToName, selectedAsset, paymentRequest } = this.props.transactionState; - const { showHexData, showCustomNonce, primaryCurrency, network } = this.props; + const { showHexData, showCustomNonce, primaryCurrency, network, chainId } = this.props; const { nonce } = this.props.transaction; const { gasEstimationReady, @@ -958,7 +962,7 @@ class Confirm extends PureComponent { {transactionValue} - {transactionValueFiat} + {isMainNet(chainId) && {transactionValueFiat}} ) : ( @@ -979,7 +983,7 @@ class Confirm extends PureComponent { } fiat={transactionValueFiat} totalValue={transactionTotalAmount} transactionValue={transactionValue} @@ -1051,6 +1055,7 @@ const mapStateToProps = state => ({ providerType: state.engine.backgroundState.NetworkController.provider.type, showHexData: state.settings.showHexData, showCustomNonce: state.settings.showCustomNonce, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, ticker: state.engine.backgroundState.NetworkController.provider.ticker, keyrings: state.engine.backgroundState.KeyringController.keyrings, transaction: getNormalizedTxState(state), diff --git a/app/core/Engine.js b/app/core/Engine.js index 70508bbf8af..52d282d76f8 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -19,7 +19,7 @@ import { WalletDevice } from '@metamask/controllers'; -import { SwapsController } from '@estebanmino/controllers'; +import { SwapsController } from '@metamask/swaps-controller'; import AsyncStorage from '@react-native-community/async-storage'; @@ -154,7 +154,7 @@ class Engine { AccountTrackerController, AssetsContractController, AssetsDetectionController, - NetworkController: { provider }, + NetworkController: { provider, state: NetworkControllerState }, TransactionController, SwapsController } = this.datamodel.context; @@ -162,8 +162,10 @@ class Engine { provider.sendAsync = provider.sendAsync.bind(provider); AccountTrackerController.configure({ provider }); AssetsContractController.configure({ provider }); + SwapsController.configure({ provider, + chainId: NetworkControllerState?.provider?.chainId, pollCountLimit: AppConstants.SWAPS.POLL_COUNT_LIMIT, quotePollingInterval: AppConstants.SWAPS.POLLING_INTERVAL }); diff --git a/app/images/bnb-logo.png b/app/images/bnb-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3712a470d2f66663a8e6f987d0e9efab2baf83c9 GIT binary patch literal 6675 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 literal 0 HcmV?d00001 diff --git a/app/reducers/swaps/index.js b/app/reducers/swaps/index.js index 48569c4e948..5c3bbd139f7 100644 --- a/app/reducers/swaps/index.js +++ b/app/reducers/swaps/index.js @@ -6,7 +6,7 @@ export const SWAPS_SET_HAS_ONBOARDED = 'SWAPS_SET_HAS_ONBOARDED'; const MAX_TOKENS_WITH_BALANCE = 5; // * Action Creator -export const setSwapsLiveness = live => ({ type: SWAPS_SET_LIVENESS, payload: live }); +export const setSwapsLiveness = (live, chainId) => ({ type: SWAPS_SET_LIVENESS, payload: { live, chainId } }); export const setSwapsHasOnboarded = hasOnboarded => ({ type: SWAPS_SET_HAS_ONBOARDED, payload: hasOnboarded }); // * Selectors @@ -15,7 +15,10 @@ export const setSwapsHasOnboarded = hasOnboarded => ({ type: SWAPS_SET_HAS_ONBOA * Returns the swaps liveness state */ -export const swapsLivenessSelector = state => state.swaps.isLive; +export const swapsLivenessSelector = state => { + const chainId = state.engine.backgroundState.NetworkController.provider.chainId; + return state.swaps[chainId]?.isLive || false; +}; /** * Returns the swaps onboarded state @@ -106,16 +109,25 @@ export const swapsTopAssetsSelector = createSelector( // * Reducer export const initialState = { - isLive: true, - hasOnboarded: false + isLive: true, // TODO: should we remove it? + hasOnboarded: false, + + '1': { + isLive: true + } }; function swapsReducer(state = initialState, action) { switch (action.type) { case SWAPS_SET_LIVENESS: { + const { live, chainId } = action.payload; + const data = state[chainId]; return { ...state, - isLive: Boolean(action.payload) + [chainId]: { + ...data, + isLive: live + } }; } case SWAPS_SET_HAS_ONBOARDED: { diff --git a/app/reducers/swaps/swaps.test.js b/app/reducers/swaps/swaps.test.js index 8ad95806c35..94812cdeae6 100644 --- a/app/reducers/swaps/swaps.test.js +++ b/app/reducers/swaps/swaps.test.js @@ -10,10 +10,10 @@ describe('swaps reducer', () => { it('should set liveness', () => { const initalState = reducer(undefined, emptyAction); - const notLiveState = reducer(initalState, { type: SWAPS_SET_LIVENESS, payload: false }); - expect(notLiveState.isLive).toBe(false); - const liveState = reducer(initalState, { type: SWAPS_SET_LIVENESS, payload: true }); - expect(liveState.isLive).toBe(true); + const notLiveState = reducer(initalState, { type: SWAPS_SET_LIVENESS, payload: { live: false, chainId: 1 } }); + expect(notLiveState['1'].isLive).toBe(false); + const liveState = reducer(initalState, { type: SWAPS_SET_LIVENESS, payload: { live: true, chainId: 1 } }); + expect(liveState['1'].isLive).toBe(true); }); it('should set has onboarded', () => { diff --git a/app/util/transactions.js b/app/util/transactions.js index 8058191ef0f..7dbf5622087 100644 --- a/app/util/transactions.js +++ b/app/util/transactions.js @@ -5,7 +5,7 @@ import { strings } from '../../locales/i18n'; import contractMap from '@metamask/contract-metadata'; import { safeToChecksumAddress } from './address'; import { util } from '@metamask/controllers'; -import { swapsUtils } from '@estebanmino/controllers'; +import { swapsUtils } from '@metamask/swaps-controller'; import { hexToBN } from './number'; import AppConstants from '../core/AppConstants'; const { SAI_ADDRESS } = AppConstants; @@ -41,7 +41,7 @@ export const TRANSACTION_TYPES = { APPROVE: 'transaction_approve' }; -const { SWAPS_CONTRACT_ADDRESS } = swapsUtils; +const { getSwapsContractAddress } = swapsUtils; /** * Utility class with the single responsibility * of caching CollectibleAddresses @@ -250,12 +250,13 @@ export async function isCollectibleAddress(address, tokenId) { * Returns corresponding transaction action key * * @param {object} transaction - Transaction object + * @param {string} chainId - Current chainId * @returns {string} - Corresponding transaction action key */ -export async function getTransactionActionKey(transaction) { +export async function getTransactionActionKey(transaction, chainId) { const { transaction: { data, to } = {} } = transaction; if (!to) return CONTRACT_METHOD_DEPLOY; - if (to === SWAPS_CONTRACT_ADDRESS) return SWAPS_TRANSACTION_ACTION_KEY; + if (to === getSwapsContractAddress(chainId)) return SWAPS_TRANSACTION_ACTION_KEY; let ret; // if data in transaction try to get method data if (data && data !== '0x') { @@ -282,7 +283,7 @@ export async function getTransactionActionKey(transaction) { * @param {string} selectedAddress - Current account public address * @returns {string} - Transaction type message */ -export async function getActionKey(tx, selectedAddress, ticker) { +export async function getActionKey(tx, selectedAddress, ticker, chainId) { if (tx && tx.isTransfer) { const selfSent = safeToChecksumAddress(tx.transaction.from) === selectedAddress; const translationKey = selfSent ? 'transactions.self_sent_unit' : 'transactions.received_unit'; @@ -290,7 +291,7 @@ export async function getActionKey(tx, selectedAddress, ticker) { if (tx.transferInformation.contractAddress === SAI_ADDRESS.toLowerCase()) tx.transferInformation.symbol = 'SAI'; return strings(translationKey, { unit: tx.transferInformation.symbol }); } - const actionKey = await getTransactionActionKey(tx); + const actionKey = await getTransactionActionKey(tx, chainId); if (actionKey === SEND_ETHER_ACTION_KEY) { const incoming = safeToChecksumAddress(tx.transaction.to) === selectedAddress; const selfSent = incoming && safeToChecksumAddress(tx.transaction.from) === selectedAddress; @@ -319,10 +320,11 @@ export async function getActionKey(tx, selectedAddress, ticker) { * Returns corresponding transaction function type * * @param {object} tx - Transaction object + * @param {string} chainId - Current chainId * @returns {string} - Transaction function type */ -export async function getTransactionReviewActionKey(transaction) { - const actionKey = await getTransactionActionKey({ transaction }); +export async function getTransactionReviewActionKey(transaction, chainId) { + const actionKey = await getTransactionActionKey({ transaction }, chainId); const transactionReviewActionKey = reviewActionKeys[actionKey]; if (transactionReviewActionKey) { return transactionReviewActionKey; diff --git a/index.js b/index.js index e33a253cd25..f6f0e7e43aa 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ import './shim.js'; import 'react-native-gesture-handler'; +import 'react-native-url-polyfill/auto'; import crypto from 'crypto'; // eslint-disable-line import/no-nodejs-modules, no-unused-vars require('react-native-browser-polyfill'); // eslint-disable-line import/no-commonjs diff --git a/locales/languages/en.json b/locales/languages/en.json index 45bb14c9d99..0fcc69a1245 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1369,6 +1369,8 @@ "verify_on": "Always verify the token address on", "verify_address_on": "Verify token address on", "only_verified_on": "{{symbol}} is only verified on {{occurances}} source.", + "block_explorer": "block explorer", + "a_block_explorer": "a block explorer", "token_verification": "Token verification", "token_multiple": "Multiple tokens can use the same name and symbol.", "token_check": "Check", diff --git a/package.json b/package.json index e84ebdfab01..28f16272902 100644 --- a/package.json +++ b/package.json @@ -73,10 +73,11 @@ "react-native-level-fs/**/semver": "^4.3.2" }, "dependencies": { - "@estebanmino/controllers": "^3.3.17", "@exodus/react-native-payments": "https://github.com/wachunei/react-native-payments.git#package-json-hack", "@metamask/contract-metadata": "^1.23.0", "@metamask/controllers": "^7.0.0", + "@metamask/etherscan-link": "^2.0.0", + "@metamask/swaps-controller": "^2.0.1", "@react-native-community/async-storage": "1.12.1", "@react-native-community/blur": "^3.6.0", "@react-native-community/checkbox": "^0.4.2", @@ -178,6 +179,7 @@ "react-native-svg": "12.1.0", "react-native-swipe-gestures": "1.0.3", "react-native-tcp": "aprock/react-native-tcp#11/head", + "react-native-url-polyfill": "^1.3.0", "react-native-v8": "^0.62.2-patch.1", "react-native-vector-icons": "6.4.2", "react-native-view-shot": "^3.1.2", diff --git a/yarn.lock b/yarn.lock index 4ff283b7705..56336bfd712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -871,35 +871,6 @@ dependencies: "@types/hammerjs" "^2.0.36" -"@estebanmino/controllers@^3.3.17": - version "3.3.17" - resolved "https://registry.yarnpkg.com/@estebanmino/controllers/-/controllers-3.3.17.tgz#22f06daf2b5a004bcf40a12f905699263276159f" - integrity sha512-tmONppQxqLOW7uZSSk9gUAiNokU1tW2LezPggjZjzOp9CTBIc3cgr28o07hss7DF4+8IX6XOEtAjfeVgUUCQ2Q== - dependencies: - "@metamask/contract-metadata" "^1.22.0" - abort-controller "^3.0.0" - async-mutex "^0.3.1" - bignumber.js "^9.0.1" - eth-ens-namehash "^2.0.8" - eth-json-rpc-infura "^5.1.0" - eth-keyring-controller "^6.1.0" - eth-method-registry "1.1.0" - eth-phishing-detect "^1.1.13" - eth-query "^2.1.2" - eth-rpc-errors "^4.0.0" - eth-sig-util "^3.0.0" - ethereumjs-util "^6.1.0" - ethereumjs-wallet "^1.0.1" - 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" - single-call-balance-checker-abi "^1.0.0" - uuid "^8.3.2" - web3 "^0.20.7" - web3-provider-engine "^16.0.1" - "@ethersproject/abi@^5.0.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.5.tgz#6e7bbf9d014791334233ba18da85331327354aa1" @@ -1548,12 +1519,7 @@ dependencies: "@json-rpc-tools/types" "^1.5.7" -"@metamask/contract-metadata@^1.22.0": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.22.0.tgz#55cc84756c703c433176b484b1d34f0e03d16d1e" - integrity sha512-t4ijbU+4OH9UAlrPkfLPFo6KmkRTRZJHB+Vly4ajF8oZMnota5YjVVl/SmltsoRC9xvJtRn9DUVf3YMHMIdofw== - -"@metamask/contract-metadata@^1.23.0": +"@metamask/contract-metadata@^1.22.0", "@metamask/contract-metadata@^1.23.0": version "1.23.0" resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.23.0.tgz#c70be7f3eaeeb791651ce793b7cdc230e9780b18" integrity sha512-oTUqL9dtXtbng60DZMRsBmZ5HiOUUfEsZjuswOJ0yHO24YsW0ktCcgCJVYPv1HcOsF0SVrRtG4rtrvOl4nY+HA== @@ -1590,6 +1556,11 @@ web3 "^0.20.7" web3-provider-engine "^16.0.1" +"@metamask/etherscan-link@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.0.0.tgz#89035736515a39532ba1142d87b9a8c2b4f920f1" + integrity sha512-/YS32hS2UTTxs0KyUmAgaDj1w4dzAvOrT+p4TJtpICeH3E/k51r2FO0Or7WJJI/mpzTqNKgcH5yyS2oCtupGiA== + "@metamask/mobile-provider@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@metamask/mobile-provider/-/mobile-provider-2.0.1.tgz#892f883deafe49200a3ae57d85237016ded63c12" @@ -1600,6 +1571,35 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q== +"@metamask/swaps-controller@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@metamask/swaps-controller/-/swaps-controller-2.0.1.tgz#b75aad7ec4c6a3d97d2d869564035561d13b0938" + integrity sha512-LEyNpQVF/0++tWu0yRJn2FJ+0ADmvnaNdkjUQef9rlOoFUCsN4OMFYivYnA5pe//VM0SCWXxMUP5gY+oYdjIVg== + dependencies: + "@metamask/contract-metadata" "^1.22.0" + abort-controller "^3.0.0" + async-mutex "^0.3.1" + bignumber.js "^9.0.1" + eth-ens-namehash "^2.0.8" + eth-json-rpc-infura "^5.1.0" + eth-keyring-controller "^6.1.0" + eth-method-registry "1.1.0" + eth-phishing-detect "^1.1.13" + eth-query "^2.1.2" + eth-rpc-errors "^4.0.0" + eth-sig-util "^3.0.0" + ethereumjs-util "^6.1.0" + ethereumjs-wallet "^1.0.1" + 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" + single-call-balance-checker-abi "^1.0.0" + uuid "^8.3.2" + web3 "^0.20.7" + web3-provider-engine "^16.0.1" + "@pedrouid/iso-crypto@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@pedrouid/iso-crypto/-/iso-crypto-1.0.0.tgz#cf06b40ef3da3d7ca7363bd7a521ed59fa2fd13d" @@ -11131,6 +11131,13 @@ react-native-tcp@aprock/react-native-tcp#11/head: process "^0.11.9" util "^0.12.1" +react-native-url-polyfill@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a" + integrity sha512-w9JfSkvpqqlix9UjDvJjm1EjSt652zVQ6iwCIj1cVVkwXf4jQhQgTNXY6EVTwuAmUjg6BC6k9RHCBynoLFo3IQ== + dependencies: + whatwg-url-without-unicode "8.0.0-3" + react-native-v8@^0.62.2-patch.1: version "0.62.2-patch.1" resolved "https://registry.yarnpkg.com/react-native-v8/-/react-native-v8-0.62.2-patch.1.tgz#016a932ed5e60f6bca6803fbdf6c746fe1b55bf5" @@ -12973,11 +12980,16 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@^2.0.0, tslib@^2.1.0: +tslib@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" + integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -13396,6 +13408,11 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -13423,6 +13440,15 @@ whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url-without-unicode@8.0.0-3: + version "8.0.0-3" + resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b" + integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig== + dependencies: + buffer "^5.4.3" + punycode "^2.1.1" + webidl-conversions "^5.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -13911,4 +13937,4 @@ yargs@^3.30.0: zxcvbn@4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" - integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA= \ No newline at end of file + integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA= From 59fa84a31b3a0312ce0c7fd853ee69dc859f4bd4 Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Fri, 23 Apr 2021 11:59:07 -0400 Subject: [PATCH 44/51] Swaps: Add cache thresholds configuration (#2514) --- app/core/AppConstants.js | 5 ++++- app/core/Engine.js | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/core/AppConstants.js b/app/core/AppConstants.js index 8e7a51578fd..d0341617c51 100644 --- a/app/core/AppConstants.js +++ b/app/core/AppConstants.js @@ -61,7 +61,10 @@ export default { CLIENT_ID: 'mobile', LIVENESS_POLLING_FREQUENCY: 5 * 60 * 1000, POLL_COUNT_LIMIT: 3, - DEFAULT_SLIPPAGE: 3 + DEFAULT_SLIPPAGE: 3, + CACHE_AGGREGATOR_METADATA_THRESHOLD: 5 * 60 * 1000, + CACHE_TOKENS_THRESHOLD: 5 * 60 * 1000, + CACHE_TOP_ASSETS_THRESHOLD: 5 * 60 * 1000 }, MAX_SAFE_CHAIN_ID: 4503599627370476, URLS: { diff --git a/app/core/Engine.js b/app/core/Engine.js index 52d282d76f8..7062dfc9127 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -119,7 +119,12 @@ class Engine { new TokenRatesController(), new TransactionController(), new TypedMessageManager(), - new SwapsController({ clientId: AppConstants.SWAPS.CLIENT_ID }) + new SwapsController({ + clientId: AppConstants.SWAPS.CLIENT_ID, + fetchAggregatorMetadataThreshold: AppConstants.SWAPS.CACHE_AGGREGATOR_METADATA_THRESHOLD, + fetchTokensThreshold: AppConstants.SWAPS.CACHE_TOKENS_THRESHOLD, + fetchTopAssetsThreshold: AppConstants.SWAPS.CACHE_TOP_ASSETS_THRESHOLD + }) ], initialState ); From e7b31d63bb33f8b612caaaffd5616642724f09a4 Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Mon, 26 Apr 2021 10:57:05 -0400 Subject: [PATCH 45/51] Upgrade .nvmrc to node v14 (#2588) --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index e338b86593f..958b5a36e1f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10 +v14 From b4c82e6d7531e5155016c0e9f9771db87af54d78 Mon Sep 17 00:00:00 2001 From: ricky Date: Mon, 26 Apr 2021 11:10:36 -0400 Subject: [PATCH 46/51] Address yarn lints (#2524) * address yarn lints * add eslint-disable * Update isENS method * fix rn-fetch-blob.js mock * Add tests for isENS * useRef instead of useMemo * Update eslint * Use lastIndexOf and add test * Add test case for ricky.metamask.eth * Add offset * Fix AppConstants import --- .eslintrc.js | 1 + app/__mocks__/rn-fetch-blob.js | 1 + app/components/UI/SignatureRequest/index.js | 5 +- app/components/Views/Entry/index.js | 2 +- app/util/address.js | 13 +- app/util/address.test.js | 19 ++ package.json | 2 +- yarn.lock | 312 +++++++++++++++----- 8 files changed, 267 insertions(+), 88 deletions(-) create mode 100644 app/util/address.test.js diff --git a/.eslintrc.js b/.eslintrc.js index bcf2b8df859..2b876e8b391 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-commonjs module.exports = { root: true, parser: 'babel-eslint', diff --git a/app/__mocks__/rn-fetch-blob.js b/app/__mocks__/rn-fetch-blob.js index 2e08193f590..efdd761092c 100644 --- a/app/__mocks__/rn-fetch-blob.js +++ b/app/__mocks__/rn-fetch-blob.js @@ -9,6 +9,7 @@ export default { config: noop, session: noop, fs: { + writeFile: () => Promise.resolve(), exists: () => Promise.resolve(), dirs: { CacheDir: noop, diff --git a/app/components/UI/SignatureRequest/index.js b/app/components/UI/SignatureRequest/index.js index fcf6b7551c1..c9b77ff7edc 100644 --- a/app/components/UI/SignatureRequest/index.js +++ b/app/components/UI/SignatureRequest/index.js @@ -232,8 +232,9 @@ class SignatureRequest extends PureComponent { let expandedHeight; if (Device.isMediumDevice()) { expandedHeight = styles.expandedHeight2; - } else if (type === 'ethSign' && Device.isMediumDevice()) { - expandedHeight = styles.expandedHeight1; + if (type === 'ethSign') { + expandedHeight = styles.expandedHeight1; + } } return ( diff --git a/app/components/Views/Entry/index.js b/app/components/Views/Entry/index.js index e458b8f5827..a5c03710ad4 100644 --- a/app/components/Views/Entry/index.js +++ b/app/components/Views/Entry/index.js @@ -76,7 +76,7 @@ const Entry = props => { const animation = useRef(null); const animationName = useRef(null); - const opacity = new Animated.Value(1); + const opacity = useRef(new Animated.Value(1)).current; const onAnimationFinished = useCallback(() => { Animated.timing(opacity, { diff --git a/app/util/address.js b/app/util/address.js index ae4e5d2e9cb..d7ae893516c 100644 --- a/app/util/address.js +++ b/app/util/address.js @@ -2,6 +2,9 @@ import { toChecksumAddress } from 'ethereumjs-util'; import Engine from '../core/Engine'; import AppConstants from '../core/AppConstants'; import { strings } from '../../locales/i18n'; +import { tlc } from '../util/general'; + +const { supportedTLDs } = AppConstants; /** * Returns full checksummed address @@ -67,11 +70,11 @@ export async function importAccountFromPrivateKey(private_key) { * @returns {boolean} - Returns a boolean indicating if it is valid */ export function isENS(name) { - const rec = name && name.split('.'); - if (!rec || rec.length === 1 || !AppConstants.supportedTLDs.includes(rec[rec.length - 1].toLowerCase())) { - return false; - } - return true; + const OFFSET = 1; + const index = name && name.lastIndexOf('.'); + const tld = index && index >= OFFSET && tlc(name.substr(index + OFFSET, name.length - OFFSET)); + if (index && tld && supportedTLDs.includes(tld)) return true; + return false; } /** diff --git a/app/util/address.test.js b/app/util/address.test.js new file mode 100644 index 00000000000..6d23dc837f6 --- /dev/null +++ b/app/util/address.test.js @@ -0,0 +1,19 @@ +import { isENS } from './address'; + +describe('isENS', () => { + it('should return false by default', () => { + expect(isENS()).toBe(false); + }); + it('should return false for normal domain', () => { + expect(isENS('ricky.codes')).toBe(false); + }); + it('should return true for ens', () => { + expect(isENS('rickycodes.eth')).toBe(true); + }); + it('should return true for ens', () => { + expect(isENS('ricky.eth.eth')).toBe(true); + }); + it('should return true for ens', () => { + expect(isENS('ricky.metamask.eth')).toBe(true); + }); +}); diff --git a/package.json b/package.json index 28f16272902..7d71ad90d16 100644 --- a/package.json +++ b/package.json @@ -219,7 +219,7 @@ "enzyme": "3.9.0", "enzyme-adapter-react-16": "1.10.0", "enzyme-to-json": "3.3.5", - "eslint": "^6.5.1", + "eslint": "^7.14.0", "eslint-config-react-native": "4.0.0", "eslint-plugin-import": "2.18.2", "eslint-plugin-prettier": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index 56336bfd712..a38aacaca60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.10.4": +"@babel/code-frame@7.12.11", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.10.4": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== @@ -871,6 +871,21 @@ dependencies: "@types/hammerjs" "^2.0.36" +"@eslint/eslintrc@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" + integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@ethersproject/abi@^5.0.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.5.tgz#6e7bbf9d014791334233ba18da85331327354aa1" @@ -2298,7 +2313,7 @@ acorn-jsx@^5.0.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== -acorn-jsx@^5.2.0: +acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -2318,7 +2333,7 @@ acorn@^7.1.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== -acorn@^7.1.1: +acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -2395,6 +2410,26 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.1.0.tgz#45d5d3d36c7cdd808930cc3e603cf6200dbeb736" + integrity sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.9" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" @@ -3617,11 +3652,6 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - cliui@^3.0.3: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -4026,7 +4056,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -4230,7 +4260,7 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -4584,7 +4614,7 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enquirer@^2.3.6: +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -4990,14 +5020,22 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.3.1, eslint-utils@^1.4.3: +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^1.3.1: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: eslint-visitor-keys "^1.1.0" -eslint-utils@^2.0.0: +eslint-utils@^2.0.0, eslint-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== @@ -5009,6 +5047,16 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== +eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + eslint@^5.6.0: version "5.16.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" @@ -5051,46 +5099,46 @@ eslint@^5.6.0: table "^5.2.3" text-table "^0.2.0" -eslint@^6.5.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== +eslint@^7.14.0: + version "7.24.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.24.0.tgz#2e44fa62d93892bfdb100521f17345ba54b8513a" + integrity sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ== dependencies: - "@babel/code-frame" "^7.0.0" + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.0" ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" + chalk "^4.0.0" + cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" - globals "^12.1.0" + globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^7.0.0" is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" + levn "^0.4.1" + lodash "^4.17.21" minimatch "^3.0.4" - mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.3" + optionator "^0.9.1" progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.4" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -5103,14 +5151,14 @@ espree@^5.0.1: acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" -espree@^6.1.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" - integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== dependencies: - acorn "^7.1.1" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.1.0" + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" esprima@3.x.x: version "3.1.3" @@ -5129,6 +5177,13 @@ esquery@^1.0.1: dependencies: estraverse "^5.1.0" +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" @@ -5136,6 +5191,13 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" @@ -5146,6 +5208,11 @@ estraverse@^5.1.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -6147,7 +6214,7 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0, figures@^3.2.0: +figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== @@ -6161,6 +6228,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + file-uri-to-path@1, file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -6249,11 +6323,24 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + follow-redirects@^1.10.0: version "1.13.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" @@ -6589,6 +6676,13 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" +globals@^13.6.0: + version "13.8.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" + integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q== + dependencies: + type-fest "^0.20.2" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -7047,25 +7141,6 @@ inquirer@^6.2.0, inquirer@^6.2.2: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - internal-slot@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" @@ -8146,6 +8221,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -8460,6 +8540,14 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lil-uuid@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/lil-uuid/-/lil-uuid-0.1.1.tgz#f9edcf23f00e42bf43f0f843d98d8b53f3341f16" @@ -8544,11 +8632,21 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -8579,6 +8677,11 @@ lodash.toarray@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + lodash@4.x.x, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" @@ -8589,6 +8692,11 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -9464,7 +9572,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -mute-stream@0.0.8, mute-stream@~0.0.4: +mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== @@ -9915,7 +10023,7 @@ opn@^5.4.0: dependencies: is-wsl "^1.1.0" -optionator@^0.8.1, optionator@^0.8.2, optionator@^0.8.3: +optionator@^0.8.1, optionator@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== @@ -9927,6 +10035,18 @@ optionator@^0.8.1, optionator@^0.8.2, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" @@ -10400,6 +10520,11 @@ precond@0.2: resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" integrity sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw= +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -11478,7 +11603,7 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpp@^3.0.0: +regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== @@ -11586,6 +11711,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -11690,7 +11820,7 @@ rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -11767,7 +11897,7 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -run-async@^2.2.0, run-async@^2.4.0: +run-async@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== @@ -11813,13 +11943,6 @@ rxjs@^5.4.3: dependencies: symbol-observable "1.0.1" -rxjs@^6.6.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== - dependencies: - tslib "^1.9.0" - rxjs@^6.6.6: version "6.6.6" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70" @@ -11992,11 +12115,18 @@ semver@^4.3.2, semver@~2.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" integrity sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto= -semver@^6.0.0, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.2.1: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + semver@^7.3.2: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" @@ -12659,7 +12789,7 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strip-json-comments@^3.0.1: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -12752,6 +12882,18 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +table@^6.0.4: + version "6.4.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.4.0.tgz#9501324358c313162cf52b2843a8b221e75fbefc" + integrity sha512-/Vfr23BDjJT2kfsCmYtnJqEPdD/8Dh/MDIQxfcbe+09lZUel6gluquwdMTrLERBw623Nv34DLGZ11krWn5AAqw== + dependencies: + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.flatten "^4.4.0" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.0" + tail@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/tail/-/tail-2.0.3.tgz#37567adc4624a70b35f1d146c3376fa3d6ef7c04" @@ -13019,6 +13161,13 @@ tweetnacl@^1.0.0: 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" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -13036,6 +13185,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -13552,7 +13706,7 @@ winston@0.8.x: pkginfo "0.3.x" stack-trace "0.0.x" -word-wrap@~1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== From 57ba91592a3c327ba139c8cb08e491b9d0665965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Mon, 26 Apr 2021 14:47:56 -0400 Subject: [PATCH 47/51] only add custom tokens if not in mainnet (#2470) * checkchainid * tests --- app/components/Views/AddAsset/index.js | 28 +++++++++++++++------ app/components/Views/AddAsset/index.test.js | 21 ++++++++++++++-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/components/Views/AddAsset/index.js b/app/components/Views/AddAsset/index.js index 62e03c44e88..2f9d425e8f1 100644 --- a/app/components/Views/AddAsset/index.js +++ b/app/components/Views/AddAsset/index.js @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; import { SafeAreaView, StyleSheet } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; +import { connect } from 'react-redux'; import DefaultTabBar from 'react-native-scrollable-tab-view/DefaultTabBar'; import AddCustomToken from '../../UI/AddCustomToken'; import SearchTokenAutocomplete from '../../UI/SearchTokenAutocomplete'; @@ -9,6 +10,7 @@ import PropTypes from 'prop-types'; import { strings } from '../../../../locales/i18n'; import AddCustomCollectible from '../../UI/AddCustomCollectible'; import { getNetworkNavbarOptions } from '../../UI/Navbar'; +import { NetworksChainId } from '@metamask/controllers'; const styles = StyleSheet.create({ wrapper: { @@ -32,7 +34,7 @@ const styles = StyleSheet.create({ /** * PureComponent that provides ability to add assets. */ -export default class AddAsset extends PureComponent { +class AddAsset extends PureComponent { static navigationOptions = ({ navigation }) => getNetworkNavbarOptions('add_asset.title', true, navigation); state = { @@ -45,7 +47,11 @@ export default class AddAsset extends PureComponent { /** /* navigation object required to push new views */ - navigation: PropTypes.object + navigation: PropTypes.object, + /** + * Chain id + */ + chainId: PropTypes.string }; renderTabBar() { @@ -74,11 +80,13 @@ export default class AddAsset extends PureComponent { {assetType === 'token' ? ( - + {NetworksChainId.mainnet === this.props.chainId && ( + + )} ({ + chainId: state.engine.backgroundState.NetworkController.provider.chainId +}); + +export default connect(mapStateToProps)(AddAsset); diff --git a/app/components/Views/AddAsset/index.test.js b/app/components/Views/AddAsset/index.test.js index 310a5389180..a3e33019bdd 100644 --- a/app/components/Views/AddAsset/index.test.js +++ b/app/components/Views/AddAsset/index.test.js @@ -1,10 +1,27 @@ import React from 'react'; +import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import AddAsset from './'; +const mockStore = configureMockStore(); + describe('AddAsset', () => { it('should render correctly', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const initialState = { + engine: { + backgroundState: { + NetworkController: { + provider: { + chainId: '1' + } + } + } + } + }; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); }); }); From 3662274c74a9592b8871d4b2b5b84c46aea87295 Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Tue, 27 Apr 2021 13:11:49 -0400 Subject: [PATCH 48/51] Fix adding custom token in custom network (#2590) --- app/components/UI/AddCustomToken/index.js | 4 +++- app/util/transactions.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/components/UI/AddCustomToken/index.js b/app/components/UI/AddCustomToken/index.js index 1e6a08cd346..d1e01408d02 100644 --- a/app/components/UI/AddCustomToken/index.js +++ b/app/components/UI/AddCustomToken/index.js @@ -125,7 +125,9 @@ export default class AddCustomToken extends PureComponent { let validated = true; const address = this.state.address; const isValidTokenAddress = isValidAddress(address); - const toSmartContract = isValidTokenAddress && (await isSmartContractAddress(address)); + const { NetworkController } = Engine.context; + const { chainId } = NetworkController?.state?.provider || {}; + const toSmartContract = isValidTokenAddress && (await isSmartContractAddress(address, chainId)); if (address.length === 0) { this.setState({ warningAddress: strings('token.address_cant_be_empty') }); validated = false; diff --git a/app/util/transactions.js b/app/util/transactions.js index 7dbf5622087..42d5aa9815d 100644 --- a/app/util/transactions.js +++ b/app/util/transactions.js @@ -8,6 +8,7 @@ import { util } from '@metamask/controllers'; import { swapsUtils } from '@metamask/swaps-controller'; import { hexToBN } from './number'; import AppConstants from '../core/AppConstants'; +import { isMainnetByChainId } from './networks'; const { SAI_ADDRESS } = AppConstants; export const TOKEN_METHOD_TRANSFER = 'transfer'; @@ -210,13 +211,14 @@ export async function getMethodData(data) { * Returns wether the given address is a contract * * @param {string} address - Ethereum address + * @param {string} chainId - Current chainId * @returns {boolean} - Whether the given address is a contract */ -export async function isSmartContractAddress(address) { +export async function isSmartContractAddress(address, chainId) { if (!address) return false; address = toChecksumAddress(address); // If in contract map we don't need to cache it - if (contractMap[address]) { + if (isMainnetByChainId(chainId) && contractMap[address]) { return Promise.resolve(true); } const { TransactionController } = Engine.context; From c30bc3d28ff836fca643c537d80369b8abc2a3a9 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 28 Apr 2021 15:02:45 -0230 Subject: [PATCH 49/51] Replace controller context (#2416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace controller context The `context` object previously constructed by the `ComposableController` is no more. Instead each controller now accepts its dependencies directly as constructor parameters, in a similar manner to the extension controllers. This was done in preparation for migrating to BaseControllerV2 and the new controller messaging system - this is just a temporary solution that will let us migrate controllers one at a time. The style of dependency injection here matches the extension (at least with newer controllers anyway). Specific methods and state snapshots are injected rather than entire controllers, to help simplify unit tests and make it easier to understand how controllers interact. The `Engine.context` property was used throughout mobile, so it has been preserved. It is now constructed explicitly, rather than being a re-export of the `ComposableController` context. This PR depends upon https://github.com/MetaMask/controllers/pull/387 * Pass in function for `getOpenSeaApiKey` rather than string The API key was passed in directly by accident, instead of a function get returned the key. This has been fixed. Co-authored-by: Esteban Miño * Update `AccountTrackerController` options The `AccountTrackerController` option `initialIdentities` was replaced with `getIdentities`. The initial identities passed in here were incorrect anyway due to a typo (`initialState.preferencesController` was used instead of `initialState.PreferencesController`). * Fix `getIdentities` handler for `AccountTrackerController` * Set initial controller state The `controllers` setter on `ComposedController` used to be responsible for setting initial state. Since that setter has been removed, the initial state is now set after the controllers have been constructed. This should be functionally equivalent to what it was before. We're setting the initial state by calling `update` on each controller, just as the `controller` setter used to. * Fix initial state variable reference Co-authored-by: Esteban Miño --- app/core/Engine.js | 201 ++++++++++++++++++++++++++-------------- app/core/Engine.test.js | 28 +++--- package.json | 2 +- yarn.lock | 15 ++- 4 files changed, 154 insertions(+), 92 deletions(-) diff --git a/app/core/Engine.js b/app/core/Engine.js index 7062dfc9127..2810a0ca719 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -65,76 +65,138 @@ class Engine { currentCurrency: 'usd' }; - this.datamodel = new ComposableController( - [ - new KeyringController({ encryptor }, initialState.KeyringController), - new AccountTrackerController(), - new AddressBookController(), - new AssetsContractController(), - new AssetsController(), - new AssetsDetectionController(), - new CurrencyRateController({ - nativeCurrency, - currentCurrency - }), - new PersonalMessageManager(), - new MessageManager(), - new NetworkController({ - infuraProjectId: process.env.MM_INFURA_PROJECT_ID || NON_EMPTY, - providerConfig: { - static: { - eth_sendTransaction: async (payload, next, end) => { - const { TransactionController } = this.datamodel.context; - try { - const hash = await (await TransactionController.addTransaction( - payload.params[0], - payload.origin, - WalletDevice.MM_MOBILE - )).result; - end(undefined, hash); - } catch (error) { - end(error); - } - } - }, - getAccounts: (end, payload) => { - const { approvedHosts, privacyMode } = store.getState(); - const isEnabled = !privacyMode || approvedHosts[payload.hostname]; - const { KeyringController } = this.datamodel.context; - const isUnlocked = KeyringController.isUnlocked(); - const selectedAddress = this.datamodel.context.PreferencesController.state - .selectedAddress; - end(null, isUnlocked && isEnabled && selectedAddress ? [selectedAddress] : []); + const preferencesController = new PreferencesController( + {}, + { + ipfsGateway: AppConstants.IPFS_DEFAULT_GATEWAY_URL + } + ); + const networkController = new NetworkController({ + infuraProjectId: process.env.MM_INFURA_PROJECT_ID || NON_EMPTY, + providerConfig: { + static: { + eth_sendTransaction: async (payload, next, end) => { + const { TransactionController } = this.context; + try { + const hash = await (await TransactionController.addTransaction( + payload.params[0], + payload.origin, + WalletDevice.MM_MOBILE + )).result; + end(undefined, hash); + } catch (error) { + end(error); } } - }), - new PhishingController(), - new PreferencesController( - {}, - { - ipfsGateway: AppConstants.IPFS_DEFAULT_GATEWAY_URL - } + }, + getAccounts: (end, payload) => { + const { approvedHosts, privacyMode } = store.getState(); + const isEnabled = !privacyMode || approvedHosts[payload.hostname]; + const { KeyringController } = this.context; + const isUnlocked = KeyringController.isUnlocked(); + const selectedAddress = this.context.PreferencesController.state.selectedAddress; + end(null, isUnlocked && isEnabled && selectedAddress ? [selectedAddress] : []); + } + } + }); + const assetsContractController = new AssetsContractController(); + const assetsController = new AssetsController({ + onPreferencesStateChange: listener => preferencesController.subscribe(listener), + onNetworkStateChange: listener => networkController.subscribe(listener), + getAssetName: assetsContractController.getAssetName.bind(assetsContractController), + getAssetSymbol: assetsContractController.getAssetSymbol.bind(assetsContractController), + getCollectibleTokenURI: assetsContractController.getCollectibleTokenURI.bind(assetsContractController) + }); + const currencyRateController = new CurrencyRateController({ + nativeCurrency, + currentCurrency + }); + + const controllers = [ + new KeyringController( + { + removeIdentity: preferencesController.removeIdentity.bind(preferencesController), + syncIdentities: preferencesController.syncIdentities.bind(preferencesController), + updateIdentities: preferencesController.updateIdentities.bind(preferencesController), + setSelectedAddress: preferencesController.setSelectedAddress.bind(preferencesController) + }, + { encryptor }, + initialState.KeyringController + ), + new AccountTrackerController({ + onPreferencesStateChange: listener => preferencesController.subscribe(listener), + getIdentities: () => preferencesController.state.identities + }), + new AddressBookController(), + assetsContractController, + assetsController, + new AssetsDetectionController({ + onAssetsStateChange: listener => assetsController.subscribe(listener), + onPreferencesStateChange: listener => preferencesController.subscribe(listener), + onNetworkStateChange: listener => networkController.subscribe(listener), + getOpenSeaApiKey: () => assetsController.openSeaApiKey, + getBalancesInSingleCall: assetsContractController.getBalancesInSingleCall.bind( + assetsContractController ), - new TokenBalancesController({ interval: 10000 }), - new TokenRatesController(), - new TransactionController(), - new TypedMessageManager(), - new SwapsController({ - clientId: AppConstants.SWAPS.CLIENT_ID, - fetchAggregatorMetadataThreshold: AppConstants.SWAPS.CACHE_AGGREGATOR_METADATA_THRESHOLD, - fetchTokensThreshold: AppConstants.SWAPS.CACHE_TOKENS_THRESHOLD, - fetchTopAssetsThreshold: AppConstants.SWAPS.CACHE_TOP_ASSETS_THRESHOLD - }) - ], - initialState - ); + addTokens: assetsController.addTokens.bind(assetsController), + addCollectible: assetsController.addCollectible.bind(assetsController), + removeCollectible: assetsController.removeCollectible.bind(assetsController), + getAssetsState: () => assetsController.state + }), + currencyRateController, + new PersonalMessageManager(), + new MessageManager(), + networkController, + new PhishingController(), + preferencesController, + new TokenBalancesController( + { + onAssetsStateChange: listener => assetsController.subscribe(listener), + getSelectedAddress: () => preferencesController.state.selectedAddress, + getBalanceOf: assetsContractController.getBalanceOf.bind(assetsContractController) + }, + { interval: 10000 } + ), + new TokenRatesController({ + onAssetsStateChange: listener => assetsController.subscribe(listener), + onCurrencyRateStateChange: listener => currencyRateController.subscribe(listener) + }), + new TransactionController({ + getNetworkState: () => networkController.state, + onNetworkStateChange: listener => networkController.subscribe(listener), + getProvider: () => networkController.provider + }), + new TypedMessageManager(), + new SwapsController({ + clientId: AppConstants.SWAPS.CLIENT_ID, + fetchAggregatorMetadataThreshold: AppConstants.SWAPS.CACHE_AGGREGATOR_METADATA_THRESHOLD, + fetchTokensThreshold: AppConstants.SWAPS.CACHE_TOKENS_THRESHOLD, + fetchTopAssetsThreshold: AppConstants.SWAPS.CACHE_TOP_ASSETS_THRESHOLD + }) + ]; + + // set initial state + // TODO: Pass initial state into each controller constructor instead + // This is being set post-construction for now to ensure it's functionally equivalent with + // how the `ComponsedController` used to set initial state. + for (const controller of controllers) { + if (initialState[controller.name]) { + controller.update(initialState[controller.name]); + } + } + + this.datamodel = new ComposableController(controllers, initialState); + this.context = controllers.reduce((context, controller) => { + context[controller.name] = controller; + return context; + }, {}); const { AssetsController: assets, KeyringController: keyring, NetworkController: network, TransactionController: transaction - } = this.datamodel.context; + } = this.context; assets.setApiKey(process.env.MM_OPENSEA_KEY); network.refreshNetwork(); @@ -162,7 +224,7 @@ class Engine { NetworkController: { provider, state: NetworkControllerState }, TransactionController, SwapsController - } = this.datamodel.context; + } = this.context; provider.sendAsync = provider.sendAsync.bind(provider); AccountTrackerController.configure({ provider }); @@ -181,7 +243,7 @@ class Engine { } refreshTransactionHistory = async forceCheck => { - const { TransactionController, PreferencesController, NetworkController } = this.datamodel.context; + const { TransactionController, PreferencesController, NetworkController } = this.context; const { selectedAddress } = PreferencesController.state; const { type: networkType } = NetworkController.state.provider; const { networkId } = Networks[networkType]; @@ -242,7 +304,7 @@ class Engine { AssetsController, TokenBalancesController, TokenRatesController - } = this.datamodel.context; + } = this.context; const { selectedAddress } = PreferencesController.state; const { conversionRate, currentCurrency } = CurrencyRateController.state; const { accounts } = AccountTrackerController.state; @@ -308,12 +370,7 @@ class Engine { // Whenever we are gonna start a new wallet // either imported or created, we need to // get rid of the old data from state - const { - TransactionController, - AssetsController, - TokenBalancesController, - TokenRatesController - } = this.datamodel.context; + const { TransactionController, AssetsController, TokenBalancesController, TokenRatesController } = this.context; //Clear assets info AssetsController.update({ @@ -346,7 +403,7 @@ class Engine { NetworkController, TransactionController, AssetsController - } = this.datamodel.context; + } = this.context; // Select same network ? await NetworkController.setProviderType(network.provider.type); @@ -434,7 +491,7 @@ let instance; export default { get context() { - return instance && instance.datamodel && instance.datamodel.context; + return instance && instance.context; }, get state() { const { diff --git a/app/core/Engine.test.js b/app/core/Engine.test.js index 8265d38a60f..a400440d10a 100644 --- a/app/core/Engine.test.js +++ b/app/core/Engine.test.js @@ -2,19 +2,19 @@ import Engine from './Engine'; describe('Engine', () => { it('should expose an API', () => { const engine = Engine.init({}); - expect(engine.datamodel.context).toHaveProperty('AccountTrackerController'); - expect(engine.datamodel.context).toHaveProperty('AddressBookController'); - expect(engine.datamodel.context).toHaveProperty('AssetsContractController'); - expect(engine.datamodel.context).toHaveProperty('AssetsController'); - expect(engine.datamodel.context).toHaveProperty('AssetsDetectionController'); - expect(engine.datamodel.context).toHaveProperty('CurrencyRateController'); - expect(engine.datamodel.context).toHaveProperty('KeyringController'); - expect(engine.datamodel.context).toHaveProperty('NetworkController'); - expect(engine.datamodel.context).toHaveProperty('PersonalMessageManager'); - expect(engine.datamodel.context).toHaveProperty('PhishingController'); - expect(engine.datamodel.context).toHaveProperty('PreferencesController'); - expect(engine.datamodel.context).toHaveProperty('TokenBalancesController'); - expect(engine.datamodel.context).toHaveProperty('TokenRatesController'); - expect(engine.datamodel.context).toHaveProperty('TypedMessageManager'); + expect(engine.context).toHaveProperty('AccountTrackerController'); + expect(engine.context).toHaveProperty('AddressBookController'); + expect(engine.context).toHaveProperty('AssetsContractController'); + expect(engine.context).toHaveProperty('AssetsController'); + expect(engine.context).toHaveProperty('AssetsDetectionController'); + expect(engine.context).toHaveProperty('CurrencyRateController'); + expect(engine.context).toHaveProperty('KeyringController'); + expect(engine.context).toHaveProperty('NetworkController'); + expect(engine.context).toHaveProperty('PersonalMessageManager'); + expect(engine.context).toHaveProperty('PhishingController'); + expect(engine.context).toHaveProperty('PreferencesController'); + expect(engine.context).toHaveProperty('TokenBalancesController'); + expect(engine.context).toHaveProperty('TokenRatesController'); + expect(engine.context).toHaveProperty('TypedMessageManager'); }); }); diff --git a/package.json b/package.json index 7d71ad90d16..0cecb3b6bd1 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "dependencies": { "@exodus/react-native-payments": "https://github.com/wachunei/react-native-payments.git#package-json-hack", "@metamask/contract-metadata": "^1.23.0", - "@metamask/controllers": "^7.0.0", + "@metamask/controllers": "^8.0.0", "@metamask/etherscan-link": "^2.0.0", "@metamask/swaps-controller": "^2.0.1", "@react-native-community/async-storage": "1.12.1", diff --git a/yarn.lock b/yarn.lock index a38aacaca60..9ff086d9574 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1539,12 +1539,17 @@ resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.23.0.tgz#c70be7f3eaeeb791651ce793b7cdc230e9780b18" integrity sha512-oTUqL9dtXtbng60DZMRsBmZ5HiOUUfEsZjuswOJ0yHO24YsW0ktCcgCJVYPv1HcOsF0SVrRtG4rtrvOl4nY+HA== -"@metamask/controllers@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-7.0.0.tgz#8daecd284faa897ca1f112a3f6f28d6936dac674" - integrity sha512-vb2/wgGfJFMUa4Ej67FMkV94s0vp765t2vwOt8EOxhWfmEP2v9myc2B95L5jJJZZgvCk2ojaV11CrFF4ookLng== +"@metamask/contract-metadata@^1.24.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.25.0.tgz#442ace91fb40165310764b68d8096d0017bb0492" + integrity sha512-yhmYB9CQPv0dckNcPoWDcgtrdUp0OgK0uvkRE5QIBv4b3qENI1/03BztvK2ijbTuMlORUpjPq7/1MQDUPoRPVw== + +"@metamask/controllers@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-8.0.0.tgz#42ac5aaef67a03d3fe599a67a36597e01902ca8d" + integrity sha512-TrteMifsCxV1g3WHcSD1X98fF4hKep3sXZNGfrvkPqa8mrF03hJke21WBSTRtvJ3vkNLRWgi+5I6lVXFTzbYuQ== dependencies: - "@metamask/contract-metadata" "^1.23.0" + "@metamask/contract-metadata" "^1.24.0" "@types/uuid" "^8.3.0" async-mutex "^0.2.6" babel-runtime "^6.26.0" From 4bfc5465e30bd5b6728be38e7c1f19a0f49dd516 Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 29 Apr 2021 18:50:34 -0400 Subject: [PATCH 50/51] fix typeface on login text field (#2610) --- app/components/Views/Login/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 518bcaa85cf..28901625909 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -520,6 +520,7 @@ class Login extends PureComponent { {strings('login.password')} Date: Thu, 29 Apr 2021 19:45:40 -0400 Subject: [PATCH 51/51] Feature/confusables (#2464) * Add confusable warning to SendTo * Highlight confusable characters * Replace zeroWidthPoints characters with ? * Add some notes * Add confusable highlight to confirm screen * Update checkZeroWidth function * Add exclamation mark to Confirm * Add handleConfusables method * Move this into one spot * Add hasZeroWidthPoints * Rename T to Texts * Use reduce * Add homoglyphic tests * Add Modal for confusable on confirm screen * Update snapshot * Use Swaps InfoModal * Increase lineheight on modals * Only display warning if address is not in addressBook * Update snapshot * Make texts lowercase * Remove unused state * Add patch * Display as warning in yelllow when not zero width * Only display confusables warnings if the user is not in addressbook * Add optional chaining for addressBook Co-authored-by: andrepimenta --- app/components/Base/Text.js | 13 ++- .../__snapshots__/index.test.js.snap | 12 +++ .../__snapshots__/index.test.js.snap | 6 ++ .../__snapshots__/index.test.js.snap | 6 ++ .../__snapshots__/index.test.js.snap | 4 + app/components/UI/Swaps/QuotesView.js | 9 ++- .../__snapshots__/TokenIcon.test.js.snap | 2 + .../TokenSelectButton.test.js.snap | 5 ++ .../__snapshots__/index.test.js.snap | 5 ++ .../__snapshots__/index.test.js.snap | 3 + .../__snapshots__/index.test.js.snap | 5 ++ .../__snapshots__/index.test.js.snap | 2 + .../Views/SendFlow/AddressInputs/index.js | 77 ++++++++++++++++-- .../Confirm/__snapshots__/index.test.js.snap | 44 +++++++++- .../Views/SendFlow/Confirm/index.js | 65 +++++++++++++-- .../__snapshots__/index.test.js.snap | 1 + .../SendTo/__snapshots__/index.test.js.snap | 33 ++++++++ app/components/Views/SendFlow/SendTo/index.js | 81 ++++++++++++++++--- app/util/validators.js | 21 +++++ app/util/validators.test.js | 22 ++++- locales/languages/en.json | 4 +- package.json | 1 + patches/unicode-confusables+0.1.1.patch | 15 ++++ yarn.lock | 5 ++ 24 files changed, 407 insertions(+), 34 deletions(-) create mode 100644 patches/unicode-confusables+0.1.1.patch diff --git a/app/components/Base/Text.js b/app/components/Base/Text.js index 906ba541319..1478cb60198 100644 --- a/app/components/Base/Text.js +++ b/app/components/Base/Text.js @@ -16,10 +16,13 @@ const style = StyleSheet.create({ right: { textAlign: 'right' }, - bold: fontStyles.bold, + red: { + color: colors.red + }, black: { color: colors.black }, + bold: fontStyles.bold, blue: { color: colors.blue }, @@ -66,6 +69,7 @@ const Text = ({ green, black, blue, + red, primary, small, upper, @@ -87,6 +91,8 @@ const Text = ({ green && style.green, black && style.black, blue && style.blue, + red && style.red, + black && style.black, primary && style.primary, disclaimer && [style.small, style.disclaimer], small && style.small, @@ -110,6 +116,7 @@ Text.defaultProps = { green: false, black: false, blue: false, + red: false, primary: false, disclaimer: false, modal: false, @@ -150,6 +157,10 @@ Text.propTypes = { * Makes text blue */ blue: PropTypes.bool, + /** + * Makes text red + */ + red: PropTypes.bool, /** * Makes text fontPrimary color */ diff --git a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap index 19c45c50f29..7d82f7e0e3d 100644 --- a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap @@ -26,6 +26,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -55,6 +56,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -83,6 +85,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -109,6 +112,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -132,6 +136,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` noMargin={true} onPress={[Function]} primary={true} + red={false} reset={false} right={false} small={false} @@ -174,6 +179,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={true} @@ -199,6 +205,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={true} small={false} @@ -232,6 +239,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={true} @@ -257,6 +265,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={true} small={false} @@ -289,6 +298,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={true} @@ -314,6 +324,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={true} small={false} @@ -350,6 +361,7 @@ exports[`AddCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap b/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap index 7e4369c5da1..08125e83109 100644 --- a/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetActionButton/__snapshots__/index.test.js.snap @@ -40,6 +40,7 @@ exports[`AssetActionButtons should render correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -97,6 +98,7 @@ exports[`AssetActionButtons should render type add correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -154,6 +156,7 @@ exports[`AssetActionButtons should render type information correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -211,6 +214,7 @@ exports[`AssetActionButtons should render type receive correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -268,6 +272,7 @@ exports[`AssetActionButtons should render type send correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} @@ -325,6 +330,7 @@ exports[`AssetActionButtons should render type swap correctly 1`] = ` modal={false} numberOfLines={1} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap b/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap index 5177dd71ee4..79962ceefa0 100644 --- a/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap @@ -88,6 +88,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -150,6 +151,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -176,6 +178,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -280,6 +283,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -309,6 +313,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -336,6 +341,7 @@ exports[`CustomNonceModal should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap index 71e670559c0..84e57f358d0 100644 --- a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap @@ -28,6 +28,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -70,6 +71,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -104,6 +106,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -126,6 +129,7 @@ exports[`ReceiveRequest should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={true} diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index d578eadd56c..66e66ce4e8e 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -199,6 +199,9 @@ const styles = StyleSheet.create({ termsButton: { marginTop: 10, marginBottom: 6 + }, + text: { + lineHeight: 20 } }); @@ -1303,20 +1306,20 @@ function SwapsQuotesView({ isVisible={isUpdateModalVisible} toggleModal={toggleUpdateModal} title={strings('swaps.quotes_update_often')} - body={{strings('swaps.quotes_update_often_text')}} + body={{strings('swaps.quotes_update_often_text')}} /> {strings('swaps.price_difference_body')}} + body={{strings('swaps.price_difference_body')}} /> + {strings('swaps.fee_text.get_the')} {strings('swaps.fee_text.best_price')}{' '} {strings('swaps.fee_text.from_the')} {strings('swaps.fee_text.top_liquidity')}{' '} {strings('swaps.fee_text.fee_is_applied', { diff --git a/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap b/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap index db85e8e40db..2e5a01e3d1d 100644 --- a/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap +++ b/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap @@ -38,6 +38,7 @@ exports[`TokenIcon component should Render correctly 3`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -134,6 +135,7 @@ exports[`TokenIcon component should Render correctly 7`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap b/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap index 60cadcd3f84..a609ae4002a 100644 --- a/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap +++ b/app/components/UI/Swaps/components/__snapshots__/TokenSelectButton.test.js.snap @@ -34,6 +34,7 @@ exports[`TokenSelectButton component should Render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -96,6 +97,7 @@ exports[`TokenSelectButton component should Render correctly 2`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -158,6 +160,7 @@ exports[`TokenSelectButton component should Render correctly 3`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -221,6 +224,7 @@ exports[`TokenSelectButton component should Render correctly 4`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -286,6 +290,7 @@ exports[`TokenSelectButton component should Render correctly 5`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap index cab6e467afd..b1c7d1a29d4 100644 --- a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap @@ -24,6 +24,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -53,6 +54,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -79,6 +81,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -97,6 +100,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={true} + red={false} reset={false} right={false} small={false} @@ -117,6 +121,7 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` modal={false} noMargin={true} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap index 554bf0e3fc0..bd9bdbcb3fe 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap @@ -30,6 +30,7 @@ exports[`TransactionDetails should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={true} @@ -58,6 +59,7 @@ exports[`TransactionDetails should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={true} @@ -87,6 +89,7 @@ exports[`TransactionDetails should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={true} diff --git a/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap index c4aede01781..e0a24358079 100644 --- a/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap @@ -20,6 +20,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -39,6 +40,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -59,6 +61,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} @@ -81,6 +84,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={true} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -121,6 +125,7 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` link={false} modal={false} primary={true} + red={false} reset={false} right={false} small={false} diff --git a/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap b/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap index 0305a7201aa..528c58446c4 100644 --- a/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap +++ b/app/components/Views/OfflineMode/__snapshots__/index.test.js.snap @@ -50,6 +50,7 @@ exports[`OfflineMode should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} @@ -78,6 +79,7 @@ exports[`OfflineMode should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={false} diff --git a/app/components/Views/SendFlow/AddressInputs/index.js b/app/components/Views/SendFlow/AddressInputs/index.js index 24e7ccf9cf7..7a59d533301 100644 --- a/app/components/Views/SendFlow/AddressInputs/index.js +++ b/app/components/Views/SendFlow/AddressInputs/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, View, Text, TextInput, TouchableOpacity } from 'react-native'; +import { StyleSheet, View, TextInput, TouchableOpacity } from 'react-native'; import { colors, fontStyles, baseStyles } from '../../../../styles/common'; import AntIcon from 'react-native-vector-icons/AntDesign'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; @@ -7,6 +7,8 @@ import PropTypes from 'prop-types'; import Identicon from '../../../UI/Identicon'; import { renderShortAddress } from '../../../../util/address'; import { strings } from '../../../../../locales/i18n'; +import Text from '../../../Base/Text'; +import { hasZeroWidthPoints } from '../../../../util/validators'; const styles = StyleSheet.create({ wrapper: { @@ -45,7 +47,15 @@ const styles = StyleSheet.create({ addressToInformation: { flex: 1, flexDirection: 'row', - alignItems: 'center' + alignItems: 'center', + position: 'relative' + }, + exclamation: { + backgroundColor: colors.white, + borderRadius: 12, + position: 'absolute', + bottom: 8, + left: 20 }, address: { flexDirection: 'column', @@ -119,6 +129,43 @@ const styles = StyleSheet.create({ } }); +const AddressName = ({ toAddressName, confusableCollection = [] }) => { + if (confusableCollection.length) { + const texts = toAddressName.split('').map((char, index) => { + // if text has a confusable highlight it red + if (confusableCollection.includes(char)) { + // if the confusable is zero width, replace it with `?` + const replacement = hasZeroWidthPoints(char) ? '?' : char; + return ( + + {replacement} + + ); + } + return ( + + {char} + + ); + }); + return ( + + {texts} + + ); + } + return ( + + {toAddressName} + + ); +}; + +AddressName.propTypes = { + toAddressName: PropTypes.string, + confusableCollection: PropTypes.array +}; + export const AddressTo = props => { const { addressToReady, @@ -132,7 +179,9 @@ export const AddressTo = props => { onInputFocus, onSubmit, onInputBlur, - inputWidth + inputWidth, + confusableCollection, + displayExclamation } = props; return ( @@ -173,12 +222,18 @@ export const AddressTo = props => { + {displayExclamation && ( + + + + )} {toAddressName && ( - - {toAddressName} - + )} { diff --git a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap index 30049b78f87..9ef917d8156 100644 --- a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.js.snap @@ -24,11 +24,40 @@ exports[`Confirm should render correctly 1`] = ` fromAccountAddress="0x1" onPressIcon={null} /> - + + + We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam. + + } + isVisible={false} + title="Check the recipient address" + toggleModal={[Function]} + /> balance */ @@ -313,6 +323,7 @@ class Confirm extends PureComponent { }; state = { + confusableCollection: [], gasSpeedSelected: 'average', gasEstimationReady: false, customGas: undefined, @@ -331,6 +342,7 @@ class Confirm extends PureComponent { transactionTotalAmountFiat: undefined, errorMessage: undefined, fromAccountModalVisible: false, + warningModalVisible: false, mode: REVIEW, over: false }; @@ -368,6 +380,13 @@ class Confirm extends PureComponent { } }; + handleConfusables = async () => { + const { transactionToName } = this.props.transactionState; + await this.setState({ confusableCollection: collectConfusables(transactionToName) }); + }; + + toggleWarningModal = () => this.setState(state => ({ warningModalVisible: !state.warningModalVisible })); + componentDidMount = async () => { // For analytics AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SEND_TRANSACTION_STARTED, this.getAnalyticsParams()); @@ -375,6 +394,7 @@ class Confirm extends PureComponent { const { showCustomNonce, navigation, providerType } = this.props; await this.handleFetchBasicEstimates(); showCustomNonce && (await this.setNetworkNonce()); + await this.handleConfusables(); navigation.setParams({ providerType }); this.parseTransactionData(); this.prepareTransaction(); @@ -911,7 +931,7 @@ class Confirm extends PureComponent { render = () => { const { transactionToName, selectedAsset, paymentRequest } = this.props.transactionState; - const { showHexData, showCustomNonce, primaryCurrency, network, chainId } = this.props; + const { addressBook, showHexData, showCustomNonce, primaryCurrency, network, chainId } = this.props; const { nonce } = this.props.transaction; const { gasEstimationReady, @@ -928,10 +948,36 @@ class Confirm extends PureComponent { errorMessage, transactionConfirmed, warningGasPriceHigh, + confusableCollection, mode, - over + over, + warningModalVisible } = this.state; + const checksummedAddress = transactionTo && toChecksumAddress(transactionTo); + const existingContact = checksummedAddress && addressBook[network] && addressBook[network][checksummedAddress]; + const displayExclamation = !existingContact && !!confusableCollection.length; + + const AdressToComponent = () => ( + + ); + + const AdressToComponentWrap = () => + !existingContact && confusableCollection.length ? ( + + + + ) : ( + + ); + const is_main_net = isMainNet(network); const errorPress = is_main_net ? this.buyEth : this.gotoFaucet; const networkName = capitalize(getNetworkName(network)); @@ -947,14 +993,16 @@ class Confirm extends PureComponent { fromAccountName={fromAccountName} fromAccountBalance={fromAccountBalance} /> - + + {strings('transaction.confusable_msg')}} + /> + {!selectedAsset.tokenId ? ( @@ -1046,6 +1094,7 @@ class Confirm extends PureComponent { const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, + addressBook: state.engine.backgroundState.AddressBookController?.addressBook, contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, diff --git a/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap index 57413a9c970..621c6cfde6f 100644 --- a/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.js.snap @@ -32,6 +32,7 @@ exports[`ErrorMessage should render correctly 1`] = ` link={false} modal={false} primary={false} + red={false} reset={false} right={false} small={true} diff --git a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap index d6855203729..c9965feb7e4 100644 --- a/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/SendTo/__snapshots__/index.test.js.snap @@ -27,6 +27,7 @@ exports[`SendTo should render correctly 1`] = ` /> Add to address book Enter an alias diff --git a/app/components/Views/SendFlow/SendTo/index.js b/app/components/Views/SendFlow/SendTo/index.js index 54a4e6adda7..bd0f0b7bd42 100644 --- a/app/components/Views/SendFlow/SendTo/index.js +++ b/app/components/Views/SendFlow/SendTo/index.js @@ -7,7 +7,6 @@ import { StyleSheet, View, TouchableOpacity, - Text, TextInput, SafeAreaView, InteractionManager, @@ -34,6 +33,9 @@ import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; import { allowedToBuy } from '../../../UI/FiatOrders'; import NetworkList from '../../../../util/networks'; +import Text from '../../../Base/Text'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import { collectConfusables, hasZeroWidthPoints } from '../../../../util/validators'; const { hexToBN } = util; const styles = StyleSheet.create({ @@ -125,12 +127,41 @@ const styles = StyleSheet.create({ marginBottom: 32 }, buyEth: { - ...fontStyles.bold, color: colors.black, textDecorationLine: 'underline' }, - bold: { - ...fontStyles.bold + confusabeError: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + margin: 16, + padding: 16, + borderWidth: 1, + borderColor: colors.red, + backgroundColor: colors.red000, + borderRadius: 8 + }, + confusabeWarning: { + borderColor: colors.yellow, + backgroundColor: colors.yellow100 + }, + confusableTitle: { + marginTop: -3, + color: colors.red, + ...fontStyles.bold, + fontSize: 14 + }, + confusableMsg: { + color: colors.red, + fontSize: 12, + lineHeight: 16, + paddingRight: 10 + }, + black: { + color: colors.black + }, + warningIcon: { + marginRight: 8 } }); @@ -210,6 +241,7 @@ class SendFlow extends PureComponent { toEnsName: undefined, addToAddressToAddressBook: false, alias: undefined, + confusableCollection: [], inputWidth: { width: '99%' } }; @@ -274,7 +306,7 @@ class SendFlow extends PureComponent { const { AssetsContractController } = Engine.context; const { addressBook, network, identities, providerType } = this.props; const networkAddressBook = addressBook[network] || {}; - let addressError, toAddressName, toEnsName, errorContinue, isOnlyWarning; + let addressError, toAddressName, toEnsName, errorContinue, isOnlyWarning, confusableCollection; let [addToAddressToAddressBook, toSelectedAddressReady] = [false, false]; if (isValidAddress(toSelectedAddress)) { const checksummedToSelectedAddress = toChecksumAddress(toSelectedAddress); @@ -304,7 +336,7 @@ class SendFlow extends PureComponent { addressError = ( {strings('transaction.tokenContractAddressWarning_1')} - {strings('transaction.tokenContractAddressWarning_2')} + {strings('transaction.tokenContractAddressWarning_2')} {strings('transaction.tokenContractAddressWarning_3')} ); @@ -329,6 +361,7 @@ class SendFlow extends PureComponent { */ } else if (isENS(toSelectedAddress)) { toEnsName = toSelectedAddress; + confusableCollection = collectConfusables(toEnsName); const resolvedAddress = await doENSLookup(toSelectedAddress, network); if (resolvedAddress) { const checksummedResolvedAddress = toChecksumAddress(resolvedAddress); @@ -352,7 +385,8 @@ class SendFlow extends PureComponent { toSelectedAddressName: toAddressName, toEnsName, errorContinue, - isOnlyWarning + isOnlyWarning, + confusableCollection }); }; @@ -510,7 +544,7 @@ class SendFlow extends PureComponent { return ( <> {'\n'} - + {strings('fiat_on_ramp.buy_eth')} @@ -519,6 +553,7 @@ class SendFlow extends PureComponent { render = () => { const { ticker } = this.props; + const { addressBook, network } = this.props; const { fromSelectedAddress, fromAccountName, @@ -532,8 +567,16 @@ class SendFlow extends PureComponent { toInputHighlighted, inputWidth, errorContinue, - isOnlyWarning + isOnlyWarning, + confusableCollection } = this.state; + + const checksummedAddress = toSelectedAddress && toChecksumAddress(toSelectedAddress); + const existingContact = checksummedAddress && addressBook[network] && addressBook[network][checksummedAddress]; + const displayConfusableWarning = !existingContact && confusableCollection && !!confusableCollection.length; + const displayAsWarning = + confusableCollection && confusableCollection.length && !confusableCollection.some(hasZeroWidthPoints); + return ( @@ -556,6 +599,7 @@ class SendFlow extends PureComponent { onInputBlur={this.onToInputFocus} onSubmit={this.onTransactionDirectionSet} inputWidth={inputWidth} + confusableCollection={(!existingContact && confusableCollection) || []} /> @@ -578,6 +622,25 @@ class SendFlow extends PureComponent { /> )} + {displayConfusableWarning && ( + + + + + + + {strings('transaction.confusable_title')} + + + {strings('transaction.confusable_msg')} + + + + )} {addToAddressToAddressBook && ( { const wordCount = seed.split(/\s/u).length; @@ -13,3 +14,23 @@ export const parseSeedPhrase = seedPhrase => ?.join(' ') || ''; export const { isValidMnemonic } = ethers.utils; + +export const collectConfusables = ensName => { + const key = 'similarTo'; + const collection = confusables(ensName).reduce( + (total, current) => (key in current ? [...total, current.point] : total), + [] + ); + return collection; +}; + +const zeroWidthPoints = new Set([ + '\u200b', // zero width space + '\u200c', // zero width non-joiner + '\u200d', // zero width joiner + '\ufeff', // zero width no-break space + '\u2028', // line separator + '\u2029' // paragraph separator, +]); + +export const hasZeroWidthPoints = char => zeroWidthPoints.has(char); diff --git a/app/util/validators.test.js b/app/util/validators.test.js index a9f8b7c90e6..0f676b55953 100644 --- a/app/util/validators.test.js +++ b/app/util/validators.test.js @@ -1,4 +1,4 @@ -import { failedSeedPhraseRequirements, parseSeedPhrase } from './validators'; +import { failedSeedPhraseRequirements, parseSeedPhrase, hasZeroWidthPoints, collectConfusables } from './validators'; const VALID_24 = 'verb middle giant soon wage common wide tool gentle garlic issue nut retreat until album recall expire bronze bundle live accident expect dry cook'; @@ -37,3 +37,23 @@ describe('parseSeedPhrase', () => { expect(parseSeedPhrase(` ${String(VALID_12).toUpperCase()}`)).toEqual(VALID_12); }); }); + +describe('hasZeroWidthPoints', () => { + it('should detect zero-width unicode', () => { + expect('vita‍lik.eth'.split('').some(hasZeroWidthPoints)).toEqual(true); + }); + it('should not detect zero-width unicode', () => { + expect('vitalik.eth'.split('').some(hasZeroWidthPoints)).toEqual(false); + }); +}); + +describe('collectConfusables', () => { + it('should detect homoglyphic unicode points', () => { + expect(collectConfusables('vita‍lik.eth')).toHaveLength(1); + expect(collectConfusables('faceboоk.eth')).toHaveLength(1); + }); + + it('should detect multiple homoglyphic unicode points', () => { + expect(collectConfusables('ѕсоре.eth')).toHaveLength(5); + }); +}); diff --git a/locales/languages/en.json b/locales/languages/en.json index 0fcc69a1245..2067e452115 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -670,7 +670,9 @@ "tokenContractAddressWarning_2": "token contract address", "tokenContractAddressWarning_3": ". If you send tokens to this address, you will lose them.", "smartContractAddressWarning": "This address is a smart contract address. Please make sure you understand what this address is for, otherwise you risk losing your funds.", - "continueError": "I understand the risks, continue" + "continueError": "I understand the risks, continue", + "confusable_title": "Check the recipient address", + "confusable_msg": "We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam." }, "custom_gas": { "total": "Total", diff --git a/package.json b/package.json index 0cecb3b6bd1..96ed4b44b85 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,7 @@ "rn-fetch-blob": "^0.12.0", "stream-browserify": "1.0.0", "through2": "3.0.1", + "unicode-confusables": "^0.1.1", "url": "0.11.0", "url-parse": "1.4.4", "valid-url": "1.0.9", diff --git a/patches/unicode-confusables+0.1.1.patch b/patches/unicode-confusables+0.1.1.patch new file mode 100644 index 00000000000..9b90e6b4c18 --- /dev/null +++ b/patches/unicode-confusables+0.1.1.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/unicode-confusables/data/confusables.json b/node_modules/unicode-confusables/data/confusables.json +index 855e49c..b0b8a0b 100644 +--- a/node_modules/unicode-confusables/data/confusables.json ++++ b/node_modules/unicode-confusables/data/confusables.json +@@ -157,8 +157,8 @@ + "໊": "๊", + "໋": "๋", + "꙯": "⃩", +- "
": " ", +- "
": " ", ++ "\u2028": " ", ++ "\u2029": " ", + " ": " ", + " ": " ", + " ": " ", diff --git a/yarn.lock b/yarn.lock index 9ff086d9574..582ba06b42b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13250,6 +13250,11 @@ unicode-canonical-property-names-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-confusables@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/unicode-confusables/-/unicode-confusables-0.1.1.tgz#17f14e8dc53ff81c12e92fd86e836ebdf14ea0c2" + integrity sha512-XTPBWmT88BDpXz9NycWk4KxDn+/AJmJYYaYBwuIH9119sopwk2E9GxU9azc+JNbhEsfiPul78DGocEihCp6MFQ== + unicode-match-property-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"

R 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 z z*R?nZ8ry<7J|K423U;37`4QWwMJ{i&p7FdYb!L%Hahi9R@rk1XMk#kVD{su;2WKng zIlwW=B)|A%QC!Mg7<)SmtzNE6JR>2O9%H zvG-b#xZJGsn(E|ceAjg2+I4O2vq_%g8lhXI;tAd`VVV5?0EGg^F zW;eWUkI=9@|24Dq4G|z-Yich@mo@pLl)oC0>*sM+iyXW2{nVVZ!AyiJ;FT6XyhohU zLYpMyW5YB*n7a`>?FSr< z47V=wF> Date: Wed, 7 Apr 2021 13:36:44 -0400 Subject: [PATCH 02/51] remove controllers tgz (#2479) --- metamask-controllers-v6.2.2.tgz | Bin 121709 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 metamask-controllers-v6.2.2.tgz diff --git a/metamask-controllers-v6.2.2.tgz b/metamask-controllers-v6.2.2.tgz deleted file mode 100644 index 990b678f8c1ec8958bd58b6b20a8b91e4ab998e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121709 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