Skip to content

Commit

Permalink
fix: add amountOutMin to longtail (#6052)
Browse files Browse the repository at this point in the history
* fix: add amountOutMin

* fix: active pool status

* fix: handle decimal percentage

* chore: enable longtail feature flag

* chore: address review feedback

* chore: paranoia assertion

* chore: keep longtail off for now

* chore: address review feedback

* chore: remove typo

* feat: add longtail source UI (#6056)

feat: add longtail source ui
  • Loading branch information
0xApotheosis committed Jan 23, 2024
1 parent 1097d93 commit 3cd639f
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 88 deletions.
Expand Up @@ -9,7 +9,10 @@ import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingl
import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter'
import { getTxLink } from 'lib/getTxLink'
import { fromBaseUnit } from 'lib/math'
import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwapper/constants'
import {
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
THORCHAIN_STREAM_SWAP_SOURCE,
} from 'lib/swapper/swappers/ThorchainSwapper/constants'
import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors'
import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types'
import { useAppSelector } from 'state/store'
Expand Down Expand Up @@ -110,7 +113,9 @@ export const HopTransactionStep = ({
)
}

const isThorStreamingSwap = tradeQuoteStep.source === THORCHAIN_STREAM_SWAP_SOURCE
const isThorStreamingSwap =
tradeQuoteStep.source === THORCHAIN_STREAM_SWAP_SOURCE ||
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE

if (sellTxHash !== undefined && isThorStreamingSwap) {
return (
Expand Down
Expand Up @@ -22,7 +22,10 @@ import type { TextPropTypes } from 'components/Text/Text'
import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { fromBaseUnit } from 'lib/math'
import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwapper/constants'
import {
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
THORCHAIN_STREAM_SWAP_SOURCE,
} from 'lib/swapper/swappers/ThorchainSwapper/constants'
import { isSome } from 'lib/utils'
import {
selectActiveQuoteAffiliateBps,
Expand Down Expand Up @@ -248,43 +251,44 @@ export const ReceiveSummary: FC<ReceiveSummaryProps> = memo(
</Skeleton>
</Row.Value>
</Row>
{swapSource !== THORCHAIN_STREAM_SWAP_SOURCE && (
<>
<Divider borderColor='border.base' />
<Row>
<Row.Label>
<Text translation={minAmountAfterSlippageTranslation} />
</Row.Label>
<Row.Value whiteSpace='nowrap'>
<Stack spacing={0} alignItems='flex-end'>
<Skeleton isLoaded={!isLoading}>
<Amount.Crypto value={amountAfterSlippage} symbol={symbol} />
</Skeleton>
{isAmountPositive &&
hasIntermediaryTransactionOutputs &&
intermediaryTransactionOutputsParsed?.map(
({ amountCryptoPrecision, symbol, chainName }) => (
<Skeleton isLoaded={!isLoading} key={`${symbol}_${chainName}`}>
<Amount.Crypto
value={amountCryptoPrecision}
symbol={symbol}
prefix={translate('trade.or')}
suffix={
chainName
? translate('trade.onChainName', {
chainName,
})
: undefined
}
/>
</Skeleton>
),
)}
</Stack>
</Row.Value>
</Row>
</>
)}
{swapSource !== THORCHAIN_STREAM_SWAP_SOURCE &&
swapSource !== THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE && (
<>
<Divider borderColor='border.base' />
<Row>
<Row.Label>
<Text translation={minAmountAfterSlippageTranslation} />
</Row.Label>
<Row.Value whiteSpace='nowrap'>
<Stack spacing={0} alignItems='flex-end'>
<Skeleton isLoaded={!isLoading}>
<Amount.Crypto value={amountAfterSlippage} symbol={symbol} />
</Skeleton>
{isAmountPositive &&
hasIntermediaryTransactionOutputs &&
intermediaryTransactionOutputsParsed?.map(
({ amountCryptoPrecision, symbol, chainName }) => (
<Skeleton isLoaded={!isLoading} key={`${symbol}_${chainName}`}>
<Amount.Crypto
value={amountCryptoPrecision}
symbol={symbol}
prefix={translate('trade.or')}
suffix={
chainName
? translate('trade.onChainName', {
chainName,
})
: undefined
}
/>
</Skeleton>
),
)}
</Stack>
</Row.Value>
</Row>
</>
)}
</Stack>
</Collapse>
<FeeModal isOpen={showFeeModal} onClose={handleFeeModal} />
Expand Down
Expand Up @@ -49,7 +49,10 @@ import { getTxLink } from 'lib/getTxLink'
import { firstNonZeroDecimal } from 'lib/math'
import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton'
import { MixPanelEvent } from 'lib/mixpanel/types'
import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwapper/constants'
import {
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
THORCHAIN_STREAM_SWAP_SOURCE,
} from 'lib/swapper/swappers/ThorchainSwapper/constants'
import { assertUnreachable } from 'lib/utils'
import { selectManualReceiveAddress } from 'state/slices/tradeInputSlice/selectors'
import {
Expand Down Expand Up @@ -156,7 +159,9 @@ export const TradeConfirm = () => {
const txHash = buyTxHash ?? sellTxHash

const isThorStreamingSwap = useMemo(
() => tradeQuoteStep?.source === THORCHAIN_STREAM_SWAP_SOURCE,
() =>
tradeQuoteStep?.source === THORCHAIN_STREAM_SWAP_SOURCE ||
tradeQuoteStep?.source === THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
[tradeQuoteStep?.source],
)

Expand Down
8 changes: 7 additions & 1 deletion src/lib/getTxLink.ts
Expand Up @@ -2,7 +2,11 @@ import type { SwapSource } from '@shapeshiftoss/swapper'
import { SwapperName } from '@shapeshiftoss/swapper'
import { Dex } from '@shapeshiftoss/unchained-client'

import { THORCHAIN_STREAM_SWAP_SOURCE } from './swapper/swappers/ThorchainSwapper/constants'
import {
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
THORCHAIN_LONGTAIL_SWAP_SOURCE,
THORCHAIN_STREAM_SWAP_SOURCE,
} from './swapper/swappers/ThorchainSwapper/constants'

type GetBaseUrl = {
name: SwapSource | Dex | undefined
Expand All @@ -21,6 +25,8 @@ export const getTxBaseUrl = ({ name, defaultExplorerBaseUrl, isOrder }: GetBaseU
case Dex.Thor:
case SwapperName.Thorchain:
case THORCHAIN_STREAM_SWAP_SOURCE:
case THORCHAIN_LONGTAIL_SWAP_SOURCE:
case THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE:
return 'https://viewblock.io/thorchain/tx/'
default:
return defaultExplorerBaseUrl
Expand Down
2 changes: 2 additions & 0 deletions src/lib/swapper/swappers/ThorchainSwapper/constants.ts
Expand Up @@ -28,6 +28,8 @@ export const buySupportedChainIds: Record<ChainId, boolean> = {
}

export const THORCHAIN_STREAM_SWAP_SOURCE: SwapSource = `${SwapperName.Thorchain} • Streaming`
export const THORCHAIN_LONGTAIL_SWAP_SOURCE: SwapSource = `${SwapperName.Thorchain} • Long-tail`
export const THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE: SwapSource = `${SwapperName.Thorchain} • Long-tail streaming`

// https://dev.thorchain.org/thorchain-dev/interface-guide/fees#thorchain-native-rune
// static automatic outbound fee as defined by: https://daemon.thorchain.shapeshift.com/lcd/thorchain/constants
Expand Down
64 changes: 49 additions & 15 deletions src/lib/swapper/swappers/ThorchainSwapper/endpoints.ts
Expand Up @@ -5,28 +5,30 @@ import { cosmosAssetId, fromAssetId, fromChainId, thorchainAssetId } from '@shap
import type { EvmChainId } from '@shapeshiftoss/chain-adapters'
import { cosmossdk as cosmossdkChainAdapter } from '@shapeshiftoss/chain-adapters'
import type { BTCSignTx } from '@shapeshiftoss/hdwallet-core'
import type {
CosmosSdkFeeData,
EvmTransactionRequest,
GetTradeQuoteInput,
GetUnsignedCosmosSdkTransactionArgs,
GetUnsignedEvmTransactionArgs,
GetUnsignedUtxoTransactionArgs,
SwapErrorRight,
SwapperApi,
TradeQuote,
UtxoFeeData,
import {
type CosmosSdkFeeData,
type EvmTransactionRequest,
type GetTradeQuoteInput,
type GetUnsignedCosmosSdkTransactionArgs,
type GetUnsignedEvmTransactionArgs,
type GetUnsignedUtxoTransactionArgs,
makeSwapErrorRight,
type SwapErrorRight,
type SwapperApi,
type TradeQuote,
TradeQuoteError,
type UtxoFeeData,
} from '@shapeshiftoss/swapper'
import type { AssetsByIdPartial } from '@shapeshiftoss/types'
import { KnownChainIds } from '@shapeshiftoss/types'
import { cosmossdk, evm, TxStatus } from '@shapeshiftoss/unchained-client'
import { type Result } from '@sniptt/monads/build'
import { Err, type Result } from '@sniptt/monads/build'
import assert from 'assert'
import axios from 'axios'
import { getConfig } from 'config'
import type { Address } from 'viem'
import { encodeFunctionData, parseAbiItem } from 'viem'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber'
import { getThorTxInfo as getUtxoThorTxInfo } from 'lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData'
import { assertUnreachable } from 'lib/utils'
import { assertGetEvmChainAdapter } from 'lib/utils/evm'
Expand Down Expand Up @@ -73,7 +75,15 @@ export const thorchainApi: SwapperApi = {
supportsEIP1559,
}: GetUnsignedEvmTransactionArgs): Promise<EvmTransactionRequest> => {
// TODO: pull these from db using id so we don't have type zoo and casting hell
const { router: to, data, steps, memo: tcMemo, tradeType } = tradeQuote as ThorEvmTradeQuote
const {
router: to,
data,
steps,
memo: tcMemo,
tradeType,
longtailData,
slippageTolerancePercentageDecimal,
} = tradeQuote as ThorEvmTradeQuote
const { sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset } = steps[0]

const value = isNativeEvmAsset(sellAsset.assetId)
Expand Down Expand Up @@ -146,9 +156,33 @@ export const thorchainApi: SwapperApi = {
const publicClient = viemClientByChainId[chainId as EvmChainId]
assert(publicClient !== undefined, `no public client found for chainId '${chainId}'`)

const expectedAmountOut = BigInt(longtailData?.longtailToL1ExpectedAmountOut ?? 0)
// Paranoia assertion - expectedAmountOut should never be 0 as it would likely lead to a loss of funds.
assert(
expectedAmountOut !== undefined && expectedAmountOut > 0n,
'expected expectedAmountOut to be a positive amount',
)

const amountOutMin = BigInt(
bnOrZero(expectedAmountOut.toString())
.times(bn(1).minus(slippageTolerancePercentageDecimal ?? 0))
.toFixed(0, BigNumber.ROUND_UP),
)

if (amountOutMin <= 0n) {
throw Err(
makeSwapErrorRight({
code: TradeQuoteError.SellAmountBelowMinimum,
message: 'Sell amount is too small',
}),
)
}

// Paranoia: ensure we have this to prevent sandwich attacks on the first step of a LongtailToL1 trade.
assert(amountOutMin > 0n, 'expected expectedAmountOut to be a positive amount')

const token: Address = fromAssetId(sellAsset.assetId).assetReference as Address
const amount: bigint = BigInt(sellAmountIncludingProtocolFeesCryptoBaseUnit)
const amountOutMin: bigint = BigInt(0) // todo: the buy amount
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000))
const tenMinutes = BigInt(600)
const deadline = currentTimestamp + tenMinutes
Expand Down
Expand Up @@ -19,6 +19,9 @@ type ThorTradeQuoteSpecificMetadata = {
isStreaming: boolean
memo: string
recommendedMinimumCryptoBaseUnit: string
longtailData?: {
longtailToL1ExpectedAmountOut?: bigint
}
}
export type ThorEvmTradeQuote = TradeQuote &
ThorTradeQuoteSpecificMetadata & {
Expand Down Expand Up @@ -58,15 +61,6 @@ export const getThorTradeQuote = async (
const buyPoolId = assetIdToPoolAssetId({ assetId: buyAsset.assetId })
const sellPoolId = assetIdToPoolAssetId({ assetId: sellAsset.assetId })

if (!buyPoolId || !sellPoolId) {
return Err(
makeSwapErrorRight({
message: 'Unsupported trade pair',
code: TradeQuoteError.UnsupportedTradePair,
}),
)
}

// If one or both of these are undefined it means we are tradeing one or more long-tail ERC20 tokens
const sellAssetPool = poolsResponse.find(pool => pool.asset === sellPoolId)
const buyAssetPool = poolsResponse.find(pool => pool.asset === buyPoolId)
Expand All @@ -83,6 +77,18 @@ export const getThorTradeQuote = async (
)
}

if (
(!buyPoolId && tradeType !== TradeType.L1ToLongTail) ||
(!sellPoolId && tradeType !== TradeType.LongTailToL1)
) {
return Err(
makeSwapErrorRight({
message: 'Unsupported trade pair',
code: TradeQuoteError.UnsupportedTradePair,
}),
)
}

const streamingInterval =
sellAssetPool && buyAssetPool
? (() => {
Expand Down
63 changes: 44 additions & 19 deletions src/lib/swapper/swappers/ThorchainSwapper/utils/getL1quote.ts
Expand Up @@ -28,7 +28,11 @@ import { THOR_PRECISION } from 'lib/utils/thorchain/constants'
import { assertGetUtxoChainAdapter } from 'lib/utils/utxo'
import { convertDecimalPercentageToBasisPoints } from 'state/slices/tradeQuoteSlice/utils'

import { THORCHAIN_STREAM_SWAP_SOURCE } from '../constants'
import {
THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
THORCHAIN_LONGTAIL_SWAP_SOURCE,
THORCHAIN_STREAM_SWAP_SOURCE,
} from '../constants'
import type {
ThorEvmTradeQuote,
ThorTradeQuote,
Expand Down Expand Up @@ -101,24 +105,45 @@ export const getL1quote = async (
}).toFixed()
: '0'

const getRouteValues = (quote: ThornodeQuoteResponseSuccess, isStreaming: boolean) => ({
source: isStreaming ? THORCHAIN_STREAM_SWAP_SOURCE : SwapperName.Thorchain,
quote,
// don't take affiliate fee into account, this will be displayed as a separate line item
expectedAmountOutThorBaseUnit: bnOrZero(quote.expected_amount_out)
.plus(bnOrZero(quote.fees.affiliate))
.toFixed(),
isStreaming,
affiliateBps: quote.fees.affiliate === '0' ? '0' : requestedAffiliateBps,
// always use TC auto stream quote (0 limit = 5bps - 50bps, sometimes up to 100bps)
// see: https://discord.com/channels/838986635756044328/1166265575941619742/1166500062101250100
slippageBps: isStreaming ? bn(0) : inputSlippageBps,
// TODO: this is off by about an hour in most cases, more work required to make it useable
// estimatedExecutionTimeMs: quote.total_swap_seconds
// ? 1000 * quote.total_swap_seconds
// : undefined,
estimatedExecutionTimeMs: undefined,
})
const getRouteValues = (quote: ThornodeQuoteResponseSuccess, isStreaming: boolean) => {
const source = (() => {
if (isStreaming && tradeType === TradeType.L1ToL1) return THORCHAIN_STREAM_SWAP_SOURCE
if (
isStreaming &&
[TradeType.L1ToLongTail, TradeType.LongTailToL1, TradeType.LongTailToLongTail].includes(
tradeType,
)
)
return THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE
if (
!isStreaming &&
[TradeType.L1ToLongTail, TradeType.LongTailToL1, TradeType.LongTailToLongTail].includes(
tradeType,
)
)
return THORCHAIN_LONGTAIL_SWAP_SOURCE
return SwapperName.Thorchain
})()

return {
source,
quote,
// don't take affiliate fee into account, this will be displayed as a separate line item
expectedAmountOutThorBaseUnit: bnOrZero(quote.expected_amount_out)
.plus(bnOrZero(quote.fees.affiliate))
.toFixed(),
isStreaming,
affiliateBps: quote.fees.affiliate === '0' ? '0' : requestedAffiliateBps,
// always use TC auto stream quote (0 limit = 5bps - 50bps, sometimes up to 100bps)
// see: https://discord.com/channels/838986635756044328/1166265575941619742/1166500062101250100
slippageBps: isStreaming ? bn(0) : inputSlippageBps,
// TODO: this is off by about an hour in most cases, more work required to make it useable
// estimatedExecutionTimeMs: quote.total_swap_seconds
// ? 1000 * quote.total_swap_seconds
// : undefined,
estimatedExecutionTimeMs: undefined,
}
}

const perRouteValues = [getRouteValues(swapQuote, false)]

Expand Down
Expand Up @@ -138,6 +138,9 @@ export const getLongtailToL1Quote = async (
allowanceContract: ALLOWANCE_CONTRACT,
})),
isLongtail: true,
longtailData: {
longtailToL1ExpectedAmountOut: quotedAmountOut,
},
}))

return Ok(updatedQuotes)
Expand Down

0 comments on commit 3cd639f

Please sign in to comment.