From e94ee6716de0b0af5bb1e7c02427af3162a0124a Mon Sep 17 00:00:00 2001 From: Alice <34962750+hensha256@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:24:39 -0400 Subject: [PATCH] fix(v4-sdk): Account for wrapping and unwrapping in v4 (#121) Co-authored-by: Emily Williams --- sdks/v4-sdk/src/entities/route.test.ts | 36 +++++++++++++++++-- sdks/v4-sdk/src/entities/route.ts | 15 ++++---- sdks/v4-sdk/src/entities/trade.test.ts | 50 ++++++++++++++++++++++++-- sdks/v4-sdk/src/entities/trade.ts | 7 ++-- sdks/v4-sdk/src/utils/pathCurrency.ts | 26 ++++++++++++++ 5 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 sdks/v4-sdk/src/utils/pathCurrency.ts diff --git a/sdks/v4-sdk/src/entities/route.test.ts b/sdks/v4-sdk/src/entities/route.test.ts index 2b4453bef..313cbec7d 100644 --- a/sdks/v4-sdk/src/entities/route.test.ts +++ b/sdks/v4-sdk/src/entities/route.test.ts @@ -77,10 +77,10 @@ describe('Route', () => { expect(route.chainId).toEqual(1) }) it('should fail if the input is not in the first pool', () => { - expect(() => new Route([pool_0_1], eth, currency1)).toThrow() + expect(() => new Route([pool_0_1], eth, currency1)).toThrow('Expected currency ETH to be either t0 or t1') }) it('should fail if output is not in the last pool', () => { - expect(() => new Route([pool_0_1], currency0, eth)).toThrow() + expect(() => new Route([pool_0_1], currency0, eth)).toThrow('Expected currency ETH to be either t0 or t1') }) }) @@ -226,5 +226,37 @@ describe('Route', () => { expect(price.baseCurrency.equals(eth)).toEqual(true) expect(price.quoteCurrency.equals(eth)).toEqual(true) }) + + it('can be constructed with ETHER as input on a WETH Pool', async () => { + const route = new Route([pool_0_weth], eth, currency0) + expect(route.input).toEqual(eth) + expect(route.pathInput).toEqual(weth) + expect(route.output).toEqual(currency0) + expect(route.pathOutput).toEqual(currency0) + }) + + it('can be constructed with WETH as input on a ETH Pool', async () => { + const route = new Route([pool_0_eth], weth, currency0) + expect(route.input).toEqual(weth) + expect(route.pathInput).toEqual(eth) + expect(route.output).toEqual(currency0) + expect(route.pathOutput).toEqual(currency0) + }) + + it('can be constructed with ETHER as output on a WETH Pool', async () => { + const route = new Route([pool_0_weth], currency0, eth) + expect(route.input).toEqual(currency0) + expect(route.pathInput).toEqual(currency0) + expect(route.output).toEqual(eth) + expect(route.pathOutput).toEqual(weth) + }) + + it('can be constructed with WETH as output on a ETH Pool', async () => { + const route = new Route([pool_0_eth], currency0, weth) + expect(route.input).toEqual(currency0) + expect(route.pathInput).toEqual(currency0) + expect(route.output).toEqual(weth) + expect(route.pathOutput).toEqual(eth) + }) }) }) diff --git a/sdks/v4-sdk/src/entities/route.ts b/sdks/v4-sdk/src/entities/route.ts index fcc1dcdc2..7e4156e2b 100644 --- a/sdks/v4-sdk/src/entities/route.ts +++ b/sdks/v4-sdk/src/entities/route.ts @@ -2,6 +2,7 @@ import invariant from 'tiny-invariant' import { Currency, Price } from '@uniswap/sdk-core' import { Pool } from './pool' +import { getPathCurrency } from '../utils/pathCurrency' /** * Represents a list of pools through which a swap can occur @@ -13,6 +14,8 @@ export class Route { public readonly currencyPath: Currency[] public readonly input: TInput public readonly output: TOutput + public readonly pathInput: Currency // equivalent or wrapped/unwrapped input to match pool + public readonly pathOutput: Currency // equivalent or wrapped/unwrapped output to match pool private _midPrice: Price | null = null @@ -29,16 +32,16 @@ export class Route { const allOnSameChain = pools.every((pool) => pool.chainId === chainId) invariant(allOnSameChain, 'CHAIN_IDS') - invariant(pools[0].involvesCurrency(input) || pools[0].involvesCurrency(input.wrapped), 'INPUT') - invariant( - pools[pools.length - 1].involvesCurrency(output) || pools[pools.length - 1].involvesCurrency(output.wrapped), - 'OUTPUT' - ) + /** + * function throws if pools do not involve the input and output currency or the native/wrapped equivalent + **/ + this.pathInput = getPathCurrency(input, pools[0]) + this.pathOutput = getPathCurrency(output, pools[pools.length - 1]) /** * Normalizes currency0-currency1 order and selects the next currency/fee step to add to the path * */ - const currencyPath: Currency[] = [input] + const currencyPath: Currency[] = [this.pathInput] for (const [i, pool] of pools.entries()) { const currentInputCurrency = currencyPath[i] invariant(currentInputCurrency.equals(pool.currency0) || currentInputCurrency.equals(pool.currency1), 'PATH') diff --git a/sdks/v4-sdk/src/entities/trade.test.ts b/sdks/v4-sdk/src/entities/trade.test.ts index 941741ba8..dac314491 100644 --- a/sdks/v4-sdk/src/entities/trade.test.ts +++ b/sdks/v4-sdk/src/entities/trade.test.ts @@ -1,4 +1,4 @@ -import { Currency, CurrencyAmount, Ether, Percent, Price, sqrt, Token, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Ether, WETH9, Percent, Price, sqrt, Token, TradeType } from '@uniswap/sdk-core' import { ADDRESS_ZERO, FEE_AMOUNT_MEDIUM, TICK_SPACING_SIXTY } from '../internalConstants' import JSBI from 'jsbi' import { nearestUsableTick, encodeSqrtRatioX96, TickMath } from '@uniswap/v3-sdk' @@ -8,7 +8,7 @@ import { Trade } from './trade' describe('Trade', () => { const ETHER = Ether.onChain(1) - + const weth = WETH9[1] const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'token0') const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1', 'token1') const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2', 'token2') @@ -81,6 +81,11 @@ describe('Trade', () => { CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(100000)) ) + const pool_weth_0 = v2StylePool( + CurrencyAmount.fromRawAmount(weth, JSBI.BigInt(100000)), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100000)) + ) + describe('#fromRoute', () => { it('can be constructed with ETHER as input', async () => { const trade = await Trade.fromRoute( @@ -91,6 +96,47 @@ describe('Trade', () => { expect(trade.inputAmount.currency).toEqual(ETHER) expect(trade.outputAmount.currency).toEqual(token0) }) + + it('can be constructed with ETHER as input on a WETH Pool', async () => { + const trade = await Trade.fromRoute( + new Route([pool_weth_0], ETHER, token0), + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token0) + }) + + it('can be constructed with WETH as input on a ETH Pool', async () => { + const trade = await Trade.fromRoute( + new Route([pool_eth_0], weth, token0), + CurrencyAmount.fromRawAmount(weth, JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(weth) + expect(trade.outputAmount.currency).toEqual(token0) + }) + + it('can be constructed with ETHER as output on a WETH Pool', async () => { + const trade = await Trade.fromRoute( + new Route([pool_weth_0], token0, ETHER), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(ETHER) + }) + + it('can be constructed with WETH as output on a ETH Pool', async () => { + const trade = await Trade.fromRoute( + new Route([pool_eth_0], token0, weth), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(weth) + }) + it('can be constructed with ETHER as input for exact output', async () => { const trade = await Trade.fromRoute( new Route([pool_eth_0], ETHER, token0), diff --git a/sdks/v4-sdk/src/entities/trade.ts b/sdks/v4-sdk/src/entities/trade.ts index 75a1daca5..90f128b79 100644 --- a/sdks/v4-sdk/src/entities/trade.ts +++ b/sdks/v4-sdk/src/entities/trade.ts @@ -3,6 +3,7 @@ import invariant from 'tiny-invariant' import { ONE, ZERO } from '../internalConstants' import { Pool } from './pool' import { Route } from './route' +import { amountWithPathCurrency } from '../utils/pathCurrency' /** * Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them @@ -233,7 +234,8 @@ export class Trade if (tradeType === TradeType.EXACT_INPUT) { invariant(amount.currency.equals(route.input), 'INPUT') - amounts[0] = amount + // Account for trades that wrap/unwrap as a first step + amounts[0] = amountWithPathCurrency(amount, route.pools[0]) for (let i = 0; i < route.currencyPath.length - 1; i++) { const pool = route.pools[i] const [outputAmount] = await pool.getOutputAmount(amounts[i]) @@ -247,7 +249,8 @@ export class Trade 0; i--) { const pool = route.pools[i - 1] const [inputAmount] = await pool.getInputAmount(amounts[i]) diff --git a/sdks/v4-sdk/src/utils/pathCurrency.ts b/sdks/v4-sdk/src/utils/pathCurrency.ts new file mode 100644 index 000000000..3ad554fea --- /dev/null +++ b/sdks/v4-sdk/src/utils/pathCurrency.ts @@ -0,0 +1,26 @@ +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { Pool } from '../entities/pool' + +export function amountWithPathCurrency(amount: CurrencyAmount, pool: Pool): CurrencyAmount { + return CurrencyAmount.fromFractionalAmount( + getPathCurrency(amount.currency, pool), + amount.numerator, + amount.denominator + ) +} + +export function getPathCurrency(currency: Currency, pool: Pool): Currency { + if (pool.involvesCurrency(currency)) { + return currency + } else if (pool.involvesCurrency(currency.wrapped)) { + return currency.wrapped + } else if (pool.currency0.wrapped.equals(currency)) { + return pool.currency0 + } else if (pool.currency1.wrapped.equals(currency)) { + return pool.currency1 + } else { + throw new Error( + `Expected currency ${currency.symbol} to be either ${pool.currency0.symbol} or ${pool.currency1.symbol}` + ) + } +}