Skip to content

Commit

Permalink
some cleanup and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
moodysalem committed Apr 8, 2021
1 parent 626eee0 commit 233c72f
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 104 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"devDependencies": {
"@types/jest": "^24.0.25",
"@uniswap/v3-core": "^1.0.0-rc.1",
"@uniswap/v3-periphery": "^1.0.0-beta.14",
"@uniswap/v3-periphery": "^1.0.0-beta.17",
"tsdx": "^0.14.1"
},
"engines": {
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export const FACTORY_ADDRESS = '0x1F98431c8aD98523631AE4a59f267346ea31F984'
// todo: replace with v3 swap router
export const SWAP_ROUTER_ADDRESS = '0x1F98431c8aD98523631AE4a59f267346ea31F984'

// todo: replace with v3 nft position manager
export const NONFUNGIBLE_POSITION_MANAGER_ADDRESS = '0x1F98431c8aD98523631AE4a59f267346ea31F984'

export const POOL_INIT_CODE_HASH = '0xa8180af292c6986c74fa300a542e049db8a89221e2452e431c3d8103b610c568'

/**
Expand Down
53 changes: 50 additions & 3 deletions src/entities/pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,62 @@ describe('Pool', () => {
describe('constructor', () => {
it('cannot be used for tokens on different chains', () => {
expect(() => {
new Pool(USDC, WETH9[ChainId.RINKEBY], FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, tickMapDefault)
new Pool(USDC, WETH9[ChainId.RINKEBY], FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
}).toThrow('CHAIN_IDS')
})

it('fee must be integer', () => {
expect(() => {
new Pool(USDC, WETH9[ChainId.MAINNET], FeeAmount.MEDIUM + 0.5, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
}).toThrow('FEE')
})

it('fee cannot be more than 1e6', () => {
expect(() => {
new Pool(USDC, WETH9[ChainId.MAINNET], 1e6, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
}).toThrow('FEE')
})

it('cannot be given two of the same token', () => {
expect(() => {
new Pool(USDC, USDC, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
}).toThrow('ADDRESSES')
})

it('price must be within tick price bounds', () => {
expect(() => {
new Pool(USDC, WETH9[ChainId.MAINNET], FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 1, new TickList([]))
}).toThrow('PRICE_BOUNDS')
expect(() => {
new Pool(
USDC,
WETH9[ChainId.MAINNET],
FeeAmount.MEDIUM,
JSBI.add(encodeSqrtRatioX96(1, 1), JSBI.BigInt(1)),
0,
-1,
new TickList([])
)
}).toThrow('PRICE_BOUNDS')
})

it('works with valid arguments for empty pool medium fee', () => {
new Pool(USDC, WETH9[ChainId.MAINNET], FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
})

it('works with valid arguments for empty pool low fee', () => {
new Pool(USDC, WETH9[ChainId.MAINNET], FeeAmount.LOW, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
})

it('works with valid arguments for empty pool high fee', () => {
new Pool(USDC, WETH9[ChainId.MAINNET], FeeAmount.HIGH, encodeSqrtRatioX96(1, 1), 0, 0, new TickList([]))
})
})

describe.skip('#getAddress', () => {
describe('#getAddress', () => {
it('matches an example', () => {
const result = Pool.getAddress(USDC, DAI, FeeAmount.LOW)
expect(result).toEqual('0x84e755dD2f34969933a9F9334C40b15146d52510')
expect(result).toEqual('0xE2E0399F5Fa02d7a3B6A9566539C14C799FAf413')
})
})

Expand Down
26 changes: 8 additions & 18 deletions src/entities/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export class Position {
private _token0Amount: TokenAmount | null = null
private _token1Amount: TokenAmount | null = null

/**
* Constructs a position for a given pool with the given liquidity
* @param pool for which pool the liquidity is assigned
* @param liquidity the amount of liquidity that is in the position
* @param tickLower the lower tick of the position
* @param tickUpper the upper tick of the position
*/
public constructor({ pool, liquidity, tickLower, tickUpper }: PositionConstructorArgs) {
invariant(tickLower < tickUpper, 'TICK_ORDER')
invariant(tickLower >= TickMath.MIN_TICK && tickLower % pool.tickSpacing === 0, 'TICK_LOWER')
Expand All @@ -46,14 +53,6 @@ export class Position {
return tickToPrice(this.pool.token0, this.pool.token1, this.tickLower)
}

/**
* Returns the price of token1 at the lower tick
*/
public get token1PriceLower(): Price {
// TODO: should this use tickUpper?
return tickToPrice(this.pool.token1, this.pool.token0, this.tickLower)
}

/**
* Returns the price of token0 at the upper tick
*/
Expand All @@ -62,16 +61,7 @@ export class Position {
}

/**
* Returns the price of token1 at the upper tick
*/
public get token1PriceUpper(): Price {
// TODO: should this use tickLower?
return tickToPrice(this.pool.token1, this.pool.token0, this.tickUpper)
}

/**
* Returns the amount of token0 that this position's liquidity could be burned for at the current pool price.
* To get the minimum amount that must be spent to mint the same amount of liquidity, add one.
* Returns the amount of token0 that this position's liquidity could be burned for at the current pool price
*/
public get amount0(): TokenAmount {
if (this._token0Amount !== null) return this._token0Amount
Expand Down
22 changes: 19 additions & 3 deletions src/nonfungiblePositionManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Percent } from '@uniswap/sdk-core'
import { ChainId, Percent, WETH9 } from '@uniswap/sdk-core'
import invariant from 'tiny-invariant'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESS } from './constants'
import { Position } from './entities/position'
import { MethodParameters } from './utils/calldata'
import { defaultAbiCoder } from '@ethersproject/abi'

/**
* Options for producing the arguments to send call to the router.
Expand All @@ -9,7 +12,7 @@ export interface MintOptions {
/**
* How much the pool price is allowed to move.
*/
allowedSlippage: Percent
slippageTolerance: Percent

/**
* The account that should receive the minted NFT.
Expand All @@ -20,15 +23,28 @@ export interface MintOptions {
* When the transaction expires, in epoch seconds.
*/
deadline: number

/**
* Whether to spend ether. If true, one of the pool tokens must be WETH
*/
useEther: boolean
}

