Compare commits

..

16 Commits

Author SHA1 Message Date
Github Actions
2fdca24d4e Publish
- @0x/asset-swapper@16.46.0
2022-01-11 01:10:02 +00:00
Github Actions
42ec0b144e Updated CHANGELOGS & MD docs 2022-01-11 01:09:58 +00:00
Jacob Evans
3f6ce78b46 chore: Enable Curve ETH/CVX (#394)
* chore: Enable Curve ETH/CVX

* pr number
2022-01-11 09:32:07 +10:00
Github Actions
c1300c1068 Publish
- @0x/asset-swapper@16.45.2
2022-01-10 15:09:26 +00:00
Github Actions
9a641cfab6 Updated CHANGELOGS & MD docs 2022-01-10 15:09:23 +00:00
Kim Persson
60345d4465 fix: don't create fills for 0 output samples and negative adjusted rate orders (#387)
* fix: don't try to create fills for 0 output samples

* fix: negative adjusted output native orders causing undefined fills

* fix: make sure to use the same sourcePathId for fills from same source

* fix: should be same sourcePathId within the same DexSample[]

* fix: split native orders into 13 samples to align with interpolation

* chore: add changelog entry for asset-swapper
2022-01-10 14:55:03 +01:00
Github Actions
11dfea47a6 Publish
- @0x/asset-swapper@16.45.1
2022-01-05 05:08:43 +00:00
Github Actions
55e9dd39a2 Updated CHANGELOGS & MD docs 2022-01-05 05:08:41 +00:00
Jacob Evans
1993929bed chore: Celo Update certain tokens since Optics v2 (#390)
* chore: Celo Update certain tokens since Optics v2

* Changelog
2022-01-05 14:44:29 +10:00
Oskar Paolini
e1d81de517 fixes axios object dumping in logs (#345) 2022-01-05 09:46:01 +10:00
Github Actions
a6b3a21635 Publish
- @0x/asset-swapper@16.45.0
2022-01-04 15:00:16 +00:00
Github Actions
fd59cdc2db Updated CHANGELOGS & MD docs 2022-01-04 15:00:13 +00:00
Jacob Evans
98e11b5189 feat: Capture Routing timing metrics (#388) 2022-01-04 15:42:14 +01:00
Github Actions
3bebc7cd62 Publish
- @0x/asset-swapper@16.44.0
2021-12-29 11:45:33 +00:00
Github Actions
56dab6ae8c Updated CHANGELOGS & MD docs 2021-12-29 11:45:30 +00:00
Kim Persson
285f98e9e9 feat: Udate neon-router and use router estimated output amount (#354)
* feat: use Rust router estimated output amount when possible

* fix: use strings for sample ids, and increase samples in the rust router

* fix: remove unnecessary interpolation of out of range values

* fix: don't recalculate sampled dist sum in a loop

* fix: use 14 samples for rust router to work around interpolation issues

* fix: unintentional logic change

* fix: remove local dev plotting param from route fn call

* feat: make neon-router number of samples configurable

* chore: bump to newly published neon-router version

* fix: handle insufficient liquidity at all requested sources

* chore: update asset-swapper changelog
2021-12-29 12:08:24 +01:00
10 changed files with 280 additions and 113 deletions

View File

@@ -1,4 +1,54 @@
[ [
{
"version": "16.46.0",
"changes": [
{
"note": "Enable `Curve` ETH/CVX pool",
"pr": 394
}
],
"timestamp": 1641863395
},
{
"version": "16.45.2",
"changes": [
{
"note": "Handle 0 output samples and negative adjusted rate native orders in routing",
"pr": 387
}
],
"timestamp": 1641827361
},
{
"version": "16.45.1",
"changes": [
{
"note": "Update `Celo` intermediate tokens",
"pr": 390
}
],
"timestamp": 1641359319
},
{
"version": "16.45.0",
"changes": [
{
"note": "Capture router timings",
"pr": 388
}
],
"timestamp": 1641308410
},
{
"version": "16.44.0",
"changes": [
{
"note": "Update neon-router and use router estimated output amount",
"pr": 354
}
],
"timestamp": 1640778328
},
{ {
"version": "16.43.0", "version": "16.43.0",
"changes": [ "changes": [

View File

@@ -5,6 +5,26 @@ Edit the package's CHANGELOG.json file only.
CHANGELOG CHANGELOG
## v16.46.0 - _January 11, 2022_
* Enable `Curve` ETH/CVX pool (#394)
## v16.45.2 - _January 10, 2022_
* Handle 0 output samples and negative adjusted rate native orders in routing (#387)
## v16.45.1 - _January 5, 2022_
* Update `Celo` intermediate tokens (#390)
## v16.45.0 - _January 4, 2022_
* Capture router timings (#388)
## v16.44.0 - _December 29, 2021_
* Update neon-router and use router estimated output amount (#354)
## v16.43.0 - _December 24, 2021_ ## v16.43.0 - _December 24, 2021_
* `UniswapV3` support for `Optimism` (#385) * `UniswapV3` support for `Optimism` (#385)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@0x/asset-swapper", "name": "@0x/asset-swapper",
"version": "16.43.0", "version": "16.46.0",
"engines": { "engines": {
"node": ">=6.12" "node": ">=6.12"
}, },
@@ -66,7 +66,7 @@
"@0x/contracts-zero-ex": "^0.30.1", "@0x/contracts-zero-ex": "^0.30.1",
"@0x/dev-utils": "^4.2.9", "@0x/dev-utils": "^4.2.9",
"@0x/json-schemas": "^6.3.0", "@0x/json-schemas": "^6.3.0",
"@0x/neon-router": "^0.2.1", "@0x/neon-router": "^0.3.1",
"@0x/protocol-utils": "^1.10.1", "@0x/protocol-utils": "^1.10.1",
"@0x/quote-server": "^6.0.6", "@0x/quote-server": "^6.0.6",
"@0x/types": "^3.3.4", "@0x/types": "^3.3.4",

View File

@@ -223,7 +223,17 @@ export async function returnQuoteFromAltMMAsync<ResponseT>(
cancelToken, cancelToken,
}) })
.catch(err => { .catch(err => {
warningLogger(err, `Alt RFQ MM request failed`); if (err.response) {
// request was made and market maker responded
warningLogger(
{ data: err.response.data, status: err.response.status, headers: err.response.headers },
`Alt RFQ MM request failed`,
);
} else if (err.request) {
warningLogger({}, 'Alt RFQ MM no response received');
} else {
warningLogger({ err: err.message }, 'Failed to construct Alt RFQ MM request');
}
throw new Error(`Alt RFQ MM request failed`); throw new Error(`Alt RFQ MM request failed`);
}); });

View File

@@ -462,6 +462,7 @@ export const MAINNET_TOKENS = {
CRV: '0xd533a949740bb3306d119cc777fa900ba034cd52', CRV: '0xd533a949740bb3306d119cc777fa900ba034cd52',
MIM: '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3', MIM: '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3',
EURT: '0xc581b735a1688071a1746c968e0798d642ede491', EURT: '0xc581b735a1688071a1746c968e0798d642ede491',
CVX: '0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b',
}; };
export const BSC_TOKENS = { export const BSC_TOKENS = {
@@ -510,9 +511,23 @@ export const AVALANCHE_TOKENS = {
}; };
export const CELO_TOKENS = { export const CELO_TOKENS = {
WETH: '0xe919f65739c26a42616b7b8eedc6b5524d1e3ac4',
WCELO: '0x471ece3750da237f93b8e339c536989b8978a438', WCELO: '0x471ece3750da237f93b8e339c536989b8978a438',
// Some of these tokens are Optics bridge? tokens which
// had an issue and migrated from v1 to v2
WETHv1: '0xe919f65739c26a42616b7b8eedc6b5524d1e3ac4',
WETH: '0x122013fd7df1c6f636a5bb8f03108e876548b455',
WBTC: '0xbaab46e28388d2779e6e31fd00cf0e5ad95e327b',
cUSD: '0x765de816845861e75a25fca122bb6898b8b1282a',
// ??
WBTCv1: '0xd629eb00deced2a080b7ec630ef6ac117e614f1b',
cETH: '0x2def4285787d58a2f811af24755a8150622f4361',
UBE: '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec',
// Moolah
mCELO: '0x7d00cd74ff385c955ea3d79e47bf06bd7386387d',
mCUSD: '0x918146359264c492bd6934071c6bd31c854edbc3', mCUSD: '0x918146359264c492bd6934071c6bd31c854edbc3',
mCEUR: '0xe273ad7ee11dcfaa87383ad5977ee1504ac07568',
amCUSD: '0x64defa3544c695db8c535d289d843a189aa26b98',
MOO: '0x17700282592d6917f6a73d0bf8accf4d578c131e',
}; };
export const FANTOM_TOKENS = { export const FANTOM_TOKENS = {
@@ -578,6 +593,7 @@ export const CURVE_POOLS = {
mim: '0x5a6a4d54456819380173272a5e8e9b9904bdf41b', mim: '0x5a6a4d54456819380173272a5e8e9b9904bdf41b',
eurt: '0xfd5db7463a3ab53fd211b4af195c5bccc1a03890', eurt: '0xfd5db7463a3ab53fd211b4af195c5bccc1a03890',
ethcrv: '0x8301ae4fc9c624d1d396cbdaa1ed877821d7c511', ethcrv: '0x8301ae4fc9c624d1d396cbdaa1ed877821d7c511',
ethcvx: '0xb576491f1e6e5e62f1d8f26062ee822b40b0e0d4',
}; };
export const CURVE_V2_POOLS = { export const CURVE_V2_POOLS = {
@@ -713,7 +729,7 @@ export const DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID = valueByChainId<string[]>(
AVALANCHE_TOKENS.USDC, AVALANCHE_TOKENS.USDC,
], ],
[ChainId.Fantom]: [FANTOM_TOKENS.WFTM, FANTOM_TOKENS.WETH, FANTOM_TOKENS.DAI, FANTOM_TOKENS.USDC], [ChainId.Fantom]: [FANTOM_TOKENS.WFTM, FANTOM_TOKENS.WETH, FANTOM_TOKENS.DAI, FANTOM_TOKENS.USDC],
[ChainId.Celo]: [CELO_TOKENS.mCUSD, CELO_TOKENS.WETH, CELO_TOKENS.WCELO], [ChainId.Celo]: [CELO_TOKENS.WCELO, CELO_TOKENS.mCUSD, CELO_TOKENS.WETH, CELO_TOKENS.amCUSD, CELO_TOKENS.WBTC],
[ChainId.Optimism]: [OPTIMISM_TOKENS.WETH, OPTIMISM_TOKENS.DAI, OPTIMISM_TOKENS.USDC], [ChainId.Optimism]: [OPTIMISM_TOKENS.WETH, OPTIMISM_TOKENS.DAI, OPTIMISM_TOKENS.USDC],
}, },
[], [],
@@ -1059,6 +1075,17 @@ export const CURVE_MAINNET_INFOS: { [name: string]: CurveInfo } = {
sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_uint256, sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_uint256,
exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying_uint256, exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying_uint256,
}, },
[CURVE_POOLS.ethcvx]: {
...createCurveExchangePool({
// This pool uses ETH
tokens: [MAINNET_TOKENS.WETH, MAINNET_TOKENS.CVX],
pool: CURVE_POOLS.ethcvx,
gasSchedule: 350e3,
}),
// This pool has a custom get_dy and exchange selector with uint256
sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_uint256,
exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying_uint256,
},
}; };
export const CURVE_V2_MAINNET_INFOS: { [name: string]: CurveInfo } = { export const CURVE_V2_MAINNET_INFOS: { [name: string]: CurveInfo } = {
@@ -2103,4 +2130,5 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice
shouldGenerateQuoteReport: true, shouldGenerateQuoteReport: true,
shouldIncludePriceComparisonsReport: false, shouldIncludePriceComparisonsReport: false,
tokenAdjacencyGraph: { default: [] }, tokenAdjacencyGraph: { default: [] },
neonRouterNumSamples: 14,
}; };

View File

@@ -443,6 +443,7 @@ export class MarketOperationUtils {
feeSchedule: _opts.feeSchedule, feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
gasPrice: _opts.gasPrice, gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples,
}, },
); );
return optimizerResult; return optimizerResult;
@@ -531,9 +532,18 @@ export class MarketOperationUtils {
penaltyOpts, penaltyOpts,
opts.feeSchedule, opts.feeSchedule,
this._sampler.chainId, this._sampler.chainId,
opts.neonRouterNumSamples,
opts.samplerMetrics,
); );
} else { } else {
optimalPath = await findOptimalPathJSAsync(side, fills, inputAmount, opts.runLimit, penaltyOpts); optimalPath = await findOptimalPathJSAsync(
side,
fills,
inputAmount,
opts.runLimit,
opts.samplerMetrics,
penaltyOpts,
);
} }
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
@@ -596,6 +606,8 @@ export class MarketOperationUtils {
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
exchangeProxyOverhead: _opts.exchangeProxyOverhead, exchangeProxyOverhead: _opts.exchangeProxyOverhead,
gasPrice: _opts.gasPrice, gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples,
samplerMetrics: _opts.samplerMetrics,
}; };
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
@@ -806,6 +818,8 @@ export class MarketOperationUtils {
sturdyPenaltyOpts, sturdyPenaltyOpts,
opts.feeSchedule, opts.feeSchedule,
this._sampler.chainId, this._sampler.chainId,
opts.neonRouterNumSamples,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
); );
} else { } else {
const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source)); const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source));
@@ -814,6 +828,7 @@ export class MarketOperationUtils {
sturdyFills, sturdyFills,
inputAmount, inputAmount,
opts.runLimit, opts.runLimit,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
sturdyPenaltyOpts, sturdyPenaltyOpts,
); );
} }

