You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
9.0 KiB
321 lines
9.0 KiB
2 years ago
|
import BigNumber from 'bignumber.js';
|
||
|
import log from 'loglevel';
|
||
|
import { CHAIN_IDS } from '../constants/network';
|
||
|
import {
|
||
|
GAS_API_BASE_URL,
|
||
|
GAS_DEV_API_BASE_URL,
|
||
|
SWAPS_API_V2_BASE_URL,
|
||
|
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
|
||
|
SWAPS_CLIENT_ID,
|
||
|
SWAPS_DEV_API_V2_BASE_URL,
|
||
|
SWAPS_WRAPPED_TOKENS_ADDRESSES,
|
||
|
} from '../constants/swaps';
|
||
|
import { SECOND } from '../constants/time';
|
||
|
import { isValidHexAddress } from '../modules/hexstring-utils';
|
||
|
import { addHexPrefix } from '../../app/scripts/lib/util';
|
||
|
import fetchWithCache from './fetch-with-cache';
|
||
|
import { decimalToHex } from './transactions-controller-utils';
|
||
|
|
||
|
const TEST_CHAIN_IDS = [CHAIN_IDS.GOERLI, CHAIN_IDS.LOCALHOST];
|
||
|
|
||
|
const clientIdHeader = { 'X-Client-Id': SWAPS_CLIENT_ID };
|
||
|
|
||
|
export const validHex = (string) => Boolean(string?.match(/^0x[a-f0-9]+$/u));
|
||
|
export const truthyString = (string) => Boolean(string?.length);
|
||
|
export const truthyDigitString = (string) =>
|
||
|
truthyString(string) && Boolean(string.match(/^\d+$/u));
|
||
|
|
||
|
export function validateData(validators, object, urlUsed, logError = true) {
|
||
|
return validators.every(({ property, type, validator }) => {
|
||
|
const types = type.split('|');
|
||
|
|
||
|
const valid =
|
||
|
types.some((_type) => typeof object[property] === _type) &&
|
||
|
(!validator || validator(object[property]));
|
||
|
if (!valid && logError) {
|
||
|
log.error(
|
||
|
`response to GET ${urlUsed} invalid for property ${property}; value was:`,
|
||
|
object[property],
|
||
|
'| type was: ',
|
||
|
typeof object[property],
|
||
|
);
|
||
|
}
|
||
|
return valid;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
export const QUOTE_VALIDATORS = [
|
||
|
{
|
||
|
property: 'trade',
|
||
|
type: 'object',
|
||
|
validator: (trade) =>
|
||
|
trade &&
|
||
|
validHex(trade.data) &&
|
||
|
isValidHexAddress(trade.to, { allowNonPrefixed: false }) &&
|
||
|
isValidHexAddress(trade.from, { allowNonPrefixed: false }) &&
|
||
|
truthyString(trade.value),
|
||
|
},
|
||
|
{
|
||
|
property: 'approvalNeeded',
|
||
|
type: 'object',
|
||
|
validator: (approvalTx) =>
|
||
|
approvalTx === null ||
|
||
|
(approvalTx &&
|
||
|
validHex(approvalTx.data) &&
|
||
|
isValidHexAddress(approvalTx.to, { allowNonPrefixed: false }) &&
|
||
|
isValidHexAddress(approvalTx.from, { allowNonPrefixed: false })),
|
||
|
},
|
||
|
{
|
||
|
property: 'sourceAmount',
|
||
|
type: 'string',
|
||
|
validator: truthyDigitString,
|
||
|
},
|
||
|
{
|
||
|
property: 'destinationAmount',
|
||
|
type: 'string',
|
||
|
validator: truthyDigitString,
|
||
|
},
|
||
|
{
|
||
|
property: 'sourceToken',
|
||
|
type: 'string',
|
||
|
validator: (input) => isValidHexAddress(input, { allowNonPrefixed: false }),
|
||
|
},
|
||
|
{
|
||
|
property: 'destinationToken',
|
||
|
type: 'string',
|
||
|
validator: (input) => isValidHexAddress(input, { allowNonPrefixed: false }),
|
||
|
},
|
||
|
{
|
||
|
property: 'aggregator',
|
||
|
type: 'string',
|
||
|
validator: truthyString,
|
||
|
},
|
||
|
{
|
||
|
property: 'aggType',
|
||
|
type: 'string',
|
||
|
validator: truthyString,
|
||
|
},
|
||
|
{
|
||
|
property: 'error',
|
||
|
type: 'object',
|
||
|
validator: (error) => error === null || typeof error === 'object',
|
||
|
},
|
||
|
{
|
||
|
property: 'averageGas',
|
||
|
type: 'number',
|
||
|
},
|
||
|
{
|
||
|
property: 'maxGas',
|
||
|
type: 'number',
|
||
|
},
|
||
|
{
|
||
|
property: 'gasEstimate',
|
||
|
type: 'number|undefined',
|
||
|
validator: (gasEstimate) => gasEstimate === undefined || gasEstimate > 0,
|
||
|
},
|
||
|
{
|
||
|
property: 'fee',
|
||
|
type: 'number',
|
||
|
},
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* @param {string} type - Type of an API call, e.g. "tokens"
|
||
|
* @param {string} chainId
|
||
|
* @returns string
|
||
|
*/
|
||
|
const getBaseUrlForNewSwapsApi = (type, chainId) => {
|
||
|
const useDevApis = process.env.SWAPS_USE_DEV_APIS;
|
||
|
const v2ApiBaseUrl = useDevApis
|
||
|
? SWAPS_DEV_API_V2_BASE_URL
|
||
|
: SWAPS_API_V2_BASE_URL;
|
||
|
const gasApiBaseUrl = useDevApis ? GAS_DEV_API_BASE_URL : GAS_API_BASE_URL;
|
||
|
const noNetworkSpecificTypes = ['refreshTime']; // These types don't need network info in the URL.
|
||
|
if (noNetworkSpecificTypes.includes(type)) {
|
||
|
return v2ApiBaseUrl;
|
||
|
}
|
||
|
const chainIdDecimal = chainId && parseInt(chainId, 16);
|
||
|
const gasApiTypes = ['gasPrices'];
|
||
|
if (gasApiTypes.includes(type)) {
|
||
|
return `${gasApiBaseUrl}/networks/${chainIdDecimal}`; // Gas calculations are in its own repo.
|
||
|
}
|
||
|
return `${v2ApiBaseUrl}/networks/${chainIdDecimal}`;
|
||
|
};
|
||
|
|
||
|
export const getBaseApi = function (type, chainId = CHAIN_IDS.MAINNET) {
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
chainId = TEST_CHAIN_IDS.includes(chainId) ? CHAIN_IDS.MAINNET : chainId;
|
||
|
const baseUrl = getBaseUrlForNewSwapsApi(type, chainId);
|
||
|
const chainIdDecimal = chainId && parseInt(chainId, 16);
|
||
|
if (!baseUrl) {
|
||
|
throw new Error(`Swaps API calls are disabled for chainId: ${chainId}`);
|
||
|
}
|
||
|
switch (type) {
|
||
|
case 'trade':
|
||
|
return `${baseUrl}/trades?`;
|
||
|
case 'tokens':
|
||
|
return `${baseUrl}/tokens`;
|
||
|
case 'token':
|
||
|
return `${baseUrl}/token`;
|
||
|
case 'topAssets':
|
||
|
return `${baseUrl}/topAssets`;
|
||
|
case 'aggregatorMetadata':
|
||
|
return `${baseUrl}/aggregatorMetadata`;
|
||
|
case 'gasPrices':
|
||
|
return `${baseUrl}/gasPrices`;
|
||
|
case 'network':
|
||
|
// Only use v2 for this endpoint.
|
||
|
return `${SWAPS_API_V2_BASE_URL}/networks/${chainIdDecimal}`;
|
||
|
default:
|
||
|
throw new Error('getBaseApi requires an api call type');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
export function calcTokenValue(value, decimals) {
|
||
|
const multiplier = Math.pow(10, Number(decimals || 0));
|
||
|
return new BigNumber(String(value)).times(multiplier);
|
||
|
}
|
||
|
|
||
|
export const shouldEnableDirectWrapping = (
|
||
|
chainId,
|
||
|
sourceToken,
|
||
|
destinationToken,
|
||
|
) => {
|
||
|
if (!sourceToken || !destinationToken) {
|
||
|
return false;
|
||
|
}
|
||
|
const wrappedToken = SWAPS_WRAPPED_TOKENS_ADDRESSES[chainId];
|
||
|
const nativeToken = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.address;
|
||
|
const sourceTokenLowerCase = sourceToken.toLowerCase();
|
||
|
const destinationTokenLowerCase = destinationToken.toLowerCase();
|
||
|
return (
|
||
|
(sourceTokenLowerCase === wrappedToken &&
|
||
|
destinationTokenLowerCase === nativeToken) ||
|
||
|
(sourceTokenLowerCase === nativeToken &&
|
||
|
destinationTokenLowerCase === wrappedToken)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Given and object where all values are strings, returns the same object with all values
|
||
|
* now prefixed with '0x'
|
||
|
*
|
||
|
* @param obj
|
||
|
*/
|
||
|
export function addHexPrefixToObjectValues(obj) {
|
||
|
return Object.keys(obj).reduce((newObj, key) => {
|
||
|
return { ...newObj, [key]: addHexPrefix(obj[key]) };
|
||
|
}, {});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given the standard set of information about a transaction, returns a transaction properly formatted for
|
||
|
* publishing via JSON RPC and web3
|
||
|
*
|
||
|
* @param {object} options
|
||
|
* @param {boolean} [options.sendToken] - Indicates whether or not the transaciton is a token transaction
|
||
|
* @param {string} options.data - A hex string containing the data to include in the transaction
|
||
|
* @param {string} options.to - A hex address of the tx recipient address
|
||
|
* @param options.amount
|
||
|
* @param {string} options.from - A hex address of the tx sender address
|
||
|
* @param {string} options.gas - A hex representation of the gas value for the transaction
|
||
|
* @param {string} options.gasPrice - A hex representation of the gas price for the transaction
|
||
|
* @returns {object} An object ready for submission to the blockchain, with all values appropriately hex prefixed
|
||
|
*/
|
||
|
export function constructTxParams({
|
||
|
sendToken,
|
||
|
data,
|
||
|
to,
|
||
|
amount,
|
||
|
from,
|
||
|
gas,
|
||
|
gasPrice,
|
||
|
}) {
|
||
|
const txParams = {
|
||
|
data,
|
||
|
from,
|
||
|
value: '0',
|
||
|
gas,
|
||
|
gasPrice,
|
||
|
};
|
||
|
|
||
|
if (!sendToken) {
|
||
|
txParams.value = amount;
|
||
|
txParams.to = to;
|
||
|
}
|
||
|
return addHexPrefixToObjectValues(txParams);
|
||
|
}
|
||
|
|
||
|
export async function fetchTradesInfo(
|
||
|
{
|
||
|
slippage,
|
||
|
sourceToken,
|
||
|
sourceDecimals,
|
||
|
destinationToken,
|
||
|
value,
|
||
|
fromAddress,
|
||
|
exchangeList,
|
||
|
},
|
||
|
{ chainId },
|
||
|
) {
|
||
|
const urlParams = {
|
||
|
destinationToken,
|
||
|
sourceToken,
|
||
|
sourceAmount: calcTokenValue(value, sourceDecimals).toString(10),
|
||
|
slippage,
|
||
|
timeout: SECOND * 10,
|
||
|
walletAddress: fromAddress,
|
||
|
};
|
||
|
|
||
|
if (exchangeList) {
|
||
|
urlParams.exchangeList = exchangeList;
|
||
|
}
|
||
|
if (shouldEnableDirectWrapping(chainId, sourceToken, destinationToken)) {
|
||
|
urlParams.enableDirectWrapping = true;
|
||
|
}
|
||
|
|
||
|
const queryString = new URLSearchParams(urlParams).toString();
|
||
|
const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`;
|
||
|
const tradesResponse = await fetchWithCache(
|
||
|
tradeURL,
|
||
|
{ method: 'GET', headers: clientIdHeader },
|
||
|
{ cacheRefreshTime: 0, timeout: SECOND * 15 },
|
||
|
);
|
||
|
const newQuotes = tradesResponse.reduce((aggIdTradeMap, quote) => {
|
||
|
if (
|
||
|
quote.trade &&
|
||
|
!quote.error &&
|
||
|
validateData(QUOTE_VALIDATORS, quote, tradeURL)
|
||
|
) {
|
||
|
const constructedTrade = constructTxParams({
|
||
|
to: quote.trade.to,
|
||
|
from: quote.trade.from,
|
||
|
data: quote.trade.data,
|
||
|
amount: decimalToHex(quote.trade.value),
|
||
|
gas: decimalToHex(quote.maxGas),
|
||
|
});
|
||
|
|
||
|
let { approvalNeeded } = quote;
|
||
|
|
||
|
if (approvalNeeded) {
|
||
|
approvalNeeded = constructTxParams({
|
||
|
...approvalNeeded,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
...aggIdTradeMap,
|
||
|
[quote.aggregator]: {
|
||
|
...quote,
|
||
|
slippage,
|
||
|
trade: constructedTrade,
|
||
|
approvalNeeded,
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
return aggIdTradeMap;
|
||
|
}, {});
|
||
|
|
||
|
return newQuotes;
|
||
|
}
|