1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +01:00

Swaps support for local testnet (#10658)

* Swaps support for local testnet

* Create util method for comparison of token addresses/symbols to default swaps token

* Get chainId from txMeta in _trackSwapsMetrics of transaction controller

* Add comment to document purpose of getTransactionGroupRecipientAddressFilter

* Use isSwapsDefaultTokenSymbol in place of repeated defaultTokenSymbol comparisons in build-quote.js
This commit is contained in:
Dan J Miller 2021-03-18 07:50:06 -02:30 committed by GitHub
parent 1c573ef852
commit 480512d14f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 466 additions and 227 deletions

View File

@ -8,12 +8,13 @@ import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util';
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils';
import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util';
import {
ETH_SWAPS_TOKEN_OBJECT,
DEFAULT_ERC20_APPROVE_GAS,
QUOTES_EXPIRED_ERROR,
QUOTES_NOT_AVAILABLE_ERROR,
SWAPS_FETCH_ORDER_CONFLICT,
} from '../../../shared/constants/swaps';
import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils';
import {
fetchTradesInfo as defaultFetchTradesInfo,
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
@ -85,6 +86,7 @@ export default class SwapsController {
fetchTradesInfo = defaultFetchTradesInfo,
fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime,
getCurrentChainId,
}) {
this.store = new ObservableStore({
swapsState: { ...initialState.swapsState },
@ -93,6 +95,7 @@ export default class SwapsController {
this._fetchTradesInfo = fetchTradesInfo;
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness;
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime;
this._getCurrentChainId = getCurrentChainId;
this.getBufferedGasLimit = getBufferedGasLimit;
this.tokenRatesStore = tokenRatesStore;
@ -116,10 +119,11 @@ export default class SwapsController {
// Sets the refresh rate for quote updates from the MetaSwap API
async _setSwapsQuoteRefreshTime() {
const chainId = this._getCurrentChainId();
// Default to fallback time unless API returns valid response
let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME;
try {
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime();
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(chainId);
} catch (e) {
console.error('Request for swaps quote refresh time failed: ', e);
}
@ -158,6 +162,8 @@ export default class SwapsController {
fetchParamsMetaData = {},
isPolledRequest,
) {
const { chainId } = fetchParamsMetaData;
if (!fetchParams) {
return null;
}
@ -177,7 +183,7 @@ export default class SwapsController {
this.indexOfNewestCallInFlight = indexOfCurrentCall;
let [newQuotes] = await Promise.all([
this._fetchTradesInfo(fetchParams),
this._fetchTradesInfo(fetchParams, fetchParamsMetaData),
this._setSwapsQuoteRefreshTime(),
]);
@ -191,7 +197,7 @@ export default class SwapsController {
let approvalRequired = false;
if (
fetchParams.sourceToken !== ETH_SWAPS_TOKEN_OBJECT.address &&
!isSwapsDefaultTokenAddress(fetchParams.sourceToken, chainId) &&
Object.values(newQuotes).length
) {
const allowance = await this._getERC20Allowance(
@ -490,6 +496,7 @@ export default class SwapsController {
const {
swapsState: { customGasPrice },
} = this.store.getState();
const chainId = this._getCurrentChainId();
const numQuotes = Object.keys(quotes).length;
if (!numQuotes) {
@ -533,8 +540,8 @@ export default class SwapsController {
// trade.value is a sum of different values depending on the transaction.
// It always includes any external fees charged by the quote source. In
// addition, if the source asset is ETH, trade.value includes the amount
// of swapped ETH.
// addition, if the source asset is the selected chain's default token, trade.value
// includes the amount of that token.
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16).plus(
trade.value,
16,
@ -549,21 +556,21 @@ export default class SwapsController {
});
// The total fee is aggregator/exchange fees plus gas fees.
// If the swap is from ETH, subtract the sourceAmount from the total cost.
// Otherwise, the total fee is simply trade.value plus gas fees.
const ethFee =
sourceToken === ETH_SWAPS_TOKEN_OBJECT.address
? conversionUtil(
totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei
{
fromCurrency: 'ETH',
fromDenomination: 'WEI',
toDenomination: 'ETH',
fromNumericBase: 'BN',
numberOfDecimals: 6,
},
)
: totalEthCost;
// If the swap is from the selected chain's default token, subtract
// the sourceAmount from the total cost. Otherwise, the total fee
// is simply trade.value plus gas fees.
const ethFee = isSwapsDefaultTokenAddress(sourceToken, chainId)
? conversionUtil(
totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei
{
fromCurrency: 'ETH',
fromDenomination: 'WEI',
toDenomination: 'ETH',
fromNumericBase: 'BN',
numberOfDecimals: 6,
},
)
: totalEthCost;
const decimalAdjustedDestinationAmount = calcTokenAmount(
destinationAmount,
@ -588,10 +595,12 @@ export default class SwapsController {
10,
);
const conversionRateForCalculations =
destinationToken === ETH_SWAPS_TOKEN_OBJECT.address
? 1
: tokenConversionRate;
const conversionRateForCalculations = isSwapsDefaultTokenAddress(
destinationToken,
chainId,
)
? 1
: tokenConversionRate;
const overallValueOfQuoteForSorting =
conversionRateForCalculations === undefined
@ -618,8 +627,10 @@ export default class SwapsController {
});
const isBest =
newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_OBJECT.address ||
Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]);
isSwapsDefaultTokenAddress(
newQuotes[topAggId].destinationToken,
chainId,
) || Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]);
let savings = null;
@ -726,13 +737,17 @@ export default class SwapsController {
async _fetchAndSetSwapsLiveness() {
const { swapsState } = this.store.getState();
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState;
const chainId = this._getCurrentChainId();
let swapsFeatureIsLive = false;
let successfullyFetched = false;
let numAttempts = 0;
const fetchAndIncrementNumAttempts = async () => {
try {
swapsFeatureIsLive = Boolean(await this._fetchSwapsFeatureLiveness());
swapsFeatureIsLive = Boolean(
await this._fetchSwapsFeatureLiveness(chainId),
);
successfullyFetched = true;
} catch (err) {
log.error(err);

View File

@ -8,6 +8,7 @@ import { ObservableStore } from '@metamask/obs-store';
import {
ROPSTEN_NETWORK_ID,
MAINNET_NETWORK_ID,
MAINNET_CHAIN_ID,
} from '../../../shared/constants/network';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../test/stub/provider';
@ -75,6 +76,7 @@ const MOCK_FETCH_METADATA = {
symbol: 'FOO',
decimals: 18,
},
chainId: MAINNET_CHAIN_ID,
};
const MOCK_TOKEN_RATES_STORE = new ObservableStore({
@ -131,6 +133,8 @@ const sandbox = sinon.createSandbox();
const fetchTradesInfoStub = sandbox.stub();
const fetchSwapsFeatureLivenessStub = sandbox.stub();
const fetchSwapsQuoteRefreshTimeStub = sandbox.stub();
const getCurrentChainIdStub = sandbox.stub();
getCurrentChainIdStub.returns(MAINNET_CHAIN_ID);
describe('SwapsController', function () {
let provider;
@ -145,6 +149,7 @@ describe('SwapsController', function () {
fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub,
getCurrentChainId: getCurrentChainIdStub,
});
};
@ -194,6 +199,7 @@ describe('SwapsController', function () {
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub,
});
const currentEthersInstance = swapsController.ethersProvider;
const onNetworkDidChange = networkController.on.getCall(0).args[1];
@ -218,6 +224,7 @@ describe('SwapsController', function () {
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub,
});
const currentEthersInstance = swapsController.ethersProvider;
const onNetworkDidChange = networkController.on.getCall(0).args[1];
@ -242,6 +249,7 @@ describe('SwapsController', function () {
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub,
});
const currentEthersInstance = swapsController.ethersProvider;
const onNetworkDidChange = networkController.on.getCall(0).args[1];
@ -686,7 +694,10 @@ describe('SwapsController', function () {
});
assert.strictEqual(
fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS),
fetchTradesInfoStub.calledOnceWithExactly(
MOCK_FETCH_PARAMS,
MOCK_FETCH_METADATA,
),
true,
);
});

View File

@ -958,6 +958,7 @@ export default class TransactionController extends EventEmitter {
txMeta.txParams.from,
txMeta.destinationTokenDecimals,
approvalTxMeta,
txMeta.chainId,
);
const quoteVsExecutionRatio = `${new BigNumber(tokensReceived, 10)

View File

@ -383,6 +383,9 @@ export default class MetamaskController extends EventEmitter {
this.networkController,
),
tokenRatesStore: this.tokenRatesController.store,
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
});
// ensure accountTracker updates balances after network change

View File

@ -1,3 +1,12 @@
import { MAINNET_CHAIN_ID } from './network';
export const QUOTES_EXPIRED_ERROR = 'quotes-expired';
export const SWAP_FAILED_ERROR = 'swap-failed-error';
export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes';
export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable';
export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance';
export const SWAPS_FETCH_ORDER_CONFLICT = 'swaps-fetch-order-conflict';
// An address that the metaswap-api recognizes as ETH, in place of the token address that ERC-20 tokens have
const ETH_SWAPS_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000';
@ -9,17 +18,42 @@ export const ETH_SWAPS_TOKEN_OBJECT = {
iconUrl: 'images/black-eth-logo.svg',
};
export const QUOTES_EXPIRED_ERROR = 'quotes-expired';
export const SWAP_FAILED_ERROR = 'swap-failed-error';
export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes';
export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable';
export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance';
export const SWAPS_FETCH_ORDER_CONFLICT = 'swaps-fetch-order-conflict';
const TEST_ETH_SWAPS_TOKEN_OBJECT = {
symbol: 'TESTETH',
name: 'Test Ether',
address: ETH_SWAPS_TOKEN_ADDRESS,
decimals: 18,
iconUrl: 'images/black-eth-logo.svg',
};
// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations
export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0';
export const SWAPS_CONTRACT_ADDRESS =
'0x881d40237659c251811cec9c364ef91dc08d300c';
const MAINNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c';
export const METASWAP_API_HOST = 'https://api.metaswap.codefi.network';
const TESTNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c';
const METASWAP_ETH_API_HOST = 'https://api.metaswap.codefi.network';
const SWAPS_TESTNET_CHAIN_ID = '0x539';
const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network';
export const ALLOWED_SWAPS_CHAIN_IDS = {
[MAINNET_CHAIN_ID]: true,
[SWAPS_TESTNET_CHAIN_ID]: true,
};
export const METASWAP_CHAINID_API_HOST_MAP = {
[MAINNET_CHAIN_ID]: METASWAP_ETH_API_HOST,
[SWAPS_TESTNET_CHAIN_ID]: SWAPS_TESTNET_HOST,
};
export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = {
[MAINNET_CHAIN_ID]: MAINNET_CONTRACT_ADDRESS,
[SWAPS_TESTNET_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS,
};
export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
[MAINNET_CHAIN_ID]: ETH_SWAPS_TOKEN_OBJECT,
[SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT,
};

View File

@ -0,0 +1,33 @@
import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/swaps';
/**
* Checks whether the provided address is strictly equal to the address for
* the default swaps token of the provided chain.
*
* @param {string} address - The string to compare to the default token address
* @param {string} chainId - The hex encoded chain ID of the default swaps token to check
* @returns {boolean} Whether the address is the provided chain's default token address
*/
export function isSwapsDefaultTokenAddress(address, chainId) {
if (!address || !chainId) {
return false;
}
return address === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.address;
}
/**
* Checks whether the provided symbol is strictly equal to the symbol for
* the default swaps token of the provided chain.
*
* @param {string} symbol - The string to compare to the default token symbol
* @param {string} chainId - The hex encoded chain ID of the default swaps token to check
* @returns {boolean} Whether the symbl is the provided chain's default token symbol
*/
export function isSwapsDefaultTokenSymbol(symbol, chainId) {
if (!symbol || !chainId) {
return false;
}
return symbol === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol;
}

View File

@ -5,20 +5,31 @@ import {
nonceSortedCompletedTransactionsSelector,
nonceSortedPendingTransactionsSelector,
} from '../../../selectors/transactions';
import { getCurrentChainId } from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import TransactionListItem from '../transaction-list-item';
import Button from '../../ui/button';
import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions';
import { SWAPS_CONTRACT_ADDRESS } from '../../../../../shared/constants/swaps';
import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../../shared/constants/swaps';
import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction';
const PAGE_INCREMENT = 10;
const getTransactionGroupRecipientAddressFilter = (recipientAddress) => {
// When we are on a token page, we only want to show transactions that involve that token.
// In the case of token transfers or approvals, these will be transactions sent to the
// token contract. In the case of swaps, these will be transactions sent to the swaps contract
// and which have the token address in the transaction data.
//
// getTransactionGroupRecipientAddressFilter is used to determine whether a transaction matches
// either of those criteria
const getTransactionGroupRecipientAddressFilter = (
recipientAddress,
chainId,
) => {
return ({ initialTransaction: { txParams } }) => {
return (
txParams?.to === recipientAddress ||
(txParams?.to === SWAPS_CONTRACT_ADDRESS &&
(txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] &&
txParams.data.match(recipientAddress.slice(2)))
);
};
@ -39,12 +50,13 @@ const getFilteredTransactionGroups = (
transactionGroups,
hideTokenTransactions,
tokenAddress,
chainId,
) => {
if (hideTokenTransactions) {
return transactionGroups.filter(tokenTransactionFilter);
} else if (tokenAddress) {
return transactionGroups.filter(
getTransactionGroupRecipientAddressFilter(tokenAddress),
getTransactionGroupRecipientAddressFilter(tokenAddress, chainId),
);
}
return transactionGroups;
@ -63,6 +75,7 @@ export default function TransactionList({
const unfilteredCompletedTransactions = useSelector(
nonceSortedCompletedTransactionsSelector,
);
const chainId = useSelector(getCurrentChainId);
const pendingTransactions = useMemo(
() =>
@ -70,8 +83,14 @@ export default function TransactionList({
unfilteredPendingTransactions,
hideTokenTransactions,
tokenAddress,
chainId,
),
[hideTokenTransactions, tokenAddress, unfilteredPendingTransactions],
[
hideTokenTransactions,
tokenAddress,
unfilteredPendingTransactions,
chainId,
],
);
const completedTransactions = useMemo(
() =>
@ -79,8 +98,14 @@ export default function TransactionList({
unfilteredCompletedTransactions,
hideTokenTransactions,
tokenAddress,
chainId,
),
[hideTokenTransactions, tokenAddress, unfilteredCompletedTransactions],
[
hideTokenTransactions,
tokenAddress,
unfilteredCompletedTransactions,
chainId,
],
);
const viewMore = useCallback(

View File

@ -25,7 +25,8 @@ import {
getIsMainnet,
getIsTestnet,
getCurrentKeyring,
getSwapsEthToken,
getSwapsDefaultToken,
getIsSwapsChain,
} from '../../../selectors/selectors';
import SwapIcon from '../../ui/icon/swap-icon.component';
import BuyIcon from '../../ui/icon/overview-buy-icon.component';
@ -63,13 +64,14 @@ const EthOverview = ({ className }) => {
const { balance } = selectedAccount;
const isMainnetChain = useSelector(getIsMainnet);
const isTestnetChain = useSelector(getIsTestnet);
const isSwapsChain = useSelector(getIsSwapsChain);
const enteredSwapsEvent = useNewMetricEvent({
event: 'Swaps Opened',
properties: { source: 'Main View', active_currency: 'ETH' },
category: 'swaps',
});
const swapsEnabled = useSelector(getSwapsFeatureLiveness);
const swapsEthToken = useSelector(getSwapsEthToken);
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
return (
<WalletOverview
@ -136,12 +138,12 @@ const EthOverview = ({ className }) => {
{swapsEnabled ? (
<IconButton
className="eth-overview__button"
disabled={!isMainnetChain}
disabled={!isSwapsChain}
Icon={SwapIcon}
onClick={() => {
if (isMainnetChain) {
if (isSwapsChain) {
enteredSwapsEvent();
dispatch(setSwapsFromToken(swapsEthToken));
dispatch(setSwapsFromToken(defaultSwapsToken));
if (usingHardwareWallet) {
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE);
} else {
@ -154,7 +156,7 @@ const EthOverview = ({ className }) => {
<Tooltip
title={t('onlyAvailableOnMainnet')}
position="bottom"
disabled={isMainnetChain}
disabled={isSwapsChain}
>
{contents}
</Tooltip>

View File

@ -25,9 +25,8 @@ import {
import {
getAssetImages,
getCurrentKeyring,
getCurrentChainId,
getIsSwapsChain,
} from '../../../selectors/selectors';
import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network';
import SwapIcon from '../../ui/icon/swap-icon.component';
import SendIcon from '../../ui/icon/overview-send-icon.component';
@ -58,7 +57,7 @@ const TokenOverview = ({ className, token }) => {
balanceToRender,
token.symbol,
);
const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain);
const enteredSwapsEvent = useNewMetricEvent({
event: 'Swaps Opened',
properties: { source: 'Token View', active_currency: token.symbol },
@ -100,10 +99,10 @@ const TokenOverview = ({ className, token }) => {
{swapsEnabled ? (
<IconButton
className="token-overview__button"
disabled={chainId !== MAINNET_CHAIN_ID}
disabled={!isSwapsChain}
Icon={SwapIcon}
onClick={() => {
if (chainId === MAINNET_CHAIN_ID) {
if (isSwapsChain) {
enteredSwapsEvent();
dispatch(
setSwapsFromToken({
@ -125,7 +124,7 @@ const TokenOverview = ({ className, token }) => {
<Tooltip
title={t('onlyAvailableOnMainnet')}
position="bottom"
disabled={chainId === MAINNET_CHAIN_ID}
disabled={isSwapsChain}
>
{contents}
</Tooltip>

View File

@ -49,7 +49,8 @@ import {
getSelectedAccount,
getTokenExchangeRates,
getUSDConversionRate,
getSwapsEthToken,
getSwapsDefaultToken,
getCurrentChainId,
} from '../../selectors';
import {
ERROR_FETCHING_QUOTES,
@ -376,9 +377,11 @@ export const fetchQuotesAndSetQuoteState = (
metaMetricsEvent,
) => {
return async (dispatch, getState) => {
const state = getState();
const chainId = getCurrentChainId(state);
let swapsFeatureIsLive = false;
try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness();
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId);
} catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
}
@ -389,13 +392,14 @@ export const fetchQuotesAndSetQuoteState = (
return;
}
const state = getState();
const fetchParams = getFetchParams(state);
const selectedAccount = getSelectedAccount(state);
const balanceError = getBalanceError(state);
const swapsDefaultToken = getSwapsDefaultToken(state);
const fetchParamsFromToken =
fetchParams?.metaData?.sourceTokenInfo?.symbol === 'ETH'
? getSwapsEthToken(state)
fetchParams?.metaData?.sourceTokenInfo?.symbol ===
swapsDefaultToken.symbol
? swapsDefaultToken
: fetchParams?.metaData?.sourceTokenInfo;
const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {};
const selectedToToken =
@ -420,7 +424,10 @@ export const fetchQuotesAndSetQuoteState = (
const contractExchangeRates = getTokenExchangeRates(state);
let destinationTokenAddedForSwap = false;
if (toTokenSymbol !== 'ETH' && !contractExchangeRates[toTokenAddress]) {
if (
toTokenSymbol !== swapsDefaultToken.symbol &&
!contractExchangeRates[toTokenAddress]
) {
destinationTokenAddedForSwap = true;
await dispatch(
addToken(
@ -433,7 +440,7 @@ export const fetchQuotesAndSetQuoteState = (
);
}
if (
fromTokenSymbol !== 'ETH' &&
fromTokenSymbol !== swapsDefaultToken.symbol &&
!contractExchangeRates[fromTokenAddress] &&
fromTokenBalance &&
new BigNumber(fromTokenBalance, 16).gt(0)
@ -494,6 +501,7 @@ export const fetchQuotesAndSetQuoteState = (
sourceTokenInfo,
destinationTokenInfo,
accountBalance: selectedAccount.balance,
chainId,
},
),
);
@ -563,9 +571,12 @@ export const fetchQuotesAndSetQuoteState = (
export const signAndSendTransactions = (history, metaMetricsEvent) => {
return async (dispatch, getState) => {
const state = getState();
const chainId = getCurrentChainId(state);
let swapsFeatureIsLive = false;
try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness();
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId);
} catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
}
@ -576,7 +587,6 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
return;
}
const state = getState();
const customSwapsGas = getCustomSwapsGas(state);
const fetchParams = getFetchParams(state);
const { metaData, value: swapTokenValue, slippage } = fetchParams;

View File

@ -1,13 +1,18 @@
import { useSelector } from 'react-redux';
import { useRouteMatch } from 'react-router-dom';
import { getTokens } from '../ducks/metamask/metamask';
import { getCurrentChainId } from '../selectors';
import { ASSET_ROUTE } from '../helpers/constants/routes';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
ETH_SWAPS_TOKEN_OBJECT,
} from '../../../shared/constants/swaps';
/**
* Returns a token object for the asset that is currently being viewed.
* Will return the ETH_SWAPS_TOKEN_OBJECT when the user is viewing either
* the primary, unfiltered, activity list or the ETH asset page.
* Will return the default token object for the current chain when the
* user is viewing either the primary, unfiltered, activity list or the
* default token asset page.
* @returns {import('./useTokenDisplayValue').Token}
*/
export function useCurrentAsset() {
@ -22,6 +27,10 @@ export function useCurrentAsset() {
const knownTokens = useSelector(getTokens);
const token =
tokenAddress && knownTokens.find(({ address }) => address === tokenAddress);
const chainId = useSelector(getCurrentChainId);
return token ?? ETH_SWAPS_TOKEN_OBJECT;
return (
token ??
(SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId] || ETH_SWAPS_TOKEN_OBJECT)
);
}

View File

@ -1,6 +1,11 @@
import { useSelector } from 'react-redux';
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../shared/modules/swaps.utils';
import { getSwapsTokensReceivedFromTxMeta } from '../pages/swaps/swaps.util';
import { getCurrentChainId } from '../selectors';
import { useTokenFiatAmount } from './useTokenFiatAmount';
/**
@ -14,10 +19,11 @@ import { useTokenFiatAmount } from './useTokenFiatAmount';
/**
* A Swap transaction group's primaryTransaction contains details of the swap,
* including the source (from) and destination (to) token type (ETH, DAI, etc..)
* When viewing a non ETH asset page, we need to determine if that asset is the
* token that was received (destination) from the swap. In that circumstance we
* would want to show the primaryCurrency in the activity list that is most relevant
* for that token (- 1000 DAI, for example, when swapping DAI for ETH).
* When viewing an asset page that is not for the current chain's default token, we
* need to determine if that asset is the token that was received (destination) from
* the swap. In that circumstance we would want to show the primaryCurrency in the
* activity list that is most relevant for that token (- 1000 DAI, for example, when
* swapping DAI for ETH).
* @param {import('../selectors').transactionGroup} transactionGroup - Group of transactions by nonce
* @param {import('./useTokenDisplayValue').Token} currentAsset - The current asset the user is looking at
* @returns {SwappedTokenValue}
@ -27,11 +33,15 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) {
const { primaryTransaction, initialTransaction } = transactionGroup;
const { type } = initialTransaction;
const { from: senderAddress } = initialTransaction.txParams || {};
const chainId = useSelector(getCurrentChainId);
const isViewingReceivedTokenFromSwap =
currentAsset?.symbol === primaryTransaction.destinationTokenSymbol ||
(currentAsset.address === ETH_SWAPS_TOKEN_OBJECT.address &&
primaryTransaction.destinationTokenSymbol === 'ETH');
(isSwapsDefaultTokenAddress(currentAsset.address, chainId) &&
isSwapsDefaultTokenSymbol(
primaryTransaction.destinationTokenSymbol,
chainId,
));
const swapTokenValue =
type === TRANSACTION_TYPES.SWAP && isViewingReceivedTokenFromSwap
@ -41,6 +51,8 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) {
address,
senderAddress,
decimals,
null,
chainId,
)
: type === TRANSACTION_TYPES.SWAP && primaryTransaction.swapTokenValue;

View File

@ -9,9 +9,11 @@ import {
getTokenExchangeRates,
getConversionRate,
getCurrentCurrency,
getSwapsEthToken,
getSwapsDefaultToken,
getCurrentChainId,
} from '../selectors';
import { getSwapsTokens } from '../ducks/swaps/swaps';
import { isSwapsDefaultTokenSymbol } from '../../../shared/modules/swaps.utils';
import { useEqualityCheck } from './useEqualityCheck';
const tokenList = shuffle(
@ -28,12 +30,15 @@ export function getRenderableTokenData(
contractExchangeRates,
conversionRate,
currentCurrency,
chainId,
) {
const { symbol, name, address, iconUrl, string, balance, decimals } = token;
const formattedFiat =
getTokenFiatAmount(
symbol === 'ETH' ? 1 : contractExchangeRates[address],
isSwapsDefaultTokenSymbol(symbol, chainId)
? 1
: contractExchangeRates[address],
conversionRate,
currentCurrency,
string,
@ -42,7 +47,9 @@ export function getRenderableTokenData(
) || '';
const rawFiat =
getTokenFiatAmount(
symbol === 'ETH' ? 1 : contractExchangeRates[address],
isSwapsDefaultTokenSymbol(symbol, chainId)
? 1
: contractExchangeRates[address],
conversionRate,
currentCurrency,
string,
@ -70,30 +77,32 @@ export function getRenderableTokenData(
}
export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
const chainId = useSelector(getCurrentChainId);
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate);
const currentCurrency = useSelector(getCurrentCurrency);
const swapsEthToken = useSelector(getSwapsEthToken);
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
const memoizedTopTokens = useEqualityCheck(topTokens);
const memoizedUsersToken = useEqualityCheck(usersTokens);
const ethToken = getRenderableTokenData(
swapsEthToken,
const defaultToken = getRenderableTokenData(
defaultSwapsToken,
tokenConversionRates,
conversionRate,
currentCurrency,
chainId,
);
const memoizedEthToken = useEqualityCheck(ethToken);
const memoizedDefaultToken = useEqualityCheck(defaultToken);
const swapsTokens = useSelector(getSwapsTokens) || [];
const tokensToSearch = swapsTokens.length
? swapsTokens
: [
memoizedEthToken,
memoizedDefaultToken,
...tokenList.filter(
(token) => token.symbol !== memoizedEthToken.symbol,
(token) => token.symbol !== memoizedDefaultToken.symbol,
),
];
@ -116,9 +125,10 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
tokenConversionRates,
conversionRate,
currentCurrency,
chainId,
);
if (
renderableDataToken.symbol === 'ETH' ||
isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) ||
(usersTokensAddressMap[token.address] &&
Number(renderableDataToken.balance ?? 0) !== 0)
) {
@ -150,5 +160,6 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) {
conversionRate,
currentCurrency,
memoizedTopTokens,
chainId,
]);
}

View File

@ -10,11 +10,13 @@ import {
getShouldShowFiat,
getNativeCurrency,
getCurrentCurrency,
getCurrentChainId,
} from '../selectors';
import { getTokens } from '../ducks/metamask/metamask';
import { getMessage } from '../helpers/utils/i18n-helper';
import messages from '../../../app/_locales/en/messages.json';
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../helpers/constants/routes';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import {
TRANSACTION_TYPES,
TRANSACTION_GROUP_CATEGORIES,
@ -164,6 +166,8 @@ describe('useTransactionDisplayData', function () {
return 'ETH';
} else if (selector === getCurrentCurrency) {
return 'ETH';
} else if (selector === getCurrentChainId) {
return MAINNET_CHAIN_ID;
}
return null;
});

View File

@ -6,12 +6,14 @@ import { useHistory } from 'react-router-dom';
import { I18nContext } from '../../../contexts/i18n';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import {
getCurrentChainId,
getCurrentCurrency,
getRpcPrefsForCurrentProvider,
getUSDConversionRate,
} from '../../../selectors';
import {
getUsedQuote,
getFetchParams,
@ -23,7 +25,6 @@ import {
prepareToLeaveSwaps,
} from '../../../ducks/swaps/swaps';
import Mascot from '../../../components/ui/mascot';
import PulseLoader from '../../../components/ui/pulse-loader';
import {
QUOTES_EXPIRED_ERROR,
SWAP_FAILED_ERROR,
@ -31,6 +32,9 @@ import {
QUOTES_NOT_AVAILABLE_ERROR,
OFFLINE_FOR_MAINTENANCE,
} from '../../../../../shared/constants/swaps';
import { isSwapsDefaultTokenSymbol } from '../../../../../shared/modules/swaps.utils';
import PulseLoader from '../../../components/ui/pulse-loader';
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { getRenderableNetworkFeesForQuote } from '../swaps.util';
@ -73,16 +77,17 @@ export default function AwaitingSwap({
let feeinUnformattedFiat;
if (usedQuote && swapsGasPrice) {
const renderableNetworkFees = getRenderableNetworkFeesForQuote(
usedQuote.gasEstimateWithRefund || usedQuote.averageGas,
approveTxParams?.gas || '0x0',
swapsGasPrice,
const renderableNetworkFees = getRenderableNetworkFeesForQuote({
tradeGas: usedQuote.gasEstimateWithRefund || usedQuote.averageGas,
approveGas: approveTxParams?.gas || '0x0',
gasPrice: swapsGasPrice,
currentCurrency,
usdConversionRate,
usedQuote?.trade?.value,
sourceTokenInfo?.symbol,
usedQuote.sourceAmount,
);
conversionRate: usdConversionRate,
tradeValue: usedQuote?.trade?.value,
sourceSymbol: sourceTokenInfo?.symbol,
sourceAmount: usedQuote.sourceAmount,
chainId,
});
feeinUnformattedFiat = renderableNetworkFees.rawNetworkFees;
}
@ -228,7 +233,9 @@ export default function AwaitingSwap({
);
} else if (errorKey) {
await dispatch(navigateBackToBuildQuote(history));
} else if (destinationTokenInfo?.symbol === 'ETH') {
} else if (
isSwapsDefaultTokenSymbol(destinationTokenInfo?.symbol, chainId)
) {
history.push(DEFAULT_ROUTE);
} else {
history.push(`${ASSET_ROUTE}/${destinationTokenInfo?.address}`);

View File

@ -29,10 +29,11 @@ import {
getFetchParams,
} from '../../../ducks/swaps/swaps';
import {
getSwapsEthToken,
getSwapsDefaultToken,
getTokenExchangeRates,
getConversionRate,
getCurrentCurrency,
getCurrentChainId,
} from '../../../selectors';
import {
getValueFromWeiHex,
@ -44,7 +45,10 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../../shared/constants/swaps';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../../../shared/modules/swaps.utils';
import { resetSwapsPostFetchState, removeToken } from '../../../store/actions';
import { fetchTokenPrice, fetchTokenBalance } from '../swaps.util';
@ -84,21 +88,29 @@ export default function BuildQuote({
const topAssets = useSelector(getTopAssets);
const fromToken = useSelector(getFromToken);
const toToken = useSelector(getToToken) || destinationTokenInfo;
const swapsEthToken = useSelector(getSwapsEthToken);
const fetchParamsFromToken =
sourceTokenInfo?.symbol === 'ETH' ? swapsEthToken : sourceTokenInfo;
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
const chainId = useSelector(getCurrentChainId);
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate);
const currentCurrency = useSelector(getCurrentCurrency);
const fetchParamsFromToken = isSwapsDefaultTokenSymbol(
sourceTokenInfo?.symbol,
chainId,
)
? defaultSwapsToken
: sourceTokenInfo;
const { loading, tokensWithBalances } = useTokenTracker(tokens);
// If the fromToken was set in a call to `onFromSelect` (see below), and that from token has a balance
// but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that
// the balance of the token can appear in the from token selection dropdown
const fromTokenArray =
fromToken?.symbol !== 'ETH' && fromToken?.balance ? [fromToken] : [];
!isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance
? [fromToken]
: [];
const usersTokens = uniqBy(
[...tokensWithBalances, ...tokens, ...fromTokenArray],
'address',
@ -110,6 +122,7 @@ export default function BuildQuote({
tokenConversionRates,
conversionRate,
currentCurrency,
chainId,
);
const tokensToSearch = useTokensToSearch({
@ -119,9 +132,9 @@ export default function BuildQuote({
const selectedToToken =
tokensToSearch.find(({ address }) => address === toToken?.address) ||
toToken;
const toTokenIsNotEth =
const toTokenIsNotDefault =
selectedToToken?.address &&
selectedToToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address;
!isSwapsDefaultTokenAddress(selectedToToken?.address, chainId);
const occurances = Number(selectedToToken?.occurances || 0);
const {
address: fromTokenAddress,
@ -151,8 +164,9 @@ export default function BuildQuote({
{ showFiat: true },
true,
);
const swapFromFiatValue =
fromTokenSymbol === 'ETH' ? swapFromEthFiatValue : swapFromTokenFiatValue;
const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId)
? swapFromEthFiatValue
: swapFromTokenFiatValue;
const onFromSelect = (token) => {
if (
@ -227,15 +241,17 @@ export default function BuildQuote({
);
useEffect(() => {
const notEth =
tokensWithBalancesFromToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address;
const notDefault = !isSwapsDefaultTokenAddress(
tokensWithBalancesFromToken?.address,
chainId,
);
const addressesAreTheSame =
tokensWithBalancesFromToken?.address ===
previousTokensWithBalancesFromToken?.address;
const balanceHasChanged =
tokensWithBalancesFromToken?.balance !==
previousTokensWithBalancesFromToken?.balance;
if (notEth && addressesAreTheSame && balanceHasChanged) {
if (notDefault && addressesAreTheSame && balanceHasChanged) {
dispatch(
setSwapsFromToken({
...fromToken,
@ -249,12 +265,13 @@ export default function BuildQuote({
tokensWithBalancesFromToken,
previousTokensWithBalancesFromToken,
fromToken,
chainId,
]);
// If the eth balance changes while on build quote, we update the selected from token
useEffect(() => {
if (
fromToken?.address === ETH_SWAPS_TOKEN_OBJECT.address &&
isSwapsDefaultTokenAddress(fromToken?.address, chainId) &&
fromToken?.balance !== hexToDecimal(ethBalance)
) {
dispatch(
@ -269,7 +286,7 @@ export default function BuildQuote({
}),
);
}
}, [dispatch, fromToken, ethBalance]);
}, [dispatch, fromToken, ethBalance, chainId]);
useEffect(() => {
if (prevFromTokenBalance !== fromTokenBalance) {
@ -286,7 +303,7 @@ export default function BuildQuote({
<div className="build-quote__content">
<div className="build-quote__dropdown-input-pair-header">
<div className="build-quote__input-label">{t('swapSwapFrom')}</div>
{fromTokenSymbol !== 'ETH' && (
{!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && (
<div
className="build-quote__max-button"
onClick={() =>
@ -384,7 +401,7 @@ export default function BuildQuote({
defaultToAll
/>
</div>
{toTokenIsNotEth &&
{toTokenIsNotDefault &&
(occurances < 2 ? (
<ActionableMessage
message={
@ -474,7 +491,7 @@ export default function BuildQuote({
!selectedToToken?.address ||
Number(maxSlippage) === 0 ||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
(toTokenIsNotEth && occurances < 2 && !verificationClicked)
(toTokenIsNotDefault && occurances < 2 && !verificationClicked)
}
hideCancel
showTermsOfService

View File

@ -12,6 +12,7 @@ import { I18nContext } from '../../contexts/i18n';
import {
getSelectedAccount,
getCurrentChainId,
getIsSwapsChain,
} from '../../selectors/selectors';
import {
getQuotes,
@ -45,7 +46,6 @@ import {
SWAP_FAILED_ERROR,
OFFLINE_FOR_MAINTENANCE,
} from '../../../../shared/constants/swaps';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
import {
resetBackgroundSwapsState,
@ -96,6 +96,8 @@ export default function Swap() {
const fetchingQuotes = useSelector(getFetchingQuotes);
let swapsErrorKey = useSelector(getSwapsErrorKey);
const swapsEnabled = useSelector(getSwapsFeatureLiveness);
const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain);
const {
balance: ethBalance,
@ -116,6 +118,7 @@ export default function Swap() {
selectedAccountAddress,
destinationTokenInfo?.decimals,
approveTxData,
chainId,
);
const tradeConfirmed = tradeTxData?.status === TRANSACTION_STATUSES.CONFIRMED;
const approveError =
@ -155,26 +158,26 @@ export default function Swap() {
}, []);
useEffect(() => {
fetchTokens()
fetchTokens(chainId)
.then((tokens) => {
dispatch(setSwapsTokens(tokens));
})
.catch((error) => console.error(error));
fetchTopAssets().then((topAssets) => {
fetchTopAssets(chainId).then((topAssets) => {
dispatch(setTopAssets(topAssets));
});
fetchAggregatorMetadata().then((newAggregatorMetadata) => {
fetchAggregatorMetadata(chainId).then((newAggregatorMetadata) => {
dispatch(setAggregatorMetadata(newAggregatorMetadata));
});
dispatch(fetchAndSetSwapsGasPriceInfo());
dispatch(fetchAndSetSwapsGasPriceInfo(chainId));
return () => {
dispatch(prepareToLeaveSwaps());
};
}, [dispatch]);
}, [dispatch, chainId]);
const exitedSwapsEvent = useNewMetricEvent({
event: 'Exited Swaps',
@ -224,8 +227,7 @@ export default function Swap() {
return () => window.removeEventListener('beforeunload', fn);
}, [dispatch, isLoadingQuotesRoute]);
const chainId = useSelector(getCurrentChainId);
if (chainId !== MAINNET_CHAIN_ID) {
if (!isSwapsChain) {
return <Redirect to={{ pathname: DEFAULT_ROUTE }} />;
}

View File

@ -6,7 +6,7 @@ import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
import { I18nContext } from '../../../contexts/i18n';
import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import { getSwapsEthToken } from '../../../selectors';
import { getSwapsDefaultToken } from '../../../selectors';
import Button from '../../../components/ui/button';
import Popover from '../../../components/ui/popover';
@ -14,9 +14,14 @@ export default function IntroPopup({ onClose }) {
const dispatch = useDispatch(useDispatch);
const history = useHistory();
const t = useContext(I18nContext);
const swapsDefaultToken = useSelector(getSwapsDefaultToken);
const enteredSwapsEvent = useNewMetricEvent({
event: 'Swaps Opened',
properties: { source: 'Intro popup', active_currency: 'ETH' },
properties: {
source: 'Intro popup',
active_currency: swapsDefaultToken.symbol,
},
category: 'swaps',
});
const blogPostVisitedEvent = useNewMetricEvent({
@ -31,7 +36,6 @@ export default function IntroPopup({ onClose }) {
event: 'Product Overview Dismissed',
category: 'swaps',
});
const swapsEthToken = useSelector(getSwapsEthToken);
return (
<div className="intro-popup">
@ -51,7 +55,7 @@ export default function IntroPopup({ onClose }) {
onClick={() => {
onClose();
enteredSwapsEvent();
dispatch(setSwapsFromToken(swapsEthToken));
dispatch(setSwapsFromToken(swapsDefaultToken));
history.push(BUILD_QUOTE_ROUTE);
}}
>

View File

@ -3,9 +3,15 @@ import BigNumber from 'bignumber.js';
import abi from 'human-standard-token-abi';
import { isValidAddress } from 'ethereumjs-util';
import {
ETH_SWAPS_TOKEN_OBJECT,
METASWAP_API_HOST,
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
METASWAP_CHAINID_API_HOST_MAP,
} from '../../../../shared/constants/swaps';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../../shared/modules/swaps.utils';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
import {
calcTokenValue,
calcTokenAmount,
@ -30,22 +36,22 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH =
const CACHE_REFRESH_ONE_HOUR = 3600000;
const getBaseApi = function (type) {
const getBaseApi = function (type, chainId = MAINNET_CHAIN_ID) {
switch (type) {
case 'trade':
return `${METASWAP_API_HOST}/trades?`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/trades?`;
case 'tokens':
return `${METASWAP_API_HOST}/tokens`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/tokens`;
case 'topAssets':
return `${METASWAP_API_HOST}/topAssets`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/topAssets`;
case 'featureFlag':
return `${METASWAP_API_HOST}/featureFlag`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/featureFlag`;
case 'aggregatorMetadata':
return `${METASWAP_API_HOST}/aggregatorMetadata`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/aggregatorMetadata`;
case 'gasPrices':
return `${METASWAP_API_HOST}/gasPrices`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/gasPrices`;
case 'refreshTime':
return `${METASWAP_API_HOST}/quoteRefreshRate`;
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/quoteRefreshRate`;
default:
throw new Error('getBaseApi requires an api call type');
}
@ -205,15 +211,18 @@ function validateData(validators, object, urlUsed) {
});
}
export async function fetchTradesInfo({
slippage,
sourceToken,
sourceDecimals,
destinationToken,
value,
fromAddress,
exchangeList,
}) {
export async function fetchTradesInfo(
{
slippage,
sourceToken,
sourceDecimals,
destinationToken,
value,
fromAddress,
exchangeList,
},
{ chainId },
) {
const urlParams = {
destinationToken,
sourceToken,
@ -228,7 +237,7 @@ export async function fetchTradesInfo({
}
const queryString = new URLSearchParams(urlParams).toString();
const tradeURL = `${getBaseApi('trade')}${queryString}`;
const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`;
const tradesResponse = await fetchWithCache(
tradeURL,
{ method: 'GET' },
@ -272,21 +281,21 @@ export async function fetchTradesInfo({
return newQuotes;
}
export async function fetchTokens() {
const tokenUrl = getBaseApi('tokens');
export async function fetchTokens(chainId) {
const tokenUrl = getBaseApi('tokens', chainId);
const tokens = await fetchWithCache(
tokenUrl,
{ method: 'GET' },
{ cacheRefreshTime: CACHE_REFRESH_ONE_HOUR },
);
const filteredTokens = [
ETH_SWAPS_TOKEN_OBJECT,
SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId],
...tokens.filter((token) => {
return (
validateData(TOKEN_VALIDATORS, token, tokenUrl) &&
!(
token.symbol === ETH_SWAPS_TOKEN_OBJECT.symbol ||
token.address === ETH_SWAPS_TOKEN_OBJECT.address
isSwapsDefaultTokenSymbol(token.symbol, chainId) ||
isSwapsDefaultTokenAddress(token.address, chainId)
)
);
}),
@ -294,8 +303,8 @@ export async function fetchTokens() {
return filteredTokens;
}
export async function fetchAggregatorMetadata() {
const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata');
export async function fetchAggregatorMetadata(chainId) {
const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata', chainId);
const aggregators = await fetchWithCache(
aggregatorMetadataUrl,
{ method: 'GET' },
@ -316,8 +325,8 @@ export async function fetchAggregatorMetadata() {
return filteredAggregators;
}
export async function fetchTopAssets() {
const topAssetsUrl = getBaseApi('topAssets');
export async function fetchTopAssets(chainId) {
const topAssetsUrl = getBaseApi('topAssets', chainId);
const response = await fetchWithCache(
topAssetsUrl,
{ method: 'GET' },
@ -332,18 +341,18 @@ export async function fetchTopAssets() {
return topAssetsMap;
}
export async function fetchSwapsFeatureLiveness() {
export async function fetchSwapsFeatureLiveness(chainId) {
const status = await fetchWithCache(
getBaseApi('featureFlag'),
getBaseApi('featureFlag', chainId),
{ method: 'GET' },
{ cacheRefreshTime: 600000 },
);
return status?.active;
}
export async function fetchSwapsQuoteRefreshTime() {
export async function fetchSwapsQuoteRefreshTime(chainId) {
const response = await fetchWithCache(
getBaseApi('refreshTime'),
getBaseApi('refreshTime', chainId),
{ method: 'GET' },
{ cacheRefreshTime: 600000 },
);
@ -378,8 +387,8 @@ export async function fetchTokenBalance(address, userAddress) {
return usersToken;
}
export async function fetchSwapsGasPrices() {
const gasPricesUrl = getBaseApi('gasPrices');
export async function fetchSwapsGasPrices(chainId) {
const gasPricesUrl = getBaseApi('gasPrices', chainId);
const response = await fetchWithCache(
gasPricesUrl,
{ method: 'GET' },
@ -408,7 +417,7 @@ export async function fetchSwapsGasPrices() {
};
}
export function getRenderableNetworkFeesForQuote(
export function getRenderableNetworkFeesForQuote({
tradeGas,
approveGas,
gasPrice,
@ -417,14 +426,18 @@ export function getRenderableNetworkFeesForQuote(
tradeValue,
sourceSymbol,
sourceAmount,
) {
chainId,
}) {
const totalGasLimitForCalculation = new BigNumber(tradeGas || '0x0', 16)
.plus(approveGas || '0x0', 16)
.toString(16);
const gasTotalInWeiHex = calcGasTotal(totalGasLimitForCalculation, gasPrice);
const nonGasFee = new BigNumber(tradeValue, 16)
.minus(sourceSymbol === 'ETH' ? sourceAmount : 0, 10)
.minus(
isSwapsDefaultTokenSymbol(sourceSymbol, chainId) ? sourceAmount : 0,
10,
)
.toString(16);
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16)
@ -447,7 +460,7 @@ export function getRenderableNetworkFeesForQuote(
rawNetworkFees,
rawEthFee: ethFee,
feeInFiat: formattedNetworkFee,
feeInEth: `${ethFee} ETH`,
feeInEth: `${ethFee} ${SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol}`,
nonGasFee,
};
}
@ -459,6 +472,7 @@ export function quotesToRenderableData(
currentCurrency,
approveGas,
tokenConversionRates,
chainId,
) {
return Object.values(quotes).map((quote) => {
const {
@ -488,16 +502,17 @@ export function quotesToRenderableData(
rawNetworkFees,
rawEthFee,
feeInEth,
} = getRenderableNetworkFeesForQuote(
gasEstimateWithRefund || decimalToHex(averageGas || 800000),
} = getRenderableNetworkFeesForQuote({
tradeGas: gasEstimateWithRefund || decimalToHex(averageGas || 800000),
approveGas,
gasPrice,
currentCurrency,
conversionRate,
trade.value,
sourceTokenInfo.symbol,
tradeValue: trade.value,
sourceSymbol: sourceTokenInfo.symbol,
sourceAmount,
);
chainId,
});
const slippageMultiplier = new BigNumber(100 - slippage).div(100);
const minimumAmountReceived = new BigNumber(destinationValue)
@ -506,18 +521,20 @@ export function quotesToRenderableData(
const tokenConversionRate =
tokenConversionRates[destinationTokenInfo.address];
const ethValueOfTrade =
destinationTokenInfo.symbol === 'ETH'
? calcTokenAmount(
destinationAmount,
destinationTokenInfo.decimals,
).minus(rawEthFee, 10)
: new BigNumber(tokenConversionRate || 0, 10)
.times(
calcTokenAmount(destinationAmount, destinationTokenInfo.decimals),
10,
)
.minus(rawEthFee, 10);
const ethValueOfTrade = isSwapsDefaultTokenSymbol(
destinationTokenInfo.symbol,
chainId,
)
? calcTokenAmount(destinationAmount, destinationTokenInfo.decimals).minus(
rawEthFee,
10,
)
: new BigNumber(tokenConversionRate || 0, 10)
.times(
calcTokenAmount(destinationAmount, destinationTokenInfo.decimals),
10,
)
.minus(rawEthFee, 10);
let liquiditySourceKey;
let renderedSlippage = slippage;
@ -566,9 +583,10 @@ export function getSwapsTokensReceivedFromTxMeta(
accountAddress,
tokenDecimals,
approvalTxMeta,
chainId,
) {
const txReceipt = txMeta?.txReceipt;
if (tokenSymbol === 'ETH') {
if (isSwapsDefaultTokenSymbol(tokenSymbol, chainId)) {
if (
!txReceipt ||
!txMeta ||

View File

@ -1,5 +1,6 @@
import { strict as assert } from 'assert';
import proxyquire from 'proxyquire';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
import {
TRADES_BASE_PROD_URL,
TOKENS_BASE_PROD_URL,
@ -89,42 +90,45 @@ describe('Swaps Util', function () {
},
};
it('should fetch trade info on prod', async function () {
const result = await fetchTradesInfo({
TOKENS,
slippage: '3',
sourceToken: TOKENS[0].address,
destinationToken: TOKENS[1].address,
value: '2000000000000000000',
fromAddress: '0xmockAddress',
sourceSymbol: TOKENS[0].symbol,
sourceDecimals: TOKENS[0].decimals,
sourceTokenInfo: { ...TOKENS[0] },
destinationTokenInfo: { ...TOKENS[1] },
});
const result = await fetchTradesInfo(
{
TOKENS,
slippage: '3',
sourceToken: TOKENS[0].address,
destinationToken: TOKENS[1].address,
value: '2000000000000000000',
fromAddress: '0xmockAddress',
sourceSymbol: TOKENS[0].symbol,
sourceDecimals: TOKENS[0].decimals,
sourceTokenInfo: { ...TOKENS[0] },
destinationTokenInfo: { ...TOKENS[1] },
},
{ chainId: MAINNET_CHAIN_ID },
);
assert.deepStrictEqual(result, expectedResult2);
});
});
describe('fetchTokens', function () {
it('should fetch tokens', async function () {
const result = await fetchTokens(true);
const result = await fetchTokens(MAINNET_CHAIN_ID);
assert.deepStrictEqual(result, EXPECTED_TOKENS_RESULT);
});
it('should fetch tokens on prod', async function () {
const result = await fetchTokens(false);
const result = await fetchTokens(MAINNET_CHAIN_ID);
assert.deepStrictEqual(result, EXPECTED_TOKENS_RESULT);
});
});
describe('fetchAggregatorMetadata', function () {
it('should fetch aggregator metadata', async function () {
const result = await fetchAggregatorMetadata(true);
const result = await fetchAggregatorMetadata(MAINNET_CHAIN_ID);
assert.deepStrictEqual(result, AGGREGATOR_METADATA);
});
it('should fetch aggregator metadata on prod', async function () {
const result = await fetchAggregatorMetadata(false);
const result = await fetchAggregatorMetadata(MAINNET_CHAIN_ID);
assert.deepStrictEqual(result, AGGREGATOR_METADATA);
});
});
@ -148,12 +152,12 @@ describe('Swaps Util', function () {
},
};
it('should fetch top assets', async function () {
const result = await fetchTopAssets(true);
const result = await fetchTopAssets(MAINNET_CHAIN_ID);
assert.deepStrictEqual(result, expectedResult);
});
it('should fetch top assets on prod', async function () {
const result = await fetchTopAssets(false);
const result = await fetchTopAssets(MAINNET_CHAIN_ID);
assert.deepStrictEqual(result, expectedResult);
});
});

View File

@ -36,7 +36,8 @@ import {
getSelectedAccount,
getCurrentCurrency,
getTokenExchangeRates,
getSwapsEthToken,
getSwapsDefaultToken,
getCurrentChainId,
} from '../../../selectors';
import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util';
import { getTokens } from '../../../ducks/metamask/metamask';
@ -125,7 +126,8 @@ export default function ViewQuote() {
const usedQuote = selectedQuote || topQuote;
const tradeValue = usedQuote?.trade?.value ?? '0x0';
const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime);
const swapsEthToken = useSelector(getSwapsEthToken);
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
const chainId = useSelector(getCurrentChainId);
const { isBestQuote } = usedQuote;
@ -151,8 +153,8 @@ export default function ViewQuote() {
const { tokensWithBalances } = useTokenTracker(swapsTokens, true);
const balanceToken =
fetchParamsSourceToken === swapsEthToken.address
? swapsEthToken
fetchParamsSourceToken === defaultSwapsToken.address
? defaultSwapsToken
: tokensWithBalances.find(
({ address }) => address === fetchParamsSourceToken,
);
@ -183,6 +185,7 @@ export default function ViewQuote() {
currentCurrency,
approveGas,
memoizedTokenConversionRates,
chainId,
);
}, [
quotes,
@ -191,6 +194,7 @@ export default function ViewQuote() {
currentCurrency,
approveGas,
memoizedTokenConversionRates,
chainId,
]);
const renderableDataForUsedQuote = renderablePopoverData.find(
@ -209,31 +213,33 @@ export default function ViewQuote() {
sourceTokenIconUrl,
} = renderableDataForUsedQuote;
const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote(
usedGasLimit,
const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote({
tradeGas: usedGasLimit,
approveGas,
gasPrice,
currentCurrency,
conversionRate,
tradeValue,
sourceTokenSymbol,
usedQuote.sourceAmount,
);
sourceSymbol: sourceTokenSymbol,
sourceAmount: usedQuote.sourceAmount,
chainId,
});
const {
feeInFiat: maxFeeInFiat,
feeInEth: maxFeeInEth,
nonGasFee,
} = getRenderableNetworkFeesForQuote(
maxGasLimit,
} = getRenderableNetworkFeesForQuote({
tradeGas: maxGasLimit,
approveGas,
gasPrice,
currentCurrency,
conversionRate,
tradeValue,
sourceTokenSymbol,
usedQuote.sourceAmount,
);
sourceSymbol: sourceTokenSymbol,
sourceAmount: usedQuote.sourceAmount,
chainId,
});
const tokenCost = new BigNumber(usedQuote.sourceAmount);
const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus(
@ -481,9 +487,9 @@ export default function ViewQuote() {
<span key="swapApproveNeedMoreTokens-1" className="view-quote__bold">
{tokenBalanceNeeded || ethBalanceNeeded}
</span>,
tokenBalanceNeeded && !(sourceTokenSymbol === 'ETH')
tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol)
? sourceTokenSymbol
: 'ETH',
: defaultSwapsToken.symbol,
]);
// Price difference warning
@ -643,7 +649,7 @@ export default function ViewQuote() {
setSelectQuotePopoverShown(true);
}}
tokenConversionRate={
destinationTokenSymbol === 'ETH'
destinationTokenSymbol === defaultSwapsToken.symbol
? 1
: memoizedTokenConversionRates[destinationToken.address]
}
@ -655,7 +661,7 @@ export default function ViewQuote() {
setSubmitClicked(true);
if (!balanceError) {
dispatch(signAndSendTransactions(history, metaMetricsEvent));
} else if (destinationToken.symbol === 'ETH') {
} else if (destinationToken.symbol === defaultSwapsToken.symbol) {
history.push(DEFAULT_ROUTE);
} else {
history.push(`${ASSET_ROUTE}/${destinationToken.address}`);

View File

@ -15,7 +15,10 @@ import {
getValueFromWeiHex,
hexToDecimal,
} from '../helpers/utils/conversions.util';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
ALLOWED_SWAPS_CHAIN_IDS,
} from '../../../shared/constants/swaps';
/**
* One of the only remaining valid uses of selecting the network subkey of the
@ -431,22 +434,26 @@ export function getWeb3ShimUsageStateForOrigin(state, origin) {
* minimal token units (according to its decimals).
* `string` is the token balance in a readable format, ready for rendering.
*
* Swaps treats ETH as a token, and we use the ETH_SWAPS_TOKEN_OBJECT constant
* to set the standard properties for the token. The getSwapsEthToken selector
* extends that object with `balance` and `balance` values of the same type as
* in regular ERC-20 token objects, per the above description.
* Swaps treats the selected chain's currency as a token, and we use the token constants
* in the SWAPS_CHAINID_DEFAULT_TOKEN_MAP to set the standard properties for
* the token. The getSwapsDefaultToken selector extends that object with
* `balance` and `string` values of the same type as in regular ERC-20 token
* objects, per the above description.
*
* @param {object} state - the redux state object
* @returns {SwapsEthToken} The token object representation of the currently
* selected account's ETH balance, as expected by the Swaps API.
*/
export function getSwapsEthToken(state) {
export function getSwapsDefaultToken(state) {
const selectedAccount = getSelectedAccount(state);
const { balance } = selectedAccount;
const chainId = getCurrentChainId(state);
const defaultTokenObject = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId];
return {
...ETH_SWAPS_TOKEN_OBJECT,
...defaultTokenObject,
balance: hexToDecimal(balance),
string: getValueFromWeiHex({
value: balance,
@ -455,3 +462,8 @@ export function getSwapsEthToken(state) {
}),
};
}
export function getIsSwapsChain(state) {
const chainId = getCurrentChainId(state);
return ALLOWED_SWAPS_CHAIN_IDS[chainId];
}