From 1727edf75a832d0a4f9971c8111dd45994900ba9 Mon Sep 17 00:00:00 2001 From: maro Date: Thu, 21 Sep 2023 18:09:04 +0900 Subject: [PATCH] fix: validates queries from auto router --- src/forms/SwapForm.tsx | 100 ++----------- src/rest/useAPI.ts | 6 +- src/rest/useAutoRouter.ts | 286 +++++++++++++++++++++++++++++--------- 3 files changed, 239 insertions(+), 153 deletions(-) diff --git a/src/forms/SwapForm.tsx b/src/forms/SwapForm.tsx index 63b68842..7a55fd33 100644 --- a/src/forms/SwapForm.tsx +++ b/src/forms/SwapForm.tsx @@ -99,7 +99,7 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { const tokenInfos = useTokenInfos() const lpTokenInfos = useLpTokenInfos() - const { getSymbol, isNativeToken } = useContractsAddress() + const { getSymbol } = useContractsAddress() const { loadTaxInfo, loadTaxRate, generateContractMessages } = useAPI() const { fee } = useNetwork() const { find } = useContract() @@ -350,7 +350,8 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { if ( gte(spread, "0.01") && !warningModal.isOpen && - !isWarningModalConfirmed + !isWarningModalConfirmed && + !isAutoRouterLoading ) { warningModal.setInfo("", percent(spread)) warningModal.open() @@ -361,7 +362,7 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { return () => { clearTimeout(timerId) } - }, [isWarningModalConfirmed, spread, warningModal]) + }, [isAutoRouterLoading, isWarningModalConfirmed, spread, warningModal]) const simulationContents = useMemo(() => { if ( @@ -510,63 +511,6 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { tokenInfos, ]) - const getMsgs = useCallback( - ( - _msg: any, - { - amount, - token, - minimumReceived, - beliefPrice, - }: { - amount?: string | number - token?: string - minimumReceived?: string | number - beliefPrice?: string | number - } - ) => { - const msg = Array.isArray(_msg) ? _msg[0] : _msg - - if (msg?.execute_msg?.swap) { - msg.execute_msg.swap.belief_price = `${beliefPrice}` - } - if (msg?.execute_msg?.send?.msg?.swap) { - msg.execute_msg.send.msg.swap.belief_price = `${beliefPrice}` - } - if (msg?.execute_msg?.send?.msg?.execute_swap_operations) { - msg.execute_msg.send.msg.execute_swap_operations.minimum_receive = - parseInt(`${minimumReceived}`, 10).toString() - if (isNativeToken(token || "")) { - msg.coins = Coins.fromString(toAmount(`${amount}`) + token) - } - - msg.execute_msg.send.msg = btoa( - JSON.stringify(msg.execute_msg.send.msg) - ) - } else if (msg?.execute_msg?.send?.msg) { - msg.execute_msg.send.msg = btoa( - JSON.stringify(msg.execute_msg.send.msg) - ) - } - if (msg?.execute_msg?.execute_swap_operations) { - msg.execute_msg.execute_swap_operations.minimum_receive = parseInt( - `${minimumReceived}`, - 10 - ).toString() - msg.execute_msg.execute_swap_operations.offer_amount = toAmount( - `${amount}`, - token - ) - - if (isNativeToken(token || "")) { - msg.coins = Coins.fromString(toAmount(`${amount}`) + token) - } - } - return [msg] - }, - [isNativeToken] - ) - const { gasPrice } = useGasPrice(formData[Key.feeSymbol]) const getTax = useCallback( async ({ @@ -865,19 +809,7 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { if (!profitableQuery?.msg) { return } - msgs = getMsgs(profitableQuery?.msg, { - amount: `${value1}`, - minimumReceived: profitableQuery - ? calc.minimumReceived({ - expectedAmount: `${profitableQuery?.simulatedAmount}`, - max_spread: String(slippageTolerance), - commission: find(infoKey, formData[Key.symbol2]), - decimals: tokenInfo1?.decimals, - }) - : "0", - token: from, - beliefPrice: `${decimal(div(value1, value2), 18)}`, - }) + msgs = profitableQuery?.msg } else { msgs = await generateContractMessages( { @@ -929,17 +861,12 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { txOptions ) - let fee = signMsg.auth_info.fee.amount.add(tax) - - txOptions.fee = new Fee(signMsg.auth_info.fee.gas_limit, fee) - setValue( - Key.feeValue, - txOptions.fee?.amount.get(feeAddress)?.amount.toString() || "" - ) + const fee = signMsg.auth_info.fee.amount.add(tax) const extensionResult = await wallet.post( { ...txOptions, + fee: new Fee(signMsg.auth_info.fee.gas_limit, fee), }, walletAddress ) @@ -959,19 +886,18 @@ const SwapForm = ({ type, tabs }: { type: Type; tabs: TabViewProps }) => { getSymbol, terra.tx, walletAddress, + tax, wallet, profitableQuery, - getMsgs, - slippageTolerance, - find, - formData, - tokenInfo1, - from, - tokenInfo2, generateContractMessages, + from, to, + slippageTolerance, + txDeadlineMinute, lpContract, poolResult?.estimated, + poolContract2, + poolContract1, ] ) diff --git a/src/rest/useAPI.ts b/src/rest/useAPI.ts index b0cb6c8d..66915f34 100644 --- a/src/rest/useAPI.ts +++ b/src/rest/useAPI.ts @@ -316,11 +316,11 @@ const useAPI = (version: ApiVersion = "v2") => { // useSwapSimulate const querySimulate = useCallback( - async (variables: { contract: string; msg: any }) => { + async (variables: { contract: string; msg: any; timeout?: number }) => { try { - const { contract, msg } = variables + const { contract, msg, timeout } = variables const url = getURL(contract, msg) - const res: SimulatedResponse = (await axios.get(url)).data + const res: SimulatedResponse = (await axios.get(url, { timeout })).data return res.data } catch (error) { const { response }: AxiosError = error as any diff --git a/src/rest/useAutoRouter.ts b/src/rest/useAutoRouter.ts index c3214472..4f5f9449 100644 --- a/src/rest/useAutoRouter.ts +++ b/src/rest/useAutoRouter.ts @@ -1,11 +1,15 @@ -import { MsgExecuteContract } from "@terra-money/terra.js" -import { useAddress } from "hooks" +import { Coins, MsgExecuteContract } from "@terra-money/terra.js" +import { useAddress, useContract } from "hooks" import { div, times } from "libs/math" -import { toAmount } from "libs/parse" +import { decimal, toAmount } from "libs/parse" import { Type } from "pages/Swap" -import { useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" import useAPI from "./useAPI" -import { tokenInfos } from "./usePairs" +import { useTokenInfos } from "./usePairs" +import { useLCDClient } from "layouts/WalletConnectProvider" +import { useContractsAddress } from "hooks/useContractsAddress" +import calc from "helpers/calc" +import { AssetInfoKey } from "hooks/contractKeys" type Params = { from: string @@ -21,7 +25,6 @@ function sleep(t: number) { } const useAutoRouter = (params: Params) => { - const walletAddress = useAddress() const { from, to, @@ -30,25 +33,95 @@ const useAutoRouter = (params: Params) => { slippageTolerance, deadline, } = params + const walletAddress = useAddress() + const { terra } = useLCDClient() const amount = Number(_amount) const { generateContractMessages, querySimulate } = useAPI() - const [isLoading, setIsLoading] = useState(false) + const [isSimulationLoading, setIsSimulationLoading] = useState(false) + const [isQueryValidationLoading, setIsQueryValidationLoading] = + useState(false) + const isLoading = isSimulationLoading || isQueryValidationLoading + const [msgs, setMsgs] = useState< (MsgExecuteContract[] | MsgExecuteContract)[] >([]) const [simulatedAmounts, setSimulatedAmounts] = useState([]) const [autoRefreshTicker, setAutoRefreshTicker] = useState(false) - const profitableQuery = useMemo(() => { - if (!to || !amount) { - return undefined + const { isNativeToken } = useContractsAddress() + const { find } = useContract() + const tokenInfos = useTokenInfos() + + const getMsgs = useCallback( + ( + _msg: any, + { + amount, + token, + minimumReceived, + beliefPrice, + }: { + amount?: string | number + token?: string + minimumReceived?: string | number + beliefPrice?: string | number + } + ) => { + const msg = Array.isArray(_msg) ? _msg[0] : _msg + + if (msg?.execute_msg?.swap) { + msg.execute_msg.swap.belief_price = `${beliefPrice}` + } + if (msg?.execute_msg?.send?.msg?.swap) { + msg.execute_msg.send.msg.swap.belief_price = `${beliefPrice}` + } + if (msg?.execute_msg?.send?.msg?.execute_swap_operations) { + msg.execute_msg.send.msg.execute_swap_operations.minimum_receive = + parseInt(`${minimumReceived}`, 10).toString() + if (isNativeToken(token || "")) { + msg.coins = Coins.fromString(toAmount(`${amount}`, token) + token) + } + + msg.execute_msg.send.msg = btoa( + JSON.stringify(msg.execute_msg.send.msg) + ) + } else if (msg?.execute_msg?.send?.msg) { + msg.execute_msg.send.msg = btoa( + JSON.stringify(msg.execute_msg.send.msg) + ) + } + if (msg?.execute_msg?.execute_swap_operations) { + msg.execute_msg.execute_swap_operations.minimum_receive = parseInt( + `${minimumReceived}`, + 10 + ).toString() + msg.execute_msg.execute_swap_operations.offer_amount = toAmount( + `${amount}`, + token + ) + + if (isNativeToken(token || "")) { + msg.coins = Coins.fromString(toAmount(`${amount}`, token) + token) + } + } + return [msg] + }, + [isNativeToken] + ) + + const queries = useMemo(() => { + if (!to || !amount || !simulatedAmounts?.length) { + return [] } - if (simulatedAmounts?.length > 0) { - const index = simulatedAmounts.indexOf( - Math.max(...simulatedAmounts.map((item) => (!item ? -1 : item))) - ) + + const indexes = simulatedAmounts + .map((value, index) => ({ value, index })) + .sort((a, b) => b.value - a.value) + .map((item) => item.index) + + return indexes.map((index) => { const simulatedAmount = simulatedAmounts[index] if (simulatedAmount < 0) { - return undefined + return null } const msg = msgs[index] const execute_msg = (Array.isArray(msg) ? msg[0] : msg) @@ -91,18 +164,44 @@ const useAutoRouter = (params: Params) => { }) } - const tokenInfo = tokenInfos.get(to) - const e = Math.pow(10, tokenInfo?.decimals || 6) + const tokenInfo1 = tokenInfos.get(from) + const tokenInfo2 = tokenInfos.get(to) + + const minimumReceived = calc.minimumReceived({ + expectedAmount: `${simulatedAmount}`, + max_spread: String(slippageTolerance), + commission: find(AssetInfoKey.COMMISSION, to), + decimals: tokenInfo1?.decimals, + }) + + const e = Math.pow(10, tokenInfo2?.decimals || 6) + + const formattedMsg = getMsgs(msg, { + amount, + minimumReceived, + token: from, + beliefPrice: `${decimal(div(times(amount, e), simulatedAmount), 18)}`, + }) + return { - msg, + msg: formattedMsg, index, simulatedAmount, tokenRoutes, price: div(times(amount, e), simulatedAmount), } - } - return undefined - }, [to, amount, msgs, simulatedAmounts]) + }) + }, [ + to, + amount, + simulatedAmounts, + msgs, + slippageTolerance, + find, + getMsgs, + from, + tokenInfos, + ]) useEffect(() => { let isCanceled = false @@ -128,7 +227,8 @@ const useAutoRouter = (params: Params) => { setMsgs(res) } } - setIsLoading(true) + setIsSimulationLoading(true) + setIsQueryValidationLoading(true) setMsgs([]) setSimulatedAmounts([]) const timerId = setTimeout(() => { @@ -148,6 +248,7 @@ const useAutoRouter = (params: Params) => { autoRefreshTicker, walletAddress, slippageTolerance, + deadline, ]) useEffect(() => { @@ -155,15 +256,15 @@ const useAutoRouter = (params: Params) => { if ( window?.navigator?.onLine && window?.document?.hasFocus() && - !isLoading + !isSimulationLoading ) { setAutoRefreshTicker((current) => !current) } - }, 30000) + }, 60000) return () => { clearInterval(timerId) } - }, [amount, from, to, type, isLoading]) + }, [amount, from, to, type, isSimulationLoading]) useEffect(() => { let isCanceled = false @@ -209,47 +310,51 @@ const useAutoRouter = (params: Params) => { return undefined }) - const result: any[] = [] - simulateQueries.forEach(async (query, index) => { - if (isCanceled) { - return - } - await sleep(100 * index) - const res = await querySimulate({ - contract: `${query?.contract}`, - msg: query?.msg, - }) - if (res) { - result[index] = res - } - if (isCanceled) { - return - } - - if (index >= simulateQueries.length - 1) { - // wait for all query done - for (let i = 0; i < 30; i++) { - if (JSON.parse(JSON.stringify(result)).includes(null)) { - await sleep(100) - } + const promises = simulateQueries.map(async (query, index) => { + try { + if (isCanceled) { + return undefined + } + await sleep(80 * index) + if (isCanceled) { + return undefined + } + const res = await querySimulate({ + contract: `${query?.contract}`, + msg: query?.msg, + timeout: 5000, + }) + if (isCanceled) { + return undefined } - setSimulatedAmounts( - result - .map((item) => { - if (item?.return_amount) { - return parseInt(item?.return_amount, 10) - } - if (item?.amount) { - return parseInt(item?.amount, 10) - } - return -1 - }) - .map((item) => (Number.isNaN(Number(item)) ? -1 : item)) - ) - setIsLoading(false) + return res + } catch (error) { + console.log(error) } + return undefined }) + + const results = await Promise.allSettled(promises) + if (isCanceled) { + return + } + setSimulatedAmounts( + results + .map((item) => { + if (item.status === "fulfilled") { + if (item?.value?.return_amount) { + return parseInt(item?.value?.return_amount, 10) + } + if (item?.value?.amount) { + return parseInt(item?.value?.amount, 10) + } + } + return -1 + }) + .map((item) => (Number.isNaN(Number(item)) ? -1 : item)) + ) + setIsSimulationLoading(false) } setSimulatedAmounts([]) @@ -260,9 +365,64 @@ const useAutoRouter = (params: Params) => { } }, [amount, from, msgs, querySimulate]) + const [profitableQuery, setProfitableQuery] = useState(queries[0]) + + useEffect(() => { + let isCanceled = false + const validateQueries = async () => { + if (!queries?.length) { + return + } + setIsQueryValidationLoading(true) + const account = walletAddress + ? await terra.auth.accountInfo(walletAddress) + : undefined + if (isCanceled) { + return + } + for await (const query of queries) { + try { + if (!account) { + setProfitableQuery(query) + break + } + if (query?.msg) { + await terra.tx.estimateFee( + [ + { + sequenceNumber: account.getSequenceNumber(), + publicKey: account.getPublicKey(), + }, + ], + { + msgs: query?.msg, + memo: undefined, + } + ) + if (isCanceled) { + return + } + setProfitableQuery(query) + break + } + } catch (error) { + console.log(error) + } + } + setIsQueryValidationLoading(false) + } + const timerId = setTimeout(() => { + validateQueries() + }, 300) + return () => { + isCanceled = true + clearTimeout(timerId) + } + }, [queries, terra, walletAddress]) + const result = useMemo(() => { if (!from || !to || !type || !amount) { - return { profitableQuery: undefined, isLoading: false } + return { profitableQuery: undefined, isLoading } } return { isLoading,