View File

@@ -1,23 +1,21 @@
import { assert } from '@0x/assert'; import { assert } from '@0x/assert';
import { ChainId } from '@0x/contract-addresses'; import { ChainId } from '@0x/contract-addresses';
import { OptimizerCapture, route, SerializedPath } from '@0x/neon-router'; import { OptimizerCapture, route, SerializedPath } from '@0x/neon-router';
import { BigNumber } from '@0x/utils'; import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { DEFAULT_INFO_LOGGER } from '../../constants';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types';
import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID } from '../market_operation_utils/constants'; import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID } from '../market_operation_utils/constants';
import { dexSamplesToFills, ethToOutputAmount, nativeOrdersToFills } from './fills'; import { dexSamplesToFills, ethToOutputAmount, nativeOrdersToFills } from './fills';
import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path'; import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path';
import { getRate } from './rate_utils'; import { getRate } from './rate_utils';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData } from './types'; import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData, SamplerMetrics } from './types';
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise
const RUN_LIMIT_DECAY_FACTOR = 0.5; const RUN_LIMIT_DECAY_FACTOR = 0.5;
const RUST_ROUTER_NUM_SAMPLES = 200;
const FILL_QUOTE_TRANSFORMER_GAS_OVERHEAD = new BigNumber(150e3); const FILL_QUOTE_TRANSFORMER_GAS_OVERHEAD = new BigNumber(150e3);
// NOTE: The Rust router will panic with less than 3 samples // NOTE: The Rust router will panic with less than 3 samples
const MIN_NUM_SAMPLE_INPUTS = 3; const MIN_NUM_SAMPLE_INPUTS = 3;
@@ -69,21 +67,6 @@ function calculateOuputFee(
} }
} }
// Use linear interpolation to approximate the output
// at a certain input somewhere between the two samples
// See https://en.wikipedia.org/wiki/Linear_interpolation
const interpolateOutputFromSamples = (
left: { input: BigNumber; output: BigNumber },
right: { input: BigNumber; output: BigNumber },
targetInput: BigNumber,
): BigNumber =>
left.output.plus(
right.output
.minus(left.output)
.dividedBy(right.input.minus(left.input))
.times(targetInput.minus(left.input)),
);
function findRoutesAndCreateOptimalPath( function findRoutesAndCreateOptimalPath(
side: MarketOperation, side: MarketOperation,
samples: DexSample[][], samples: DexSample[][],
@@ -91,29 +74,27 @@ function findRoutesAndCreateOptimalPath(
input: BigNumber, input: BigNumber,
opts: PathPenaltyOpts, opts: PathPenaltyOpts,
fees: FeeSchedule, fees: FeeSchedule,
neonRouterNumSamples: number,
): Path | undefined { ): Path | undefined {
const createFill = (sample: DexSample) => const createFill = (sample: DexSample): Fill | undefined => {
dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees)[0]; const fills = dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees);
// Track sample id's to integers (required by rust router) // NOTE: If the sample has 0 output dexSamplesToFills will return [] because no fill can be created
const sampleIdLookup: { [key: string]: number } = {}; if (fills.length === 0) {
let sampleIdCounter = 0; return undefined;
const sampleToId = (source: ERC20BridgeSource, index: number): number => {
const key = `${source}-${index}`;
if (sampleIdLookup[key]) {
return sampleIdLookup[key];
} else {
sampleIdLookup[key] = ++sampleIdCounter;
return sampleIdLookup[key];
} }
return fills[0];
}; };
const samplesAndNativeOrdersWithResults: Array<DexSample[] | NativeOrderWithFillableAmounts[]> = []; const samplesAndNativeOrdersWithResults: Array<DexSample[] | NativeOrderWithFillableAmounts[]> = [];
const serializedPaths: SerializedPath[] = []; const serializedPaths: SerializedPath[] = [];
const sampleSourcePathIds: string[] = [];
for (const singleSourceSamples of samples) { for (const singleSourceSamples of samples) {
if (singleSourceSamples.length === 0) { if (singleSourceSamples.length === 0) {
continue; continue;
} }
const sourcePathId = hexUtils.random();
const singleSourceSamplesWithOutput = [...singleSourceSamples]; const singleSourceSamplesWithOutput = [...singleSourceSamples];
for (let i = singleSourceSamples.length - 1; i >= 0; i--) { for (let i = singleSourceSamples.length - 1; i >= 0; i--) {
if (singleSourceSamples[i].output.isZero()) { if (singleSourceSamples[i].output.isZero()) {
@@ -131,7 +112,7 @@ function findRoutesAndCreateOptimalPath(
// TODO(kimpers): Do we need to handle 0 entries, from eg Kyber? // TODO(kimpers): Do we need to handle 0 entries, from eg Kyber?
const serializedPath = singleSourceSamplesWithOutput.reduce<SerializedPath>( const serializedPath = singleSourceSamplesWithOutput.reduce<SerializedPath>(
(memo, sample, sampleIdx) => { (memo, sample, sampleIdx) => {
memo.ids.push(sampleToId(sample.source, sampleIdx)); memo.ids.push(`${sample.source}-${serializedPaths.length}-${sampleIdx}`);
memo.inputs.push(sample.input.integerValue().toNumber()); memo.inputs.push(sample.input.integerValue().toNumber());
memo.outputs.push(sample.output.integerValue().toNumber()); memo.outputs.push(sample.output.integerValue().toNumber());
memo.outputFees.push( memo.outputFees.push(
@@ -152,8 +133,10 @@ function findRoutesAndCreateOptimalPath(
samplesAndNativeOrdersWithResults.push(singleSourceSamplesWithOutput); samplesAndNativeOrdersWithResults.push(singleSourceSamplesWithOutput);
serializedPaths.push(serializedPath); serializedPaths.push(serializedPath);
sampleSourcePathIds.push(sourcePathId);
} }
const nativeOrdersourcePathId = hexUtils.random();
for (const [idx, nativeOrder] of nativeOrders.entries()) { for (const [idx, nativeOrder] of nativeOrders.entries()) {
const { input: normalizedOrderInput, output: normalizedOrderOutput } = nativeOrderToNormalizedAmounts( const { input: normalizedOrderInput, output: normalizedOrderOutput } = nativeOrderToNormalizedAmounts(
side, side,
@@ -164,32 +147,25 @@ function findRoutesAndCreateOptimalPath(
if (normalizedOrderInput.isLessThanOrEqualTo(0) || normalizedOrderOutput.isLessThanOrEqualTo(0)) { if (normalizedOrderInput.isLessThanOrEqualTo(0) || normalizedOrderOutput.isLessThanOrEqualTo(0)) {
continue; continue;
} }
// HACK: the router requires at minimum 3 samples as a basis for interpolation
const inputs = [
0,
normalizedOrderInput
.dividedBy(2)
.integerValue()
.toNumber(),
normalizedOrderInput.integerValue().toNumber(),
];
const outputs = [
0,
normalizedOrderOutput
.dividedBy(2)
.integerValue()
.toNumber(),
normalizedOrderOutput.integerValue().toNumber(),
];
// NOTE: same fee no matter if full or partial fill
const fee = calculateOuputFee(side, nativeOrder, opts.outputAmountPerEth, opts.inputAmountPerEth, fees) const fee = calculateOuputFee(side, nativeOrder, opts.outputAmountPerEth, opts.inputAmountPerEth, fees)
.integerValue() .integerValue()
.toNumber(); .toNumber();
const outputFees = [fee, fee, fee];
// NOTE: ids can be the same for all fake samples // HACK: due to an issue with the Rust router interpolation we need to create exactly 13 samples from the native order
const id = sampleToId(ERC20BridgeSource.Native, idx); const ids = [];
const ids = [id, id, id]; const inputs = [];
const outputs = [];
const outputFees = [];
for (let i = 1; i <= 13; i++) {
const fraction = i / 13;
const currentInput = BigNumber.min(normalizedOrderInput.times(fraction), normalizedOrderInput);
const currentOutput = BigNumber.min(normalizedOrderOutput.times(fraction), normalizedOrderOutput);
const id = `${ERC20BridgeSource.Native}-${serializedPaths.length}-${idx}-${i}`;
inputs.push(currentInput.integerValue().toNumber());
outputs.push(currentOutput.integerValue().toNumber());
outputFees.push(fee);
ids.push(id);
}
const serializedPath: SerializedPath = { const serializedPath: SerializedPath = {
ids, ids,
@@ -200,6 +176,7 @@ function findRoutesAndCreateOptimalPath(
samplesAndNativeOrdersWithResults.push([nativeOrder]); samplesAndNativeOrdersWithResults.push([nativeOrder]);
serializedPaths.push(serializedPath); serializedPaths.push(serializedPath);
sampleSourcePathIds.push(nativeOrdersourcePathId);
} }
if (serializedPaths.length === 0) { if (serializedPaths.length === 0) {
@@ -212,30 +189,33 @@ function findRoutesAndCreateOptimalPath(
pathsIn: serializedPaths, pathsIn: serializedPaths,
}; };
const before = performance.now();
const allSourcesRustRoute = new Float64Array(rustArgs.pathsIn.length); const allSourcesRustRoute = new Float64Array(rustArgs.pathsIn.length);
route(rustArgs, allSourcesRustRoute, RUST_ROUTER_NUM_SAMPLES); const strategySourcesOutputAmounts = new Float64Array(rustArgs.pathsIn.length);
DEFAULT_INFO_LOGGER( route(rustArgs, allSourcesRustRoute, strategySourcesOutputAmounts, neonRouterNumSamples);
{ router: 'neon-router', performanceMs: performance.now() - before, type: 'real' },
'Rust router real routing performance',
);
assert.assert( assert.assert(
rustArgs.pathsIn.length === allSourcesRustRoute.length, rustArgs.pathsIn.length === allSourcesRustRoute.length,
'different number of sources in the Router output than the input', 'different number of sources in the Router output than the input',
); );
assert.assert(
rustArgs.pathsIn.length === strategySourcesOutputAmounts.length,
'different number of sources in the Router output amounts results than the input',
);
const routesAndSamples = _.zip(allSourcesRustRoute, samplesAndNativeOrdersWithResults); const routesAndSamplesAndOutputs = _.zip(
allSourcesRustRoute,
samplesAndNativeOrdersWithResults,
strategySourcesOutputAmounts,
sampleSourcePathIds,
);
const adjustedFills: Fill[] = []; const adjustedFills: Fill[] = [];
const totalRoutedAmount = BigNumber.sum(...allSourcesRustRoute); const totalRoutedAmount = BigNumber.sum(...allSourcesRustRoute);
const scale = input.dividedBy(totalRoutedAmount); const scale = input.dividedBy(totalRoutedAmount);
for (const [routeInput, routeSamplesAndNativeOrders] of routesAndSamples) { for (const [routeInput, routeSamplesAndNativeOrders, outputAmount, sourcePathId] of routesAndSamplesAndOutputs) {
if (!routeInput || !routeSamplesAndNativeOrders) { if (!routeInput || !routeSamplesAndNativeOrders || !outputAmount || !Number.isFinite(outputAmount)) {
continue; continue;
} }
// TODO(kimpers): [TKR-241] amounts are sometimes clipped in the router due to precisions loss for number/f64 // TODO(kimpers): [TKR-241] amounts are sometimes clipped in the router due to precision loss for number/f64
// we can work around it by scaling it and rounding up. However now we end up with a total amount of a couple base units too much // we can work around it by scaling it and rounding up. However now we end up with a total amount of a couple base units too much
const rustInputAdjusted = BigNumber.min( const rustInputAdjusted = BigNumber.min(
new BigNumber(routeInput).multipliedBy(scale).integerValue(BigNumber.ROUND_CEIL), new BigNumber(routeInput).multipliedBy(scale).integerValue(BigNumber.ROUND_CEIL),
@@ -251,14 +231,21 @@ function findRoutesAndCreateOptimalPath(
opts.outputAmountPerEth, opts.outputAmountPerEth,
opts.inputAmountPerEth, opts.inputAmountPerEth,
fees, fees,
)[0]; )[0] as Fill | undefined;
// NOTE: For Limit/RFQ orders we are done here. No need to scale output // Note: If the order has an adjusted rate of less than or equal to 0 it will be skipped
adjustedFills.push(nativeFill); // and nativeFill will be `undefined`
if (nativeFill) {
// NOTE: For Limit/RFQ orders we are done here. No need to scale output
adjustedFills.push({ ...nativeFill, sourcePathId: sourcePathId ?? hexUtils.random() });
}
continue; continue;
} }
// NOTE: For DexSamples only // NOTE: For DexSamples only
let fill = createFill(current); let fill = createFill(current);
if (!fill) {
continue;
}
const routeSamples = routeSamplesAndNativeOrders as Array<DexSample<FillData>>; const routeSamples = routeSamplesAndNativeOrders as Array<DexSample<FillData>>;
// Descend to approach a closer fill for fillData which may not be consistent // Descend to approach a closer fill for fillData which may not be consistent
// throughout the path (UniswapV3) and for a closer guesstimate at // throughout the path (UniswapV3) and for a closer guesstimate at
@@ -267,49 +254,47 @@ function findRoutesAndCreateOptimalPath(
assert.assert(routeSamples.length >= 1, 'Found no sample to use for source'); assert.assert(routeSamples.length >= 1, 'Found no sample to use for source');
for (let k = routeSamples.length - 1; k >= 0; k--) { for (let k = routeSamples.length - 1; k >= 0; k--) {
if (k === 0) { if (k === 0) {
fill = createFill(routeSamples[0]); fill = createFill(routeSamples[0]) ?? fill;
} }
if (rustInputAdjusted.isGreaterThan(routeSamples[k].input)) { if (rustInputAdjusted.isGreaterThan(routeSamples[k].input)) {
// Between here and the previous fill
// HACK: Use the midpoint between the two
const left = routeSamples[k]; const left = routeSamples[k];
const right = routeSamples[k + 1]; const right = routeSamples[k + 1];
if (left && right) { if (left && right) {
// Approximate how much output we get for the input with the surrounding samples fill =
const interpolatedOutput = interpolateOutputFromSamples( createFill({
left, ...right, // default to the greater (for gas used)
right, input: rustInputAdjusted,
rustInputAdjusted, output: new BigNumber(outputAmount),
).decimalPlaces(0, side === MarketOperation.Sell ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL); }) ?? fill;
fill = createFill({
...right, // default to the greater (for gas used)
input: rustInputAdjusted,
output: interpolatedOutput,
});
} else { } else {
assert.assert(Boolean(left || right), 'No valid sample to use'); assert.assert(Boolean(left || right), 'No valid sample to use');
fill = createFill(left || right); fill = createFill(left || right) ?? fill;
} }
break; break;
} }
} }
const scaleOutput = (output: BigNumber) => // TODO(kimpers): remove once we have solved the rounding/precision loss issues in the Rust router
const scaleOutput = (fillInput: BigNumber, output: BigNumber) =>
output output
.dividedBy(fill.input) .dividedBy(fillInput)
.times(rustInputAdjusted) .times(rustInputAdjusted)
.decimalPlaces(0, side === MarketOperation.Sell ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL); .decimalPlaces(0, side === MarketOperation.Sell ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
adjustedFills.push({ adjustedFills.push({
...fill, ...fill,
input: rustInputAdjusted, input: rustInputAdjusted,
output: scaleOutput(fill.output), output: scaleOutput(fill.input, fill.output),
adjustedOutput: scaleOutput(fill.adjustedOutput), adjustedOutput: scaleOutput(fill.input, fill.adjustedOutput),
index: 0, index: 0,
parent: undefined, parent: undefined,
sourcePathId: sourcePathId ?? hexUtils.random(),
}); });
} }
if (adjustedFills.length === 0) {
return undefined;
}
const pathFromRustInputs = Path.create(side, adjustedFills, input); const pathFromRustInputs = Path.create(side, adjustedFills, input);
return pathFromRustInputs; return pathFromRustInputs;
@@ -323,15 +308,27 @@ export function findOptimalRustPathFromSamples(
opts: PathPenaltyOpts, opts: PathPenaltyOpts,
fees: FeeSchedule, fees: FeeSchedule,
chainId: ChainId, chainId: ChainId,
neonRouterNumSamples: number,
samplerMetrics?: SamplerMetrics,
): Path | undefined { ): Path | undefined {
const before = performance.now(); const beforeAllTimeMs = performance.now();
const logPerformance = () => let beforeTimeMs = performance.now();
DEFAULT_INFO_LOGGER( const allSourcesPath = findRoutesAndCreateOptimalPath(
{ router: 'neon-router', performanceMs: performance.now() - before, type: 'total' }, side,
'Rust router total routing performance', samples,
); nativeOrders,
input,
const allSourcesPath = findRoutesAndCreateOptimalPath(side, samples, nativeOrders, input, opts, fees); opts,
fees,
neonRouterNumSamples,
);
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'neon-router',
type: 'all',
timingMs: performance.now() - beforeTimeMs,
});
if (!allSourcesPath) { if (!allSourcesPath) {
return undefined; return undefined;
} }
@@ -341,11 +338,27 @@ export function findOptimalRustPathFromSamples(
// HACK(kimpers): The Rust router currently doesn't account for VIP sources correctly // HACK(kimpers): The Rust router currently doesn't account for VIP sources correctly
// we need to try to route them in isolation and compare with the results all sources // we need to try to route them in isolation and compare with the results all sources
if (vipSources.length > 0) { if (vipSources.length > 0) {
beforeTimeMs = performance.now();
const vipSourcesSet = new Set(vipSources); const vipSourcesSet = new Set(vipSources);
const vipSourcesSamples = samples.filter(s => s[0] && vipSourcesSet.has(s[0].source)); const vipSourcesSamples = samples.filter(s => s[0] && vipSourcesSet.has(s[0].source));
if (vipSourcesSamples.length > 0) { if (vipSourcesSamples.length > 0) {
const vipSourcesPath = findRoutesAndCreateOptimalPath(side, vipSourcesSamples, [], input, opts, fees); const vipSourcesPath = findRoutesAndCreateOptimalPath(
side,
vipSourcesSamples,
[],
input,
opts,
fees,
neonRouterNumSamples,
);
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'neon-router',
type: 'vip',
timingMs: performance.now() - beforeTimeMs,
});
const { input: allSourcesInput, output: allSourcesOutput } = allSourcesPath.adjustedSize(); const { input: allSourcesInput, output: allSourcesOutput } = allSourcesPath.adjustedSize();
// NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset // NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset
@@ -358,13 +371,18 @@ export function findOptimalRustPathFromSamples(
const allSourcesAdjustedRateWithFqtOverhead = getRate(side, allSourcesInput, outputWithFqtOverhead); const allSourcesAdjustedRateWithFqtOverhead = getRate(side, allSourcesInput, outputWithFqtOverhead);
if (vipSourcesPath?.adjustedRate().isGreaterThan(allSourcesAdjustedRateWithFqtOverhead)) { if (vipSourcesPath?.adjustedRate().isGreaterThan(allSourcesAdjustedRateWithFqtOverhead)) {
logPerformance();
return vipSourcesPath; return vipSourcesPath;
} }
} }
} }
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'neon-router',
type: 'total',
timingMs: performance.now() - beforeAllTimeMs,
});
logPerformance();
return allSourcesPath; return allSourcesPath;
} }
@@ -377,8 +395,10 @@ export async function findOptimalPathJSAsync(
fills: Fill[][], fills: Fill[][],
targetInput: BigNumber, targetInput: BigNumber,
runLimit: number = 2 ** 8, runLimit: number = 2 ** 8,
samplerMetrics?: SamplerMetrics,
opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS, opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
): Promise<Path | undefined> { ): Promise<Path | undefined> {
const beforeTimeMs = performance.now();
// Sort fill arrays by descending adjusted completed rate. // Sort fill arrays by descending adjusted completed rate.
// Remove any paths which cannot impact the optimal path // Remove any paths which cannot impact the optimal path
const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts), side); const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts), side);
@@ -392,7 +412,15 @@ export async function findOptimalPathJSAsync(
// Yield to event loop. // Yield to event loop.
await Promise.resolve(); await Promise.resolve();
} }
return optimalPath.isComplete() ? optimalPath : undefined; const finalPath = optimalPath.isComplete() ? optimalPath : undefined;
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'js',
type: 'total',
timingMs: performance.now() - beforeTimeMs,
});
return finalPath;
} }
// Sort fill arrays by descending adjusted completed rate. // Sort fill arrays by descending adjusted completed rate.

View File

@@ -14,7 +14,8 @@ import { BatchedOperation, ERC20BridgeSource, LiquidityProviderRegistry, TokenAd
*/ */
export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] { export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] {
const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) => new BigNumber(expBase).pow(i)); const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) => new BigNumber(expBase).pow(i));
const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); const distributionSum = BigNumber.sum(...distribution);
const stepSizes = distribution.map(d => d.div(distributionSum));
const amounts = stepSizes.map((_s, i) => { const amounts = stepSizes.map((_s, i) => {
if (i === numSamples - 1) { if (i === numSamples - 1) {
return maxFillAmount; return maxFillAmount;

View File

@@ -455,6 +455,10 @@ export interface GetMarketOrdersOpts {
* Default: 1.25. * Default: 1.25.
*/ */
sampleDistributionBase: number; sampleDistributionBase: number;
/**
* Number of samples to use when creating fill curves with neon-router
*/
neonRouterNumSamples: number;
/** /**
* Fees for each liquidity source, expressed in gas. * Fees for each liquidity source, expressed in gas.
*/ */
@@ -514,6 +518,15 @@ export interface SamplerMetrics {
* @param blockNumber block number of the sampler call * @param blockNumber block number of the sampler call
*/ */
logBlockNumber(blockNumber: BigNumber): void; logBlockNumber(blockNumber: BigNumber): void;
/**
* Logs the routing timings
*
* @param data.router The router type (neon-router or js)
* @param data.type The type of timing being recorded (e.g total timing, all sources timing or vip timing)
* @param data.timingMs The timing in milliseconds
*/
logRouterDetails(data: { router: 'neon-router' | 'js'; type: 'all' | 'vip' | 'total'; timingMs: number }): void;
} }
/** /**
@@ -599,6 +612,8 @@ export interface GenerateOptimizedOrdersOpts {
allowFallback?: boolean; allowFallback?: boolean;
shouldBatchBridgeOrders?: boolean; shouldBatchBridgeOrders?: boolean;
gasPrice: BigNumber; gasPrice: BigNumber;
neonRouterNumSamples: number;
samplerMetrics?: SamplerMetrics;
} }
export interface ComparisonPrice { export interface ComparisonPrice {

View File

@@ -959,10 +959,10 @@
typedoc "~0.16.11" typedoc "~0.16.11"
yargs "^10.0.3" yargs "^10.0.3"
"@0x/neon-router@^0.2.1": "@0x/neon-router@^0.3.1":
version "0.2.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/@0x/neon-router/-/neon-router-0.2.1.tgz#23bb3cedc0eafd55a8ba6b6ea8a59ee4c538064b" resolved "https://registry.yarnpkg.com/@0x/neon-router/-/neon-router-0.3.1.tgz#4ec13e750d1435357c4928d7f2521a2b4376f27e"
integrity sha512-feCCKuox4staZl8lxLY4nf5U256NcDHrgvSFra5cU/TUhoblLHb8F7eWAC9ygpukZUCVFLy13mExkFQHXlEOYw== integrity sha512-M4ypTov9KyxsGJpYwobrld3Y2JOlR7U0XjR6BEQE2gQ1k3nie/1wNEI2J4ZjKw++RLDxdv/RCqhgA5VnINzjxA==
dependencies: dependencies:
"@mapbox/node-pre-gyp" "^1.0.5" "@mapbox/node-pre-gyp" "^1.0.5"