Skip to content

Commit

Permalink
fix(v4-sdk): Account for wrapping and unwrapping in v4 (#121)
Browse files Browse the repository at this point in the history
Co-authored-by: Emily Williams <[email protected]>
  • Loading branch information
hensha256 and ewilz authored Oct 1, 2024
1 parent 2825351 commit e94ee67
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 12 deletions.
36 changes: 34 additions & 2 deletions sdks/v4-sdk/src/entities/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})

Expand Down Expand Up @@ -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)
})
})
})
15 changes: 9 additions & 6 deletions sdks/v4-sdk/src/entities/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,6 +14,8 @@ export class Route<TInput extends Currency, TOutput extends Currency> {
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<TInput, TOutput> | null = null

Expand All @@ -29,16 +32,16 @@ export class Route<TInput extends Currency, TOutput extends Currency> {
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')
Expand Down
50 changes: 48 additions & 2 deletions sdks/v4-sdk/src/entities/trade.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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')
Expand Down Expand Up @@ -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(
Expand All @@ -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),
Expand Down
7 changes: 5 additions & 2 deletions sdks/v4-sdk/src/entities/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -233,7 +234,8 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
let outputAmount: CurrencyAmount<TOutput>
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])
Expand All @@ -247,7 +249,8 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
)
} else {
invariant(amount.currency.equals(route.output), 'OUTPUT')
amounts[amounts.length - 1] = amount
// Account for trades that wrap/unwrap as a last step
amounts[amounts.length - 1] = amountWithPathCurrency(amount, route.pools[route.pools.length - 1])
for (let i = route.currencyPath.length - 1; i > 0; i--) {
const pool = route.pools[i - 1]
const [inputAmount] = await pool.getInputAmount(amounts[i])
Expand Down
26 changes: 26 additions & 0 deletions sdks/v4-sdk/src/utils/pathCurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Pool } from '../entities/pool'

export function amountWithPathCurrency(amount: CurrencyAmount<Currency>, pool: Pool): CurrencyAmount<Currency> {
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}`
)
}
}

0 comments on commit e94ee67

Please sign in to comment.