export abstract class NonfungiblePositionManager {
public static ADDRESS: string = NONFUNGIBLE_POSITION_MANAGER_ADDRESS

/**
* Cannot be constructed.
*/
private constructor() {}

public static mintCallParameters(_position: Position, _options: MintOptions): MethodParameters {
public static mintCallParameters(position: Position, options: MintOptions): MethodParameters {
if (options.useEther) {
const weth = WETH9[position.pool.chainId as ChainId]
invariant((weth && position.pool.token0.equals(weth)) || position.pool.token0.equals(weth), 'NO_WETH')
}

defaultAbiCoder.encode(['bytes4'], [])
throw new Error('todo')
}
}
127 changes: 58 additions & 69 deletions src/swapRouter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ETHER, Percent, TradeType, validateAndParseAddress } from '@uniswap/sdk-core'
import { ETHER, Percent } from '@uniswap/sdk-core'
import invariant from 'tiny-invariant'
import { SWAP_ROUTER_ADDRESS } from './constants'
import { Trade } from './entities/trade'
import { MethodParameters } from './utils/calldata'
import { toHex, ZERO_HEX } from './utils/toHex'

/**
* Options for producing the arguments to send call to the router.
Expand All @@ -11,7 +11,7 @@ export interface TradeOptions {
/**
* How much the execution price is allowed to move unfavorably from the trade execution price.
*/
allowedSlippage: Percent
slippageTolerance: Percent

/**
* When the transaction expires, in epoch seconds.
Expand All @@ -22,90 +22,79 @@ export interface TradeOptions {
* The account that should receive the output of the swap.
*/
recipient: string

/**
* Whether any of the tokens in the path are fee on transfer tokens, which should be handled with special methods
*/
feeOnTransfer?: boolean
}

