Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add error handling to thorchain swapper route generation #5115

Merged
merged 2 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/lib/swapper/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TradeQuote2[], SwapErrorRight>

export type Swapper2 = {
filterAssetIdsBySellable: (assets: Asset[]) => Promise<AssetId[]>
filterBuyAssetsBySellAssetId: (input: BuyAssetBySellIdInput) => Promise<AssetId[]>
Expand All @@ -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<Result<TradeQuote2[], SwapErrorRight>>
getTradeQuote: (input: GetTradeQuoteInput, deps: TradeQuoteDeps) => Promise<TradeQuoteResult>
getUnsignedTx(input: GetUnsignedTxArgs): Promise<UnsignedTx2>
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -59,5 +45,5 @@ export const getThorTxInfo = async ({
memo,
)

return Ok({ data, router })
return { data, router }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ jest.mock('../utils/thorService', () => {
}
})

const mockOk = Ok as jest.MockedFunction<typeof Ok>

jest.mock('context/PluginProvider/chainAdapterSingleton', () => {
return {
getChainAdapterManager: () => mockChainAdapterManager,
Expand Down Expand Up @@ -106,7 +104,7 @@ const expectedQuoteResponse: ThorTradeQuote[] = [

describe('getTradeQuote', () => {
;(getThorTxInfo as jest.Mock<unknown>).mockReturnValue(
Promise.resolve(mockOk({ data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976' })),
Promise.resolve({ data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976' }),
)

const { quoteInput } = setupQuote()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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: {
Expand Down