This repository has been archived by the owner on Apr 25, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 95
/
trade.ts
500 lines (454 loc) · 18.6 KB
/
trade.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
import { Currency, Fraction, Percent, Price, sortedInsert, CurrencyAmount, TradeType, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { BestTradeOptions, Pool } from '@uniswap/v3-sdk'
import invariant from 'tiny-invariant'
import { ONE, ZERO } from '../../constants'
import { MixedRouteSDK } from './route'
/**
* Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The trade type, either exact input or exact output
* @param a The first trade to compare
* @param b The second trade to compare
* @returns A sorted ordering for two neighboring elements in a trade array
*/
export function tradeComparator<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
a: MixedRouteTrade<TInput, TOutput, TTradeType>,
b: MixedRouteTrade<TInput, TOutput, TTradeType>
) {
// must have same input and output token for comparison
invariant(a.inputAmount.currency.equals(b.inputAmount.currency), 'INPUT_CURRENCY')
invariant(a.outputAmount.currency.equals(b.outputAmount.currency), 'OUTPUT_CURRENCY')
if (a.outputAmount.equalTo(b.outputAmount)) {
if (a.inputAmount.equalTo(b.inputAmount)) {
// consider the number of hops since each hop costs gas
const aHops = a.swaps.reduce((total, cur) => total + cur.route.path.length, 0)
const bHops = b.swaps.reduce((total, cur) => total + cur.route.path.length, 0)
return aHops - bHops
}
// trade A requires less input than trade B, so A should come first
if (a.inputAmount.lessThan(b.inputAmount)) {
return -1
} else {
return 1
}
} else {
// tradeA has less output than trade B, so should come second
if (a.outputAmount.lessThan(b.outputAmount)) {
return 1
} else {
return -1
}
}
}
/**
* Represents a trade executed against a set of routes where some percentage of the input is
* split across each route.
*
* Each route has its own set of pools. Pools can not be re-used across routes.
*
* Does not account for slippage, i.e., changes in price environment that can occur between
* the time the trade is submitted and when it is executed.
* @notice This class is functionally the same as the `Trade` class in the `@uniswap/v3-sdk` package, aside from typing and some input validation.
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The trade type, either exact input or exact output
*/
export class MixedRouteTrade<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType> {
/**
* @deprecated Deprecated in favor of 'swaps' property. If the trade consists of multiple routes
* this will return an error.
*
* When the trade consists of just a single route, this returns the route of the trade,
* i.e. which pools the trade goes through.
*/
public get route(): MixedRouteSDK<TInput, TOutput> {
invariant(this.swaps.length == 1, 'MULTIPLE_ROUTES')
return this.swaps[0].route
}
/**
* The swaps of the trade, i.e. which routes and how much is swapped in each that
* make up the trade.
*/
public readonly swaps: {
route: MixedRouteSDK<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
/**
* The type of the trade, either exact in or exact out.
*/
public readonly tradeType: TTradeType
/**
* The cached result of the input amount computation
* @private
*/
private _inputAmount: CurrencyAmount<TInput> | undefined
/**
* The input amount for the trade assuming no slippage.
*/
public get inputAmount(): CurrencyAmount<TInput> {
if (this._inputAmount) {
return this._inputAmount
}
const inputCurrency = this.swaps[0].inputAmount.currency
const totalInputFromRoutes = this.swaps
.map(({ inputAmount }) => inputAmount)
.reduce((total, cur) => total.add(cur), CurrencyAmount.fromRawAmount(inputCurrency, 0))
this._inputAmount = totalInputFromRoutes
return this._inputAmount
}
/**
* The cached result of the output amount computation
* @private
*/
private _outputAmount: CurrencyAmount<TOutput> | undefined
/**
* The output amount for the trade assuming no slippage.
*/
public get outputAmount(): CurrencyAmount<TOutput> {
if (this._outputAmount) {
return this._outputAmount
}
const outputCurrency = this.swaps[0].outputAmount.currency
const totalOutputFromRoutes = this.swaps
.map(({ outputAmount }) => outputAmount)
.reduce((total, cur) => total.add(cur), CurrencyAmount.fromRawAmount(outputCurrency, 0))
this._outputAmount = totalOutputFromRoutes
return this._outputAmount
}
/**
* The cached result of the computed execution price
* @private
*/
private _executionPrice: Price<TInput, TOutput> | undefined
/**
* The price expressed in terms of output amount/input amount.
*/
public get executionPrice(): Price<TInput, TOutput> {
return (
this._executionPrice ??
(this._executionPrice = new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.inputAmount.quotient,
this.outputAmount.quotient
))
)
}
/**
* The cached result of the price impact computation
* @private
*/
private _priceImpact: Percent | undefined
/**
* Returns the percent difference between the route's mid price and the price impact
*/
public get priceImpact(): Percent {
if (this._priceImpact) {
return this._priceImpact
}
let spotOutputAmount = CurrencyAmount.fromRawAmount(this.outputAmount.currency, 0)
for (const { route, inputAmount } of this.swaps) {
const midPrice = route.midPrice
spotOutputAmount = spotOutputAmount.add(midPrice.quote(inputAmount))
}
const priceImpact = spotOutputAmount.subtract(this.outputAmount).divide(spotOutputAmount)
this._priceImpact = new Percent(priceImpact.numerator, priceImpact.denominator)
return this._priceImpact
}
/**
* Constructs a trade by simulating swaps through the given route
* @template TInput The input token, either Ether or an ERC-20.
* @template TOutput The output token, either Ether or an ERC-20.
* @template TTradeType The type of the trade, either exact in or exact out.
* @param route route to swap through
* @param amount the amount specified, either input or output, depending on tradeType
* @param tradeType whether the trade is an exact input or exact output swap
* @returns The route
*/
public static async fromRoute<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
route: MixedRouteSDK<TInput, TOutput>,
amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>,
tradeType: TTradeType
): Promise<MixedRouteTrade<TInput, TOutput, TTradeType>> {
const amounts: CurrencyAmount<Token>[] = new Array(route.path.length)
let inputAmount: CurrencyAmount<TInput>
let outputAmount: CurrencyAmount<TOutput>
invariant(tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE')
invariant(amount.currency.equals(route.input), 'INPUT')
amounts[0] = amount.wrapped
for (let i = 0; i < route.path.length - 1; i++) {
const pool = route.pools[i]
const [outputAmount] = await pool.getOutputAmount(amounts[i])
amounts[i + 1] = outputAmount
}
inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator)
outputAmount = CurrencyAmount.fromFractionalAmount(
route.output,
amounts[amounts.length - 1].numerator,
amounts[amounts.length - 1].denominator
)
return new MixedRouteTrade({
routes: [{ inputAmount, outputAmount, route }],
tradeType,
})
}
/**
* Constructs a trade from routes by simulating swaps
*
* @template TInput The input token, either Ether or an ERC-20.
* @template TOutput The output token, either Ether or an ERC-20.
* @template TTradeType The type of the trade, either exact in or exact out.
* @param routes the routes to swap through and how much of the amount should be routed through each
* @param tradeType whether the trade is an exact input or exact output swap
* @returns The trade
*/
public static async fromRoutes<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
routes: {
amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>
route: MixedRouteSDK<TInput, TOutput>
}[],
tradeType: TTradeType
): Promise<MixedRouteTrade<TInput, TOutput, TTradeType>> {
const populatedRoutes: {
route: MixedRouteSDK<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[] = []
invariant(tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE')
for (const { route, amount } of routes) {
const amounts: CurrencyAmount<Token>[] = new Array(route.path.length)
let inputAmount: CurrencyAmount<TInput>
let outputAmount: CurrencyAmount<TOutput>
invariant(amount.currency.equals(route.input), 'INPUT')
inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator)
amounts[0] = CurrencyAmount.fromFractionalAmount(route.input.wrapped, amount.numerator, amount.denominator)
for (let i = 0; i < route.path.length - 1; i++) {
const pool = route.pools[i]
const [outputAmount] = await pool.getOutputAmount(amounts[i])
amounts[i + 1] = outputAmount
}
outputAmount = CurrencyAmount.fromFractionalAmount(
route.output,
amounts[amounts.length - 1].numerator,
amounts[amounts.length - 1].denominator
)
populatedRoutes.push({ route, inputAmount, outputAmount })
}
return new MixedRouteTrade({
routes: populatedRoutes,
tradeType,
})
}
/**
* Creates a trade without computing the result of swapping through the route. Useful when you have simulated the trade
* elsewhere and do not have any tick data
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The type of the trade, either exact in or exact out
* @param constructorArguments The arguments passed to the trade constructor
* @returns The unchecked trade
*/
public static createUncheckedTrade<
TInput extends Currency,
TOutput extends Currency,
TTradeType extends TradeType
>(constructorArguments: {
route: MixedRouteSDK<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
tradeType: TTradeType
}): MixedRouteTrade<TInput, TOutput, TTradeType> {
return new MixedRouteTrade({
...constructorArguments,
routes: [
{
inputAmount: constructorArguments.inputAmount,
outputAmount: constructorArguments.outputAmount,
route: constructorArguments.route,
},
],
})
}
/**
* Creates a trade without computing the result of swapping through the routes. Useful when you have simulated the trade
* elsewhere and do not have any tick data
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The type of the trade, either exact in or exact out
* @param constructorArguments The arguments passed to the trade constructor
* @returns The unchecked trade
*/
public static createUncheckedTradeWithMultipleRoutes<
TInput extends Currency,
TOutput extends Currency,
TTradeType extends TradeType
>(constructorArguments: {
routes: {
route: MixedRouteSDK<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
tradeType: TTradeType
}): MixedRouteTrade<TInput, TOutput, TTradeType> {
return new MixedRouteTrade(constructorArguments)
}
/**
* Construct a trade by passing in the pre-computed property values
* @param routes The routes through which the trade occurs
* @param tradeType The type of trade, exact input or exact output
*/
private constructor({
routes,
tradeType,
}: {
routes: {
route: MixedRouteSDK<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
tradeType: TTradeType
}) {
const inputCurrency = routes[0].inputAmount.currency
const outputCurrency = routes[0].outputAmount.currency
invariant(
routes.every(({ route }) => inputCurrency.wrapped.equals(route.input.wrapped)),
'INPUT_CURRENCY_MATCH'
)
invariant(
routes.every(({ route }) => outputCurrency.wrapped.equals(route.output.wrapped)),
'OUTPUT_CURRENCY_MATCH'
)
const numPools = routes.map(({ route }) => route.pools.length).reduce((total, cur) => total + cur, 0)
const poolAddressSet = new Set<string>()
for (const { route } of routes) {
for (const pool of route.pools) {
pool instanceof Pool
? poolAddressSet.add(Pool.getAddress(pool.token0, pool.token1, pool.fee))
: poolAddressSet.add(Pair.getAddress(pool.token0, pool.token1))
}
}
invariant(numPools == poolAddressSet.size, 'POOLS_DUPLICATED')
invariant(tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE')
this.swaps = routes
this.tradeType = tradeType
}
/**
* Get the minimum amount that must be received from this trade for the given slippage tolerance
* @param slippageTolerance The tolerance of unfavorable slippage from the execution price of this trade
* @returns The amount out
*/
public minimumAmountOut(slippageTolerance: Percent, amountOut = this.outputAmount): CurrencyAmount<TOutput> {
invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
/// does not support exactOutput, as enforced in the constructor
const slippageAdjustedAmountOut = new Fraction(ONE)
.add(slippageTolerance)
.invert()
.multiply(amountOut.quotient).quotient
return CurrencyAmount.fromRawAmount(amountOut.currency, slippageAdjustedAmountOut)
}
/**
* Get the maximum amount in that can be spent via this trade for the given slippage tolerance
* @param slippageTolerance The tolerance of unfavorable slippage from the execution price of this trade
* @returns The amount in
*/
public maximumAmountIn(slippageTolerance: Percent, amountIn = this.inputAmount): CurrencyAmount<TInput> {
invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
return amountIn
/// does not support exactOutput
}
/**
* Return the execution price after accounting for slippage tolerance
* @param slippageTolerance the allowed tolerated slippage
* @returns The execution price
*/
public worstExecutionPrice(slippageTolerance: Percent): Price<TInput, TOutput> {
return new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.maximumAmountIn(slippageTolerance).quotient,
this.minimumAmountOut(slippageTolerance).quotient
)
}
/**
* Given a list of pools, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token
* amount to an output token, making at most `maxHops` hops.
* Note this does not consider aggregation, as routes are linear. It's possible a better route exists by splitting
* the amount in among multiple routes.
* @param pools the pools to consider in finding the best trade
* @param nextAmountIn exact amount of input currency to spend
* @param currencyOut the desired currency out
* @param maxNumResults maximum number of results to return
* @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pool
* @param currentPools used in recursion; the current list of pools
* @param currencyAmountIn used in recursion; the original value of the currencyAmountIn parameter
* @param bestTrades used in recursion; the current list of best trades
* @returns The exact in trade
*/
public static async bestTradeExactIn<TInput extends Currency, TOutput extends Currency>(
pools: (Pool | Pair)[],
currencyAmountIn: CurrencyAmount<TInput>,
currencyOut: TOutput,
{ maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {},
// used in recursion.
currentPools: (Pool | Pair)[] = [],
nextAmountIn: CurrencyAmount<Currency> = currencyAmountIn,
bestTrades: MixedRouteTrade<TInput, TOutput, TradeType.EXACT_INPUT>[] = []
): Promise<MixedRouteTrade<TInput, TOutput, TradeType.EXACT_INPUT>[]> {
invariant(pools.length > 0, 'POOLS')
invariant(maxHops > 0, 'MAX_HOPS')
invariant(currencyAmountIn === nextAmountIn || currentPools.length > 0, 'INVALID_RECURSION')
const amountIn = nextAmountIn.wrapped
const tokenOut = currencyOut.wrapped
for (let i = 0; i < pools.length; i++) {
const pool = pools[i]
// pool irrelevant
if (!pool.token0.equals(amountIn.currency) && !pool.token1.equals(amountIn.currency)) continue
if (pool instanceof Pair) {
if ((pool as Pair).reserve0.equalTo(ZERO) || (pool as Pair).reserve1.equalTo(ZERO)) continue
}
let amountOut: CurrencyAmount<Token>
try {
;[amountOut] = await pool.getOutputAmount(amountIn)
} catch (error) {
// input too low
// @ts-ignore[2571] error is unknown
if (error.isInsufficientInputAmountError) {
continue
}
throw error
}
// we have arrived at the output token, so this is the final trade of one of the paths
if (amountOut.currency.isToken && amountOut.currency.equals(tokenOut)) {
sortedInsert(
bestTrades,
await MixedRouteTrade.fromRoute(
new MixedRouteSDK([...currentPools, pool], currencyAmountIn.currency, currencyOut),
currencyAmountIn,
TradeType.EXACT_INPUT
),
maxNumResults,
tradeComparator
)
} else if (maxHops > 1 && pools.length > 1) {
const poolsExcludingThisPool = pools.slice(0, i).concat(pools.slice(i + 1, pools.length))
// otherwise, consider all the other paths that lead from this token as long as we have not exceeded maxHops
await MixedRouteTrade.bestTradeExactIn(
poolsExcludingThisPool,
currencyAmountIn,
currencyOut,
{
maxNumResults,
maxHops: maxHops - 1,
},
[...currentPools, pool],
amountOut,
bestTrades
)
}
}
return bestTrades
}
}