2023-01-24 15:10:36 +01:00
import { Web3Provider } from '@ethersproject/providers' ;
import { Contract } from '@ethersproject/contracts' ;
2021-02-04 19:15:23 +01:00
import BigNumber from 'bignumber.js' ;
import { ObservableStore } from '@metamask/obs-store' ;
import { mapValues , cloneDeep } from 'lodash' ;
import abi from 'human-standard-token-abi' ;
2023-08-29 15:15:33 +02:00
import { captureException } from '@sentry/browser' ;
2021-07-30 13:35:30 +02:00
import {
decGWEIToHexWEI ,
2023-01-20 18:04:37 +01:00
sumHexes ,
2021-07-30 13:35:30 +02:00
} from '../../../shared/modules/conversion.utils' ;
2020-10-06 20:28:38 +02:00
import {
DEFAULT _ERC20 _APPROVE _GAS ,
QUOTES _EXPIRED _ERROR ,
QUOTES _NOT _AVAILABLE _ERROR ,
2020-10-21 22:05:48 +02:00
SWAPS _FETCH _ORDER _CONFLICT ,
2021-03-30 20:55:14 +02:00
SWAPS _CHAINID _CONTRACT _ADDRESS _MAP ,
2021-03-15 14:21:35 +01:00
} from '../../../shared/constants/swaps' ;
2023-01-27 19:28:03 +01:00
import { GasEstimateTypes } from '../../../shared/constants/gas' ;
NetworkController: Split `network` into `networkId` and `networkStatus` (#17556)
The `network` store of the network controller crams two types of data
into one place. It roughly tracks whether we have enough information to
make requests to the network and whether the network is capable of
receiving requests, but it also stores the ID of the network (as
obtained via `net_version`).
Generally we shouldn't be using the network ID for anything, as it has
been completely replaced by chain ID, which all custom RPC endpoints
have been required to support for over a year now. However, as the
network ID is used in various places within the extension codebase,
removing it entirely would be a non-trivial effort. So, minimally, this
commit splits `network` into two stores: `networkId` and
`networkStatus`. But it also expands the concept of network status.
Previously, the network was in one of two states: "loading" and
"not-loading". But now it can be in one of four states:
- `available`: The network is able to receive and respond to requests.
- `unavailable`: The network is not able to receive and respond to
requests for unknown reasons.
- `blocked`: The network is actively blocking requests based on the
user's geolocation. (This is specific to Infura.)
- `unknown`: We don't know whether the network can receive and respond
to requests, either because we haven't checked or we tried to check
and were unsuccessful.
This commit also changes how the network status is determined —
specifically, how many requests are used to determine that status, when
they occur, and whether they are awaited. Previously, the network
controller would make 2 to 3 requests during the course of running
`lookupNetwork`.
* First, if it was an Infura network, it would make a request for
`eth_blockNumber` to determine whether Infura was blocking requests or
not, then emit an appropriate event. This operation was not awaited.
* Then, regardless of the network, it would fetch the network ID via
`net_version`. This operation was awaited.
* Finally, regardless of the network, it would fetch the latest block
via `eth_getBlockByNumber`, then use the result to determine whether
the network supported EIP-1559. This operation was awaited.
Now:
* One fewer request is made, specifically `eth_blockNumber`, as we don't
need to make an extra request to determine whether Infura is blocking
requests; we can reuse `eth_getBlockByNumber`;
* All requests are awaited, which makes `lookupNetwork` run fully
in-band instead of partially out-of-band; and
* Both requests for `net_version` and `eth_getBlockByNumber` are
performed in parallel to make `lookupNetwork` run slightly faster.
2023-03-31 00:49:12 +02:00
import { CHAIN _IDS , NetworkStatus } from '../../../shared/constants/network' ;
2023-08-29 15:15:33 +02:00
import {
MetaMetricsEventCategory ,
MetaMetricsEventName ,
MetaMetricsEventErrorType ,
} from '../../../shared/constants/metametrics' ;
2022-05-09 18:48:14 +02:00
import {
FALLBACK _SMART _TRANSACTIONS _REFRESH _TIME ,
FALLBACK _SMART _TRANSACTIONS _DEADLINE ,
FALLBACK _SMART _TRANSACTIONS _MAX _FEE _MULTIPLIER ,
} from '../../../shared/constants/smartTransactions' ;
2021-06-10 21:27:03 +02:00
2021-03-18 11:20:06 +01:00
import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils' ;
2020-10-06 20:28:38 +02:00
import {
fetchTradesInfo as defaultFetchTradesInfo ,
2021-09-15 15:13:18 +02:00
getBaseApi ,
2022-09-16 21:05:21 +02:00
} from '../../../shared/lib/swaps-utils' ;
import fetchWithCache from '../../../shared/lib/fetch-with-cache' ;
2021-06-10 21:27:03 +02:00
import { MINUTE , SECOND } from '../../../shared/constants/time' ;
2022-03-07 19:54:36 +01:00
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils' ;
2022-09-16 21:05:21 +02:00
import {
calcGasTotal ,
calcTokenAmount ,
} from '../../../shared/lib/transactions-controller-utils' ;
2022-12-21 20:59:34 +01:00
import fetchEstimatedL1Fee from '../../../ui/helpers/utils/optimism/fetchEstimatedL1Fee' ;
2022-09-16 21:05:21 +02:00
2023-01-23 18:41:01 +01:00
import { Numeric } from '../../../shared/modules/Numeric' ;
import { EtherDenomination } from '../../../shared/constants/common' ;
2020-10-06 20:28:38 +02:00
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
2021-02-04 19:15:23 +01:00
const MAX _GAS _LIMIT = 2500000 ;
2020-10-06 20:28:38 +02:00
// To ensure that our serves are not spammed if MetaMask is left idle, we limit the number of fetches for quotes that are made on timed intervals.
// 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is.
2021-02-04 19:15:23 +01:00
const POLL _COUNT _LIMIT = 3 ;
2020-10-06 20:28:38 +02:00
2020-12-15 21:24:22 +01:00
// If for any reason the MetaSwap API fails to provide a refresh time,
// provide a reasonable fallback to avoid further errors
2021-06-10 21:27:03 +02:00
const FALLBACK _QUOTE _REFRESH _TIME = MINUTE ;
2020-12-15 21:24:22 +01:00
2020-11-03 00:41:28 +01:00
function calculateGasEstimateWithRefund (
maxGas = MAX _GAS _LIMIT ,
estimatedRefund = 0 ,
estimatedGas = 0 ,
) {
2021-02-04 19:15:23 +01:00
const maxGasMinusRefund = new BigNumber ( maxGas , 10 ) . minus (
estimatedRefund ,
10 ,
) ;
2022-02-07 11:58:31 +01:00
const isMaxGasMinusRefundNegative = maxGasMinusRefund . lt ( 0 ) ;
2020-11-03 00:41:28 +01:00
2022-02-07 11:58:31 +01:00
const gasEstimateWithRefund =
! isMaxGasMinusRefundNegative && maxGasMinusRefund . lt ( estimatedGas , 16 )
2022-02-08 19:14:50 +01:00
? ` 0x ${ maxGasMinusRefund . toString ( 16 ) } `
2022-02-07 11:58:31 +01:00
: estimatedGas ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
return gasEstimateWithRefund ;
2020-10-06 20:28:38 +02:00
}
const initialState = {
swapsState : {
quotes : { } ,
2021-09-15 15:13:18 +02:00
quotesPollingLimitEnabled : false ,
2020-10-06 20:28:38 +02:00
fetchParams : null ,
tokens : null ,
tradeTxId : null ,
approveTxId : null ,
quotesLastFetched : null ,
customMaxGas : '' ,
customGasPrice : null ,
2021-07-30 13:35:30 +02:00
customMaxFeePerGas : null ,
customMaxPriorityFeePerGas : null ,
2021-08-06 21:31:30 +02:00
swapsUserFeeLevel : '' ,
2020-10-06 20:28:38 +02:00
selectedAggId : null ,
customApproveTxData : '' ,
errorKey : '' ,
topAggId : null ,
routeState : '' ,
2021-04-14 09:16:27 +02:00
swapsFeatureIsLive : true ,
2021-10-28 08:53:26 +02:00
saveFetchedQuotes : false ,
2020-12-15 21:24:22 +01:00
swapsQuoteRefreshTime : FALLBACK _QUOTE _REFRESH _TIME ,
2021-09-15 15:13:18 +02:00
swapsQuotePrefetchingRefreshTime : FALLBACK _QUOTE _REFRESH _TIME ,
2022-05-09 18:48:14 +02:00
swapsStxBatchStatusRefreshTime : FALLBACK _SMART _TRANSACTIONS _REFRESH _TIME ,
2022-07-31 20:26:40 +02:00
swapsStxGetTransactionsRefreshTime :
FALLBACK _SMART _TRANSACTIONS _REFRESH _TIME ,
2022-05-09 18:48:14 +02:00
swapsStxMaxFeeMultiplier : FALLBACK _SMART _TRANSACTIONS _MAX _FEE _MULTIPLIER ,
2022-02-18 17:48:38 +01:00
swapsFeatureFlags : { } ,
2020-10-06 20:28:38 +02:00
} ,
2021-02-04 19:15:23 +01:00
} ;
2020-10-06 20:28:38 +02:00
export default class SwapsController {
2023-06-15 20:17:21 +02:00
constructor (
{
getBufferedGasLimit ,
networkController ,
provider ,
getProviderConfig ,
getTokenRatesState ,
fetchTradesInfo = defaultFetchTradesInfo ,
getCurrentChainId ,
getEIP1559GasFeeEstimates ,
onNetworkStateChange ,
2023-08-29 15:15:33 +02:00
trackMetaMetricsEvent ,
2023-06-15 20:17:21 +02:00
} ,
state ,
) {
2020-10-06 20:28:38 +02:00
this . store = new ObservableStore ( {
2023-06-15 20:17:21 +02:00
swapsState : {
... initialState . swapsState ,
swapsFeatureFlags : state ? . swapsState ? . swapsFeatureFlags || { } ,
} ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
2022-11-22 17:56:26 +01:00
this . resetState = ( ) => {
2023-06-15 20:17:21 +02:00
this . store . updateState ( {
swapsState : {
... initialState . swapsState ,
swapsFeatureFlags : state ? . swapsState ? . swapsFeatureFlags ,
} ,
} ) ;
2022-11-22 17:56:26 +01:00
} ;
2021-02-04 19:15:23 +01:00
this . _fetchTradesInfo = fetchTradesInfo ;
2021-03-18 11:20:06 +01:00
this . _getCurrentChainId = getCurrentChainId ;
2021-07-30 13:35:30 +02:00
this . _getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
this . getBufferedGasLimit = getBufferedGasLimit ;
2021-11-22 13:04:31 +01:00
this . getTokenRatesState = getTokenRatesState ;
2023-08-29 15:15:33 +02:00
this . trackMetaMetricsEvent = trackMetaMetricsEvent ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
this . pollCount = 0 ;
this . getProviderConfig = getProviderConfig ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
this . indexOfNewestCallInFlight = 0 ;
2020-10-21 22:05:48 +02:00
2023-01-24 15:10:36 +01:00
this . ethersProvider = new Web3Provider ( provider ) ;
2023-06-22 20:46:09 +02:00
this . _currentNetworkId = networkController . state . networkId ;
2023-04-19 22:45:26 +02:00
onNetworkStateChange ( ( ) => {
2023-08-03 19:31:35 +02:00
const { networkId , networksMetadata , selectedNetworkClientId } =
networkController . state ;
const selectedNetworkStatus =
networksMetadata [ selectedNetworkClientId ] ? . status ;
NetworkController: Split `network` into `networkId` and `networkStatus` (#17556)
The `network` store of the network controller crams two types of data
into one place. It roughly tracks whether we have enough information to
make requests to the network and whether the network is capable of
receiving requests, but it also stores the ID of the network (as
obtained via `net_version`).
Generally we shouldn't be using the network ID for anything, as it has
been completely replaced by chain ID, which all custom RPC endpoints
have been required to support for over a year now. However, as the
network ID is used in various places within the extension codebase,
removing it entirely would be a non-trivial effort. So, minimally, this
commit splits `network` into two stores: `networkId` and
`networkStatus`. But it also expands the concept of network status.
Previously, the network was in one of two states: "loading" and
"not-loading". But now it can be in one of four states:
- `available`: The network is able to receive and respond to requests.
- `unavailable`: The network is not able to receive and respond to
requests for unknown reasons.
- `blocked`: The network is actively blocking requests based on the
user's geolocation. (This is specific to Infura.)
- `unknown`: We don't know whether the network can receive and respond
to requests, either because we haven't checked or we tried to check
and were unsuccessful.
This commit also changes how the network status is determined —
specifically, how many requests are used to determine that status, when
they occur, and whether they are awaited. Previously, the network
controller would make 2 to 3 requests during the course of running
`lookupNetwork`.
* First, if it was an Infura network, it would make a request for
`eth_blockNumber` to determine whether Infura was blocking requests or
not, then emit an appropriate event. This operation was not awaited.
* Then, regardless of the network, it would fetch the network ID via
`net_version`. This operation was awaited.
* Finally, regardless of the network, it would fetch the latest block
via `eth_getBlockByNumber`, then use the result to determine whether
the network supported EIP-1559. This operation was awaited.
Now:
* One fewer request is made, specifically `eth_blockNumber`, as we don't
need to make an extra request to determine whether Infura is blocking
requests; we can reuse `eth_getBlockByNumber`;
* All requests are awaited, which makes `lookupNetwork` run fully
in-band instead of partially out-of-band; and
* Both requests for `net_version` and `eth_getBlockByNumber` are
performed in parallel to make `lookupNetwork` run slightly faster.
2023-03-31 00:49:12 +02:00
if (
2023-08-03 19:31:35 +02:00
selectedNetworkStatus === NetworkStatus . Available &&
NetworkController: Split `network` into `networkId` and `networkStatus` (#17556)
The `network` store of the network controller crams two types of data
into one place. It roughly tracks whether we have enough information to
make requests to the network and whether the network is capable of
receiving requests, but it also stores the ID of the network (as
obtained via `net_version`).
Generally we shouldn't be using the network ID for anything, as it has
been completely replaced by chain ID, which all custom RPC endpoints
have been required to support for over a year now. However, as the
network ID is used in various places within the extension codebase,
removing it entirely would be a non-trivial effort. So, minimally, this
commit splits `network` into two stores: `networkId` and
`networkStatus`. But it also expands the concept of network status.
Previously, the network was in one of two states: "loading" and
"not-loading". But now it can be in one of four states:
- `available`: The network is able to receive and respond to requests.
- `unavailable`: The network is not able to receive and respond to
requests for unknown reasons.
- `blocked`: The network is actively blocking requests based on the
user's geolocation. (This is specific to Infura.)
- `unknown`: We don't know whether the network can receive and respond
to requests, either because we haven't checked or we tried to check
and were unsuccessful.
This commit also changes how the network status is determined —
specifically, how many requests are used to determine that status, when
they occur, and whether they are awaited. Previously, the network
controller would make 2 to 3 requests during the course of running
`lookupNetwork`.
* First, if it was an Infura network, it would make a request for
`eth_blockNumber` to determine whether Infura was blocking requests or
not, then emit an appropriate event. This operation was not awaited.
* Then, regardless of the network, it would fetch the network ID via
`net_version`. This operation was awaited.
* Finally, regardless of the network, it would fetch the latest block
via `eth_getBlockByNumber`, then use the result to determine whether
the network supported EIP-1559. This operation was awaited.
Now:
* One fewer request is made, specifically `eth_blockNumber`, as we don't
need to make an extra request to determine whether Infura is blocking
requests; we can reuse `eth_getBlockByNumber`;
* All requests are awaited, which makes `lookupNetwork` run fully
in-band instead of partially out-of-band; and
* Both requests for `net_version` and `eth_getBlockByNumber` are
performed in parallel to make `lookupNetwork` run slightly faster.
2023-03-31 00:49:12 +02:00
networkId !== this . _currentNetworkId
) {
this . _currentNetworkId = networkId ;
2023-01-24 15:10:36 +01:00
this . ethersProvider = new Web3Provider ( provider ) ;
2020-10-28 19:47:32 +01:00
}
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
}
2022-05-09 18:48:14 +02:00
async fetchSwapsNetworkConfig ( chainId ) {
2021-09-15 15:13:18 +02:00
const response = await fetchWithCache (
2021-11-30 19:56:28 +01:00
getBaseApi ( 'network' , chainId ) ,
2021-09-15 15:13:18 +02:00
{ method : 'GET' } ,
{ cacheRefreshTime : 600000 } ,
) ;
2022-05-09 18:48:14 +02:00
const { refreshRates , parameters = { } } = response || { } ;
2021-09-15 15:13:18 +02:00
if (
! refreshRates ||
typeof refreshRates . quotes !== 'number' ||
2022-04-04 15:14:50 +02:00
typeof refreshRates . quotesPrefetching !== 'number'
2021-09-15 15:13:18 +02:00
) {
throw new Error (
` MetaMask - invalid response for refreshRates: ${ response } ` ,
) ;
}
// We presently use milliseconds in the UI.
return {
quotes : refreshRates . quotes * 1000 ,
quotesPrefetching : refreshRates . quotesPrefetching * 1000 ,
2022-02-18 17:48:38 +01:00
stxGetTransactions : refreshRates . stxGetTransactions * 1000 ,
stxBatchStatus : refreshRates . stxBatchStatus * 1000 ,
stxStatusDeadline : refreshRates . stxStatusDeadline ,
2022-05-09 18:48:14 +02:00
stxMaxFeeMultiplier : parameters . stxMaxFeeMultiplier ,
2021-09-15 15:13:18 +02:00
} ;
}
2022-05-09 18:48:14 +02:00
// Sets the network config from the MetaSwap API.
async _setSwapsNetworkConfig ( ) {
2021-03-18 11:20:06 +01:00
const chainId = this . _getCurrentChainId ( ) ;
2022-05-09 18:48:14 +02:00
let swapsNetworkConfig ;
2020-12-15 21:24:22 +01:00
try {
2022-05-09 18:48:14 +02:00
swapsNetworkConfig = await this . fetchSwapsNetworkConfig ( chainId ) ;
2020-12-15 21:24:22 +01:00
} catch ( e ) {
2022-05-09 18:48:14 +02:00
console . error ( 'Request for Swaps network config failed: ' , e ) ;
2020-12-15 21:24:22 +01:00
}
2021-07-15 20:13:53 +02:00
const { swapsState : latestSwapsState } = this . store . getState ( ) ;
2020-12-15 21:24:22 +01:00
this . store . updateState ( {
2021-09-15 15:13:18 +02:00
swapsState : {
... latestSwapsState ,
swapsQuoteRefreshTime :
2022-05-09 18:48:14 +02:00
swapsNetworkConfig ? . quotes || FALLBACK _QUOTE _REFRESH _TIME ,
2021-09-15 15:13:18 +02:00
swapsQuotePrefetchingRefreshTime :
2022-05-09 18:48:14 +02:00
swapsNetworkConfig ? . quotesPrefetching || FALLBACK _QUOTE _REFRESH _TIME ,
2022-02-18 17:48:38 +01:00
swapsStxGetTransactionsRefreshTime :
2022-05-09 18:48:14 +02:00
swapsNetworkConfig ? . stxGetTransactions ||
FALLBACK _SMART _TRANSACTIONS _REFRESH _TIME ,
2022-02-18 17:48:38 +01:00
swapsStxBatchStatusRefreshTime :
2022-05-09 18:48:14 +02:00
swapsNetworkConfig ? . stxBatchStatus ||
FALLBACK _SMART _TRANSACTIONS _REFRESH _TIME ,
2022-02-18 17:48:38 +01:00
swapsStxStatusDeadline :
2022-05-09 18:48:14 +02:00
swapsNetworkConfig ? . stxStatusDeadline ||
2022-02-18 17:48:38 +01:00
FALLBACK _SMART _TRANSACTIONS _DEADLINE ,
2022-05-09 18:48:14 +02:00
swapsStxMaxFeeMultiplier :
swapsNetworkConfig ? . stxMaxFeeMultiplier ||
FALLBACK _SMART _TRANSACTIONS _MAX _FEE _MULTIPLIER ,
2021-09-15 15:13:18 +02:00
} ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-12-15 21:24:22 +01:00
}
2020-10-06 20:28:38 +02:00
// Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough
2021-09-15 15:13:18 +02:00
// that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called, it receives fetch parameters that are stored in
2020-10-06 20:28:38 +02:00
// state. These stored parameters are used on subsequent calls made during polling.
// Note: we stop polling after 3 requests, until new quotes are explicitly asked for. The logic that enforces that maximum is in the body of fetchAndSetQuotes
2020-11-03 00:41:28 +01:00
pollForNewQuotes ( ) {
2020-12-15 21:24:22 +01:00
const {
2021-09-15 15:13:18 +02:00
swapsState : {
swapsQuoteRefreshTime ,
swapsQuotePrefetchingRefreshTime ,
quotesPollingLimitEnabled ,
} ,
2021-02-04 19:15:23 +01:00
} = this . store . getState ( ) ;
2021-09-15 15:13:18 +02:00
// swapsQuoteRefreshTime is used on the View Quote page, swapsQuotePrefetchingRefreshTime is used on the Build Quote page.
const quotesRefreshRateInMs = quotesPollingLimitEnabled
? swapsQuoteRefreshTime
: swapsQuotePrefetchingRefreshTime ;
2020-10-06 20:28:38 +02:00
this . pollingTimeout = setTimeout ( ( ) => {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-11-03 00:41:28 +01:00
this . fetchAndSetQuotes (
swapsState . fetchParams ,
swapsState . fetchParams ? . metaData ,
true ,
2021-02-04 19:15:23 +01:00
) ;
2021-09-15 15:13:18 +02:00
} , quotesRefreshRateInMs ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
stopPollingForQuotes ( ) {
2021-09-15 15:13:18 +02:00
if ( this . pollingTimeout ) {
clearTimeout ( this . pollingTimeout ) ;
}
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
async fetchAndSetQuotes (
fetchParams ,
fetchParamsMetaData = { } ,
isPolledRequest ,
) {
2021-03-18 11:20:06 +01:00
const { chainId } = fetchParamsMetaData ;
2021-07-09 17:24:00 +02:00
const {
2021-11-30 19:56:28 +01:00
swapsState : { quotesPollingLimitEnabled , saveFetchedQuotes } ,
2021-07-09 17:24:00 +02:00
} = this . store . getState ( ) ;
2021-03-18 11:20:06 +01:00
2020-10-06 20:28:38 +02:00
if ( ! fetchParams ) {
2021-02-04 19:15:23 +01:00
return null ;
2020-10-06 20:28:38 +02:00
}
// Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params.
if ( ! isPolledRequest ) {
2021-02-04 19:15:23 +01:00
this . pollCount = 0 ;
2020-10-06 20:28:38 +02:00
}
// If there are any pending poll requests, clear them so that they don't get call while this new fetch is in process
2021-02-04 19:15:23 +01:00
clearTimeout ( this . pollingTimeout ) ;
2020-10-06 20:28:38 +02:00
if ( ! isPolledRequest ) {
2021-02-04 19:15:23 +01:00
this . setSwapsErrorKey ( '' ) ;
2020-10-06 20:28:38 +02:00
}
2020-10-21 22:05:48 +02:00
2021-02-04 19:15:23 +01:00
const indexOfCurrentCall = this . indexOfNewestCallInFlight + 1 ;
this . indexOfNewestCallInFlight = indexOfCurrentCall ;
2020-10-21 22:05:48 +02:00
2021-10-28 08:53:26 +02:00
if ( ! saveFetchedQuotes ) {
this . setSaveFetchedQuotes ( true ) ;
}
2021-09-29 18:13:34 +02:00
2020-12-15 21:24:22 +01:00
let [ newQuotes ] = await Promise . all ( [
2021-07-09 17:24:00 +02:00
this . _fetchTradesInfo ( fetchParams , {
... fetchParamsMetaData ,
} ) ,
2022-05-09 18:48:14 +02:00
this . _setSwapsNetworkConfig ( ) ,
2021-02-04 19:15:23 +01:00
] ) ;
2020-10-06 20:28:38 +02:00
2021-09-29 18:13:34 +02:00
const {
2021-10-28 08:53:26 +02:00
swapsState : { saveFetchedQuotes : saveFetchedQuotesAfterResponse } ,
2021-09-29 18:13:34 +02:00
} = this . store . getState ( ) ;
2021-10-28 08:53:26 +02:00
// If saveFetchedQuotesAfterResponse is false, it means a user left Swaps (we cleaned the state)
2021-09-29 18:13:34 +02:00
// and we don't want to set any API response with quotes into state.
2021-10-28 08:53:26 +02:00
if ( ! saveFetchedQuotesAfterResponse ) {
2021-09-29 18:13:34 +02:00
return [
{ } , // quotes
null , // selectedAggId
] ;
}
2020-10-06 20:28:38 +02:00
newQuotes = mapValues ( newQuotes , ( quote ) => ( {
... quote ,
sourceTokenInfo : fetchParamsMetaData . sourceTokenInfo ,
destinationTokenInfo : fetchParamsMetaData . destinationTokenInfo ,
2021-02-04 19:15:23 +01:00
} ) ) ;
2020-10-06 20:28:38 +02:00
2022-12-21 20:59:34 +01:00
if ( chainId === CHAIN _IDS . OPTIMISM && Object . values ( newQuotes ) . length > 0 ) {
await Promise . all (
Object . values ( newQuotes ) . map ( async ( quote ) => {
if ( quote . trade ) {
const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee (
2023-04-18 16:45:18 +02:00
chainId ,
2022-12-21 20:59:34 +01:00
{
txParams : quote . trade ,
chainId ,
} ,
this . ethersProvider ,
) ;
quote . multiLayerL1TradeFeeTotal = multiLayerL1TradeFeeTotal ;
}
return quote ;
} ) ,
) ;
}
2021-02-04 19:15:23 +01:00
const quotesLastFetched = Date . now ( ) ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
let approvalRequired = false ;
2020-11-03 00:41:28 +01:00
if (
2021-03-18 11:20:06 +01:00
! isSwapsDefaultTokenAddress ( fetchParams . sourceToken , chainId ) &&
2020-11-03 00:41:28 +01:00
Object . values ( newQuotes ) . length
) {
2020-10-06 20:28:38 +02:00
const allowance = await this . _getERC20Allowance (
fetchParams . sourceToken ,
fetchParams . fromAddress ,
2021-03-30 20:55:14 +02:00
chainId ,
2021-02-04 19:15:23 +01:00
) ;
2022-02-23 22:10:28 +01:00
const [ firstQuote ] = Object . values ( newQuotes ) ;
2020-10-06 20:28:38 +02:00
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
2021-11-12 15:40:50 +01:00
// than 0, it means that approval has already occurred and is not needed. Otherwise, for tokens to be swapped, a new
2020-10-06 20:28:38 +02:00
// call of the ERC-20 approve method is required.
2021-10-20 13:07:34 +02:00
approvalRequired =
2022-02-23 22:10:28 +01:00
firstQuote . approvalNeeded &&
2021-10-20 13:07:34 +02:00
allowance . eq ( 0 ) &&
2022-02-23 22:10:28 +01:00
firstQuote . aggregator !== 'wrappedNative' ;
2020-10-06 20:28:38 +02:00
if ( ! approvalRequired ) {
newQuotes = mapValues ( newQuotes , ( quote ) => ( {
... quote ,
approvalNeeded : null ,
2021-02-04 19:15:23 +01:00
} ) ) ;
2020-10-06 20:28:38 +02:00
} else if ( ! isPolledRequest ) {
2020-11-03 00:41:28 +01:00
const { gasLimit : approvalGas } = await this . timedoutGasReturn (
2022-02-23 22:10:28 +01:00
firstQuote . approvalNeeded ,
2023-08-29 15:15:33 +02:00
firstQuote . aggregator ,
2021-02-04 19:15:23 +01:00
) ;
2020-10-06 20:28:38 +02:00
newQuotes = mapValues ( newQuotes , ( quote ) => ( {
... quote ,
approvalNeeded : {
... quote . approvalNeeded ,
gas : approvalGas || DEFAULT _ERC20 _APPROVE _GAS ,
} ,
2021-02-04 19:15:23 +01:00
} ) ) ;
2020-10-06 20:28:38 +02:00
}
}
2021-02-04 19:15:23 +01:00
let topAggId = null ;
2020-10-06 20:28:38 +02:00
// We can reduce time on the loading screen by only doing this after the
// loading screen and best quote have rendered.
if ( ! approvalRequired && ! fetchParams ? . balanceError ) {
2021-02-04 19:15:23 +01:00
newQuotes = await this . getAllQuotesWithGasEstimates ( newQuotes ) ;
2020-10-09 20:00:20 +02:00
}
2020-10-06 20:28:38 +02:00
2020-10-09 20:00:20 +02:00
if ( Object . values ( newQuotes ) . length === 0 ) {
2021-02-04 19:15:23 +01:00
this . setSwapsErrorKey ( QUOTES _NOT _AVAILABLE _ERROR ) ;
2020-10-09 19:14:10 +02:00
} else {
2022-07-31 20:26:40 +02:00
const [ _topAggId , quotesWithSavingsAndFeeData ] =
await this . _findTopQuoteAndCalculateSavings ( newQuotes ) ;
2021-02-04 19:15:23 +01:00
topAggId = _topAggId ;
newQuotes = quotesWithSavingsAndFeeData ;
2020-10-06 20:28:38 +02:00
}
2020-10-21 22:05:48 +02:00
// If a newer call has been made, don't update state with old information
// Prevents timing conflicts between fetches
if ( this . indexOfNewestCallInFlight !== indexOfCurrentCall ) {
2021-02-04 19:15:23 +01:00
throw new Error ( SWAPS _FETCH _ORDER _CONFLICT ) ;
2020-10-21 22:05:48 +02:00
}
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
let { selectedAggId } = swapsState ;
2020-10-06 20:28:38 +02:00
if ( ! newQuotes [ selectedAggId ] ) {
2021-02-04 19:15:23 +01:00
selectedAggId = null ;
2020-10-06 20:28:38 +02:00
}
2020-10-21 22:05:48 +02:00
2020-10-06 20:28:38 +02:00
this . store . updateState ( {
swapsState : {
... swapsState ,
quotes : newQuotes ,
fetchParams : { ... fetchParams , metaData : fetchParamsMetaData } ,
quotesLastFetched ,
selectedAggId ,
topAggId ,
} ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
2021-09-15 15:13:18 +02:00
if ( quotesPollingLimitEnabled ) {
// We only want to do up to a maximum of three requests from polling if polling limit is enabled.
// Otherwise we won't increase pollCount, so polling will run without a limit.
this . pollCount += 1 ;
}
if ( ! quotesPollingLimitEnabled || this . pollCount < POLL _COUNT _LIMIT + 1 ) {
2021-02-04 19:15:23 +01:00
this . pollForNewQuotes ( ) ;
2020-10-06 20:28:38 +02:00
} else {
2021-02-04 19:15:23 +01:00
this . resetPostFetchState ( ) ;
this . setSwapsErrorKey ( QUOTES _EXPIRED _ERROR ) ;
return null ;
2020-10-06 20:28:38 +02:00
}
2021-02-04 19:15:23 +01:00
return [ newQuotes , topAggId ] ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
safeRefetchQuotes ( ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
if ( ! this . pollingTimeout && swapsState . fetchParams ) {
2021-02-04 19:15:23 +01:00
this . fetchAndSetQuotes ( swapsState . fetchParams ) ;
2020-10-06 20:28:38 +02:00
}
}
2020-11-03 00:41:28 +01:00
setSelectedQuoteAggId ( selectedAggId ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , selectedAggId } } ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setSwapsTokens ( tokens ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , tokens } } ) ;
2020-10-06 20:28:38 +02:00
}
2021-09-15 15:13:18 +02:00
clearSwapsQuotes ( ) {
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , quotes : { } } } ) ;
}
2020-11-03 00:41:28 +01:00
setSwapsErrorKey ( errorKey ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , errorKey } } ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
async getAllQuotesWithGasEstimates ( quotes ) {
2020-10-06 20:28:38 +02:00
const quoteGasData = await Promise . all (
Object . values ( quotes ) . map ( async ( quote ) => {
2020-11-03 00:41:28 +01:00
const { gasLimit , simulationFails } = await this . timedoutGasReturn (
quote . trade ,
2023-08-29 15:15:33 +02:00
quote . aggregator ,
2021-02-04 19:15:23 +01:00
) ;
return [ gasLimit , simulationFails , quote . aggregator ] ;
2020-10-06 20:28:38 +02:00
} ) ,
2021-02-04 19:15:23 +01:00
) ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
const newQuotes = { } ;
2020-10-06 20:28:38 +02:00
quoteGasData . forEach ( ( [ gasLimit , simulationFails , aggId ] ) => {
if ( gasLimit && ! simulationFails ) {
2020-11-03 00:41:28 +01:00
const gasEstimateWithRefund = calculateGasEstimateWithRefund (
quotes [ aggId ] . maxGas ,
quotes [ aggId ] . estimatedRefund ,
gasLimit ,
2021-02-04 19:15:23 +01:00
) ;
2020-10-06 20:28:38 +02:00
newQuotes [ aggId ] = {
... quotes [ aggId ] ,
gasEstimate : gasLimit ,
gasEstimateWithRefund ,
2021-02-04 19:15:23 +01:00
} ;
2020-10-06 20:28:38 +02:00
} else if ( quotes [ aggId ] . approvalNeeded ) {
// If gas estimation fails, but an ERC-20 approve is needed, then we do not add any estimate property to the quote object
// Such quotes will rely on the maxGas and averageGas properties from the api
2021-02-04 19:15:23 +01:00
newQuotes [ aggId ] = quotes [ aggId ] ;
2020-10-06 20:28:38 +02:00
}
// If gas estimation fails and no approval is needed, then we filter that quote out, so that it is not shown to the user
2021-02-04 19:15:23 +01:00
} ) ;
return newQuotes ;
2020-10-06 20:28:38 +02:00
}
2023-08-29 15:15:33 +02:00
timedoutGasReturn ( tradeTxParams , aggregator = '' ) {
2020-10-06 20:28:38 +02:00
return new Promise ( ( resolve ) => {
2021-02-04 19:15:23 +01:00
let gasTimedOut = false ;
2020-10-06 20:28:38 +02:00
const gasTimeout = setTimeout ( ( ) => {
2021-02-04 19:15:23 +01:00
gasTimedOut = true ;
2023-08-29 15:15:33 +02:00
this . trackMetaMetricsEvent ( {
event : MetaMetricsEventName . QuoteError ,
category : MetaMetricsEventCategory . Swaps ,
properties : {
error _type : MetaMetricsEventErrorType . GasTimeout ,
aggregator ,
} ,
} ) ;
resolve ( {
gasLimit : null ,
simulationFails : true ,
} ) ;
2021-06-10 21:27:03 +02:00
} , SECOND * 5 ) ;
2020-10-06 20:28:38 +02:00
2020-10-07 21:00:17 +02:00
// Remove gas from params that will be passed to the `estimateGas` call
// Including it can cause the estimate to fail if the actual gas needed
// exceeds the passed gas
const tradeTxParamsForGasEstimate = {
data : tradeTxParams . data ,
from : tradeTxParams . from ,
to : tradeTxParams . to ,
value : tradeTxParams . value ,
2021-02-04 19:15:23 +01:00
} ;
2020-10-07 21:00:17 +02:00
this . getBufferedGasLimit ( { txParams : tradeTxParamsForGasEstimate } , 1 )
2020-10-06 20:28:38 +02:00
. then ( ( { gasLimit , simulationFails } ) => {
if ( ! gasTimedOut ) {
2021-02-04 19:15:23 +01:00
clearTimeout ( gasTimeout ) ;
resolve ( { gasLimit , simulationFails } ) ;
2020-10-06 20:28:38 +02:00
}
} )
. catch ( ( e ) => {
2023-08-29 15:15:33 +02:00
captureException ( e , {
extra : {
aggregator ,
} ,
} ) ;
2020-10-06 20:28:38 +02:00
if ( ! gasTimedOut ) {
2021-02-04 19:15:23 +01:00
clearTimeout ( gasTimeout ) ;
resolve ( { gasLimit : null , simulationFails : true } ) ;
2020-10-06 20:28:38 +02:00
}
2021-02-04 19:15:23 +01:00
} ) ;
} ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
async setInitialGasEstimate ( initialAggId ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
const quoteToUpdate = { ... swapsState . quotes [ initialAggId ] } ;
2020-10-06 20:28:38 +02:00
2022-07-31 20:26:40 +02:00
const { gasLimit : newGasEstimate , simulationFails } =
2023-08-29 15:15:33 +02:00
await this . timedoutGasReturn (
quoteToUpdate . trade ,
quoteToUpdate . aggregator ,
) ;
2020-10-06 20:28:38 +02:00
if ( newGasEstimate && ! simulationFails ) {
2020-11-03 00:41:28 +01:00
const gasEstimateWithRefund = calculateGasEstimateWithRefund (
quoteToUpdate . maxGas ,
quoteToUpdate . estimatedRefund ,
newGasEstimate ,
2021-02-04 19:15:23 +01:00
) ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
quoteToUpdate . gasEstimate = newGasEstimate ;
quoteToUpdate . gasEstimateWithRefund = gasEstimateWithRefund ;
2020-10-06 20:28:38 +02:00
}
this . store . updateState ( {
2020-11-03 00:41:28 +01:00
swapsState : {
... swapsState ,
quotes : { ... swapsState . quotes , [ initialAggId ] : quoteToUpdate } ,
} ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setApproveTxId ( approveTxId ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , approveTxId } } ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setTradeTxId ( tradeTxId ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , tradeTxId } } ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setQuotesLastFetched ( quotesLastFetched ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , quotesLastFetched } ,
} ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setSwapsTxGasPrice ( gasPrice ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
this . store . updateState ( {
swapsState : { ... swapsState , customGasPrice : gasPrice } ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
}
2021-07-30 13:35:30 +02:00
setSwapsTxMaxFeePerGas ( maxFeePerGas ) {
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , customMaxFeePerGas : maxFeePerGas } ,
} ) ;
}
2021-08-06 21:31:30 +02:00
setSwapsUserFeeLevel ( swapsUserFeeLevel ) {
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , swapsUserFeeLevel } ,
} ) ;
}
2021-09-15 15:13:18 +02:00
setSwapsQuotesPollingLimitEnabled ( quotesPollingLimitEnabled ) {
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , quotesPollingLimitEnabled } ,
} ) ;
}
2021-07-30 13:35:30 +02:00
setSwapsTxMaxFeePriorityPerGas ( maxPriorityFeePerGas ) {
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : {
... swapsState ,
customMaxPriorityFeePerGas : maxPriorityFeePerGas ,
} ,
} ) ;
}
2020-11-03 00:41:28 +01:00
setSwapsTxGasLimit ( gasLimit ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
this . store . updateState ( {
swapsState : { ... swapsState , customMaxGas : gasLimit } ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setCustomApproveTxData ( data ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
this . store . updateState ( {
swapsState : { ... swapsState , customApproveTxData : data } ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
setBackgroundSwapRouteState ( routeState ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , routeState } } ) ;
2020-10-06 20:28:38 +02:00
}
2021-10-28 08:53:26 +02:00
setSaveFetchedQuotes ( status ) {
2021-09-29 18:13:34 +02:00
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
2021-10-28 08:53:26 +02:00
swapsState : { ... swapsState , saveFetchedQuotes : status } ,
2021-09-29 18:13:34 +02:00
} ) ;
}
2021-07-09 17:24:00 +02:00
setSwapsLiveness ( swapsLiveness ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2021-11-30 19:56:28 +01:00
const { swapsFeatureIsLive } = swapsLiveness ;
2020-11-03 00:41:28 +01:00
this . store . updateState ( {
2021-11-30 19:56:28 +01:00
swapsState : { ... swapsState , swapsFeatureIsLive } ,
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
}
2022-02-18 17:48:38 +01:00
setSwapsFeatureFlags ( swapsFeatureFlags ) {
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , swapsFeatureFlags } ,
} ) ;
}
2020-11-03 00:41:28 +01:00
resetPostFetchState ( ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
this . store . updateState ( {
swapsState : {
... initialState . swapsState ,
tokens : swapsState . tokens ,
fetchParams : swapsState . fetchParams ,
swapsFeatureIsLive : swapsState . swapsFeatureIsLive ,
2020-12-15 21:24:22 +01:00
swapsQuoteRefreshTime : swapsState . swapsQuoteRefreshTime ,
2021-09-15 15:13:18 +02:00
swapsQuotePrefetchingRefreshTime :
swapsState . swapsQuotePrefetchingRefreshTime ,
2022-02-18 17:48:38 +01:00
swapsFeatureFlags : swapsState . swapsFeatureFlags ,
2020-10-06 20:28:38 +02:00
} ,
2021-02-04 19:15:23 +01:00
} ) ;
clearTimeout ( this . pollingTimeout ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
resetSwapsState ( ) {
2021-02-04 19:15:23 +01:00
const { swapsState } = this . store . getState ( ) ;
2020-10-06 20:28:38 +02:00
this . store . updateState ( {
2020-11-03 00:41:28 +01:00
swapsState : {
... initialState . swapsState ,
2020-12-15 21:24:22 +01:00
swapsQuoteRefreshTime : swapsState . swapsQuoteRefreshTime ,
2021-09-15 15:13:18 +02:00
swapsQuotePrefetchingRefreshTime :
swapsState . swapsQuotePrefetchingRefreshTime ,
2023-06-15 20:17:21 +02:00
swapsFeatureFlags : swapsState . swapsFeatureFlags ,
2020-11-03 00:41:28 +01:00
} ,
2021-02-04 19:15:23 +01:00
} ) ;
clearTimeout ( this . pollingTimeout ) ;
2020-10-06 20:28:38 +02:00
}
2020-11-03 00:41:28 +01:00
async _findTopQuoteAndCalculateSavings ( quotes = { } ) {
2022-07-31 20:26:40 +02:00
const { contractExchangeRates : tokenConversionRates } =
this . getTokenRatesState ( ) ;
2020-10-06 20:28:38 +02:00
const {
2021-07-30 13:35:30 +02:00
swapsState : { customGasPrice , customMaxPriorityFeePerGas } ,
2021-02-04 19:15:23 +01:00
} = this . store . getState ( ) ;
2021-03-18 11:20:06 +01:00
const chainId = this . _getCurrentChainId ( ) ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
const numQuotes = Object . keys ( quotes ) . length ;
2020-10-19 23:52:47 +02:00
if ( ! numQuotes ) {
2021-02-04 19:15:23 +01:00
return { } ;
2020-10-06 20:28:38 +02:00
}
2021-02-04 19:15:23 +01:00
const newQuotes = cloneDeep ( quotes ) ;
2020-11-09 18:09:38 +01:00
2022-07-31 20:26:40 +02:00
const { gasFeeEstimates , gasEstimateType } =
await this . _getEIP1559GasFeeEstimates ( ) ;
2021-07-30 13:35:30 +02:00
let usedGasPrice = '0x0' ;
2023-01-27 19:28:03 +01:00
if ( gasEstimateType === GasEstimateTypes . feeMarket ) {
2021-07-30 13:35:30 +02:00
const {
high : { suggestedMaxPriorityFeePerGas } ,
estimatedBaseFee ,
} = gasFeeEstimates ;
2023-01-23 18:41:01 +01:00
const suggestedMaxPriorityFeePerGasInHexWEI = decGWEIToHexWEI (
suggestedMaxPriorityFeePerGas ,
2021-07-30 13:35:30 +02:00
) ;
2023-01-23 18:41:01 +01:00
const estimatedBaseFeeNumeric = new Numeric (
estimatedBaseFee ,
10 ,
EtherDenomination . GWEI ,
) . toDenomination ( EtherDenomination . WEI ) ;
usedGasPrice = new Numeric (
customMaxPriorityFeePerGas || suggestedMaxPriorityFeePerGasInHexWEI ,
16 ,
)
. add ( estimatedBaseFeeNumeric )
. round ( 6 )
. toString ( ) ;
2023-01-27 19:28:03 +01:00
} else if ( gasEstimateType === GasEstimateTypes . legacy ) {
2021-07-30 13:35:30 +02:00
usedGasPrice = customGasPrice || decGWEIToHexWEI ( gasFeeEstimates . high ) ;
2023-01-27 19:28:03 +01:00
} else if ( gasEstimateType === GasEstimateTypes . ethGasPrice ) {
2021-07-30 13:35:30 +02:00
usedGasPrice =
customGasPrice || decGWEIToHexWEI ( gasFeeEstimates . gasPrice ) ;
}
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
let topAggId = null ;
let overallValueOfBestQuoteForSorting = null ;
2020-10-06 20:28:38 +02:00
2020-11-09 18:09:38 +01:00
Object . values ( newQuotes ) . forEach ( ( quote ) => {
2020-10-06 20:28:38 +02:00
const {
2020-10-19 23:52:47 +02:00
aggregator ,
approvalNeeded ,
averageGas ,
2020-10-06 20:28:38 +02:00
destinationAmount = 0 ,
destinationToken ,
destinationTokenInfo ,
2022-09-26 18:30:17 +02:00
gasEstimateWithRefund ,
2020-10-19 23:52:47 +02:00
sourceAmount ,
sourceToken ,
trade ,
2020-11-09 18:09:38 +01:00
fee : metaMaskFee ,
2022-12-21 20:59:34 +01:00
multiLayerL1TradeFeeTotal ,
2021-02-04 19:15:23 +01:00
} = quote ;
2020-10-19 23:52:47 +02:00
2022-09-26 18:30:17 +02:00
const tradeGasLimitForCalculation = gasEstimateWithRefund
? new BigNumber ( gasEstimateWithRefund , 16 )
2021-02-04 19:15:23 +01:00
: new BigNumber ( averageGas || MAX _GAS _LIMIT , 10 ) ;
2020-10-19 23:52:47 +02:00
2020-10-06 20:28:38 +02:00
const totalGasLimitForCalculation = tradeGasLimitForCalculation
. plus ( approvalNeeded ? . gas || '0x0' , 16 )
2021-02-04 19:15:23 +01:00
. toString ( 16 ) ;
2020-10-19 23:52:47 +02:00
2022-12-21 20:59:34 +01:00
let gasTotalInWeiHex = calcGasTotal (
2020-10-06 20:28:38 +02:00
totalGasLimitForCalculation ,
usedGasPrice ,
2021-02-04 19:15:23 +01:00
) ;
2022-12-21 20:59:34 +01:00
if ( multiLayerL1TradeFeeTotal !== null ) {
gasTotalInWeiHex = sumHexes (
gasTotalInWeiHex || '0x0' ,
multiLayerL1TradeFeeTotal || '0x0' ,
) ;
}
2020-10-19 23:52:47 +02:00
// trade.value is a sum of different values depending on the transaction.
// It always includes any external fees charged by the quote source. In
2021-03-18 11:20:06 +01:00
// addition, if the source asset is the selected chain's default token, trade.value
// includes the amount of that token.
2023-01-23 18:41:01 +01:00
const totalWeiCost = new Numeric (
gasTotalInWeiHex ,
2020-11-03 00:41:28 +01:00
16 ,
2023-01-23 18:41:01 +01:00
EtherDenomination . WEI ,
) . add ( new Numeric ( trade . value , 16 , EtherDenomination . WEI ) ) ;
2020-10-19 23:52:47 +02:00
2023-01-23 18:41:01 +01:00
const totalEthCost = totalWeiCost
. toDenomination ( EtherDenomination . ETH )
. round ( 6 ) . value ;
2020-10-06 20:28:38 +02:00
2020-10-19 23:52:47 +02:00
// The total fee is aggregator/exchange fees plus gas fees.
2021-03-18 11:20:06 +01:00
// 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 )
2023-01-23 18:41:01 +01:00
? totalWeiCost
. minus ( new Numeric ( sourceAmount , 10 ) )
. toDenomination ( EtherDenomination . ETH )
. round ( 6 ) . value
2021-03-18 11:20:06 +01:00
: totalEthCost ;
2020-10-19 23:52:47 +02:00
2020-11-09 18:09:38 +01:00
const decimalAdjustedDestinationAmount = calcTokenAmount (
destinationAmount ,
destinationTokenInfo . decimals ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
const tokenPercentageOfPreFeeDestAmount = new BigNumber ( 100 , 10 )
. minus ( metaMaskFee , 10 )
2021-02-04 19:15:23 +01:00
. div ( 100 ) ;
2022-07-31 20:26:40 +02:00
const destinationAmountBeforeMetaMaskFee =
decimalAdjustedDestinationAmount . div ( tokenPercentageOfPreFeeDestAmount ) ;
2020-11-09 18:09:38 +01:00
const metaMaskFeeInTokens = destinationAmountBeforeMetaMaskFee . minus (
decimalAdjustedDestinationAmount ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
2021-11-22 13:04:31 +01:00
const tokenConversionRate =
tokenConversionRates [
Object . keys ( tokenConversionRates ) . find ( ( tokenAddress ) =>
isEqualCaseInsensitive ( tokenAddress , destinationToken ) ,
)
] ;
2021-02-04 19:15:23 +01:00
const conversionRateForSorting = tokenConversionRate || 1 ;
2020-11-09 18:09:38 +01:00
const ethValueOfTokens = decimalAdjustedDestinationAmount . times (
2021-11-23 17:33:32 +01:00
conversionRateForSorting . toString ( 10 ) ,
2020-11-09 18:09:38 +01:00
10 ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
2021-03-18 11:20:06 +01:00
const conversionRateForCalculations = isSwapsDefaultTokenAddress (
destinationToken ,
chainId ,
)
? 1
: tokenConversionRate ;
2020-11-09 18:09:38 +01:00
const overallValueOfQuoteForSorting =
conversionRateForCalculations === undefined
? ethValueOfTokens
2021-02-04 19:15:23 +01:00
: ethValueOfTokens . minus ( ethFee , 10 ) ;
2020-11-09 18:09:38 +01:00
2021-02-04 19:15:23 +01:00
quote . ethFee = ethFee . toString ( 10 ) ;
2020-11-09 18:09:38 +01:00
if ( conversionRateForCalculations !== undefined ) {
2021-02-04 19:15:23 +01:00
quote . ethValueOfTokens = ethValueOfTokens . toString ( 10 ) ;
quote . overallValueOfQuote = overallValueOfQuoteForSorting . toString ( 10 ) ;
2020-11-09 18:09:38 +01:00
quote . metaMaskFeeInEth = metaMaskFeeInTokens
2021-11-23 17:33:32 +01:00
. times ( conversionRateForCalculations . toString ( 10 ) )
2021-02-04 19:15:23 +01:00
. toString ( 10 ) ;
2020-11-09 18:09:38 +01:00
}
2020-10-06 20:28:38 +02:00
if (
2020-11-09 18:09:38 +01:00
overallValueOfBestQuoteForSorting === null ||
overallValueOfQuoteForSorting . gt ( overallValueOfBestQuoteForSorting )
2020-10-06 20:28:38 +02:00
) {
2021-02-04 19:15:23 +01:00
topAggId = aggregator ;
overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting ;
2020-10-06 20:28:38 +02:00
}
2021-02-04 19:15:23 +01:00
} ) ;
2020-10-06 20:28:38 +02:00
const isBest =
2021-03-18 11:20:06 +01:00
isSwapsDefaultTokenAddress (
newQuotes [ topAggId ] . destinationToken ,
chainId ,
2021-11-22 13:04:31 +01:00
) ||
Boolean (
tokenConversionRates [
Object . keys ( tokenConversionRates ) . find ( ( tokenAddress ) =>
isEqualCaseInsensitive (
tokenAddress ,
newQuotes [ topAggId ] ? . destinationToken ,
) ,
)
] ,
) ;
2020-10-06 20:28:38 +02:00
2021-02-04 19:15:23 +01:00
let savings = null ;
2020-10-19 23:52:47 +02:00
if ( isBest ) {
2021-02-04 19:15:23 +01:00
const bestQuote = newQuotes [ topAggId ] ;
2020-11-09 18:09:38 +01:00
2021-02-04 19:15:23 +01:00
savings = { } ;
2020-11-09 18:09:38 +01:00
const {
ethFee : medianEthFee ,
metaMaskFeeInEth : medianMetaMaskFee ,
ethValueOfTokens : medianEthValueOfTokens ,
2021-02-04 19:15:23 +01:00
} = getMedianEthValueQuote ( Object . values ( newQuotes ) ) ;
2020-11-09 18:09:38 +01:00
2020-10-19 23:52:47 +02:00
// Performance savings are calculated as:
2020-11-09 18:09:38 +01:00
// (ethValueOfTokens for the best trade) - (ethValueOfTokens for the media trade)
savings . performance = new BigNumber ( bestQuote . ethValueOfTokens , 10 ) . minus (
medianEthValueOfTokens ,
2020-10-19 23:52:47 +02:00
10 ,
2021-02-04 19:15:23 +01:00
) ;
2020-10-19 23:52:47 +02:00
2020-11-09 18:09:38 +01:00
// Fee savings are calculated as:
// (fee for the median trade) - (fee for the best trade)
2021-02-04 19:15:23 +01:00
savings . fee = new BigNumber ( medianEthFee ) . minus ( bestQuote . ethFee , 10 ) ;
2020-11-09 18:09:38 +01:00
2021-02-04 19:15:23 +01:00
savings . metaMaskFee = bestQuote . metaMaskFeeInEth ;
2020-10-19 23:52:47 +02:00
2020-11-09 18:09:38 +01:00
// Total savings are calculated as:
// performance savings + fee savings - metamask fee
savings . total = savings . performance
. plus ( savings . fee )
. minus ( savings . metaMaskFee )
2021-02-04 19:15:23 +01:00
. toString ( 10 ) ;
savings . performance = savings . performance . toString ( 10 ) ;
savings . fee = savings . fee . toString ( 10 ) ;
savings . medianMetaMaskFee = medianMetaMaskFee ;
2020-11-09 18:09:38 +01:00
2021-02-04 19:15:23 +01:00
newQuotes [ topAggId ] . isBestQuote = true ;
newQuotes [ topAggId ] . savings = savings ;
2020-10-19 23:52:47 +02:00
}
2021-02-04 19:15:23 +01:00
return [ topAggId , newQuotes ] ;
2020-10-06 20:28:38 +02:00
}
2021-03-30 20:55:14 +02:00
async _getERC20Allowance ( contractAddress , walletAddress , chainId ) {
2023-01-24 15:10:36 +01:00
const contract = new Contract ( contractAddress , abi , this . ethersProvider ) ;
2021-03-30 20:55:14 +02:00
return await contract . allowance (
walletAddress ,
SWAPS _CHAINID _CONTRACT _ADDRESS _MAP [ chainId ] ,
) ;
2020-10-06 20:28:38 +02:00
}
2020-10-19 23:52:47 +02:00
}
/ * *
2020-11-09 18:09:38 +01:00
* Calculates the median overallValueOfQuote of a sample of quotes .
2020-10-19 23:52:47 +02:00
*
2022-01-07 16:57:33 +01:00
* @ param { Array } _quotes - A sample of quote objects with overallValueOfQuote , ethFee , metaMaskFeeInEth , and ethValueOfTokens properties
2022-07-27 15:28:05 +02:00
* @ returns { object } An object with the ethValueOfTokens , ethFee , and metaMaskFeeInEth of the quote with the median overallValueOfQuote
2020-10-19 23:52:47 +02:00
* /
2020-11-09 18:09:38 +01:00
function getMedianEthValueQuote ( _quotes ) {
if ( ! Array . isArray ( _quotes ) || _quotes . length === 0 ) {
2021-02-04 19:15:23 +01:00
throw new Error ( 'Expected non-empty array param.' ) ;
2020-10-19 23:52:47 +02:00
}
2021-02-04 19:15:23 +01:00
const quotes = [ ... _quotes ] ;
2020-11-09 18:09:38 +01:00
quotes . sort ( ( quoteA , quoteB ) => {
2021-02-04 19:15:23 +01:00
const overallValueOfQuoteA = new BigNumber ( quoteA . overallValueOfQuote , 10 ) ;
const overallValueOfQuoteB = new BigNumber ( quoteB . overallValueOfQuote , 10 ) ;
2020-11-09 18:09:38 +01:00
if ( overallValueOfQuoteA . equals ( overallValueOfQuoteB ) ) {
2021-02-04 19:15:23 +01:00
return 0 ;
2020-10-19 23:52:47 +02:00
}
2021-02-04 19:15:23 +01:00
return overallValueOfQuoteA . lessThan ( overallValueOfQuoteB ) ? - 1 : 1 ;
} ) ;
2020-10-19 23:52:47 +02:00
2020-11-09 18:09:38 +01:00
if ( quotes . length % 2 === 1 ) {
// return middle values
const medianOverallValue =
2021-02-04 19:15:23 +01:00
quotes [ ( quotes . length - 1 ) / 2 ] . overallValueOfQuote ;
2020-11-09 18:09:38 +01:00
const quotesMatchingMedianQuoteValue = quotes . filter (
( quote ) => medianOverallValue === quote . overallValueOfQuote ,
2021-02-04 19:15:23 +01:00
) ;
return meansOfQuotesFeesAndValue ( quotesMatchingMedianQuoteValue ) ;
2020-10-19 23:52:47 +02:00
}
// return mean of middle two values
2021-02-04 19:15:23 +01:00
const upperIndex = quotes . length / 2 ;
const lowerIndex = upperIndex - 1 ;
2020-11-09 18:09:38 +01:00
2021-02-04 19:15:23 +01:00
const overallValueAtUpperIndex = quotes [ upperIndex ] . overallValueOfQuote ;
const overallValueAtLowerIndex = quotes [ lowerIndex ] . overallValueOfQuote ;
2020-11-09 18:09:38 +01:00
const quotesMatchingUpperIndexValue = quotes . filter (
( quote ) => overallValueAtUpperIndex === quote . overallValueOfQuote ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
const quotesMatchingLowerIndexValue = quotes . filter (
( quote ) => overallValueAtLowerIndex === quote . overallValueOfQuote ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
const feesAndValueAtUpperIndex = meansOfQuotesFeesAndValue (
quotesMatchingUpperIndexValue ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
const feesAndValueAtLowerIndex = meansOfQuotesFeesAndValue (
quotesMatchingLowerIndexValue ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
return {
ethFee : new BigNumber ( feesAndValueAtUpperIndex . ethFee , 10 )
. plus ( feesAndValueAtLowerIndex . ethFee , 10 )
. dividedBy ( 2 )
. toString ( 10 ) ,
metaMaskFeeInEth : new BigNumber (
feesAndValueAtUpperIndex . metaMaskFeeInEth ,
10 ,
)
. plus ( feesAndValueAtLowerIndex . metaMaskFeeInEth , 10 )
. dividedBy ( 2 )
. toString ( 10 ) ,
ethValueOfTokens : new BigNumber (
feesAndValueAtUpperIndex . ethValueOfTokens ,
10 ,
)
. plus ( feesAndValueAtLowerIndex . ethValueOfTokens , 10 )
. dividedBy ( 2 )
. toString ( 10 ) ,
2021-02-04 19:15:23 +01:00
} ;
2020-11-09 18:09:38 +01:00
}
/ * *
* Calculates the arithmetic mean for each of three properties - ethFee , metaMaskFeeInEth and ethValueOfTokens - across
* an array of objects containing those properties .
*
* @ param { Array } quotes - A sample of quote objects with overallValueOfQuote , ethFee , metaMaskFeeInEth and
* ethValueOfTokens properties
2022-07-27 15:28:05 +02:00
* @ returns { object } An object with the arithmetic mean each of the ethFee , metaMaskFeeInEth and ethValueOfTokens of
2020-11-09 18:09:38 +01:00
* the passed quote objects
* /
function meansOfQuotesFeesAndValue ( quotes ) {
const feeAndValueSumsAsBigNumbers = quotes . reduce (
( feeAndValueSums , quote ) => ( {
ethFee : feeAndValueSums . ethFee . plus ( quote . ethFee , 10 ) ,
metaMaskFeeInEth : feeAndValueSums . metaMaskFeeInEth . plus (
quote . metaMaskFeeInEth ,
10 ,
) ,
ethValueOfTokens : feeAndValueSums . ethValueOfTokens . plus (
quote . ethValueOfTokens ,
10 ,
) ,
} ) ,
{
ethFee : new BigNumber ( 0 , 10 ) ,
metaMaskFeeInEth : new BigNumber ( 0 , 10 ) ,
ethValueOfTokens : new BigNumber ( 0 , 10 ) ,
} ,
2021-02-04 19:15:23 +01:00
) ;
2020-11-09 18:09:38 +01:00
return {
ethFee : feeAndValueSumsAsBigNumbers . ethFee
. div ( quotes . length , 10 )
. toString ( 10 ) ,
metaMaskFeeInEth : feeAndValueSumsAsBigNumbers . metaMaskFeeInEth
. div ( quotes . length , 10 )
. toString ( 10 ) ,
ethValueOfTokens : feeAndValueSumsAsBigNumbers . ethValueOfTokens
. div ( quotes . length , 10 )
. toString ( 10 ) ,
2021-02-04 19:15:23 +01:00
} ;
2020-10-19 23:52:47 +02:00
}
2020-10-06 20:28:38 +02:00
2020-10-19 23:52:47 +02:00
export const utils = {
2020-11-09 18:09:38 +01:00
getMedianEthValueQuote ,
meansOfQuotesFeesAndValue ,
2021-02-04 19:15:23 +01:00
} ;