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

Version v9.3.0 RC (#10739)

* Replace logic for eth swap token in fetchQuotesAndSetQuoteState with getSwapsEthToken call (#10624)

* Move swaps constants to the shared constants directory (#10614)

* Fix: ETH 'token' now only appears once in the swaps to and from dropdowns. (#10650)

* 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

* Additional swaps network support (#10721)

* Add swaps support for bnc chain

* Use single default token address in shared/constants/swaps

* Ensure swaps gas prices are fetched from the correct chain specific endpoint (#10744)

* Ensure swaps gas prices are fetched from the correct chain specific endpoint

* Just rely on fetchWithCache to cache swaps gas prices, instead of directly using storage in getSwapsPriceEstimatesLastRetrieved

* Empty commit

* update @metamask/etherscan-link to v2.0.0 (#10747)

* Use correct block explorer name and link in swaps when on custom network (#10743)

* Use correct block explorer name and link in swaps when on custom network.

* Fix up custom etherscan link code in build-quote.js

* Use blockExplorerUrl hostname instead of 'blockExplorerBaseUrl'

* Use correct etherscan-link method for token links in build-quote

* Create correct token link in build-quote for mainnet AND custom networks

* Block explorer url improvements in awaiting-swap.js and build-quote.js

* Use swapVerifyTokenExplanation message with substitutable block explorer for all applicable locales

* Ensure that block explorer links are not shown in awaiting-swap if no url is available

* Ensure that the correct default currency symbols are used for fees on the view quote screen (#10753)

* Updating y18n and netmask to resolve dependency issues (#10765)

netmask@1.0.6 -> 2.0.1, y18n@3.2.1 -> 3.2.2, y18n@4.0.0 -> 4.0.1

* Ensure that priceSlippage fiat amounts are always shown in view-quote.js (#10762)

* Ensure that the approval fee in the swaps custom gas modal is in network specific currency (#10763)

* Use network specific swaps contract address when checking swap contract token approval (#10774)

* Set the BSC_CONTRACT_ADDRESS to lowercase (#10800)

* Ensure correct primary currency image is displayed on home screen and token list (#10777)

* [skip e2e] Update changelog for v9.3.0 (#10740)

* Version v9.3.0

* [skip e2e] Update changelog for v9.3.0 (#10803)

Co-authored-by: Dan J Miller <danjm.com@gmail.com>
Co-authored-by: ryanml <ryanlanese@gmail.com>
Co-authored-by: David Walsh <davidwalsh83@gmail.com>
Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com>
This commit is contained in:
MetaMask Bot 2021-04-02 12:30:57 -07:00 committed by GitHub
parent 73caeb2d4d
commit 14b5c389ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 845 additions and 503 deletions

View File

@ -2,6 +2,12 @@
## Current Develop Branch
## 9.3.0 Fri Mar 26 2021
- [#10777](https://github.com/MetaMask/metamask-extension/pull/10777): Display BNB token image for default currency on BSC network home screen
- [#10721](https://github.com/MetaMask/metamask-extension/pull/10721): Swaps support for the Binance network
- [#10658](https://github.com/MetaMask/metamask-extension/pull/10658): Swaps support for forked Mainnet on localhost
- [#10650](https://github.com/MetaMask/metamask-extension/pull/10650): Fix: ETH now only appears once in the swaps "to" and "from" dropdowns.
## 9.2.1 Thu Mar 25 2021
- [#10692](https://github.com/MetaMask/metamask-extension/pull/10692): Prevent UI crash when a 'wallet_requestPermissions" confirmation is queued behind a "wallet_addEthereumChain" confirmation
- [#10712](https://github.com/MetaMask/metamask-extension/pull/10712): Fix infinite spinner when request for token symbol fails while attempting an approve transaction

View File

@ -143,10 +143,6 @@
"amount": {
"message": "Amount"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Amount:"
},
@ -1940,7 +1936,8 @@
"message": "Using the best quote"
},
"swapVerifyTokenExplanation": {
"message": "Multiple tokens can use the same name and symbol. Check Etherscan to verify this is the token you're looking for."
"message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "View $1"

View File

@ -126,10 +126,6 @@
"amount": {
"message": "Cantidad"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Cantidad:"
},
@ -1814,7 +1810,8 @@
"message": "Utilizando la mejor cotización"
},
"swapVerifyTokenExplanation": {
"message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique Etherscan para verificar que este es el token que está buscando."
"message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique $1 para verificar que este es el token que está buscando.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Ver $1"

View File

@ -126,10 +126,6 @@
"amount": {
"message": "Monto"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Monto:"
},
@ -1814,7 +1810,8 @@
"message": "Utilizando la mejor cotización"
},
"swapVerifyTokenExplanation": {
"message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique Etherscan para verificar que este es el token que está buscando."
"message": "Varios tokens pueden usar el mismo nombre y símbolo. Verifique $1 para verificar que este es el token que está buscando.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Ver $1"

View File

@ -120,10 +120,6 @@
"amount": {
"message": "राशि"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "राशि:"
},
@ -1781,7 +1777,8 @@
"message": "अज्ञात"
},
"swapVerifyTokenExplanation": {
"message": "एकाधिक टोकन एक ही नाम और प्रतीक का उपयोग कर सकते हैं। यह सत्यापित करने के लिए Etherscan की जाँच करें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।"
"message": "एकाधिक टोकन एक ही नाम और प्रतीक का उपयोग कर सकते हैं। यह सत्यापित करने के लिए $1 की जाँच करें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "$1 देखें"

View File

@ -120,10 +120,6 @@
"amount": {
"message": "Jumlah"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Jumlah:"
},
@ -1781,7 +1777,8 @@
"message": "Tidak diketahui"
},
"swapVerifyTokenExplanation": {
"message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa Etherscan untuk memverifikasi inilah token yang Anda cari."
"message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Lihat $1"

View File

@ -126,10 +126,6 @@
"amount": {
"message": "Importo"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Importo:"
},
@ -1837,7 +1833,8 @@
"message": "Quotazione migliore"
},
"swapVerifyTokenExplanation": {
"message": "Più token possono usare lo stesso nome e simbolo. Verifica su Etherscan che questo sia il token che stai cercando."
"message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Vedi $1"

View File

@ -126,10 +126,6 @@
"amount": {
"message": "金額"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "金額:"
},
@ -1814,7 +1810,8 @@
"message": "最適な見積を使用する"
},
"swapVerifyTokenExplanation": {
"message": "複数のトークンが同じ名前とシンボルであることがあります。Etherscanで実際のトークンでを確認してください。"
"message": "複数のトークンが同じ名前とシンボルであることがあります。$1で実際のトークンでを確認してください。",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "$1 を表示"

View File

@ -120,10 +120,6 @@
"amount": {
"message": "금액"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "금액:"
},
@ -1778,7 +1774,8 @@
"message": "알 수 없음"
},
"swapVerifyTokenExplanation": {
"message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. Etherscan을 확인하여 이것이 원하는 토큰인지 확인하세요."
"message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1을 확인하여 이것이 원하는 토큰인지 확인하세요.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "$1 보기"

View File

@ -120,10 +120,6 @@
"amount": {
"message": "Сумма"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Сумма:"
},
@ -1781,7 +1777,8 @@
"message": "Неизвестный"
},
"swapVerifyTokenExplanation": {
"message": "Несколько токенов могут использовать одно и то же имя и символ. Проверьте Etherscan, чтобы убедиться, что это именно тот токен, который вы ищете."
"message": "Несколько токенов могут использовать одно и то же имя и символ. Проверьте $1, чтобы убедиться, что это именно тот токен, который вы ищете.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Просмотреть $1"

View File

@ -120,10 +120,6 @@
"amount": {
"message": "Halaga"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Halaga:"
},
@ -1778,7 +1774,8 @@
"message": "Hindi Alam"
},
"swapVerifyTokenExplanation": {
"message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang Etherscan para ma-verify na ito ang token na hinahanap mo."
"message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Tingnan ang $1"

View File

@ -120,10 +120,6 @@
"amount": {
"message": "Số tiền"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "Số tiền:"
},
@ -1781,7 +1777,8 @@
"message": "Không xác định"
},
"swapVerifyTokenExplanation": {
"message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra trên Etherscan để xác minh xem đây có phải là token bạn đang tìm kiếm không."
"message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra trên $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Xem $1"

View File

@ -126,10 +126,6 @@
"amount": {
"message": "数额"
},
"amountInEth": {
"message": "$1 ETH",
"description": "Displays an eth amount to the user. $1 is a decimal number"
},
"amountWithColon": {
"message": "数额:"
},
@ -1814,7 +1810,8 @@
"message": "使用最好的报价"
},
"swapVerifyTokenExplanation": {
"message": "多个代币可以使用相同的名称和符号。检查 Etherscan以太坊浏览器以确认这是您正在寻找的代币。"
"message": "多个代币可以使用相同的名称和符号。检查 $1以太坊浏览器以确认这是您正在寻找的代币。",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "查看 $1"

BIN
app/images/bnb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -78,6 +78,6 @@
"notifications"
],
"short_name": "__MSG_appName__",
"version": "9.2.1",
"version": "9.3.0",
"web_accessible_resources": ["inpage.js", "phishing.html"]
}

View File

@ -8,12 +8,14 @@ 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 '../../../ui/app/helpers/constants/swaps';
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
} from '../../../shared/constants/swaps';
import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils';
import {
fetchTradesInfo as defaultFetchTradesInfo,
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
@ -21,8 +23,6 @@ import {
} from '../../../ui/app/pages/swaps/swaps.util';
import { NETWORK_EVENTS } from './network';
const METASWAP_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c';
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
const MAX_GAS_LIMIT = 2500000;
@ -85,6 +85,7 @@ export default class SwapsController {
fetchTradesInfo = defaultFetchTradesInfo,
fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime,
getCurrentChainId,
}) {
this.store = new ObservableStore({
swapsState: { ...initialState.swapsState },
@ -93,6 +94,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 +118,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 +161,8 @@ export default class SwapsController {
fetchParamsMetaData = {},
isPolledRequest,
) {
const { chainId } = fetchParamsMetaData;
if (!fetchParams) {
return null;
}
@ -177,7 +182,7 @@ export default class SwapsController {
this.indexOfNewestCallInFlight = indexOfCurrentCall;
let [newQuotes] = await Promise.all([
this._fetchTradesInfo(fetchParams),
this._fetchTradesInfo(fetchParams, fetchParamsMetaData),
this._setSwapsQuoteRefreshTime(),
]);
@ -191,12 +196,13 @@ 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(
fetchParams.sourceToken,
fetchParams.fromAddress,
chainId,
);
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
@ -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;
@ -664,13 +675,16 @@ export default class SwapsController {
return [topAggId, newQuotes];
}
async _getERC20Allowance(contractAddress, walletAddress) {
async _getERC20Allowance(contractAddress, walletAddress, chainId) {
const contract = new ethers.Contract(
contractAddress,
abi,
this.ethersProvider,
);
return await contract.allowance(walletAddress, METASWAP_ADDRESS);
return await contract.allowance(
walletAddress,
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId],
);
}
/**
@ -726,13 +740,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

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

View File

@ -373,6 +373,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

@ -72,6 +72,7 @@
"3box/**/libp2p-crypto/node-forge": "^0.10.0",
"3box/**/libp2p-keychain/node-forge": "^0.10.0",
"analytics-node/axios": "^0.21.1",
"netmask": "^2.0.1",
"pull-ws": "^3.3.2"
},
"dependencies": {
@ -86,7 +87,7 @@
"@metamask/controllers": "^5.1.0",
"@metamask/eth-ledger-bridge-keyring": "^0.3.0",
"@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^1.5.0",
"@metamask/etherscan-link": "^2.0.0",
"@metamask/inpage-provider": "^8.0.4",
"@metamask/jazzicon": "^2.0.0",
"@metamask/logo": "^2.5.0",

View File

@ -29,6 +29,14 @@ export const KOVAN_DISPLAY_NAME = 'Kovan';
export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet';
export const GOERLI_DISPLAY_NAME = 'Goerli';
export const ETH_SYMBOL = 'ETH';
export const TEST_ETH_SYMBOL = 'TESTETH';
export const BNB_SYMBOL = 'BNB';
export const ETH_TOKEN_IMAGE_URL = './images/eth_logo.svg';
export const TEST_ETH_TOKEN_IMAGE_URL = './images/black-eth-logo.svg';
export const BNB_TOKEN_IMAGE_URL = './images/bnb.png';
export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI];
export const TEST_CHAINS = [
@ -79,3 +87,9 @@ export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values(
chainIdToNetworkIdMap[chainId] = networkId;
return chainIdToNetworkIdMap;
}, {});
export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = {
[ETH_SYMBOL]: ETH_TOKEN_IMAGE_URL,
[TEST_ETH_SYMBOL]: TEST_ETH_TOKEN_IMAGE_URL,
[BNB_SYMBOL]: BNB_TOKEN_IMAGE_URL,
};

89
shared/constants/swaps.js Normal file
View File

@ -0,0 +1,89 @@
import {
MAINNET_CHAIN_ID,
ETH_SYMBOL,
TEST_ETH_SYMBOL,
BNB_SYMBOL,
TEST_ETH_TOKEN_IMAGE_URL,
BNB_TOKEN_IMAGE_URL,
} 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 the default token for the current network, in place of the token address that ERC-20 tokens have
const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ETH_SWAPS_TOKEN_OBJECT = {
symbol: ETH_SYMBOL,
name: 'Ether',
address: DEFAULT_TOKEN_ADDRESS,
decimals: 18,
iconUrl: './images/black-eth-logo.svg',
};
export const BNB_SWAPS_TOKEN_OBJECT = {
symbol: BNB_SYMBOL,
name: 'Binance Coin',
address: DEFAULT_TOKEN_ADDRESS,
decimals: 18,
iconUrl: BNB_TOKEN_IMAGE_URL,
};
export const TEST_ETH_SWAPS_TOKEN_OBJECT = {
symbol: TEST_ETH_SYMBOL,
name: 'Test Ether',
address: DEFAULT_TOKEN_ADDRESS,
decimals: 18,
iconUrl: TEST_ETH_TOKEN_IMAGE_URL,
};
// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations
export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0';
const MAINNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c';
const TESTNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c';
const BSC_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
const METASWAP_ETH_API_HOST = 'https://api.metaswap.codefi.network';
const METASWAP_BNB_API_HOST = 'https://bsc-api.metaswap.codefi.network';
export const BNB_CHAIN_ID = '0x38';
const SWAPS_TESTNET_CHAIN_ID = '0x539';
const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network';
const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/';
export const ALLOWED_SWAPS_CHAIN_IDS = {
[MAINNET_CHAIN_ID]: true,
[SWAPS_TESTNET_CHAIN_ID]: true,
[BNB_CHAIN_ID]: true,
};
export const METASWAP_CHAINID_API_HOST_MAP = {
[MAINNET_CHAIN_ID]: METASWAP_ETH_API_HOST,
[SWAPS_TESTNET_CHAIN_ID]: SWAPS_TESTNET_HOST,
[BNB_CHAIN_ID]: METASWAP_BNB_API_HOST,
};
export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = {
[MAINNET_CHAIN_ID]: MAINNET_CONTRACT_ADDRESS,
[SWAPS_TESTNET_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS,
[BNB_CHAIN_ID]: BSC_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,
[BNB_CHAIN_ID]: BNB_SWAPS_TOKEN_OBJECT,
};
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BNB_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL,
};

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

@ -8,8 +8,9 @@ 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 '../../../../ui/app/helpers/constants/swaps';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../stub/provider';
import SwapsController, {
utils,
@ -77,6 +78,7 @@ const MOCK_FETCH_METADATA = {
symbol: 'FOO',
decimals: 18,
},
chainId: MAINNET_CHAIN_ID,
};
const MOCK_TOKEN_RATES_STORE = new ObservableStore({
@ -133,6 +135,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;
@ -147,6 +151,7 @@ describe('SwapsController', function () {
fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub,
getCurrentChainId: getCurrentChainIdStub,
});
};
@ -196,6 +201,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];
@ -220,6 +226,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];
@ -244,6 +251,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];
@ -688,7 +696,10 @@ describe('SwapsController', function () {
});
assert.strictEqual(
fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS),
fetchTradesInfoStub.calledOnceWithExactly(
MOCK_FETCH_PARAMS,
MOCK_FETCH_METADATA,
),
true,
);
});
@ -710,6 +721,7 @@ describe('SwapsController', function () {
allowanceStub.calledOnceWithExactly(
MOCK_FETCH_PARAMS.sourceToken,
MOCK_FETCH_PARAMS.fromAddress,
MAINNET_CHAIN_ID,
),
true,
);

View File

@ -26,6 +26,7 @@ const AssetListItem = ({
warning,
primary,
secondary,
identiconBorder,
}) => {
const t = useI18nContext();
const dispatch = useDispatch();
@ -115,6 +116,7 @@ const AssetListItem = ({
address={tokenAddress}
image={tokenImage}
alt={`${primary} ${tokenSymbol}`}
imageBorder={identiconBorder}
/>
}
midContent={midContent}
@ -140,6 +142,7 @@ AssetListItem.propTypes = {
warning: PropTypes.node,
primary: PropTypes.string,
secondary: PropTypes.string,
identiconBorder: PropTypes.bool,
};
AssetListItem.defaultProps = {

View File

@ -13,6 +13,7 @@ import {
getCurrentAccountWithSendEtherInfo,
getNativeCurrency,
getShouldShowFiat,
getNativeCurrencyImage,
} from '../../../selectors';
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay';
@ -63,6 +64,8 @@ const AssetList = ({ onClickAsset }) => {
},
);
const primaryTokenImage = useSelector(getNativeCurrencyImage);
return (
<>
<AssetListItem
@ -71,6 +74,8 @@ const AssetList = ({ onClickAsset }) => {
primary={primaryCurrencyProperties.value}
tokenSymbol={primaryCurrencyProperties.suffix}
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
tokenImage={primaryTokenImage}
identiconBorder
/>
<TokenList
onTokenClick={(tokenAddress) => {

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 '../../../helpers/constants/swaps';
import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../../shared/constants/swaps';
import { TRANSACTION_CATEGORIES } 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)))
);
};
@ -43,12 +54,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;
@ -67,6 +79,7 @@ export default function TransactionList({
const unfilteredCompletedTransactions = useSelector(
nonceSortedCompletedTransactionsSelector,
);
const chainId = useSelector(getCurrentChainId);
const pendingTransactions = useMemo(
() =>
@ -74,8 +87,14 @@ export default function TransactionList({
unfilteredPendingTransactions,
hideTokenTransactions,
tokenAddress,
chainId,
),
[hideTokenTransactions, tokenAddress, unfilteredPendingTransactions],
[
hideTokenTransactions,
tokenAddress,
unfilteredPendingTransactions,
chainId,
],
);
const completedTransactions = useMemo(
() =>
@ -83,8 +102,14 @@ export default function TransactionList({
unfilteredCompletedTransactions,
hideTokenTransactions,
tokenAddress,
chainId,
),
[hideTokenTransactions, tokenAddress, unfilteredCompletedTransactions],
[
hideTokenTransactions,
tokenAddress,
unfilteredCompletedTransactions,
chainId,
],
);
const viewMore = useCallback(

View File

@ -25,7 +25,9 @@ import {
getIsMainnet,
getIsTestnet,
getCurrentKeyring,
getSwapsEthToken,
getSwapsDefaultToken,
getIsSwapsChain,
getNativeCurrencyImage,
} from '../../../selectors/selectors';
import SwapIcon from '../../ui/icon/swap-icon.component';
import BuyIcon from '../../ui/icon/overview-buy-icon.component';
@ -63,13 +65,16 @@ const EthOverview = ({ className }) => {
const { balance } = selectedAccount;
const isMainnetChain = useSelector(getIsMainnet);
const isTestnetChain = useSelector(getIsTestnet);
const isSwapsChain = useSelector(getIsSwapsChain);
const primaryTokenImage = useSelector(getNativeCurrencyImage);
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 +141,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 +159,7 @@ const EthOverview = ({ className }) => {
<Tooltip
title={t('onlyAvailableOnMainnet')}
position="bottom"
disabled={isMainnetChain}
disabled={isSwapsChain}
>
{contents}
</Tooltip>
@ -164,7 +169,7 @@ const EthOverview = ({ className }) => {
</>
}
className={className}
icon={<Identicon diameter={32} />}
icon={<Identicon diameter={32} image={primaryTokenImage} imageBorder />}
/>
);
};

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

@ -22,6 +22,7 @@ export default class Identicon extends PureComponent {
image: PropTypes.string,
useBlockie: PropTypes.bool,
alt: PropTypes.string,
imageBorder: PropTypes.bool,
};
static defaultProps = {
@ -35,11 +36,13 @@ export default class Identicon extends PureComponent {
};
renderImage() {
const { className, diameter, image, alt } = this.props;
const { className, diameter, image, alt, imageBorder } = this.props;
return (
<img
className={classnames('identicon', className)}
className={classnames('identicon', className, {
'identicon__image-border': imageBorder,
})}
src={image}
style={getStyles(diameter)}
alt={alt}
@ -75,15 +78,7 @@ export default class Identicon extends PureComponent {
}
render() {
const {
className,
address,
image,
diameter,
useBlockie,
addBorder,
alt,
} = this.props;
const { address, image, useBlockie, addBorder, diameter } = this.props;
if (image) {
return this.renderImage();
@ -106,12 +101,7 @@ export default class Identicon extends PureComponent {
}
return (
<img
className={classnames('identicon__eth-logo', className)}
src="./images/eth_logo.svg"
style={getStyles(diameter)}
alt={alt}
/>
<div style={getStyles(diameter)} className="identicon__image-border" />
);
}
}

View File

@ -20,7 +20,7 @@
border-color: $primary-blue;
}
&__eth-logo {
&__image-border {
border: 1px solid $alto;
background: $white;
}

View File

@ -16,13 +16,10 @@ describe('Identicon', function () {
const mockStore = configureMockStore(middlewares);
const store = mockStore(state);
it('renders default eth_logo identicon with no props', function () {
it('renders empty identicon with no props', function () {
const wrapper = mount(<Identicon store={store} />);
assert.strictEqual(
wrapper.find('img.identicon__eth-logo').prop('src'),
'./images/eth_logo.svg',
);
assert.ok(wrapper.find('div'), 'Empty identicon found');
});
it('renders custom image and add className props', function () {

View File

@ -2,8 +2,6 @@ import { createSlice } from '@reduxjs/toolkit';
import BigNumber from 'bignumber.js';
import log from 'loglevel';
import { getStorageItem, setStorageItem } from '../../../lib/storage-helpers';
import {
addToken,
addUnapprovedTransaction,
@ -41,7 +39,6 @@ import {
decimalToHex,
getValueFromWeiHex,
decGWEIToHexWEI,
hexToDecimal,
hexWEIToDecGWEI,
} from '../../helpers/utils/conversions.util';
import { conversionLessThan } from '../../helpers/utils/conversion-util';
@ -50,14 +47,15 @@ import {
getSelectedAccount,
getTokenExchangeRates,
getUSDConversionRate,
getSwapsDefaultToken,
getCurrentChainId,
} from '../../selectors';
import {
ERROR_FETCHING_QUOTES,
QUOTES_NOT_AVAILABLE_ERROR,
ETH_SWAPS_TOKEN_OBJECT,
SWAP_FAILED_ERROR,
SWAPS_FETCH_ORDER_CONFLICT,
} from '../../helpers/constants/swaps';
} from '../../../../shared/constants/swaps';
import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction';
const GAS_PRICES_LOADING_STATES = {
@ -83,7 +81,6 @@ const initialState = {
limit: null,
loading: GAS_PRICES_LOADING_STATES.INITIAL,
priceEstimates: {},
priceEstimatesLastRetrieved: 0,
fallBackPrice: null,
},
};
@ -145,8 +142,6 @@ const slice = createSlice({
swapGasPriceEstimatesFetchCompleted: (state, action) => {
state.customGas.priceEstimates = action.payload.priceEstimates;
state.customGas.loading = GAS_PRICES_LOADING_STATES.COMPLETED;
state.customGas.priceEstimatesLastRetrieved =
action.payload.priceEstimatesLastRetrieved;
},
retrievedFallbackSwapsGasPrice: (state, action) => {
state.customGas.fallBackPrice = action.payload;
@ -190,9 +185,6 @@ export const swapGasEstimateLoadingHasFailed = (state) =>
export const getSwapGasPriceEstimateData = (state) =>
state.swaps.customGas.priceEstimates;
export const getSwapsPriceEstimatesLastRetrieved = (state) =>
state.swaps.customGas.priceEstimatesLastRetrieved;
export const getSwapsFallbackGasPrice = (state) =>
state.swaps.customGas.fallBackPrice;
@ -377,9 +369,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);
}
@ -390,21 +384,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'
? {
...ETH_SWAPS_TOKEN_OBJECT,
string: getValueFromWeiHex({
value: selectedAccount.balance,
numberOfDecimals: 4,
toDenomination: 'ETH',
}),
balance: hexToDecimal(selectedAccount.balance),
}
fetchParams?.metaData?.sourceTokenInfo?.symbol ===
swapsDefaultToken.symbol
? swapsDefaultToken
: fetchParams?.metaData?.sourceTokenInfo;
const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {};
const selectedToToken =
@ -429,7 +416,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(
@ -442,7 +432,7 @@ export const fetchQuotesAndSetQuoteState = (
);
}
if (
fromTokenSymbol !== 'ETH' &&
fromTokenSymbol !== swapsDefaultToken.symbol &&
!contractExchangeRates[fromTokenAddress] &&
fromTokenBalance &&
new BigNumber(fromTokenBalance, 16).gt(0)
@ -503,6 +493,7 @@ export const fetchQuotesAndSetQuoteState = (
sourceTokenInfo,
destinationTokenInfo,
accountBalance: selectedAccount.balance,
chainId,
},
),
);
@ -572,9 +563,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);
}
@ -585,7 +579,6 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
return;
}
const state = getState();
const customSwapsGas = getCustomSwapsGas(state);
const fetchParams = getFetchParams(state);
const { metaData, value: swapTokenValue, slippage } = fetchParams;
@ -746,26 +739,13 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
export function fetchMetaSwapsGasPriceEstimates() {
return async (dispatch, getState) => {
const state = getState();
const priceEstimatesLastRetrieved = getSwapsPriceEstimatesLastRetrieved(
state,
);
const timeLastRetrieved =
priceEstimatesLastRetrieved ||
(await getStorageItem('METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED')) ||
0;
const chainId = getCurrentChainId(state);
dispatch(swapGasPriceEstimatesFetchStarted());
let priceEstimates;
try {
if (Date.now() - timeLastRetrieved > 30000) {
priceEstimates = await fetchSwapsGasPrices();
} else {
const cachedPriceEstimates = await getStorageItem(
'METASWAP_GAS_PRICE_ESTIMATES',
);
priceEstimates = cachedPriceEstimates || (await fetchSwapsGasPrices());
}
priceEstimates = await fetchSwapsGasPrices(chainId);
} catch (e) {
log.warn('Fetching swaps gas prices failed:', e);
@ -790,20 +770,9 @@ export function fetchMetaSwapsGasPriceEstimates() {
}
}
const timeRetrieved = Date.now();
await Promise.all([
setStorageItem('METASWAP_GAS_PRICE_ESTIMATES', priceEstimates),
setStorageItem(
'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED',
timeRetrieved,
),
]);
dispatch(
swapGasPriceEstimatesFetchCompleted({
priceEstimates,
priceEstimatesLastRetrieved: timeRetrieved,
}),
);
return priceEstimates;

View File

@ -1,25 +0,0 @@
// 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';
export const ETH_SWAPS_TOKEN_OBJECT = {
symbol: 'ETH',
name: 'Ether',
address: ETH_SWAPS_TOKEN_ADDRESS,
decimals: 18,
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';
// 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';
export const METASWAP_API_HOST = 'https://api.metaswap.codefi.network';

View File

@ -1,3 +1,4 @@
export function formatETHFee(ethFee) {
return `${ethFee} ETH`;
// TODO: Rename to reflect that this function is used for more cases than ETH, and update all uses.
export function formatETHFee(ethFee, currencySymbol = 'ETH') {
return `${ethFee} ${currencySymbol}`;
}

View File

@ -12,12 +12,14 @@ import {
getShouldShowFiat,
getNativeCurrency,
getCurrentCurrency,
getCurrentChainId,
} from '../../selectors';
import { getTokens } from '../../ducks/metamask/metamask';
import * as i18nhooks from '../useI18nContext';
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_CATEGORIES,
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

@ -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 '../helpers/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 {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../shared/modules/swaps.utils';
import { TRANSACTION_CATEGORIES } from '../../../shared/constants/transaction';
import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps';
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 { transactionCategory } = 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 =
transactionCategory === TRANSACTION_CATEGORIES.SWAP &&
@ -42,6 +52,8 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) {
address,
senderAddress,
decimals,
null,
chainId,
)
: transactionCategory === TRANSACTION_CATEGORIES.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,
@ -69,42 +76,36 @@ export function getRenderableTokenData(
};
}
export function useTokensToSearch({
providedTokens,
usersTokens = [],
topTokens = {},
onlyEth,
singleToken,
}) {
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) || [];
let tokensToSearch;
if (onlyEth) {
tokensToSearch = [memoizedEthToken];
} else if (singleToken) {
tokensToSearch = providedTokens;
} else if (providedTokens) {
tokensToSearch = [memoizedEthToken, ...providedTokens];
} else if (swapsTokens.length) {
tokensToSearch = [memoizedEthToken, ...swapsTokens];
} else {
tokensToSearch = [memoizedEthToken, ...tokenList];
}
const tokensToSearch = swapsTokens.length
? swapsTokens
: [
memoizedDefaultToken,
...tokenList.filter(
(token) => token.symbol !== memoizedDefaultToken.symbol,
),
];
const memoizedTokensToSearch = useEqualityCheck(tokensToSearch);
return useMemo(() => {
const usersTokensAddressMap = memoizedUsersToken.reduce(
@ -113,7 +114,7 @@ export function useTokensToSearch({
);
const tokensToSearchBuckets = {
owned: singleToken ? [] : [memoizedEthToken],
owned: [],
top: [],
others: [],
};
@ -124,10 +125,11 @@ export function useTokensToSearch({
tokenConversionRates,
conversionRate,
currentCurrency,
chainId,
);
if (
usersTokensAddressMap[token.address] &&
(renderableDataToken.symbol === 'ETH' ||
isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) ||
(usersTokensAddressMap[token.address] &&
Number(renderableDataToken.balance ?? 0) !== 0)
) {
tokensToSearchBuckets.owned.push(renderableDataToken);
@ -158,7 +160,6 @@ export function useTokensToSearch({
conversionRate,
currentCurrency,
memoizedTopTokens,
memoizedEthToken,
singleToken,
chainId,
]);
}

View File

@ -4,7 +4,7 @@ import SendRowWrapper from '../send-row-wrapper';
import Identicon from '../../../../components/ui/identicon/identicon.component';
import TokenBalance from '../../../../components/ui/token-balance';
import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display';
import { ERC20, ETH, PRIMARY } from '../../../../helpers/constants/common';
import { ERC20, PRIMARY } from '../../../../helpers/constants/common';
export default class SendAssetRow extends Component {
static propTypes = {
@ -20,6 +20,7 @@ export default class SendAssetRow extends Component {
sendTokenAddress: PropTypes.string,
setSendToken: PropTypes.func.isRequired,
nativeCurrency: PropTypes.string,
nativeCurrencyImage: PropTypes.string,
};
static contextTypes = {
@ -103,7 +104,12 @@ export default class SendAssetRow extends Component {
renderNativeCurrency(insideDropdown = false) {
const { t } = this.context;
const { accounts, selectedAddress, nativeCurrency } = this.props;
const {
accounts,
selectedAddress,
nativeCurrency,
nativeCurrencyImage,
} = this.props;
const balanceValue = accounts[selectedAddress]
? accounts[selectedAddress].balance
@ -121,7 +127,8 @@ export default class SendAssetRow extends Component {
<div className="send-v2__asset-dropdown__asset-icon">
<Identicon
diameter={36}
address={nativeCurrency === ETH ? undefined : nativeCurrency}
image={nativeCurrencyImage}
address={nativeCurrency}
/>
</div>
<div className="send-v2__asset-dropdown__asset-data">

View File

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import {
getMetaMaskAccounts,
getNativeCurrency,
getNativeCurrencyImage,
getSendTokenAddress,
} from '../../../../selectors';
import { updateSendToken } from '../../../../store/actions';
@ -14,6 +15,7 @@ function mapStateToProps(state) {
sendTokenAddress: getSendTokenAddress(state),
accounts: getMetaMaskAccounts(state),
nativeCurrency: getNativeCurrency(state),
nativeCurrencyImage: getNativeCurrencyImage(state),
};
}

View File

@ -3,15 +3,18 @@ import React, { useContext, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import { createCustomExplorerLink } from '@metamask/etherscan-link';
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,19 +26,24 @@ 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,
ERROR_FETCHING_QUOTES,
QUOTES_NOT_AVAILABLE_ERROR,
OFFLINE_FOR_MAINTENANCE,
} from '../../../helpers/constants/swaps';
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP,
} from '../../../../../shared/constants/swaps';
import { CHAIN_ID_TO_TYPE_MAP as VALID_INFURA_CHAIN_IDS } from '../../../../../shared/constants/network';
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';
import SwapsFooter from '../swaps-footer';
import { getBlockExplorerUrlForTx } from '../../../../../shared/modules/transaction.utils';
import SwapFailureIcon from './swap-failure-icon';
import SwapSuccessIcon from './swap-success-icon';
import QuotesTimeoutIcon from './quotes-timeout-icon';
@ -73,16 +81,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;
}
@ -100,8 +109,21 @@ export default function AwaitingSwap({
category: 'swaps',
});
const blockExplorerUrl =
txHash && getBlockExplorerUrlForTx({ chainId, hash: txHash }, rpcPrefs);
let blockExplorerUrl;
if (txHash && rpcPrefs.blockExplorerUrl) {
blockExplorerUrl = getBlockExplorerUrlForTx({ hash: txHash }, rpcPrefs);
} else if (txHash && SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) {
blockExplorerUrl = createCustomExplorerLink(
txHash,
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId],
);
} else if (txHash && VALID_INFURA_CHAIN_IDS[chainId]) {
blockExplorerUrl = getBlockExplorerUrlForTx({ chainId, hash: txHash });
}
const isCustomBlockExplorerUrl =
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ||
rpcPrefs.blockExplorerUrl;
let headerText;
let statusImage;
@ -133,7 +155,7 @@ export default function AwaitingSwap({
<ViewOnEtherScanLink
txHash={txHash}
blockExplorerUrl={blockExplorerUrl}
isCustomBlockExplorerUrl={Boolean(rpcPrefs.blockExplorerUrl)}
isCustomBlockExplorerUrl={isCustomBlockExplorerUrl}
/>
);
} else if (errorKey === QUOTES_EXPIRED_ERROR) {
@ -172,7 +194,7 @@ export default function AwaitingSwap({
<ViewOnEtherScanLink
txHash={txHash}
blockExplorerUrl={blockExplorerUrl}
isCustomBlockExplorerUrl={Boolean(rpcPrefs.blockExplorerUrl)}
isCustomBlockExplorerUrl={isCustomBlockExplorerUrl}
/>
);
} else if (!errorKey && swapComplete) {
@ -191,7 +213,7 @@ export default function AwaitingSwap({
<ViewOnEtherScanLink
txHash={txHash}
blockExplorerUrl={blockExplorerUrl}
isCustomBlockExplorerUrl={Boolean(rpcPrefs.blockExplorerUrl)}
isCustomBlockExplorerUrl={isCustomBlockExplorerUrl}
/>
);
}
@ -228,7 +250,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

@ -18,7 +18,7 @@ export default function ViewOnEtherScanLink({
onClick={() => global.platform.openTab({ url: blockExplorerUrl })}
>
{isCustomBlockExplorerUrl
? t('viewOnCustomBlockExplorer', [blockExplorerUrl])
? t('viewOnCustomBlockExplorer', [new URL(blockExplorerUrl).hostname])
: t('viewOnEtherscan')}
</div>
);

View File

@ -2,10 +2,17 @@ import React, { useContext, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import classnames from 'classnames';
import { uniqBy } from 'lodash';
import { uniqBy, isEqual } from 'lodash';
import { useHistory } from 'react-router-dom';
import {
createCustomTokenTrackerLink,
createTokenTrackerLinkForChain,
} from '@metamask/etherscan-link';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { useTokensToSearch } from '../../../hooks/useTokensToSearch';
import {
useTokensToSearch,
getRenderableTokenData,
} from '../../../hooks/useTokensToSearch';
import { useEqualityCheck } from '../../../hooks/useEqualityCheck';
import { I18nContext } from '../../../contexts/i18n';
import DropdownInputPair from '../dropdown-input-pair';
@ -25,7 +32,14 @@ import {
getTopAssets,
getFetchParams,
} from '../../../ducks/swaps/swaps';
import { getSwapsEthToken } from '../../../selectors';
import {
getSwapsDefaultToken,
getTokenExchangeRates,
getConversionRate,
getCurrentCurrency,
getCurrentChainId,
getRpcPrefsForCurrentProvider,
} from '../../../selectors';
import {
getValueFromWeiHex,
hexToDecimal,
@ -36,7 +50,11 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../helpers/constants/swaps';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../../../shared/modules/swaps.utils';
import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/swaps';
import { resetSwapsPostFetchState, removeToken } from '../../../store/actions';
import { fetchTokenPrice, fetchTokenBalance } from '../swaps.util';
@ -76,9 +94,20 @@ 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 rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
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);
@ -86,22 +115,22 @@ export default function BuildQuote({
// 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',
);
const memoizedUsersTokens = useEqualityCheck(usersTokens);
const selectedFromToken = useTokensToSearch({
providedTokens:
fromToken || fetchParamsFromToken
? [fromToken || fetchParamsFromToken]
: [],
usersTokens: memoizedUsersTokens,
onlyEth: (fromToken || fetchParamsFromToken)?.symbol === 'ETH',
singleToken: true,
})[0];
const selectedFromToken = getRenderableTokenData(
fromToken || fetchParamsFromToken,
tokenConversionRates,
conversionRate,
currentCurrency,
chainId,
);
const tokensToSearch = useTokensToSearch({
usersTokens: memoizedUsersTokens,
@ -110,9 +139,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,
@ -142,8 +171,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 (
@ -192,6 +222,30 @@ export default function BuildQuote({
);
};
let blockExplorerTokenLink;
let blockExplorerLabel;
if (rpcPrefs.blockExplorerUrl) {
blockExplorerTokenLink = createCustomTokenTrackerLink(
selectedToToken.address,
rpcPrefs.blockExplorerUrl,
);
blockExplorerLabel = new URL(rpcPrefs.blockExplorerUrl).hostname;
} else if (SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) {
blockExplorerTokenLink = createCustomTokenTrackerLink(
selectedToToken.address,
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId],
);
blockExplorerLabel = new URL(
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId],
).hostname;
} else {
blockExplorerTokenLink = createTokenTrackerLinkForChain(
selectedToToken.address,
chainId,
);
blockExplorerLabel = t('etherscan');
}
const { destinationTokenAddedForSwap } = fetchParams || {};
const { address: toAddress } = toToken || {};
const onToSelect = useCallback(
@ -218,15 +272,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,
@ -240,12 +296,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(
@ -260,7 +317,7 @@ export default function BuildQuote({
}),
);
}
}, [dispatch, fromToken, ethBalance]);
}, [dispatch, fromToken, ethBalance, chainId]);
useEffect(() => {
if (prevFromTokenBalance !== fromTokenBalance) {
@ -277,7 +334,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={() =>
@ -375,7 +432,7 @@ export default function BuildQuote({
defaultToAll
/>
</div>
{toTokenIsNotEth &&
{toTokenIsNotDefault &&
(occurances < 2 ? (
<ActionableMessage
message={
@ -386,17 +443,18 @@ export default function BuildQuote({
: t('swapTokenVerificationNoSource')}
</div>
<div>
{t('verifyThisTokenOn', [
<a
className="build-quote__token-etherscan-link build-quote__underline"
key="build-quote-etherscan-link"
href={`https://etherscan.io/token/${selectedToToken.address}`}
target="_blank"
rel="noopener noreferrer"
>
{t('etherscan')}
</a>,
])}
{blockExplorerTokenLink &&
t('verifyThisTokenOn', [
<a
className="build-quote__token-etherscan-link build-quote__underline"
key="build-quote-etherscan-link"
href={blockExplorerTokenLink}
target="_blank"
rel="noopener noreferrer"
>
{blockExplorerLabel}
</a>,
])}
</div>
</div>
}
@ -410,7 +468,10 @@ export default function BuildQuote({
}
type="warning"
withRightButton
infoTooltipText={t('swapVerifyTokenExplanation')}
infoTooltipText={
blockExplorerTokenLink &&
t('swapVerifyTokenExplanation', [blockExplorerLabel])
}
/>
) : (
<div className="build-quote__token-message">
@ -420,23 +481,29 @@ export default function BuildQuote({
>
{t('swapTokenVerificationSources', [occurances])}
</span>
{t('swapTokenVerificationMessage', [
<a
className="build-quote__token-etherscan-link"
key="build-quote-etherscan-link"
href={`https://etherscan.io/token/${selectedToToken.address}`}
target="_blank"
rel="noopener noreferrer"
>
{t('etherscan')}
</a>,
])}
<InfoTooltip
position="top"
contentText={t('swapVerifyTokenExplanation')}
containerClassName="build-quote__token-tooltip-container"
key="token-verification-info-tooltip"
/>
{blockExplorerTokenLink && (
<>
{t('swapTokenVerificationMessage', [
<a
className="build-quote__token-etherscan-link"
key="build-quote-etherscan-link"
href={blockExplorerTokenLink}
target="_blank"
rel="noopener noreferrer"
>
{blockExplorerLabel}
</a>,
])}
<InfoTooltip
position="top"
contentText={t('swapVerifyTokenExplanation', [
blockExplorerLabel,
])}
containerClassName="build-quote__token-tooltip-container"
key="token-verification-info-tooltip"
/>
</>
)}
</div>
))}
<div className="build-quote__slippage-buttons-container">
@ -465,7 +532,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,
@ -44,8 +45,7 @@ import {
QUOTES_NOT_AVAILABLE_ERROR,
SWAP_FAILED_ERROR,
OFFLINE_FOR_MAINTENANCE,
} from '../../helpers/constants/swaps';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
} from '../../../../shared/constants/swaps';
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

@ -8,6 +8,8 @@ import {
getDefaultActiveButtonIndex,
getRenderableGasButtonData,
getUSDConversionRate,
getNativeCurrency,
getSwapsDefaultToken,
} from '../../../selectors';
import {
@ -21,7 +23,6 @@ import {
shouldShowCustomPriceTooLowWarning,
swapCustomGasModalClosed,
} from '../../../ducks/swaps/swaps';
import {
addHexes,
getValueFromWeiHex,
@ -34,6 +35,9 @@ import SwapsGasCustomizationModalComponent from './swaps-gas-customization-modal
const mapStateToProps = (state) => {
const currentCurrency = getCurrentCurrency(state);
const conversionRate = getConversionRate(state);
const nativeCurrencySymbol = getNativeCurrency(state);
const { symbol: swapsDefaultCurrencySymbol } = getSwapsDefaultToken(state);
const usedCurrencySymbol = nativeCurrencySymbol || swapsDefaultCurrencySymbol;
const { modalState: { props: modalProps } = {} } = state.appState.modal || {};
const {
@ -63,6 +67,7 @@ const mapStateToProps = (state) => {
true,
conversionRate,
currentCurrency,
usedCurrencySymbol,
);
const gasButtonInfo = [averageEstimateData, fastEstimateData];
@ -74,13 +79,15 @@ const mapStateToProps = (state) => {
const balance = getCurrentEthBalance(state);
const newTotalEth = sumHexWEIsToRenderableEth([
value,
customGasTotal,
customTotalSupplement,
]);
const newTotalEth = sumHexWEIsToRenderableEth(
[value, customGasTotal, customTotalSupplement],
usedCurrencySymbol,
);
const sendAmount = sumHexWEIsToRenderableEth([value, '0x0']);
const sendAmount = sumHexWEIsToRenderableEth(
[value, '0x0'],
usedCurrencySymbol,
);
const insufficientBalance = !isBalanceSufficient({
amount: value,
@ -112,14 +119,16 @@ const mapStateToProps = (state) => {
currentCurrency,
conversionRate,
),
originalTotalEth: sumHexWEIsToRenderableEth([
value,
customGasTotal,
customTotalSupplement,
]),
originalTotalEth: sumHexWEIsToRenderableEth(
[value, customGasTotal, customTotalSupplement],
usedCurrencySymbol,
),
newTotalFiat,
newTotalEth,
transactionFee: sumHexWEIsToRenderableEth(['0x0', customGasTotal]),
transactionFee: sumHexWEIsToRenderableEth(
['0x0', customGasTotal],
usedCurrencySymbol,
),
sendAmount,
extraInfoRow,
},
@ -158,13 +167,15 @@ export default connect(
mapDispatchToProps,
)(SwapsGasCustomizationModalComponent);
function sumHexWEIsToRenderableEth(hexWEIs) {
function sumHexWEIsToRenderableEth(hexWEIs, currencySymbol = 'ETH') {
const hexWEIsSum = hexWEIs.filter(Boolean).reduce(addHexes);
return formatETHFee(
getValueFromWeiHex({
value: hexWEIsSum,
toCurrency: 'ETH',
fromCurrency: currencySymbol,
toCurrency: currencySymbol,
numberOfDecimals: 6,
}),
currencySymbol,
);
}

View File

@ -1,4 +1,4 @@
import { ETH_SWAPS_TOKEN_OBJECT } from '../../helpers/constants/swaps';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps';
export const TRADES_BASE_PROD_URL =
'https://api.metaswap.codefi.network/trades?';
@ -9,7 +9,7 @@ export const AGGREGATOR_METADATA_BASE_PROD_URL =
export const TOP_ASSET_BASE_PROD_URL =
'https://api.metaswap.codefi.network/topAssets';
export const TOKENS = [
const BASE_TOKENS = [
{
erc20: true,
symbol: 'META',
@ -82,9 +82,12 @@ export const TOKENS = [
decimals: 8,
address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
},
ETH_SWAPS_TOKEN_OBJECT,
];
export const TOKENS = [...BASE_TOKENS, ETH_SWAPS_TOKEN_OBJECT];
export const EXPECTED_TOKENS_RESULT = [ETH_SWAPS_TOKEN_OBJECT, ...BASE_TOKENS];
export const MOCK_TRADE_RESPONSE_1 = [
{
trade: {

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,
} from '../../helpers/constants/swaps';
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,25 +281,30 @@ 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 = tokens.filter((token) => {
return (
validateData(TOKEN_VALIDATORS, token, tokenUrl) &&
token.address !== ETH_SWAPS_TOKEN_OBJECT.address
);
});
filteredTokens.push(ETH_SWAPS_TOKEN_OBJECT);
const filteredTokens = [
SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId],
...tokens.filter((token) => {
return (
validateData(TOKEN_VALIDATORS, token, tokenUrl) &&
!(
isSwapsDefaultTokenSymbol(token.symbol, chainId) ||
isSwapsDefaultTokenAddress(token.address, chainId)
)
);
}),
];
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' },
@ -311,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' },
@ -327,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 },
);
@ -373,12 +387,12 @@ 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' },
{ cacheRefreshTime: 15000 },
{ cacheRefreshTime: 30000 },
);
const responseIsValid = validateData(
SWAP_GAS_PRICE_VALIDATOR,
@ -403,7 +417,7 @@ export async function fetchSwapsGasPrices() {
};
}
export function getRenderableNetworkFeesForQuote(
export function getRenderableNetworkFeesForQuote({
tradeGas,
approveGas,
gasPrice,
@ -412,14 +426,19 @@ export function getRenderableNetworkFeesForQuote(
tradeValue,
sourceSymbol,
sourceAmount,
) {
chainId,
nativeCurrencySymbol,
}) {
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)
@ -438,11 +457,15 @@ export function getRenderableNetworkFeesForQuote(
numberOfDecimals: 2,
});
const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency);
const chainCurrencySymbolToUse =
nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol;
return {
rawNetworkFees,
rawEthFee: ethFee,
feeInFiat: formattedNetworkFee,
feeInEth: `${ethFee} ETH`,
feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`,
nonGasFee,
};
}
@ -454,6 +477,7 @@ export function quotesToRenderableData(
currentCurrency,
approveGas,
tokenConversionRates,
chainId,
) {
return Object.values(quotes).map((quote) => {
const {
@ -483,16 +507,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)
@ -501,18 +526,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;
@ -561,9 +588,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,11 +1,13 @@
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,
AGGREGATOR_METADATA_BASE_PROD_URL,
TOP_ASSET_BASE_PROD_URL,
TOKENS,
EXPECTED_TOKENS_RESULT,
MOCK_TRADE_RESPONSE_2,
AGGREGATOR_METADATA,
TOP_ASSETS,
@ -88,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);
assert.deepStrictEqual(result, TOKENS);
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);
assert.deepStrictEqual(result, TOKENS);
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);
});
});
@ -147,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,9 @@ import {
getSelectedAccount,
getCurrentCurrency,
getTokenExchangeRates,
getSwapsEthToken,
getSwapsDefaultToken,
getCurrentChainId,
getNativeCurrency,
} from '../../../selectors';
import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util';
import { getTokens } from '../../../ducks/metamask/metamask';
@ -73,7 +75,7 @@ import {
getRenderableNetworkFeesForQuote,
} from '../swaps.util';
import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps';
import { QUOTES_EXPIRED_ERROR } from '../../../../../shared/constants/swaps';
import CountdownTimer from '../countdown-timer';
import SwapsFooter from '../swaps-footer';
import ViewQuotePriceDifference from './view-quote-price-difference';
@ -125,7 +127,9 @@ 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 nativeCurrencySymbol = useSelector(getNativeCurrency);
const { isBestQuote } = usedQuote;
@ -151,8 +155,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 +187,7 @@ export default function ViewQuote() {
currentCurrency,
approveGas,
memoizedTokenConversionRates,
chainId,
);
}, [
quotes,
@ -191,6 +196,7 @@ export default function ViewQuote() {
currentCurrency,
approveGas,
memoizedTokenConversionRates,
chainId,
]);
const renderableDataForUsedQuote = renderablePopoverData.find(
@ -209,31 +215,35 @@ 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,
nativeCurrencySymbol,
});
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,
nativeCurrencySymbol,
});
const tokenCost = new BigNumber(usedQuote.sourceAmount);
const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus(
@ -460,7 +470,7 @@ export default function ViewQuote() {
extraInfoRow: extraInfoRowLabel
? {
label: extraInfoRowLabel,
value: t('amountInEth', [extraNetworkFeeTotalInEth]),
value: `${extraNetworkFeeTotalInEth} ${nativeCurrencySymbol}`,
}
: null,
initialGasPrice: gasPrice,
@ -481,9 +491,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
@ -508,9 +518,11 @@ export default function ViewQuote() {
let viewQuotePriceDifferenceComponent = null;
const priceSlippageFromSource = useEthFiatAmount(
usedQuote?.priceSlippage?.sourceAmountInETH || 0,
{ showFiat: true },
);
const priceSlippageFromDestination = useEthFiatAmount(
usedQuote?.priceSlippage?.destinationAmountInETH || 0,
{ showFiat: true },
);
// We cannot present fiat value if there is a calculation error or no slippage
@ -643,7 +655,7 @@ export default function ViewQuote() {
setSelectQuotePopoverShown(true);
}}
tokenConversionRate={
destinationTokenSymbol === 'ETH'
destinationTokenSymbol === defaultSwapsToken.symbol
? 1
: memoizedTokenConversionRates[destinationToken.address]
}
@ -655,7 +667,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

@ -134,13 +134,18 @@ export function basicPriceEstimateToETHTotal(
});
}
export function getRenderableEthFee(estimate, gasLimit, numberOfDecimals = 9) {
export function getRenderableEthFee(
estimate,
gasLimit,
numberOfDecimals = 9,
nativeCurrency = 'ETH',
) {
const value = conversionUtil(estimate, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
});
const fee = basicPriceEstimateToETHTotal(value, gasLimit, numberOfDecimals);
return formatETHFee(fee);
return formatETHFee(fee, nativeCurrency);
}
export function getRenderableConvertedCurrencyFee(
@ -186,12 +191,18 @@ export function getRenderableGasButtonData(
showFiat,
conversionRate,
currentCurrency,
nativeCurrency,
) {
const { safeLow, average, fast } = estimates;
const slowEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit),
feeInPrimaryCurrency: getRenderableEthFee(
safeLow,
gasLimit,
9,
nativeCurrency,
),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
safeLow,
@ -204,7 +215,12 @@ export function getRenderableGasButtonData(
};
const averageEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInPrimaryCurrency: getRenderableEthFee(average, gasLimit),
feeInPrimaryCurrency: getRenderableEthFee(
average,
gasLimit,
9,
nativeCurrency,
),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
average,
@ -217,7 +233,12 @@ export function getRenderableGasButtonData(
};
const fastEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit),
feeInPrimaryCurrency: getRenderableEthFee(
fast,
gasLimit,
9,
nativeCurrency,
),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
fast,
@ -295,7 +316,6 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
safeLow,
gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS,
true,
),
priceInHexWei: getGasPriceInHexWei(safeLow, true),
},
@ -313,7 +333,6 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
average,
gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS,
true,
),
priceInHexWei: getGasPriceInHexWei(average, true),
},
@ -331,7 +350,6 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
fast,
gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS,
true,
),
priceInHexWei: getGasPriceInHexWei(fast, true),
},

View File

@ -5,7 +5,14 @@ import {
MAINNET_CHAIN_ID,
TEST_CHAINS,
NETWORK_TYPE_RPC,
NATIVE_CURRENCY_TOKEN_IMAGE_MAP,
} from '../../../shared/constants/network';
import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
ALLOWED_SWAPS_CHAIN_IDS,
} from '../../../shared/constants/swaps';
import {
shortenAddress,
checksumAddress,
@ -15,9 +22,11 @@ import {
getValueFromWeiHex,
hexToDecimal,
} from '../helpers/utils/conversions.util';
import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps';
import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/templates';
import { getNativeCurrency } from './send';
/**
* One of the only remaining valid uses of selecting the network subkey of the
* metamask state tree is to determine if the network is currently 'loading'.
@ -441,22 +450,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,
@ -465,3 +478,13 @@ export function getSwapsEthToken(state) {
}),
};
}
export function getIsSwapsChain(state) {
const chainId = getCurrentChainId(state);
return ALLOWED_SWAPS_CHAIN_IDS[chainId];
}
export function getNativeCurrencyImage(state) {
const nativeCurrency = getNativeCurrency(state).toUpperCase();
return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency];
}

View File

@ -2271,10 +2271,10 @@
human-standard-token-abi "^1.0.2"
safe-event-emitter "^1.0.1"
"@metamask/etherscan-link@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-1.5.0.tgz#c6940ea934b3a7dcf04e459d9ea3c630b69f6b5f"
integrity sha512-vPCkZJwZ5p933n20Zh+cC3umJv05un2CRZ8y+14KgMq3I4eOwllqmqxoYf9tn3BLGM8QXm/Nie+aBjmoe/T9ag==
"@metamask/etherscan-link@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.0.0.tgz#89035736515a39532ba1142d87b9a8c2b4f920f1"
integrity sha512-/YS32hS2UTTxs0KyUmAgaDj1w4dzAvOrT+p4TJtpICeH3E/k51r2FO0Or7WJJI/mpzTqNKgcH5yyS2oCtupGiA==
"@metamask/forwarder@^1.1.0":
version "1.1.0"
@ -17729,10 +17729,10 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
netmask@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
netmask@^1.0.6, netmask@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.1.tgz#5a5cbdcbb7b6de650870e15e83d3e9553a414cf4"
integrity sha512-gB8eG6ubxz67c7O2gaGiyWdRUIbH61q7anjgueDqCC9kvIs/b4CTtCMaQKeJbv1/Y7FT19I4zKwYmjnjInRQsg==
next-tick@1, next-tick@^1.0.0:
version "1.0.0"
@ -26205,14 +26205,14 @@ xtend@~3.0.0:
integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=
y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
version "3.2.2"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
y18n@^5.0.5:
version "5.0.5"