diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index 1276967b124..e424255ed1c 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -302,11 +302,11 @@ const FirstHop = ({ const { buyAsset, sellAsset, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit, } = tradeQuoteStep const sellAmountCryptoPrecision = fromBaseUnit( - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset.precision, ) const buyAmountCryptoPrecision = fromBaseUnit( @@ -472,11 +472,11 @@ const SecondHop = ({ const { buyAsset, sellAsset, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit, } = tradeQuoteStep const sellAmountCryptoPrecision = fromBaseUnit( - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset.precision, ) const buyAmountCryptoPrecision = fromBaseUnit( diff --git a/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx b/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx index f34ce076eed..50f6666d557 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/ReceiveSummary.tsx @@ -19,6 +19,8 @@ import { bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' import type { AmountDisplayMeta, ProtocolFee } from 'lib/swapper/api' import { SwapperName } from 'lib/swapper/api' +import type { PartialRecord } from 'lib/utils' +import { isSome } from 'lib/utils' type ReceiveSummaryProps = { isLoading?: boolean @@ -27,7 +29,7 @@ type ReceiveSummaryProps = { intermediaryTransactionOutputs?: AmountDisplayMeta[] fiatAmount?: string amountBeforeFeesCryptoPrecision?: string - protocolFees?: Record + protocolFees?: PartialRecord shapeShiftFee?: string slippage: string swapperName: string @@ -72,7 +74,10 @@ export const ReceiveSummary: FC = memo( }, []) const protocolFeesParsed = useMemo( - () => (protocolFees ? parseAmountDisplayMeta(Object.values(protocolFees)) : undefined), + () => + protocolFees + ? parseAmountDisplayMeta(Object.values(protocolFees).filter(isSome)) + : undefined, [protocolFees, parseAmountDisplayMeta], ) diff --git a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx index 16532a44365..943e051c7cd 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/TradeConfirm.tsx @@ -55,9 +55,9 @@ import { selectFirstHopSellFeeAsset, selectLastHop, selectLastHopBuyAsset, - selectNetBuyAmountCryptoPrecision, - selectNetBuyAmountUserCurrency, + selectNetReceiveAmountCryptoPrecision, selectQuoteDonationAmountUserCurrency, + selectReceiveBuyAmountUserCurrency, selectSellAmountBeforeFeesCryptoPrecision, selectSellAmountUserCurrency, selectTotalNetworkFeeUserCurrencyPrecision, @@ -125,9 +125,9 @@ export const TradeConfirm = () => { const lastStep = useAppSelector(selectLastHop) const swapperName = useAppSelector(selectActiveSwapperName) const defaultFeeAsset = useAppSelector(selectFirstHopSellFeeAsset) - const netBuyAmountCryptoPrecision = useAppSelector(selectNetBuyAmountCryptoPrecision) + const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectNetReceiveAmountCryptoPrecision) const slippageDecimal = useAppSelector(selectTradeSlippagePercentageDecimal) - const netBuyAmountUserCurrency = useAppSelector(selectNetBuyAmountUserCurrency) + const netBuyAmountUserCurrency = useAppSelector(selectReceiveBuyAmountUserCurrency) const buyAmountBeforeFeesUserCurrency = useAppSelector(selectBuyAmountBeforeFeesUserCurrency) const sellAmountBeforeFeesUserCurrency = useAppSelector(selectSellAmountUserCurrency) const networkFeeCryptoHuman = useAppSelector(selectFirstHopNetworkFeeCryptoPrecision) @@ -365,7 +365,7 @@ export const TradeConfirm = () => { { sellAsset?.symbol, sellAmountBeforeFeesUserCurrency, buyAsset?.symbol, - netBuyAmountCryptoPrecision, + buyAmountAfterFeesCryptoPrecision, buyAmountBeforeFeesCryptoPrecision, tradeQuoteStep?.feeData.protocolFees, tradeQuoteStep?.intermediaryTransactionOutputs, diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index e8369b5c71e..b07739e6305 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -58,8 +58,8 @@ import { selectBuyAmountBeforeFeesCryptoPrecision, selectBuyAmountBeforeFeesUserCurrency, selectFirstHop, - selectNetBuyAmountUserCurrency, selectNetReceiveAmountCryptoPrecision, + selectReceiveBuyAmountUserCurrency, selectSellAmountUserCurrency, selectSwapperSupportsCrossAccountTrade, selectTotalNetworkFeeUserCurrencyPrecision, @@ -105,7 +105,7 @@ export const TradeInput = memo(() => { const totalProtocolFees = useAppSelector(selectTotalProtocolFeeByAsset) const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectNetReceiveAmountCryptoPrecision) const buyAmountBeforeFeesUserCurrency = useAppSelector(selectBuyAmountBeforeFeesUserCurrency) - const buyAmountAfterFeesUserCurrency = useAppSelector(selectNetBuyAmountUserCurrency) + const buyAmountAfterFeesUserCurrency = useAppSelector(selectReceiveBuyAmountUserCurrency) const totalNetworkFeeFiatPrecision = useAppSelector(selectTotalNetworkFeeUserCurrencyPrecision) const manualReceiveAddressIsValidating = useAppSelector(selectManualReceiveAddressIsValidating) const sellAmountCryptoPrecision = useAppSelector(selectSellAmountCryptoPrecision) diff --git a/src/components/MultiHopTrade/hooks/useAllowanceApproval/helpers.ts b/src/components/MultiHopTrade/hooks/useAllowanceApproval/helpers.ts index a060deb6f65..14c016e8304 100644 --- a/src/components/MultiHopTrade/hooks/useAllowanceApproval/helpers.ts +++ b/src/components/MultiHopTrade/hooks/useAllowanceApproval/helpers.ts @@ -37,7 +37,9 @@ export const checkApprovalNeeded = async ( chainId: sellAsset.chainId, }) - return bn(allowanceOnChainCryptoBaseUnit).lt(tradeQuoteStep.sellAmountBeforeFeesCryptoBaseUnit) + return bn(allowanceOnChainCryptoBaseUnit).lt( + tradeQuoteStep.sellAmountIncludingProtocolFeesCryptoBaseUnit, + ) } export const getApprovalTxData = async ( @@ -47,7 +49,7 @@ export const getApprovalTxData = async ( isExactAllowance: boolean, ): Promise<{ buildCustomTxInput: evm.BuildCustomTxInput; networkFeeCryptoBaseUnit: string }> => { const approvalAmountCryptoBaseUnit = isExactAllowance - ? tradeQuoteStep.sellAmountBeforeFeesCryptoBaseUnit + ? tradeQuoteStep.sellAmountIncludingProtocolFeesCryptoBaseUnit : MAX_ALLOWANCE const { assetReference } = fromAssetId(tradeQuoteStep.sellAsset.assetId) diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteArgs.ts b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteArgs.ts index d99e9fa1c8f..679af0b263e 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteArgs.ts +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteArgs.ts @@ -43,7 +43,7 @@ export const getTradeQuoteArgs = async ({ }: GetTradeQuoteInputArgs): Promise => { if (!sellAsset || !buyAsset) return undefined const tradeQuoteInputCommonArgs: TradeQuoteInputCommonArgs = { - sellAmountBeforeFeesCryptoBaseUnit: toBaseUnit( + sellAmountIncludingProtocolFeesCryptoBaseUnit: toBaseUnit( sellAmountBeforeFeesCryptoPrecision, sellAsset?.precision || 0, ), diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index 98d8b044bb5..919505597fd 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -43,6 +43,7 @@ type GetMixPanelDataFromApiQuotesReturn = { sellAssetId: string | undefined buyAssetId: string | undefined sellAmountUsd: string | undefined + version: string // ISO 8601 standard basic format date } const getMixPanelDataFromApiQuotes = (quotes: ApiQuote[]): GetMixPanelDataFromApiQuotesReturn => { @@ -62,7 +63,10 @@ const getMixPanelDataFromApiQuotes = (quotes: ApiQuote[]): GetMixPanelDataFromAp }) .filter(isSome) - return { quoteMeta, sellAssetId, buyAssetId, sellAmountUsd } + // Add a version string, in the form of an ISO 8601 standard basic format date, to the JSON blob to help with reporting + const version = '20230823' + + return { quoteMeta, sellAssetId, buyAssetId, sellAmountUsd, version } } const isEqualExceptAffiliateBpsAndSlippage = ( diff --git a/src/components/MultiHopTrade/types.ts b/src/components/MultiHopTrade/types.ts index 751d3bf2f44..1f358060c3e 100644 --- a/src/components/MultiHopTrade/types.ts +++ b/src/components/MultiHopTrade/types.ts @@ -52,7 +52,7 @@ export type GetReceiveAddressArgs = { export type TradeQuoteInputCommonArgs = Pick< GetTradeQuoteInput, - | 'sellAmountBeforeFeesCryptoBaseUnit' + | 'sellAmountIncludingProtocolFeesCryptoBaseUnit' | 'sellAsset' | 'buyAsset' | 'receiveAddress' diff --git a/src/lib/swapper/api.ts b/src/lib/swapper/api.ts index 32007df968b..9bcc2c83346 100644 --- a/src/lib/swapper/api.ts +++ b/src/lib/swapper/api.ts @@ -6,6 +6,7 @@ import type { ChainSpecific, KnownChainIds, UtxoAccountType } from '@shapeshifto import type { TxStatus } from '@shapeshiftoss/unchained-client' import type { Result } from '@sniptt/monads' import type { Asset } from 'lib/asset-service' +import type { PartialRecord } from 'lib/utils' import type { ReduxState } from 'state/reducer' import type { AccountMetadata } from 'state/slices/portfolioSlice/portfolioSliceCommon' @@ -64,7 +65,7 @@ export type ProtocolFee = { requiresBalance: boolean } & AmountDisplayMeta export type QuoteFeeData = { networkFeeCryptoBaseUnit: string | undefined // fee paid to the network from the fee asset (undefined if unknown) - protocolFees: Record // fee(s) paid to the protocol(s) + protocolFees: PartialRecord // fee(s) paid to the protocol(s) } & ChainSpecificQuoteFeeData export type BuyAssetBySellIdInput = { @@ -75,7 +76,7 @@ export type BuyAssetBySellIdInput = { type CommonTradeInput = { sellAsset: Asset buyAsset: Asset - sellAmountBeforeFeesCryptoBaseUnit: string + sellAmountIncludingProtocolFeesCryptoBaseUnit: string sendAddress?: string receiveAddress: string accountNumber: number @@ -108,12 +109,12 @@ export type GetTradeQuoteInput = export type AmountDisplayMeta = { amountCryptoBaseUnit: string - asset: Pick + asset: Partial & Pick } export type TradeBase = { buyAmountBeforeFeesCryptoBaseUnit: string - sellAmountBeforeFeesCryptoBaseUnit: string + sellAmountIncludingProtocolFeesCryptoBaseUnit: string feeData: QuoteFeeData rate: string sources: SwapSource[] @@ -134,6 +135,7 @@ export type TradeQuote = { recommendedSlippage?: string id?: string steps: TradeQuoteStep[] + rate: string // top-level rate for all steps (i.e. output amount / input amount) } export type SwapSource = { diff --git a/src/lib/swapper/swappers/CowSwapper/endpoints.ts b/src/lib/swapper/swappers/CowSwapper/endpoints.ts index 8f4394816a0..d2b45663e69 100644 --- a/src/lib/swapper/swappers/CowSwapper/endpoints.ts +++ b/src/lib/swapper/swappers/CowSwapper/endpoints.ts @@ -62,7 +62,7 @@ export const cowApi: Swapper2Api = { }, getUnsignedTx: async ({ from, tradeQuote, stepIndex }: GetUnsignedTxArgs): Promise => { - const { accountNumber, buyAsset, sellAsset, sellAmountBeforeFeesCryptoBaseUnit } = + const { accountNumber, buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = tradeQuote.steps[stepIndex] const { receiveAddress } = tradeQuote @@ -88,7 +88,7 @@ export const cowApi: Swapper2Api = { partiallyFillable: false, from, kind: ORDER_KIND_SELL, - sellAmountBeforeFee: sellAmountBeforeFeesCryptoBaseUnit, + sellAmountBeforeFee: sellAmountIncludingProtocolFeesCryptoBaseUnit, }, ) diff --git a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts index 8fbf217b836..f36d4570be1 100644 --- a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts +++ b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.test.ts @@ -92,7 +92,9 @@ const expectedApiInputUsdcGnosisToXdai: CowSwapSellQuoteApiInput = { } const expectedTradeQuoteWethToFox: TradeQuote = { + id: '123', minimumCryptoHuman: '0.01621193001101461472', + rate: '14924.80846543344314936607', // 14942 FOX per WETH steps: [ { allowanceContract: '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110', @@ -107,7 +109,7 @@ const expectedTradeQuoteWethToFox: TradeQuote = { }, networkFeeCryptoBaseUnit: '0', }, - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000', buyAmountBeforeFeesCryptoBaseUnit: '14913256100953839475750', // 14913 FOX sources: [{ name: SwapperName.CowSwap, proportion: '1' }], buyAsset: FOX_MAINNET, @@ -118,7 +120,9 @@ const expectedTradeQuoteWethToFox: TradeQuote = { } const expectedTradeQuoteFoxToEth: TradeQuote = { + id: '123', minimumCryptoHuman: '229.09507445589919816724', + rate: '0.00004995640398295996', steps: [ { allowanceContract: '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110', @@ -133,7 +137,7 @@ const expectedTradeQuoteFoxToEth: TradeQuote = { }, networkFeeCryptoBaseUnit: '0', }, - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000000', buyAmountBeforeFeesCryptoBaseUnit: '51242479117266593', sources: [{ name: SwapperName.CowSwap, proportion: '1' }], buyAsset: ETH, @@ -144,7 +148,9 @@ const expectedTradeQuoteFoxToEth: TradeQuote = { } const expectedTradeQuoteUsdcToXdai: TradeQuote = { + id: '123', minimumCryptoHuman: '0.00999000999000999001', + rate: '1.0003121775396440882', steps: [ { allowanceContract: '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110', @@ -159,7 +165,7 @@ const expectedTradeQuoteUsdcToXdai: TradeQuote = { }, networkFeeCryptoBaseUnit: '0', }, - sellAmountBeforeFeesCryptoBaseUnit: '20000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000000', buyAmountBeforeFeesCryptoBaseUnit: '21006555357465608755', sources: [{ name: SwapperName.CowSwap, proportion: '1' }], buyAsset: XDAI, @@ -170,7 +176,9 @@ const expectedTradeQuoteUsdcToXdai: TradeQuote = { } const expectedTradeQuoteSmallAmountWethToFox: TradeQuote = { + id: '123', minimumCryptoHuman: '0.01621193001101461472', + rate: '14716.04718939437523468382', // 14716 FOX per WETH steps: [ { allowanceContract: '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110', @@ -185,7 +193,7 @@ const expectedTradeQuoteSmallAmountWethToFox: TradeQuote { chainId: KnownChainIds.EthereumMainnet, sellAsset: ETH, buyAsset: FOX_MAINNET, - sellAmountBeforeFeesCryptoBaseUnit: '11111', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '11111', accountNumber: 0, receiveAddress: DEFAULT_ADDRESS, affiliateBps: '0', @@ -229,7 +237,7 @@ describe('getCowTradeQuote', () => { chainId: KnownChainIds.EthereumMainnet, sellAsset: WETH, buyAsset: FOX_MAINNET, - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000', accountNumber: 0, receiveAddress: DEFAULT_ADDRESS, affiliateBps: '0', @@ -242,6 +250,7 @@ describe('getCowTradeQuote', () => { Promise.resolve( Ok({ data: { + id: 123, quote: { ...expectedApiInputWethToFox, sellAmountBeforeFee: undefined, @@ -274,7 +283,7 @@ describe('getCowTradeQuote', () => { chainId: KnownChainIds.EthereumMainnet, sellAsset: FOX_MAINNET, buyAsset: ETH, - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000000', accountNumber: 0, receiveAddress: DEFAULT_ADDRESS, affiliateBps: '0', @@ -287,6 +296,7 @@ describe('getCowTradeQuote', () => { Promise.resolve( Ok({ data: { + id: 123, quote: { ...expectedApiInputFoxToEth, sellAmountBeforeFee: undefined, @@ -319,7 +329,7 @@ describe('getCowTradeQuote', () => { chainId: KnownChainIds.GnosisMainnet, sellAsset: USDC_GNOSIS, buyAsset: XDAI, - sellAmountBeforeFeesCryptoBaseUnit: '20000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000000', accountNumber: 0, receiveAddress: DEFAULT_ADDRESS, affiliateBps: '0', @@ -332,6 +342,7 @@ describe('getCowTradeQuote', () => { Promise.resolve( Ok({ data: { + id: 123, quote: { ...expectedApiInputUsdcGnosisToXdai, sellAmountBeforeFee: undefined, @@ -364,7 +375,7 @@ describe('getCowTradeQuote', () => { chainId: KnownChainIds.EthereumMainnet, sellAsset: WETH, buyAsset: FOX_MAINNET, - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000', accountNumber: 0, receiveAddress: DEFAULT_ADDRESS, affiliateBps: '0', @@ -377,6 +388,7 @@ describe('getCowTradeQuote', () => { Promise.resolve( Ok({ data: { + id: 123, quote: { ...expectedApiInputSmallAmountWethToFox, sellAmountBeforeFee: undefined, diff --git a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts index b0dab8e1c04..71f3d88c813 100644 --- a/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts +++ b/src/lib/swapper/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts @@ -35,7 +35,7 @@ export async function getCowSwapTradeQuote( ): Promise, SwapErrorRight>> { const { sellAsset, buyAsset, accountNumber, chainId, receiveAddress } = input const supportedChainIds = getSupportedChainIds() - const sellAmount = input.sellAmountBeforeFeesCryptoBaseUnit + const sellAmount = input.sellAmountIncludingProtocolFeesCryptoBaseUnit const assertion = assertValidTrade({ buyAsset, sellAsset, supportedChainIds, receiveAddress }) if (assertion.isErr()) return Err(assertion.unwrapErr()) @@ -107,7 +107,8 @@ export async function getCowSwapTradeQuote( const quote: TradeQuote = { minimumCryptoHuman, - id: data.id, + id: data.id.toString(), + rate, steps: [ { allowanceContract: COW_SWAP_VAULT_RELAYER_ADDRESS, @@ -122,7 +123,7 @@ export async function getCowSwapTradeQuote( }, }, }, - sellAmountBeforeFeesCryptoBaseUnit: normalizedSellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: normalizedSellAmountCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit: buyAmountCryptoBaseUnit, sources: DEFAULT_SOURCE, buyAsset, diff --git a/src/lib/swapper/swappers/CowSwapper/types.ts b/src/lib/swapper/swappers/CowSwapper/types.ts index 837e6e1a3a7..bc06857aaa5 100644 --- a/src/lib/swapper/swappers/CowSwapper/types.ts +++ b/src/lib/swapper/swappers/CowSwapper/types.ts @@ -18,7 +18,7 @@ export type CowSwapQuoteResponse = { } from: string expiration: string - id: string + id: number } export enum CowNetwork { diff --git a/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.test.ts b/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.test.ts index 17108c5e578..4346b348145 100644 --- a/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.test.ts +++ b/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.test.ts @@ -46,7 +46,7 @@ describe('utils', () => { receiver: '0xFc81A7B9f715A344A7c4ABFc444A774c3E9BA42D', sellTokenBalance: 'erc20', buyTokenBalance: 'erc20', - quoteId: '1', + quoteId: 1, } const orderDigest = hashOrder(domain(1, '0x9008D19f58AAbD9eD0D60971565AA8510560ab41'), order) diff --git a/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.ts b/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.ts index 5b7d37648ed..854282ab1e3 100644 --- a/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.ts +++ b/src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.ts @@ -48,7 +48,7 @@ export type CowSwapOrder = { receiver: string sellTokenBalance: string buyTokenBalance: string - quoteId: string + quoteId: number } export type CowSwapQuoteApiInputBase = { diff --git a/src/lib/swapper/swappers/LifiSwapper/endpoints.ts b/src/lib/swapper/swappers/LifiSwapper/endpoints.ts index eb21bf493a1..49c52496bb3 100644 --- a/src/lib/swapper/swappers/LifiSwapper/endpoints.ts +++ b/src/lib/swapper/swappers/LifiSwapper/endpoints.ts @@ -30,7 +30,7 @@ export const lifiApi: Swapper2Api = { input: GetTradeQuoteInput, { assets, sellAssetUsdRate }: TradeQuoteDeps, ): Promise> => { - if (input.sellAmountBeforeFeesCryptoBaseUnit === '0') { + if (input.sellAmountIncludingProtocolFeesCryptoBaseUnit === '0') { return Err( makeSwapErrorRight({ message: 'sell amount too low', diff --git a/src/lib/swapper/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts index a2ea6162771..ba8f73f7ff9 100644 --- a/src/lib/swapper/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/LifiSwapper/getTradeQuote/getTradeQuote.ts @@ -34,7 +34,7 @@ export async function getTradeQuote( chainId, sellAsset, buyAsset, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sendAddress, receiveAddress, accountNumber, @@ -71,7 +71,7 @@ export async function getTradeQuote( // this swapper is not cross-account so this works fromAddress: sendAddress, toAddress: receiveAddress, - fromAmount: sellAmountBeforeFeesCryptoBaseUnit, + fromAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, // as recommended by lifi, dodo is denied until they fix their gas estimates // TODO: convert this config to .env variable options: { @@ -122,6 +122,7 @@ export async function getTradeQuote( const steps = await Promise.all( selectedLifiRoute.steps.map(async lifiStep => { // for the rate to be valid, both amounts must be converted to the same precision + // TODO: this should be the step rate. It's currently the entire route rate. const estimateRate = convertPrecision({ value: selectedLifiRoute.toAmountMin, inputExponent: buyAsset.precision, @@ -157,10 +158,9 @@ export async function getTradeQuote( protocolFees, networkFeeCryptoBaseUnit, }, - // TODO(woodenfurniture): the rate should be top level not step level // might be better replaced by inputOutputRatio downstream rate: estimateRate, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset, sources: [ { name: `${selectedLifiRoute.steps[0].tool} (${SwapperName.LIFI})`, proportion: '1' }, @@ -170,12 +170,22 @@ export async function getTradeQuote( ) const isSameChainSwap = sellAsset.chainId === buyAsset.chainId + + // The rate for the entire multi-hop swap + const netRate = convertPrecision({ + value: selectedLifiRoute.toAmountMin, + inputExponent: buyAsset.precision, + outputExponent: sellAsset.precision, + }) + .dividedBy(bn(selectedLifiRoute.fromAmount)) + .toString() // TODO(gomes): intermediary error-handling within this module function calls return Ok({ minimumCryptoHuman: getMinimumCryptoHuman( sellAssetPriceUsdPrecision, isSameChainSwap, ).toString(), + rate: netRate, steps, selectedLifiRoute, }) diff --git a/src/lib/swapper/swappers/LifiSwapper/utils/transformLifiFeeData/transformLifiFeeData.ts b/src/lib/swapper/swappers/LifiSwapper/utils/transformLifiFeeData/transformLifiFeeData.ts index 76cd26d91c0..688789ef933 100644 --- a/src/lib/swapper/swappers/LifiSwapper/utils/transformLifiFeeData/transformLifiFeeData.ts +++ b/src/lib/swapper/swappers/LifiSwapper/utils/transformLifiFeeData/transformLifiFeeData.ts @@ -41,6 +41,7 @@ export const transformLifiStepFeeData = ({ chainId: asset?.chainId ?? lifiChainIdToChainId(token.chainId), precision: asset?.precision ?? token.decimals, symbol: asset?.symbol ?? token.symbol, + ...asset, }, requiresBalance: true, } diff --git a/src/lib/swapper/swappers/OneInchSwapper/endpoints.ts b/src/lib/swapper/swappers/OneInchSwapper/endpoints.ts index 165edd7fe88..1929548cd0c 100644 --- a/src/lib/swapper/swappers/OneInchSwapper/endpoints.ts +++ b/src/lib/swapper/swappers/OneInchSwapper/endpoints.ts @@ -25,10 +25,15 @@ export const oneInchApi: Swapper2Api = { input: GetTradeQuoteInput, { sellAssetUsdRate }: { sellAssetUsdRate: string }, ): Promise> => { - const { sellAsset, sellAmountBeforeFeesCryptoBaseUnit, affiliateBps, receiveAddress } = input + const { + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + affiliateBps, + receiveAddress, + } = input const sellAmountBeforeFeesCryptoPrecision = fromBaseUnit( - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset.precision, ) const sellAmountBeforeFeesUsd = bnOrZero(sellAmountBeforeFeesCryptoPrecision).times( @@ -65,7 +70,7 @@ export const oneInchApi: Swapper2Api = { tradeQuote, stepIndex, }: GetUnsignedTxArgs): Promise => { - const { accountNumber, buyAsset, sellAsset, sellAmountBeforeFeesCryptoBaseUnit } = + const { accountNumber, buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = tradeQuote.steps[stepIndex] const { receiveAddress, affiliateBps } = tradeQuote @@ -78,7 +83,7 @@ export const oneInchApi: Swapper2Api = { affiliateBps, buyAsset, receiveAddress, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset, maximumSlippageDecimalPercentage: slippageTolerancePercentageDecimal, }) diff --git a/src/lib/swapper/swappers/OneInchSwapper/getTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/OneInchSwapper/getTradeQuote/getTradeQuote.ts index f1029f29fd3..9d0358a32ab 100644 --- a/src/lib/swapper/swappers/OneInchSwapper/getTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/OneInchSwapper/getTradeQuote/getTradeQuote.ts @@ -31,7 +31,7 @@ export async function getTradeQuote( receiveAddress, } = input const apiUrl = getConfig().REACT_APP_ONE_INCH_API_URL - const sellAmountBeforeFeesCryptoBaseUnit = input.sellAmountBeforeFeesCryptoBaseUnit + const sellAmountBeforeFeesCryptoBaseUnit = input.sellAmountIncludingProtocolFeesCryptoBaseUnit const assertion = assertValidTrade({ buyAsset, sellAsset, receiveAddress }) if (assertion.isErr()) return Err(assertion.unwrapErr()) @@ -86,6 +86,7 @@ export async function getTradeQuote( return Ok({ minimumCryptoHuman, + rate, steps: [ { allowanceContract, @@ -94,7 +95,7 @@ export async function getTradeQuote( sellAsset, accountNumber, buyAmountBeforeFeesCryptoBaseUnit: buyAmountCryptoBaseUnit, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, feeData: { protocolFees: {}, networkFeeCryptoBaseUnit, diff --git a/src/lib/swapper/swappers/OneInchSwapper/utils/fetchOneInchSwap.ts b/src/lib/swapper/swappers/OneInchSwapper/utils/fetchOneInchSwap.ts index c9aa55a95e6..3970945fa67 100644 --- a/src/lib/swapper/swappers/OneInchSwapper/utils/fetchOneInchSwap.ts +++ b/src/lib/swapper/swappers/OneInchSwapper/utils/fetchOneInchSwap.ts @@ -12,7 +12,7 @@ export type FetchOneInchSwapInput = { affiliateBps: string | undefined buyAsset: Asset receiveAddress: string - sellAmountBeforeFeesCryptoBaseUnit: string + sellAmountIncludingProtocolFeesCryptoBaseUnit: string sellAsset: Asset maximumSlippageDecimalPercentage: string } @@ -21,7 +21,7 @@ export const fetchOneInchSwap = async ({ affiliateBps, buyAsset, receiveAddress, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset, maximumSlippageDecimalPercentage, }: FetchOneInchSwapInput) => { @@ -43,7 +43,7 @@ export const fetchOneInchSwap = async ({ // 1inch uses this to check allowance on their side // this swapper is not cross-account so this works fromAddress: receiveAddress, - amount: sellAmountBeforeFeesCryptoBaseUnit, + amount: sellAmountIncludingProtocolFeesCryptoBaseUnit, slippage: maximumSlippagePercentage, allowPartialFill: false, referrerAddress: getTreasuryAddressFromChainId(buyAsset.chainId), diff --git a/src/lib/swapper/swappers/OsmosisSwapper/endpoints.ts b/src/lib/swapper/swappers/OsmosisSwapper/endpoints.ts index 2cd8bd169b4..2ea669d067d 100644 --- a/src/lib/swapper/swappers/OsmosisSwapper/endpoints.ts +++ b/src/lib/swapper/swappers/OsmosisSwapper/endpoints.ts @@ -57,7 +57,7 @@ export const osmosisApi: Swapper2Api = { accountNumber, buyAsset: stepBuyAsset, sellAsset: stepSellAsset, - sellAmountBeforeFeesCryptoBaseUnit: stepSellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: stepSellAmountBeforeFeesCryptoBaseUnit, } = tradeQuote.steps[stepIndex] const quoteSellAsset = tradeQuote.steps[0].sellAsset const { receiveAddress, receiveAccountNumber } = tradeQuote diff --git a/src/lib/swapper/swappers/OsmosisSwapper/getTradeQuote/getMultiHopTradeQuote.ts b/src/lib/swapper/swappers/OsmosisSwapper/getTradeQuote/getMultiHopTradeQuote.ts index 6f3b28dfa7b..c2ffe230ec3 100644 --- a/src/lib/swapper/swappers/OsmosisSwapper/getTradeQuote/getMultiHopTradeQuote.ts +++ b/src/lib/swapper/swappers/OsmosisSwapper/getTradeQuote/getMultiHopTradeQuote.ts @@ -31,7 +31,7 @@ export const getTradeQuote = async ( accountNumber, sellAsset, buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, } = input if (!sellAmountCryptoBaseUnit) { return Err( @@ -147,7 +147,7 @@ export const getTradeQuote = async ( accountNumber, rate, sellAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit: sellAssetIsOnOsmosisNetwork ? buyAmountCryptoBaseUnit // OSMO -> ATOM, the ATOM on OSMO before fees is the same as the ATOM buy amount intent : sellAmountCryptoBaseUnit, // ATOM -> ATOM, the ATOM on OSMO before fees is the same as the sold ATOM amount @@ -166,7 +166,7 @@ export const getTradeQuote = async ( accountNumber, rate, sellAsset: atomOnOsmosisAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAssetIsOnOsmosisNetwork + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAssetIsOnOsmosisNetwork ? bnOrZero(firstStep.buyAmountBeforeFeesCryptoBaseUnit) .minus(firstHopFeeData.slow.txFee) .toString() @@ -178,6 +178,7 @@ export const getTradeQuote = async ( } return Ok({ + rate, minimumCryptoHuman, steps: [firstStep, secondStep], }) diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts index 08a96259e57..3916262aa03 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.test.ts @@ -41,13 +41,14 @@ jest.mock('config', () => { const expectedQuoteResponse: ThorEvmTradeQuote = { minimumCryptoHuman: '149.14668013703712946932', + rate: '144114.94366197183098591549', recommendedSlippage: '0.04357', data: '0x', router: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', steps: [ { allowanceContract: '0x3624525075b88B24ecc29CE226b0CEc1fFcB6976', - sellAmountBeforeFeesCryptoBaseUnit: '713014679420', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420', buyAmountBeforeFeesCryptoBaseUnit: '114321610000000000', feeData: { protocolFees: { @@ -119,7 +120,7 @@ describe('getTradeQuote', () => { const input: GetTradeQuoteInput = { ...quoteInput, - sellAmountBeforeFeesCryptoBaseUnit: '713014679420', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '713014679420', buyAsset: ETH, sellAsset: FOX_MAINNET, } diff --git a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts index 67ddda706c3..6e649b54b92 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote.ts @@ -61,7 +61,7 @@ export const getThorTradeQuote = async ( const { sellAsset, buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, slippageTolerancePercentage, accountNumber, chainId, @@ -193,7 +193,7 @@ export const getThorTradeQuote = async ( const commonStepFields = { rate, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit: buyAmountCryptoBaseUnit, sources: [{ name: SwapperName.Thorchain, proportion: '1' }], buyAsset, @@ -253,6 +253,7 @@ export const getThorTradeQuote = async ( return Ok({ ...commonQuoteFields, + rate, data, router, steps: [ @@ -298,6 +299,7 @@ export const getThorTradeQuote = async ( return Ok({ ...commonQuoteFields, + rate, steps: [ { ...commonStepFields, @@ -319,6 +321,7 @@ export const getThorTradeQuote = async ( return Ok({ ...commonQuoteFields, + rate, steps: [ { ...commonStepFields, diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts index f827a02b66d..3dc162429d0 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/getLimit/getLimit.ts @@ -19,6 +19,7 @@ import { } 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' export type GetLimitArgs = { receiveAddress: string | undefined @@ -26,7 +27,7 @@ export type GetLimitArgs = { sellAsset: Asset sellAmountCryptoBaseUnit: string slippageTolerance: string - protocolFees: Record + protocolFees: PartialRecord affiliateBps: string buyAssetUsdRate: string feeAssetUsdRate: string diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts b/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts index 762de3af528..c6e06d6ff12 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utils/getSignTxFromQuote.ts @@ -48,7 +48,7 @@ export const getSignTxFromQuote = async ({ const { buyAsset, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, sellAsset, accountNumber, } = quote.steps[0] diff --git a/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts b/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts index 831fe7dc8cf..56a96032498 100644 --- a/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts +++ b/src/lib/swapper/swappers/ThorchainSwapper/utxo/utils/getThorTxData.ts @@ -7,6 +7,7 @@ import type { ProtocolFee, 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' type GetThorTxInfoArgs = { sellAsset: Asset @@ -15,7 +16,7 @@ type GetThorTxInfoArgs = { slippageTolerance: string destinationAddress: string | undefined xpub: string - protocolFees: Record + protocolFees: PartialRecord affiliateBps: string buyAssetUsdRate: string feeAssetUsdRate: string diff --git a/src/lib/swapper/swappers/ZrxSwapper/endpoints.ts b/src/lib/swapper/swappers/ZrxSwapper/endpoints.ts index 1d3377d3a3c..3eae7adce1e 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/endpoints.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/endpoints.ts @@ -25,9 +25,14 @@ export const zrxApi: Swapper2Api = { input: GetTradeQuoteInput, { sellAssetUsdRate }: { sellAssetUsdRate: string }, ): Promise> => { - const { sellAsset, sellAmountBeforeFeesCryptoBaseUnit, affiliateBps, receiveAddress } = input + const { + sellAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + affiliateBps, + receiveAddress, + } = input const sellAmountBeforeFeesCryptoPrecision = fromBaseUnit( - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset.precision, ) const sellAmountBeforeFeesUsd = bnOrZero(sellAmountBeforeFeesCryptoPrecision).times( @@ -65,7 +70,7 @@ export const zrxApi: Swapper2Api = { stepIndex, slippageTolerancePercentageDecimal, }: GetUnsignedTxArgs): Promise => { - const { accountNumber, buyAsset, sellAsset, sellAmountBeforeFeesCryptoBaseUnit } = + const { accountNumber, buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = tradeQuote.steps[stepIndex] const { receiveAddress, affiliateBps } = tradeQuote @@ -78,7 +83,7 @@ export const zrxApi: Swapper2Api = { receiveAddress, slippageTolerancePercentageDecimal, affiliateBps, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, }) if (maybeZrxQuote.isErr()) throw maybeZrxQuote.unwrapErr() diff --git a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts index db21145269f..8d0db112120 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.test.ts @@ -190,7 +190,7 @@ describe('getZrxTradeQuote', () => { const maybeQuote = await getZrxTradeQuote( { ...quoteInput, - sellAmountBeforeFeesCryptoBaseUnit: '0', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '0', }, sellAssetUsdRate, ) @@ -198,6 +198,8 @@ describe('getZrxTradeQuote', () => { expect(maybeQuote.isErr()).toBe(false) const quote = maybeQuote.unwrap() - expect(quote?.steps[0].sellAmountBeforeFeesCryptoBaseUnit).toBe('1000000000000000000') + expect(quote?.steps[0].sellAmountIncludingProtocolFeesCryptoBaseUnit).toBe( + '1000000000000000000', + ) }) }) diff --git a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts index e3ba5d98195..9d80d7b50a3 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts @@ -38,7 +38,7 @@ export async function getZrxTradeQuote( supportsEIP1559, slippageTolerancePercentage, } = input - const sellAmountBeforeFeesCryptoBaseUnit = input.sellAmountBeforeFeesCryptoBaseUnit + const sellAmountBeforeFeesCryptoBaseUnit = input.sellAmountIncludingProtocolFeesCryptoBaseUnit const assertion = assertValidTrade({ buyAsset, sellAsset, receiveAddress }) if (assertion.isErr()) return Err(assertion.unwrapErr()) @@ -98,6 +98,7 @@ export async function getZrxTradeQuote( }) return Ok({ + rate, minimumCryptoHuman, steps: [ { @@ -111,11 +112,11 @@ export async function getZrxTradeQuote( protocolFees: {}, }, buyAmountBeforeFeesCryptoBaseUnit: buyAmountCryptoBaseUnit, - sellAmountBeforeFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, sources: data.sources?.filter(s => parseFloat(s.proportion) > 0) || DEFAULT_SOURCE, }, ], - } as TradeQuote) + } as TradeQuote) // TODO: remove this cast, it's a recipe for bugs } catch (err) { return Err( makeSwapErrorRight({ diff --git a/src/lib/swapper/swappers/ZrxSwapper/utils/fetchZrxQuote.ts b/src/lib/swapper/swappers/ZrxSwapper/utils/fetchZrxQuote.ts index ceb58b3d9fc..4721b94be3e 100644 --- a/src/lib/swapper/swappers/ZrxSwapper/utils/fetchZrxQuote.ts +++ b/src/lib/swapper/swappers/ZrxSwapper/utils/fetchZrxQuote.ts @@ -18,7 +18,7 @@ export type FetchZrxQuoteInput = { receiveAddress: string slippageTolerancePercentageDecimal: string affiliateBps: string | undefined - sellAmountBeforeFeesCryptoBaseUnit: string + sellAmountIncludingProtocolFeesCryptoBaseUnit: string } export const fetchZrxQuote = async ({ @@ -27,7 +27,7 @@ export const fetchZrxQuote = async ({ receiveAddress, slippageTolerancePercentageDecimal, affiliateBps, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, }: FetchZrxQuoteInput) => { const withZrxAxiosRetry = (baseService: AxiosInstance) => { return withAxiosRetry(baseService, { @@ -65,7 +65,7 @@ export const fetchZrxQuote = async ({ params: { buyToken: assetToToken(buyAsset), sellToken: assetToToken(sellAsset), - sellAmount: sellAmountBeforeFeesCryptoBaseUnit, + sellAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, takerAddress: receiveAddress, slippagePercentage: bnOrZero(slippageTolerancePercentageDecimal).toString(), affiliateAddress: AFFILIATE_ADDRESS, // Used for 0x analytics diff --git a/src/lib/swapper/swappers/utils/helpers/helpers.ts b/src/lib/swapper/swappers/utils/helpers/helpers.ts index 66b32f11e82..a6645b46a6f 100644 --- a/src/lib/swapper/swappers/utils/helpers/helpers.ts +++ b/src/lib/swapper/swappers/utils/helpers/helpers.ts @@ -56,11 +56,13 @@ export const createEmptyEvmTradeQuote = ( ): TradeQuote => { return { minimumCryptoHuman, + rate: '0', steps: [ { allowanceContract: '', buyAmountBeforeFeesCryptoBaseUnit: '0', - sellAmountBeforeFeesCryptoBaseUnit: input.sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + input.sellAmountIncludingProtocolFeesCryptoBaseUnit, feeData: { networkFeeCryptoBaseUnit: undefined, protocolFees: {}, diff --git a/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts b/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts index 73bf302ce59..2e5e4817863 100644 --- a/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts +++ b/src/lib/swapper/swappers/utils/test-data/setupSwapQuote.ts @@ -10,11 +10,12 @@ export const setupQuote = () => { const buyAsset: Asset = { ...WETH } const tradeQuote: TradeQuote = { minimumCryptoHuman: '0', + rate: '1', steps: [ { allowanceContract: 'allowanceContractAddress', buyAmountBeforeFeesCryptoBaseUnit: '', - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000', sellAsset, buyAsset, accountNumber: 0, @@ -30,7 +31,7 @@ export const setupQuote = () => { const quoteInput: GetTradeQuoteInput = { chainId: KnownChainIds.EthereumMainnet, - sellAmountBeforeFeesCryptoBaseUnit: '1000000000000000000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000000000000', sellAsset, buyAsset, accountNumber: 0, diff --git a/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.test.ts b/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.test.ts new file mode 100644 index 00000000000..93514a13a8b --- /dev/null +++ b/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.test.ts @@ -0,0 +1,128 @@ +import type { AssetId } from '@shapeshiftoss/caip' +import { mockMarketData } from 'test/mocks/marketData' +import { mockStore } from 'test/mocks/store' +import { SwapperName } from 'lib/swapper/api' +import { getInputOutputRatioFromQuote } from 'state/apis/swappers/helpers/getInputOutputRatioFromQuote' +import { + cowQuote, + lifiQuote, + oneInchQuote, + thorQuote, + zrxQuote, +} from 'state/apis/swappers/helpers/testData' +import type { ReduxState } from 'state/reducer' + +const usdcAssetId: AssetId = 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + +jest.mock('state/slices/assetsSlice/selectors', () => { + const { ETH } = require('lib/swapper/swappers/utils/test-data/assets') + const { ethAssetId, foxAssetId } = require('@shapeshiftoss/caip') + const { assertUnreachable } = require('lib/utils') + + return { + ...jest.requireActual('state/slices/assetsSlice/selectors'), + selectFeeAssetById: jest.fn((_state: ReduxState, assetId: AssetId) => { + switch (assetId) { + case ethAssetId: + case usdcAssetId: + case foxAssetId: + return ETH + default: + assertUnreachable(assetId) + } + }), + } +}) + +jest.mock('state/slices/marketDataSlice/selectors', () => { + const { ethAssetId, foxAssetId } = require('@shapeshiftoss/caip') + const { assertUnreachable } = require('lib/utils') + + return { + ...jest.requireActual('state/slices/marketDataSlice/selectors'), + selectCryptoMarketData: jest.fn(() => ({ + [ethAssetId]: mockMarketData({ price: '1844' }), + [foxAssetId]: mockMarketData({ price: '0.02' }), + [usdcAssetId]: mockMarketData({ price: '1' }), + })), + selectUsdRateByAssetId: jest.fn((_state: ReduxState, assetId: AssetId) => { + switch (assetId) { + case ethAssetId: + return '2831' + case foxAssetId: + return '0.01927' + case usdcAssetId: + return '1' + default: + assertUnreachable(assetId) + } + }), + } +}) + +describe('getInputOutputRatioFromQuote', () => { + test('should return correct ratio for a Lifi quote', () => { + const mockState = { + ...mockStore, + } + const ratio = getInputOutputRatioFromQuote({ + state: mockState, + quote: lifiQuote, + swapperName: SwapperName.LIFI, + }) + + expect(ratio).toBe(0.5806430732969714) + }) + + test('should return correct ratio for a CoW quote', () => { + const mockState = { + ...mockStore, + } + const ratio = getInputOutputRatioFromQuote({ + state: mockState, + quote: cowQuote, + swapperName: SwapperName.CowSwap, + }) + + expect(ratio).toBe(0.6753421967591836) + }) + + test('should return correct ratio for a THORSwap quote', () => { + const mockState = { + ...mockStore, + } + const ratio = getInputOutputRatioFromQuote({ + state: mockState, + quote: thorQuote, + swapperName: SwapperName.Thorchain, + }) + + expect(ratio).toBe(0.6454036704419476) + }) + + test('should return correct ratio for a 0x quote', () => { + const mockState = { + ...mockStore, + } + const ratio = getInputOutputRatioFromQuote({ + state: mockState, + quote: zrxQuote, + swapperName: SwapperName.Zrx, + }) + + expect(ratio).toBe(0.7499671179394174) + }) + + test('should return correct ratio for a 1inch quote', () => { + const mockState = { + ...mockStore, + } + const ratio = getInputOutputRatioFromQuote({ + state: mockState, + quote: oneInchQuote, + swapperName: SwapperName.OneInch, + }) + + expect(ratio).toBe(0.6491538483448477) + }) +}) diff --git a/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.ts b/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.ts index bc639fdf223..019c5f02142 100644 --- a/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.ts +++ b/src/state/apis/swappers/helpers/getInputOutputRatioFromQuote.ts @@ -1,17 +1,18 @@ import type { AssetId } from '@shapeshiftoss/caip' +import { cosmosChainId } from '@shapeshiftoss/caip' import { getDefaultSlippagePercentageForSwapper } from 'constants/constants' import type { Asset } from 'lib/asset-service' import type { BigNumber } from 'lib/bignumber/bignumber' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' -import type { SwapperName, TradeQuote } from 'lib/swapper/api' +import type { TradeQuote } from 'lib/swapper/api' +import { SwapperName } from 'lib/swapper/api' import type { ReduxState } from 'state/reducer' import { selectFeeAssetById } from 'state/slices/assetsSlice/selectors' import { selectCryptoMarketData, selectUsdRateByAssetId, } from 'state/slices/marketDataSlice/selectors' -import { sumProtocolFeesToDenom } from 'state/slices/tradeQuoteSlice/utils' const getHopTotalNetworkFeeFiatPrecisionWithGetFeeAssetRate = ( state: ReduxState, @@ -48,22 +49,6 @@ const getTotalNetworkFeeFiatPrecisionWithGetFeeAssetRate = ( return acc.plus(networkFeeFiatPrecision) }, bn(0)) -const _getTotalProtocolFeesUsdPrecision = (state: ReduxState, quote: TradeQuote): BigNumber => { - const cryptoMarketDataById = selectCryptoMarketData(state) - return quote.steps.reduce( - (acc, step) => - acc.plus( - sumProtocolFeesToDenom({ - cryptoMarketDataById, - protocolFees: step.feeData.protocolFees, - outputExponent: 0, - outputAssetPriceUsd: '1', - }), - ), - bn(0), - ) -} - /** * Computes the total network fee across all hops * @param state @@ -94,21 +79,49 @@ const _getReceiveSideAmountsCryptoBaseUnit = ({ quote: TradeQuote swapperName: SwapperName }) => { + const firstStep = quote.steps[0] const lastStep = quote.steps[quote.steps.length - 1] const slippageDecimalPercentage = quote.recommendedSlippage ?? getDefaultSlippagePercentageForSwapper(swapperName) + const rate = quote.rate const buyAmountCryptoBaseUnit = bn(lastStep.buyAmountBeforeFeesCryptoBaseUnit) const slippageAmountCryptoBaseUnit = buyAmountCryptoBaseUnit.times(slippageDecimalPercentage) - const buySideNetworkFeeCryptoBaseUnit = bn(0) // TODO(woodenfurniture): handle osmo swapper crazy network fee logic here - const buySideProtocolFeeCryptoBaseUnit = bnOrZero( - lastStep.feeData.protocolFees[lastStep.buyAsset.assetId]?.amountCryptoBaseUnit, - ) + const sellAssetProtocolFee = firstStep.feeData.protocolFees[firstStep.sellAsset.assetId] + const buyAssetProtocolFee = lastStep.feeData.protocolFees[lastStep.buyAsset.assetId] + const sellSideProtocolFeeCryptoBaseUnit = bnOrZero(sellAssetProtocolFee?.amountCryptoBaseUnit) + const sellSideProtocolFeeBuyAssetBaseUnit = bnOrZero( + convertPrecision({ + value: sellSideProtocolFeeCryptoBaseUnit, + inputExponent: firstStep.sellAsset.precision, + outputExponent: lastStep.buyAsset.precision, + }), + ).times(rate) + // Network fee represented as protocol fee for Osmosis swaps + const buySideNetworkFeeCryptoBaseUnit = + swapperName === SwapperName.Osmosis + ? (() => { + const isAtomOsmo = firstStep.sellAsset.chainId === cosmosChainId + + // Subtract ATOM fees converted to OSMO for ATOM -> OSMO + if (isAtomOsmo) { + const otherDenomFee = lastStep.feeData.protocolFees[firstStep.sellAsset.assetId] + if (!otherDenomFee) return bn(0) + return bnOrZero(otherDenomFee.amountCryptoBaseUnit).times(rate) + } + + const firstHopNetworkFee = firstStep.feeData.networkFeeCryptoBaseUnit + // Subtract the first-hop network fees for OSMO -> ATOM, which aren't automagically subtracted in the multi-hop abstraction + return bnOrZero(firstHopNetworkFee) + })() + : bn(0) + const buySideProtocolFeeCryptoBaseUnit = bnOrZero(buyAssetProtocolFee?.amountCryptoBaseUnit) const netReceiveAmountCryptoBaseUnit = buyAmountCryptoBaseUnit .minus(slippageAmountCryptoBaseUnit) .minus(buySideNetworkFeeCryptoBaseUnit) .minus(buySideProtocolFeeCryptoBaseUnit) + .minus(sellSideProtocolFeeBuyAssetBaseUnit) return { netReceiveAmountCryptoBaseUnit, @@ -149,19 +162,15 @@ export const getInputOutputRatioFromQuote = ({ quote: TradeQuote swapperName: SwapperName }): number => { - const totalProtocolFeeUsdPrecision = _getTotalProtocolFeesUsdPrecision(state, quote) const totalNetworkFeeUsdPrecision = _getTotalNetworkFeeUsdPrecision(state, quote) - const { sellAmountBeforeFeesCryptoBaseUnit, sellAsset } = quote.steps[0] + const { sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset } = quote.steps[0] const { buyAsset } = quote.steps[quote.steps.length - 1] - const { - netReceiveAmountCryptoBaseUnit, - buySideNetworkFeeCryptoBaseUnit, - buySideProtocolFeeCryptoBaseUnit, - } = _getReceiveSideAmountsCryptoBaseUnit({ - quote, - swapperName, - }) + const { netReceiveAmountCryptoBaseUnit, buySideNetworkFeeCryptoBaseUnit } = + _getReceiveSideAmountsCryptoBaseUnit({ + quote, + swapperName, + }) const netReceiveAmountUsdPrecision = _convertCryptoBaseUnitToUsdPrecision( state, @@ -175,28 +184,17 @@ export const getInputOutputRatioFromQuote = ({ buySideNetworkFeeCryptoBaseUnit, ) - const buySideProtocolFeeUsdPrecision = _convertCryptoBaseUnitToUsdPrecision( - state, - buyAsset, - buySideProtocolFeeCryptoBaseUnit, - ) - const sellAmountCryptoBaseUnit = _convertCryptoBaseUnitToUsdPrecision( state, sellAsset, - sellAmountBeforeFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, ) const sellSideNetworkFeeUsdPrecision = totalNetworkFeeUsdPrecision.minus( buySideNetworkFeeUsdPrecision, ) - const sellSideProtocolFeeUsdPrecision = totalProtocolFeeUsdPrecision.minus( - buySideProtocolFeeUsdPrecision, - ) - const netSendAmountUsdPrecision = sellAmountCryptoBaseUnit - .plus(sellSideNetworkFeeUsdPrecision) - .plus(sellSideProtocolFeeUsdPrecision) + const netSendAmountUsdPrecision = sellAmountCryptoBaseUnit.plus(sellSideNetworkFeeUsdPrecision) return netReceiveAmountUsdPrecision.div(netSendAmountUsdPrecision).toNumber() } diff --git a/src/state/apis/swappers/helpers/testData.ts b/src/state/apis/swappers/helpers/testData.ts new file mode 100644 index 00000000000..a279836f04a --- /dev/null +++ b/src/state/apis/swappers/helpers/testData.ts @@ -0,0 +1,309 @@ +import type { TradeQuote2 } from 'lib/swapper/api' + +export const lifiQuote: TradeQuote2 = { + id: '0x5ba393814e096f79f4316615b82462eaaee2cf4e1c935d35624a6390bc932b83', + rate: '51.34579860391078801712', + affiliateBps: undefined, + receiveAddress: '0x31b5c4ab7d020de87901c736535aeb4769806947', + minimumCryptoHuman: '0.01000010000100001', + steps: [ + { + allowanceContract: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + accountNumber: 0, + buyAmountBeforeFeesCryptoBaseUnit: '1.0269262412379365425e+21', + buyAsset: { + assetId: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + chainId: 'eip155:1', + name: 'FOX on Ethereum', + precision: 18, + color: '#3761F9', + icon: 'https://assets.coincap.io/assets/icons/256/fox.png', + symbol: 'FOX', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit: '7543572217388900', + }, + rate: '51.34579860391078801712', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000200', + sellAsset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + sources: [ + { + name: '0x (LI.FI)', + proportion: '1', + }, + ], + }, + ], +} + +export const thorQuote: TradeQuote2 = { + id: 'f4636745-bf07-4799-9efb-c056691b652f', + rate: '39.23942597524024759752', + receiveAddress: '0x31b5c4ab7d020de87901c736535aeb4769806947', + affiliateBps: '30', + minimumCryptoHuman: '5.201707582929838658388', + recommendedSlippage: '0.00001', + steps: [ + { + rate: '39.23942597524024759752', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000200', + buyAmountBeforeFeesCryptoBaseUnit: '1013948034150000000000', + sources: [ + { + name: 'THORChain', + proportion: '1', + }, + ], + buyAsset: { + assetId: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + chainId: 'eip155:1', + name: 'FOX on Ethereum', + precision: 18, + color: '#3761F9', + icon: 'https://assets.coincap.io/assets/icons/256/fox.png', + symbol: 'FOX', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + sellAsset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + accountNumber: 0, + allowanceContract: '0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146', + feeData: { + networkFeeCryptoBaseUnit: '1873039322000000', + protocolFees: { + 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d': { + amountCryptoBaseUnit: '226109822660000000000', + requiresBalance: false, + asset: { + assetId: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + chainId: 'eip155:1', + name: 'FOX on Ethereum', + precision: 18, + color: '#3761F9', + icon: 'https://assets.coincap.io/assets/icons/256/fox.png', + symbol: 'FOX', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + }, + 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { + amountCryptoBaseUnit: '58315.259280195801954911697', + requiresBalance: false, + asset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + }, + }, + }, + }, + ], +} + +export const oneInchQuote: TradeQuote2 = { + id: '89654b4f-c90b-4578-bb9f-7c93e7ad227d', + rate: '51.63754486526613135844', + affiliateBps: undefined, + receiveAddress: '0x31b5c4ab7d020de87901c736535aeb4769806947', + minimumCryptoHuman: '1.00001000010000100001', + steps: [ + { + allowanceContract: '0x1111111254eeb25477b68fb85ed929f73a960582', + rate: '51.63754486526613135844', + buyAsset: { + assetId: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + chainId: 'eip155:1', + name: 'FOX on Ethereum', + precision: 18, + color: '#3761F9', + icon: 'https://assets.coincap.io/assets/icons/256/fox.png', + symbol: 'FOX', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + sellAsset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + accountNumber: 0, + buyAmountBeforeFeesCryptoBaseUnit: '1032761224814295680395', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000200', + feeData: { + protocolFees: {}, + networkFeeCryptoBaseUnit: '5746091301638380', + }, + sources: [ + { + name: '1INCH', + proportion: '1', + }, + ], + }, + ], +} + +export const cowQuote: TradeQuote2 = { + id: '220858750', + rate: '51.86127422365727736757', + affiliateBps: undefined, + receiveAddress: '0x31b5c4ab7d020de87901c736535aeb4769806947', + minimumCryptoHuman: '20.0002000020000200002', + steps: [ + { + allowanceContract: '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110', + rate: '51.86127422365727736757', + feeData: { + networkFeeCryptoBaseUnit: '0', + protocolFees: { + 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { + amountCryptoBaseUnit: '6421720', + requiresBalance: false, + asset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + }, + }, + }, + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000200', + buyAmountBeforeFeesCryptoBaseUnit: '1039167423885457658942', + sources: [ + { + name: 'CoW Swap', + proportion: '1', + }, + ], + buyAsset: { + assetId: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + chainId: 'eip155:1', + name: 'FOX on Ethereum', + precision: 18, + color: '#3761F9', + icon: 'https://assets.coincap.io/assets/icons/256/fox.png', + symbol: 'FOX', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + sellAsset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + accountNumber: 0, + }, + ], +} + +export const zrxQuote: TradeQuote2 = { + id: 'dfb5f2e6-9cb9-4865-9ef5-6b54d203affa', + rate: '51.603817692372651273', + affiliateBps: undefined, + receiveAddress: '0x31b5c4ab7d020de87901c736535aeb4769806947', + minimumCryptoHuman: '1.00001000010000100001', + steps: [ + { + allowanceContract: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', + buyAsset: { + assetId: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + chainId: 'eip155:1', + name: 'FOX on Ethereum', + precision: 18, + color: '#3761F9', + icon: 'https://assets.coincap.io/assets/icons/256/fox.png', + symbol: 'FOX', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + sellAsset: { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + name: 'USD Coin on Ethereum', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + symbol: 'USDC', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + }, + accountNumber: 0, + rate: '51.603817692372651273', + feeData: { + networkFeeCryptoBaseUnit: '3506329610784000', + protocolFees: {}, + }, + buyAmountBeforeFeesCryptoBaseUnit: '1032086674610991500000', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '20000200', + sources: [ + { + name: 'Uniswap_V2', + proportion: '1', + }, + ], + }, + ], +} + +export const quotes: TradeQuote2[] = [lifiQuote, thorQuote, zrxQuote, cowQuote, oneInchQuote] diff --git a/src/state/slices/tradeQuoteSlice/helpers.ts b/src/state/slices/tradeQuoteSlice/helpers.ts index a6dc02d3bc9..8342e88f758 100644 --- a/src/state/slices/tradeQuoteSlice/helpers.ts +++ b/src/state/slices/tradeQuoteSlice/helpers.ts @@ -2,7 +2,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import { cosmosChainId } from '@shapeshiftoss/caip' import { getDefaultSlippagePercentageForSwapper } from 'constants/constants' import type { BigNumber } from 'lib/bignumber/bignumber' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' import type { ProtocolFee, TradeQuote2 } from 'lib/swapper/api' import { SwapperName } from 'lib/swapper/api' @@ -38,6 +38,7 @@ const getHopTotalNetworkFeeFiatPrecisionWithGetFeeAssetRate = ( return networkFeeFiatPrecision } +// TODO: this logic is duplicated - consolidate it ASAP // NOTE: "Receive side" refers to "last hop AND buy asset AND receive account". // TODO: we'll need a check to ensure any fees included here impact the final amount received in the // receive account @@ -76,14 +77,23 @@ const _getReceiveSideAmountsCryptoBaseUnit = ({ })() : bn(0) - const buySideProtocolFeeCryptoBaseUnit = bnOrZero( - lastStep.feeData.protocolFees[lastStep.buyAsset.assetId]?.amountCryptoBaseUnit, - ) + const sellAssetProtocolFee = firstStep.feeData.protocolFees[firstStep.sellAsset.assetId] + const buyAssetProtocolFee = lastStep.feeData.protocolFees[lastStep.buyAsset.assetId] + const sellSideProtocolFeeCryptoBaseUnit = bnOrZero(sellAssetProtocolFee?.amountCryptoBaseUnit) + const sellSideProtocolFeeBuyAssetBaseUnit = bnOrZero( + convertPrecision({ + value: sellSideProtocolFeeCryptoBaseUnit, + inputExponent: firstStep.sellAsset.precision, + outputExponent: lastStep.buyAsset.precision, + }), + ).times(rate) + const buySideProtocolFeeCryptoBaseUnit = bnOrZero(buyAssetProtocolFee?.amountCryptoBaseUnit) const netReceiveAmountCryptoBaseUnit = buyAmountCryptoBaseUnit .minus(slippageAmountCryptoBaseUnit) .minus(buySideNetworkFeeCryptoBaseUnit) .minus(buySideProtocolFeeCryptoBaseUnit) + .minus(sellSideProtocolFeeBuyAssetBaseUnit) return { netReceiveAmountCryptoBaseUnit, @@ -183,6 +193,7 @@ export const getTotalProtocolFeeByAsset = (quote: TradeQuote2): Record>((acc, step) => { return Object.entries(step.feeData.protocolFees).reduce>( (innerAcc, [assetId, protocolFee]) => { + if (!protocolFee) return innerAcc if (innerAcc[assetId] === undefined) { innerAcc[assetId] = protocolFee return innerAcc diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index 22ebcb76ea6..b10c93feb9d 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -160,7 +160,7 @@ export const selectLastHopBuyAsset: Selector = export const selectSellAmountCryptoBaseUnit: Selector = createSelector(selectFirstHop, firstHop => - firstHop ? firstHop.sellAmountBeforeFeesCryptoBaseUnit : undefined, + firstHop ? firstHop.sellAmountIncludingProtocolFeesCryptoBaseUnit : undefined, ) export const selectSellAmountCryptoPrecision: Selector = @@ -302,9 +302,9 @@ export const selectTotalTradeFeeBuyAssetBaseUnit = createSelector( }, ) -export const selectSellAmountBeforeFeesCryptoBaseUnit = createSelector( +export const selectSellAmountIncludingProtocolFeesCryptoBaseUnit = createSelector( selectFirstHop, - firstHop => firstHop?.sellAmountBeforeFeesCryptoBaseUnit, + firstHop => firstHop?.sellAmountIncludingProtocolFeesCryptoBaseUnit, ) export const selectBuyAmountBeforeFeesCryptoBaseUnit = createSelector( @@ -313,7 +313,7 @@ export const selectBuyAmountBeforeFeesCryptoBaseUnit = createSelector( ) export const selectSellAmountBeforeFeesCryptoPrecision = createSelector( - selectSellAmountBeforeFeesCryptoBaseUnit, + selectSellAmountIncludingProtocolFeesCryptoBaseUnit, selectFirstHopSellAsset, (sellAmountBeforeFeesCryptoBaseUnit, sellAsset) => { if (!sellAmountBeforeFeesCryptoBaseUnit || !sellAsset) return @@ -339,26 +339,12 @@ export const selectBuyAssetProtocolFeesCryptoPrecision = createSelector(selectLa ) }) -export const selectNetBuyAmountCryptoPrecision = createSelector( - selectLastHop, - selectBuyAmountBeforeFeesCryptoPrecision, - selectBuyAssetProtocolFeesCryptoPrecision, - selectQuoteOrDefaultSlippagePercentageDecimal, - (lastHop, buyAmountBeforeFeesCryptoBaseUnit, buyAssetProtocolFeeCryptoBaseUnit, slippage) => { - if (!lastHop) return - return bnOrZero(buyAmountBeforeFeesCryptoBaseUnit) - .minus(buyAssetProtocolFeeCryptoBaseUnit) - .times(bn(1).minus(slippage)) - .toFixed() - }, -) - -export const selectNetBuyAmountUserCurrency = createSelector( - selectNetBuyAmountCryptoPrecision, +export const selectReceiveBuyAmountUserCurrency = createSelector( + selectNetReceiveAmountCryptoPrecision, selectBuyAssetUserCurrencyRate, - (netBuyAmountCryptoPrecision, buyAssetUserCurrencyRate) => { - if (!netBuyAmountCryptoPrecision || !buyAssetUserCurrencyRate) return - return bn(netBuyAmountCryptoPrecision).times(buyAssetUserCurrencyRate).toFixed() + (netReceiveAmountCryptoPrecision, buyAssetUserCurrencyRate) => { + if (!netReceiveAmountCryptoPrecision || !buyAssetUserCurrencyRate) return + return bn(netReceiveAmountCryptoPrecision).times(buyAssetUserCurrencyRate).toFixed() }, ) diff --git a/src/state/slices/tradeQuoteSlice/utils.ts b/src/state/slices/tradeQuoteSlice/utils.ts index 4c40dc70ece..d341b925ecb 100644 --- a/src/state/slices/tradeQuoteSlice/utils.ts +++ b/src/state/slices/tradeQuoteSlice/utils.ts @@ -3,6 +3,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import type { MarketData } from '@shapeshiftoss/types' import { BigNumber, bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber' import type { ProtocolFee } from 'lib/swapper/api' +import type { PartialRecord } from 'lib/utils' export const convertBasisPointsToDecimalPercentage = (basisPoints: string) => bnOrZero(basisPoints).div(10000) @@ -17,7 +18,7 @@ type SumProtocolFeesToDenomArgs = { cryptoMarketDataById: Partial>> outputAssetPriceUsd: BigNumber.Value outputExponent: number - protocolFees: Record + protocolFees: PartialRecord } /** @@ -53,7 +54,8 @@ export const sumProtocolFeesToDenom = ({ protocolFees, }: SumProtocolFeesToDenomArgs): string => { return Object.entries(protocolFees) - .reduce((acc: BigNumber, [assetId, protocolFee]: [AssetId, ProtocolFee]) => { + .reduce((acc: BigNumber, [assetId, protocolFee]: [AssetId, ProtocolFee | undefined]) => { + if (!protocolFee) return acc const inputExponent = protocolFee.asset.precision const priceUsd = cryptoMarketDataById[assetId]?.price