diff --git a/src/common/utils/request.ts b/src/common/utils/request.ts index c10369d283..613616c762 100644 --- a/src/common/utils/request.ts +++ b/src/common/utils/request.ts @@ -112,10 +112,11 @@ const sendRequest = (options) => { const opts = { ...defaultOptions, ...options } const req = request[opts.method](opts.endpoint) - // req.set({ - // 'Content-Type': opts.formData ? 'application/x-www-form-urlencoded; charset=UTF-8' : 'application/json', - // ...(opts.headers || {}), - // }) + if (opts.headers) { + req.set({ + ...opts.headers, + }) + } if (opts.timeout) { req.timeout({ diff --git a/src/front/config/mainnet/api.js b/src/front/config/mainnet/api.js index 3c4e254ea6..8293e9ef94 100644 --- a/src/front/config/mainnet/api.js +++ b/src/front/config/mainnet/api.js @@ -4,6 +4,7 @@ export default { zeroxPolygon: 'https://polygon.api.0x.org', zeroxFantom: 'https://fantom.api.0x.org', zeroxAvalanche: 'https://avalanche.api.0x.org', + zeroxArbitrum: 'https://arbitrum.api.0x.org', oneinch: 'https://api.1inch.exchange/v3.0', limitOrders: 'https://limit-orders.1inch.exchange/v1.0', horizon: 'https://horizon.stellar.org', diff --git a/src/front/config/mainnet/swapContract.js b/src/front/config/mainnet/swapContract.js index e7c9514c49..a29aada91d 100644 --- a/src/front/config/mainnet/swapContract.js +++ b/src/front/config/mainnet/swapContract.js @@ -8,7 +8,7 @@ export default { protectedBtcKey: '025c8ee352e8b0d12aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac800bd', btcPinKey: '032aec5d20f9a0bb913a9835330259748392927c9a812299c4498a9e2ed3e78d3f', zerox: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', - + zeroxFantom: '0xdef189deaef76e379df891899eb5a00a94cbc250', uniswapRouter: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', // Ethereum uniswapFactory: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', pancakeswapRouter: '0x10ED43C718714eb63d5aA57B78B54704E256024E', // BSC diff --git a/src/front/config/testnet/api.js b/src/front/config/testnet/api.js index 0dfbf10ba0..5783886d90 100644 --- a/src/front/config/testnet/api.js +++ b/src/front/config/testnet/api.js @@ -1,5 +1,6 @@ export default { - zeroxRopsten: 'https://ropsten.api.0x.org', + zeroxSepolia: 'https://sepolia.api.0x.org', + zeroxMumbai: 'https://mumbai.api.0x.org', oneinch: 'https://api.1inch.exchange/v3.0', limitOrders: 'https://limit-orders.1inch.exchange/v1.0', horizon: 'https://horizon-testnet.stellar.org', diff --git a/src/front/config/testnet/swapContract.js b/src/front/config/testnet/swapContract.js index d3a0f1faca..1b59a6cfd3 100644 --- a/src/front/config/testnet/swapContract.js +++ b/src/front/config/testnet/swapContract.js @@ -9,7 +9,8 @@ export default { reputationOracle: '0x6260B5ef52d72732674fF4BDE3B37a4222dB1785', protectedBtcKey: '023d894571a253b87868db7d54a8b583e0c8ce53b484af8a0b0390b7722975cfaa', btcPinKey: '02094916ddab5abf215a49422a71be54ceb92c3d8114909048fc45ee90acdb5b32', - + zeroxSepolia: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', + zeroxMumbai: '0xf471d32cb40837bf24529fcf17418fc1a4807626', uniswapRouter: '0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506', // Rinkeby uniswapFactory: '0xc35DADB65012eC5796536bD9864eD8773aBc74C4', pancakeswapRouter: '0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3', // BSC testnet diff --git a/src/front/shared/pages/Exchange/QuickSwap/Feedback.tsx b/src/front/shared/pages/Exchange/QuickSwap/Feedback.tsx index 1c6511b03d..13e2353855 100644 --- a/src/front/shared/pages/Exchange/QuickSwap/Feedback.tsx +++ b/src/front/shared/pages/Exchange/QuickSwap/Feedback.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl' import CSSModules from 'react-css-modules' import styles from './index.scss' import { BlockReasons, Actions } from './types' -import { API_NAME } from './constants' +import { SWAP_API } from './constants' function Feedback(props) { const { @@ -25,7 +25,7 @@ function Feedback(props) { return (
- {!isSourceMode && !API_NAME[network?.networkVersion] ? ( + {!isSourceMode && !SWAP_API[network?.networkVersion] ? (

void - resetSwapData: () => void - resetSpendedAmount: () => void + resetSwapData: VoidFunction + resetSpendedAmount: VoidFunction setBlockReason: (a: BlockReasons) => void isApiRequestBlocking: () => boolean setPending: (a: boolean) => void - onInputDataChange: () => void + onInputDataChange: VoidFunction + finalizeApiSwapData: () => Promise baseChainWallet: IUniversalObj } function Footer(props: FooterProps) { const { + history, parentState, isSourceMode, sourceAction, @@ -40,6 +51,7 @@ function Footer(props: FooterProps) { setPending, baseChainWallet, onInputDataChange, + finalizeApiSwapData, } = props const { blockReason, @@ -60,12 +72,22 @@ function Footer(props: FooterProps) { error, slippage, currentLiquidityPair, + swapFee, + serviceFee, + fiat, } = parentState + const [finalizeSwap, setFinalizeSwap] = useState(false) + + const startSwapReview = async () => { + setFinalizeSwap(true) + await finalizeApiSwapData() + } + const approve = async (direction) => { - const spender = isSourceMode + const spender: `0x${number}` = isSourceMode ? LIQUIDITY_SOURCE_DATA[network.networkVersion]?.router - : externalConfig.swapContract.zerox + : externalConfig.swapContract[SWAP_API[network.networkVersion].spender] let wallet = fromWallet let amount = spendedAmount @@ -97,6 +119,10 @@ function Footer(props: FooterProps) { const apiSwap = async () => { if (isSourceMode) return + if (!swapData) throw new Error('No swap data. Can not complete swap') + if (swapData.to !== externalConfig.swapContract[SWAP_API[network.networkVersion].spender]) { + return console.log('%c0x constant proxy is not equal to swap transaction proxy', 'color:red') + } const baseCurrency = fromWallet.standard ? fromWallet.baseCurrency : fromWallet.currency const assetName = fromWallet.standard ? fromWallet.tokenKey : fromWallet.currency @@ -107,10 +133,6 @@ function Footer(props: FooterProps) { setPending(true) try { - if (!swapData) { - throw new Error('No swap data. Can not complete swap') - } - if (gasLimit) swapData.gas = gasLimit if (gasPrice) swapData.gasPrice = utils.amount.formatWithDecimals(gasPrice, GWEI_DECIMALS) @@ -120,7 +142,6 @@ function Footer(props: FooterProps) { if (txHash) { const txInfoUrl = transactions.getTxRouter(assetName.toLowerCase(), txHash) - routing.redirectTo(txInfoUrl) } @@ -131,6 +152,7 @@ function Footer(props: FooterProps) { } setPending(false) + setFinalizeSwap(false) } const directSwap = async () => { @@ -227,7 +249,11 @@ function Footer(props: FooterProps) { const doNotMakeApiRequest = isApiRequestBlocking() - const commonBlockReasons = isPending || (blockReason !== BlockReasons.NotApproved && !!error && (!error.message?.match('transfer amount exceeds allowance'))) + const commonBlockReasons = + isPending || + (blockReason !== BlockReasons.NotApproved && + !!error && + !error.message?.match('transfer amount exceeds allowance')) const formFilled = !!spendedAmount && !!receivedAmount const approvingDoesNotMakeSense = @@ -252,6 +278,24 @@ function Footer(props: FooterProps) { return (

+ {finalizeSwap && ( + setFinalizeSwap(false)} + history={history} + swapFee={swapFee} + fiat={fiat} + serviceFee={serviceFee} + slippage={slippage} + network={network} + spendedAmount={spendedAmount} + baseChainWallet={baseChainWallet} + fromWallet={fromWallet} + toWallet={toWallet} + /> + )} {needApproveA ? ( ) : !isSourceMode ? ( - ) : sourceAction === Actions.Swap ? ( +
+ + ) +} diff --git a/src/front/shared/pages/Exchange/QuickSwap/constants.ts b/src/front/shared/pages/Exchange/QuickSwap/constants.ts index 877e7dee9e..98f4f3cc51 100644 --- a/src/front/shared/pages/Exchange/QuickSwap/constants.ts +++ b/src/front/shared/pages/Exchange/QuickSwap/constants.ts @@ -11,12 +11,39 @@ export const GWEI_DECIMALS = 9 export const MAX_PERCENT = 100 export const SEC_PER_MINUTE = 60 -export const API_NAME = { - 1: 'zeroxEthereum', - 56: 'zeroxBsc', - 137: 'zeroxPolygon', - 250: 'zeroxFantom', - 43114: 'zeroxAvalanche', +export const SWAP_API = { + 1: { + name: 'zeroxEthereum', + spender: 'zerox', + }, + 11155111: { + name: 'zeroxSepolia', + spender: 'zeroxSepolia', + }, + 56: { + name: 'zeroxBsc', + spender: 'zerox', + }, + 137: { + name: 'zeroxPolygon', + spender: 'zerox', + }, + 80001: { + name: 'zeroxMumbai', + spender: 'zeroxMumbai', + }, + 250: { + name: 'zeroxFantom', + spender: 'zeroxFantom', + }, + 43114: { + name: 'zeroxAvalanche', + spender: 'zerox', + }, + 42161: { + name: 'zeroxArbitrum', + spender: 'zerox', + }, } export const API_GAS_LIMITS = { diff --git a/src/front/shared/pages/Exchange/QuickSwap/index.scss b/src/front/shared/pages/Exchange/QuickSwap/index.scss index b6190dd278..26b74e986f 100644 --- a/src/front/shared/pages/Exchange/QuickSwap/index.scss +++ b/src/front/shared/pages/Exchange/QuickSwap/index.scss @@ -216,6 +216,16 @@ $maxWidth: 38em; margin-bottom: 0.5rem; } +.swapPreviewWrapper { + max-width: $maxWidth; +} + +.swapInProgressMessage { + width: 100%; + padding: 30px 0; + text-align: center; +} + @media all and (max-width: 500px) { .newTokenInstruction { font-size: 0.95rem; diff --git a/src/front/shared/pages/Exchange/QuickSwap/index.tsx b/src/front/shared/pages/Exchange/QuickSwap/index.tsx index 0f353f4c8c..d0cb3e7797 100644 --- a/src/front/shared/pages/Exchange/QuickSwap/index.tsx +++ b/src/front/shared/pages/Exchange/QuickSwap/index.tsx @@ -22,6 +22,7 @@ import { import { localisedUrl } from 'helpers/locale' import actions from 'redux/actions' import Link from 'local_modules/sw-valuelink' +import { buildApiSwapParams, estimateApiSwapData } from './swapApi' import { ComponentState, Direction, @@ -31,7 +32,7 @@ import { CurrencyMenuItem, } from './types' import { - API_NAME, + SWAP_API, GWEI_DECIMALS, COIN_DECIMALS, API_GAS_LIMITS, @@ -192,6 +193,7 @@ class QuickSwap extends PureComponent { gasLimit: '', blockReason: undefined, serviceFee: false, + zeroxApiKey: window.zeroxApiKey || '', } } @@ -314,7 +316,7 @@ class QuickSwap extends PureComponent { const { coin, blockchain } = getCoinInfo(currency.value) const network = externalConfig.evmNetworks[blockchain || coin] - return !!API_NAME[network?.networkVersion] + return !!SWAP_API[network?.networkVersion] }) } @@ -346,7 +348,7 @@ class QuickSwap extends PureComponent { const feeOptsKey = fromWallet?.standard || fromWallet?.currency const currentFeeOpts = externalConfig.opts.fee[feeOptsKey?.toLowerCase()] - const correctFeeRepresentation = !Number.isNaN(window?.zeroxFeePercent) + const correctFeeRepresentation = !Number.isNaN(window?.zeroxFeePercent) && window.zeroxFeePercent >= 0 && window.zeroxFeePercent <= 100 @@ -502,55 +504,6 @@ class QuickSwap extends PureComponent { })) } - createSwapRequest = (skipValidation = false) => { - const { slippage, spendedAmount, fromWallet, toWallet, serviceFee } = this.state - - const sellToken = fromWallet?.contractAddress || EVM_COIN_ADDRESS - const buyToken = toWallet?.contractAddress || EVM_COIN_ADDRESS - - const sellAmount = utils.amount.formatWithDecimals( - spendedAmount, - fromWallet.decimals || COIN_DECIMALS, - ) - - const enoughBalanceForSwap = new BigNumber(fromWallet.balance).isGreaterThan(new BigNumber(spendedAmount)) - - const request = [ - `/swap/v1/quote?`, - `buyToken=${buyToken}&`, - `sellToken=${sellToken}&`, - `sellAmount=${sellAmount}`, - ] - if (enoughBalanceForSwap) { - request.push(`&takerAddress=${fromWallet.address}`) - } - - if (window?.STATISTICS_ENABLED) { - request.push(`&affiliateAddress=${externalConfig.swapContract.affiliateAddress}`) - } - - if (serviceFee) { - const { address, percent } = serviceFee - - request.push(`&feeRecipient=${address}`) - request.push(`&buyTokenPercentageFee=${percent}`) - } - - if (skipValidation) { - request.push(`&skipValidation=true`) - } - - if (slippage) { - // allow users to enter an amount up to 100, because it's more easy then enter the amount from 0 to 1 - // and now convert it into the api format - const correctValue = new BigNumber(slippage).dividedBy(MAX_PERCENT) - - request.push(`&slippagePercentage=${correctValue}`) - } - - return request.join('') - } - onInputDataChange = async () => { const { activeSection, sourceAction, currentLiquidityPair } = this.state @@ -570,7 +523,7 @@ class QuickSwap extends PureComponent { })) if (activeSection === Sections.Aggregator) { - await this.fetchSwapAPIData() + await this.fetchApiSwapPrice() } else if (activeSection === Sections.Source) { await this.processingSourceActions() // start approve check only after the received amount request in processingSourceActions() @@ -601,46 +554,25 @@ class QuickSwap extends PureComponent { return false } - calculateDataFromSwap = async (params) => { - const { baseChainWallet, toWallet, gasLimit, gasPrice } = this.state - const { swap, withoutValidation } = params - - // we've had a special error in the previous request. It means there is - // some problem and we add a "skip validation" parameter to bypass it. - // Usually the swap tx with this parameter fails in the blockchain, - // because it's not enough gas limit. Estimate it by yourself - if (withoutValidation) { - const estimatedGas = await actions[baseChainWallet.currency.toLowerCase()]?.estimateGas(swap) - - if (typeof estimatedGas === 'number') { - swap.gas = estimatedGas - } else if (estimatedGas instanceof Error) { - this.reportError(estimatedGas) - } - } - - const customGasLimit = gasLimit && gasLimit > swap.gas ? gasLimit : swap.gas - const customGasPrice = gasPrice - ? utils.amount.formatWithDecimals(gasPrice, GWEI_DECIMALS) - : swap.gasPrice - - const weiFee = new BigNumber(customGasLimit).times(customGasPrice) - const swapFee = utils.amount.formatWithoutDecimals(weiFee, COIN_DECIMALS) - const receivedAmount = utils.amount.formatWithoutDecimals( - swap.buyAmount, - toWallet?.decimals || COIN_DECIMALS, - ) - - this.setState(() => ({ - receivedAmount, - swapData: swap, - swapFee, - isPending: false, - })) - } + fetchApiSwapPrice = async () => { + const { + isSourceMode, + network, + spendedAmount, + isPending, + zeroxApiKey, + slippage, + fromWallet, + toWallet, + serviceFee, + baseChainWallet, + gasLimit, + gasPrice, + } = this.state - fetchSwapAPIData = async () => { - const { network, spendedAmount, isPending } = this.state + if (!isSourceMode && !zeroxApiKey) { + return console.log('%c0x API key is not set', 'color:red') + } const dontFetch = ( new BigNumber(spendedAmount).isNaN() @@ -656,36 +588,106 @@ class QuickSwap extends PureComponent { })) let repeatRequest = true - let swapRequest = this.createSwapRequest() + const params = buildApiSwapParams({ + route: '/price', + slippage, + spendedAmount, + fromWallet, + toWallet, + serviceFee, + zeroxApiKey, + }) + let { headers, endpoint } = params while (repeatRequest) { - const swap: any = await apiLooper.get(API_NAME[network.networkVersion], swapRequest, { - reportErrors: (error) => { + const swap: any = await apiLooper.get(SWAP_API[network.networkVersion].name, endpoint, { + headers, + sourceError: true, + reportErrors: (error: IError) => { if (!repeatRequest) { this.reportError(error) } }, - sourceError: true, }) if (!(swap instanceof Error)) { repeatRequest = false - await this.calculateDataFromSwap({ - swap, - withoutValidation: swapRequest.match(/skipValidation/), + const data = await estimateApiSwapData({ + data: swap, + withoutValidation: endpoint.match(/skipValidation/), + baseChainWallet, + toWallet, + gasLimit, + gasPrice, + onError: this.reportError, }) + console.log('SWAP PRICE response', data) + this.setState(() => ({ ...data })) } else if (this.tryToSkipValidation(swap)) { - // it's a special error. Will be a new request - swapRequest = this.createSwapRequest(true) + const p = buildApiSwapParams({ + route: '/price', + skipValidation: true, + slippage, + spendedAmount, + fromWallet, + toWallet, + serviceFee, + zeroxApiKey, + }) + headers = p.headers + endpoint = p.endpoint } else { this.reportError(swap) - repeatRequest = false } } } + finalizeApiSwapData = async () => { + const { + network, + spendedAmount, + zeroxApiKey, + slippage, + fromWallet, + toWallet, + serviceFee, + baseChainWallet, + gasLimit, + gasPrice, + } = this.state + + const { headers, endpoint } = buildApiSwapParams({ + route: '/quote', + slippage, + spendedAmount, + fromWallet, + toWallet, + serviceFee, + zeroxApiKey, + }) + + this.resetSwapData() + this.setState(() => ({ + swapFee: '', + isPending: true, + })) + const rawQuote: any = await apiLooper.get(SWAP_API[network.networkVersion].name, endpoint, { + headers, + sourceError: true, + reportErrors: this.reportError, + }) + const data = await estimateApiSwapData({ + data: rawQuote, + baseChainWallet, + toWallet, + gasLimit, + gasPrice, + }) + this.setState(() => ({ ...data })) + } + updateCurrentPairAddress = async () => { const { network, baseChainWallet, fromWallet, toWallet } = this.state const tokenA = fromWallet?.contractAddress || EVM_COIN_ADDRESS @@ -828,9 +830,9 @@ class QuickSwap extends PureComponent { wallet = toWallet } - const spender = isSourceMode + const spender: `0x${number}` = isSourceMode ? LIQUIDITY_SOURCE_DATA[network.networkVersion]?.router - : externalConfig.swapContract.zerox + : externalConfig.swapContract[SWAP_API[network.networkVersion].spender] if (!wallet.isToken) { this.setNeedApprove(direction, false) @@ -1232,6 +1234,7 @@ class QuickSwap extends PureComponent { />