diff --git a/.circleci/config.yml b/.circleci/config.yml index 29ca80eddf2..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 @@ -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/.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/.nvmrc b/.nvmrc index e338b86593f..958b5a36e1f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10 +v14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 90310404a41..fb592ef25a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # 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 +- [#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.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 + +## 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) +- [#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/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 diff --git a/android/app/build.gradle b/android/app/build.gradle index bd75d063563..87caea76f7b 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 54 + versionName "2.2.0" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" 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/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/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..1478cb60198 100644 --- a/app/components/Base/Text.js +++ b/app/components/Base/Text.js @@ -16,7 +16,16 @@ const style = StyleSheet.create({ right: { textAlign: 'right' }, + red: { + color: colors.red + }, + black: { + color: colors.black + }, bold: fontStyles.bold, + blue: { + color: colors.blue + }, green: { color: colors.green400 }, @@ -58,6 +67,9 @@ const Text = ({ right, bold, green, + black, + blue, + red, primary, small, upper, @@ -77,6 +89,10 @@ const Text = ({ right && style.right, bold && style.bold, 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, @@ -98,6 +114,9 @@ Text.defaultProps = { right: false, bold: false, green: false, + black: false, + blue: false, + red: false, primary: false, disclaimer: false, modal: false, @@ -130,6 +149,18 @@ Text.propTypes = { * Makes text green */ green: PropTypes.bool, + /** + * Makes text black + */ + black: PropTypes.bool, + /** + * Makes text blue + */ + blue: PropTypes.bool, + /** + * Makes text red + */ + red: PropTypes.bool, /** * Makes text fontPrimary color */ diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index ff7a77f8bde..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,11 +57,12 @@ 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'; import BigNumber from 'bignumber.js'; +import { setInfuraAvailabilityBlocked, setInfuraAvailabilityNotBlocked } from '../../../actions/infuraAvailability'; const styles = StyleSheet.create({ flex: { @@ -79,9 +79,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: '' }); @@ -128,24 +127,51 @@ 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( state => { + if (!state) return; + const { isConnected } = state; // Show the modal once the status changes to offline - if (connected && !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 () => { + 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); @@ -283,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); @@ -361,6 +388,7 @@ const Main = props => { }, [ props.tokens, + props.chainId, setEtherTransaction, setTransactionObject, toggleApproveModal, @@ -541,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); @@ -569,8 +605,6 @@ const Main = props => { } }); - Engine.context.TransactionController.hub.on('unapprovedTransaction', onUnapprovedTransaction); - Engine.context.MessageManager.hub.on('unapprovedMessage', messageParams => onUnapprovedMessage(messageParams, 'eth') ); @@ -592,7 +626,7 @@ const Main = props => { removeNotificationById: props.removeNotificationById }); pollForIncomingTransactions(); - + checkInfuraAvailability(); removeConnectionStatusListener.current = NetInfo.addEventListener(connectionChangeHandler); }, 1000); @@ -601,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(); }; @@ -709,18 +742,36 @@ Main.propTypes = { /** * Selected address */ - selectedAddress: PropTypes.string + selectedAddress: PropTypes.string, + /** + * Chain id + */ + chainId: 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 => ({ 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, 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 +782,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/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js index 3b644ba7c6b..a53493d3daf 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,35 @@ 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 = () => { + 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 = () => { - 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 +142,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 +150,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 +213,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/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/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/AddCustomCollectible/index.js b/app/components/UI/AddCustomCollectible/index.js index fb538ef8435..5983e6fd1e8 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,19 @@ class AddCustomCollectible extends PureComponent { this.mounted = false; }; + getAnalyticsParams = () => { + try { + const { NetworkController } = Engine.context; + const { chainId, type } = NetworkController?.state?.provider || {}; + return { + network_name: type, + chain_id: chainId + }; + } catch (error) { + return {}; + } + }; + addCollectible = async () => { if (!(await this.validateCustomCollectible())) return; const isOwner = await this.validateCollectibleOwnership(); @@ -82,6 +96,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..7d82f7e0e3d 100644 --- a/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomNetwork/__snapshots__/index.test.js.snap @@ -1,7 +1,448 @@ // 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..d1e01408d02 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,31 @@ export default class AddCustomToken extends PureComponent { navigation: PropTypes.object }; + getAnalyticsParams = () => { + 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 () => { 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( { @@ -104,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/components/UI/ApproveTransactionReview/index.js b/app/components/UI/ApproveTransactionReview/index.js index 37831fc34f0..35873b19479 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'; @@ -35,8 +36,8 @@ import AppConstants from '../../../core/AppConstants'; import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect'; import { withNavigation } from 'react-navigation'; import { getNetworkName, isMainNet } from '../../../util/networks'; -import { capitalize } from '../../../util/format'; import scaling from '../../../util/scaling'; +import { capitalize } from '../../../util/general'; import EditPermission from './EditPermission'; const { hexToBN } = util; @@ -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,33 @@ class ApproveTransactionReview extends PureComponent { } } + getAnalyticsParams = () => { + 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 => { const { transaction, tokensLength, accountsLength, providerType } = this.props; InteractionManager.runAfterInteractions(() => { @@ -392,6 +430,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 +441,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 +578,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} > { 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/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`] = ` 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.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'); + //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'); @@ -488,16 +524,27 @@ class CustomGas extends PureComponent { }); }; + getAnalyticsParams = () => { + 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 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 +556,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); @@ -517,7 +567,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 = () => { @@ -636,7 +688,7 @@ class CustomGas extends PureComponent { @@ -693,6 +745,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 +782,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 +831,9 @@ 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), + networkType: state.engine.backgroundState.NetworkController.provider.type, + 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/CustomNonceModal/__snapshots__/index.test.js.snap b/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..79962ceefa0 --- /dev/null +++ b/app/components/UI/CustomNonceModal/__snapshots__/index.test.js.snap @@ -0,0 +1,419 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CustomNonceModal 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/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 4b22602b96f..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'; @@ -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: { @@ -337,6 +338,10 @@ class DrawerView extends PureComponent { * Wizard onboarding state */ wizard: PropTypes.object, + /** + * Chain Id + */ + chainId: PropTypes.string, /** * Current provider ticker */ @@ -497,7 +502,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 = () => { @@ -572,7 +577,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 = () => { @@ -721,7 +729,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 }, @@ -826,6 +834,7 @@ class DrawerView extends PureComponent { selectedAddress, keyrings, currentCurrency, + chainId, ticker, seedphraseBackedUp } = this.props; @@ -872,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/MessageSign/index.js b/app/components/UI/MessageSign/index.js index d8d4a6c7f2c..09e15d7c66d 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,28 @@ export default class MessageSign extends PureComponent { truncateMessage: false }; + getAnalyticsParams = () => { + 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 = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_STARTED, this.getAnalyticsParams()); + }; + showWalletConnectNotification = (messageParams = {}, confirmation = false) => { InteractionManager.runAfterInteractions(() => { messageParams.origin && @@ -94,11 +118,13 @@ export default class MessageSign extends PureComponent { cancelSignature = () => { this.rejectMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.props.onCancel(); }; confirmSignature = () => { this.signMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); 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/Notification/BaseNotification/index.js b/app/components/UI/Notification/BaseNotification/index.js index 97a54f6ef29..f4b90393ac4 100644 --- a/app/components/UI/Notification/BaseNotification/index.js +++ b/app/components/UI/Notification/BaseNotification/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import { TouchableOpacity, StyleSheet, View, Text } from 'react-native'; +import { TouchableOpacity, StyleSheet, View } from 'react-native'; import PropTypes from 'prop-types'; import { colors, fontStyles, baseStyles } from '../../../../styles/common'; import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; @@ -7,6 +7,7 @@ import AnimatedSpinner from '../../AnimatedSpinner'; import { strings } from '../../../../../locales/i18n'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import AntIcon from 'react-native-vector-icons/AntDesign'; +import Text from '../../../Base/Text'; const styles = StyleSheet.create({ defaultFlashFloating: { 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/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/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..91e21679a4b 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,29 @@ export default class PersonalSign extends PureComponent { truncateMessage: false }; + getAnalyticsParams = () => { + 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 = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_STARTED, this.getAnalyticsParams()); + }; + showWalletConnectNotification = (messageParams = {}, confirmation = false) => { InteractionManager.runAfterInteractions(() => { messageParams.origin && @@ -99,11 +124,13 @@ export default class PersonalSign extends PureComponent { cancelSignature = () => { this.rejectMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.props.onCancel(); }; confirmSignature = () => { this.signMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams()); this.props.onConfirm(); }; diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap index 76937393466..84e57f358d0 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`] = ` } > { + this.getAnalyticsParams(); + }; + + getAnalyticsParams = () => { + 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 () => { 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/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/UI/SlippageSlider/__snapshots__/index.test.js.snap b/app/components/UI/SlippageSlider/__snapshots__/index.test.js.snap index 1f421b08bdc..1307399c9d6 100644 --- a/app/components/UI/SlippageSlider/__snapshots__/index.test.js.snap +++ b/app/components/UI/SlippageSlider/__snapshots__/index.test.js.snap @@ -215,7 +215,7 @@ exports[`SlippageSlider should render correctly 1`] = ` "bottom": 0, "height": 30, "position": "absolute", - "shadowColor": "#000000", + "shadowColor": "#24292E", "shadowOffset": Object { "height": 0, "width": 0, diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index 124c82db1b8..66e66ce4e8e 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -7,7 +7,8 @@ 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 { BNToHex, @@ -19,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'; @@ -48,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; @@ -197,6 +199,9 @@ const styles = StyleSheet.create({ termsButton: { marginTop: 10, marginBottom: 6 + }, + text: { + lineHeight: 20 } }); @@ -208,7 +213,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; @@ -252,6 +257,8 @@ function SwapsQuotesView({ selectedAddress, currentCurrency, conversionRate, + chainId, + ticker, isInPolling, quotesLastFetched, pollingCyclesLeft, @@ -279,7 +286,7 @@ function SwapsQuotesView({ const hasConversionRate = Boolean(destinationToken) && - (isSwapsETH(destinationToken) || + (isSwapsNativeAsset(destinationToken) || Boolean( Engine.context.TokenRatesController.state.contractExchangeRates?.[ safeToChecksumAddress(destinationToken.address) @@ -372,7 +379,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)); @@ -552,7 +559,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 +579,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) { @@ -1019,22 +1028,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')} + + )} )} @@ -1206,7 +1216,7 @@ function SwapsQuotesView({ - {renderFromWei(toWei(selectedQuoteValue?.ethFee))} ETH + {renderFromWei(toWei(selectedQuoteValue?.ethFee))} {getTicker(ticker)} {` ${weiToFiat( @@ -1230,7 +1240,10 @@ function SwapsQuotesView({ - {renderFromWei(toWei(selectedQuoteValue?.maxEthFee || '0x0'))} ETH + + {renderFromWei(toWei(selectedQuoteValue?.maxEthFee || '0x0'))}{' '} + {getTicker(ticker)} + {` ${weiToFiat( toWei(selectedQuoteValue?.maxEthFee), @@ -1293,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', { @@ -1323,6 +1336,7 @@ function SwapsQuotesView({ destinationToken={destinationToken} selectedQuote={selectedQuoteId} showOverallValue={hasConversionRate} + ticker={getTicker(ticker)} /> ); @@ -1371,6 +1386,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, @@ -1389,6 +1412,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 ( )} @@ -157,7 +160,8 @@ TransactionsEditionModal.propTypes = { onHandleGasFeeSelection: PropTypes.func, setApprovalTransaction: PropTypes.func, selectedQuote: PropTypes.object, - sourceToken: PropTypes.object + sourceToken: PropTypes.object, + chainId: PropTypes.string }; const mapStateToProps = state => ({ 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 ed438b8f218..2e5a01e3d1d 100644 --- a/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap +++ b/app/components/UI/Swaps/components/__snapshots__/TokenIcon.test.js.snap @@ -29,6 +29,8 @@ exports[`TokenIcon component should Render correctly 2`] = ` exports[`TokenIcon component should Render correctly 3`] = ` { (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/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/SwitchCustomNetwork/__snapshots__/index.test.js.snap b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap index f61fb102fa0..b1c7d1a29d4 100644 --- a/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap +++ b/app/components/UI/SwitchCustomNetwork/__snapshots__/index.test.js.snap @@ -14,6 +14,8 @@ exports[`SwitchCustomNetwork should render correctly 1`] = ` } > - "undefined" + "" { it('should render correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/UI/Tabs/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/__snapshots__/index.test.js.snap index 3595f187e79..07f81d00944 100644 --- a/app/components/UI/Tabs/__snapshots__/index.test.js.snap +++ b/app/components/UI/Tabs/__snapshots__/index.test.js.snap @@ -50,7 +50,7 @@ exports[`Tabs should render correctly 1`] = ` "marginBottom": 0, "paddingHorizontal": 20, "paddingTop": 17, - "shadowColor": "#000000", + "shadowColor": "#24292E", "shadowOffset": Object { "height": 12, "width": 0, 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/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 1c1fb135d69..dfdb3fa2a1c 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 { getBasicGasEstimates, apiEstimateModifiedToWEI } 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 = { @@ -596,9 +600,28 @@ 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 }); + }; + + getGasAnalyticsParams = () => { + 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 = () => { @@ -623,6 +646,8 @@ class TransactionEditor extends PureComponent { gasPrice={transaction.gasPrice} gasError={gasError} mode={mode} + view={'Transaction'} + analyticsParams={this.getGasAnalyticsParams()} /> @@ -639,7 +664,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/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/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap index e3639c236a7..bd9bdbcb3fe 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap @@ -21,6 +21,8 @@ exports[`TransactionDetails should render correctly 1`] = ` Date { 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 79e61ac7e16..b49481973b6 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,8 @@ 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({ row: { @@ -38,6 +41,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 +94,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 */ @@ -88,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 = { @@ -96,6 +127,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 +156,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 +173,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 +237,41 @@ class TransactionElement extends PureComponent { */ renderTxElement = transactionElement => { const { - tx: { status } + identities, + chainId, + 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} + {isMainNet(chainId) && {fiatValue}} + + )} + + {!!renderTxActions && ( + + {this.renderSpeedUpButton()} + {this.renderCancelButton()} + )} - - {!!renderTxActions && ( - - {this.renderSpeedUpButton()} - {this.renderCancelButton()} - - )} - + + {accountImportTime <= time && this.renderImportTime()} + ); }; @@ -241,7 +317,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,6 +352,25 @@ class TransactionElement extends PureComponent { /> + + + + + {strings('transactions.import_wallet_label')} + + + + + {strings('transactions.import_wallet_tip')} + + + ); } @@ -283,7 +378,10 @@ 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 }); 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/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/TransactionHeader/__snapshots__/index.test.js.snap b/app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap index ee3ebce1772..b60c6a06a7a 100644 --- a/app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap @@ -50,7 +50,7 @@ exports[`TransactionHeader 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`] = ` + - - - 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..500e0393fe3 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,53 @@ 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 () => { + const { showCustomNonce } = this.props; + showCustomNonce && (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 +347,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 +361,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 +377,7 @@ class TransactionReviewInformation extends PureComponent { return ( + {nonceModalVisible && this.renderCustomNonceModal()} {!!amountError && ( @@ -370,7 +418,7 @@ class TransactionReviewInformation extends PureComponent { {warningGasPriceHigh} )} - {!over && ( + {!over && !showCustomNonce && ( {strings('transaction.view_data')} @@ -389,7 +437,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/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap index 4d6170c0902..b0fe994646c 100644 --- a/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap @@ -21,7 +21,7 @@ exports[`TransactionReviewSummary should render correctly 1`] = ` "borderColor": "#848c96", "borderRadius": 12, "borderWidth": 1, - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", "fontSize": 10, "fontWeight": "400", 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/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 f863c5bd064..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: {} }, @@ -24,6 +28,11 @@ describe('Transactions', () => { CurrencyRateController: { currentCurrency: 'USD', conversionRate: 1 + }, + NetworkController: { + provider: { + chainId: '1' + } } } }, diff --git a/app/components/UI/TypedSign/index.js b/app/components/UI/TypedSign/index.js index aa09e788723..09e9b5e1425 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,29 @@ export default class TypedSign extends PureComponent { truncateMessage: false }; + getAnalyticsParams = () => { + 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 = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_STARTED, this.getAnalyticsParams()); + }; + showWalletConnectNotification = (messageParams = {}, confirmation = false) => { InteractionManager.runAfterInteractions(() => { messageParams.origin && @@ -109,11 +134,13 @@ export default class TypedSign extends PureComponent { cancelSignature = () => { this.rejectMessage(); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_CANCELLED, this.getAnalyticsParams()); this.props.onCancel(); }; confirmSignature = () => { 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 c8283859300..2651f41c88a 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,36 @@ 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 = () => { + try { + 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)' + }; + } catch (error) { + return {}; + } }; componentWillUnmount = async () => { @@ -109,6 +140,7 @@ class WatchAssetRequest extends PureComponent { const { onConfirm, suggestedAssetMeta } = this.props; const { AssetsController } = Engine.context; await AssetsController.acceptWatchAsset(suggestedAssetMeta.id); + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.TOKEN_ADDED, this.getAnalyticsParams()); onConfirm && onConfirm(); }; diff --git a/app/components/UI/WhatsNewModal/__snapshots__/index.test.js.snap b/app/components/UI/WhatsNewModal/__snapshots__/index.test.js.snap index c612138ea05..2ae9617eab7 100644 --- a/app/components/UI/WhatsNewModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/WhatsNewModal/__snapshots__/index.test.js.snap @@ -48,7 +48,7 @@ exports[`WhatsNewModal should render correctly 1`] = ` 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(); }); }); diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js index 73bceb4ebcd..a72d4d62c4e 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,20 @@ class Approval extends PureComponent { /** * Tells whether or not dApp transaction modal is visible */ - dappTransactionModalVisible: PropTypes.bool + 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 + */ + activeTabUrl: PropTypes.string, + /** + * A string representing the network chainId + */ + chainId: PropTypes.string }; state = { @@ -93,7 +107,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 +168,23 @@ class Approval extends PureComponent { }; }; + getAnalyticsParams = () => { + 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 {}; + } + }; + /** * Transaction state is erased, ready to create a new clean transaction */ @@ -180,6 +212,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()); }; /** @@ -189,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); @@ -224,7 +260,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_COMPLETED, this.getAnalyticsParams()); }; /** @@ -313,7 +349,10 @@ 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, + showCustomNonce: state.settings.showCustomNonce, + chainId: state.engine.backgroundState.NetworkController.provider.chainId, + activeTabUrl: getActiveTabUrl(state) }); const mapDispatchToProps = dispatch => ({ 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/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js index 879111b5e25..727c3758d4b 100644 --- a/app/components/Views/ApproveView/Approve/index.js +++ b/app/components/Views/ApproveView/Approve/index.js @@ -14,12 +14,13 @@ 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'; 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 = () => { @@ -121,9 +123,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 => { @@ -205,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)'); @@ -214,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); }; @@ -231,10 +235,29 @@ class Approve extends PureComponent { } }; + setAnalyticsParams = analyticsParams => { + this.setState({ analyticsParams }); + }; + + getGasAnalyticsParams = () => { + 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 } + }; + } catch (error) { + return {}; + } + }; + render = () => { const { gasError, basicGasEstimates, mode, ready, over, warningGasPriceHigh } = this.state; const { transaction } = this.props; if (!transaction.id) return null; + return ( @@ -280,8 +306,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/Asset/index.js b/app/components/Views/Asset/index.js index 0ed4e3cd97d..7fed702bfcf 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -2,13 +2,15 @@ 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({ wrapper: { @@ -46,6 +48,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 */ @@ -163,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; @@ -174,15 +181,24 @@ 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 = []; 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, + accountAddedTimeInsertPointFound + ); + if (tx.insertImportTime) accountAddedTimeInsertPointFound = true; switch (tx.status) { case 'submitted': case 'signed': @@ -197,13 +213,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); @@ -211,6 +223,10 @@ 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 +311,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/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} > - { 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.'); } @@ -1757,6 +1757,11 @@ export const BrowserTab = props => { onCancel={onCancelWatchAsset} onConfirm={onCancelWatchAsset} suggestedAssetMeta={suggestedAssetMeta} + currentPageInformation={{ + title: title.current, + url: getMaskedUrl(url.current), + icon: icon.current + }} /> ); @@ -1956,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/ChoosePassword/__snapshots__/index.test.js.snap b/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap index 7a05bd3f229..9e928630fda 100644 --- a/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap +++ b/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap @@ -69,9 +69,9 @@ exports[`ChoosePassword should render correctly 1`] = ` style={ Object { "color": "#000000", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 24, - "fontWeight": "400", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 25, + "fontWeight": "600", "justifyContent": "center", "marginBottom": 20, "marginTop": 20, @@ -110,6 +110,7 @@ exports[`ChoosePassword 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`] = ` { 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/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap b/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap index 78a1ae10949..25e9974b730 100644 --- a/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap +++ b/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap @@ -67,8 +67,9 @@ exports[`ImportFromSeed 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 9ea55a275af..28901625909 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', @@ -278,7 +285,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); } @@ -477,6 +485,7 @@ class Login extends PureComponent { {strings('login.type_delete')} {strings('login.password')} - + - 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/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 ( - - + + - - + + ); }; diff --git a/app/components/Views/QRScanner/__snapshots__/index.test.js.snap b/app/components/Views/QRScanner/__snapshots__/index.test.js.snap index 4332e2d6208..8846c353cd8 100644 --- a/app/components/Views/QRScanner/__snapshots__/index.test.js.snap +++ b/app/components/Views/QRScanner/__snapshots__/index.test.js.snap @@ -4,7 +4,7 @@ exports[`QrScanner should render correctly 1`] = ` getOnboardingNavbarOptions(navigation); + static navigationOptions = ({ navigation }) => + getNavigationOptionsTitle(strings('password_reset.change_password'), navigation); static propTypes = { /** @@ -400,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 { @@ -426,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++) { @@ -442,16 +444,17 @@ 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); } else { - PreferencesController.setSelectedAddress(hdKeyring[0]); + PreferencesController.setSelectedAddress(hdKeyring.accounts[0]); } }; 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/AddressElement/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/AddressElement/__snapshots__/index.test.js.snap index ea86e74defe..fe1a33ccc96 100644 --- a/app/components/Views/SendFlow/AddressElement/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/AddressElement/__snapshots__/index.test.js.snap @@ -37,7 +37,7 @@ exports[`AddressElement should render correctly 1`] = ` numberOfLines={1} style={ Object { - "color": "#000000", + "color": "#24292E", "flex": 1, "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, 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/Amount/__snapshots__/index.test.js.snap b/app/components/Views/SendFlow/Amount/__snapshots__/index.test.js.snap index 63bca618303..b51c9b1fe36 100644 --- a/app/components/Views/SendFlow/Amount/__snapshots__/index.test.js.snap +++ b/app/components/Views/SendFlow/Amount/__snapshots__/index.test.js.snap @@ -56,6 +56,8 @@ exports[`Amount should render correctly 1`] = ` "backgroundColor": "#037dd6", "borderRadius": 100, "flexDirection": "row", + "fontFamily": "EuclidCircularB-Regular", + "fontWeight": "400", "paddingHorizontal": 16, "paddingVertical": 2, } @@ -147,7 +149,8 @@ exports[`Amount should render correctly 1`] = ` placeholder="0" style={ Object { - "fontFamily": "Roboto-Light", + "color": "#24292E", + "fontFamily": "EuclidCircularB-Regular", "fontSize": 44, "fontWeight": "300", "textAlign": "center", diff --git a/app/components/Views/SendFlow/Amount/index.js b/app/components/Views/SendFlow/Amount/index.js index b8b98cbebfd..6f64eb6bf7d 100644 --- a/app/components/Views/SendFlow/Amount/index.js +++ b/app/components/Views/SendFlow/Amount/index.js @@ -41,18 +41,18 @@ import { import { getTicker, generateTransferData, getEther } from '../../../../util/transactions'; import { util } from '@metamask/controllers'; import ErrorMessage from '../ErrorMessage'; -import { fetchBasicGasEstimates, convertApiValueToGWEI } from '../../../../util/custom-gas'; +import { getGasPriceByChainId } from '../../../../util/custom-gas'; import Engine from '../../../../core/Engine'; import CollectibleImage from '../../../UI/CollectibleImage'; import collectiblesTransferInformation from '../../../../util/collectibles-transfer'; import { strings } from '../../../../../locales/i18n'; -import TransactionTypes from '../../../../core/TransactionTypes'; import Device from '../../../../util/Device'; import { BN } from 'ethereumjs-util'; 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; @@ -91,6 +91,7 @@ const styles = StyleSheet.create({ flex: 0.8 }, actionDropdown: { + ...fontStyles.normal, backgroundColor: colors.blue, paddingHorizontal: 16, paddingVertical: 2, @@ -128,7 +129,7 @@ const styles = StyleSheet.create({ flexDirection: 'row' }, inputCurrencyText: { - fontFamily: 'Roboto-Light', + ...fontStyles.normal, fontWeight: fontStyles.light.fontWeight, color: colors.black, fontSize: 44, @@ -139,10 +140,11 @@ const styles = StyleSheet.create({ textTransform: 'uppercase' }, textInput: { - fontFamily: 'Roboto-Light', + ...fontStyles.normal, fontWeight: fontStyles.light.fontWeight, fontSize: 44, - textAlign: 'center' + textAlign: 'center', + color: colors.black }, switch: { flex: 1, @@ -329,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 */ @@ -398,7 +404,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({ @@ -614,27 +620,15 @@ class Amount extends PureComponent { * Estimate transaction gas with information available */ estimateTransactionTotalGas = async () => { - 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); }; @@ -675,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 @@ -689,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( @@ -703,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)}`; @@ -746,7 +740,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) { @@ -778,6 +771,7 @@ class Amount extends PureComponent { renderToken = (token, index) => { const { accounts, + chainId, selectedAddress, conversionRate, currentCurrency, @@ -788,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 4ef738915b4..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]} + /> Amount - } + totalFiat={ + + } totalGasFiat="" - totalValue={} + totalValue={ + + } transactionValue="" /> Hex Data @@ -285,7 +388,7 @@ exports[`Confirm should render correctly 1`] = ` > @@ -304,10 +407,24 @@ 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 3f42d50dbd8..ba9afcd178e 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 @@ -24,23 +23,22 @@ import { weiToFiatNumber, balanceToFiatNumber, renderFiatAddition, - toWei, isDecimal, toBN } from '../../../../util/number'; import { getTicker, decodeTransferData, getNormalizedTxState } from '../../../../util/transactions'; import StyledButton from '../../../UI/StyledButton'; -import { util } from '@metamask/controllers'; -import { prepareTransaction, resetTransaction } from '../../../../actions/transaction'; +import { util, WalletDevice } from '@metamask/controllers'; +import { prepareTransaction, resetTransaction, setNonce, setProposedNonce } from '../../../../actions/transaction'; import { - fetchBasicGasEstimates, - convertApiValueToGWEI, apiEstimateModifiedToWEI, - getBasicGasEstimates + getGasPriceByChainId, + getBasicGasEstimatesByChainId } from '../../../../util/custom-gas'; 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'; @@ -54,16 +52,19 @@ 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'; +import { collectConfusables } from '../../../../util/validators'; +import InfoModal from '../../../UI/Swaps/components/InfoModal'; +import { toChecksumAddress } from 'ethereumjs-util'; const EDIT = 'edit'; +const EDIT_NONCE = 'edit_nonce'; const REVIEW = 'review'; const { hexToBN, BNToHex } = util; -const { - CUSTOM_GAS: { AVERAGE_GAS, FAST_GAS, LOW_GAS } -} = TransactionTypes; const styles = StyleSheet.create({ wrapper: { @@ -89,7 +90,7 @@ const styles = StyleSheet.create({ marginVertical: 3 }, textAmount: { - fontFamily: 'Roboto-Light', + ...fontStyles.normal, fontWeight: fontStyles.light.fontWeight, color: colors.black, fontSize: 44, @@ -213,6 +214,9 @@ const styles = StyleSheet.create({ over: { color: colors.red, ...fontStyles.bold + }, + text: { + lineHeight: 20 } }); @@ -232,6 +236,10 @@ class Confirm extends PureComponent { * Map of accounts to information objects including balances */ accounts: PropTypes.object, + /** + * Map representing the address book + */ + addressBook: PropTypes.object, /** * Object containing token balances in the format address => balance */ @@ -264,6 +272,10 @@ class Confirm extends PureComponent { * Set transaction object to be sent */ prepareTransaction: PropTypes.func, + /** + * Chain Id + */ + chainId: PropTypes.string, /** * Network id */ @@ -272,6 +284,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 */ @@ -295,10 +311,19 @@ 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 = { + confusableCollection: [], gasSpeedSelected: 'average', gasEstimationReady: false, customGas: undefined, @@ -317,14 +342,59 @@ class Confirm extends PureComponent { transactionTotalAmountFiat: undefined, errorMessage: undefined, fromAccountModalVisible: false, + warningModalVisible: false, mode: REVIEW, 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; + 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 = () => { + try { + const { selectedAsset } = this.props; + return { + active_currency: { value: selectedAsset.symbol, anonymous: true } + }; + } catch (error) { + return {}; + } + }; + + handleConfusables = async () => { + const { transactionToName } = this.props.transactionState; + await this.setState({ confusableCollection: collectConfusables(transactionToName) }); + }; + + toggleWarningModal = () => this.setState(state => ({ warningModalVisible: !state.warningModalVisible })); + componentDidMount = async () => { // For analytics - const { navigation, providerType } = this.props; + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SEND_TRANSACTION_STARTED, this.getAnalyticsParams()); + + const { showCustomNonce, navigation, providerType } = this.props; await this.handleFetchBasicEstimates(); + showCustomNonce && (await this.setNetworkNonce()); + await this.handleConfusables(); navigation.setParams({ providerType }); this.parseTransactionData(); this.prepareTransaction(); @@ -362,8 +432,8 @@ class Confirm extends PureComponent { this.onModeChange(REVIEW); }; - edit = () => { - this.onModeChange(EDIT); + edit = MODE => { + this.onModeChange(MODE); }; onModeChange = mode => { @@ -377,9 +447,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 +466,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 = () => { @@ -576,13 +632,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; }; @@ -657,7 +716,6 @@ class Confirm extends PureComponent { const { transactionState: { assetType }, navigation, - providerType, resetTransaction } = this.props; this.setState({ transactionConfirmed: true }); @@ -673,9 +731,9 @@ 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)); @@ -689,9 +747,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(); }); @@ -779,6 +838,8 @@ class Confirm extends PureComponent { mode={mode} onPress={this.handleSetGasSpeed} gasSpeedSelected={gasSpeedSelected} + view={'SendTo (Confirm)'} + analyticsParams={this.getGasAnalyticsParams()} /> @@ -786,6 +847,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; @@ -857,7 +931,8 @@ class Confirm extends PureComponent { render = () => { const { transactionToName, selectedAsset, paymentRequest } = this.props.transactionState; - const { showHexData, primaryCurrency, network } = this.props; + const { addressBook, showHexData, showCustomNonce, primaryCurrency, network, chainId } = this.props; + const { nonce } = this.props.transaction; const { gasEstimationReady, fromAccountBalance, @@ -873,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)); @@ -892,14 +993,16 @@ class Confirm extends PureComponent { fromAccountName={fromAccountName} fromAccountBalance={fromAccountBalance} /> - + + {strings('transaction.confusable_msg')}} + /> + {!selectedAsset.tokenId ? ( @@ -907,7 +1010,7 @@ class Confirm extends PureComponent { {transactionValue} - {transactionValueFiat} + {isMainNet(chainId) && {transactionValueFiat}} ) : ( @@ -928,15 +1031,18 @@ class Confirm extends PureComponent { } fiat={transactionValueFiat} totalValue={transactionTotalAmount} 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 && ( @@ -979,6 +1085,7 @@ class Confirm extends PureComponent { {this.renderFromAccountModal()} {mode === EDIT && this.renderCustomGasModal()} + {mode === EDIT_NONCE && this.renderCustomNonceModal()} {this.renderHexDataModal()} ); @@ -987,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, @@ -995,6 +1103,8 @@ const mapStateToProps = state => ({ identities: state.engine.backgroundState.PreferencesController.identities, 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), @@ -1005,7 +1115,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..621c6cfde6f 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`] = ` > Add to address book Enter an alias @@ -219,7 +252,7 @@ exports[`SendTo should render correctly 1`] = ` spellCheck={false} style={ Object { - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", "fontSize": 20, "fontWeight": "400", 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 && ( + + + 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/components/Views/Settings/Contacts/ContactForm/__snapshots__/index.test.js.snap b/app/components/Views/Settings/Contacts/ContactForm/__snapshots__/index.test.js.snap index c37bd5ce870..d176f0e713e 100644 --- a/app/components/Views/Settings/Contacts/ContactForm/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/Contacts/ContactForm/__snapshots__/index.test.js.snap @@ -130,7 +130,7 @@ exports[`ContactForm should render correctly 1`] = ` style={ Array [ Object { - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", "fontWeight": "400", "padding": 0, @@ -217,7 +217,7 @@ exports[`ContactForm should render correctly 1`] = ` style={ Array [ Object { - "color": "#000000", + "color": "#24292E", "fontFamily": "EuclidCircularB-Regular", "fontWeight": "400", "padding": 0, diff --git a/app/components/Views/Settings/GeneralSettings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/GeneralSettings/__snapshots__/index.test.js.snap index 157dc464f67..faea4ed4e5f 100644 --- a/app/components/Views/Settings/GeneralSettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/GeneralSettings/__snapshots__/index.test.js.snap @@ -675,7 +675,7 @@ exports[`GeneralSettings should render correctly 1`] = ` "marginHorizontal": 10, }, Object { - "color": "#000000", + "color": "#24292E", }, ] } 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/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap index 2cade6fbd2f..7f7062dde0d 100644 --- a/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/SecuritySettings/__snapshots__/index.test.js.snap @@ -86,7 +86,7 @@ exports[`SecuritySettings should render correctly 1`] = ` style={ Array [ Object { - "color": "#000000", + "color": "#24292E", "flex": 1, "fontFamily": "EuclidCircularB-Regular", "fontSize": 12, 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/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/AppConstants.js b/app/core/AppConstants.js index 48c3d4ca9ff..d0341617c51 100644 --- a/app/core/AppConstants.js +++ b/app/core/AppConstants.js @@ -61,11 +61,18 @@ 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: { 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/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/app/core/DeeplinkManager.js b/app/core/DeeplinkManager.js index 5e5cf90b337..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) { @@ -63,9 +64,12 @@ 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); + TransactionController.addTransaction(txParams, origin, WalletDevice.MM_MOBILE); } } diff --git a/app/core/Engine.js b/app/core/Engine.js index c0f46a229ab..2810a0ca719 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -15,10 +15,11 @@ import { TokenBalancesController, TokenRatesController, TransactionController, - TypedMessageManager + TypedMessageManager, + WalletDevice } from '@metamask/controllers'; -import { SwapsController } from '@estebanmino/controllers'; +import { SwapsController } from '@metamask/swaps-controller'; import AsyncStorage from '@react-native-community/async-storage'; @@ -33,7 +34,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; @@ -64,70 +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 || 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 - )).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 }) - ], - 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(); @@ -152,16 +221,18 @@ class Engine { AccountTrackerController, AssetsContractController, AssetsDetectionController, - NetworkController: { provider }, + NetworkController: { provider, state: NetworkControllerState }, TransactionController, SwapsController - } = this.datamodel.context; + } = this.context; 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 }); @@ -172,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]; @@ -233,15 +304,16 @@ class Engine { AssetsController, TokenBalancesController, TokenRatesController - } = this.datamodel.context; + } = this.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; + const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; if (accounts[selectedAddress]) { - ethFiat = weiToFiatNumber(accounts[selectedAddress].balance, conversionRate); + ethFiat = weiToFiatNumber(accounts[selectedAddress].balance, conversionRate, decimalsToShow); } if (tokens.length > 0) { const { contractBalances: tokenBalances } = TokenBalancesController.state; @@ -253,7 +325,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; }); } @@ -293,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({ @@ -331,7 +403,7 @@ class Engine { NetworkController, TransactionController, AssetsController - } = this.datamodel.context; + } = this.context; // Select same network ? await NetworkController.setProviderType(network.provider.type); @@ -380,6 +452,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); @@ -418,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/app/core/RPCMethods/wallet_addEthereumChain.js b/app/core/RPCMethods/wallet_addEthereumChain.js index d33e50d1870..feceb2d88af 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/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/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/images/bnb-logo.png b/app/images/bnb-logo.png new file mode 100644 index 00000000000..3712a470d2f Binary files /dev/null and b/app/images/bnb-logo.png differ 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/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/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/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/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/app/styles/common.js b/app/styles/common.js index d6431dd19e9..fbde6595fab 100644 --- a/app/styles/common.js +++ b/app/styles/common.js @@ -12,7 +12,7 @@ export const colors = { fontError: '#D73A49', fontWarning: '#f66a0a', primaryFox: '#f66a0a', - black: '#000000', + black: '#24292E', white: '#FFFFFF', white100: '#F9FAFB', grey450: '#8E8E93', diff --git a/app/util/address.js b/app/util/address.js index 842a597adbd..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])) { - 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/app/util/analyticsV2.js b/app/util/analyticsV2.js new file mode 100644 index 00000000000..c1083b179fe --- /dev/null +++ b/app/util/analyticsV2.js @@ -0,0 +1,94 @@ +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'), + // 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) => { + InteractionManager.runAfterInteractions(() => { + try { + if (!params) { + Analytics.trackEvent(eventName); + } + + const userParams = {}; + const anonymousParams = {}; + + for (const key in params) { + const property = params[key]; + + if (property && 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; + } + } else { + // Non-anonymous properties - add to both + userParams[key] = property; + anonymousParams[key] = property; + } + } + + // 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/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; } 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/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 ab859d33e33..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 @@ -72,6 +74,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) { @@ -156,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/app/util/number.js b/app/util/number.js index 33aed748ef5..13131b7227b 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 * @@ -321,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}`; @@ -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/app/util/password.js b/app/util/password.js index e5d3ee89cfd..b7d81a1e390 100644 --- a/app/util/password.js +++ b/app/util/password.js @@ -1,4 +1,4 @@ -const MIN_PASSWORD_LENGTH = 8; +export const MIN_PASSWORD_LENGTH = 8; export const getPasswordStrengthWord = strength => { switch (strength) { case 0: 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); + }); +}); 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/app/util/transactions.js b/app/util/transactions.js index 30d5243cc45..42d5aa9815d 100644 --- a/app/util/transactions.js +++ b/app/util/transactions.js @@ -5,9 +5,10 @@ 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'; +import { isMainnetByChainId } from './networks'; const { SAI_ADDRESS } = AppConstants; export const TOKEN_METHOD_TRANSFER = 'transfer'; @@ -41,7 +42,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 @@ -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; @@ -250,12 +252,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 +285,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 +293,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 +322,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; @@ -408,6 +412,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/app/util/validators.js b/app/util/validators.js index 25195c8e272..1eb1f24207e 100644 --- a/app/util/validators.js +++ b/app/util/validators.js @@ -1,4 +1,5 @@ import { ethers } from 'ethers'; +import { confusables } from 'unicode-confusables'; export const failedSeedPhraseRequirements = seed => { 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/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/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) diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index d610c89a9bf..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 = 605; + CURRENT_PROJECT_VERSION = 613; 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.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 = 605; + CURRENT_PROJECT_VERSION = 613; 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.2.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", 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" diff --git a/locales/languages/en.json b/locales/languages/en.json index c51c0708d17..2067e452115 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", @@ -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", @@ -429,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", @@ -590,6 +593,7 @@ "reject": "Reject", "edit": "Edit", "cancel": "Cancel", + "save": "Save", "speedup": "Speed up", "from": "From", "gas_fee": "Network fee", @@ -597,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", @@ -660,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", @@ -812,7 +824,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", @@ -1247,8 +1263,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!", @@ -1354,6 +1371,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/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 f8c08c1d07a..d9e9c954ae1 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -314,9 +314,10 @@ "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_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!" }, "token": { "token_symbol": "Símbolo del token", @@ -1291,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 afb8c033072..f1ea43747bd 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": "टोकन का प्रतीक", @@ -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 202b22b59db..bd98a57fc64 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", @@ -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 6cb7b5c2e8e..8bade3dfb1f 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": "トークン シンボル", @@ -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 5450959fe90..4ef4f20c992 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": "토큰 기호", @@ -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 94d4a9ba48f..f47d2e8f816 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", @@ -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 abbaf14093b..15144420bfb 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": "Символ токена", @@ -1288,7 +1288,7 @@ }, "offline_mode": { "title": "Вы не в сети", - "text": "Проверьте подключение к интернету и попробуйте еще раз", + "text": "Невозможно подключиться к хосту блокчейна.", "try_again": "Попробуйте еще раз" }, "payment_channel_request": { 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..22bf24e6d65 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", @@ -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/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": "代币符号", diff --git a/package.json b/package.json index 655c9a26b13..96ed4b44b85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "2.0.1", + "version": "2.2.0", "private": true, "scripts": { "watch": "./scripts/build.sh watcher watch", @@ -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", @@ -71,17 +73,18 @@ "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": "^6.1.1", + "@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", "@react-native-community/blur": "^3.6.0", "@react-native-community/checkbox": "^0.4.2", "@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", @@ -117,7 +120,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", @@ -170,12 +173,13 @@ "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", "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", @@ -194,10 +198,11 @@ "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", - "vm-browserify": "0.0.4", + "vm-browserify": "1.1.2", "web3-provider-engine": "^16.0.1", "zxcvbn": "4.4.2" }, @@ -208,22 +213,22 @@ "@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", "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.0", + "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-react": "7.16.0", "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", @@ -330,7 +335,7 @@ "fs": "react-native-level-fs" }, "engines": { - "node": "^10.17.0", + "node": "^14.0.0", "yarn": "^1.22.0" }, "rnpm": { 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/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; diff --git a/yarn.lock b/yarn.lock index abe97353c8d..582ba06b42b 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.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== @@ -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== @@ -921,34 +871,20 @@ 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== +"@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: - "@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" + 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" @@ -1598,22 +1534,23 @@ 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== -"@metamask/controllers@^6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-6.1.1.tgz#9ce6b2d58e7205ad7e5bf30449f84385c6bed4bd" - integrity sha512-RZ4YRT34+uV2dk1pITKc7+F4zvd5jJO3y3U1xP16C/ATu1rSw57EM4pOlJmERTbRZ2ImP8wFZm9e4wpbpQuUQg== +"@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" eth-ens-namehash "^2.0.8" @@ -1624,8 +1561,10 @@ eth-query "^2.1.2" eth-rpc-errors "^4.0.0" eth-sig-util "^3.0.0" + ethereumjs-tx "^1.3.7" ethereumjs-util "^6.1.0" ethereumjs-wallet "^1.0.1" + ethjs-util "^0.1.6" human-standard-collectible-abi "^1.0.2" human-standard-token-abi "^2.0.0" immer "^8.0.1" @@ -1637,6 +1576,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" @@ -1647,6 +1591,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" @@ -1837,10 +1810,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" @@ -2145,6 +2118,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/uuid@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -2340,7 +2318,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== @@ -2360,7 +2338,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== @@ -2437,6 +2415,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" @@ -2821,18 +2819,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" @@ -3671,11 +3657,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" @@ -4080,7 +4061,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== @@ -4284,7 +4265,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= @@ -4638,7 +4619,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== @@ -4942,10 +4923,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" @@ -5044,14 +5025,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== @@ -5063,6 +5052,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" @@ -5105,46 +5104,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" @@ -5157,14 +5156,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" @@ -5183,6 +5182,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" @@ -5190,6 +5196,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" @@ -5200,6 +5213,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" @@ -5855,7 +5873,7 @@ ethjs-util@0.1.3: is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" -ethjs-util@0.1.6, ethjs-util@^0.1.3: +ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -6201,7 +6219,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== @@ -6215,6 +6233,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" @@ -6303,11 +6328,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" @@ -6643,6 +6681,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" @@ -7029,7 +7074,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= @@ -7101,25 +7146,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" @@ -7914,10 +7940,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" @@ -7931,14 +7960,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" @@ -8187,13 +8208,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" @@ -8205,6 +8226,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" @@ -8519,6 +8545,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" @@ -8603,11 +8637,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" @@ -8638,6 +8682,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" @@ -8648,6 +8697,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" @@ -9523,7 +9577,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== @@ -9974,7 +10028,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== @@ -9986,6 +10040,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" @@ -10459,6 +10525,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" @@ -11140,10 +11211,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" @@ -11190,6 +11261,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" @@ -11530,7 +11608,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== @@ -11638,6 +11716,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" @@ -11742,7 +11825,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== @@ -11819,7 +11902,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== @@ -11865,13 +11948,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" @@ -12044,11 +12120,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" @@ -12711,7 +12794,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== @@ -12804,6 +12887,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" @@ -13032,11 +13127,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" @@ -13066,6 +13166,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" @@ -13083,6 +13190,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" @@ -13138,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" @@ -13368,12 +13485,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" @@ -13457,6 +13572,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" @@ -13484,6 +13604,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" @@ -13587,7 +13716,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==