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

fix: thorchain swap improvements #5748

Merged
merged 11 commits into from
Dec 4, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingl
import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { fromBaseUnit } from 'lib/math'
import type { AmountDisplayMeta, ProtocolFee } from 'lib/swapper/types'
import { THORCHAIN_STREAM_SWAP_SOURCE } from 'lib/swapper/swappers/ThorchainSwapper/constants'
import type { AmountDisplayMeta, ProtocolFee, SwapSource } from 'lib/swapper/types'
import { SwapperName } from 'lib/swapper/types'
import type { PartialRecord } from 'lib/utils'
import { isSome } from 'lib/utils'
Expand Down Expand Up @@ -61,6 +62,7 @@ type ReceiveSummaryProps = {
swapperName: string
donationAmountUserCurrency?: string
defaultIsOpen?: boolean
swapSource?: SwapSource
} & RowProps

const ShapeShiftFeeModalRowHover = { textDecoration: 'underline', cursor: 'pointer' }
Expand All @@ -84,6 +86,7 @@ export const ReceiveSummary: FC<ReceiveSummaryProps> = memo(
isLoading,
donationAmountUserCurrency,
defaultIsOpen = false,
swapSource,
...rest
}) => {
const translate = useTranslate()
Expand Down Expand Up @@ -279,41 +282,43 @@ export const ReceiveSummary: FC<ReceiveSummaryProps> = memo(
</Row.Value>
</Row>
)}
<>
<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 && (
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
<>
<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>
<Modal isOpen={showFeeModal} onClose={handleFeeModal} size='lg'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,24 +405,26 @@ export const TradeConfirm = () => {
fiatAmount={positiveOrZero(netBuyAmountUserCurrency).toFixed(2)}
swapperName={swapperName ?? ''}
intermediaryTransactionOutputs={tradeQuoteStep?.intermediaryTransactionOutputs}
swapSource={tradeQuoteStep?.source}
/>
</>
),
[
translate,
sellAmountBeforeFeesCryptoPrecision,
sellAsset?.symbol,
sellAmountBeforeFeesUserCurrency,
buyAsset?.symbol,
buyAmountAfterFeesCryptoPrecision,
buyAmountBeforeFeesCryptoPrecision,
tradeQuoteStep?.feeData.protocolFees,
tradeQuoteStep?.intermediaryTransactionOutputs,
shapeShiftFee,
buyAsset?.symbol,
donationAmountUserCurrency,
slippageDecimal,
netBuyAmountUserCurrency,
sellAmountBeforeFeesCryptoPrecision,
sellAmountBeforeFeesUserCurrency,
sellAsset?.symbol,
shapeShiftFee,
slippageDecimal,
swapperName,
tradeQuoteStep?.feeData.protocolFees,
tradeQuoteStep?.intermediaryTransactionOutputs,
tradeQuoteStep?.source,
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
translate,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ export const TradeInput = memo(() => {
slippageDecimalPercentage={slippageDecimal}
swapperName={activeSwapperName ?? ''}
defaultIsOpen={true}
swapSource={tradeQuoteStep?.source}
/>
) : null}
{isModeratePriceImpact && (
Expand Down Expand Up @@ -464,6 +465,7 @@ export const TradeInput = memo(() => {
slippageDecimal,
totalNetworkFeeFiatPrecision,
totalProtocolFees,
tradeQuoteStep?.source,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ const expectedQuoteResponse: Omit<ThorEvmTradeQuote, 'id'>[] = [
affiliateBps: '0',
potentialAffiliateBps: '0',
isStreaming: false,
rate: '137845.94361267605633802817',
rate: '144114.94366197183098591549',
data: '0x',
router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976',
memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:9360638:ss:0',
memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:9786345:ss:0',
tradeType: TradeType.L1ToL1,
steps: [
{
estimatedExecutionTimeMs: 1600000,
allowanceContract: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976',
sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420',
buyAmountBeforeFeesCryptoBaseUnit: '114321610000000000',
buyAmountAfterFeesCryptoBaseUnit: '97870619965000000',
buyAmountAfterFeesCryptoBaseUnit: '102321610000000000',
feeData: {
protocolFees: {
[ETH.assetId]: {
Expand All @@ -72,7 +72,7 @@ const expectedQuoteResponse: Omit<ThorEvmTradeQuote, 'id'>[] = [
},
networkFeeCryptoBaseUnit: '400000',
},
rate: '137845.94361267605633802817',
rate: '144114.94366197183098591549',
source: SwapperName.Thorchain,
buyAsset: ETH,
sellAsset: FOX_MAINNET,
Expand All @@ -85,18 +85,18 @@ const expectedQuoteResponse: Omit<ThorEvmTradeQuote, 'id'>[] = [
affiliateBps: '0',
potentialAffiliateBps: '0',
isStreaming: true,
rate: '151555.07377464788732394366',
rate: '158199.45070422535211267606',
data: '0x',
router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976',
memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:10291578/10/0:ss:0',
memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:0/10/0:ss:0',
tradeType: TradeType.L1ToL1,
steps: [
{
estimatedExecutionTimeMs: 1600000,
allowanceContract: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976',
sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420',
buyAmountBeforeFeesCryptoBaseUnit: '124321610000000000',
buyAmountAfterFeesCryptoBaseUnit: '107604102380000000',
buyAmountAfterFeesCryptoBaseUnit: '112321610000000000',
feeData: {
protocolFees: {
[ETH.assetId]: {
Expand All @@ -107,7 +107,7 @@ const expectedQuoteResponse: Omit<ThorEvmTradeQuote, 'id'>[] = [
},
networkFeeCryptoBaseUnit: '400000',
},
rate: '151555.07377464788732394366',
rate: '158199.45070422535211267606',
source: `${SwapperName.Thorchain} • Streaming`,
buyAsset: ETH,
sellAsset: FOX_MAINNET,
Expand Down Expand Up @@ -170,6 +170,8 @@ describe('getTradeQuote', () => {
if ((url as string).includes('streaming_interval')) {
mockThorQuote.data.expected_amount_out = '11232161'
mockThorQuote.data.fees.slippage_bps = 420
mockThorQuote.data.memo =
'=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6:0/10/0:ss:0'
}

return Promise.resolve(Ok(mockThorQuote))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,41 @@ import type { ChainId } from '@shapeshiftoss/caip'
import { BigNumber, bn } from 'lib/bignumber/bignumber'
import { subtractBasisPointAmount } from 'state/slices/tradeQuoteSlice/utils'

import { DEFAULT_STREAMING_NUM_SWAPS, LIMIT_PART_DELIMITER, MEMO_PART_DELIMITER } from './constants'
import { MEMO_PART_DELIMITER } from './constants'
import { assertIsValidMemo } from './makeSwapMemo/assertIsValidMemo'

export const addSlippageToMemo = ({
expectedAmountOutThorBaseUnit,
affiliateFeesThorBaseUnit,
quotedMemo,
slippageBps,
isStreaming,
chainId,
affiliateBps,
streamingInterval,
}: {
expectedAmountOutThorBaseUnit: string
affiliateFeesThorBaseUnit: string
quotedMemo: string | undefined
slippageBps: BigNumber.Value
chainId: ChainId
affiliateBps: string
isStreaming: boolean
streamingInterval: number
}) => {
if (!quotedMemo) throw new Error('no memo provided')

// always use TC auto stream quote (0 limit = 5bps - 50bps, sometimes up to 100bps)
// see: https://discord.com/channels/838986635756044328/1166265575941619742/1166500062101250100
if (isStreaming) return quotedMemo

// the missing element is the original limit with (optional, missing) streaming parameters
const [prefix, pool, address, , affiliate, memoAffiliateBps] =
quotedMemo.split(MEMO_PART_DELIMITER)

const limitWithManualSlippage = subtractBasisPointAmount(
bn(expectedAmountOutThorBaseUnit)
.minus(affiliateFeesThorBaseUnit)
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
.toFixed(0, BigNumber.ROUND_DOWN),
bn(expectedAmountOutThorBaseUnit).toFixed(0, BigNumber.ROUND_DOWN),
slippageBps,
BigNumber.ROUND_DOWN,
)

const updatedLimitComponent = isStreaming
? [limitWithManualSlippage, streamingInterval, DEFAULT_STREAMING_NUM_SWAPS].join(
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
LIMIT_PART_DELIMITER,
)
: [limitWithManualSlippage]

const memo = [prefix, pool, address, updatedLimitComponent, affiliate, memoAffiliateBps].join(
const memo = [prefix, pool, address, limitWithManualSlippage, affiliate, memoAffiliateBps].join(
MEMO_PART_DELIMITER,
)

Expand Down
40 changes: 15 additions & 25 deletions src/lib/swapper/swappers/ThorchainSwapper/utils/getL1quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import { assertGetCosmosSdkChainAdapter } from 'lib/utils/cosmosSdk'
import { assertGetEvmChainAdapter } from 'lib/utils/evm'
import { THOR_PRECISION } from 'lib/utils/thorchain/constants'
import { assertGetUtxoChainAdapter } from 'lib/utils/utxo'
import {
convertDecimalPercentageToBasisPoints,
subtractBasisPointAmount,
} from 'state/slices/tradeQuoteSlice/utils'
import { convertDecimalPercentageToBasisPoints } from 'state/slices/tradeQuoteSlice/utils'

import { THORCHAIN_STREAM_SWAP_SOURCE } from '../constants'
import type {
Expand Down Expand Up @@ -62,7 +59,7 @@ export const getL1quote = async (
const inputSlippageBps = convertDecimalPercentageToBasisPoints(
slippageTolerancePercentage ??
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Thorchain),
).toString()
)

const maybeSwapQuote = await getQuote({
sellAsset,
Expand Down Expand Up @@ -115,16 +112,15 @@ export const getL1quote = async (
const getRouteValues = (quote: ThornodeQuoteResponseSuccess, isStreaming: boolean) => ({
source: isStreaming ? THORCHAIN_STREAM_SWAP_SOURCE : SwapperName.Thorchain,
quote,
// expected receive amount after slippage (no affiliate_fee or liquidity_fee taken out of this value)
// TODO: slippage is currently being applied on expected_amount_out which is emit_asset - outbound_fee,
// should slippage actually be applied on emit_asset?
expectedAmountOutThorBaseUnit: subtractBasisPointAmount(
quote.expected_amount_out,
quote.fees.slippage_bps,
),
// don't take affiliate fee into account, this will be displayed as a separate line item
expectedAmountOutThorBaseUnit: bnOrZero(quote.expected_amount_out)
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
.plus(bnOrZero(quote.fees.affiliate))
.toFixed(),
isStreaming,
affiliateBps: quote.fees.affiliate === '0' ? '0' : requestedAffiliateBps,
potentialAffiliateBps,
// 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,
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
estimatedExecutionTimeMs: quote.total_swap_seconds
? 1000 * quote.total_swap_seconds
: undefined,
Expand Down Expand Up @@ -194,20 +190,18 @@ export const getL1quote = async (
isStreaming,
estimatedExecutionTimeMs,
affiliateBps,
potentialAffiliateBps,
slippageBps,
}): Promise<ThorEvmTradeQuote> => {
const rate = getRouteRate(expectedAmountOutThorBaseUnit)
const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount(quote)

const updatedMemo = addSlippageToMemo({
expectedAmountOutThorBaseUnit,
affiliateFeesThorBaseUnit: quote.fees.affiliate,
quotedMemo: quote.memo,
slippageBps: inputSlippageBps,
slippageBps,
chainId: sellAsset.chainId,
affiliateBps,
isStreaming,
streamingInterval,
})
const { data, router } = await getEvmThorTxInfo({
sellAsset,
Expand Down Expand Up @@ -282,20 +276,18 @@ export const getL1quote = async (
isStreaming,
estimatedExecutionTimeMs,
affiliateBps,
potentialAffiliateBps,
slippageBps,
}): Promise<ThorTradeUtxoOrCosmosQuote> => {
const rate = getRouteRate(expectedAmountOutThorBaseUnit)
const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount(quote)

const updatedMemo = addSlippageToMemo({
expectedAmountOutThorBaseUnit,
affiliateFeesThorBaseUnit: quote.fees.affiliate,
quotedMemo: quote.memo,
slippageBps: inputSlippageBps,
slippageBps,
isStreaming,
chainId: sellAsset.chainId,
affiliateBps,
streamingInterval,
})
const { vault, opReturnData, pubkey } = await getUtxoThorTxInfo({
sellAsset,
Expand Down Expand Up @@ -376,7 +368,7 @@ export const getL1quote = async (
isStreaming,
estimatedExecutionTimeMs,
affiliateBps,
potentialAffiliateBps,
slippageBps,
}): ThorTradeUtxoOrCosmosQuote => {
const rate = getRouteRate(expectedAmountOutThorBaseUnit)
const buyAmountBeforeFeesCryptoBaseUnit = getRouteBuyAmount(quote)
Expand All @@ -389,13 +381,11 @@ export const getL1quote = async (

const updatedMemo = addSlippageToMemo({
expectedAmountOutThorBaseUnit,
affiliateFeesThorBaseUnit: quote.fees.affiliate,
quotedMemo: quote.memo,
slippageBps: inputSlippageBps,
slippageBps,
isStreaming,
chainId: sellAsset.chainId,
affiliateBps,
streamingInterval,
})

return {
Expand Down