Commit 1fcc0a59 authored by Noah Zinsmeister's avatar Noah Zinsmeister

finalize MVP beta release

add transact logic

augment tests
parent 96a412f6
import BigNumber from 'bignumber.js'
import { ethers } from 'ethers'
import { BigNumberish } from '../../types'
import { normalizeBigNumberish } from '../../_utils'
import { BigNumberish } from '../types'
import { normalizeBigNumberish } from '../_utils'
interface TestCase {
input: BigNumberish
expectedOutput: BigNumber
}
function constructTestCase(input: BigNumberish, expectedOutput: BigNumber): TestCase {
return { input, expectedOutput }
}
......@@ -52,9 +53,9 @@ describe('normalizeBigNumberish', (): void => {
describe('number', (): void => {
const expectedSuccesses: TestCase[] = [
constructTestCase(0, new BigNumber(0)),
constructTestCase(1, new BigNumber(1)),
constructTestCase(1.234, new BigNumber(1.234))
constructTestCase(0, new BigNumber('0')),
constructTestCase(1, new BigNumber('1')),
constructTestCase(1.234, new BigNumber('1.234'))
]
const expectedFailures: number[] = [NaN, Infinity]
......@@ -64,8 +65,8 @@ describe('normalizeBigNumberish', (): void => {
describe('BigNumber', (): void => {
const expectedSuccesses: TestCase[] = [
constructTestCase(new BigNumber(0), new BigNumber(0)),
constructTestCase(new BigNumber(1), new BigNumber(1)),
constructTestCase(new BigNumber(0), new BigNumber('0')),
constructTestCase(new BigNumber(1), new BigNumber('1')),
constructTestCase(new BigNumber('1.234'), new BigNumber('1.234'))
]
const expectedFailures: BigNumber[] = [new BigNumber(NaN)]
......@@ -76,8 +77,8 @@ describe('normalizeBigNumberish', (): void => {
describe('ethers.utils.BigNumber', (): void => {
const expectedSuccesses: TestCase[] = [
constructTestCase(ethers.constants.Zero, new BigNumber(0)),
constructTestCase(ethers.constants.One, new BigNumber(1)),
constructTestCase(ethers.constants.Zero, new BigNumber('0')),
constructTestCase(ethers.constants.One, new BigNumber('1')),
constructTestCase(ethers.utils.bigNumberify('1234'), new BigNumber('1234')),
constructTestCase(ethers.utils.parseUnits('1.234', 3), new BigNumber('1234'))
]
......
import BigNumber from 'bignumber.js'
import { BigNumberish, TokenReserves, MarketDetails, OptionalReserves, TradeDetails } from '../types'
import { TRADE_TYPE, TRADE_EXACT, _10 } from '../constants'
import { TRADE_EXACT } from '../constants'
import { getMarketDetails, getTradeDetails } from '../computation'
function constructTokenReserves(
......@@ -16,80 +14,55 @@ function constructTokenReserves(
}
}
const DAIReserves: TokenReserves = constructTokenReserves(18, '4039700561005906883487', '1094055210563660633471343')
const USDCReserves: TokenReserves = constructTokenReserves(6, '1076592291503763426634', '292657693901')
function testMarketRates(
tradeType: TRADE_TYPE,
inputTokenReserves: OptionalReserves,
outputTokenReserves: OptionalReserves,
expectedMarketRate: string,
expectedMarketRateInverted: string
): void {
test('not inverted', (): void => {
const marketDetails: MarketDetails = getMarketDetails(tradeType, inputTokenReserves, outputTokenReserves)
test('normal', (): void => {
const marketDetails: MarketDetails = getMarketDetails(inputTokenReserves, outputTokenReserves)
expect(marketDetails.marketRate.rate.toFixed(18)).toBe(expectedMarketRate)
expect(marketDetails.marketRate.rateInverted.toFixed(18)).toBe(expectedMarketRateInverted)
})
test('manually inverted', (): void => {
const tradeTypeInverted = tradeType === TRADE_TYPE.ETH_TO_TOKEN ? TRADE_TYPE.TOKEN_TO_ETH : TRADE_TYPE.ETH_TO_TOKEN
const marketDetails: MarketDetails = getMarketDetails(
tradeType === TRADE_TYPE.TOKEN_TO_TOKEN ? TRADE_TYPE.TOKEN_TO_TOKEN : tradeTypeInverted,
outputTokenReserves,
inputTokenReserves
)
test('inverted', (): void => {
const marketDetails: MarketDetails = getMarketDetails(outputTokenReserves, inputTokenReserves)
expect(marketDetails.marketRate.rate.toFixed(18)).toBe(expectedMarketRateInverted)
expect(marketDetails.marketRate.rateInverted.toFixed(18)).toBe(expectedMarketRate)
})
}
describe('getMarketDetails', (): void => {
describe('dummy ETH/DAI and DAI/ETH', (): void => {
const tokenReserves: TokenReserves = constructTokenReserves(
18,
'4039700561005906883487',
'1094055210563660633471343'
)
describe('DAI', (): void => {
const expectedMarketRate = '270.825818409480102284'
const expectedMarketRateInverted = '0.003692410147130181'
testMarketRates(TRADE_TYPE.ETH_TO_TOKEN, null, tokenReserves, expectedMarketRate, expectedMarketRateInverted)
testMarketRates(undefined, DAIReserves, expectedMarketRate, expectedMarketRateInverted)
})
describe('dummy ETH/USDC and USDC/ETH', (): void => {
const tokenReserves: TokenReserves = constructTokenReserves(6, '1076592291503763426634', '292657693901')
describe('USDC', (): void => {
const expectedMarketRate = '0.003678674143683891'
const expectedMarketRateInverted = '271.837069808684359442'
testMarketRates(TRADE_TYPE.TOKEN_TO_ETH, tokenReserves, null, expectedMarketRate, expectedMarketRateInverted)
testMarketRates(USDCReserves, undefined, expectedMarketRate, expectedMarketRateInverted)
})
describe('dummy DAI/USDC and USDC/DAI', (): void => {
const DAITokenReserves: TokenReserves = constructTokenReserves(
18,
'4039700561005906883487',
'1094055210563660633471343'
)
const USDCTokenReserves: TokenReserves = constructTokenReserves(6, '1076592291503763426634', '292657693901')
describe('DAI and USDC', (): void => {
const expectedMarketRate = '1.003733954927721392'
const expectedMarketRateInverted = '0.996279935624983143'
testMarketRates(
TRADE_TYPE.TOKEN_TO_TOKEN,
DAITokenReserves,
USDCTokenReserves,
expectedMarketRate,
expectedMarketRateInverted
)
testMarketRates(DAIReserves, USDCReserves, expectedMarketRate, expectedMarketRateInverted)
})
})
function testTradeDetails(
tradeExact: TRADE_EXACT,
tradeAmount: BigNumber,
tradeAmount: BigNumberish,
marketDetails: MarketDetails,
expectedInputValue: string,
expectedOutputValue: string,
expectedExecutionRate: string,
expectedExecutionRateInverted: string,
expectedMarketRateSlippage: string,
......@@ -99,6 +72,8 @@ function testTradeDetails(
const tradeDetails: TradeDetails = getTradeDetails(tradeExact, tradeAmount, marketDetails)
expect(tradeDetails.inputAmount.amount.toFixed(0)).toBe(expectedInputValue)
expect(tradeDetails.outputAmount.amount.toFixed(0)).toBe(expectedOutputValue)
expect(tradeDetails.executionRate.rate.toFixed(18)).toBe(expectedExecutionRate)
expect(tradeDetails.executionRate.rateInverted.toFixed(18)).toBe(expectedExecutionRateInverted)
......@@ -108,25 +83,68 @@ function testTradeDetails(
}
describe('getTradeDetails', (): void => {
describe('dummy ETH/DAI and DAI/ETH', (): void => {
const tokenReserves: TokenReserves = constructTokenReserves(
18,
'4039700561005906883487',
'1094055210563660633471343'
)
describe('ETH for DAI exact output', (): void => {
const marketDetails: MarketDetails = getMarketDetails(undefined, DAIReserves)
const expectedInputValue = '370385925334764803'
const expectedOutputValue = '100000000000000000000'
const expectedExecutionRate = '269.988660907180258319'
const expectedExecutionRateInverted = '0.003703859253347648'
const expectedMarketRateSlippage = '1.830727602963479922'
const expectedExecutionRateSlippage = '30.911288562381013644'
const marketDetails: MarketDetails = getMarketDetails(TRADE_TYPE.ETH_TO_TOKEN, null, tokenReserves)
testTradeDetails(
TRADE_EXACT.OUTPUT,
new BigNumber(100).multipliedBy(_10.exponentiatedBy(18)),
expectedOutputValue,
marketDetails,
expectedInputValue,
expectedOutputValue,
expectedExecutionRate,
expectedExecutionRateInverted,
expectedMarketRateSlippage,
expectedExecutionRateSlippage
)
})
describe('DAI for ETH exact output', (): void => {
const marketDetails: MarketDetails = getMarketDetails(DAIReserves, undefined)
const expectedInputValue = '271708000072010674989'
const expectedOutputValue = '1000000000000000000'
const expectedExecutionRate = '0.003680421628126409'
const expectedExecutionRateInverted = '271.708000072010674989'
const expectedMarketRateSlippage = '4.957694170529155578'
const expectedExecutionRateSlippage = '32.468004707141566275'
testTradeDetails(
TRADE_EXACT.OUTPUT,
expectedOutputValue,
marketDetails,
expectedInputValue,
expectedOutputValue,
expectedExecutionRate,
expectedExecutionRateInverted,
expectedMarketRateSlippage,
expectedExecutionRateSlippage
)
})
describe('DAI for USDC exact input', (): void => {
const marketDetails: MarketDetails = getMarketDetails(DAIReserves, USDCReserves)
const expectedInputValue = '1000000000000000000'
const expectedOutputValue = '997716'
const expectedExecutionRate = '0.997716000000000000'
const expectedExecutionRateInverted = '1.002289228598118102'
const expectedMarketRateSlippage = '0.086538655703617094'
const expectedExecutionRateSlippage = '59.955677479843185050'
testTradeDetails(
TRADE_EXACT.INPUT,
expectedInputValue,
marketDetails,
expectedInputValue,
expectedOutputValue,
expectedExecutionRate,
expectedExecutionRateInverted,
expectedMarketRateSlippage,
......
import { TokenReserves, Token } from '../types'
import { ETH as _ETH } from '../constants'
import { getTokenReserves } from '../data'
import { Token, TokenReservesNormalized } from '../types'
import { ETH as _ETH, _CHAIN_ID_NAME } from '../constants'
import { getTokenReserves, getEthToken } from '../data'
import { ethers } from 'ethers'
const ETH: Token = {
chainId: 1,
......@@ -21,9 +22,17 @@ const DAI_EXCHANGE: Token = {
}
describe('getTokenReserves', (): void => {
jest.setTimeout(10000) // 10 seconds
test('DAI', async (done: jest.DoneCallback): Promise<void> => {
jest.setTimeout(10000) // 10 seconds
const tokenReserves: TokenReserves = await getTokenReserves(DAI.address as string)
const tokenReserves: TokenReservesNormalized = await getTokenReserves(DAI.address as string)
const tokenReservesProvider: TokenReservesNormalized = await getTokenReserves(
DAI.address as string,
ethers.getDefaultProvider(_CHAIN_ID_NAME[1])
)
expect(tokenReserves.token).toEqual(tokenReservesProvider.token)
expect(tokenReserves.token).toEqual(DAI)
expect(tokenReserves.exchange).toEqual(DAI_EXCHANGE)
......@@ -35,3 +44,7 @@ describe('getTokenReserves', (): void => {
done()
})
})
test('getEthToken', (): void => {
expect(getEthToken(1)).toEqual(ETH)
})
......@@ -2,11 +2,11 @@ import BigInteger from 'bignumber.js'
import { FlexibleFormat, FormatSignificantOptions, FormatFixedOptions } from '../types'
import { formatSignificant, formatSignificantDecimals, formatFixed, formatFixedDecimals } from '../format'
import { FIXED_UNDERFLOW_BEHAVIOR, ROUNDING_MODE } from '../constants'
import { FIXED_UNDERFLOW_BEHAVIOR, _ROUNDING_MODE } from '../constants'
function constructFormatSignificantOptions(
significantDigits: number,
roundingMode: BigInteger.RoundingMode = ROUNDING_MODE,
roundingMode: BigInteger.RoundingMode = _ROUNDING_MODE,
forceIntegerSignificance: boolean = false,
format: FlexibleFormat = false
): FormatSignificantOptions {
......@@ -20,7 +20,7 @@ function constructFormatSignificantOptions(
function constructFormatFixedOptions(
decimalPlaces: number,
roundingMode: BigInteger.RoundingMode = ROUNDING_MODE,
roundingMode: BigInteger.RoundingMode = _ROUNDING_MODE,
dropTrailingZeros: boolean = true,
underflowBehavior: FIXED_UNDERFLOW_BEHAVIOR = FIXED_UNDERFLOW_BEHAVIOR.ONE_DIGIT,
format: FlexibleFormat = false
......
import BigNumber from 'bignumber.js'
import { TradeDetails } from '../types'
import { _10, ETH, TRADE_TYPE, TRADE_EXACT } from '../constants'
import { getTrade } from '../orchestration'
import { tradeExactEthForTokens } from '../orchestration'
import { TRADE_TYPE, TRADE_EXACT } from '../constants'
describe('tradeExactEthForTokens', (): void => {
jest.setTimeout(10000) // 10 seconds
describe('getTrade', (): void => {
test('DAI', async (done: jest.DoneCallback): Promise<void> => {
jest.setTimeout(10000) // 10 seconds
const tradeDetails: TradeDetails = await getTrade(
ETH,
const tradeDetails: TradeDetails = await tradeExactEthForTokens(
'0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359',
TRADE_TYPE.ETH_TO_TOKEN,
TRADE_EXACT.OUTPUT,
new BigNumber(100).multipliedBy(_10.exponentiatedBy(18))
'1000000000000000000'
)
expect(tradeDetails.inputAmount).toBeTruthy()
expect(tradeDetails.tradeType).toEqual(TRADE_TYPE.ETH_TO_TOKEN)
expect(tradeDetails.tradeExact).toEqual(TRADE_EXACT.INPUT)
expect(tradeDetails.inputAmount.amount.toFixed()).toEqual('1000000000000000000')
expect(tradeDetails.outputAmount.amount).toBeTruthy()
expect(tradeDetails.executionRate.rate).toBeTruthy()
expect(tradeDetails.executionRate.rateInverted).toBeTruthy()
expect(tradeDetails.marketRateSlippage).toBeTruthy()
expect(tradeDetails.executionRateSlippage).toBeTruthy()
done()
})
})
import { TradeDetails, ExecutionDetails } from '../types'
import { TRADE_METHODS, TRADE_METHOD_IDS } from '../constants'
import { tradeExactEthForTokens } from '../orchestration'
import { getExecutionDetails } from '../transact'
describe('tradeExactEthForTokens', (): void => {
jest.setTimeout(10000) // 10 seconds
test('DAI', async (done: jest.DoneCallback): Promise<void> => {
const tradeDetails: TradeDetails = await tradeExactEthForTokens(
'0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359',
'1000000000000000000'
)
const executionDetails: ExecutionDetails = getExecutionDetails(tradeDetails)
expect(executionDetails.exchangeAddress).toEqual('0x09cabEC1eAd1c0Ba254B09efb3EE13841712bE14')
expect(executionDetails.methodName).toEqual(TRADE_METHODS.ethToTokenSwapInput)
expect(executionDetails.methodId).toEqual(TRADE_METHOD_IDS[TRADE_METHODS.ethToTokenSwapInput])
expect(executionDetails.value.toFixed()).toEqual('1000000000000000000')
done()
})
})
......@@ -2,11 +2,10 @@ import BigNumber from 'bignumber.js'
import { ethers } from 'ethers'
import { BigNumberish } from '../types'
import { _0, MAX_UINT8, MAX_UINT256 } from '../constants'
import { _0, _MAX_UINT8, _MAX_UINT256 } from '../constants'
// check(s) that number(s) is(are) uint8(s)
function ensureUInt8(number: number): void {
if (!Number.isInteger(number) || number < 0 || number > MAX_UINT8) {
if (!Number.isInteger(number) || number < 0 || number > _MAX_UINT8) {
throw Error(`Passed number '${number}' is not a valid uint8.`)
}
}
......@@ -15,9 +14,8 @@ export function ensureAllUInt8(numbers: number[]): void {
numbers.forEach(ensureUInt8)
}
// check(s) that BigNumber(s) is(are) uint256(s)
function ensureUInt256(bigNumber: BigNumber): void {
if (!bigNumber.isInteger() || bigNumber.isLessThan(_0) || bigNumber.isGreaterThan(MAX_UINT256)) {
if (!bigNumber.isInteger() || bigNumber.isLessThan(_0) || bigNumber.isGreaterThan(_MAX_UINT256)) {
throw Error(`Passed bigNumber '${bigNumber}' is not a valid uint256.`)
}
}
......@@ -26,31 +24,26 @@ export function ensureAllUInt256(bigNumbers: BigNumber[]): void {
bigNumbers.forEach(ensureUInt256)
}
// check that number is valid decimals places/significant digits
export function ensureBoundedInteger(number: number, bounds: number | number[]): void {
const [minimum, maximum]: [number, number] = typeof bounds === 'number' ? [0, bounds] : [bounds[0], bounds[1]]
if (!Number.isInteger(number) || number < minimum || number > maximum) {
throw Error(`Passed number '${number}' is not an integer between '${minimum}' and '${maximum}', inclusive.`)
throw Error(`Passed number '${number}' is not an integer between '${minimum}' and '${maximum}' (inclusive).`)
}
}
export function normalizeBigNumberish(bigNumberish: BigNumberish): BigNumber {
try {
const bigNumber = BigNumber.isBigNumber(bigNumberish) ? bigNumberish : new BigNumber(bigNumberish.toString())
if (!bigNumber.isFinite()) {
throw Error
}
return bigNumber
} catch (error) {
throw Error(`Passed bigNumberish '${bigNumberish}' of type '${typeof bigNumberish}' is invalid. Error: '${error}'.`)
const bigNumber: BigNumber = BigNumber.isBigNumber(bigNumberish)
? bigNumberish
: new BigNumber(bigNumberish.toString())
if (!bigNumber.isFinite()) {
throw Error(`Passed bigNumberish '${bigNumberish}' of type '${typeof bigNumberish}' is not valid.`)
}
return bigNumber
}
export function normalizeAddress(address: string): string {
try {
return ethers.utils.getAddress(address.toLowerCase())
} catch {
throw Error(`Passed address '${address}' is not valid.`)
}
return ethers.utils.getAddress(address.toLowerCase())
}
import BigNumber from 'bignumber.js'
import {
TokenAmount,
TokenAmountNormalized,
TokenReserves,
areTokenReserves,
areETHReserves,
OptionalReserves,
TokenReservesNormalized,
_ParsedOptionalReserves,
_DecimalRate,
_AnyRate
} from '../types'
import { _1, _10, _10000, TRADE_TYPE, ETH_TOKEN } from '../constants'
import { ensureAllUInt8, ensureAllUInt256, normalizeBigNumberish } from '../_utils'
function normalizeTokenAmount(tokenAmount: TokenAmount): TokenAmountNormalized {
ensureAllUInt8([tokenAmount.token.decimals])
const normalizedAmount: BigNumber = normalizeBigNumberish(tokenAmount.amount)
ensureAllUInt256([normalizedAmount])
return {
token: { ...tokenAmount.token },
amount: normalizedAmount
}
}
function normalizeTokenReserves(tokenReserves: TokenReserves): TokenReservesNormalized {
ensureAllUInt8([tokenReserves.token.decimals])
if (tokenReserves.exchange) {
ensureAllUInt8([tokenReserves.exchange.decimals])
}
return {
token: { ...tokenReserves.token },
...(tokenReserves.exchange ? { ...tokenReserves.exchange } : {}),
ethReserve: normalizeTokenAmount(tokenReserves.ethReserve),
tokenReserve: normalizeTokenAmount(tokenReserves.tokenReserve)
}
}
function ensureTradeTypesMatch(computedTradeType: TRADE_TYPE, passedTradeType?: TRADE_TYPE): never | void {
if (passedTradeType && passedTradeType !== computedTradeType) {
throw Error(`passedTradeType '${passedTradeType}' does not match computedTradeType '${computedTradeType}'.`)
}
}
export function parseOptionalReserves(
optionalReservesInput: OptionalReserves,
optionalReservesOutput: OptionalReserves,
passedTradeType?: TRADE_TYPE
): _ParsedOptionalReserves {
if (areTokenReserves(optionalReservesInput) && areTokenReserves(optionalReservesOutput)) {
const computedTradeType: TRADE_TYPE = TRADE_TYPE.TOKEN_TO_TOKEN
ensureTradeTypesMatch(computedTradeType, passedTradeType)
return {
tradeType: computedTradeType,
inputReserves: normalizeTokenReserves(optionalReservesInput),
outputReserves: normalizeTokenReserves(optionalReservesOutput)
}
} else if (areTokenReserves(optionalReservesInput) && !areTokenReserves(optionalReservesOutput)) {
const computedTradeType: TRADE_TYPE = TRADE_TYPE.TOKEN_TO_ETH
ensureTradeTypesMatch(computedTradeType, passedTradeType)
return {
tradeType: computedTradeType,
inputReserves: normalizeTokenReserves(optionalReservesInput),
outputReserves: {
token: ETH_TOKEN(areETHReserves(optionalReservesOutput) ? optionalReservesOutput.token.chainId : undefined)
}
}
} else if (!areTokenReserves(optionalReservesInput) && areTokenReserves(optionalReservesOutput)) {
const computedTradeType: TRADE_TYPE = TRADE_TYPE.ETH_TO_TOKEN
ensureTradeTypesMatch(computedTradeType, passedTradeType)
return {
tradeType: computedTradeType,
inputReserves: {
token: ETH_TOKEN(areETHReserves(optionalReservesInput) ? optionalReservesInput.token.chainId : undefined)
},
outputReserves: normalizeTokenReserves(optionalReservesOutput)
}
} else {
throw Error('optionalReservesInput, optionalReservesOutput, or both must be defined.')
}
}
import { TokenAmountNormalized, _DecimalRate, _AnyRate } from '../types'
import { _1, _10 } from '../constants'
export function calculateDecimalRate(
numerator: TokenAmountNormalized,
......@@ -119,9 +34,3 @@ export function calculateDecimalRate(
.dividedBy(decimalRate.numerator)
}
}
// slippage in basis points, to 18 decimals
export function calculateSlippage(baseRate: BigNumber, newRate: BigNumber): BigNumber {
const difference: BigNumber = baseRate.minus(newRate).absoluteValue()
return difference.multipliedBy(_10000).dividedBy(baseRate)
}
import BigNumber from 'bignumber.js'
import {
TokenAmount,
TokenAmountNormalized,
OptionalReserves,
TokenReserves,
TokenReservesNormalized,
NormalizedReserves,
areTokenReservesNormalized,
areETHReserves,
areTokenReserves,
OptionalReserves,
NormalizedReserves,
Rate,
MarketDetails,
_ParsedOptionalReserves,
......@@ -13,7 +17,67 @@ import {
_AnyRate
} from '../types'
import { TRADE_TYPE } from '../constants'
import { parseOptionalReserves, calculateDecimalRate } from './_utils'
import { normalizeBigNumberish, ensureAllUInt8, ensureAllUInt256 } from '../_utils'
import { getEthToken } from '../data'
import { calculateDecimalRate } from './_utils'
function normalizeTokenAmount(tokenAmount: TokenAmount): TokenAmountNormalized {
ensureAllUInt8([tokenAmount.token.decimals])
const normalizedAmount: BigNumber = normalizeBigNumberish(tokenAmount.amount)
ensureAllUInt256([normalizedAmount])
return {
token: { ...tokenAmount.token },
amount: normalizedAmount
}
}
function normalizeTokenReserves(tokenReserves: TokenReserves): TokenReservesNormalized {
ensureAllUInt8([tokenReserves.token.decimals])
return {
token: { ...tokenReserves.token },
...(tokenReserves.exchange ? { exchange: { ...tokenReserves.exchange } } : {}),
ethReserve: normalizeTokenAmount(tokenReserves.ethReserve),
tokenReserve: normalizeTokenAmount(tokenReserves.tokenReserve)
}
}
function parseOptionalReserves(
optionalReservesInput: OptionalReserves,
optionalReservesOutput: OptionalReserves
): _ParsedOptionalReserves {
if (areTokenReserves(optionalReservesInput) && areTokenReserves(optionalReservesOutput)) {
return {
tradeType: TRADE_TYPE.TOKEN_TO_TOKEN,
inputReserves: normalizeTokenReserves(optionalReservesInput),
outputReserves: normalizeTokenReserves(optionalReservesOutput)
}
} else if (areTokenReserves(optionalReservesInput) && !areTokenReserves(optionalReservesOutput)) {
return {
tradeType: TRADE_TYPE.TOKEN_TO_ETH,
inputReserves: normalizeTokenReserves(optionalReservesInput),
outputReserves: areETHReserves(optionalReservesOutput)
? optionalReservesOutput
: {
token: getEthToken(optionalReservesInput.token.chainId)
}
}
} else if (!areTokenReserves(optionalReservesInput) && areTokenReserves(optionalReservesOutput)) {
return {
tradeType: TRADE_TYPE.ETH_TO_TOKEN,
inputReserves: areETHReserves(optionalReservesInput)
? optionalReservesInput
: {
token: getEthToken(optionalReservesOutput.token.chainId)
},
outputReserves: normalizeTokenReserves(optionalReservesOutput)
}
} else {
throw Error('optionalReservesInput, optionalReservesOutput, or both must be defined.')
}
}
// calculates the market rate for ETH_TO_TOKEN or TOKEN_TO_ETH trades
function getMarketRate(tradeType: TRADE_TYPE, reserves: NormalizedReserves, keepAsDecimal?: boolean): _AnyRate {
......@@ -31,14 +95,12 @@ function getMarketRate(tradeType: TRADE_TYPE, reserves: NormalizedReserves, keep
// note: rounds rates to 18 decimal places
export function getMarketDetails(
tradeType: TRADE_TYPE,
optionalReservesInput: OptionalReserves,
optionalReservesOutput: OptionalReserves
): MarketDetails {
const { inputReserves, outputReserves }: _ParsedOptionalReserves = parseOptionalReserves(
const { tradeType, inputReserves, outputReserves }: _ParsedOptionalReserves = parseOptionalReserves(
optionalReservesInput,
optionalReservesOutput,
tradeType
optionalReservesOutput
)
if (tradeType === TRADE_TYPE.TOKEN_TO_TOKEN) {
......
......@@ -3,19 +3,20 @@ import cloneDeepWith from 'lodash.clonedeepwith'
import {
BigNumberish,
Rate,
MarketDetails,
TradeDetails,
TokenAmountNormalized,
areTokenReservesNormalized,
NormalizedReserves,
Rate,
MarketDetails,
TradeDetails,
_PartialTradeDetails
} from '../types'
import { _0, _1, _997, _1000, TRADE_TYPE, TRADE_EXACT } from '../constants'
import { ensureAllUInt256, normalizeBigNumberish } from '../_utils'
import { calculateDecimalRate, calculateSlippage } from './_utils'
import { _0, _1, _997, _1000, TRADE_TYPE, TRADE_EXACT, _10000 } from '../constants'
import { normalizeBigNumberish, ensureAllUInt256 } from '../_utils'
import { calculateDecimalRate } from './_utils'
import { getMarketDetails } from './market'
// emulates the uniswap smart contract logic
function getInputPrice(inputAmount: BigNumber, inputReserve: BigNumber, outputReserve: BigNumber): BigNumber {
ensureAllUInt256([inputAmount, inputReserve, outputReserve])
......@@ -33,6 +34,7 @@ function getInputPrice(inputAmount: BigNumber, inputReserve: BigNumber, outputRe
return outputAmount
}
// emulates the uniswap smart contract logic
function getOutputPrice(outputAmount: BigNumber, inputReserve: BigNumber, outputReserve: BigNumber): BigNumber {
ensureAllUInt256([outputAmount, inputReserve, outputReserve])
......@@ -49,7 +51,7 @@ function getOutputPrice(outputAmount: BigNumber, inputReserve: BigNumber, output
return inputAmount
}
function _getTradeTransput(
function getSingleTradeTransput(
tradeType: TRADE_TYPE,
tradeExact: TRADE_EXACT,
tradeAmount: BigNumber,
......@@ -72,11 +74,10 @@ function _getTradeTransput(
return calculatedAmount
}
function customizer(value: BigNumber): BigNumber | undefined {
function customizer(value: BigNumber): BigNumber | void {
if (BigNumber.isBigNumber(value)) {
return new BigNumber(value)
}
return
}
// gets the corresponding input/output amount for the passed output/input amount
......@@ -96,13 +97,13 @@ function getTradeTransput(
}
if (tradeExact === TRADE_EXACT.INPUT) {
const intermediateTransput: BigNumber = _getTradeTransput(
const intermediateTransput: BigNumber = getSingleTradeTransput(
TRADE_TYPE.TOKEN_TO_ETH,
TRADE_EXACT.INPUT,
tradeAmount,
inputReserves
)
const finalTransput: BigNumber = _getTradeTransput(
const finalTransput: BigNumber = getSingleTradeTransput(
TRADE_TYPE.ETH_TO_TOKEN,
TRADE_EXACT.INPUT,
intermediateTransput,
......@@ -120,13 +121,13 @@ function getTradeTransput(
outputReservesPost
}
} else {
const intermediateTransput: BigNumber = _getTradeTransput(
const intermediateTransput: BigNumber = getSingleTradeTransput(
TRADE_TYPE.ETH_TO_TOKEN,
TRADE_EXACT.OUTPUT,
tradeAmount,
outputReserves
)
const finalTransput: BigNumber = _getTradeTransput(
const finalTransput: BigNumber = getSingleTradeTransput(
TRADE_TYPE.TOKEN_TO_ETH,
TRADE_EXACT.OUTPUT,
intermediateTransput,
......@@ -147,7 +148,7 @@ function getTradeTransput(
} else {
const reserves: NormalizedReserves = tradeType === TRADE_TYPE.ETH_TO_TOKEN ? outputReserves : inputReserves
const finalTransput: BigNumber = _getTradeTransput(tradeType, tradeExact, tradeAmount, reserves)
const finalTransput: BigNumber = getSingleTradeTransput(tradeType, tradeExact, tradeAmount, reserves)
if (tradeType === TRADE_TYPE.ETH_TO_TOKEN) {
if (!areTokenReservesNormalized(outputReservesPost)) {
......@@ -181,13 +182,20 @@ function getTradeTransput(
}
}
// slippage in basis points, to 18 decimals
function calculateSlippage(baseRate: BigNumber, newRate: BigNumber): BigNumber {
const difference: BigNumber = baseRate.minus(newRate).absoluteValue()
return difference.multipliedBy(_10000).dividedBy(baseRate)
}
export function getTradeDetails(
tradeExact: TRADE_EXACT,
_tradeAmount: BigNumberish,
marketDetails: MarketDetails
): TradeDetails {
// get other input/output amount
const tradeAmount: BigNumber = normalizeBigNumberish(_tradeAmount)
// get other input/output amount
const { transput, inputReservesPost, outputReservesPost }: _PartialTradeDetails = getTradeTransput(
marketDetails.tradeType,
tradeExact,
......@@ -206,11 +214,7 @@ export function getTradeDetails(
amount: tradeExact === TRADE_EXACT.INPUT ? transput : tradeAmount
}
const marketDetailsPost: MarketDetails = getMarketDetails(
marketDetails.tradeType,
inputReservesPost,
outputReservesPost
)
const marketDetailsPost: MarketDetails = getMarketDetails(inputReservesPost, outputReservesPost)
const executionRate: Rate = calculateDecimalRate(outputAmount, inputAmount) as Rate
......
......@@ -3,16 +3,31 @@
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [{ "name": "", "type": "uint256" }],
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
......
This diff is collapsed.
[
{
"constant": true,
"inputs": [{ "name": "token", "type": "address" }],
"name": "NewExchange",
"inputs": [
{ "type": "address", "name": "token", "indexed": true },
{ "type": "address", "name": "exchange", "indexed": true }
],
"anonymous": false,
"type": "event"
},
{
"name": "initializeFactory",
"outputs": [],
"inputs": [{ "type": "address", "name": "template" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 35725
},
{
"name": "createExchange",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "token" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 187911
},
{
"name": "getExchange",
"outputs": [{ "name": "", "type": "address" }],
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "token" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 715
},
{
"name": "getToken",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "exchange" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 745
},
{
"name": "getTokenWithId",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "uint256", "name": "token_id" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 736
},
{
"name": "exchangeTemplate",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 633
},
{
"name": "tokenCount",
"outputs": [{ "type": "uint256", "name": "out" }],
"inputs": [],
"constant": true,
"payable": false,
"stateMutability": "view",
"type": "function"
"type": "function",
"gas": 663
}
]
import BigNumber from 'bignumber.js'
import { Token } from '../types'
import ERC20 from './abis/ERC20.json'
import FACTORY from './abis/FACTORY.json'
import EXCHANGE from './abis/EXCHANGE.json'
//// constants for internal and external use
export const ETH = 'ETH'
......@@ -21,9 +21,12 @@ export const FACTORY_ADDRESS: { [key: number]: string } = {
[SUPPORTED_CHAIN_ID.Kovan]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
}
export const FACTORY_ABI: string = JSON.stringify(FACTORY)
export const EXCHANGE_ABI: string = JSON.stringify(EXCHANGE)
export enum TRADE_TYPE {
TOKEN_TO_ETH = 'TOKEN_TO_ETH',
ETH_TO_TOKEN = 'ETH_TO_TOKEN',
TOKEN_TO_ETH = 'TOKEN_TO_ETH',
TOKEN_TO_TOKEN = 'TOKEN_TO_TOKEN'
}
......@@ -32,6 +35,36 @@ export enum TRADE_EXACT {
OUTPUT = 'OUTPUT'
}
export enum TRADE_METHODS {
ethToTokenSwapInput = 'ethToTokenSwapInput',
ethToTokenTransferInput = 'ethToTokenTransferInput',
ethToTokenSwapOutput = 'ethToTokenSwapOutput',
ethToTokenTransferOutput = 'ethToTokenTransferOutput',
tokenToEthSwapInput = 'tokenToEthSwapInput',
tokenToEthTransferInput = 'tokenToEthTransferInput',
tokenToEthSwapOutput = 'tokenToEthSwapOutput',
tokenToEthTransferOutput = 'tokenToEthTransferOutput',
tokenToTokenSwapInput = 'tokenToTokenSwapInput',
tokenToTokenTransferInput = 'tokenToTokenTransferInput',
tokenToTokenSwapOutput = 'tokenToTokenSwapOutput',
tokenToTokenTransferOutput = 'tokenToTokenTransferOutput'
}
export const TRADE_METHOD_IDS: { [key: string]: string } = {
[TRADE_METHODS.ethToTokenSwapInput]: '0xf39b5b9b',
[TRADE_METHODS.ethToTokenTransferInput]: '0xad65d76d',
[TRADE_METHODS.ethToTokenSwapOutput]: '0x6b1d4db7',
[TRADE_METHODS.ethToTokenTransferOutput]: '0x0b573638',
[TRADE_METHODS.tokenToEthSwapInput]: '0x95e3c50b',
[TRADE_METHODS.tokenToEthTransferInput]: '0x7237e031',
[TRADE_METHODS.tokenToEthSwapOutput]: '0x013efd8b',
[TRADE_METHODS.tokenToEthTransferOutput]: '0xd4e4841d',
[TRADE_METHODS.tokenToTokenSwapInput]: '0xddf7e1a7',
[TRADE_METHODS.tokenToTokenTransferInput]: '0xf552d91b',
[TRADE_METHODS.tokenToTokenSwapOutput]: '0xb040d545',
[TRADE_METHODS.tokenToTokenTransferOutput]: '0xf3c0efe9'
}
export enum FIXED_UNDERFLOW_BEHAVIOR {
ZERO = 'ZERO',
LESS_THAN = 'LESS_THAN',
......@@ -39,9 +72,9 @@ export enum FIXED_UNDERFLOW_BEHAVIOR {
}
//// constants for internal use
export const MAX_DECIMAL_PLACES = 18
export const ROUNDING_MODE = BigNumber.ROUND_HALF_UP
BigNumber.set({ DECIMAL_PLACES: MAX_DECIMAL_PLACES, ROUNDING_MODE })
export const _MAX_DECIMAL_PLACES = 18
export const _ROUNDING_MODE = BigNumber.ROUND_HALF_UP
BigNumber.set({ DECIMAL_PLACES: _MAX_DECIMAL_PLACES, ROUNDING_MODE: _ROUNDING_MODE })
export const _0: BigNumber = new BigNumber('0')
export const _1: BigNumber = new BigNumber('1')
......@@ -49,23 +82,16 @@ export const _10: BigNumber = new BigNumber('10')
export const _997: BigNumber = new BigNumber('997')
export const _1000: BigNumber = new BigNumber('1000')
export const _10000: BigNumber = new BigNumber('10000')
export const MAX_UINT8: number = 2 ** 8 - 1
export const MAX_UINT256: BigNumber = new BigNumber('2').exponentiatedBy(new BigNumber('256')).minus(_1)
export const _MAX_UINT8 = 255
export const _MAX_UINT256: BigNumber = new BigNumber(
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
)
export function ETH_TOKEN(chainId?: number): Token {
return {
...(chainId ? { chainId } : {}),
address: ETH,
decimals: 18
}
}
export const CHAIN_ID_NAME: { [key: number]: string } = {
export const _CHAIN_ID_NAME: { [key: number]: string } = {
[SUPPORTED_CHAIN_ID.Mainnet]: 'homestead',
[SUPPORTED_CHAIN_ID.Ropsten]: 'ropsten',
[SUPPORTED_CHAIN_ID.Rinkeby]: 'rinkeby',
[SUPPORTED_CHAIN_ID.Kovan]: 'kovan'
}
export const ERC20_ABI: string = JSON.stringify(ERC20)
export const FACTORY_ABI: string = JSON.stringify(FACTORY)
export const _ERC20_ABI: string = JSON.stringify(ERC20)
import { ethers } from 'ethers'
import { ETH, ETH_TOKEN, CHAIN_ID_NAME, ERC20_ABI, FACTORY_ADDRESS, FACTORY_ABI } from '../constants'
import { ChainIdOrProvider, isChainId, _ChainIdAndProvider, Token, TokenReservesNormalized } from '../types'
import { normalizeAddress, normalizeBigNumberish } from '../_utils'
import { ChainIdOrProvider, isChainId, Token, TokenReservesNormalized, _ChainIdAndProvider } from '../types'
import { ETH, SUPPORTED_CHAIN_ID, FACTORY_ABI, FACTORY_ADDRESS, _CHAIN_ID_NAME, _ERC20_ABI } from '../constants'
import { normalizeBigNumberish, normalizeAddress } from '../_utils'
// get contract object with address, ABI, and provider
function _getContract(address: string, ABI: string, provider: ethers.providers.BaseProvider): ethers.Contract {
return new ethers.Contract(normalizeAddress(address), ABI, provider)
function getContract(address: string, ABI: string, provider: ethers.providers.BaseProvider): ethers.Contract {
return new ethers.Contract(address, ABI, provider)
}
// get chain id and provider with either a chain id or a provider
async function _getChainIdAndProvider(chainIdOrProvider: ChainIdOrProvider): Promise<_ChainIdAndProvider> {
async function getChainIdAndProvider(chainIdOrProvider: ChainIdOrProvider): Promise<_ChainIdAndProvider> {
// if a chainId is provided, get a default provider for it
if (isChainId(chainIdOrProvider)) {
return {
chainId: chainIdOrProvider,
provider: ethers.getDefaultProvider(CHAIN_ID_NAME[chainIdOrProvider])
provider: ethers.getDefaultProvider(_CHAIN_ID_NAME[chainIdOrProvider])
}
}
// if a provider is provided, fetch the chainId from it
else {
const { chainId }: ethers.utils.Network = await chainIdOrProvider.getNetwork()
if (!(chainId in SUPPORTED_CHAIN_ID)) {
throw Error(`chainId ${chainId} is not valid.`)
}
return {
chainId,
provider: chainIdOrProvider
......@@ -28,13 +31,21 @@ async function _getChainIdAndProvider(chainIdOrProvider: ChainIdOrProvider): Pro
}
}
// get token data from an address and chain id/provider
async function _getToken(tokenAddress: string, chainIdAndProvider: _ChainIdAndProvider): Promise<Token> {
export function getEthToken(chainId?: number): Token {
return {
...(chainId ? { chainId } : {}),
address: ETH,
decimals: 18
}
}
async function getToken(tokenAddress: string, chainIdAndProvider: _ChainIdAndProvider): Promise<Token> {
if (tokenAddress === ETH) {
return ETH_TOKEN(chainIdAndProvider.chainId)
return getEthToken(chainIdAndProvider.chainId)
} else {
const ERC20Contract: ethers.Contract = _getContract(tokenAddress, ERC20_ABI, chainIdAndProvider.provider)
const ERC20Contract: ethers.Contract = getContract(tokenAddress, _ERC20_ABI, chainIdAndProvider.provider)
const decimals: number = await ERC20Contract.decimals()
return {
chainId: chainIdAndProvider.chainId,
address: ERC20Contract.address,
......@@ -43,28 +54,31 @@ async function _getToken(tokenAddress: string, chainIdAndProvider: _ChainIdAndPr
}
}
// external function to get token reserves
export async function getTokenReserves(
tokenAddress: string,
chainIdOrProvider: ChainIdOrProvider = 1
): Promise<TokenReservesNormalized> {
const chainIdAndProvider: _ChainIdAndProvider = await _getChainIdAndProvider(chainIdOrProvider)
// validate input arguments
const normalizedTokenAddress: string = normalizeAddress(tokenAddress)
const chainIdAndProvider: _ChainIdAndProvider = await getChainIdAndProvider(chainIdOrProvider)
// fetch tokens
const ethTokenPromise: Promise<Token> = _getToken(ETH, chainIdAndProvider)
const tokenPromise: Promise<Token> = _getToken(tokenAddress, chainIdAndProvider)
// fetch tokens (async)
const ethTokenPromise: Promise<Token> = getToken(ETH, chainIdAndProvider)
const tokenPromise: Promise<Token> = getToken(normalizedTokenAddress, chainIdAndProvider)
// get contracts
const factoryContract: ethers.Contract = _getContract(
const factoryContract: ethers.Contract = getContract(
FACTORY_ADDRESS[chainIdAndProvider.chainId],
FACTORY_ABI,
chainIdAndProvider.provider
)
const tokenContract: ethers.Contract = _getContract(tokenAddress, ERC20_ABI, chainIdAndProvider.provider)
const tokenContract: ethers.Contract = getContract(normalizedTokenAddress, _ERC20_ABI, chainIdAndProvider.provider)
const exchangeAddress: string = await factoryContract.getExchange(tokenContract.address)
// fetch exchange adddress (blocking async)
const exchangeAddress: string = await factoryContract.getExchange(normalizedTokenAddress)
const exchangeTokenPromise: Promise<Token> = _getToken(exchangeAddress, chainIdAndProvider)
// fetch exchange token and eth/token balances (async)
const exchangeTokenPromise: Promise<Token> = getToken(exchangeAddress, chainIdAndProvider)
const ethBalancePromise: Promise<ethers.utils.BigNumber> = chainIdAndProvider.provider.getBalance(exchangeAddress)
const tokenBalancePromise: Promise<ethers.utils.BigNumber> = tokenContract.balanceOf(exchangeAddress)
......
import BigNumber from 'bignumber.js'
import { BigNumberish, FlexibleFormat, isFormat, FormatSignificantOptions, FormatFixedOptions } from '../types'
import { _0, _10, MAX_DECIMAL_PLACES, ROUNDING_MODE, FIXED_UNDERFLOW_BEHAVIOR } from '../constants'
import { _0, _10, _MAX_DECIMAL_PLACES, _ROUNDING_MODE, FIXED_UNDERFLOW_BEHAVIOR } from '../constants'
import { normalizeBigNumberish, ensureBoundedInteger, ensureAllUInt256, ensureAllUInt8 } from '../_utils'
function _format(
bigNumber: BigNumber,
decimalPlaces: number,
roundingMode: BigNumber.RoundingMode = ROUNDING_MODE,
roundingMode: BigNumber.RoundingMode = _ROUNDING_MODE,
format: FlexibleFormat
): string {
return isFormat(format) || format
......@@ -17,11 +17,11 @@ function _format(
// bignumberish is converted to significantDigits, then cast back as a bignumber and formatted, dropping trailing 0s
export function formatSignificant(bigNumberish: BigNumberish, options?: FormatSignificantOptions): string {
const { significantDigits = 6, roundingMode = ROUNDING_MODE, forceIntegerSignificance = true, format = false } =
const { significantDigits = 6, roundingMode = _ROUNDING_MODE, forceIntegerSignificance = true, format = false } =
options || {}
const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish)
ensureBoundedInteger(significantDigits, [1, MAX_DECIMAL_PLACES])
ensureBoundedInteger(significantDigits, [1, _MAX_DECIMAL_PLACES])
const minimumSignificantDigits: number = forceIntegerSignificance ? bigNumber.integerValue().toFixed().length : 0
const preciseBigNumber: BigNumber = new BigNumber(
......@@ -34,14 +34,14 @@ export function formatSignificant(bigNumberish: BigNumberish, options?: FormatSi
export function formatFixed(bigNumberish: BigNumberish, options?: FormatFixedOptions): string {
const {
decimalPlaces = 4,
roundingMode = ROUNDING_MODE,
roundingMode = _ROUNDING_MODE,
dropTrailingZeros = true,
underflowBehavior = FIXED_UNDERFLOW_BEHAVIOR.ONE_DIGIT,
format = false
} = options || {}
const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish)
ensureBoundedInteger(decimalPlaces, MAX_DECIMAL_PLACES)
ensureBoundedInteger(decimalPlaces, _MAX_DECIMAL_PLACES)
const minimumNonZeroValue: BigNumber = new BigNumber(decimalPlaces === 0 ? '0.5' : `0.${'0'.repeat(decimalPlaces)}5`)
if (bigNumber.isLessThan(minimumNonZeroValue)) {
......@@ -75,8 +75,8 @@ function decimalize(bigNumberish: BigNumberish, decimals: number): BigNumber {
ensureAllUInt8([decimals])
if (decimals > MAX_DECIMAL_PLACES) {
throw Error(`This function does not support decimals greater than ${MAX_DECIMAL_PLACES}.`)
if (decimals > _MAX_DECIMAL_PLACES) {
throw Error(`This function does not support decimals greater than ${_MAX_DECIMAL_PLACES}.`)
}
return bigNumber.dividedBy(_10.exponentiatedBy(decimals))
......
......@@ -5,13 +5,17 @@ export {
ETH,
SUPPORTED_CHAIN_ID,
FACTORY_ADDRESS,
FACTORY_ABI,
EXCHANGE_ABI,
TRADE_TYPE,
TRADE_EXACT,
TRADE_METHODS,
TRADE_METHOD_IDS,
FIXED_UNDERFLOW_BEHAVIOR
} from './constants'
export * from './data'
export * from './computation'
export * from './format'
export * from './orchestration'
export * from './transact'
import { BigNumberish, ChainIdOrProvider, TokenReservesNormalized, MarketDetails, TradeDetails } from '../types'
import { TRADE_TYPE, TRADE_EXACT, ETH } from '../constants'
import {
BigNumberish,
ChainIdOrProvider,
TokenReservesNormalized,
OptionalReserves,
MarketDetails,
TradeDetails
} from '../types'
import { TRADE_EXACT } from '../constants'
import { getTokenReserves } from '../data'
import { getMarketDetails, getTradeDetails } from '../computation'
export async function getTrade(
inputTokenAddress: string,
outputTokenAddress: string,
tradeType: TRADE_TYPE,
tradeExact: TRADE_EXACT,
tradeAmount: BigNumberish,
//// eth for tokens
export function tradeExactEthForTokensWithData(reserves: OptionalReserves, ethAmount: BigNumberish): TradeDetails {
const marketDetails: MarketDetails = getMarketDetails(undefined, reserves)
return getTradeDetails(TRADE_EXACT.INPUT, ethAmount, marketDetails)
}
export async function tradeExactEthForTokens(
tokenAddress: string,
ethAmount: BigNumberish,
chainIdOrProvider?: ChainIdOrProvider
): Promise<TradeDetails> {
const tokenReserves: TokenReservesNormalized = await getTokenReserves(tokenAddress, chainIdOrProvider)
return tradeExactEthForTokensWithData(tokenReserves, ethAmount)
}
export function tradeEthForExactTokensWithData(reserves: OptionalReserves, tokenAmount: BigNumberish): TradeDetails {
const marketDetails: MarketDetails = getMarketDetails(undefined, reserves)
return getTradeDetails(TRADE_EXACT.OUTPUT, tokenAmount, marketDetails)
}
export async function tradeEthForExactTokens(
tokenAddress: string,
tokenAmount: BigNumberish,
chainIdOrProvider?: ChainIdOrProvider
): Promise<TradeDetails> {
const tokenReservesInput: TokenReservesNormalized | null =
inputTokenAddress === ETH ? null : await getTokenReserves(inputTokenAddress, chainIdOrProvider)
const tokenReservesOutput: TokenReservesNormalized | null =
outputTokenAddress === ETH ? null : await getTokenReserves(outputTokenAddress, chainIdOrProvider)
const tokenReserves: TokenReservesNormalized = await getTokenReserves(tokenAddress, chainIdOrProvider)
return tradeEthForExactTokensWithData(tokenReserves, tokenAmount)
}
//// tokens to eth
export function tradeExactTokensForEthWithData(reserves: OptionalReserves, tokenAmount: BigNumberish): TradeDetails {
const marketDetails: MarketDetails = getMarketDetails(reserves, undefined)
return getTradeDetails(TRADE_EXACT.INPUT, tokenAmount, marketDetails)
}
const marketDetails: MarketDetails = getMarketDetails(tradeType, tokenReservesInput, tokenReservesOutput)
export async function tradeExactTokensForEth(
tokenAddress: string,
tokenAmount: BigNumberish,
chainIdOrProvider?: ChainIdOrProvider
): Promise<TradeDetails> {
const tokenReserves: TokenReservesNormalized = await getTokenReserves(tokenAddress, chainIdOrProvider)
return tradeExactTokensForEthWithData(tokenReserves, tokenAmount)
}
return getTradeDetails(tradeExact, tradeAmount, marketDetails)
export function tradeTokensForExactEthWithData(reserves: OptionalReserves, ethAmount: BigNumberish): TradeDetails {
const marketDetails: MarketDetails = getMarketDetails(reserves, undefined)
return getTradeDetails(TRADE_EXACT.OUTPUT, ethAmount, marketDetails)
}
export async function tradeTokensForExactEth(
tokenAddress: string,
ethAmount: BigNumberish,
chainIdOrProvider?: ChainIdOrProvider
): Promise<TradeDetails> {
const tokenReserves: TokenReservesNormalized = await getTokenReserves(tokenAddress, chainIdOrProvider)
return tradeTokensForExactEthWithData(tokenReserves, ethAmount)
}
//// tokens for tokens
export function tradeExactTokensForTokensWithData(
reservesInput: OptionalReserves,
reservesOutput: OptionalReserves,
tokenAmount: BigNumberish
): TradeDetails {
const marketDetails: MarketDetails = getMarketDetails(reservesInput, reservesOutput)
return getTradeDetails(TRADE_EXACT.INPUT, tokenAmount, marketDetails)
}
export async function tradeExactTokensForTokens(
tokenAddressInput: string,
tokenAddressOutput: string,
tokenAmount: BigNumberish,
chainIdOrProvider?: ChainIdOrProvider
): Promise<TradeDetails> {
const tokenReservesInput: TokenReservesNormalized = await getTokenReserves(tokenAddressInput, chainIdOrProvider)
const tokenReservesOutput: TokenReservesNormalized = await getTokenReserves(tokenAddressOutput, chainIdOrProvider)
return tradeExactTokensForTokensWithData(tokenReservesInput, tokenReservesOutput, tokenAmount)
}
export function tradeTokensForExactTokensWithData(
reservesInput: OptionalReserves,
reservesOutput: OptionalReserves,
tokenAmount: BigNumberish
): TradeDetails {
const marketDetails: MarketDetails = getMarketDetails(reservesInput, reservesOutput)
return getTradeDetails(TRADE_EXACT.OUTPUT, tokenAmount, marketDetails)
}
export async function tradeTokensForExactTokens(
tokenAddressInput: string,
tokenAddressOutput: string,
tokenAmount: BigNumberish,
chainIdOrProvider?: ChainIdOrProvider
): Promise<TradeDetails> {
const tokenReservesInput: TokenReservesNormalized = await getTokenReserves(tokenAddressInput, chainIdOrProvider)
const tokenReservesOutput: TokenReservesNormalized = await getTokenReserves(tokenAddressOutput, chainIdOrProvider)
return tradeTokensForExactTokensWithData(tokenReservesInput, tokenReservesOutput, tokenAmount)
}
import BigNumber from 'bignumber.js'
import {
TokenReservesNormalized,
TradeDetails,
MethodArgument,
ExecutionDetails,
_SlippageBounds,
_PartialExecutionDetails
} from '../types'
import { _0, _1, _10000, _MAX_UINT256, TRADE_TYPE, TRADE_EXACT, TRADE_METHODS, TRADE_METHOD_IDS } from '../constants'
import { normalizeAddress } from '../_utils'
function removeUndefined(methodArguments: (MethodArgument | undefined)[]): MethodArgument[] {
return methodArguments.filter((a: MethodArgument | undefined): boolean => a !== undefined) as MethodArgument[]
}
function calculateSlippage(value: BigNumber, maxSlippage: number): _SlippageBounds {
const offset: BigNumber = value.multipliedBy(maxSlippage).dividedBy(_10000)
const minimum: BigNumber = value.minus(offset).integerValue(BigNumber.ROUND_FLOOR)
const maximum: BigNumber = value.plus(offset).integerValue(BigNumber.ROUND_CEIL)
return {
minimum: minimum.isLessThan(_0) ? _0 : minimum,
maximum: maximum.isGreaterThan(_MAX_UINT256) ? _MAX_UINT256 : maximum
}
}
function getReserves(trade: TradeDetails): TokenReservesNormalized {
switch (trade.tradeType) {
case TRADE_TYPE.ETH_TO_TOKEN: {
return trade.marketDetailsPre.outputReserves as TokenReservesNormalized
}
case TRADE_TYPE.TOKEN_TO_ETH: {
return trade.marketDetailsPre.inputReserves as TokenReservesNormalized
}
case TRADE_TYPE.TOKEN_TO_TOKEN: {
return trade.marketDetailsPre.inputReserves as TokenReservesNormalized
}
default: {
throw Error(`tradeType ${trade.tradeType} is invalid.`)
}
}
}
function getMethodName(trade: TradeDetails, transfer: boolean = false): string {
switch (trade.tradeType) {
case TRADE_TYPE.ETH_TO_TOKEN: {
if (trade.tradeExact === TRADE_EXACT.INPUT && !transfer) {
return TRADE_METHODS.ethToTokenSwapInput
} else if (trade.tradeExact === TRADE_EXACT.INPUT && transfer) {
return TRADE_METHODS.ethToTokenTransferInput
} else if (trade.tradeExact === TRADE_EXACT.OUTPUT && !transfer) {
return TRADE_METHODS.ethToTokenSwapOutput
} else {
return TRADE_METHODS.ethToTokenTransferOutput
}
}
case TRADE_TYPE.TOKEN_TO_ETH: {
if (trade.tradeExact === TRADE_EXACT.INPUT && !transfer) {
return TRADE_METHODS.tokenToEthSwapInput
} else if (trade.tradeExact === TRADE_EXACT.INPUT && transfer) {
return TRADE_METHODS.tokenToEthTransferInput
} else if (trade.tradeExact === TRADE_EXACT.OUTPUT && !transfer) {
return TRADE_METHODS.tokenToEthSwapOutput
} else {
return TRADE_METHODS.tokenToEthTransferOutput
}
}
case TRADE_TYPE.TOKEN_TO_TOKEN: {
if (trade.tradeExact === TRADE_EXACT.INPUT && !transfer) {
return TRADE_METHODS.tokenToTokenSwapInput
} else if (trade.tradeExact === TRADE_EXACT.INPUT && transfer) {
return TRADE_METHODS.tokenToTokenTransferInput
} else if (trade.tradeExact === TRADE_EXACT.OUTPUT && !transfer) {
return TRADE_METHODS.tokenToTokenSwapOutput
} else {
return TRADE_METHODS.tokenToTokenTransferOutput
}
}
default: {
throw Error(`tradeType ${trade.tradeType} is invalid.`)
}
}
}
function getValueAndMethodArguments(
trade: TradeDetails,
methodName: string,
maxSlippage: number,
deadline: number,
recipient?: string
): _PartialExecutionDetails {
switch (methodName) {
case TRADE_METHODS.ethToTokenSwapInput:
case TRADE_METHODS.ethToTokenTransferInput: {
return {
value: trade.inputAmount.amount,
methodArguments: removeUndefined([
calculateSlippage(trade.outputAmount.amount, maxSlippage).minimum,
deadline,
recipient
])
}
}
case TRADE_METHODS.ethToTokenSwapOutput:
case TRADE_METHODS.ethToTokenTransferOutput: {
return {
value: calculateSlippage(trade.inputAmount.amount, maxSlippage).maximum,
methodArguments: removeUndefined([trade.outputAmount.amount, deadline, recipient])
}
}
case TRADE_METHODS.tokenToEthSwapInput:
case TRADE_METHODS.tokenToEthTransferInput: {
return {
value: _0,
methodArguments: removeUndefined([
trade.inputAmount.amount,
calculateSlippage(trade.outputAmount.amount, maxSlippage).minimum,
deadline,
recipient
])
}
}
case TRADE_METHODS.tokenToEthSwapOutput:
case TRADE_METHODS.tokenToEthTransferOutput: {
return {
value: _0,
methodArguments: removeUndefined([
trade.outputAmount.amount,
calculateSlippage(trade.inputAmount.amount, maxSlippage).maximum,
deadline,
recipient
])
}
}
case TRADE_METHODS.tokenToTokenSwapInput:
case TRADE_METHODS.tokenToTokenTransferInput: {
if (!trade.outputAmount.token.address) {
throw Error('trade does not include output token address.')
}
return {
value: _0,
methodArguments: removeUndefined([
trade.inputAmount.amount,
calculateSlippage(trade.outputAmount.amount, maxSlippage).minimum,
_1,
deadline,
recipient,
trade.outputAmount.token.address
])
}
}
case TRADE_METHODS.tokenToTokenSwapOutput:
case TRADE_METHODS.tokenToTokenTransferOutput: {
if (!trade.outputAmount.token.address) {
throw Error('trade does not include output token address.')
}
return {
value: _0,
methodArguments: removeUndefined([
trade.outputAmount.amount,
calculateSlippage(trade.inputAmount.amount, maxSlippage).maximum,
_MAX_UINT256,
deadline,
recipient,
trade.outputAmount.token.address
])
}
}
default: {
throw Error(`methodName ${methodName} is invalid.`)
}
}
}
export function getExecutionDetails(
trade: TradeDetails,
maxSlippage?: number,
deadline?: number,
recipient?: string
): ExecutionDetails {
const reserves: TokenReservesNormalized = getReserves(trade)
if (!reserves.exchange || !reserves.exchange.address) {
throw Error('trade does not include exchange address.')
}
const methodName: string = getMethodName(trade, !!recipient)
const methodId: string = TRADE_METHOD_IDS[methodName]
const { value, methodArguments }: _PartialExecutionDetails = getValueAndMethodArguments(
trade,
methodName,
maxSlippage || 200,
deadline || Math.round(Date.now() / 1000 + 60 * 10),
recipient && normalizeAddress(recipient)
)
return {
exchangeAddress: reserves.exchange.address,
methodName,
methodId,
value,
methodArguments: methodArguments
}
}
......@@ -49,18 +49,22 @@ export interface EthReserves {
}
// type for input data
export type OptionalReserves = TokenReserves | EthReserves | undefined | null
export type OptionalReserves = TokenReserves | EthReserves | undefined
// type guard for OptionalReserves
export function areTokenReserves(reserves: OptionalReserves): reserves is TokenReserves {
const tokenReserves: TokenReserves = reserves as TokenReserves
return !!tokenReserves && tokenReserves.ethReserve !== undefined && tokenReserves.tokenReserve !== undefined
return (
tokenReserves !== undefined && tokenReserves.ethReserve !== undefined && tokenReserves.tokenReserve !== undefined
)
}
// type guard for OptionalReserves
export function areETHReserves(reserves: OptionalReserves): reserves is TokenReserves {
export function areETHReserves(reserves: OptionalReserves): reserves is EthReserves {
const tokenReserves: TokenReserves = reserves as TokenReserves
return !!tokenReserves && tokenReserves.ethReserve !== undefined && tokenReserves.tokenReserve !== undefined
return (
tokenReserves !== undefined && tokenReserves.ethReserve === undefined && tokenReserves.tokenReserve === undefined
)
}
// type for output data
......@@ -97,6 +101,16 @@ export interface TradeDetails {
executionRateSlippage: BigNumber
}
export type MethodArgument = BigNumber | number | string
export interface ExecutionDetails {
exchangeAddress: string
methodName: string
methodId: string
value: BigNumber
methodArguments: MethodArgument[]
}
//// types for formatting data
export type FlexibleFormat = BigNumber.Format | boolean
......@@ -147,3 +161,13 @@ export interface _PartialTradeDetails {
inputReservesPost: NormalizedReserves
outputReservesPost: NormalizedReserves
}
export interface _SlippageBounds {
minimum: BigNumber
maximum: BigNumber
}
export interface _PartialExecutionDetails {
value: BigNumber
methodArguments: MethodArgument[]
}
......@@ -12,7 +12,7 @@
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
// "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "incremental": true, /* Enable incremental compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment