From de06be03e34503c16090b1bd5d8ad300d36dd0de Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 21 Aug 2023 08:45:50 +1000 Subject: [PATCH 1/2] chore: add error handling to thorchain swaper route generation --- src/lib/swapper/api.ts | 9 +- .../evm/utils/getThorTxData.ts | 32 +-- .../getThorTradeQuote/getTradeQuote.ts | 209 ++++++++++-------- 3 files changed, 130 insertions(+), 120 deletions(-) diff --git a/src/lib/swapper/api.ts b/src/lib/swapper/api.ts index c67bb18802a..472bb620fcb 100644 --- a/src/lib/swapper/api.ts +++ b/src/lib/swapper/api.ts @@ -221,6 +221,10 @@ export type CheckTradeStatusInput = { getState: () => ReduxState } +// a result containing all routes that were successfully generated, or an error in the case where +// no routes could be generated +type TradeQuoteResult = Result + export type Swapper2 = { filterAssetIdsBySellable: (assets: Asset[]) => Promise filterBuyAssetsBySellAssetId: (input: BuyAssetBySellIdInput) => Promise @@ -231,9 +235,6 @@ export type Swapper2Api = { checkTradeStatus: ( input: CheckTradeStatusInput, ) => Promise<{ status: TxStatus; buyTxHash: string | undefined; message: string | undefined }> - getTradeQuote: ( - input: GetTradeQuoteInput, - deps: TradeQuoteDeps, - ) => Promise> + getTradeQuote: (input: GetTradeQuoteInput, deps: TradeQuoteDeps) => Promise getUnsignedTx(input: GetUnsignedTxArgs): Promise } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts b/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts index 24cce901ce7..6bf2aa97e10 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts @@ -1,10 +1,6 @@ import { fromAssetId } from '@shapeshiftoss/caip' -import type { Result } from '@sniptt/monads' -import { Err, Ok } from '@sniptt/monads' import { getConfig } from 'config' import type { Asset } from 'lib/asset-service' -import type { SwapErrorRight } from 'lib/swapper/api' -import { makeSwapErrorRight, SwapErrorType } from 'lib/swapper/api' import { deposit } from 'lib/swapper/swappers/ThorchainSwapper/evm/routerCalldata' import { getInboundAddressDataForChain } from 'lib/swapper/swappers/ThorchainSwapper/utils/getInboundAddressDataForChain' import { isNativeEvmAsset } from 'lib/swapper/swappers/utils/helpers/helpers' @@ -15,15 +11,10 @@ type GetEvmThorTxInfoArgs = { memo: string } -type GetEvmThorTxInfoReturn = Promise< - Result< - { - data: string - router: string - }, - SwapErrorRight - > -> +type GetEvmThorTxInfoReturn = Promise<{ + data: string + router: string +}> export const getThorTxInfo = async ({ sellAsset, @@ -34,20 +25,15 @@ export const getThorTxInfo = async ({ const { assetReference } = fromAssetId(sellAsset.assetId) const maybeInboundAddress = await getInboundAddressDataForChain(daemonUrl, sellAsset.assetId) - if (maybeInboundAddress.isErr()) return Err(maybeInboundAddress.unwrapErr()) + if (maybeInboundAddress.isErr()) throw maybeInboundAddress.unwrapErr() const inboundAddress = maybeInboundAddress.unwrap() const router = inboundAddress.router const vault = inboundAddress.address - if (!router) - return Err( - makeSwapErrorRight({ - message: `[getPriceRatio]: No router found for ${sellAsset.assetId}`, - code: SwapErrorType.RESPONSE_ERROR, - details: { inboundAddress: maybeInboundAddress }, - }), - ) + if (!router) { + throw Error(`No router found for ${sellAsset.assetId} at inbound address ${inboundAddress}`) + } const data = deposit( router, @@ -59,5 +45,5 @@ export const getThorTxInfo = async ({ memo, ) - return Ok({ data, router }) + return { data, router } } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts index 8929a765a13..e7cc7e64030 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts @@ -26,7 +26,7 @@ import { THORCHAIN_FIXED_PRECISION } from 'lib/swapper/swappers/ThorchainSwapper import { getQuote } from 'lib/swapper/swappers/ThorchainSwapper/utils/getQuote/getQuote' import { getUtxoTxFees } from 'lib/swapper/swappers/ThorchainSwapper/utils/txFeeHelpers/utxoTxFees/getUtxoTxFees' import { getThorTxInfo as getUtxoThorTxInfo } from 'lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData' -import { assertUnreachable } from 'lib/utils' +import { assertUnreachable, isFulfilled, isRejected } from 'lib/utils' import { assertGetCosmosSdkChainAdapter } from 'lib/utils/cosmosSdk' import { assertGetEvmChainAdapter } from 'lib/utils/evm' import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' @@ -179,106 +179,129 @@ export const getThorTradeQuote = async ( supportsEIP1559: (input as GetEvmTradeQuoteInput).supportsEIP1559, }) - return Ok( - await Promise.all( - perRouteValues.map( - async ({ source, slippageBps, expectedAmountOutThorBaseUnit, isStreaming }) => { - const rate = getRouteRate(expectedAmountOutThorBaseUnit) - const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount( - expectedAmountOutThorBaseUnit, - ) - - const updatedMemo = addSlippageToMemo(thornodeQuote, inputSlippageBps, isStreaming) - const maybeThorTxInfo = await getEvmThorTxInfo({ - sellAsset, - sellAmountCryptoBaseUnit, - memo: updatedMemo, - }) - - if (maybeThorTxInfo.isErr()) throw maybeThorTxInfo.unwrapErr() - const { data, router } = maybeThorTxInfo.unwrap() - - return { - isStreaming, - recommendedSlippage: convertBasisPointsToDecimalPercentage(slippageBps).toString(), - rate, - data, - router, - steps: [ - { - rate, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - buyAmountBeforeFeesCryptoBaseUnit, - source, - buyAsset, - sellAsset, - accountNumber, - allowanceContract: router, - feeData: { - networkFeeCryptoBaseUnit, - protocolFees, - }, + const maybeRoutes = await Promise.allSettled( + perRouteValues.map( + async ({ source, slippageBps, expectedAmountOutThorBaseUnit, isStreaming }) => { + const rate = getRouteRate(expectedAmountOutThorBaseUnit) + const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount( + expectedAmountOutThorBaseUnit, + ) + + const updatedMemo = addSlippageToMemo(thornodeQuote, inputSlippageBps, isStreaming) + const { data, router } = await getEvmThorTxInfo({ + sellAsset, + sellAmountCryptoBaseUnit, + memo: updatedMemo, + }) + + return { + isStreaming, + recommendedSlippage: convertBasisPointsToDecimalPercentage(slippageBps).toString(), + rate, + data, + router, + steps: [ + { + rate, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + source, + buyAsset, + sellAsset, + accountNumber, + allowanceContract: router, + feeData: { + networkFeeCryptoBaseUnit, + protocolFees, }, - ], - } - }, - ), + }, + ], + } + }, ), ) + + const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value) + + // if no routes succeeded, return failure from swapper + if (!routes.length) + return Err( + makeSwapErrorRight({ + message: 'Unable to create any routes', + code: SwapErrorType.TRADE_QUOTE_FAILED, + cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason), + }), + ) + + // otherwise, return all that succeeded + return Ok(routes) } case CHAIN_NAMESPACE.Utxo: { - return Ok( - await Promise.all( - perRouteValues.map( - async ({ source, slippageBps, expectedAmountOutThorBaseUnit, isStreaming }) => { - const rate = getRouteRate(expectedAmountOutThorBaseUnit) - const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount( - expectedAmountOutThorBaseUnit, - ) - - const updatedMemo = addSlippageToMemo(thornodeQuote, inputSlippageBps, isStreaming) - const maybeThorTxInfo = await getUtxoThorTxInfo({ - sellAsset, - xpub: (input as GetUtxoTradeQuoteInput).xpub, - memo: updatedMemo, - }) - if (maybeThorTxInfo.isErr()) throw maybeThorTxInfo.unwrapErr() - const thorTxInfo = maybeThorTxInfo.unwrap() - const { vault, opReturnData, pubkey } = thorTxInfo - - const sellAdapter = assertGetUtxoChainAdapter(sellAsset.chainId) - const feeData = await getUtxoTxFees({ - sellAmountCryptoBaseUnit, - vault, - opReturnData, - pubkey, - sellAdapter, - protocolFees, - }) - - return { - isStreaming, - recommendedSlippage: convertBasisPointsToDecimalPercentage(slippageBps).toString(), - rate, - steps: [ - { - rate, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - buyAmountBeforeFeesCryptoBaseUnit, - source, - buyAsset, - sellAsset, - accountNumber, - allowanceContract: '0x0', // not applicable to UTXOs - feeData, - }, - ], - } - }, - ), + const maybeRoutes = await Promise.allSettled( + perRouteValues.map( + async ({ source, slippageBps, expectedAmountOutThorBaseUnit, isStreaming }) => { + const rate = getRouteRate(expectedAmountOutThorBaseUnit) + const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount( + expectedAmountOutThorBaseUnit, + ) + + const updatedMemo = addSlippageToMemo(thornodeQuote, inputSlippageBps, isStreaming) + const maybeThorTxInfo = await getUtxoThorTxInfo({ + sellAsset, + xpub: (input as GetUtxoTradeQuoteInput).xpub, + memo: updatedMemo, + }) + if (maybeThorTxInfo.isErr()) throw maybeThorTxInfo.unwrapErr() + const thorTxInfo = maybeThorTxInfo.unwrap() + const { vault, opReturnData, pubkey } = thorTxInfo + + const sellAdapter = assertGetUtxoChainAdapter(sellAsset.chainId) + const feeData = await getUtxoTxFees({ + sellAmountCryptoBaseUnit, + vault, + opReturnData, + pubkey, + sellAdapter, + protocolFees, + }) + + return { + isStreaming, + recommendedSlippage: convertBasisPointsToDecimalPercentage(slippageBps).toString(), + rate, + steps: [ + { + rate, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + source, + buyAsset, + sellAsset, + accountNumber, + allowanceContract: '0x0', // not applicable to UTXOs + feeData, + }, + ], + } + }, ), ) + + const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value) + + // if no routes succeeded, return failure from swapper + if (!routes.length) + return Err( + makeSwapErrorRight({ + message: 'Unable to create any routes', + code: SwapErrorType.TRADE_QUOTE_FAILED, + cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason), + }), + ) + + // otherwise, return all that succeeded + return Ok(routes) } case CHAIN_NAMESPACE.CosmosSdk: { From 50bb98e01eab19523e03b26d7d29853f313ff5b8 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 21 Aug 2023 09:22:13 +1000 Subject: [PATCH 2/2] fix: tests --- .../ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index 9579e185cf1..8183ea58e2a 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -27,8 +27,6 @@ jest.mock('../utils/thorService', () => { } }) -const mockOk = Ok as jest.MockedFunction - jest.mock('context/PluginProvider/chainAdapterSingleton', () => { return { getChainAdapterManager: () => mockChainAdapterManager, @@ -106,7 +104,7 @@ const expectedQuoteResponse: ThorTradeQuote[] = [ describe('getTradeQuote', () => { ;(getThorTxInfo as jest.Mock).mockReturnValue( - Promise.resolve(mockOk({ data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976' })), + Promise.resolve({ data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976' }), ) const { quoteInput } = setupQuote()