@alcorexchange/alcor-swap-sdk 1.0.33 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@ const swapMath_1 = require("../utils/swapMath");
17
17
  const tickMath_1 = require("../utils/tickMath");
18
18
  const tickDataProvider_1 = require("./tickDataProvider");
19
19
  const tickListDataProvider_1 = require("./tickListDataProvider");
20
+ const errors_1 = require("../errors");
20
21
  /**
21
22
  * By default, pools will not allow operations that require ticks.
22
23
  */
@@ -97,8 +98,14 @@ class Pool {
97
98
  getOutputAmount(inputAmount, sqrtPriceLimitX64) {
98
99
  (0, tiny_invariant_1.default)(this.involvesToken(inputAmount.currency), "TOKEN");
99
100
  const zeroForOne = inputAmount.currency.equals(this.tokenA);
100
- const { amountCalculated: outputAmount, } = this.swap(zeroForOne, inputAmount.quotient, sqrtPriceLimitX64);
101
+ const { amountA, amountB } = this.swap(zeroForOne, inputAmount.quotient, sqrtPriceLimitX64);
101
102
  const outputToken = zeroForOne ? this.tokenB : this.tokenA;
103
+ const outputAmount = zeroForOne ? amountB : amountA;
104
+ const amountIn = zeroForOne ? amountA : amountB;
105
+ //console.log(JSBI.equal(amountIn, inputAmount.quotient))
106
+ if (!jsbi_1.default.equal(amountIn, inputAmount.quotient)) {
107
+ throw new errors_1.InsufficientInputAmountError;
108
+ }
102
109
  return fractions_1.CurrencyAmount.fromRawAmount(outputToken, jsbi_1.default.multiply(outputAmount, internalConstants_2.NEGATIVE_ONE));
103
110
  }
104
111
  /**
@@ -109,8 +116,13 @@ class Pool {
109
116
  */
110
117
  getInputAmount(outputAmount, sqrtPriceLimitX64) {
111
118
  const zeroForOne = outputAmount.currency.equals(this.tokenB);
112
- const { amountCalculated: inputAmount, } = this.swap(zeroForOne, jsbi_1.default.multiply(outputAmount.quotient, internalConstants_2.NEGATIVE_ONE), sqrtPriceLimitX64);
119
+ const { amountA, amountB } = this.swap(zeroForOne, jsbi_1.default.multiply(outputAmount.quotient, internalConstants_2.NEGATIVE_ONE), sqrtPriceLimitX64);
113
120
  const inputToken = zeroForOne ? this.tokenA : this.tokenB;
121
+ const inputAmount = zeroForOne ? amountA : amountB;
122
+ const amountOutReceived = jsbi_1.default.multiply(zeroForOne ? amountB : amountA, internalConstants_2.NEGATIVE_ONE);
123
+ if (!jsbi_1.default.equal(amountOutReceived, outputAmount.quotient)) {
124
+ throw new errors_1.InsufficientReservesError;
125
+ }
114
126
  return fractions_1.CurrencyAmount.fromRawAmount(inputToken, inputAmount);
115
127
  }
116
128
  /**
@@ -137,7 +149,6 @@ class Pool {
137
149
  (0, tiny_invariant_1.default)(jsbi_1.default.greaterThan(sqrtPriceLimitX64, this.sqrtPriceX64), "RATIO_CURRENT");
138
150
  }
139
151
  const exactInput = jsbi_1.default.greaterThanOrEqual(amountSpecified, internalConstants_2.ZERO);
140
- // keep track of swap state
141
152
  const state = {
142
153
  amountSpecifiedRemaining: amountSpecified,
143
154
  amountCalculated: internalConstants_2.ZERO,
@@ -145,14 +156,10 @@ class Pool {
145
156
  tick: this.tickCurrent,
146
157
  liquidity: this.liquidity,
147
158
  };
148
- // start swap while loop
149
159
  while (jsbi_1.default.notEqual(state.amountSpecifiedRemaining, internalConstants_2.ZERO) &&
150
160
  state.sqrtPriceX64 != sqrtPriceLimitX64) {
151
161
  const step = {};
152
162
  step.sqrtPriceStartX64 = state.sqrtPriceX64;
153
- // because each iteration of the while loop rounds, we can't optimize this code (relative to the smart contract)
154
- // by simply traversing to the next available tick, we instead need to exactly replicate
155
- // tickBitmap.nextInitializedTickWithinOneWord
156
163
  [step.tickNext, step.initialized] =
157
164
  this.tickDataProvider.nextInitializedTickWithinOneWord(state.tick, zeroForOne, this.tickSpacing);
158
165
  if (step.tickNext < tickMath_1.TickMath.MIN_TICK) {
@@ -176,13 +183,9 @@ class Pool {
176
183
  state.amountSpecifiedRemaining = jsbi_1.default.add(state.amountSpecifiedRemaining, step.amountOut);
177
184
  state.amountCalculated = jsbi_1.default.add(state.amountCalculated, jsbi_1.default.add(step.amountIn, step.feeAmount));
178
185
  }
179
- // TODO
180
186
  if (jsbi_1.default.equal(state.sqrtPriceX64, step.sqrtPriceNextX64)) {
181
- // if the tick is initialized, run the tick transition
182
187
  if (step.initialized) {
183
188
  let liquidityNet = jsbi_1.default.BigInt((this.tickDataProvider.getTick(step.tickNext)).liquidityNet);
184
- // if we're moving leftward, we interpret liquidityNet as the opposite sign
185
- // safe because liquidityNet cannot be type(int128).min
186
189
  if (zeroForOne)
187
190
  liquidityNet = jsbi_1.default.multiply(liquidityNet, internalConstants_2.NEGATIVE_ONE);
188
191
  state.liquidity = liquidityMath_1.LiquidityMath.addDelta(state.liquidity, liquidityNet);
@@ -190,13 +193,18 @@ class Pool {
190
193
  state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
191
194
  }
192
195
  else if (jsbi_1.default.notEqual(state.sqrtPriceX64, step.sqrtPriceStartX64)) {
193
- // updated comparison function
194
- // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
195
196
  state.tick = tickMath_1.TickMath.getTickAtSqrtRatio(state.sqrtPriceX64);
196
197
  }
197
198
  }
199
+ const amountA = zeroForOne == exactInput
200
+ ? jsbi_1.default.subtract(amountSpecified, state.amountSpecifiedRemaining)
201
+ : state.amountCalculated;
202
+ const amountB = zeroForOne == exactInput
203
+ ? state.amountCalculated
204
+ : jsbi_1.default.subtract(amountSpecified, state.amountSpecifiedRemaining);
198
205
  return {
199
- amountCalculated: state.amountCalculated,
206
+ amountA,
207
+ amountB,
200
208
  sqrtPriceX64: state.sqrtPriceX64,
201
209
  liquidity: state.liquidity,
202
210
  tickCurrent: state.tick,
@@ -42,6 +42,7 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
42
42
  * make up the trade.
43
43
  */
44
44
  readonly swaps: {
45
+ percent: number;
45
46
  route: Route<TInput, TOutput>;
46
47
  inputAmount: CurrencyAmount<TInput>;
47
48
  outputAmount: CurrencyAmount<TOutput>;
@@ -114,7 +115,7 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
114
115
  * @param tradeType whether the trade is an exact input or exact output swap
115
116
  * @returns The route
116
117
  */
117
- static fromRoute<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(route: Route<TInput, TOutput>, amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>, tradeType: TTradeType): Trade<TInput, TOutput, TTradeType>;
118
+ static fromRoute<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(route: Route<TInput, TOutput>, amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>, tradeType: TTradeType, percent?: number): Trade<TInput, TOutput, TTradeType>;
118
119
  /**
119
120
  * Constructs a trade from routes by simulating swaps
120
121
  *
@@ -128,6 +129,7 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
128
129
  static fromRoutes<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(routes: {
129
130
  amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>;
130
131
  route: Route<TInput, TOutput>;
132
+ percent: number;
131
133
  }[], tradeType: TTradeType): Trade<TInput, TOutput, TTradeType>;
132
134
  /**
133
135
  * Creates a trade without computing the result of swapping through the route. Useful when you have simulated the trade
@@ -139,6 +141,7 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
139
141
  * @returns The unchecked trade
140
142
  */
141
143
  static createUncheckedTrade<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(constructorArguments: {
144
+ percent: number;
142
145
  route: Route<TInput, TOutput>;
143
146
  inputAmount: CurrencyAmount<TInput>;
144
147
  outputAmount: CurrencyAmount<TOutput>;
@@ -155,6 +158,7 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
155
158
  */
156
159
  static createUncheckedTradeWithMultipleRoutes<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(constructorArguments: {
157
160
  routes: {
161
+ percent: number;
158
162
  route: Route<TInput, TOutput>;
159
163
  inputAmount: CurrencyAmount<TInput>;
160
164
  outputAmount: CurrencyAmount<TOutput>;
@@ -187,4 +191,8 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
187
191
  worstExecutionPrice(slippageTolerance: Percent): Price<TInput, TOutput>;
188
192
  static bestTradeExactIn<TInput extends Currency, TOutput extends Currency>(routes: Route<TInput, TOutput>[], currencyAmountIn: CurrencyAmount<TInput>, maxNumResults?: number): Trade<TInput, TOutput, TradeType.EXACT_INPUT>[];
189
193
  static bestTradeExactOut<TInput extends Currency, TOutput extends Currency>(routes: Route<TInput, TOutput>[], currencyAmountOut: CurrencyAmount<TOutput>, maxNumResults?: number): Trade<TInput, TOutput, TradeType.EXACT_OUTPUT>[];
194
+ static bestTradeWithSplit<TInput extends Currency, TOutput extends Currency>(routes: Route<TInput, TOutput>[], amount: CurrencyAmount<Currency>, percents: number[], tradeType: TradeType, swapConfig?: {
195
+ minSplits: number;
196
+ maxSplits: number;
197
+ }): Trade<Currency, Currency, TradeType> | null;
190
198
  }
@@ -8,6 +8,7 @@ const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
8
8
  const fractions_1 = require("./fractions");
9
9
  const utils_1 = require("../utils");
10
10
  const internalConstants_1 = require("../internalConstants");
11
+ const getBestSwapRoute_1 = require("../utils/getBestSwapRoute");
11
12
  /**
12
13
  * Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them
13
14
  * @template TInput The input token, either Ether or an ERC-20
@@ -154,7 +155,7 @@ class Trade {
154
155
  * @param tradeType whether the trade is an exact input or exact output swap
155
156
  * @returns The route
156
157
  */
157
- static fromRoute(route, amount, tradeType) {
158
+ static fromRoute(route, amount, tradeType, percent = 100) {
158
159
  const amounts = new Array(route.tokenPath.length);
159
160
  let inputAmount;
160
161
  let outputAmount;
@@ -181,7 +182,7 @@ class Trade {
181
182
  outputAmount = fractions_1.CurrencyAmount.fromFractionalAmount(route.output, amount.numerator, amount.denominator);
182
183
  }
183
184
  return new Trade({
184
- routes: [{ inputAmount, outputAmount, route }],
185
+ routes: [{ inputAmount, outputAmount, route, percent }],
185
186
  tradeType
186
187
  });
187
188
  }
@@ -197,7 +198,7 @@ class Trade {
197
198
  */
198
199
  static fromRoutes(routes, tradeType) {
199
200
  const populatedRoutes = [];
200
- for (const { route, amount } of routes) {
201
+ for (const { route, amount, percent } of routes) {
201
202
  const amounts = new Array(route.tokenPath.length);
202
203
  let inputAmount;
203
204
  let outputAmount;
@@ -223,7 +224,7 @@ class Trade {
223
224
  }
224
225
  inputAmount = fractions_1.CurrencyAmount.fromFractionalAmount(route.input, amounts[0].numerator, amounts[0].denominator);
225
226
  }
226
- populatedRoutes.push({ route, inputAmount, outputAmount });
227
+ populatedRoutes.push({ route, inputAmount, outputAmount, percent });
227
228
  }
228
229
  return new Trade({
229
230
  routes: populatedRoutes,
@@ -244,6 +245,7 @@ class Trade {
244
245
  ...constructorArguments,
245
246
  routes: [
246
247
  {
248
+ percent: constructorArguments.percent,
247
249
  inputAmount: constructorArguments.inputAmount,
248
250
  outputAmount: constructorArguments.outputAmount,
249
251
  route: constructorArguments.route
@@ -329,11 +331,20 @@ class Trade {
329
331
  (0, tiny_invariant_1.default)(routes.length > 0, 'ROUTES');
330
332
  const bestTrades = [];
331
333
  for (const route of routes) {
332
- const trade = Trade.fromRoute(route, currencyAmountIn, internalConstants_1.TradeType.EXACT_INPUT);
334
+ let trade;
335
+ try {
336
+ trade = Trade.fromRoute(route, currencyAmountIn, internalConstants_1.TradeType.EXACT_INPUT);
337
+ }
338
+ catch (error) {
339
+ // not enough liquidity in this pair
340
+ if (error.isInsufficientInputAmountError) {
341
+ continue;
342
+ }
343
+ throw error;
344
+ }
333
345
  // FIXME! Sorting bug multiple pools
334
- if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) {
346
+ if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0))
335
347
  continue;
336
- }
337
348
  (0, utils_1.sortedInsert)(bestTrades, trade, maxNumResults, tradeComparator);
338
349
  }
339
350
  return bestTrades;
@@ -342,13 +353,57 @@ class Trade {
342
353
  (0, tiny_invariant_1.default)(routes.length > 0, 'ROUTES');
343
354
  const bestTrades = [];
344
355
  for (const route of routes) {
345
- const trade = Trade.fromRoute(route, currencyAmountOut, internalConstants_1.TradeType.EXACT_OUTPUT);
346
- if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) {
347
- continue;
356
+ let trade;
357
+ try {
358
+ trade = Trade.fromRoute(route, currencyAmountOut, internalConstants_1.TradeType.EXACT_OUTPUT);
348
359
  }
360
+ catch (error) {
361
+ // not enough liquidity in this pair
362
+ if (error.isInsufficientReservesError) {
363
+ continue;
364
+ }
365
+ throw error;
366
+ }
367
+ if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0))
368
+ continue;
349
369
  (0, utils_1.sortedInsert)(bestTrades, trade, maxNumResults, tradeComparator);
350
370
  }
351
371
  return bestTrades;
352
372
  }
373
+ static bestTradeWithSplit(routes, amount, percents, tradeType, swapConfig = { minSplits: 1, maxSplits: 10 }) {
374
+ (0, tiny_invariant_1.default)(routes.length > 0, 'ROUTES');
375
+ (0, tiny_invariant_1.default)(percents.length > 0, 'PERCENTS');
376
+ // Compute routes for all percents for all routes
377
+ const percentToTrades = {};
378
+ for (const percent of percents) {
379
+ const splitAmount = amount.multiply(percent).divide(100);
380
+ for (const route of routes) {
381
+ let trade;
382
+ try {
383
+ trade = Trade.fromRoute(route, splitAmount, tradeType, percent);
384
+ }
385
+ catch (error) {
386
+ // not enough liquidity in this pair
387
+ if (error.isInsufficientReservesError || error.isInsufficientInputAmountError) {
388
+ continue;
389
+ }
390
+ throw error;
391
+ }
392
+ if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0))
393
+ continue;
394
+ if (!percentToTrades[percent]) {
395
+ percentToTrades[percent] = [];
396
+ }
397
+ percentToTrades[percent].push(trade);
398
+ }
399
+ }
400
+ const bestTrades = (0, getBestSwapRoute_1.getBestSwapRoute)(tradeType, percentToTrades, percents, swapConfig);
401
+ if (!bestTrades)
402
+ return null;
403
+ return new Trade({
404
+ routes: bestTrades.map(({ inputAmount, outputAmount, route, swaps }) => ({ inputAmount, outputAmount, route, percent: swaps[0].percent })),
405
+ tradeType
406
+ });
407
+ }
353
408
  }
354
409
  exports.Trade = Trade;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Indicates that the pair has insufficient reserves for a desired output amount. I.e. the amount of output cannot be
3
+ * obtained by sending any amount of input.
4
+ */
5
+ export declare class InsufficientReservesError extends Error {
6
+ readonly isInsufficientReservesError = true;
7
+ constructor();
8
+ }
9
+ /**
10
+ * Indicates that the input amount is too small to produce any amount of output. I.e. the amount of input sent is less
11
+ * than the price of a single unit of output after fees.
12
+ */
13
+ export declare class InsufficientInputAmountError extends Error {
14
+ readonly isInsufficientInputAmountError = true;
15
+ constructor();
16
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InsufficientInputAmountError = exports.InsufficientReservesError = void 0;
4
+ // see https://stackoverflow.com/a/41102306
5
+ const CAN_SET_PROTOTYPE = 'setPrototypeOf' in Object;
6
+ /**
7
+ * Indicates that the pair has insufficient reserves for a desired output amount. I.e. the amount of output cannot be
8
+ * obtained by sending any amount of input.
9
+ */
10
+ class InsufficientReservesError extends Error {
11
+ constructor() {
12
+ super();
13
+ this.isInsufficientReservesError = true;
14
+ this.name = this.constructor.name;
15
+ if (CAN_SET_PROTOTYPE)
16
+ Object.setPrototypeOf(this, new.target.prototype);
17
+ }
18
+ }
19
+ exports.InsufficientReservesError = InsufficientReservesError;
20
+ /**
21
+ * Indicates that the input amount is too small to produce any amount of output. I.e. the amount of input sent is less
22
+ * than the price of a single unit of output after fees.
23
+ */
24
+ class InsufficientInputAmountError extends Error {
25
+ constructor() {
26
+ super();
27
+ this.isInsufficientInputAmountError = true;
28
+ this.name = this.constructor.name;
29
+ if (CAN_SET_PROTOTYPE)
30
+ Object.setPrototypeOf(this, new.target.prototype);
31
+ }
32
+ }
33
+ exports.InsufficientInputAmountError = InsufficientInputAmountError;
@@ -1,11 +1,12 @@
1
1
  export declare function parseTrade(trade: any): {
2
+ route: never[];
3
+ memo: string;
4
+ swaps: any;
2
5
  input: any;
3
6
  output: any;
4
7
  minReceived: any;
5
8
  maxSent: any;
6
9
  priceImpact: any;
7
- memo: string;
8
- route: any;
9
10
  executionPrice: {
10
11
  numerator: any;
11
12
  denominator: any;
@@ -7,24 +7,37 @@ function parseTrade(trade) {
7
7
  // Parse Trade into api format object
8
8
  const slippage = new entities_1.Percent(3, 100); // 0.3%
9
9
  const receiver = '<receiver>';
10
- const tradeType = trade.tradeType == internalConstants_1.TradeType.EXACT_INPUT ? 'swapexactin' : 'swapexactout';
11
- const route = trade.route.pools.map(p => p.id);
12
- const maxSent = trade.inputAmount;
13
- const minReceived = trade.minimumAmountOut(slippage);
14
- const memo = `${tradeType}#${route.join(',')}#${receiver}#${minReceived.toExtendedAsset()}#0`;
10
+ const exactIn = trade.tradeType === internalConstants_1.TradeType.EXACT_INPUT;
11
+ console.log('trade.tradeType', trade.tradeType, internalConstants_1.TradeType.EXACT_INPUT);
12
+ const maxSent = exactIn ? trade.inputAmount : trade.maximumAmountIn(slippage);
13
+ const minReceived = exactIn ? trade.minimumAmountOut(slippage) : trade.outputAmount;
14
+ const tradeType = exactIn ? 'swapexactin' : 'swapexactout';
15
+ const swaps = trade.swaps.map(({ route, percent, inputAmount, outputAmount }) => {
16
+ route = route.pools.map(p => p.id);
17
+ const maxSent = exactIn ? inputAmount : trade.maximumAmountIn(slippage, inputAmount);
18
+ const minReceived = exactIn ? trade.minimumAmountOut(slippage, outputAmount) : outputAmount;
19
+ const input = inputAmount.toAsset();
20
+ const output = outputAmount.toAsset();
21
+ const memo = `${tradeType}#${route.join(',')}#${receiver}#${minReceived.toExtendedAsset()}#0`;
22
+ return { input, route, output, percent, memo, maxSent: maxSent.toFixed(), minReceived: minReceived.toFixed() };
23
+ });
15
24
  const result = {
25
+ route: [],
26
+ memo: '',
27
+ swaps,
16
28
  input: trade.inputAmount.toFixed(),
17
29
  output: trade.outputAmount.toFixed(),
18
30
  minReceived: minReceived.toFixed(),
19
31
  maxSent: maxSent.toFixed(),
20
32
  priceImpact: trade.priceImpact.toSignificant(2),
21
- memo,
22
- route,
23
33
  executionPrice: {
24
34
  numerator: trade.executionPrice.numerator.toString(),
25
35
  denominator: trade.executionPrice.denominator.toString()
26
36
  }
27
37
  };
38
+ // FIXME DEPRECATED Hotfix for legacy v1
39
+ result.route = trade.swaps[0].route.pools.map((p) => p.id);
40
+ result.memo = `${tradeType}#${result.route.join(',')}#${receiver}#${minReceived.toExtendedAsset()}#0`;
28
41
  return result;
29
42
  }
30
43
  exports.parseTrade = parseTrade;
@@ -0,0 +1,9 @@
1
+ import { Currency } from '../entities/currency';
2
+ import { Trade } from '../entities/trade';
3
+ import { TradeType } from '../internalConstants';
4
+ export declare function getBestSwapRoute(routeType: TradeType, percentToQuotes: {
5
+ [percent: number]: Trade<Currency, Currency, TradeType>[];
6
+ }, percents: number[], swapRouteConfig?: {
7
+ minSplits: number;
8
+ maxSplits: number;
9
+ }): Trade<Currency, Currency, TradeType>[] | null;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getBestSwapRoute = void 0;
7
+ const lodash_1 = __importDefault(require("lodash"));
8
+ const queue_1 = __importDefault(require("mnemonist/queue"));
9
+ const fixed_reverse_heap_1 = __importDefault(require("mnemonist/fixed-reverse-heap"));
10
+ const internalConstants_1 = require("../internalConstants");
11
+ function getBestSwapRoute(routeType, percentToQuotes, percents, swapRouteConfig = { minSplits: 1, maxSplits: 8 }) {
12
+ const percentToSortedQuotes = lodash_1.default.mapValues(percentToQuotes, (routeQuotes) => {
13
+ return routeQuotes.sort((routeQuoteA, routeQuoteB) => {
14
+ if (routeType == internalConstants_1.TradeType.EXACT_INPUT) {
15
+ return routeQuoteA.outputAmount.greaterThan(routeQuoteB.outputAmount) ? -1 : 1;
16
+ }
17
+ else {
18
+ return routeQuoteA.inputAmount.lessThan(routeQuoteB.inputAmount) ? -1 : 1;
19
+ }
20
+ });
21
+ });
22
+ const quoteCompFn = routeType == internalConstants_1.TradeType.EXACT_INPUT
23
+ ? (a, b) => a.greaterThan(b)
24
+ : (a, b) => a.lessThan(b);
25
+ const sumFn = (currencyAmounts) => {
26
+ let sum = currencyAmounts[0];
27
+ for (let i = 1; i < currencyAmounts.length; i++) {
28
+ sum = sum.add(currencyAmounts[i]);
29
+ }
30
+ return sum;
31
+ };
32
+ let bestQuote;
33
+ let bestSwap;
34
+ const bestSwapsPerSplit = new fixed_reverse_heap_1.default(Array, (a, b) => {
35
+ return quoteCompFn(a.quote, b.quote) ? -1 : 1;
36
+ }, 3);
37
+ const { minSplits, maxSplits } = swapRouteConfig;
38
+ if (!percentToSortedQuotes[100] || minSplits > 1) {
39
+ console.log({
40
+ percentToSortedQuotes: lodash_1.default.mapValues(percentToSortedQuotes, (p) => p.length),
41
+ }, 'Did not find a valid route without any splits. Continuing search anyway.');
42
+ }
43
+ else {
44
+ bestQuote = percentToSortedQuotes[100][0].outputAmount;
45
+ bestSwap = [percentToSortedQuotes[100][0]];
46
+ for (const routeWithQuote of percentToSortedQuotes[100].slice(0, 5)) {
47
+ bestSwapsPerSplit.push({
48
+ quote: routeWithQuote.outputAmount,
49
+ routes: [routeWithQuote],
50
+ });
51
+ }
52
+ }
53
+ const queue = new queue_1.default();
54
+ if (percents.length === 0)
55
+ return null; // Handle empty percents array
56
+ for (let i = percents.length - 1; i >= 0; i--) {
57
+ const percent = percents[i];
58
+ if (!percentToSortedQuotes[percent]) {
59
+ console.log('continue', { percent });
60
+ continue;
61
+ }
62
+ queue.enqueue({
63
+ curRoutes: [percentToSortedQuotes[percent][0]],
64
+ percentIndex: i,
65
+ remainingPercent: 100 - percent,
66
+ special: false,
67
+ });
68
+ if (!percentToSortedQuotes[percent] ||
69
+ !percentToSortedQuotes[percent][1]) {
70
+ console.log('continue2', { percent });
71
+ continue;
72
+ }
73
+ queue.enqueue({
74
+ curRoutes: [percentToSortedQuotes[percent][1]],
75
+ percentIndex: i,
76
+ remainingPercent: 100 - percent,
77
+ special: true,
78
+ });
79
+ }
80
+ let splits = 1;
81
+ while (queue.size > 0) {
82
+ bestSwapsPerSplit.clear();
83
+ let layer = queue.size;
84
+ splits++;
85
+ if (splits >= 3 && bestSwap && bestSwap.length < splits - 1) {
86
+ break;
87
+ }
88
+ if (splits > maxSplits) {
89
+ break;
90
+ }
91
+ while (layer > 0) {
92
+ layer--;
93
+ const { remainingPercent, curRoutes, percentIndex, special } = queue.dequeue();
94
+ for (let i = percentIndex; i >= 0; i--) {
95
+ const percentA = percents[i];
96
+ if (percentA > remainingPercent) {
97
+ continue;
98
+ }
99
+ if (!percentToSortedQuotes[percentA]) {
100
+ continue;
101
+ }
102
+ const candidateRoutesA = percentToSortedQuotes[percentA];
103
+ const routeWithQuoteA = findFirstRouteNotUsingUsedPools(curRoutes, candidateRoutesA);
104
+ if (!routeWithQuoteA) {
105
+ continue;
106
+ }
107
+ const remainingPercentNew = remainingPercent - percentA;
108
+ const curRoutesNew = [...curRoutes, routeWithQuoteA];
109
+ if (remainingPercentNew == 0 && splits >= minSplits) {
110
+ const quotesNew = lodash_1.default.map(curRoutesNew, (r) => r.outputAmount);
111
+ const quoteNew = sumFn(quotesNew);
112
+ bestSwapsPerSplit.push({
113
+ quote: quoteNew,
114
+ routes: curRoutesNew,
115
+ });
116
+ if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) {
117
+ bestQuote = quoteNew;
118
+ bestSwap = curRoutesNew;
119
+ }
120
+ }
121
+ else {
122
+ queue.enqueue({
123
+ curRoutes: curRoutesNew,
124
+ remainingPercent: remainingPercentNew,
125
+ percentIndex: i,
126
+ special,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ if (!bestSwap) {
133
+ console.log(`Could not find a valid swap`);
134
+ return null;
135
+ }
136
+ const quote = sumFn(lodash_1.default.map(bestSwap, (routeWithValidQuote) => routeWithValidQuote.outputAmount));
137
+ const routeWithQuotes = bestSwap.sort((routeAmountA, routeAmountB) => routeAmountB.outputAmount.greaterThan(routeAmountA.outputAmount) ? 1 : -1);
138
+ return routeWithQuotes;
139
+ }
140
+ exports.getBestSwapRoute = getBestSwapRoute;
141
+ const findFirstRouteNotUsingUsedPools = (usedRoutes, candidateRoutes) => {
142
+ const usedPools = new Set();
143
+ usedRoutes.forEach(r => r.route.pools.forEach(pool => usedPools.add(pool.id)));
144
+ for (const candidate of candidateRoutes) {
145
+ if (candidate.route.pools.every(pool => !usedPools.has(pool.id))) {
146
+ return candidate;
147
+ }
148
+ }
149
+ return null;
150
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alcorexchange/alcor-swap-sdk",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -45,7 +45,7 @@
45
45
  "redis": "^4.6.10",
46
46
  "rimraf": "^4.1.1",
47
47
  "ts-jest": "^28.0.4",
48
- "ts-node": "^10.9.1",
48
+ "ts-node": "^10.9.2",
49
49
  "typescript": "^4.9.4"
50
50
  },
51
51
  "dependencies": {
@@ -55,6 +55,8 @@
55
55
  "eosjs": "^22.1.0",
56
56
  "eosjs-account-name": "2.3.0",
57
57
  "jsbi": "^4.3.0",
58
+ "lodash": "^4.17.21",
59
+ "mnemonist": "^0.39.8",
58
60
  "msgpack-lite": "^0.1.26",
59
61
  "node-fetch": "2",
60
62
  "tiny-invariant": "^1.1.0",
@@ -13,6 +13,7 @@ import { TickMath } from "../utils/tickMath";
13
13
  import { Tick, TickConstructorArgs } from "./tick";
14
14
  import { NoTickDataProvider, TickDataProvider } from "./tickDataProvider";
15
15
  import { TickListDataProvider } from "./tickListDataProvider";
16
+ import {InsufficientReservesError, InsufficientInputAmountError} from "../errors";
16
17
 
17
18
  export interface PoolConstructorArgs {
18
19
  id: number,
@@ -190,10 +191,17 @@ export class Pool {
190
191
 
191
192
  const zeroForOne = inputAmount.currency.equals(this.tokenA);
192
193
 
193
- const {
194
- amountCalculated: outputAmount,
195
- } = this.swap(zeroForOne, inputAmount.quotient, sqrtPriceLimitX64);
194
+ const { amountA, amountB } = this.swap(zeroForOne, inputAmount.quotient, sqrtPriceLimitX64);
195
+
196
196
  const outputToken = zeroForOne ? this.tokenB : this.tokenA;
197
+ const outputAmount = zeroForOne ? amountB : amountA;
198
+ const amountIn = zeroForOne ? amountA : amountB;
199
+
200
+ //console.log(JSBI.equal(amountIn, inputAmount.quotient))
201
+ if (!JSBI.equal(amountIn, inputAmount.quotient)) {
202
+ throw new InsufficientInputAmountError
203
+ }
204
+
197
205
  return CurrencyAmount.fromRawAmount(
198
206
  outputToken,
199
207
  JSBI.multiply(outputAmount, NEGATIVE_ONE)
@@ -212,14 +220,16 @@ export class Pool {
212
220
  ): CurrencyAmount<Token> {
213
221
  const zeroForOne = outputAmount.currency.equals(this.tokenB);
214
222
 
215
- const {
216
- amountCalculated: inputAmount,
217
- } = this.swap(
218
- zeroForOne,
219
- JSBI.multiply(outputAmount.quotient, NEGATIVE_ONE),
220
- sqrtPriceLimitX64
221
- );
223
+ const { amountA, amountB } = this.swap(zeroForOne, JSBI.multiply(outputAmount.quotient, NEGATIVE_ONE), sqrtPriceLimitX64);
224
+
222
225
  const inputToken = zeroForOne ? this.tokenA : this.tokenB;
226
+ const inputAmount = zeroForOne ? amountA : amountB;
227
+ const amountOutReceived = JSBI.multiply(zeroForOne ? amountB : amountA, NEGATIVE_ONE)
228
+
229
+ if (!JSBI.equal(amountOutReceived, outputAmount.quotient)) {
230
+ throw new InsufficientReservesError
231
+ }
232
+
223
233
  return CurrencyAmount.fromRawAmount(inputToken, inputAmount)
224
234
  }
225
235
 
@@ -238,7 +248,8 @@ export class Pool {
238
248
  amountSpecified: JSBI,
239
249
  sqrtPriceLimitX64?: JSBI
240
250
  ): {
241
- amountCalculated: JSBI;
251
+ amountA: JSBI;
252
+ amountB: JSBI;
242
253
  sqrtPriceX64: JSBI;
243
254
  liquidity: JSBI;
244
255
  tickCurrent: number;
@@ -270,8 +281,6 @@ export class Pool {
270
281
 
271
282
  const exactInput = JSBI.greaterThanOrEqual(amountSpecified, ZERO);
272
283
 
273
- // keep track of swap state
274
-
275
284
  const state = {
276
285
  amountSpecifiedRemaining: amountSpecified,
277
286
  amountCalculated: ZERO,
@@ -280,7 +289,6 @@ export class Pool {
280
289
  liquidity: this.liquidity,
281
290
  };
282
291
 
283
- // start swap while loop
284
292
  while (
285
293
  JSBI.notEqual(state.amountSpecifiedRemaining, ZERO) &&
286
294
  state.sqrtPriceX64 != sqrtPriceLimitX64
@@ -288,9 +296,6 @@ export class Pool {
288
296
  const step: Partial<StepComputations> = {};
289
297
  step.sqrtPriceStartX64 = state.sqrtPriceX64;
290
298
 
291
- // because each iteration of the while loop rounds, we can't optimize this code (relative to the smart contract)
292
- // by simply traversing to the next available tick, we instead need to exactly replicate
293
- // tickBitmap.nextInitializedTickWithinOneWord
294
299
  [step.tickNext, step.initialized] =
295
300
  this.tickDataProvider.nextInitializedTickWithinOneWord(
296
301
  state.tick,
@@ -340,15 +345,11 @@ export class Pool {
340
345
  );
341
346
  }
342
347
 
343
- // TODO
344
348
  if (JSBI.equal(state.sqrtPriceX64, step.sqrtPriceNextX64)) {
345
- // if the tick is initialized, run the tick transition
346
349
  if (step.initialized) {
347
350
  let liquidityNet = JSBI.BigInt(
348
351
  (this.tickDataProvider.getTick(step.tickNext)).liquidityNet
349
352
  );
350
- // if we're moving leftward, we interpret liquidityNet as the opposite sign
351
- // safe because liquidityNet cannot be type(int128).min
352
353
  if (zeroForOne)
353
354
  liquidityNet = JSBI.multiply(liquidityNet, NEGATIVE_ONE);
354
355
 
@@ -360,14 +361,20 @@ export class Pool {
360
361
 
361
362
  state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
362
363
  } else if (JSBI.notEqual(state.sqrtPriceX64, step.sqrtPriceStartX64)) {
363
- // updated comparison function
364
- // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
365
364
  state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX64);
366
365
  }
367
366
  }
368
367
 
368
+ const amountA = zeroForOne == exactInput
369
+ ? JSBI.subtract(amountSpecified, state.amountSpecifiedRemaining)
370
+ : state.amountCalculated;
371
+ const amountB = zeroForOne == exactInput
372
+ ? state.amountCalculated
373
+ : JSBI.subtract(amountSpecified, state.amountSpecifiedRemaining);
374
+
369
375
  return {
370
- amountCalculated: state.amountCalculated,
376
+ amountA,
377
+ amountB,
371
378
  sqrtPriceX64: state.sqrtPriceX64,
372
379
  liquidity: state.liquidity,
373
380
  tickCurrent: state.tick,
@@ -1,12 +1,13 @@
1
1
  import invariant from 'tiny-invariant'
2
2
 
3
+
3
4
  import { Currency } from './currency'
4
5
  import { Fraction, Percent, Price, CurrencyAmount } from './fractions'
5
- import { sortedInsert, parseTrade } from '../utils'
6
+ import { sortedInsert } from '../utils'
6
7
  import { Token } from './token'
7
8
  import { ONE, ZERO, TradeType } from '../internalConstants'
8
- import { Pool } from './pool'
9
9
  import { Route } from './route'
10
+ import { getBestSwapRoute } from '../utils/getBestSwapRoute'
10
11
 
11
12
  /**
12
13
  * Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them
@@ -84,6 +85,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
84
85
  * make up the trade.
85
86
  */
86
87
  public readonly swaps: {
88
+ percent: number,
87
89
  route: Route<TInput, TOutput>
88
90
  inputAmount: CurrencyAmount<TInput>
89
91
  outputAmount: CurrencyAmount<TOutput>
@@ -230,7 +232,8 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
230
232
  public static fromRoute<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
231
233
  route: Route<TInput, TOutput>,
232
234
  amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>,
233
- tradeType: TTradeType
235
+ tradeType: TTradeType,
236
+ percent = 100
234
237
  ): Trade<TInput, TOutput, TTradeType> {
235
238
  const amounts: CurrencyAmount<Token>[] = new Array(route.tokenPath.length)
236
239
  let inputAmount: CurrencyAmount<TInput>
@@ -262,7 +265,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
262
265
  }
263
266
 
264
267
  return new Trade({
265
- routes: [{ inputAmount, outputAmount, route }],
268
+ routes: [{ inputAmount, outputAmount, route, percent }],
266
269
  tradeType
267
270
  })
268
271
  }
@@ -280,17 +283,19 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
280
283
  public static fromRoutes<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
281
284
  routes: {
282
285
  amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>
283
- route: Route<TInput, TOutput>
286
+ route: Route<TInput, TOutput>,
287
+ percent: number
284
288
  }[],
285
289
  tradeType: TTradeType
286
290
  ): Trade<TInput, TOutput, TTradeType> {
287
291
  const populatedRoutes: {
292
+ percent: number,
288
293
  route: Route<TInput, TOutput>
289
294
  inputAmount: CurrencyAmount<TInput>
290
295
  outputAmount: CurrencyAmount<TOutput>
291
296
  }[] = []
292
297
 
293
- for (const { route, amount } of routes) {
298
+ for (const { route, amount, percent } of routes) {
294
299
  const amounts: CurrencyAmount<Token>[] = new Array(route.tokenPath.length)
295
300
  let inputAmount: CurrencyAmount<TInput>
296
301
  let outputAmount: CurrencyAmount<TOutput>
@@ -329,7 +334,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
329
334
  inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amounts[0].numerator, amounts[0].denominator)
330
335
  }
331
336
 
332
- populatedRoutes.push({ route, inputAmount, outputAmount })
337
+ populatedRoutes.push({ route, inputAmount, outputAmount, percent })
333
338
  }
334
339
 
335
340
  return new Trade({
@@ -352,6 +357,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
352
357
  TOutput extends Currency,
353
358
  TTradeType extends TradeType
354
359
  >(constructorArguments: {
360
+ percent: number,
355
361
  route: Route<TInput, TOutput>
356
362
  inputAmount: CurrencyAmount<TInput>
357
363
  outputAmount: CurrencyAmount<TOutput>
@@ -361,6 +367,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
361
367
  ...constructorArguments,
362
368
  routes: [
363
369
  {
370
+ percent: constructorArguments.percent,
364
371
  inputAmount: constructorArguments.inputAmount,
365
372
  outputAmount: constructorArguments.outputAmount,
366
373
  route: constructorArguments.route
@@ -384,6 +391,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
384
391
  TTradeType extends TradeType
385
392
  >(constructorArguments: {
386
393
  routes: {
394
+ percent: number
387
395
  route: Route<TInput, TOutput>
388
396
  inputAmount: CurrencyAmount<TInput>
389
397
  outputAmount: CurrencyAmount<TOutput>
@@ -403,6 +411,7 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
403
411
  tradeType
404
412
  }: {
405
413
  routes: {
414
+ percent: number,
406
415
  route: Route<TInput, TOutput>
407
416
  inputAmount: CurrencyAmount<TInput>
408
417
  outputAmount: CurrencyAmount<TOutput>
@@ -490,12 +499,20 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
490
499
 
491
500
  const bestTrades: Trade<TInput, TOutput, TradeType.EXACT_INPUT>[] = []
492
501
  for (const route of routes) {
493
- const trade = Trade.fromRoute(route, currencyAmountIn, TradeType.EXACT_INPUT)
502
+ let trade
503
+
504
+ try {
505
+ trade = Trade.fromRoute(route, currencyAmountIn, TradeType.EXACT_INPUT)
506
+ } catch (error) {
507
+ // not enough liquidity in this pair
508
+ if ((error as any).isInsufficientInputAmountError) {
509
+ continue
510
+ }
511
+ throw error
512
+ }
494
513
 
495
514
  // FIXME! Sorting bug multiple pools
496
- if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) {
497
- continue
498
- }
515
+ if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) continue
499
516
 
500
517
  sortedInsert(
501
518
  bestTrades,
@@ -517,12 +534,19 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
517
534
 
518
535
  const bestTrades: Trade<TInput, TOutput, TradeType.EXACT_OUTPUT>[] = []
519
536
  for (const route of routes) {
520
- const trade = Trade.fromRoute(route, currencyAmountOut, TradeType.EXACT_OUTPUT)
521
-
522
- if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) {
523
- continue
537
+ let trade
538
+ try {
539
+ trade = Trade.fromRoute(route, currencyAmountOut, TradeType.EXACT_OUTPUT)
540
+ } catch (error) {
541
+ // not enough liquidity in this pair
542
+ if ((error as any).isInsufficientReservesError) {
543
+ continue
544
+ }
545
+ throw error
524
546
  }
525
547
 
548
+ if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) continue
549
+
526
550
  sortedInsert(
527
551
  bestTrades,
528
552
  trade,
@@ -533,4 +557,50 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
533
557
 
534
558
  return bestTrades
535
559
  }
560
+
561
+ public static bestTradeWithSplit<TInput extends Currency, TOutput extends Currency>(
562
+ routes: Route<TInput, TOutput>[],
563
+ amount: CurrencyAmount<Currency>,
564
+ percents: number[],
565
+ tradeType: TradeType,
566
+ swapConfig = { minSplits: 1, maxSplits: 10 }
567
+ ): Trade<Currency, Currency, TradeType> | null {
568
+ invariant(routes.length > 0, 'ROUTES')
569
+ invariant(percents.length > 0, 'PERCENTS')
570
+
571
+ // Compute routes for all percents for all routes
572
+ const percentToTrades: { [percent: number]: Trade<Currency, Currency, TradeType>[] } = {};
573
+ for (const percent of percents) {
574
+ const splitAmount = amount.multiply(percent).divide(100)
575
+
576
+ for (const route of routes) {
577
+ let trade
578
+ try {
579
+ trade = Trade.fromRoute(route, splitAmount, tradeType, percent)
580
+ } catch (error) {
581
+ // not enough liquidity in this pair
582
+ if ((error as any).isInsufficientReservesError || (error as any).isInsufficientInputAmountError) {
583
+ continue
584
+ }
585
+ throw error
586
+ }
587
+
588
+ if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) continue
589
+
590
+ if (!percentToTrades[percent]) {
591
+ percentToTrades[percent] = []
592
+ }
593
+
594
+ percentToTrades[percent].push(trade)
595
+ }
596
+ }
597
+ const bestTrades = getBestSwapRoute(tradeType, percentToTrades, percents, swapConfig)
598
+ if (!bestTrades) return null
599
+
600
+ return new Trade({
601
+ routes: bestTrades.map(({ inputAmount, outputAmount, route, swaps }) =>
602
+ ({ inputAmount, outputAmount, route, percent: swaps[0].percent })),
603
+ tradeType }
604
+ )
605
+ }
536
606
  }
package/src/errors.ts ADDED
@@ -0,0 +1,30 @@
1
+ // see https://stackoverflow.com/a/41102306
2
+ const CAN_SET_PROTOTYPE = 'setPrototypeOf' in Object
3
+
4
+ /**
5
+ * Indicates that the pair has insufficient reserves for a desired output amount. I.e. the amount of output cannot be
6
+ * obtained by sending any amount of input.
7
+ */
8
+ export class InsufficientReservesError extends Error {
9
+ public readonly isInsufficientReservesError = true
10
+
11
+ public constructor() {
12
+ super()
13
+ this.name = this.constructor.name
14
+ if (CAN_SET_PROTOTYPE) Object.setPrototypeOf(this, new.target.prototype)
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Indicates that the input amount is too small to produce any amount of output. I.e. the amount of input sent is less
20
+ * than the price of a single unit of output after fees.
21
+ */
22
+ export class InsufficientInputAmountError extends Error {
23
+ public readonly isInsufficientInputAmountError = true
24
+
25
+ public constructor() {
26
+ super()
27
+ this.name = this.constructor.name
28
+ if (CAN_SET_PROTOTYPE) Object.setPrototypeOf(this, new.target.prototype)
29
+ }
30
+ }
@@ -1,4 +1,4 @@
1
- import { Percent } from "../entities"
1
+ import { Percent, Trade } from "../entities"
2
2
  import { TradeType } from "../internalConstants"
3
3
 
4
4
  export function parseTrade(trade) {
@@ -6,27 +6,46 @@ export function parseTrade(trade) {
6
6
  const slippage = new Percent(3, 100) // 0.3%
7
7
  const receiver = '<receiver>'
8
8
 
9
- const tradeType = trade.tradeType == TradeType.EXACT_INPUT ? 'swapexactin' : 'swapexactout'
9
+ const exactIn = trade.tradeType === TradeType.EXACT_INPUT
10
+ console.log('trade.tradeType', trade.tradeType, TradeType.EXACT_INPUT)
10
11
 
11
- const route = trade.route.pools.map(p => p.id)
12
- const maxSent = trade.inputAmount
13
- const minReceived = trade.minimumAmountOut(slippage)
14
- const memo = `${tradeType}#${route.join(',')}#${receiver}#${minReceived.toExtendedAsset()}#0`
12
+ const maxSent = exactIn ? trade.inputAmount : trade.maximumAmountIn(slippage)
13
+ const minReceived = exactIn ? trade.minimumAmountOut(slippage) : trade.outputAmount
14
+
15
+ const tradeType = exactIn ? 'swapexactin' : 'swapexactout'
16
+
17
+ const swaps = trade.swaps.map(({ route, percent, inputAmount, outputAmount }) => {
18
+ route = route.pools.map(p => p.id)
19
+
20
+ const maxSent = exactIn ? inputAmount : trade.maximumAmountIn(slippage, inputAmount)
21
+ const minReceived = exactIn ? trade.minimumAmountOut(slippage, outputAmount) : outputAmount
22
+
23
+ const input = inputAmount.toAsset()
24
+ const output = outputAmount.toAsset()
25
+
26
+ const memo = `${tradeType}#${route.join(',')}#${receiver}#${minReceived.toExtendedAsset()}#0`
27
+ return { input, route, output, percent, memo, maxSent: maxSent.toFixed(), minReceived: minReceived.toFixed() }
28
+ })
15
29
 
16
30
  const result = {
31
+ route: [],
32
+ memo: '',
33
+ swaps,
17
34
  input: trade.inputAmount.toFixed(),
18
35
  output: trade.outputAmount.toFixed(),
19
36
  minReceived: minReceived.toFixed(),
20
37
  maxSent: maxSent.toFixed(),
21
38
  priceImpact: trade.priceImpact.toSignificant(2),
22
- memo,
23
- route,
39
+
24
40
  executionPrice: {
25
41
  numerator: trade.executionPrice.numerator.toString(),
26
42
  denominator: trade.executionPrice.denominator.toString()
27
43
  }
28
44
  }
29
45
 
46
+ // FIXME DEPRECATED Hotfix for legacy v1
47
+ result!.route = trade.swaps[0].route.pools.map((p) => p.id)
48
+ result!.memo = `${tradeType}#${result.route.join(',')}#${receiver}#${minReceived.toExtendedAsset()}#0`
49
+
30
50
  return result
31
51
  }
32
-
@@ -0,0 +1,223 @@
1
+ import _ from 'lodash'
2
+ import Queue from 'mnemonist/queue'
3
+ import FixedReverseHeap from 'mnemonist/fixed-reverse-heap'
4
+
5
+ import { Currency } from '../entities/currency'
6
+ import { Trade } from '../entities/trade'
7
+ import { CurrencyAmount } from '../entities/fractions'
8
+ import { TradeType } from '../internalConstants'
9
+
10
+ export function getBestSwapRoute(
11
+ routeType: TradeType,
12
+ percentToQuotes: { [percent: number]: Trade<Currency, Currency, TradeType>[] },
13
+ percents: number[],
14
+ swapRouteConfig = { minSplits: 1, maxSplits: 8 }
15
+ ): Trade<Currency, Currency, TradeType>[] | null {
16
+ const percentToSortedQuotes = _.mapValues(
17
+ percentToQuotes,
18
+ (routeQuotes: Trade<Currency, Currency, TradeType>[]) => {
19
+ return routeQuotes.sort((routeQuoteA, routeQuoteB) => {
20
+ if (routeType == TradeType.EXACT_INPUT) {
21
+ return routeQuoteA.outputAmount.greaterThan(routeQuoteB.outputAmount) ? -1 : 1;
22
+ } else {
23
+ return routeQuoteA.inputAmount.lessThan(routeQuoteB.inputAmount) ? -1 : 1;
24
+ }
25
+ });
26
+ }
27
+ );
28
+
29
+ const quoteCompFn =
30
+ routeType == TradeType.EXACT_INPUT
31
+ ? (a: CurrencyAmount<Currency>, b: CurrencyAmount<Currency>) => a.greaterThan(b)
32
+ : (a: CurrencyAmount<Currency>, b: CurrencyAmount<Currency>) => a.lessThan(b);
33
+
34
+ const sumFn = (currencyAmounts: CurrencyAmount<Currency>[]): CurrencyAmount<Currency> => {
35
+ let sum = currencyAmounts[0]!;
36
+ for (let i = 1; i < currencyAmounts.length; i++) {
37
+ sum = sum.add(currencyAmounts[i]!);
38
+ }
39
+ return sum;
40
+ };
41
+
42
+ let bestQuote: CurrencyAmount<Currency> | undefined;
43
+ let bestSwap: Trade<Currency, Currency, TradeType>[] | undefined;
44
+
45
+ const bestSwapsPerSplit = new FixedReverseHeap<{
46
+ quote: CurrencyAmount<Currency>;
47
+ routes: Trade<Currency, Currency, TradeType>[];
48
+ }>(
49
+ Array,
50
+ (a, b) => {
51
+ return quoteCompFn(a.quote, b.quote) ? -1 : 1;
52
+ },
53
+ 3
54
+ );
55
+
56
+ const { minSplits, maxSplits } = swapRouteConfig;
57
+
58
+ if (!percentToSortedQuotes[100] || minSplits > 1) {
59
+ console.log(
60
+ {
61
+ percentToSortedQuotes: _.mapValues(
62
+ percentToSortedQuotes,
63
+ (p) => p.length
64
+ ),
65
+ },
66
+ 'Did not find a valid route without any splits. Continuing search anyway.'
67
+ );
68
+ } else {
69
+ bestQuote = percentToSortedQuotes[100][0]!.outputAmount;
70
+ bestSwap = [percentToSortedQuotes[100][0]!];
71
+
72
+ for (const routeWithQuote of percentToSortedQuotes[100].slice(0, 5)) {
73
+ bestSwapsPerSplit.push({
74
+ quote: routeWithQuote.outputAmount,
75
+ routes: [routeWithQuote],
76
+ });
77
+ }
78
+ }
79
+
80
+ const queue = new Queue<{
81
+ percentIndex: number;
82
+ curRoutes: Trade<Currency, Currency, TradeType>[];
83
+ remainingPercent: number;
84
+ special: boolean;
85
+ }>();
86
+
87
+ if (percents.length === 0) return null; // Handle empty percents array
88
+
89
+ for (let i = percents.length - 1; i >= 0; i--) {
90
+ const percent = percents[i];
91
+
92
+ if (!percentToSortedQuotes[percent]) {
93
+ console.log('continue', { percent });
94
+ continue;
95
+ }
96
+
97
+ queue.enqueue({
98
+ curRoutes: [percentToSortedQuotes[percent]![0]!],
99
+ percentIndex: i,
100
+ remainingPercent: 100 - percent,
101
+ special: false,
102
+ });
103
+
104
+ if (
105
+ !percentToSortedQuotes[percent] ||
106
+ !percentToSortedQuotes[percent]![1]
107
+ ) {
108
+ console.log('continue2', { percent });
109
+ continue;
110
+ }
111
+
112
+ queue.enqueue({
113
+ curRoutes: [percentToSortedQuotes[percent]![1]!],
114
+ percentIndex: i,
115
+ remainingPercent: 100 - percent,
116
+ special: true,
117
+ });
118
+ }
119
+
120
+ let splits = 1;
121
+
122
+ while (queue.size > 0) {
123
+ bestSwapsPerSplit.clear();
124
+
125
+ let layer = queue.size;
126
+ splits++;
127
+
128
+ if (splits >= 3 && bestSwap && bestSwap.length < splits - 1) {
129
+ break;
130
+ }
131
+
132
+ if (splits > maxSplits) {
133
+ break;
134
+ }
135
+
136
+ while (layer > 0) {
137
+ layer--;
138
+
139
+ const { remainingPercent, curRoutes, percentIndex, special } =
140
+ queue.dequeue()!;
141
+
142
+ for (let i = percentIndex; i >= 0; i--) {
143
+ const percentA = percents[i]!;
144
+
145
+ if (percentA > remainingPercent) {
146
+ continue;
147
+ }
148
+
149
+ if (!percentToSortedQuotes[percentA]) {
150
+ continue;
151
+ }
152
+
153
+ const candidateRoutesA = percentToSortedQuotes[percentA]!;
154
+
155
+ const routeWithQuoteA = findFirstRouteNotUsingUsedPools(
156
+ curRoutes,
157
+ candidateRoutesA,
158
+ );
159
+
160
+ if (!routeWithQuoteA) {
161
+ continue;
162
+ }
163
+
164
+ const remainingPercentNew = remainingPercent - percentA;
165
+ const curRoutesNew = [...curRoutes, routeWithQuoteA];
166
+
167
+ if (remainingPercentNew == 0 && splits >= minSplits) {
168
+ const quotesNew = _.map(curRoutesNew, (r) => r.outputAmount);
169
+ const quoteNew = sumFn(quotesNew);
170
+
171
+ bestSwapsPerSplit.push({
172
+ quote: quoteNew,
173
+ routes: curRoutesNew,
174
+ });
175
+
176
+ if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) {
177
+ bestQuote = quoteNew;
178
+ bestSwap = curRoutesNew;
179
+ }
180
+ } else {
181
+ queue.enqueue({
182
+ curRoutes: curRoutesNew,
183
+ remainingPercent: remainingPercentNew,
184
+ percentIndex: i,
185
+ special,
186
+ });
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ if (!bestSwap) {
193
+ console.log(`Could not find a valid swap`);
194
+ return null;
195
+ }
196
+
197
+ const quote = sumFn(
198
+ _.map(bestSwap, (routeWithValidQuote) => routeWithValidQuote.outputAmount)
199
+ );
200
+
201
+ const routeWithQuotes = bestSwap.sort((routeAmountA, routeAmountB) =>
202
+ routeAmountB.outputAmount.greaterThan(routeAmountA.outputAmount) ? 1 : -1
203
+ );
204
+
205
+ return routeWithQuotes;
206
+ }
207
+
208
+
209
+ const findFirstRouteNotUsingUsedPools = (
210
+ usedRoutes: Trade<Currency, Currency, TradeType>[],
211
+ candidateRoutes: Trade<Currency, Currency, TradeType>[],
212
+ ): Trade<Currency, Currency, TradeType> | null => {
213
+ const usedPools = new Set<number>();
214
+ usedRoutes.forEach(r => r.route.pools.forEach(pool => usedPools.add(pool.id)));
215
+
216
+ for (const candidate of candidateRoutes) {
217
+ if (candidate.route.pools.every(pool => !usedPools.has(pool.id))) {
218
+ return candidate;
219
+ }
220
+ }
221
+
222
+ return null;
223
+ }