diff --git a/.env b/.env index b9738d42a..1b088f106 100644 --- a/.env +++ b/.env @@ -10,4 +10,6 @@ REACT_APP__APP_VERSION=$npm_package_version # Contract Info REACT_APP__CHAIN_ID=31337 -REACT_APP__CONTRACT_ADDRESS__DUALITY_CORE=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 \ No newline at end of file +REACT_APP__CONTRACT_ADDRESS__DUALITY_CORE=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 +REACT_APP__REST_API=http://localhost:1317 +REACT_APP__WEBSOCKET_URL=ws://localhost:26657/websocket \ No newline at end of file diff --git a/.env.template b/.env.template index d9481d8d8..ea7699413 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,4 @@ REACT_APP__CHAIN_ID=31337 REACT_APP__CONTRACT_ADDRESS__DUALITY_CORE=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 +REACT_APP__REST_API=http://localhost:1317 +REACT_APP__WEBSOCKET_URL=ws://localhost:26657/websocket \ No newline at end of file diff --git a/.env.test b/.env.test index d9481d8d8..ea7699413 100644 --- a/.env.test +++ b/.env.test @@ -1,2 +1,4 @@ REACT_APP__CHAIN_ID=31337 REACT_APP__CONTRACT_ADDRESS__DUALITY_CORE=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 +REACT_APP__REST_API=http://localhost:1317 +REACT_APP__WEBSOCKET_URL=ws://localhost:26657/websocket \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c46fe8129..94fc2aea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@floating-ui/react-dom": "^0.6.3", "@reach/dialog": "^0.17.0", + "bignumber.js": "^9.0.2", "buffer": "^6.0.3", "ethers": "^5.6.4", "invariant": "^2.2.4", @@ -7181,6 +7182,14 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -10032,15 +10041,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/expect/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/expect/node_modules/jest-diff": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", @@ -28382,6 +28382,11 @@ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -30514,12 +30519,6 @@ "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "jest-diff": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", diff --git a/package.json b/package.json index 849faac19..4a2eb6590 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@floating-ui/react-dom": "^0.6.3", "@reach/dialog": "^0.17.0", + "bignumber.js": "^9.0.2", "buffer": "^6.0.3", "ethers": "^5.6.4", "invariant": "^2.2.4", @@ -75,4 +76,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/src/components/TokenPicker/mockHooks.ts b/src/components/TokenPicker/mockHooks.ts index a37b11419..2b23535a0 100644 --- a/src/components/TokenPicker/mockHooks.ts +++ b/src/components/TokenPicker/mockHooks.ts @@ -31,13 +31,13 @@ const tokens: Array = [ logo: null, symbol: 'Dai', name: 'Dai Stablecoin', - address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', + address: '0x0002', }, { logo: null, symbol: 'USDC', name: 'USDCoin', - address: '0x5FbDB2315678afecb367f032d93F642f64180aa3', + address: '0x0003', }, { logo: null, symbol: 'USDT', name: 'Tether USD', address: '0x0004' }, { logo: null, symbol: 'WBTC', name: 'Wrapped BTC', address: '0x0005' }, diff --git a/src/lib/web3/indexerProvider.tsx b/src/lib/web3/indexerProvider.tsx new file mode 100644 index 000000000..dcc94ff1d --- /dev/null +++ b/src/lib/web3/indexerProvider.tsx @@ -0,0 +1,235 @@ +import { useContext, createContext, useState, useEffect } from 'react'; +import { + EventType, + createSubscriptionManager, + MessageActionEvent, +} from './events'; +import { BigNumber } from 'bignumber.js'; + +const { REACT_APP__REST_API, REACT_APP__WEBSOCKET_URL } = process.env; + +type TokenAddress = string; // a valid hex address, eg. 0x01 +type BigNumberString = string; // a number in string format, eg. "1" + +if (!REACT_APP__WEBSOCKET_URL) + throw new Error('Invalid value for env variable REACT_APP__WEBSOCKET_URL'); +const subscriber = createSubscriptionManager(REACT_APP__WEBSOCKET_URL); + +export interface PairInfo { + token0: string; + token1: string; + ticks: { [tickID: string]: TickInfo }; +} + +export interface TickInfo { + price0: BigNumber; + price1: BigNumber; + reserves0: BigNumber; + reserves1: BigNumber; + fee: BigNumber; +} + +export interface PairMap { + [pairID: string]: PairInfo; +} + +interface IndexerContextType { + data?: PairMap; + error?: string; + isValidating: boolean; +} + +const IndexerContext = createContext({ + isValidating: true, +}); + +function getFullData(): Promise { + return new Promise(function (resolve, reject) { + if (!REACT_APP__REST_API) { + reject(new Error('Undefined rest api base URL')); + } else { + fetch(`${REACT_APP__REST_API}/duality/duality/ticks`) + .then((res) => res.json()) + .then(transformData) + .then(resolve) + .catch(reject); + } + }); +} + +/** + * Gets the pair id for a sorted pair of tokens + * @param token0 address of token 0 + * @param token1 address of token 1 + * @returns pair id for tokens + */ +function getPairID(token0: TokenAddress, token1: TokenAddress) { + return `${token0}-${token1}`; +} + +/** + * Gets the tick id + * @param price0 price of token 0 + * @param price1 price of token 1 + * @param fee tick's fee + * @returns tick id + */ +function getTickID( + price0: BigNumberString, + price1: BigNumberString, + fee: BigNumberString +) { + return `${price0}-${price1}-${fee}`; +} + +function transformData(data: { + tick: Array<{ + token0: TokenAddress; + token1: TokenAddress; + price0: string; + price1: string; + fee: string; + reserves0: string; + reserves1: string; + }>; +}): PairMap { + return data.tick.reduce(function ( + result, + { token0, token1, price0, price1, fee, reserves0, reserves1 } + ) { + const pairID = getPairID(token0, token1); + const tickID = getTickID(price0, price1, fee); + result[pairID] = result[pairID] || { + token0: token0, + token1: token1, + ticks: {}, + }; + result[pairID].ticks[tickID] = { + price0: new BigNumber(price0), + price1: new BigNumber(price1), + reserves0: new BigNumber(reserves0), + reserves1: new BigNumber(reserves1), + fee: new BigNumber(fee), + }; + return result; + }, + {}); +} + +export function IndexerProvider({ children }: { children: React.ReactNode }) { + const [indexerData, setIndexerData] = useState(); + const [error, setError] = useState(); + // avoid sending more than once + const [, setRequestedFlag] = useState(false); + const [result, setResult] = useState({ + data: indexerData, + error: error, + isValidating: true, + }); + + useEffect(() => { + const onTickChange = function (event: MessageActionEvent) { + const { + Token0, + Token1, + NewReserves0, + NewReserves1, + Price0, + Price1, + Fee, + } = event; + if ( + !Token0 || + !Token1 || + !Price0 || + !Price1 || + !NewReserves0 || + !NewReserves1 || + !Fee + ) { + setError('Invalid event response from server'); + return; + } else { + setError(undefined); + } + const pairID = getPairID(Token0, Token1); + const tickID = getTickID(Price0, Price1, Fee); + setIndexerData((oldData = {}) => { + const oldPairInfo = oldData[pairID]; + const oldTickInfo = oldPairInfo?.ticks?.[tickID]; + return { + ...oldData, + [pairID]: { + ...oldPairInfo, // not needed, displayed for consistency + token0: Token0, + token1: Token1, + ticks: { + ...oldPairInfo?.ticks, + [tickID]: { + ...oldTickInfo, // not needed, displayed for consistency + price0: new BigNumber(Price0), + price1: new BigNumber(Price1), + fee: new BigNumber(Fee), + reserves0: new BigNumber(NewReserves0), + reserves1: new BigNumber(NewReserves1), + }, + }, + }, + }; + }); + }; + subscriber.subscribeMessage(onTickChange, EventType.EventTxValue, { + messageAction: 'NewDeposit', + }); + subscriber.subscribeMessage(onTickChange, EventType.EventTxValue, { + messageAction: 'NewWithdraw', + }); + return () => { + subscriber.unsubscribeMessage(onTickChange); + }; + }, []); + + useEffect(() => { + setResult({ + data: indexerData, + error: error, + isValidating: !indexerData && !error, + }); + }, [indexerData, error]); + + useEffect(() => { + setRequestedFlag((oldValue) => { + if (oldValue) return true; + getFullData() + .then(function (res) { + setIndexerData(res); + }) + .catch(function (err: Error) { + setError(err?.message ?? 'Unknown Error'); + }); + return true; + }); + }, []); + + return ( + {children} + ); +} + +export function useIndexerData() { + return useContext(IndexerContext); +} + +export function useIndexerPairData( + tokenA?: TokenAddress, + tokenB?: TokenAddress +) { + const { data: pairs, isValidating, error } = useIndexerData(); + const [token0, token1] = [tokenA, tokenB].sort(); + const pairID = token0 && token1 && getPairID(token0, token1); + return { + data: pairID && pairs?.[pairID], + error, + isValidating, + }; +} diff --git a/src/pages/App/App.tsx b/src/pages/App/App.tsx index 7d9938153..641618849 100644 --- a/src/pages/App/App.tsx +++ b/src/pages/App/App.tsx @@ -1,4 +1,5 @@ import { Web3Provider } from '../../lib/web3/useWeb3'; +import { IndexerProvider } from '../../lib/web3/indexerProvider'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; @@ -11,17 +12,19 @@ import './App.scss'; function App() { return ( - -
-
- - Home} /> - } /> - } /> - Not found} /> - -
- + + +
+
+ + Home} /> + } /> + } /> + Not found} /> + +
+ + ); } diff --git a/src/pages/Swap/Swap.tsx b/src/pages/Swap/Swap.tsx index ab6a1c099..79cb9f639 100644 --- a/src/pages/Swap/Swap.tsx +++ b/src/pages/Swap/Swap.tsx @@ -9,7 +9,7 @@ import { import { PairRequest } from './hooks/index'; -import { useIndexer } from './hooks/useIndexer'; +import { useRouter } from './hooks/useRouter'; import { useSwap } from './hooks/useSwap'; import './Swap.scss'; @@ -25,10 +25,11 @@ export default function Swap() { data: rateData, isValidating: isValidatingRate, error: rateError, - } = useIndexer({ - token0: lastUpdatedA ? tokenA?.address : tokenB?.address, - token1: lastUpdatedA ? tokenB?.address : tokenA?.address, - value0: lastUpdatedA ? valueA : valueB, + } = useRouter({ + tokenA: tokenA?.address, + tokenB: tokenB?.address, + valueA: lastUpdatedA ? valueA : undefined, + valueB: lastUpdatedA ? undefined : valueB, }); const [swapRequest, setSwapRequest] = useState(); const { @@ -38,8 +39,8 @@ export default function Swap() { } = useSwap(swapRequest); const dotCount = useDotCounter(0.25e3); - const valueAConverted = lastUpdatedA ? valueA : rateData?.value1; - const valueBConverted = lastUpdatedA ? rateData?.value1 : valueB; + const valueAConverted = lastUpdatedA ? valueA : rateData?.valueA; + const valueBConverted = lastUpdatedA ? rateData?.valueB : valueB; const swapTokens = useCallback( function () { @@ -47,7 +48,7 @@ export default function Swap() { setTokenB(tokenA); setValueA(valueBConverted); setValueB(valueAConverted); - setLastUpdatedA((a) => !a); + setLastUpdatedA((flag) => !flag); }, [tokenA, tokenB, valueAConverted, valueBConverted] ); @@ -56,12 +57,13 @@ export default function Swap() { function (event?: React.FormEvent) { if (event) event.preventDefault(); setSwapRequest({ - token0: tokenA?.address, - token1: tokenB?.address, - value0: valueAConverted, + tokenA: tokenA?.address, + tokenB: tokenB?.address, + valueA: lastUpdatedA ? valueA : undefined, + valueB: lastUpdatedA ? undefined : valueB, }); }, - [tokenA?.address, tokenB?.address, valueAConverted] + [tokenA?.address, tokenB?.address, valueA, valueB, lastUpdatedA] ); const onValueAChanged = useCallback((newValue: string) => { @@ -120,7 +122,7 @@ export default function Swap() {
{rateError}
{!isValidatingSwap && swapResponse - ? `Traded ${swapResponse.value0} ${swapResponse.token0} to ${swapResponse.value1} ${swapResponse.token1}` + ? `Traded ${swapResponse.valueA} ${swapResponse.tokenA} to ${swapResponse.valueB} ${swapResponse.tokenB}` : ''}
; + prices0: Array>; + prices1: Array>; + fees: Array>; + reserves0: Array>; + reserves1: Array>; +} diff --git a/src/pages/Swap/hooks/router.ts b/src/pages/Swap/hooks/router.ts new file mode 100644 index 000000000..f48ccd6b4 --- /dev/null +++ b/src/pages/Swap/hooks/router.ts @@ -0,0 +1,98 @@ +import { PairMap } from '../../../lib/web3/indexerProvider'; +import { RouterResult } from './index'; +import { BigNumber } from 'bignumber.js'; + +// mock implementation of router (no hop) +export function router( + state: PairMap, + tokenA: string, + tokenB: string, + value0: string +): RouterResult { + const [token0, token1] = [tokenA, tokenB].sort(); + const exactPair = Object.values(state).find( + (pairInfo) => pairInfo.token0 === token0 && pairInfo.token1 === token1 + ); + if (!exactPair) { + throw new Error('There are no ticks for the supplied token pair'); + } else { + const sortMultiplier = token1 === tokenA ? -1 : 1; + const sortedTicks = Object.values(exactPair.ticks).sort( + (tick0, tick1) => + sortMultiplier * + tick0.price0 + .dividedBy(tick0.price1) + .comparedTo(tick1.price0.dividedBy(tick1.price1)) + ); + return { + amountIn: new BigNumber(value0), + tokens: [tokenA, tokenB], + prices0: [sortedTicks.map((tickInfo) => tickInfo.price0)], // price + prices1: [sortedTicks.map((tickInfo) => tickInfo.price1)], + fees: [sortedTicks.map((tickInfo) => tickInfo.fee)], + reserves0: [sortedTicks.map((tickInfo) => tickInfo.reserves0)], // reserves + reserves1: [sortedTicks.map((tickInfo) => tickInfo.reserves1)], + }; + } +} + +export async function routerAsync( + state: PairMap, + token0: string, + token1: string, + value0: string +): Promise { + return await router(state, token0, token1, value0); +} + +/** + * Calculates the amountOut using the (amountIn * price0) / (price1) formula + * for each tick, until the amountIn amount has been covered + * @param data the RouteInput struct + * @returns estimated value for amountOut + */ +export function calculateOut(data: RouterResult): BigNumber { + let amountLeft = data.amountIn; + let amountOut = new BigNumber(0); + for (let pairIndex = 0; pairIndex < data.tokens.length - 1; pairIndex++) { + const tokens = [data.tokens[pairIndex], data.tokens[pairIndex + 1]].sort(); + for ( + let tickIndex = 0; + tickIndex < data.prices0[pairIndex].length; + tickIndex++ + ) { + const isSameOrder = tokens[0] === data.tokens[pairIndex]; + const priceIn = isSameOrder + ? data.prices0[pairIndex][tickIndex] + : data.prices1[pairIndex][tickIndex]; + const priceOut = isSameOrder + ? data.prices1[pairIndex][tickIndex] + : data.prices0[pairIndex][tickIndex]; + const reservesOut = isSameOrder + ? data.reserves1[pairIndex][tickIndex] + : data.reserves0[pairIndex][tickIndex]; + const maxOut = amountLeft.multipliedBy(priceIn).dividedBy(priceOut); + + if (reservesOut.isLessThan(maxOut)) { + const amountInTraded = reservesOut + .multipliedBy(priceOut) + .dividedBy(priceIn); + amountLeft = amountLeft.minus(amountInTraded); + amountOut = amountOut.plus(reservesOut); + if (amountLeft.isEqualTo(0)) return amountOut; + if (amountLeft.isLessThan(0)) + throw new Error( + 'Error while calculating amount out (negative amount)' + ); + } else { + return amountOut.plus(maxOut); + } + } + } + return amountOut; +} + +// mock implementation of fee calculation +export function calculateFee(data: RouterResult): BigNumber { + return data.fees[0][0]; +} diff --git a/src/pages/Swap/hooks/useIndexer.ts b/src/pages/Swap/hooks/useIndexer.ts deleted file mode 100644 index 6d9dd6efc..000000000 --- a/src/pages/Swap/hooks/useIndexer.ts +++ /dev/null @@ -1,197 +0,0 @@ -import getContract, { Contract } from '../../../lib/web3/getContract'; -import { useWeb3 } from '../../../lib/web3/useWeb3'; -import { useCallback, useEffect, useState } from 'react'; -import { PairRequest, PairResult } from './index'; -import { utils, BigNumber } from 'ethers'; -import data from './data.json'; - -const cachedRequests: { - [token0: string]: { [token1: string]: PairResult }; -} = {}; - -const pairMap = data.reduce<{ [pairID: string]: typeof data[0] }>(function ( - result, - pairInfo -) { - const list = [pairInfo.token0, pairInfo.token1].sort(); - const pairID = utils.solidityKeccak256(['address[2]'], [list]); - result[pairID] = pairInfo; - return result; -}, -{}); - -function useEvents() { - const { provider } = useWeb3(); - const contract = provider - ? getContract(Contract.DUALITY_CORE, provider) - : null; - useEffect(() => { - contract?.on( - 'Swap0to1', - function ( - pairID: string, - [price0, price1]: Array, - amount: BigNumber - ) { - const pair = pairMap[pairID]; - if (!pair) return; - pair.ticks[0].reserves0 -= Number(amount.toBigInt()); - pair.ticks[0].reserves1 += Number( - (amount.toBigInt() * price0.toBigInt()) / price1.toBigInt() - ); - } - ); - contract?.on( - 'Swap1to0', - function (pairID, [price0, price1]: Array, amount: BigNumber) { - const pair = pairMap[pairID]; - if (!pair) return; - pair.ticks[0].reserves0 += Number( - (amount.toBigInt() * price1.toBigInt()) / price0.toBigInt() - ); - pair.ticks[0].reserves1 -= Number(amount.toBigInt()); - } - ); - - return () => { - contract?.removeAllListeners(); - }; - }, [contract]); -} - -function fetchEstimates({ - token0, - token1, - value0, -}: PairRequest): Promise { - const averageDelay = 1e3, - delayDiff = 0.5e3; - return new Promise(function (resolve, reject) { - if (!token0 || !token1 || !value0) - return reject(new Error('Invalid Input')); - - setTimeout(function () { - const sortedList = [token0, token1].sort(); - const pairInfo = data.find( - (pairInfo) => - pairInfo.token0 === sortedList[0] && pairInfo.token1 === sortedList[1] - ); - if (!pairInfo) return reject(new Error('Insufficient data')); - const totalReserves = pairInfo.ticks.reduce( - function (total, tick) { - total.value0 += tick.reserves0; - total.value1 += tick.reserves1; - return total; - }, - { value0: 0, value1: 0 } - ); - const rate = - token0 === sortedList[0] - ? (totalReserves.value1 - Number(value0)) / totalReserves.value0 - : (totalReserves.value0 - Number(value0)) / totalReserves.value1; - const safeRate = Math.max(rate, 0); - const value1 = (Number(value0) * safeRate).toLocaleString('en-US', { - maximumSignificantDigits: 6, - useGrouping: false, - }); - - resolve({ - token0, - token1, - rate: `${safeRate}`, - value0, - value1, - gas: '5', - }); - }, Math.random() * delayDiff * 2 + (averageDelay - delayDiff)); - }); -} - -/** - * Gets the estimated info of a swap transaction - * @param pairRequest the respective addresses and value - * @returns estimated info of swap, loading state and possible error - */ -export function useIndexer(pairRequest: PairRequest): { - data?: PairResult; - isValidating: boolean; - error?: string; -} { - const [data, setData] = useState(); - const [isValidating, setIsValidating] = useState(false); - const [error, setError] = useState(); - useEvents(); - - const setSwappedResult = useCallback( - (result: PairResult, originalToken0: string) => { - if (result.token0 === originalToken0) return setData(result); - const { token0, token1, value0, value1 } = result; - setData({ - ...result, - token0: token1, - token1: token0, - value0: value1, - value1: value0, - }); - }, - [] - ); - - useEffect(() => { - if (!pairRequest.token0 || !pairRequest.token1 || !pairRequest.value0) - return; - setIsValidating(true); - setData(undefined); - setError(undefined); - const [token0, token1] = [pairRequest.token0, pairRequest.token1].sort(); - const originalToken0 = pairRequest.token0; - let cancelled = false; - cachedRequests[token0] = cachedRequests[token0] || {}; - const cachedPairInfo = cachedRequests[token0][token1]; - if (cachedPairInfo) { - if (originalToken0 === cachedPairInfo.token0) { - const tempValue1 = - Number(pairRequest.value0) * Number(cachedPairInfo.rate); - setSwappedResult( - { ...cachedPairInfo, value1: `${tempValue1}` }, - originalToken0 - ); - } else { - const tempValue1 = - Number(pairRequest.value0) / Number(cachedPairInfo.rate); - setSwappedResult( - { ...cachedPairInfo, value0: `${tempValue1}` }, - originalToken0 - ); - } - } - - fetchEstimates({ - token0: pairRequest.token0, - token1: pairRequest.token1, - value0: pairRequest.value0, - }) - .then(function (result) { - if (cancelled) return; - cachedRequests[token0][token1] = result; - setIsValidating(false); - setSwappedResult(result, originalToken0); - }) - .catch(function (err: Error) { - if (cancelled) return; - setIsValidating(false); - setError(err?.message ?? 'Unknown error'); - }); - - return () => { - cancelled = true; - }; - }, [ - pairRequest.token0, - pairRequest.token1, - pairRequest.value0, - setSwappedResult, - ]); - - return { data, isValidating, error }; -} diff --git a/src/pages/Swap/hooks/useRouter.ts b/src/pages/Swap/hooks/useRouter.ts new file mode 100644 index 000000000..f4ba8f64e --- /dev/null +++ b/src/pages/Swap/hooks/useRouter.ts @@ -0,0 +1,143 @@ +import { useIndexerData, PairMap } from '../../../lib/web3/indexerProvider'; +import { useEffect, useState } from 'react'; +import { PairRequest, PairResult } from './index'; +import { routerAsync, calculateOut, calculateFee } from './router'; +import BigNumber from 'bignumber.js'; + +const cachedRequests: { + [token0: string]: { [token1: string]: PairResult }; +} = {}; + +async function fetchEstimates( + state: PairMap, + tokenA: string, + tokenB: string, + alteredValue: string, + reverseSwap: boolean +): Promise { + if (reverseSwap) { + // The router can't calculate the value of the buying token based on the value of the selling token (yet) + throw new Error('Cannot calculate the reverse value'); + } else { + const result = await routerAsync(state, tokenA, tokenB, alteredValue); + const valueB = calculateOut(result); + const rate = result.amountIn.dividedBy(valueB); + const extraFee = calculateFee(result); + return { + tokenA, + tokenB, + rate: rate.toString(), + valueA: alteredValue, + valueB: valueB.toString(), + gas: extraFee.toString(), + }; + } +} + +/** + * Gets the estimated info of a swap transaction + * @param pairRequest the respective addresses and value + * @returns estimated info of swap, loading state and possible error + */ +export function useRouter(pairRequest: PairRequest): { + data?: PairResult; + isValidating: boolean; + error?: string; +} { + const [data, setData] = useState(); + const [isValidating, setIsValidating] = useState(false); + const [error, setError] = useState(); + const { data: pairs } = useIndexerData(); + + useEffect(() => { + if ( + !pairRequest.tokenA || + !pairRequest.tokenB || + (!pairRequest.valueA && !pairRequest.valueB) || + !pairs + ) { + return; + } + if (pairRequest.tokenA === pairRequest.tokenB) { + setData(undefined); + setError('The tokens cannot be the same'); + return; + } + if (pairRequest.valueA && pairRequest.valueB) { + setData(undefined); + setError('One value must be falsy'); + return; + } + setIsValidating(true); + setData(undefined); + setError(undefined); + const alteredValue = pairRequest.valueA ?? pairRequest.valueB; + const reverseSwap = !!pairRequest.valueB; + if (!alteredValue || alteredValue === '0') { + setIsValidating(false); + setData({ + valueA: '0', + valueB: '0', + rate: '0', + gas: '0', + tokenA: pairRequest.tokenA, + tokenB: pairRequest.tokenB, + }); + return; + } + const [token0, token1] = [pairRequest.tokenA, pairRequest.tokenB].sort(); + let cancelled = false; + cachedRequests[token0] = cachedRequests[token0] || {}; + const cachedPairInfo = cachedRequests[token0][token1]; + if (cachedPairInfo) { + const { rate, gas } = cachedPairInfo; + const convertedRate = + pairRequest.tokenA === cachedPairInfo.tokenA + ? new BigNumber(rate) + : new BigNumber(1).dividedBy(rate); + const roughEstimate = new BigNumber(alteredValue) + .multipliedBy(convertedRate) + .toString(); + setData({ + tokenA: pairRequest.tokenA, + tokenB: pairRequest.tokenB, + rate: convertedRate.toString(), + valueA: reverseSwap ? roughEstimate : alteredValue, + valueB: reverseSwap ? alteredValue : roughEstimate, + gas, + }); + } + + fetchEstimates( + pairs, + pairRequest.tokenA, + pairRequest.tokenB, + alteredValue, + reverseSwap + ) + .then(function (result) { + if (cancelled) return; + cachedRequests[token0][token1] = result; + setIsValidating(false); + setData(result); + }) + .catch(function (err: Error) { + if (cancelled) return; + setIsValidating(false); + setError(err?.message ?? 'Unknown error'); + setData(undefined); + }); + + return () => { + cancelled = true; + }; + }, [ + pairRequest.tokenA, + pairRequest.tokenB, + pairRequest.valueA, + pairRequest.valueB, + pairs, + ]); + + return { data, isValidating, error }; +} diff --git a/src/pages/Swap/hooks/useSwap.ts b/src/pages/Swap/hooks/useSwap.ts index 6ca5db16f..f8b0f52b6 100644 --- a/src/pages/Swap/hooks/useSwap.ts +++ b/src/pages/Swap/hooks/useSwap.ts @@ -1,43 +1,16 @@ -import getContract, { Contract } from '../../../lib/web3/getContract'; -import { useWeb3 } from '../../../lib/web3/useWeb3'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; import { PairRequest, PairResult } from './index'; -import { ethers, utils, BigNumber } from 'ethers'; -function sendSwap( - provider: ethers.providers.Web3Provider, - { token0, token1, value0 }: PairRequest -): Promise { +function sendSwap({ + tokenA, + tokenB, + valueA, + valueB, +}: PairRequest): Promise { return new Promise(function (resolve, reject) { - if (!token0 || !token1 || !value0) + if (!tokenA || !tokenB || (!valueA && !valueB)) return reject(new Error('Invalid Input')); - const contract = getContract(Contract.DUALITY_CORE, provider); - contract - .connect(provider.getSigner()) - .route({ - amountIn: utils.parseEther(`${value0}`), - tokens: [token0, token1], - prices0: [[100]], - prices1: [[100]], - fee: [[10000]], - jitProtectionArr: [[false]], - useInternalAccounts: false, - permitData: [], - }) - .then(function (res?: { gasPrice: BigNumber }) { - if (!res) return reject('No response'); - resolve({ - token0: token0, - token1: token1, - value0: value0, - value1: '??', - rate: '??', - gas: res.gasPrice.toString(), - }); - }) - .catch(function (err: Error) { - reject(err); - }); + reject('Not yet implemented'); }); } @@ -54,33 +27,19 @@ export function useSwap(request?: PairRequest): { const [data, setData] = useState(); const [validating, setValidating] = useState(false); const [error, setError] = useState(); - const providerRef = useRef(); - const { provider } = useWeb3(); - - useEffect(() => { - providerRef.current = provider ?? undefined; - }, [provider]); useEffect(() => { if (!request) return onError('Missing Tokens and value'); - if (!providerRef.current) return onError('Missing Provider'); - const { token0, token1, value0 } = request; - if (!token0 || !token1) return onError('Missing token pair'); - if (!value0) return onError('Missing value'); + const { tokenA, tokenB, valueA, valueB } = request; + if (!tokenA || !tokenB) return onError('Missing token pair'); + if (!valueA && !valueB) return onError('Missing value'); setValidating(true); setError(undefined); setData(undefined); - sendSwap(providerRef.current, request) + sendSwap(request) .then(function (result: PairResult) { setValidating(false); - setData({ - token0: token0, - token1: token1, - value0: value0, - value1: result.value1, - rate: result.rate, - gas: result.gas, - }); + setData(result); }) .catch(function (err: Error) { onError(err?.message ?? 'Unknown error');