From c4e178f76ee20e8c4446d85a6c42c2169e9e6a87 Mon Sep 17 00:00:00 2001 From: Apotheosis <0xapotheosis@gmail.com> Date: Fri, 18 Aug 2023 21:59:46 +1000 Subject: [PATCH] chore: remove manual memo calcs (#5094) --- .../cosmossdk/getCosmosTxData.ts | 47 +---- .../swappers/ThorchainSwapper/endpoints.ts | 7 +- .../evm/utils/getThorTxData.ts | 63 ++----- .../getThorTradeQuote/getTradeQuote.test.ts | 12 +- .../getThorTradeQuote/getTradeQuote.ts | 45 ++--- .../utils/getLimit/getLimit.test.ts | 150 ---------------- .../utils/getLimit/getLimit.ts | 160 ------------------ .../utils/getQuote/getQuote.ts | 28 ++- .../utils/getSignTxFromQuote.ts | 20 +-- .../utils/makeSwapMemo/assertIsValidMemo.ts | 2 + .../utxo/utils/getThorTxData.ts | 62 +------ .../slices/tradeQuoteSlice/utils.test.ts | 7 +- src/state/slices/tradeQuoteSlice/utils.ts | 9 +- 13 files changed, 100 insertions(+), 512 deletions(-) delete mode 100644 src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.test.ts delete mode 100644 src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts diff --git a/src/lib/swapper/swappers/ThorchainSwapper/cosmossdk/getCosmosTxData.ts b/src/lib/swapper/swappers/ThorchainSwapper/cosmossdk/getCosmosTxData.ts index 8bde0b6e9be..3e677343386 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/cosmossdk/getCosmosTxData.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/cosmossdk/getCosmosTxData.ts @@ -9,13 +9,8 @@ import { getConfig } from 'config' import type { Asset } from 'lib/asset-service' import type { SwapErrorRight, TradeQuote } from 'lib/swapper/api' import { makeSwapErrorRight, SwapErrorType } from 'lib/swapper/api' -import type { - ThorCosmosSdkSupportedChainId, - ThornodeQuoteResponseSuccess, -} from 'lib/swapper/swappers/ThorchainSwapper/types' +import type { ThorCosmosSdkSupportedChainId } from 'lib/swapper/swappers/ThorchainSwapper/types' import { getInboundAddressDataForChain } from 'lib/swapper/swappers/ThorchainSwapper/utils/getInboundAddressDataForChain' -import { getLimit } from 'lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit' -import { makeSwapMemo } from 'lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/makeSwapMemo' type GetCosmosTxDataInput = { accountNumber: number @@ -31,27 +26,14 @@ type GetCosmosTxDataInput = { affiliateBps: string buyAssetUsdRate: string feeAssetUsdRate: string - thornodeQuote: ThornodeQuoteResponseSuccess + memo: string } export const getCosmosTxData = async ( input: GetCosmosTxDataInput, ): Promise> => { - const { - accountNumber, - destinationAddress, - sellAmountCryptoBaseUnit, - sellAsset, - buyAsset, - slippageTolerance, - quote, - from, - sellAdapter, - affiliateBps, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, - } = input + const { accountNumber, sellAmountCryptoBaseUnit, sellAsset, quote, from, sellAdapter, memo } = + input const fromThorAsset = sellAsset.chainId === KnownChainIds.ThorchainMainnet const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL const maybeVault = await (async () => { @@ -73,27 +55,6 @@ export const getCosmosTxData = async ( }), ) - const maybeLimit = await getLimit({ - buyAsset, - sellAmountCryptoBaseUnit, - sellAsset, - slippageTolerance, - protocolFees: quote.steps[0].feeData.protocolFees, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, - }) - - if (maybeLimit.isErr()) return Err(maybeLimit.unwrapErr()) - - const limit = maybeLimit.unwrap() - const memo = makeSwapMemo({ - buyAssetId: buyAsset.assetId, - destinationAddress, - limit, - affiliateBps, - }) - const maybeBuiltTxResponse = (() => { switch (true) { case fromThorAsset: diff --git a/src/lib/swapper/swappers/ThorchainSwapper/endpoints.ts b/src/lib/swapper/swappers/ThorchainSwapper/endpoints.ts index 4fbb1fec383..3f481c19e73 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/endpoints.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/endpoints.ts @@ -41,7 +41,7 @@ export const thorchainApi: Swapper2Api = { })) } - return await getThorTradeQuote(input, rates).then(async firstQuote => { + return await getThorTradeQuote(input).then(async firstQuote => { // If the first quote fails there is no need to check if the donation amount is below the minimum if (firstQuote.isErr()) return mapTradeQuoteToTradeQuote2(firstQuote, receiveAddress, affiliateBps) @@ -65,7 +65,10 @@ export const thorchainApi: Swapper2Api = { If the donation amount is below the minimum, we need to fetch a new quote with no affiliate fee */ - await getThorTradeQuote({ ...input, affiliateBps: '0' }, rates) + await getThorTradeQuote({ + ...input, + affiliateBps: '0', + }) : firstQuote return mapTradeQuoteToTradeQuote2( diff --git a/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts b/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts index 7c2e0372176..e859da8d4b5 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData.ts @@ -1,30 +1,18 @@ -import type { AssetId } from '@shapeshiftoss/caip' 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 { ProtocolFee, SwapErrorRight } from 'lib/swapper/api' +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 { getLimit } from 'lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit' -import { makeSwapMemo } from 'lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/makeSwapMemo' import { isNativeEvmAsset } from 'lib/swapper/swappers/utils/helpers/helpers' -import type { ThornodeQuoteResponseSuccess } from '../../types' - type GetEvmThorTxInfoArgs = { sellAsset: Asset - buyAsset: Asset sellAmountCryptoBaseUnit: string - slippageTolerance: string - destinationAddress: string - protocolFees: Record - affiliateBps: string - buyAssetUsdRate: string - feeAssetUsdRate: string - thornodeQuote: ThornodeQuoteResponseSuccess + memo: string } type GetEvmThorTxInfoReturn = Promise< @@ -41,15 +29,8 @@ type GetBtcThorTxInfo = (args: GetEvmThorTxInfoArgs) => GetEvmThorTxInfoReturn export const getThorTxInfo: GetBtcThorTxInfo = async ({ sellAsset, - buyAsset, sellAmountCryptoBaseUnit, - slippageTolerance, - destinationAddress, - protocolFees, - affiliateBps, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, + memo, }) => { const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL const { assetReference } = fromAssetId(sellAsset.assetId) @@ -70,35 +51,15 @@ export const getThorTxInfo: GetBtcThorTxInfo = async ({ }), ) - const maybeLimit = await getLimit({ - buyAsset, + const data = deposit( + router, + vault, + isNativeEvmAsset(sellAsset.assetId) + ? '0x0000000000000000000000000000000000000000' + : assetReference, sellAmountCryptoBaseUnit, - sellAsset, - slippageTolerance, - protocolFees, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, - }) - - return maybeLimit.andThen(limit => { - const memo = makeSwapMemo({ - buyAssetId: buyAsset.assetId, - destinationAddress, - limit, - affiliateBps, - }) - - const data = deposit( - router, - vault, - isNativeEvmAsset(sellAsset.assetId) - ? '0x0000000000000000000000000000000000000000' - : assetReference, - sellAmountCryptoBaseUnit, - memo, - ) + memo, + ) - return Ok({ data, router }) - }) + return Ok({ data, router }) } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index 4d987fcf174..d2a8f92b18a 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -41,7 +41,7 @@ jest.mock('config', () => { const expectedQuoteResponse: ThorEvmTradeQuote = { rate: '144114.94366197183098591549', - recommendedSlippage: '0.04357', + recommendedSlippage: '0.0435', data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', steps: [ @@ -108,7 +108,7 @@ describe('getTradeQuote', () => { 'First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats (P2WSH with Bech32 address format preferred).', outbound_delay_blocks: 575, outbound_delay_seconds: 6900, - slippage_bps: 4357, + slippage_bps: 435, warning: 'Do not cache this response. Do not send funds after the expiry.', memo: '=:ETH.ETH:0x32DBc9Cf9E8FbCebE1e0a2ecF05Ed86Ca3096Cb6::ss:0', }, @@ -122,14 +122,10 @@ describe('getTradeQuote', () => { sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420', buyAsset: ETH, sellAsset: FOX_MAINNET, + slippageTolerancePercentage: '0.04357', } - const maybeTradeQuote = await getThorTradeQuote(input, { - sellAssetUsdRate: '0.15399605260336216', - buyAssetUsdRate: '1595', - feeAssetUsdRate: '1595', - runeAssetUsdRate: '0.30', - }) + const maybeTradeQuote = await getThorTradeQuote(input) expect(maybeTradeQuote.isOk()).toBe(true) expect(maybeTradeQuote.unwrap()).toEqual(expectedQuoteResponse) }) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts index 98749822d4e..955e3d6222b 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts @@ -25,7 +25,6 @@ import type { import { makeSwapErrorRight, SwapErrorType, SwapperName } from 'lib/swapper/api' import { getThorTxInfo as getEvmThorTxInfo } from 'lib/swapper/swappers/ThorchainSwapper/evm/utils/getThorTxData' import type { - Rates, ThorCosmosSdkSupportedChainId, ThorEvmSupportedChainId, ThorUtxoSupportedChainId, @@ -34,6 +33,10 @@ 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 { + convertBasisPointsToDecimalPercentage, + convertDecimalPercentageToBasisPoints, +} from 'state/slices/tradeQuoteSlice/utils' import { isNativeEvmAsset } from '../../utils/helpers/helpers' import { getEvmTxFees } from '../utils/txFeeHelpers/evmTxFees/getEvmTxFees' @@ -50,26 +53,23 @@ type ThorTradeQuote = export const getThorTradeQuote = async ( input: GetTradeQuoteInput & { wallet?: HDWallet }, - rates: Rates, ): Promise> => { const { sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, - slippageTolerancePercentage, accountNumber, chainId, receiveAddress, affiliateBps, wallet, + slippageTolerancePercentage, } = input - const slippageTolerance = - slippageTolerancePercentage ?? getDefaultSlippagePercentageForSwapper(SwapperName.Thorchain) - - const { buyAssetUsdRate, feeAssetUsdRate } = rates - const { chainId: buyAssetChainId } = fromAssetId(buyAsset.assetId) + const slippageBps = convertDecimalPercentageToBasisPoints( + slippageTolerancePercentage ?? getDefaultSlippagePercentageForSwapper(SwapperName.Thorchain), + ).toString() const chainAdapterManager = getChainAdapterManager() const sellAdapter = chainAdapterManager.get(chainId) @@ -100,18 +100,22 @@ export const getThorTradeQuote = async ( sellAmountCryptoBaseUnit, receiveAddress, affiliateBps, + slippageBps, }) if (maybeQuote.isErr()) return Err(maybeQuote.unwrapErr()) const thornodeQuote = maybeQuote.unwrap() const { - slippage_bps: slippageBps, + slippage_bps: recommendedSlippageBps, fees, expected_amount_out: expectedAmountOutThorBaseUnit, + memo, } = thornodeQuote - const slippagePercentage = bn(slippageBps).div(1000) + const recommendedSlippageDecimalPercentage = convertBasisPointsToDecimalPercentage( + recommendedSlippageBps.toString(), + ).toString() const rate = (() => { const THOR_PRECISION = 8 @@ -147,7 +151,7 @@ export const getThorTradeQuote = async ( })() const commonQuoteFields = { - recommendedSlippage: slippagePercentage.div(100).toString(), + recommendedSlippage: recommendedSlippageDecimalPercentage, } const commonStepFields = { @@ -176,15 +180,8 @@ export const getThorTradeQuote = async ( return (async (): Promise>> => { const maybeThorTxInfo = await getEvmThorTxInfo({ sellAsset, - buyAsset, sellAmountCryptoBaseUnit, - slippageTolerance, - destinationAddress: receiveAddress, - protocolFees, - affiliateBps, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, + memo, }) if (maybeThorTxInfo.isErr()) return Err(maybeThorTxInfo.unwrapErr()) @@ -225,16 +222,8 @@ export const getThorTradeQuote = async ( return (async (): Promise, SwapErrorRight>> => { const maybeThorTxInfo = await getUtxoThorTxInfo({ sellAsset, - buyAsset, - sellAmountCryptoBaseUnit, - slippageTolerance, - destinationAddress: receiveAddress, xpub: (input as GetUtxoTradeQuoteInput).xpub, - protocolFees, - affiliateBps, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, + memo, }) if (maybeThorTxInfo.isErr()) return Err(maybeThorTxInfo.unwrapErr()) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.test.ts deleted file mode 100644 index 5decfa8f704..00000000000 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Ok } from '@sniptt/monads' -import { mockChainAdapters } from 'test/mocks/portfolio' - -import { DEFAULT_SLIPPAGE } from '../../../utils/constants' -import { BTC, ETH, FOX_MAINNET, RUNE } from '../../../utils/test-data/assets' -import type { ThornodeQuoteResponseSuccess } from '../../types' -import { getInboundAddressDataForChain } from '../getInboundAddressDataForChain' -import { getTradeRate, getTradeRateBelowMinimum } from '../getTradeRate/getTradeRate' -import { mockInboundAddresses } from '../test-data/responses' -import type { GetLimitArgs } from './getLimit' -import { getLimit } from './getLimit' - -jest.mock('../getTradeRate/getTradeRate') -jest.mock('../getInboundAddressDataForChain') -jest.mock('context/PluginProvider/chainAdapterSingleton', () => ({ - getChainAdapterManager: () => mockChainAdapters, -})) - -const mockOk = Ok as jest.MockedFunction - -describe('getLimit', () => { - beforeEach(() => { - jest.resetAllMocks() - }) - - it('should get limit when sell asset is EVM fee asset and buy asset is a UTXO', async () => { - ;(getTradeRate as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('0.07714399680893498205')), - ) - ;(getTradeRateBelowMinimum as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('42.22')), - ) - ;(getInboundAddressDataForChain as jest.Mock).mockReturnValue( - Promise.resolve(mockOk(mockInboundAddresses.find(address => address.chain === 'ETH'))), - ) - const getLimitArgs: GetLimitArgs = { - sellAsset: ETH, - buyAsset: BTC, - sellAmountCryptoBaseUnit: '82535000000000000', - slippageTolerance: DEFAULT_SLIPPAGE, - protocolFees: { - [BTC.assetId]: { - amountCryptoBaseUnit: '30000', - requiresBalance: false, - asset: BTC, - }, - }, - buyAssetUsdRate: '20683', // buyAssetUsdRate (BTC) - feeAssetUsdRate: '1595', // sellFeeAssetUsdRate (ETH) - thornodeQuote: {} as unknown as ThornodeQuoteResponseSuccess, - } - const maybeLimit = await getLimit(getLimitArgs) - expect(maybeLimit.isOk()).toBe(true) - expect(maybeLimit.unwrap()).toBe('592064') - }) - - it('should get limit when sell asset is EVM non-fee asset and buy asset is a UTXO', async () => { - ;(getTradeRate as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('0.00000199048641810579')), - ) - ;(getTradeRateBelowMinimum as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('42.22')), - ) - ;(getInboundAddressDataForChain as jest.Mock).mockReturnValue( - Promise.resolve(mockOk(mockInboundAddresses.find(address => address.chain === 'ETH'))), - ) - const getLimitArgs: GetLimitArgs = { - sellAsset: FOX_MAINNET, - buyAsset: BTC, - sellAmountCryptoBaseUnit: '489830019000000000000', - slippageTolerance: DEFAULT_SLIPPAGE, - protocolFees: { - [BTC.assetId]: { - amountCryptoBaseUnit: '30000', - requiresBalance: false, - asset: BTC, - }, - }, - buyAssetUsdRate: '20683', // buyAssetUsdRate (BTC) - feeAssetUsdRate: '1595', // sellFeeAssetUsdRate (ETH) - thornodeQuote: {} as unknown as ThornodeQuoteResponseSuccess, - } - const maybeLimit = await getLimit(getLimitArgs) - expect(maybeLimit.isOk()).toBe(true) - expect(maybeLimit.unwrap()).toBe('59316') - }) - - it('should get limit when buy asset is RUNE and sell asset is not', async () => { - ;(getTradeRate as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('0.02583433052665346349')), - ) - ;(getTradeRateBelowMinimum as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('42.22')), - ) - ;(getInboundAddressDataForChain as jest.Mock).mockReturnValue( - Promise.resolve(mockOk(mockInboundAddresses.find(address => address.chain === 'ETH'))), - ) - const getLimitArgs: GetLimitArgs = { - sellAsset: FOX_MAINNET, - buyAsset: RUNE, - sellAmountCryptoBaseUnit: '984229076000000000000', - slippageTolerance: DEFAULT_SLIPPAGE, - protocolFees: { - [RUNE.assetId]: { - amountCryptoBaseUnit: '219316', - requiresBalance: false, - asset: RUNE, - }, - }, - buyAssetUsdRate: '14.51', // buyAssetUsdRate (RUNE) - feeAssetUsdRate: '1595', // sellFeeAssetUsdRate (ETH) - thornodeQuote: {} as unknown as ThornodeQuoteResponseSuccess, - } - const maybeLimit = await getLimit(getLimitArgs) - expect(maybeLimit.isOk()).toBe(true) - expect(maybeLimit.unwrap()).toBe('2459464890') - }) - - it('should get limit when sell asset is RUNE and buy asset is not', async () => { - ;(getTradeRate as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('38.68447363336979738738')), - ) - ;(getTradeRateBelowMinimum as jest.Mock).mockReturnValue( - Promise.resolve(mockOk('42.22')), - ) - ;(getInboundAddressDataForChain as jest.Mock).mockReturnValue( - Promise.resolve(mockOk(undefined)), - ) - const getLimitArgs: GetLimitArgs = { - sellAsset: RUNE, - buyAsset: FOX_MAINNET, - sellAmountCryptoBaseUnit: '988381400', - slippageTolerance: DEFAULT_SLIPPAGE, - protocolFees: { - [FOX_MAINNET.assetId]: { - amountCryptoBaseUnit: '65000000000', - requiresBalance: false, - asset: FOX_MAINNET, - }, - }, - buyAssetUsdRate: '0.04', // buyAssetUsdRate (FOX) - feeAssetUsdRate: '14.51', // sellFeeAssetUsdRate (RUNE) - thornodeQuote: {} as unknown as ThornodeQuoteResponseSuccess, - } - const maybeLimit = await getLimit(getLimitArgs) - if (maybeLimit.isErr()) console.log(maybeLimit.unwrapErr()) - expect(maybeLimit.isOk()).toBe(true) - expect(maybeLimit.unwrap()).toBe('37051458738') - }) -}) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts deleted file mode 100644 index 599f1ac1090..00000000000 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { AssetId } from '@shapeshiftoss/caip' -import { fromAssetId } from '@shapeshiftoss/caip' -import type { Result } from '@sniptt/monads' -import { Err, Ok } from '@sniptt/monads' -import { getConfig } from 'config' -import max from 'lodash/max' -import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import type { Asset } from 'lib/asset-service' -import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' -import { fromBaseUnit, toBaseUnit } from 'lib/math' -import type { ProtocolFee, SwapErrorRight } from 'lib/swapper/api' -import { makeSwapErrorRight, SwapErrorType } from 'lib/swapper/api' -import { RUNE_OUTBOUND_TRANSACTION_FEE_CRYPTO_HUMAN } from 'lib/swapper/swappers/ThorchainSwapper/constants' -import { THORCHAIN_FIXED_PRECISION } from 'lib/swapper/swappers/ThorchainSwapper/utils/constants' -import { getInboundAddressDataForChain } from 'lib/swapper/swappers/ThorchainSwapper/utils/getInboundAddressDataForChain' -import { - getTradeRate, - getTradeRateBelowMinimum, -} from 'lib/swapper/swappers/ThorchainSwapper/utils/getTradeRate/getTradeRate' -import { isRune } from 'lib/swapper/swappers/ThorchainSwapper/utils/isRune/isRune' -import { ALLOWABLE_MARKET_MOVEMENT } from 'lib/swapper/swappers/utils/constants' -import type { PartialRecord } from 'lib/utils' - -import type { ThornodeQuoteResponseSuccess } from '../../types' - -export type GetLimitArgs = { - buyAsset: Asset - sellAsset: Asset - sellAmountCryptoBaseUnit: string - slippageTolerance: string - protocolFees: PartialRecord - buyAssetUsdRate: string - feeAssetUsdRate: string - thornodeQuote: ThornodeQuoteResponseSuccess -} - -export const getLimit = async ({ - sellAsset, - buyAsset, - sellAmountCryptoBaseUnit, - slippageTolerance, - protocolFees, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, -}: GetLimitArgs): Promise> => { - const maybeTradeRate = await getTradeRate({ - sellAsset, - buyAssetId: buyAsset.assetId, - sellAmountCryptoBaseUnit, - thornodeQuote, - }) - - const tradeRateBelowMinimum = await getTradeRateBelowMinimum({ - sellAssetId: sellAsset.assetId, - buyAssetId: buyAsset.assetId, - }) - const maybeTradeRateOrMinimum = maybeTradeRate.match({ - ok: tradeRate => Ok(tradeRate), - err: () => tradeRateBelowMinimum, - }) - - // This should not happen but it may - we should be able to get either a trade rate, or a minimum as a default - if (maybeTradeRateOrMinimum.isErr()) return Err(maybeTradeRateOrMinimum.unwrapErr()) - const tradeRateOrMinimum = maybeTradeRateOrMinimum.unwrap() - - const chainAdapterManager = getChainAdapterManager() - const sellAssetChainFeeAssetId = chainAdapterManager.get(sellAsset.chainId)?.getFeeAssetId() - const buyAssetChainFeeAssetId = chainAdapterManager - .get(fromAssetId(buyAsset.assetId).chainId) - ?.getFeeAssetId() - if (!sellAssetChainFeeAssetId || !buyAssetChainFeeAssetId) { - return Err( - makeSwapErrorRight({ - message: '[getLimit]: no sellAssetChainFeeAsset or buyAssetChainFeeAssetId', - code: SwapErrorType.BUILD_TRADE_FAILED, - details: { sellAssetChainFeeAssetId, buyAssetChainFeeAssetId }, - }), - ) - } - - const expectedBuyAmountCryptoPrecision8 = toBaseUnit( - fromBaseUnit(bnOrZero(sellAmountCryptoBaseUnit).times(tradeRateOrMinimum), sellAsset.precision), - THORCHAIN_FIXED_PRECISION, - ) - - const isValidSlippageRange = - bnOrZero(slippageTolerance).gte(0) && bnOrZero(slippageTolerance).lte(1) - if (bnOrZero(expectedBuyAmountCryptoPrecision8).lt(0) || !isValidSlippageRange) - return Err( - makeSwapErrorRight({ - message: '[getLimit]: bad expected buy amount or bad slippage tolerance', - code: SwapErrorType.BUILD_TRADE_FAILED, - details: { expectedBuyAmountCryptoPrecision8, slippageTolerance }, - }), - ) - - const buyAssetTradeFeeCryptoPrecision8 = convertPrecision({ - value: bnOrZero(protocolFees[buyAsset.assetId]?.amountCryptoBaseUnit), - inputExponent: buyAsset.precision, - outputExponent: THORCHAIN_FIXED_PRECISION, - }) - - const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL - const sellAssetAddressData = await getInboundAddressDataForChain(daemonUrl, sellAsset.assetId) - - const maybeRefundFeeBuyAssetCryptoPrecision8: Result = (() => { - switch (true) { - // If the sell asset is on THOR the return fee is fixed at 0.02 RUNE - case isRune(sellAsset.assetId): { - const runeFeeUsd = RUNE_OUTBOUND_TRANSACTION_FEE_CRYPTO_HUMAN.times(feeAssetUsdRate) - return Ok(toBaseUnit(bnOrZero(runeFeeUsd).div(buyAssetUsdRate), THORCHAIN_FIXED_PRECISION)) - } - // Else the return fee is the outbound fee of the sell asset's chain - default: { - return sellAssetAddressData.andThen(sellAssetAddressData => { - const sellAssetTradeFeeCryptoHuman = fromBaseUnit( - bnOrZero(sellAssetAddressData.outbound_fee), - THORCHAIN_FIXED_PRECISION, - ) - const sellAssetTradeFeeUsd = bnOrZero(sellAssetTradeFeeCryptoHuman).times(feeAssetUsdRate) - return Ok( - toBaseUnit( - bnOrZero(sellAssetTradeFeeUsd).div(buyAssetUsdRate), - THORCHAIN_FIXED_PRECISION, - ), - ) - }) - } - } - })() - - if (maybeRefundFeeBuyAssetCryptoPrecision8.isErr()) - return Err(maybeRefundFeeBuyAssetCryptoPrecision8.unwrapErr()) - const refundFeeBuyAssetCryptoPrecision8 = maybeRefundFeeBuyAssetCryptoPrecision8.unwrap() - - const highestPossibleFeeCryptoPrecision8 = max([ - // both fees are denominated in buy asset crypto precision 8 - bnOrZero(buyAssetTradeFeeCryptoPrecision8).toNumber(), - bnOrZero(refundFeeBuyAssetCryptoPrecision8).toNumber(), - ]) - - const expectedBuyAmountAfterHighestFeeCryptoPrecision8 = bnOrZero( - expectedBuyAmountCryptoPrecision8, - ) - .times(bn(1).minus(slippageTolerance)) - .times(bn(1).minus(ALLOWABLE_MARKET_MOVEMENT)) - .minus(highestPossibleFeeCryptoPrecision8 ?? 0) - .decimalPlaces(0) - - // expectedBuyAmountAfterHighestFeeCryptoPrecision8 can be negative if the sell asset has a higher inbound_fee - // (a refund) than the buy asset's inbound_fee + buy amount. - // I.e. we've come this far - we don't have enough to refund, so the limit can slide all the way to 0. - - return Ok( - expectedBuyAmountAfterHighestFeeCryptoPrecision8.isPositive() - ? expectedBuyAmountAfterHighestFeeCryptoPrecision8.toString() - : '0', - ) -} diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts index d1e70ee703d..a21cfa44cc8 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/getQuote/getQuote.ts @@ -2,6 +2,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import { bchAssetId } from '@shapeshiftoss/caip' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' +import BigNumber from 'bignumber.js' import { getConfig } from 'config' import qs from 'qs' import type { Asset } from 'lib/asset-service' @@ -17,8 +18,10 @@ import { THORCHAIN_AFFILIATE_NAME, THORCHAIN_FIXED_PRECISION, } from 'lib/swapper/swappers/ThorchainSwapper/utils/constants' +import { assertIsValidMemo } from 'lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/assertIsValidMemo' import { assetIdToPoolAssetId } from 'lib/swapper/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers' import { createTradeAmountTooSmallErr } from 'lib/swapper/utils' +import { subtractBasisPointAmount } from 'state/slices/tradeQuoteSlice/utils' import { thorService } from '../thorService' @@ -28,6 +31,7 @@ export const getQuote = async ({ sellAmountCryptoBaseUnit, receiveAddress, affiliateBps = '0', + slippageBps, }: { sellAsset: Asset buyAssetId: AssetId @@ -35,6 +39,7 @@ export const getQuote = async ({ // Receive address is optional for THOR quotes, and will be in case we are getting a quote with a missing manual receive address receiveAddress: string | undefined affiliateBps: string + slippageBps: string }): Promise> => { const buyPoolId = assetIdToPoolAssetId({ assetId: buyAssetId }) const sellPoolId = assetIdToPoolAssetId({ assetId: sellAsset.assetId }) @@ -94,6 +99,27 @@ export const getQuote = async ({ }), ) } else { - return Ok(data) + const memoWithManualSlippage = (() => { + const MEMO_PART_DELIMITER = ':' + // TODO: Woody you'll need to use expected_amount_out_streaming for streaming swaps + const { memo: quotedMemo, expected_amount_out: expectedAmountOut } = data + const memoParts = quotedMemo.split(MEMO_PART_DELIMITER) + + const pool = memoParts[1] + const address = memoParts[2] + const affiliate = memoParts[4] + const affiliateBps = memoParts[5] + + const limitWithManualSlippage = subtractBasisPointAmount( + expectedAmountOut, + slippageBps, + BigNumber.ROUND_DOWN, + ) + + const memo = `s${MEMO_PART_DELIMITER}${pool}${MEMO_PART_DELIMITER}${address}${MEMO_PART_DELIMITER}${limitWithManualSlippage}${MEMO_PART_DELIMITER}${affiliate}${MEMO_PART_DELIMITER}${affiliateBps}` + assertIsValidMemo(memo) + return memo + })() + return Ok({ ...data, memo: memoWithManualSlippage }) } } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts index 33aa611c728..be6db8c03b6 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts @@ -16,6 +16,7 @@ import type { import { getThorTxInfo } from 'lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData' import { assertUnreachable } from 'lib/utils' import { createBuildCustomApiTxInput } from 'lib/utils/evm' +import { convertDecimalPercentageToBasisPoints } from 'state/slices/tradeQuoteSlice/utils' import { isNativeEvmAsset } from '../../utils/helpers/helpers' import { getQuote } from './getQuote/getQuote' @@ -46,6 +47,7 @@ export const getSignTxFromQuote = async ({ const { recommendedSlippage } = quote const slippageTolerance = slippageTolerancePercentage ?? recommendedSlippage + const slippageBps = convertDecimalPercentageToBasisPoints(slippageTolerance).toString() const { buyAsset, @@ -85,9 +87,12 @@ export const getSignTxFromQuote = async ({ sellAmountCryptoBaseUnit, receiveAddress, affiliateBps, + slippageBps, }) if (maybeThornodeQuote.isErr()) throw maybeThornodeQuote.unwrapErr() + const thorchainQuote = maybeThornodeQuote.unwrap() + const { memo } = thorchainQuote const cosmosSdkChainAdapter = adapter as unknown as CosmosSdkBaseAdapter @@ -105,7 +110,7 @@ export const getSignTxFromQuote = async ({ affiliateBps, buyAssetUsdRate, feeAssetUsdRate, - thornodeQuote: maybeThornodeQuote.unwrap(), + memo, }) if (maybeTxData.isErr()) throw maybeTxData.unwrapErr() @@ -120,25 +125,20 @@ export const getSignTxFromQuote = async ({ sellAmountCryptoBaseUnit, receiveAddress, affiliateBps, + slippageBps, }) if (maybeThornodeQuote.isErr()) throw maybeThornodeQuote.unwrapErr() + const thorchainQuote = maybeThornodeQuote.unwrap() + const { memo } = thorchainQuote const utxoChainAdapter = adapter as unknown as UtxoBaseAdapter if (!chainSpecific) throw Error('missing UTXO chainSpecific parameters') const maybeThorTxInfo = await getThorTxInfo({ sellAsset, - buyAsset, - sellAmountCryptoBaseUnit, - slippageTolerance, - destinationAddress: receiveAddress, xpub: xpub!, - protocolFees: quote.steps[0].feeData.protocolFees, - affiliateBps, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote: maybeThornodeQuote.unwrap(), + memo, }) if (maybeThorTxInfo.isErr()) throw maybeThorTxInfo.unwrapErr() diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/assertIsValidMemo.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/assertIsValidMemo.ts index d9a44a8127e..8ba19149854 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/assertIsValidMemo.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/assertIsValidMemo.ts @@ -26,6 +26,7 @@ const thorChainAssetToChainId: Map = new Map([ ['DOGE', dogeChainId], ['GAIA', cosmosChainId], ['RUNE', thorchainChainId], + ['THOR', thorchainChainId], ]) export const isValidMemoAddress = (chainId: ChainId, thorId: string, address: string): boolean => { @@ -46,6 +47,7 @@ export const isValidMemoAddress = (chainId: ChainId, thorId: string, address: st // See https://github.com/shapeshift/lib/blob/6b5c9c8e855ffb68d865cfae8f545e7a819a9667/packages/swapper/src/swappers/thorchain/utils/makeSwapMemo/makeSwapMemo.ts#L10 // RUNE isn't a pool, it is the native asset of the THORChain network case thorId.startsWith('RUNE'): + case thorId.startsWith('THOR'): return address.startsWith('thor') default: return false diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts b/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts index 4978729c10a..492ee7c31dc 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts @@ -1,28 +1,14 @@ -import type { AssetId } from '@shapeshiftoss/caip' import type { Result } from '@sniptt/monads' -import { Err } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' import { getConfig } from 'config' import type { Asset } from 'lib/asset-service' -import type { ProtocolFee, SwapErrorRight } from 'lib/swapper/api' +import type { SwapErrorRight } from 'lib/swapper/api' import { getInboundAddressDataForChain } from 'lib/swapper/swappers/ThorchainSwapper/utils/getInboundAddressDataForChain' -import { getLimit } from 'lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit' -import { makeSwapMemo } from 'lib/swapper/swappers/ThorchainSwapper/utils/makeSwapMemo/makeSwapMemo' -import type { PartialRecord } from 'lib/utils' - -import type { ThornodeQuoteResponseSuccess } from '../../types' type GetThorTxInfoArgs = { sellAsset: Asset - buyAsset: Asset - sellAmountCryptoBaseUnit: string - slippageTolerance: string - destinationAddress: string | undefined xpub: string - protocolFees: PartialRecord - affiliateBps: string - buyAssetUsdRate: string - feeAssetUsdRate: string - thornodeQuote: ThornodeQuoteResponseSuccess + memo: string } type GetThorTxInfoReturn = Promise< Result< @@ -36,19 +22,7 @@ type GetThorTxInfoReturn = Promise< > type GetThorTxInfo = (args: GetThorTxInfoArgs) => GetThorTxInfoReturn -export const getThorTxInfo: GetThorTxInfo = async ({ - sellAsset, - buyAsset, - sellAmountCryptoBaseUnit, - slippageTolerance, - destinationAddress, - xpub, - protocolFees, - affiliateBps, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, -}) => { +export const getThorTxInfo: GetThorTxInfo = async ({ sellAsset, xpub, memo }) => { const daemonUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL const maybeInboundAddress = await getInboundAddressDataForChain( daemonUrl, @@ -60,29 +34,9 @@ export const getThorTxInfo: GetThorTxInfo = async ({ const inboundAddress = maybeInboundAddress.unwrap() const vault = inboundAddress.address - const maybeLimit = await getLimit({ - buyAsset, - sellAmountCryptoBaseUnit, - sellAsset, - slippageTolerance, - protocolFees, - buyAssetUsdRate, - feeAssetUsdRate, - thornodeQuote, - }) - - return maybeLimit.map(limit => { - const memo = makeSwapMemo({ - buyAssetId: buyAsset.assetId, - destinationAddress, - limit, - affiliateBps, - }) - - return { - opReturnData: memo, - vault, - pubkey: xpub, - } + return Ok({ + opReturnData: memo, + vault, + pubkey: xpub, }) } diff --git a/src/state/slices/tradeQuoteSlice/utils.test.ts b/src/state/slices/tradeQuoteSlice/utils.test.ts index 28379df5f2f..f63efd5e0ac 100644 --- a/src/state/slices/tradeQuoteSlice/utils.test.ts +++ b/src/state/slices/tradeQuoteSlice/utils.test.ts @@ -1,4 +1,5 @@ import type { AssetId } from '@shapeshiftoss/caip' +import BigNumber from 'bignumber.js' import { baseUnitToHuman, bn, convertPrecision } from 'lib/bignumber/bignumber' import type { ProtocolFee } from 'lib/swapper/api' import { BTC, ETH, FOX_MAINNET } from 'lib/swapper/swappers/utils/test-data/assets' @@ -134,7 +135,11 @@ describe('subtractBasisPoints', () => { }) test('should round up correctly', () => { - const result = subtractBasisPointAmount('123456789012345678901234567890', '100', true) + const result = subtractBasisPointAmount( + '123456789012345678901234567890', + '100', + BigNumber.ROUND_UP, + ) expect(result).toBe('122222221122222222112222222212') }) }) diff --git a/src/state/slices/tradeQuoteSlice/utils.ts b/src/state/slices/tradeQuoteSlice/utils.ts index d341b925ecb..958f2829d19 100644 --- a/src/state/slices/tradeQuoteSlice/utils.ts +++ b/src/state/slices/tradeQuoteSlice/utils.ts @@ -1,7 +1,8 @@ // Helper function to convert basis points to percentage import type { AssetId } from '@shapeshiftoss/caip' import type { MarketData } from '@shapeshiftoss/types' -import { BigNumber, bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' +import type { BigNumber } from 'lib/bignumber/bignumber' +import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' import type { ProtocolFee } from 'lib/swapper/api' import type { PartialRecord } from 'lib/utils' @@ -26,13 +27,13 @@ type SumProtocolFeesToDenomArgs = { * * @param value The value to subtract basis points from. * @param basisPoints The number of basis points to subtract. - * @param roundUp Round up the result to the nearest integer. + * @param roundingMode * @returns The new number that is the input value minus the basis points of the value. */ export const subtractBasisPointAmount = ( value: string, basisPoints: string, - roundUp?: boolean, + roundingMode?: BigNumber.RoundingMode, ): string => { const bigNumValue = bn(value) @@ -42,7 +43,7 @@ export const subtractBasisPointAmount = ( // Subtract basis points from the original value const resultValue = bigNumValue.minus(subtractValue) - return roundUp ? resultValue.toFixed(0, BigNumber.ROUND_UP) : resultValue.toFixed() + return roundingMode !== undefined ? resultValue.toFixed(0, roundingMode) : resultValue.toFixed() } // this converts the collection of protocol fees denominated in various assets to the sum of all of