diff --git a/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java b/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java index 62c2409e175..c6fce4ffcb2 100644 --- a/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java +++ b/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java @@ -54,6 +54,16 @@ public void trackEvent(ReadableMap e) { this.mixpanel.track(eventCategory, props); } + @ReactMethod + public void trackEventAnonymously(ReadableMap e) { + String eventCategory = e.getString("category"); + String distinctId = this.mixpanel.getDistinctId(); + this.mixpanel.identify("0x0000000000000000"); + JSONObject props = toJSONObject(e); + this.mixpanel.track(eventCategory, props); + this.mixpanel.identify(distinctId); + } + @ReactMethod public void getDistinctId(Promise promise) { String distinctId = this.mixpanel.getDistinctId(); diff --git a/app/components/Base/Keypad/components.js b/app/components/Base/Keypad/components.js index 6e3c30e9027..1a78aecf960 100644 --- a/app/components/Base/Keypad/components.js +++ b/app/components/Base/Keypad/components.js @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ }, keypadButton: { paddingHorizontal: 20, - paddingVertical: Device.isMediumDevice() ? (Device.isIphone5() ? 5 : 10) : 12, + paddingVertical: Device.isMediumDevice() ? (Device.isIphone5() ? 4 : 8) : 12, flex: 1, justifyContent: 'center', alignItems: 'center' diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 465e3cfb1c9..d9bd8db1849 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -269,13 +269,15 @@ const Main = props => { delete newSwapsTransactions[transactionMeta.id].paramsForAnalytics; InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(event, { + const parameters = { ...analyticsParams, time_to_mine: timeToMine, estimated_vs_used_gasRatio: estimatedVsUsedGasRatio, quote_vs_executionRatio: quoteVsExecutionRatio, - token_to_amount_received: tokenToAmountReceived - }); + token_to_amount_received: tokenToAmountReceived.toString() + }; + Analytics.trackEventWithParameters(event, {}); + Analytics.trackEventWithParameters(event, parameters, true); }); } catch (e) { Logger.error(e, ANALYTICS_EVENT_OPTS.SWAP_TRACKING_FAILED); diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index a90a2649457..97cecaf15c9 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -27,6 +27,7 @@ import AssetActionButton from '../AssetActionButton'; import EthereumAddress from '../EthereumAddress'; import { colors, fontStyles, baseStyles } from '../../../styles/common'; import { allowedToBuy } from '../FiatOrders'; +import AssetSwapButton from '../Swaps/components/AssetSwapButton'; const styles = StyleSheet.create({ scrollView: { @@ -351,13 +352,11 @@ class AccountOverview extends PureComponent { label={strings('asset_overview.send_button')} /> {AppConstants.SWAPS.ACTIVE && ( - )} diff --git a/app/components/UI/AssetOverview/index.js b/app/components/UI/AssetOverview/index.js index 7ae3afb4564..8ecd66229f0 100644 --- a/app/components/UI/AssetOverview/index.js +++ b/app/components/UI/AssetOverview/index.js @@ -21,6 +21,7 @@ import Logger from '../../../util/Logger'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; import { allowedToBuy } from '../FiatOrders'; +import AssetSwapButton from '../Swaps/components/AssetSwapButton'; const styles = StyleSheet.create({ wrapper: { @@ -297,14 +298,10 @@ class AssetOverview extends PureComponent { label={strings('asset_overview.send_button')} /> {AppConstants.SWAPS.ACTIVE && ( - )} diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index 9935e63d620..6c25be7e6af 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -225,6 +225,10 @@ class CustomGas extends PureComponent { * Object BN containing estimated gas limit */ gas: PropTypes.object, + /** + * Gas limit estimation that should be the minimum value to set + */ + minimumGasLimit: PropTypes.string, /** * Object BN containing gas price */ @@ -417,7 +421,10 @@ class CustomGas extends PureComponent { else if (bnValue && !isBN(bnValue)) warningGasLimit = strings('transaction.invalid_gas'); else if (bnValue.lt(new BN(21000)) || bnValue.gt(new BN(7920028))) warningGasLimit = strings('custom_gas.warning_gas_limit'); - + else if (this.props.minimumGasLimit && bnValue.lt(new BN(this.props.minimumGasLimit))) + warningGasLimit = strings('custom_gas.warning_gas_limit_estimated', { + gas: this.props.minimumGasLimit.toString(10) + }); this.setState({ customGasLimit: value, customGasLimitBN: bnValue, @@ -450,7 +457,7 @@ class CustomGas extends PureComponent { //Handle gas fee selection when save button is pressed instead of everytime a change is made, otherwise cannot switch back to review mode if there is an error saveCustomGasSelection = () => { - const { gasSpeedSelected, customGasLimit, customGasPrice } = this.state; + const { gasSpeedSelected, customGasLimit, customGasPriceBNWei } = this.state; const { review, gas, @@ -459,7 +466,7 @@ class CustomGas extends PureComponent { basicGasEstimates: { fastGwei, averageGwei, safeLowGwei } } = this.props; if (advancedCustomGas) { - handleGasFeeSelection(new BN(customGasLimit), apiEstimateModifiedToWEI(customGasPrice), { + handleGasFeeSelection(new BN(customGasLimit), customGasPriceBNWei, { mode: 'advanced' }); } else { diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index 4b49f1c2905..6b8704a56b0 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -6,7 +6,6 @@ import IonicIcon from 'react-native-vector-icons/Ionicons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import FAIcon from 'react-native-vector-icons/FontAwesome'; import BigNumber from 'bignumber.js'; -import { toChecksumAddress } from 'ethereumjs-util'; import { NavigationContext } from 'react-navigation'; import { swapsUtils, util } from '@estebanmino/controllers'; @@ -20,8 +19,8 @@ import { toWei, weiToFiat } from '../../../util/number'; -import { apiEstimateModifiedToWEI } from '../../../util/custom-gas'; -import { getErrorMessage, getFetchParams, getQuotesNavigationsParams } from './utils'; +import { safeToChecksumAddress } from '../../../util/address'; +import { getErrorMessage, getFetchParams, getQuotesNavigationsParams, isSwapsETH } from './utils'; import { colors } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; @@ -210,7 +209,7 @@ async function resetAndStartPolling({ slippage, sourceToken, destinationToken, s // ff the token is not in the wallet, we'll add it if ( destinationToken.address !== swapsUtils.ETH_SWAPS_TOKEN_ADDRESS && - !contractExchangeRates[toChecksumAddress(destinationToken.address)] + !contractExchangeRates[safeToChecksumAddress(destinationToken.address)] ) { const { address, symbol, decimals } = destinationToken; await AssetsController.addToken(address, symbol, decimals); @@ -221,7 +220,7 @@ async function resetAndStartPolling({ slippage, sourceToken, destinationToken, s ); } const destinationTokenConversionRate = - TokenRatesController.state.contractExchangeRates[toChecksumAddress(destinationToken.address)] || 0; + TokenRatesController.state.contractExchangeRates[safeToChecksumAddress(destinationToken.address)] || 0; // TODO: destinationToken could be the 0 address for ETH, also tokens that aren't on the wallet const fetchParams = getFetchParams({ @@ -243,10 +242,7 @@ async function resetAndStartPolling({ slippage, sourceToken, destinationToken, s */ const gasLimitWithMultiplier = (gasLimit, multiplier) => { if (!gasLimit || !multiplier) return; - return new BigNumber(gasLimit) - .times(multiplier) - .integerValue() - .toString(16); + return new BigNumber(gasLimit).times(multiplier).integerValue(); }; function SwapsQuotesView({ @@ -265,7 +261,8 @@ function SwapsQuotesView({ quotes, quoteValues, error, - quoteRefreshSeconds + quoteRefreshSeconds, + usedGasPrice }) { const navigation = useContext(NavigationContext); /* Get params from navigation */ @@ -280,6 +277,15 @@ function SwapsQuotesView({ token => token.address?.toLowerCase() === destinationTokenAddress.toLowerCase() ); + const hasConversionRate = + Boolean(destinationToken) && + (isSwapsETH(destinationToken) || + Boolean( + Engine.context.TokenRatesController.state.contractExchangeRates?.[ + safeToChecksumAddress(destinationToken.address) + ] + )); + /* State */ const [firstLoadTime, setFirstLoadTime] = useState(Date.now()); const [isFirstLoad, setIsFirstLoad] = useState(true); @@ -312,70 +318,56 @@ function SwapsQuotesView({ return []; } - const orderedValues = Object.values(quoteValues).sort( - (a, b) => Number(b.overallValueOfQuote) - Number(a.overallValueOfQuote) - ); + const orderedAggregators = hasConversionRate + ? Object.values(quoteValues).sort((a, b) => Number(b.overallValueOfQuote) - Number(a.overallValueOfQuote)) + : Object.values(quotes).sort((a, b) => new BigNumber(b.destinationAmount).comparedTo(a.destinationAmount)); - return orderedValues.map(quoteValue => quotes[quoteValue.aggregator]); - }, [quoteValues, quotes]); + return orderedAggregators.map(quoteValue => quotes[quoteValue.aggregator]); + }, [hasConversionRate, quoteValues, quotes]); /* Get the selected quote, by default is topAggId */ - const selectedQuote = useMemo(() => allQuotes.find(quote => quote.aggregator === selectedQuoteId), [ + const selectedQuote = useMemo(() => allQuotes.find(quote => quote?.aggregator === selectedQuoteId), [ allQuotes, selectedQuoteId ]); - const selectedQuoteValue = useMemo(() => quoteValues[selectedQuoteId], [quoteValues, selectedQuoteId]); + const selectedQuoteValue = useMemo(() => quoteValues[selectedQuoteId], [ + // eslint-disable-next-line react-hooks/exhaustive-deps + quoteValues[selectedQuoteId], + quoteValues, + selectedQuoteId + ]); /* gas estimations */ - const gasPrice = useMemo( - () => - customGasPrice - ? customGasPrice.toString(16) - : !!apiGasPrice && apiEstimateModifiedToWEI(apiGasPrice?.averageGwei).toString(16), - [customGasPrice, apiGasPrice] - ); + const gasPrice = useMemo(() => customGasPrice?.toString(16) || usedGasPrice?.toString(16), [ + customGasPrice, + usedGasPrice + ]); const gasLimit = useMemo( () => (Boolean(customGasLimit) && BNToHex(customGasLimit)) || - gasLimitWithMultiplier(selectedQuote?.gasEstimate, selectedQuote?.gasMultiplier) || + gasLimitWithMultiplier(selectedQuote?.gasEstimate, selectedQuote?.gasMultiplier)?.toString(16) || selectedQuote?.maxGas?.toString(16), [customGasLimit, selectedQuote] ); - /* Total gas fee in decimal */ - const gasFee = useMemo(() => { - if (customGasPrice) { - return util.calcTokenAmount(customGasPrice * gasLimit, 18); - } - return selectedQuoteValue?.ethFee; - }, [selectedQuoteValue, customGasPrice, gasLimit]); - - /* Maximum gas fee in decimal */ - const maxGasFee = useMemo(() => { - if (customGasPrice && selectedQuote?.maxGas) { - return util.calcTokenAmount(customGasPrice * selectedQuote?.maxGas, 18); - } - return selectedQuoteValue?.maxEthFee; - }, [selectedQuote, selectedQuoteValue, customGasPrice]); - /* Balance */ const balance = useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits: true }); const [hasEnoughTokenBalance, missingTokenBalance, hasEnoughEthBalance, missingEthBalance] = useMemo(() => { // Token const sourceBN = new BigNumber(sourceAmount); - const tokenBalanceBN = new BigNumber(balance.toString()); + const tokenBalanceBN = new BigNumber(balance.toString(10)); 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 ethBalanceBN = new BigNumber(accounts[selectedAddress].balance); - const gasBN = new BigNumber((maxGasFee && toWei(maxGasFee)) || 0); + const gasBN = new BigNumber(selectedQuoteValue?.maxEthFee || '0x0'); const hasEnoughEthBalance = ethBalanceBN.gte(gasBN.plus(ethAmountBN)); const missingEthBalance = hasEnoughEthBalance ? null : gasBN.plus(ethAmountBN).minus(ethBalanceBN); return [hasEnoughTokenBalance, missingTokenBalance, hasEnoughEthBalance, missingEthBalance]; - }, [accounts, balance, maxGasFee, selectedAddress, sourceAmount, sourceToken.address]); + }, [accounts, balance, selectedQuoteValue, selectedAddress, sourceAmount, sourceToken.address]); /* Selected quote slippage */ const shouldDisplaySlippage = useMemo( @@ -475,15 +467,18 @@ function SwapsQuotesView({ ), analytics: { token_from: sourceToken.symbol, - token_from_amount: sourceAmount, + token_from_amount: fromTokenMinimalUnitString(sourceAmount, sourceToken.decimals), token_to: destinationToken.symbol, - token_to_amount: selectedQuote.destinationAmount, + token_to_amount: fromTokenMinimalUnitString( + selectedQuote.destinationAmount, + destinationToken.decimals + ), request_type: hasEnoughTokenBalance ? 'Order' : 'Quote', custom_slippage: slippage !== AppConstants.SWAPS.DEFAULT_SLIPPAGE, best_quote_source: selectedQuote.aggregator, available_quotes: allQuotes.length, - network_fees_USD: weiToFiat(toWei(gasFee), conversionRate, currentCurrency), - network_fees_ETH: renderFromWei(toWei(gasFee)), + network_fees_USD: weiToFiat(toWei(selectedQuoteValue?.ethFee), conversionRate, currentCurrency), + network_fees_ETH: renderFromWei(toWei(selectedQuoteValue?.ethFee)), other_quote_selected: allQuotes[selectedQuoteId] === selectedQuote }, paramsForAnalytics: { @@ -508,7 +503,7 @@ function SwapsQuotesView({ allQuotes, selectedQuoteId, conversionRate, - gasFee + selectedQuoteValue ] ); @@ -518,7 +513,7 @@ function SwapsQuotesView({ } InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SWAP_STARTED, { + const parameters = { token_from: sourceToken.symbol, token_from_amount: fromTokenMinimalUnitString(sourceAmount, sourceToken.decimals), token_to: destinationToken.symbol, @@ -529,9 +524,11 @@ function SwapsQuotesView({ best_quote_source: selectedQuote.aggregator, available_quotes: allQuotes, other_quote_selected: allQuotes[selectedQuoteId] === selectedQuote, - network_fees_USD: weiToFiat(toWei(selectedQuoteValue.ethFee), conversionRate, 'usd'), - network_fees_ETH: renderFromWei(toWei(selectedQuoteValue.ethFee)) - }); + network_fees_USD: weiToFiat(toWei(selectedQuoteValue?.ethFee), conversionRate, 'usd'), + network_fees_ETH: renderFromWei(toWei(selectedQuoteValue?.ethFee)) + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SWAP_STARTED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SWAP_STARTED, parameters, true); }); const { TransactionController } = Engine.context; @@ -600,14 +597,14 @@ function SwapsQuotesView({ const originalApprovalTransactionEncodedAmount = decodeApproveData(originalApprovalTransaction.data) .encodedAmount; const originalAmount = fromTokenMinimalUnitString( - hexToBN(originalApprovalTransactionEncodedAmount).toString(), + hexToBN(originalApprovalTransactionEncodedAmount).toString(10), sourceToken.decimals ); const currentApprovalTransactionEncodedAmount = approvalTransaction ? decodeApproveData(approvalTransaction.data).encodedAmount : '0'; const currentAmount = fromTokenMinimalUnitString( - hexToBN(currentApprovalTransactionEncodedAmount).toString(), + hexToBN(currentApprovalTransactionEncodedAmount).toString(10), sourceToken.decimals ); @@ -615,7 +612,7 @@ function SwapsQuotesView({ setEditQuoteTransactionsVisible(true); InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.EDIT_SPEND_LIMIT_OPENED, { + const parameters = { token_from: sourceToken.symbol, token_from_amount: fromTokenMinimalUnitString(sourceAmount, sourceToken.decimals), token_to: destinationToken.symbol, @@ -626,10 +623,12 @@ function SwapsQuotesView({ available_quotes: allQuotes.length, best_quote_source: selectedQuote.aggregator, other_quote_selected: allQuotes[selectedQuoteId] === selectedQuote, - gas_fees: weiToFiat(toWei(gasFee), conversionRate, currentCurrency), + gas_fees: weiToFiat(toWei(selectedQuoteValue?.ethFee), conversionRate, currentCurrency), custom_spend_limit_set: originalAmount !== currentAmount, custom_spend_limit_amount: currentAmount - }); + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.EDIT_SPEND_LIMIT_OPENED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.EDIT_SPEND_LIMIT_OPENED, parameters, true); }); }, [ allQuotes, @@ -637,7 +636,7 @@ function SwapsQuotesView({ conversionRate, currentCurrency, destinationToken, - gasFee, + selectedQuoteValue, hasEnoughTokenBalance, originalApprovalTransaction, selectedQuote, @@ -648,28 +647,41 @@ function SwapsQuotesView({ ]); const onHandleGasFeeSelection = useCallback( - (gas, gasPrice, details) => { - setCustomGasPrice(gasPrice); - setCustomGasLimit(gas); - InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.GAS_FEES_CHANGED, { - speed_set: details.mode === 'advanced' ? undefined : details.mode, - gas_mode: details.mode === 'advanced' ? 'Advanced' : 'Basic', - gas_fees: weiToFiat( - toWei(util.calcTokenAmount(gasPrice * gas, 18)), - conversionRate, - currentCurrency - ) + (customGasLimit, customGasPrice, details) => { + const { SwapsController } = Engine.context; + const newGasLimit = new BigNumber(customGasLimit); + const newGasPrice = new BigNumber(customGasPrice); + if (newGasPrice.toString(16) !== gasPrice) { + setCustomGasPrice(newGasPrice); + SwapsController.updateQuotesWithGasPrice(newGasPrice.toString(16)); + } + if (newGasLimit.toString(16) !== gasLimit) { + setCustomGasLimit(newGasLimit); + SwapsController.updateSelectedQuoteWithGasLimit(newGasLimit.toString(16)); + } + if (newGasLimit?.toString(16) !== gasLimit || newGasPrice?.toString(16) !== gasPrice) { + InteractionManager.runAfterInteractions(() => { + const parameters = { + speed_set: details.mode === 'advanced' ? undefined : details.mode, + gas_mode: details.mode === 'advanced' ? 'Advanced' : 'Basic', + gas_fees: weiToFiat( + toWei(util.calcTokenAmount(newGasLimit.times(newGasPrice), 18)), + conversionRate, + currentCurrency + ) + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.GAS_FEES_CHANGED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.GAS_FEES_CHANGED, parameters, true); }); - }); + } }, - [conversionRate, currentCurrency] + [conversionRate, currentCurrency, gasLimit, gasPrice] ); const handleQuotesReceivedMetric = useCallback(() => { if (!selectedQuote || !selectedQuoteValue) return; InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_RECEIVED, { + const parameters = { token_from: sourceToken.symbol, token_from_amount: fromTokenMinimalUnitString(sourceAmount, sourceToken.decimals), token_to: destinationToken.symbol, @@ -682,7 +694,9 @@ function SwapsQuotesView({ network_fees_USD: weiToFiat(toWei(selectedQuoteValue.ethFee), conversionRate, 'usd'), network_fees_ETH: renderFromWei(toWei(selectedQuoteValue.ethFee)), available_quotes: allQuotes.length - }); + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_RECEIVED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_RECEIVED, parameters, true); }); }, [ sourceToken, @@ -701,7 +715,7 @@ function SwapsQuotesView({ if (!selectedQuote || !selectedQuoteValue) return; toggleQuotesModal(); InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.ALL_AVAILABLE_QUOTES_OPENED, { + const parameters = { token_from: sourceToken.symbol, token_from_amount: fromTokenMinimalUnitString(sourceAmount, sourceToken.decimals), token_to: destinationToken.symbol, @@ -714,7 +728,9 @@ function SwapsQuotesView({ network_fees_USD: weiToFiat(toWei(selectedQuoteValue.ethFee), conversionRate, 'usd'), network_fees_ETH: renderFromWei(toWei(selectedQuoteValue.ethFee)), available_quotes: allQuotes.length - }); + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.ALL_AVAILABLE_QUOTES_OPENED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.ALL_AVAILABLE_QUOTES_OPENED, parameters, true); }); }, [ selectedQuote, @@ -743,14 +759,18 @@ function SwapsQuotesView({ Logger.error(error?.description, `Swaps: ${error?.key}`); if (error?.key === swapsUtils.SwapsError.QUOTES_EXPIRED_ERROR) { InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_TIMED_OUT, { + const parameters = { ...data, gas_fees: '' - }); + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_TIMED_OUT, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_TIMED_OUT, parameters, true); }); } else if (error?.key === swapsUtils.SwapsError.QUOTES_NOT_AVAILABLE_ERROR) { InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.NO_QUOTES_AVAILABLE, { data }); + const parameters = { data }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.NO_QUOTES_AVAILABLE, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.NO_QUOTES_AVAILABLE, parameters, true); }); } }, @@ -822,7 +842,7 @@ function SwapsQuotesView({ useEffect(() => { let maxFetchTime = 0; allQuotes.forEach(quote => { - maxFetchTime = Math.max(maxFetchTime, quote.fetchTime); + maxFetchTime = Math.max(maxFetchTime, quote?.fetchTime); }); setAllQuotesFetchTime(maxFetchTime); }, [allQuotes]); @@ -902,7 +922,8 @@ function SwapsQuotesView({ navigation.setParams({ selectedQuote: undefined }); navigation.setParams({ quoteBegin: Date.now() }); InteractionManager.runAfterInteractions(() => { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_REQUESTED, data); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_REQUESTED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.QUOTES_REQUESTED, data, true); }); }, [ destinationToken, @@ -1166,10 +1187,14 @@ function SwapsQuotesView({ - {renderFromWei(toWei(gasFee))} ETH + {renderFromWei(toWei(selectedQuoteValue?.ethFee))} ETH - {` ${weiToFiat(toWei(gasFee), conversionRate, currentCurrency)}`} + {` ${weiToFiat( + toWei(selectedQuoteValue?.ethFee), + conversionRate, + currentCurrency + )}`} @@ -1186,9 +1211,13 @@ function SwapsQuotesView({ - {renderFromWei(toWei(maxGasFee))} ETH + {renderFromWei(toWei(selectedQuoteValue?.maxEthFee || '0x0'))} ETH - {` ${weiToFiat(toWei(maxGasFee), conversionRate, currentCurrency)}`} + {` ${weiToFiat( + toWei(selectedQuoteValue?.maxEthFee), + conversionRate, + currentCurrency + )}`} @@ -1274,6 +1303,7 @@ function SwapsQuotesView({ sourceToken={sourceToken} destinationToken={destinationToken} selectedQuote={selectedQuoteId} + showOverallValue={hasConversionRate} /> @@ -1330,7 +1364,8 @@ SwapsQuotesView.propTypes = { quoteValues: PropTypes.object, approvalTransaction: PropTypes.object, error: PropTypes.object, - quoteRefreshSeconds: PropTypes.number + quoteRefreshSeconds: PropTypes.number, + usedGasPrice: PropTypes.string }; const mapStateToProps = state => ({ @@ -1349,7 +1384,8 @@ const mapStateToProps = state => ({ quoteValues: state.engine.backgroundState.SwapsController.quoteValues, approvalTransaction: state.engine.backgroundState.SwapsController.approvalTransaction, error: state.engine.backgroundState.SwapsController.error, - quoteRefreshSeconds: state.engine.backgroundState.SwapsController.quoteRefreshSeconds + quoteRefreshSeconds: state.engine.backgroundState.SwapsController.quoteRefreshSeconds, + usedGasPrice: state.engine.backgroundState.SwapsController.usedGasPrice }); export default connect(mapStateToProps)(SwapsQuotesView); diff --git a/app/components/UI/Swaps/components/AssetSwapButton.js b/app/components/UI/Swaps/components/AssetSwapButton.js new file mode 100644 index 00000000000..c8632a4b983 --- /dev/null +++ b/app/components/UI/Swaps/components/AssetSwapButton.js @@ -0,0 +1,54 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View } from 'react-native'; + +import Text from '../../../Base/Text'; +import AssetActionButton from '../../AssetActionButton'; +import InfoModal from './InfoModal'; + +import useModalHandler from '../../../Base/hooks/useModalHandler'; +import { strings } from '../../../../../locales/i18n'; + +const styles = StyleSheet.create({ + disabledButton: { + opacity: 0.5 + } +}); + +function AssetSwapButton({ isFeatureLive, isNetworkAllowed, isAssetAllowed, onPress }) { + const [isModalOpen, , showModal, hideModal] = useModalHandler(false); + const isDisabled = !isFeatureLive || !isNetworkAllowed || !isAssetAllowed; + + const [title, body] = useMemo(() => { + if (!isNetworkAllowed) return [strings('swaps.wrong_network_title'), strings('swaps.wrong_network_body')]; + if (!isAssetAllowed) return [strings('swaps.unallowed_asset_title'), strings('swaps.unallowed_asset_body')]; + if (!isFeatureLive) return [strings('swaps.feature_off_title'), strings('swaps.feature_off_body')]; + return ['', '']; + }, [isAssetAllowed, isFeatureLive, isNetworkAllowed]); + return ( + <> + + + + {body}} + /> + + ); +} + +AssetSwapButton.propTypes = { + isFeatureLive: PropTypes.bool, + isNetworkAllowed: PropTypes.bool, + isAssetAllowed: PropTypes.bool, + onPress: PropTypes.func +}; + +export default AssetSwapButton; diff --git a/app/components/UI/Swaps/components/QuotesModal.js b/app/components/UI/Swaps/components/QuotesModal.js index 58026d75faf..9078cd8ca13 100644 --- a/app/components/UI/Swaps/components/QuotesModal.js +++ b/app/components/UI/Swaps/components/QuotesModal.js @@ -5,6 +5,7 @@ import { SafeAreaView } from 'react-navigation'; import Modal from 'react-native-modal'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import { connect } from 'react-redux'; +import BigNumber from 'bignumber.js'; import { strings } from '../../../../../locales/i18n'; import { fromTokenMinimalUnitString, @@ -135,7 +136,8 @@ function QuotesModal({ destinationToken, conversionRate, currentCurrency, - quoteValues + quoteValues, + showOverallValue }) { const bestOverallValue = quoteValues[quotes[0].aggregator].overallValueOfQuote; const [displayDetails, setDisplayDetails] = useState(false); @@ -386,14 +388,18 @@ function QuotesModal({ {index === 0 ? ( - - - - {strings('swaps.best')} - + showOverallValue ? ( + + + + {strings('swaps.best')} + + - - ) : ( + ) : ( + - + ) + ) : showOverallValue ? ( - {weiToFiat( @@ -407,6 +413,16 @@ function QuotesModal({ currentCurrency )} + ) : ( + + - + {renderFromTokenMinimalUnit( + new BigNumber(quotes[0].destinationAmount) + .minus(quote.destinationAmount) + .toString(10), + destinationToken.decimals + )} + )} ({ diff --git a/app/components/UI/Swaps/components/TokenSelectModal.js b/app/components/UI/Swaps/components/TokenSelectModal.js index 64f5cbaba8d..3af36b70f47 100644 --- a/app/components/UI/Swaps/components/TokenSelectModal.js +++ b/app/components/UI/Swaps/components/TokenSelectModal.js @@ -1,16 +1,16 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, TextInput, SafeAreaView, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TextInput, SafeAreaView, TouchableOpacity, View, TouchableWithoutFeedback } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Modal from 'react-native-modal'; import Icon from 'react-native-vector-icons/Ionicons'; import Fuse from 'fuse.js'; -import { toChecksumAddress } from 'ethereumjs-util'; import { connect } from 'react-redux'; import { swapsUtils } from '@estebanmino/controllers'; import Device from '../../../../util/Device'; import { balanceToFiat, hexToBN, renderFromTokenMinimalUnit, renderFromWei, weiToFiat } from '../../../../util/number'; +import { safeToChecksumAddress } from '../../../../util/address'; import { strings } from '../../../../../locales/i18n'; import { colors, fontStyles } from '../../../../styles/common'; @@ -65,6 +65,8 @@ const styles = StyleSheet.create({ } }); +const MAX_TOKENS_RESULTS = 20; + function TokenSelectModal({ isVisible, dismiss, @@ -81,6 +83,7 @@ function TokenSelectModal({ balances }) { const searchInput = useRef(null); + const list = useRef(); const [searchString, setSearchString] = useState(''); const filteredTokens = useMemo(() => tokens?.filter(token => !excludeAddresses.includes(token.address)), [ @@ -108,13 +111,16 @@ function TokenSelectModal({ [filteredTokens] ); const tokenSearchResults = useMemo( - () => (searchString.length > 0 ? tokenFuse.search(searchString)?.slice(0, 5) : filteredInitialTokens), + () => + searchString.length > 0 + ? tokenFuse.search(searchString)?.slice(0, MAX_TOKENS_RESULTS) + : filteredInitialTokens, [searchString, tokenFuse, filteredInitialTokens] ); const renderItem = useCallback( ({ item }) => { - const itemAddress = toChecksumAddress(item.address); + const itemAddress = safeToChecksumAddress(item.address); let balance, balanceFiat; if (item.address === swapsUtils.ETH_SWAPS_TOKEN_ADDRESS) { @@ -160,6 +166,11 @@ function TokenSelectModal({ [searchString] ); + const handleSearchTextChange = useCallback(text => { + setSearchString(text); + if (list.current) list.current.scrollToOffset({ animated: false, y: 0 }); + }, []); + return ( {title} - - - - + + + + + + { - Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SWAPS_OPENED, { + const parameters = { source: initialSource === SWAPS_ETH_ADDRESS ? 'MainView' : 'TokenView', activeCurrency: swapsTokens?.find( token => token.address?.toLowerCase() === initialSource.toLowerCase() )?.symbol - }); + }; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SWAPS_OPENED, {}); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.SWAPS_OPENED, parameters, true); }); } else { navigation.pop(); @@ -237,6 +250,26 @@ function SwapsAmountView({ setHasDismissedTokenAlert(false); }, [destinationToken]); + const isTokenInBalances = + sourceToken && !isSwapsETH(sourceToken) ? toChecksumAddress(sourceToken.address) in balances : false; + + useEffect(() => { + (async () => { + if (sourceToken && !isSwapsETH(sourceToken) && !isTokenInBalances) { + setContractBalance(null); + setContractBalanceAsUnits(numberToBN(0)); + const { AssetsContractController } = Engine.context; + try { + const balance = await AssetsContractController.getBalanceOf(sourceToken.address, selectedAddress); + setContractBalanceAsUnits(balance); + setContractBalance(renderFromTokenMinimalUnit(balance, sourceToken.decimals)); + } catch (e) { + // Don't validate balance if error + } + } + })(); + }, [isTokenInBalances, selectedAddress, sourceToken]); + const hasInvalidDecimals = useMemo(() => { if (sourceToken) { return amount?.split('.')[1]?.length > sourceToken.decimals; @@ -249,8 +282,12 @@ function SwapsAmountView({ hasInvalidDecimals, sourceToken ]); - const balance = useBalance(accounts, balances, selectedAddress, sourceToken); - const balanceAsUnits = useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits: true }); + const controllerBalance = useBalance(accounts, balances, selectedAddress, sourceToken); + const controllerBalanceAsUnits = useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits: true }); + + const balance = isSwapsETH(sourceToken) || isTokenInBalances ? controllerBalance : contractBalance; + const balanceAsUnits = + isSwapsETH(sourceToken) || isTokenInBalances ? controllerBalanceAsUnits : contractBalanceAsUnits; const hasBalance = useMemo(() => { if (!balanceAsUnits || !sourceToken) { return false; @@ -271,7 +308,7 @@ function SwapsAmountView({ return undefined; } let balanceFiat; - if (sourceToken.address === SWAPS_ETH_ADDRESS) { + if (isSwapsETH(sourceToken)) { balanceFiat = weiToFiat(toTokenMinimalUnit(amount, sourceToken?.decimals), conversionRate, currentCurrency); } else { const sourceAddress = toChecksumAddress(sourceToken.address); @@ -282,27 +319,41 @@ function SwapsAmountView({ }, [amount, conversionRate, currentCurrency, hasInvalidDecimals, sourceToken, tokenExchangeRates]); const destinationTokenHasEnoughOcurrances = useMemo(() => { - if (!destinationToken || destinationToken?.address === SWAPS_ETH_ADDRESS) { + if (!destinationToken || isSwapsETH(destinationToken)) { return true; } return destinationToken?.occurances > TOKEN_MINIMUM_SOURCES; }, [destinationToken]); /* Navigation handler */ - const handleGetQuotesPress = useCallback(() => { + const handleGetQuotesPress = useCallback(async () => { if (hasInvalidDecimals) { return; } + if (!isSwapsETH(sourceToken) && !isTokenInBalances && !balanceAsUnits?.isZero()) { + const { AssetsController } = Engine.context; + const { address, symbol, decimals } = sourceToken; + await AssetsController.addToken(address, symbol, decimals); + } return navigation.navigate( 'SwapsQuotesView', setQuotesNavigationsParams( sourceToken?.address, destinationToken?.address, - toTokenMinimalUnit(amount, sourceToken?.decimals).toString(), + toTokenMinimalUnit(amount, sourceToken?.decimals).toString(10), slippage ) ); - }, [amount, destinationToken, hasInvalidDecimals, navigation, slippage, sourceToken]); + }, [ + amount, + balanceAsUnits, + destinationToken, + hasInvalidDecimals, + isTokenInBalances, + navigation, + slippage, + sourceToken + ]); /* Keypad Handlers */ const handleKeypadChange = useCallback( @@ -336,7 +387,7 @@ function SwapsAmountView({ if (!sourceToken || !balanceAsUnits) { return; } - setAmount(fromTokenMinimalUnitString(balanceAsUnits.toString(), sourceToken.decimals)); + setAmount(fromTokenMinimalUnitString(balanceAsUnits.toString(10), sourceToken.decimals)); }, [balanceAsUnits, sourceToken]); const handleSlippageChange = useCallback(value => { @@ -430,7 +481,7 @@ function SwapsAmountView({ strings('swaps.available_to_swap', { asset: `${balance} ${sourceToken.symbol}` })} - {sourceToken.address !== SWAPS_ETH_ADDRESS && hasBalance && ( + {!isSwapsETH(sourceToken) && hasBalance && ( {' '} {strings('swaps.use_max')} @@ -468,13 +519,13 @@ function SwapsAmountView({ dismiss={toggleDestinationModal} title={strings('swaps.convert_to')} tokens={swapsTokens} - initialTokens={[swapsUtils.ETH_SWAPS_TOKEN_OBJECT, ...tokensTopAssets.slice(0, 5)]} + initialTokens={[swapsUtils.ETH_SWAPS_TOKEN_OBJECT, ...tokensTopAssets.slice(0, MAX_TOP_ASSETS)]} onItemPress={handleDestinationTokenPress} excludeAddresses={[sourceToken?.address]} /> - {Boolean(destinationToken) && destinationToken.symbol !== 'ETH' ? ( + {Boolean(destinationToken) && !isSwapsETH(destinationToken) ? ( destinationTokenHasEnoughOcurrances ? ( diff --git a/app/components/UI/Swaps/utils/index.js b/app/components/UI/Swaps/utils/index.js index 3ebcde802ca..31bf2a6f10c 100644 --- a/app/components/UI/Swaps/utils/index.js +++ b/app/components/UI/Swaps/utils/index.js @@ -3,6 +3,10 @@ import BigNumber from 'bignumber.js'; import { swapsUtils } from '@estebanmino/controllers'; import { strings } from '../../../../../locales/i18n'; +export function isSwapsETH(token) { + return Boolean(token) && token?.address === swapsUtils.ETH_SWAPS_TOKEN_ADDRESS; +} + /** * Sets required parameters for Swaps Quotes View * @param {string} sourceTokenAddress Token contract address used as swaps source diff --git a/app/components/UI/Swaps/utils/useBalance.js b/app/components/UI/Swaps/utils/useBalance.js index ecc423832e6..4d4e09bfee1 100644 --- a/app/components/UI/Swaps/utils/useBalance.js +++ b/app/components/UI/Swaps/utils/useBalance.js @@ -1,8 +1,8 @@ import { swapsUtils } from '@estebanmino/controllers'; -import { toChecksumAddress } from '@walletconnect/utils'; import { useMemo } from 'react'; import numberToBN from 'number-to-bn'; import { renderFromTokenMinimalUnit, renderFromWei } from '../../../../util/number'; +import { safeToChecksumAddress } from '../../../../util/address'; function useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits = false } = {}) { const balance = useMemo(() => { @@ -16,7 +16,7 @@ function useBalance(accounts, balances, selectedAddress, sourceToken, { asUnits } return renderFromWei(accounts[selectedAddress] && accounts[selectedAddress].balance); } - const tokenAddress = toChecksumAddress(sourceToken.address); + const tokenAddress = safeToChecksumAddress(sourceToken.address); if (tokenAddress in balances) { if (asUnits) { diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js index 45a68665cf3..c8a442327b8 100644 --- a/app/components/UI/TransactionElement/utils.js +++ b/app/components/UI/TransactionElement/utils.js @@ -691,7 +691,7 @@ function decodeSwapsTx(args) { ? 1 : contractExchangeRates[safeToChecksumAddress(destinationToken.address)]; const renderDestinationTokenFiatNumber = balanceToFiatNumber( - decimalSourceAmount, + decimalDestinationAmount, conversionRate, destinationExchangeRate ); diff --git a/app/core/Analytics.js b/app/core/Analytics.js index db3b24ccde2..8f1c1f655f7 100644 --- a/app/core/Analytics.js +++ b/app/core/Analytics.js @@ -41,15 +41,24 @@ class Analytics { /** * Track event if enabled and not DEV mode */ - _trackEvent(name, { event, params = {}, value, info }) { + _trackEvent(name, { event, params = {}, value, info, anonymously = false }) { if (!this.enabled) return; if (!__DEV__) { - RCTAnalytics.trackEvent({ - ...event, - ...params, - value, - info - }); + if (!anonymously) { + RCTAnalytics.trackEvent({ + ...event, + ...params, + value, + info + }); + } else { + RCTAnalytics.trackEventAnonymously({ + ...event, + ...params, + value, + info + }); + } } else { Logger.log(`Analytics '${name}' -`, event, params, value, info); } @@ -119,8 +128,9 @@ class Analytics { * Track event * * @param {object} event - Object containing event category, action and name + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEvent = event => { + trackEvent = (event, anonymously = false) => { this._trackEvent('trackEvent', { event }); }; @@ -129,9 +139,10 @@ class Analytics { * * @param {object} event - Object containing event category, action and name * @param {number} value - Value number to send with event + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEventWithValue = (event, value) => { - this._trackEvent('trackEventWithValue', { event, value }); + trackEventWithValue = (event, value, anonymously = false) => { + this._trackEvent('trackEventWithValue', { event, value, anonymously }); }; /** @@ -139,9 +150,10 @@ class Analytics { * * @param {object} event - Object containing event category, action and name * @param {string} info - Information string to send with event + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEventWithInfo = (event, info) => { - this._trackEvent('trackEventWithInfo', { event, info }); + trackEventWithInfo = (event, info, anonymously = false) => { + this._trackEvent('trackEventWithInfo', { event, info, anonymously }); }; /** @@ -150,9 +162,10 @@ class Analytics { * @param {object} event - Object containing event category, action and name * @param {number} value - Value number to send with event * @param {string} info - Information string to send with event + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEventWithValueAndInfo = (event, value, info) => { - this._trackEvent('trackEventWithValueAndInfo', { event, value, info }); + trackEventWithValueAndInfo = (event, value, info, anonymously = false) => { + this._trackEvent('trackEventWithValueAndInfo', { event, value, info, anonymously }); }; /** @@ -160,9 +173,10 @@ class Analytics { * * @param {object} event - Object containing event category, action and name * @param {object} params - Object containing other params to send with event + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEventWithParameters = (event, params) => { - this._trackEvent('trackEventWithParameters', { event, params }); + trackEventWithParameters = (event, params, anonymously = false) => { + this._trackEvent('trackEventWithParameters', { event, params, anonymously }); }; /** @@ -171,9 +185,10 @@ class Analytics { * @param {object} event - Object containing event category, action and name * @param {number} value - Value number to send with event * @param {object} params - Object containing other params to send with event + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEventWithValueAndParameters = (event, value, params) => { - this._trackEvent('trackEventWithValueAndParameters', { event, value, params }); + trackEventWithValueAndParameters = (event, value, params, anonymously = false) => { + this._trackEvent('trackEventWithValueAndParameters', { event, value, params, anonymously }); }; /** @@ -183,9 +198,10 @@ class Analytics { * @param {number} value - Value number to send with event * @param {string} info - Information string to send with event * @param {object} params - Object containing other params to send with event + * @param {boolean} anonymously - Whether the tracking should be without the right distinctId */ - trackEventWithValueAndInfoAndParameters = (event, value, info, params) => { - this._trackEvent('trackEventWithValueAndInfoAndParameters', { event, value, info, params }); + trackEventWithValueAndInfoAndParameters = (event, value, info, params, anonymously = false) => { + this._trackEvent('trackEventWithValueAndInfoAndParameters', { event, value, info, params, anonymously }); }; } @@ -220,11 +236,11 @@ export default { getDistinctId() { return instance && instance.getDistinctId(); }, - trackEvent(event) { - return instance && instance.trackEvent(event); + trackEvent(event, anonymously) { + return instance && instance.trackEvent(event, anonymously); }, - trackEventWithParameters(event, parameters) { - return instance && instance.trackEventWithParameters(event, parameters); + trackEventWithParameters(event, parameters, anonymously) { + return instance && instance.trackEventWithParameters(event, parameters, anonymously); }, getRemoteVariables() { return instance.remoteVariables; diff --git a/app/core/AppConstants.js b/app/core/AppConstants.js index aa12dbb6968..3d686cb5164 100644 --- a/app/core/AppConstants.js +++ b/app/core/AppConstants.js @@ -57,8 +57,9 @@ export default { ORIGIN_QR_CODE: 'qr-code' }, SWAPS: { - ACTIVE: false, - ONLY_MAINNET: false, + ACTIVE: true, + ONLY_MAINNET: true, + CLIENT_ID: 'mobile', LIVENESS_POLLING_FREQUENCY: 5 * 60 * 1000, POLL_COUNT_LIMIT: 3, DEFAULT_SLIPPAGE: 3 diff --git a/app/core/Engine.js b/app/core/Engine.js index 3c9d41406c9..c0f46a229ab 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -14,11 +14,11 @@ import { PreferencesController, TokenBalancesController, TokenRatesController, - TypedMessageManager, - TransactionController + TransactionController, + TypedMessageManager } from '@metamask/controllers'; -import { /*TransactionController,*/ SwapsController } from '@estebanmino/controllers'; +import { SwapsController } from '@estebanmino/controllers'; import AsyncStorage from '@react-native-community/async-storage'; @@ -117,7 +117,7 @@ class Engine { new TokenRatesController(), new TransactionController(), new TypedMessageManager(), - new SwapsController() + new SwapsController({ clientId: AppConstants.SWAPS.CLIENT_ID }) ], initialState ); @@ -126,8 +126,7 @@ class Engine { AssetsController: assets, KeyringController: keyring, NetworkController: network, - TransactionController: transaction, - PreferencesController: preferences + TransactionController: transaction } = this.datamodel.context; assets.setApiKey(process.env.MM_OPENSEA_KEY); @@ -144,24 +143,6 @@ class Engine { }); this.configureControllersOnNetworkChange(); Engine.instance = this; - - if (AppConstants.SWAPS.ACTIVE) { - const swapsTestInState = preferences.state.frequentRpcList.find(({ chainId }) => chainId === 1337); - if (!swapsTestInState) { - preferences.addToFrequentRpcList( - 'https://ganache-testnet.airswap-dev.codefi.network/', - '1337', - 'ETH', - 'Swaps Test Network' - ); - network.setRpcTarget( - 'https://ganache-testnet.airswap-dev.codefi.network/', - '1337', - 'ETH', - 'Swaps Test Network' - ); - } - } } return Engine.instance; } diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index b9bb9e91c6b..b30638432d4 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 = 587; + CURRENT_PROJECT_VERSION = 590; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -913,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 587; + CURRENT_PROJECT_VERSION = 590; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/MetaMask/NativeModules/RCTAnalytics/RCTAnalytics.m b/ios/MetaMask/NativeModules/RCTAnalytics/RCTAnalytics.m index 26d9f200ea3..4a30ca11653 100644 --- a/ios/MetaMask/NativeModules/RCTAnalytics/RCTAnalytics.m +++ b/ios/MetaMask/NativeModules/RCTAnalytics/RCTAnalytics.m @@ -29,6 +29,14 @@ @implementation RCTAnalytics [[Mixpanel sharedInstance] track: [self getCategory:event] properties:[self getInfo:event]]; } +RCT_EXPORT_METHOD(trackEventAnonymously:(NSDictionary *)event) +{ + NSString *const distinctId = [[Mixpanel sharedInstance] distinctId]; + [[Mixpanel sharedInstance] identify:@"0x0000000000000000"]; + [[Mixpanel sharedInstance] track: [self getCategory:event] properties:[self getInfo:event]]; + [[Mixpanel sharedInstance] identify:distinctId]; +} + RCT_EXPORT_METHOD(peopleIdentify) { diff --git a/locales/en.json b/locales/en.json index 5b3c1e48638..9e1c8a2c5b2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -674,6 +674,7 @@ "gas_price": "Gas Price: (GWEI)", "save": "Save", "warning_gas_limit": "Gas limit must be greater than 20999 and less than 7920027", + "warning_gas_limit_estimated": "Estimated gas limit is {{gas}}, use it as minimum value", "cost_explanation": "The network fee covers the cost of processing your transaction on the Ethereum network. MetaMask does not profit from this fee. The higher the fee the better chances of your transaction getting processed." }, "spend_limit_edition": { @@ -1425,6 +1426,12 @@ "review_audits": "Review our official contracts audit", "start_swapping": "Start swapping" }, + "feature_off_title": "Temporarily unavailable", + "feature_off_body": "MetaMask Swaps is undergoing maintenance. Please check back later.", + "wrong_network_title": "Swaps not available", + "wrong_network_body": "You’re only able to swap tokens on the Ethereum Main Network.", + "unallowed_asset_title": "Can’t swap this token", + "unallowed_asset_body": "Some tokens with unique mechanics are currently not supported for swapping.", "convert_from": "Convert from", "convert_to": "Convert to", "verify": "Verify", diff --git a/package.json b/package.json index cad1f86de9f..2945908b8e5 100644 --- a/package.json +++ b/package.json @@ -71,10 +71,10 @@ "react-native-level-fs/**/semver": "^4.3.2" }, "dependencies": { - "@estebanmino/controllers": "^3.3.14", + "@estebanmino/controllers": "^3.3.17", "@exodus/react-native-payments": "https://github.com/wachunei/react-native-payments.git#package-json-hack", - "@metamask/controllers": "^6.1.0", "@metamask/contract-metadata": "^1.23.0", + "@metamask/controllers": "^6.1.0", "@react-native-community/async-storage": "1.12.1", "@react-native-community/blur": "^3.6.0", "@react-native-community/checkbox": "^0.4.2", diff --git a/yarn.lock b/yarn.lock index 4de1addb26d..c09371c1457 100644 --- a/yarn.lock +++ b/yarn.lock @@ -909,10 +909,10 @@ dependencies: "@types/hammerjs" "^2.0.36" -"@estebanmino/controllers@^3.3.14": - version "3.3.14" - resolved "https://registry.yarnpkg.com/@estebanmino/controllers/-/controllers-3.3.14.tgz#67d2f1e1e1176bb270df05c8dc00e92f488934ed" - integrity sha512-NxUrN5kLOKN3aBRu+aPTUvmDGKJQiwjLAMavBDDbCjlckmgh3yVyDwVOHcKSpOpDKjpoa8hEuLBbzR+0R+Sqyg== +"@estebanmino/controllers@^3.3.17": + version "3.3.17" + resolved "https://registry.yarnpkg.com/@estebanmino/controllers/-/controllers-3.3.17.tgz#22f06daf2b5a004bcf40a12f905699263276159f" + integrity sha512-tmONppQxqLOW7uZSSk9gUAiNokU1tW2LezPggjZjzOp9CTBIc3cgr28o07hss7DF4+8IX6XOEtAjfeVgUUCQ2Q== dependencies: "@metamask/contract-metadata" "^1.22.0" abort-controller "^3.0.0"