/**
* Represents the Uniswap V2 SwapRouter, and has static methods for helping execute trades.
*/
export abstract class SwapRouter {
public static ADDRESS: string = SWAP_ROUTER_ADDRESS

/**
* Cannot be constructed.
*/
private constructor() {}
/**
* Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade.
* @param trade to produce call parameters for
* @param options options for the call parameters
* @param _options options for the call parameters
*/
public static swapCallParameters(trade: Trade, options: TradeOptions): MethodParameters {
public static swapCallParameters(trade: Trade, _options: TradeOptions): MethodParameters {
const etherIn = trade.inputAmount.currency === ETHER
const etherOut = trade.outputAmount.currency === ETHER
// the router does not support both ether in and out
invariant(!(etherIn && etherOut), 'ETHER_IN_OUT')
//
// const to: string = validateAndParseAddress(options.recipient)
// const amountIn: string = toHex(trade.maximumAmountIn(options.slippageTolerance))
// const amountOut: string = toHex(trade.minimumAmountOut(options.slippageTolerance))
// const path: string[] = trade.route.tokenPath.map(token => token.address)
// const deadline = `0x${options.deadline.toString(16)}`
//
// let methodName: string
// let args: (string | string[])[]
// let value: string
// switch (trade.tradeType) {
// case TradeType.EXACT_INPUT:
// if (etherIn) {
// methodName = 'swapExactETHForTokensSupportingFeeOnTransferTokens'
// // (uint amountOutMin, address[] calldata path, address to, uint deadline)
// args = [amountOut, path, to, deadline]
// value = amountIn
// } else if (etherOut) {
// methodName = 'swapExactTokensForETHSupportingFeeOnTransferTokens'
// // (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
// args = [amountIn, amountOut, path, to, deadline]
// value = ZERO_HEX
// } else {
// methodName = 'swapExactTokensForTokens'
// // (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
// args = [amountIn, amountOut, path, to, deadline]
// value = ZERO_HEX
// }
// break
// case TradeType.EXACT_OUTPUT:
// if (etherIn) {
// methodName = 'swapETHForExactTokens'
// // (uint amountOut, address[] calldata path, address to, uint deadline)
// args = [amountOut, path, to, deadline]
// value = amountIn
// } else if (etherOut) {
// methodName = 'swapTokensForExactETH'
// // (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
// args = [amountOut, amountIn, path, to, deadline]
// value = ZERO_HEX
// } else {
// methodName = 'swapTokensForExactTokens'
// // (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
// args = [amountOut, amountIn, path, to, deadline]
// value = ZERO_HEX
// }
// break
// default:
// throw new Error('invalid trade type')
// }

const to: string = validateAndParseAddress(options.recipient)
const amountIn: string = toHex(trade.maximumAmountIn(options.allowedSlippage))
const amountOut: string = toHex(trade.minimumAmountOut(options.allowedSlippage))
const path: string[] = trade.route.tokenPath.map(token => token.address)
const deadline = `0x${options.deadline.toString(16)}`

const useFeeOnTransfer = Boolean(options.feeOnTransfer)

let methodName: string
let args: (string | string[])[]
let value: string
switch (trade.tradeType) {
case TradeType.EXACT_INPUT:
if (etherIn) {
methodName = useFeeOnTransfer ? 'swapExactETHForTokensSupportingFeeOnTransferTokens' : 'swapExactETHForTokens'
// (uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountOut, path, to, deadline]
value = amountIn
} else if (etherOut) {
methodName = useFeeOnTransfer ? 'swapExactTokensForETHSupportingFeeOnTransferTokens' : 'swapExactTokensForETH'
// (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountIn, amountOut, path, to, deadline]
value = ZERO_HEX
} else {
methodName = useFeeOnTransfer
? 'swapExactTokensForTokensSupportingFeeOnTransferTokens'
: 'swapExactTokensForTokens'
// (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountIn, amountOut, path, to, deadline]
value = ZERO_HEX
}
break
case TradeType.EXACT_OUTPUT:
invariant(!useFeeOnTransfer, 'EXACT_OUT_FOT')
if (etherIn) {
methodName = 'swapETHForExactTokens'
// (uint amountOut, address[] calldata path, address to, uint deadline)
args = [amountOut, path, to, deadline]
value = amountIn
} else if (etherOut) {
methodName = 'swapTokensForExactETH'
// (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
args = [amountOut, amountIn, path, to, deadline]
value = ZERO_HEX
} else {
methodName = 'swapTokensForExactTokens'
// (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
args = [amountOut, amountIn, path, to, deadline]
value = ZERO_HEX
}
break
default:
throw new Error('invalid trade type')
}
return {
methodName,
args,
value
}
throw new Error('todo')
}
}
8 changes: 2 additions & 6 deletions src/utils/calldata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@
*/
export interface MethodParameters {
/**
* The method to call on the given address.
* The hex encoded calldata to perform the given operation
*/
methodName: string
/**
* The arguments to pass to the method, all hex encoded.
*/
args: (string | string[])[]
calldata: string
/**
* The amount of ether (wei) to send in hex.
*/
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1552,10 +1552,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0-rc.1.tgz#f2bbc483451364a951fba06eb2d978c6e8bdd58f"
integrity sha512-4ET2H0a8p7nVBGFWfio9SHc+RA6UIXEvlTRIJNsDwjQvfs8Jq9lfJ+eSOUTGmiB8Vp8V5dWarLDBU/rDE159pQ==

"@uniswap/v3-periphery@^1.0.0-beta.14":
version "1.0.0-beta.14"
resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.0.0-beta.14.tgz#9af4bd8c2e41a4699ccc6684b2b8f04d84670a9b"
integrity sha512-/uMEJqswU0ooRH4lyF0xDaQ422zPojEL01UePihDbGszU+VpxmGwBoiLAWECpV27V+znZ+ZAkqWG1KndJ8j8QQ==
"@uniswap/v3-periphery@^1.0.0-beta.17":
version "1.0.0-beta.17"
resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.0.0-beta.17.tgz#da150f1c08259be3c0452d0bcd46f4f19fa26d49"
integrity sha512-yhxdRGI2ZO/hdqYUMGCkeaPXADNHE88p5nDtufmhIH80n7Xyb2WyMUQ1e5obkJZ9EM848rZ8PFzHsdvBP5kC0A==
dependencies:
"@openzeppelin/contracts" "3.4.1-solc-0.7-2"
"@uniswap/lib" "^4.0.1-alpha"
Expand Down

0 comments on commit 233c72f

Please sign in to comment.