From 8c897356a46ba6502fbe116e6f6b36d935f520cf Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:30:43 -0700 Subject: [PATCH] complete unit test --- test/test-util/mock-data.ts | 8 ++ test/unit/providers/v2/quote-provider.test.ts | 105 +++++++++++++++--- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/test/test-util/mock-data.ts b/test/test-util/mock-data.ts index 5c4ee5442..c0d71f926 100644 --- a/test/test-util/mock-data.ts +++ b/test/test-util/mock-data.ts @@ -397,6 +397,14 @@ export const BULLET = new Token( BigNumber.from(500), BigNumber.from(500) ) +export const STETH_WITHOUT_TAX = new Token( + ChainId.MAINNET, + '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + 18, + 'stETH', + 'stETH', + false +) // stETH is a special case (rebase token), that would make the token include buyFeeBps and sellFeeBps of 0 as always export const STETH = new Token( ChainId.MAINNET, diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts index 17966d0f6..6396104d8 100644 --- a/test/unit/providers/v2/quote-provider.test.ts +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -1,10 +1,11 @@ -import { CurrencyAmount, V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; +import { V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; import { ProviderConfig } from '../../../../src/providers/provider'; import { BLAST, BLAST_WITHOUT_TAX, BULLET, BULLET_WITHOUT_TAX, STETH } from '../../../test-util/mock-data'; import JSBI from 'jsbi'; -import { ChainId, Fraction } from '@uniswap/sdk-core'; +import { ChainId, CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core'; import { computeAllV2Routes } from '../../../../src/routers/alpha-router/functions/compute-all-routes'; import { Pair } from '@uniswap/v2-sdk'; +import { BigNumber } from 'ethers'; const tokenIn = BULLET_WITHOUT_TAX const tokenOut = BLAST_WITHOUT_TAX @@ -18,38 +19,114 @@ const stEthCurrencyAmount = CurrencyAmount.fromRawAmount(STETH, JSBI.exponentiat const blastOriginalAmount = JSBI.BigInt(10) const blastCurrencyAmount = CurrencyAmount.fromRawAmount(BLAST, JSBI.exponentiate(blastOriginalAmount, JSBI.BigInt(BLAST.decimals))) -// split input amount by 10%, 40%, 50% -const inputBulletCurrencyAmounts: Array = [ +// split input amount by 10%, 20%, 30%, 40% +const inputBulletCurrencyAmounts: Array> = [ inputBulletCurrencyAmount.multiply(new Fraction(10, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(20, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(30, 100)), inputBulletCurrencyAmount.multiply(new Fraction(40, 100)), - inputBulletCurrencyAmount.multiply(new Fraction(50, 100)), ] const amountFactorForReserves = JSBI.BigInt(100) const bulletReserve = CurrencyAmount.fromRawAmount(BULLET, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) +const bulletWithoutTaxReserve = CurrencyAmount.fromRawAmount(BULLET_WITHOUT_TAX, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) const WETHReserve = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], wethCurrencyAmount.multiply(amountFactorForReserves).quotient) const bulletWETHPool = new Pair(bulletReserve, WETHReserve) +const bulletWithoutTaxWETHPool = new Pair(bulletWithoutTaxReserve, WETHReserve) const blastReserve = CurrencyAmount.fromRawAmount(BLAST, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) +const blastWithoutTaxReserve = CurrencyAmount.fromRawAmount(BLAST_WITHOUT_TAX, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) const WETHBlastPool = new Pair(WETHReserve, blastReserve) +const WETHBlastWithoutTaxPool = new Pair(WETHReserve, blastWithoutTaxReserve) const stETHReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) +const stETHWithoutTaxReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) const bulletSTETHPool = new Pair(bulletReserve, stETHReserve) +const bulletWithoutTaxSTETHWithoutTaxPool = new Pair(bulletWithoutTaxReserve, stETHWithoutTaxReserve) const stETHBlastPool = new Pair(stETHReserve, blastReserve) +const stETHWithoutTaxBlastWithoutTaxPool = new Pair(stETHWithoutTaxReserve, blastWithoutTaxReserve) -const pools: Pair[] = [bulletWETHPool, WETHBlastPool, bulletSTETHPool, stETHBlastPool] -const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, pools, 7) +const poolsWithTax: Pair[] = [bulletWETHPool, WETHBlastPool, bulletSTETHPool, stETHBlastPool] +const poolsWithoutTax: Pair[] = [bulletWithoutTaxWETHPool, WETHBlastWithoutTaxPool, bulletWithoutTaxSTETHWithoutTaxPool, stETHWithoutTaxBlastWithoutTaxPool] const quoteProvider = new V2QuoteProvider() describe('QuoteProvider', () => { + const enableFeeOnTransferFeeFetching = [undefined] - describe('fee-on-transfer flag enabled', () => { - const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: true } + enableFeeOnTransferFeeFetching.forEach((enableFeeOnTransferFeeFetching) => { + describe(`fee-on-transfer flag enableFeeOnTransferFeeFetching = ${enableFeeOnTransferFeeFetching}`, () => { + const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, enableFeeOnTransferFeeFetching ? poolsWithTax : poolsWithoutTax, 7) + const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: enableFeeOnTransferFeeFetching } - it('should return correct quote for exact in', async () => { - const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) - expect(routesWithQuotes.length).toEqual(2) - routesWithQuotes.forEach(([route, quote]) => { - route.path + // we are leaving exact out, since fot can't quote exact out + it('should return correct quote for exact in', async () => { + const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) + expect(routesWithQuotes.length).toEqual(2) + + console.log(JSON.stringify(routesWithQuotes)) + + routesWithQuotes.forEach(([route, quote]) => { + expect(quote.length).toEqual(inputBulletCurrencyAmounts.length) + expect(route.path.length).toEqual(3) + + inputBulletCurrencyAmounts.map((inputAmount, index) => { + let currentInputAmount = inputAmount + + for (let i = 0; i < route.path.length - 1; i++) { + const token = route.path[i]! + const nextToken = route.path[i + 1]! + const pair = route.pairs.find((pair) => pair.involvesToken(token) && pair.involvesToken(nextToken))! + + if (pair.reserve0.currency.equals(BULLET) || pair.reserve0.currency.equals(BLAST)) { + if (enableFeeOnTransferFeeFetching) { + expect(pair.reserve0.currency.sellFeeBps).toBeDefined() + expect(pair.reserve0.currency.buyFeeBps).toBeDefined() + } else { + expect(pair.reserve0.currency.sellFeeBps === undefined || pair.reserve0.currency.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() + expect(pair.reserve0.currency.buyFeeBps === undefined || pair.reserve0.currency.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() + } + } + + if (pair.reserve1.currency.equals(BULLET) || pair.reserve1.currency.equals(BLAST)) { + if (enableFeeOnTransferFeeFetching) { + expect(pair.reserve1.currency.sellFeeBps).toBeDefined() + expect(pair.reserve1.currency.buyFeeBps).toBeDefined() + } else { + expect(pair.reserve1.currency.sellFeeBps === undefined || pair.reserve1.currency.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() + expect(pair.reserve1.currency.buyFeeBps === undefined || pair.reserve1.currency.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() + } + } + + const [outputAmount] = pair.getOutputAmount(currentInputAmount) + currentInputAmount = outputAmount + + if (enableFeeOnTransferFeeFetching) { + if (nextToken.equals(tokenOut)) { + expect(nextToken.sellFeeBps).toBeDefined() + expect(nextToken.buyFeeBps).toBeDefined() + } + } else { + expect(nextToken.sellFeeBps === undefined || nextToken.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() + expect(nextToken.buyFeeBps === undefined || nextToken.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() + } + } + + // This is the raw input amount from tokenIn, no fot tax applied + // this is important to assert, since interface expects no fot tax applied + // for tokenIn, see https://www.notion.so/router-sdk-changes-for-fee-on-transfer-support-856392a72df64d628efb7b7a29ed9034?d=8d45715a31364360885eaa7e8bdd3370&pvs=4 + expect(inputAmount.toExact()).toEqual(quote[index]!.amount.toExact()) + + // we need to account for the round down/up during quote, + // 0.001 should be small enough rounding error + // this is the post fot tax quote amount + // this is the most important assertion, since interface & mobile + // uses this post fot tax quote amount to calculate the quote from each route + expect(CurrencyAmount.fromRawAmount(tokenOut, quote[index]!.quote!.toString()).subtract(currentInputAmount) + .lessThan(CurrencyAmount.fromFractionalAmount(tokenOut, 1, 1000))) + + expect(route.input.equals(tokenIn)).toBeTruthy() + expect(route.output.equals(tokenOut)).toBeTruthy() + }) + }) }) }) })