@0x/asset-swapper: hack together basic sampler service integration

This commit is contained in:
Lawrence Forman
2021-11-03 13:12:39 -04:00
parent fce3664258
commit 83cae575fa
24 changed files with 890 additions and 2653 deletions

View File

@@ -80,6 +80,7 @@
"@ethersproject/contracts": "^5.0.1",
"@ethersproject/providers": "^5.0.4",
"@ethersproject/strings": "^5.0.10",
"@open-rpc/client-js": "^1.7.1",
"axios": "^0.21.1",
"axios-mock-adapter": "^1.19.0",
"cream-sor": "^0.3.3",

View File

@@ -1,4 +1,3 @@
import { ChainId } from '@0x/contract-addresses';
import { SignatureType } from '@0x/protocol-utils';
import { BigNumber, logUtils } from '@0x/utils';
@@ -11,12 +10,10 @@ import {
RfqRequestOpts,
SwapQuoteGetOutputOpts,
SwapQuoteRequestOpts,
SwapQuoterOpts,
} from './types';
import {
DEFAULT_GET_MARKET_ORDERS_OPTS,
DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID,
DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID,
} from './utils/market_operation_utils/constants';
const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json';
@@ -43,20 +40,6 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(0);
// default 50% buffer for selecting native orders to be aggregated with other sources
const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5;
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
chainId: ChainId.Mainnet,
orderRefreshIntervalMs: 10000, // 10 seconds
...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 500e6,
ethGasStationUrl: ETH_GAS_STATION_API_URL,
rfqt: {
integratorsWhitelist: [],
makerAssetOfferings: {},
txOriginBlacklist: new Set(),
},
tokenAdjacencyGraph: DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID[ChainId.Mainnet],
};
const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = {
isFromETH: false,
isToETH: false,
@@ -111,7 +94,6 @@ export const constants = {
ONE_AMOUNT: new BigNumber(1),
ONE_SECOND_MS,
ONE_MINUTE_MS,
DEFAULT_SWAP_QUOTER_OPTS,
DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS,

View File

@@ -95,9 +95,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
private readonly _exchangeProxy: IZeroExContract;
constructor(public readonly contractAddresses: ContractAddresses, options: Partial<SwapQuoteConsumerOpts> = {}) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
constructor(public readonly contractAddresses: ContractAddresses, options: SwapQuoteConsumerOpts) {
const { chainId } = options;
this.chainId = chainId;
this.contractAddresses = contractAddresses;
this._exchangeProxy = new IZeroExContract(contractAddresses.exchangeProxy, FAKE_PROVIDER);
@@ -276,62 +275,62 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
};
}
if (
this.chainId === ChainId.Mainnet &&
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Curve, ERC20BridgeSource.Swerve]) &&
// Curve VIP cannot currently support WETH buy/sell as the functionality needs to WITHDRAW or DEPOSIT
// into WETH prior/post the trade.
// ETH buy/sell is supported
![sellToken, buyToken].includes(NATIVE_FEE_TOKEN_BY_CHAIN_ID[ChainId.Mainnet])
) {
const fillData = slippedOrders[0].fills[0].fillData as CurveFillData;
return {
calldataHexString: this._exchangeProxy
.sellToLiquidityProvider(
isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
isToETH ? ETH_TOKEN_ADDRESS : buyToken,
CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID[this.chainId],
NULL_ADDRESS,
sellAmount,
minBuyAmount,
encodeCurveLiquidityProviderData({
curveAddress: fillData.pool.poolAddress,
exchangeFunctionSelector: fillData.pool.exchangeFunctionSelector,
fromCoinIdx: new BigNumber(fillData.fromTokenIdx),
toCoinIdx: new BigNumber(fillData.toTokenIdx),
}),
)
.getABIEncodedTransactionData(),
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address,
allowanceTarget: this._exchangeProxy.address,
gasOverhead: ZERO_AMOUNT,
};
}
// if (
// this.chainId === ChainId.Mainnet &&
// isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Curve, ERC20BridgeSource.Swerve]) &&
// // Curve VIP cannot currently support WETH buy/sell as the functionality needs to WITHDRAW or DEPOSIT
// // into WETH prior/post the trade.
// // ETH buy/sell is supported
// ![sellToken, buyToken].includes(NATIVE_FEE_TOKEN_BY_CHAIN_ID[ChainId.Mainnet])
// ) {
// const fillData = slippedOrders[0].fills[0].fillData as CurveFillData;
// return {
// calldataHexString: this._exchangeProxy
// .sellToLiquidityProvider(
// isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
// isToETH ? ETH_TOKEN_ADDRESS : buyToken,
// CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID[this.chainId],
// NULL_ADDRESS,
// sellAmount,
// minBuyAmount,
// encodeCurveLiquidityProviderData({
// curveAddress: fillData.pool.poolAddress,
// exchangeFunctionSelector: fillData.pool.exchangeFunctionSelector,
// fromCoinIdx: new BigNumber(fillData.fromTokenIdx),
// toCoinIdx: new BigNumber(fillData.toTokenIdx),
// }),
// )
// .getABIEncodedTransactionData(),
// ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
// toAddress: this._exchangeProxy.address,
// allowanceTarget: this._exchangeProxy.address,
// gasOverhead: ZERO_AMOUNT,
// };
// }
if (
this.chainId === ChainId.Mainnet &&
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap])
) {
const fillData = slippedOrders[0].fills[0].fillData as MooniswapFillData;
return {
calldataHexString: this._exchangeProxy
.sellToLiquidityProvider(
isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
isToETH ? ETH_TOKEN_ADDRESS : buyToken,
MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID[this.chainId],
NULL_ADDRESS,
sellAmount,
minBuyAmount,
poolEncoder.encode([fillData.poolAddress]),
)
.getABIEncodedTransactionData(),
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address,
allowanceTarget: this.contractAddresses.exchangeProxy,
gasOverhead: ZERO_AMOUNT,
};
}
// if (
// this.chainId === ChainId.Mainnet &&
// isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap])
// ) {
// const fillData = slippedOrders[0].fills[0].fillData as MooniswapFillData;
// return {
// calldataHexString: this._exchangeProxy
// .sellToLiquidityProvider(
// isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
// isToETH ? ETH_TOKEN_ADDRESS : buyToken,
// MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID[this.chainId],
// NULL_ADDRESS,
// sellAmount,
// minBuyAmount,
// poolEncoder.encode([fillData.poolAddress]),
// )
// .getABIEncodedTransactionData(),
// ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
// toAddress: this._exchangeProxy.address,
// allowanceTarget: this.contractAddresses.exchangeProxy,
// gasOverhead: ZERO_AMOUNT,
// };
// }
if (this.chainId === ChainId.Mainnet && isMultiplexBatchFillCompatible(quote, optsWithDefaults)) {
return {

View File

@@ -20,13 +20,12 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
private readonly _contractAddresses: ContractAddresses;
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
public static getSwapQuoteConsumer(options: Partial<SwapQuoteConsumerOpts> = {}): SwapQuoteConsumer {
public static getSwapQuoteConsumer(options: SwapQuoteConsumerOpts): SwapQuoteConsumer {
return new SwapQuoteConsumer(options);
}
constructor(options: Partial<SwapQuoteConsumerOpts> = {}) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
constructor(options: SwapQuoteConsumerOpts) {
const { chainId } = options;
this.chainId = chainId;
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);

View File

@@ -1,4 +1,4 @@
import { ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { FillQuoteTransformerOrderType, LimitOrder } from '@0x/protocol-utils';
import { BigNumber, providerUtils } from '@0x/utils';
import Axios, { AxiosInstance } from 'axios';
@@ -26,9 +26,8 @@ import {
} from './types';
import { assert } from './utils/assert';
import { MarketOperationUtils } from './utils/market_operation_utils';
import { BancorService } from './utils/market_operation_utils/bancor_service';
import { SAMPLER_ADDRESS, SOURCE_FLAGS, ZERO_AMOUNT } from './utils/market_operation_utils/constants';
import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { SamplerClient } from './utils/market_operation_utils/sampler';
import { SourceFilters } from './utils/market_operation_utils/source_filters';
import {
ERC20BridgeSource,
@@ -85,20 +84,15 @@ export class SwapQuoter {
*
* @return An instance of SwapQuoter
*/
constructor(supportedProvider: SupportedProvider, orderbook: Orderbook, options: Partial<SwapQuoterOpts> = {}) {
constructor(supportedProvider: SupportedProvider, orderbook: Orderbook, options: SwapQuoterOpts) {
const {
chainId,
expiryBufferMs,
permittedOrderFeeTypes,
samplerGasLimit,
rfqt,
tokenAdjacencyGraph,
liquidityProviderRegistry,
} = { ...constants.DEFAULT_SWAP_QUOTER_OPTS, ...options };
} = options;
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderbook('orderbook', orderbook);
assert.isNumber('chainId', chainId);
assert.isNumber('expiryBufferMs', expiryBufferMs);
this.chainId = chainId;
this.provider = provider;
this.orderbook = orderbook;
@@ -113,45 +107,11 @@ export class SwapQuoter {
constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
options.ethGasStationUrl,
);
// Allow the sampler bytecode to be overwritten using geths override functionality
const samplerBytecode = _.get(artifacts.ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object');
// Allow address of the Sampler to be overridden, i.e in Ganache where overrides do not work
const samplerAddress = (options.samplerOverrides && options.samplerOverrides.to) || SAMPLER_ADDRESS;
const defaultCodeOverrides = samplerBytecode
? {
[samplerAddress]: { code: samplerBytecode },
}
: {};
const samplerOverrides = _.assign(
{ block: BlockParamLiteral.Latest, overrides: defaultCodeOverrides },
options.samplerOverrides,
);
const fastAbi = new FastABI(ERC20BridgeSamplerContract.ABI() as MethodAbi[], { BigNumber });
const samplerContract = new ERC20BridgeSamplerContract(
samplerAddress,
this.provider,
{
gas: samplerGasLimit,
},
{},
undefined,
{
encodeInput: (fnName: string, values: any) => fastAbi.encodeInput(fnName, values),
decodeOutput: (fnName: string, data: string) => fastAbi.decodeOutput(fnName, data),
},
);
this._marketOperationUtils = new MarketOperationUtils(
new DexOrderSampler(
SamplerClient.createFromChainIdAndEndpoint(
this.chainId,
samplerContract,
samplerOverrides,
undefined, // pools caches for balancer and cream
tokenAdjacencyGraph,
liquidityProviderRegistry,
this.chainId === ChainId.Mainnet // Enable Bancor only on Mainnet
? async () => BancorService.createAsync(provider)
: async () => undefined,
options.samplerServiceUrl,
),
this._contractAddresses,
{
@@ -243,49 +203,50 @@ export class SwapQuoter {
takerAssetAmount: BigNumber,
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<MarketDepth> {
assert.isString('makerToken', makerToken);
assert.isString('takerToken', takerToken);
const sourceFilters = new SourceFilters([], options.excludedSources, options.includedSources);
let [sellOrders, buyOrders] = !sourceFilters.isAllowed(ERC20BridgeSource.Native)
? [[], []]
: await Promise.all([
this.orderbook.getOrdersAsync(makerToken, takerToken),
this.orderbook.getOrdersAsync(takerToken, makerToken),
]);
if (!sellOrders || sellOrders.length === 0) {
sellOrders = [createDummyOrder(makerToken, takerToken)];
}
if (!buyOrders || buyOrders.length === 0) {
buyOrders = [createDummyOrder(takerToken, makerToken)];
}
const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => {
const { dexQuotes, nativeOrders } = marketSideLiquidity.quotes;
const { side } = marketSideLiquidity;
return [
...dexQuotes,
nativeOrders.map(o => {
return {
input: side === MarketOperation.Sell ? o.fillableTakerAmount : o.fillableMakerAmount,
output: side === MarketOperation.Sell ? o.fillableMakerAmount : o.fillableTakerAmount,
fillData: o,
source: ERC20BridgeSource.Native,
};
}),
];
};
const [bids, asks] = await Promise.all([
this._marketOperationUtils.getMarketBuyLiquidityAsync(buyOrders, takerAssetAmount, options),
this._marketOperationUtils.getMarketSellLiquidityAsync(sellOrders, takerAssetAmount, options),
]);
return {
bids: getMarketDepthSide(bids),
asks: getMarketDepthSide(asks),
makerTokenDecimals: asks.makerTokenDecimals,
takerTokenDecimals: asks.takerTokenDecimals,
};
throw new Error(`Not implemented`);
// assert.isString('makerToken', makerToken);
// assert.isString('takerToken', takerToken);
// const sourceFilters = new SourceFilters([], options.excludedSources, options.includedSources);
//
// let [sellOrders, buyOrders] = !sourceFilters.isAllowed(ERC20BridgeSource.Native)
// ? [[], []]
// : await Promise.all([
// this.orderbook.getOrdersAsync(makerToken, takerToken),
// this.orderbook.getOrdersAsync(takerToken, makerToken),
// ]);
// if (!sellOrders || sellOrders.length === 0) {
// sellOrders = [createDummyOrder(makerToken, takerToken)];
// }
// if (!buyOrders || buyOrders.length === 0) {
// buyOrders = [createDummyOrder(takerToken, makerToken)];
// }
//
// const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => {
// const { dexQuotes, nativeOrders } = marketSideLiquidity.quotes;
// const { side } = marketSideLiquidity;
//
// return [
// ...dexQuotes,
// nativeOrders.map(o => {
// return {
// input: side === MarketOperation.Sell ? o.fillableTakerAmount : o.fillableMakerAmount,
// output: side === MarketOperation.Sell ? o.fillableMakerAmount : o.fillableTakerAmount,
// fillData: o,
// source: ERC20BridgeSource.Native,
// };
// }),
// ];
// };
// const [bids, asks] = await Promise.all([
// this._marketOperationUtils.getMarketBuyLiquidityAsync(buyOrders, takerAssetAmount, options),
// this._marketOperationUtils.getMarketSellLiquidityAsync(sellOrders, takerAssetAmount, options),
// ]);
// return {
// bids: getMarketDepthSide(bids),
// asks: getMarketDepthSide(asks),
// makerTokenDecimals: asks.makerTokenDecimals,
// takerTokenDecimals: asks.takerTokenDecimals,
// };
}
/**

View File

@@ -22,6 +22,9 @@ import {
import { PriceComparisonsReport, QuoteReport } from './utils/quote_report_generator';
import { MetricsProxy } from './utils/quote_requestor';
export type Address = string;
export type Bytes = string;
/**
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
* permittedOrderFeeTypes: A set of all the takerFee types that OrderPruner will filter for
@@ -326,15 +329,16 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
chainId: ChainId;
orderRefreshIntervalMs: number;
expiryBufferMs: number;
ethereumRpcUrl?: string;
// ethereumRpcUrl?: string;
contractAddresses?: AssetSwapperContractAddresses;
samplerGasLimit?: number;
multiBridgeAddress?: string;
// multiBridgeAddress?: string;
ethGasStationUrl?: string;
rfqt?: SwapQuoterRfqOpts;
samplerOverrides?: SamplerOverrides;
tokenAdjacencyGraph?: TokenAdjacencyGraph;
liquidityProviderRegistry?: LiquidityProviderRegistry;
// samplerOverrides?: SamplerOverrides;
// tokenAdjacencyGraph?: TokenAdjacencyGraph;
// liquidityProviderRegistry?: LiquidityProviderRegistry;
samplerServiceUrl: string;
}
/**

View File

@@ -1722,13 +1722,14 @@ export const VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID = valueByChainId<ERC20BridgeSo
);
const uniswapV2CloneGasSchedule = (fillData?: FillData) => {
// TODO: Different base cost if to/from ETH.
let gas = 90e3;
const path = (fillData as UniswapV2FillData).tokenAddressPath;
if (path.length > 2) {
gas += (path.length - 2) * 60e3; // +60k for each hop.
}
return gas;
return 90e3;
// // TODO: Different base cost if to/from ETH.
// let gas = 90e3;
// const path = (fillData as UniswapV2FillData).tokenAddressPath;
// if (path.length > 2) {
// gas += (path.length - 2) * 60e3; // +60k for each hop.
// }
// return gas;
};
/**
@@ -1777,21 +1778,23 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
[ERC20BridgeSource.Cream]: () => 120e3,
[ERC20BridgeSource.MStable]: () => 200e3,
[ERC20BridgeSource.MakerPsm]: (fillData?: FillData) => {
const psmFillData = fillData as MakerPsmFillData;
return psmFillData.takerToken === psmFillData.gemTokenAddress ? 210e3 : 290e3;
return 210e3;
// const psmFillData = fillData as MakerPsmFillData;
// return psmFillData.takerToken === psmFillData.gemTokenAddress ? 210e3 : 290e3;
},
[ERC20BridgeSource.Mooniswap]: () => 130e3,
[ERC20BridgeSource.Shell]: () => 170e3,
[ERC20BridgeSource.Component]: () => 188e3,
[ERC20BridgeSource.MultiHop]: (fillData?: FillData) => {
const firstHop = (fillData as MultiHopFillData).firstHopSource;
const secondHop = (fillData as MultiHopFillData).secondHopSource;
const firstHopGas = DEFAULT_GAS_SCHEDULE[firstHop.source](firstHop.fillData);
const secondHopGas = DEFAULT_GAS_SCHEDULE[secondHop.source](secondHop.fillData);
return new BigNumber(firstHopGas)
.plus(secondHopGas)
.plus(30e3)
.toNumber();
return 0;
// const firstHop = (fillData as MultiHopFillData).firstHopSource;
// const secondHop = (fillData as MultiHopFillData).secondHopSource;
// const firstHopGas = DEFAULT_GAS_SCHEDULE[firstHop.source](firstHop.fillData);
// const secondHopGas = DEFAULT_GAS_SCHEDULE[secondHop.source](secondHop.fillData);
// return new BigNumber(firstHopGas)
// .plus(secondHopGas)
// .plus(30e3)
// .toNumber();
},
[ERC20BridgeSource.Dodo]: (fillData?: FillData) => {
const isSellBase = (fillData as DODOFillData).isSellBase;
@@ -1801,29 +1804,32 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
},
[ERC20BridgeSource.DodoV2]: (_fillData?: FillData) => 100e3,
[ERC20BridgeSource.Bancor]: (fillData?: FillData) => {
let gas = 200e3;
const path = (fillData as BancorFillData).path;
if (path.length > 2) {
gas += (path.length - 2) * 60e3; // +60k for each hop.
}
return gas;
return 200e3;
// let gas = 200e3;
// const path = (fillData as BancorFillData).path;
// if (path.length > 2) {
// gas += (path.length - 2) * 60e3; // +60k for each hop.
// }
// return gas;
},
[ERC20BridgeSource.KyberDmm]: (fillData?: FillData) => {
return 95e3;
// TODO: Different base cost if to/from ETH.
let gas = 95e3;
const path = (fillData as UniswapV2FillData).tokenAddressPath;
if (path.length > 2) {
gas += (path.length - 2) * 65e3; // +65k for each hop.
}
return gas;
// let gas = 95e3;
// const path = (fillData as UniswapV2FillData).tokenAddressPath;
// if (path.length > 2) {
// gas += (path.length - 2) * 65e3; // +65k for each hop.
// }
// return gas;
},
[ERC20BridgeSource.UniswapV3]: (fillData?: FillData) => {
let gas = 100e3;
const path = (fillData as UniswapV3FillData).tokenAddressPath;
if (path.length > 2) {
gas += (path.length - 2) * 32e3; // +32k for each hop.
}
return gas;
return 100e3;
// let gas = 100e3;
// const path = (fillData as UniswapV3FillData).tokenAddressPath;
// if (path.length > 2) {
// gas += (path.length - 2) * 32e3; // +32k for each hop.
// }
// return gas;
},
[ERC20BridgeSource.Lido]: () => 226e3,

View File

@@ -97,61 +97,62 @@ export function nativeOrdersToFills(
inputAmountPerEth: BigNumber,
fees: FeeSchedule,
): Fill[] {
const sourcePathId = hexUtils.random();
// Create a single path from all orders.
let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
for (const o of orders) {
const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o;
const makerAmount = fillableMakerAmount;
const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o);
const outputPenalty = ethToOutputAmount({
input,
output,
inputAmountPerEth,
outputAmountPerEth,
ethAmount: fee,
});
// targetInput can be less than the order size
// whilst the penalty is constant, it affects the adjusted output
// only up until the target has been exhausted.
// A large order and an order at the exact target should be penalized
// the same.
const clippedInput = BigNumber.min(targetInput, input);
// scale the clipped output inline with the input
const clippedOutput = clippedInput.dividedBy(input).times(output);
const adjustedOutput =
side === MarketOperation.Sell ? clippedOutput.minus(outputPenalty) : clippedOutput.plus(outputPenalty);
const adjustedRate =
side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput);
// Skip orders with rates that are <= 0.
if (adjustedRate.lte(0)) {
continue;
}
fills.push({
sourcePathId,
adjustedRate,
adjustedOutput,
input: clippedInput,
output: clippedOutput,
flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'],
index: 0, // TBD
parent: undefined, // TBD
source: ERC20BridgeSource.Native,
type,
fillData: { ...o },
});
}
// Sort by descending adjusted rate.
fills = fills.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
// Re-index fills.
for (let i = 0; i < fills.length; ++i) {
fills[i].parent = i === 0 ? undefined : fills[i - 1];
fills[i].index = i;
}
return fills;
throw new Error(`Not implemented`);
// const sourcePathId = hexUtils.random();
// // Create a single path from all orders.
// let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
// for (const o of orders) {
// const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o;
// const makerAmount = fillableMakerAmount;
// const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
// const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
// const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
// const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o);
// const outputPenalty = ethToOutputAmount({
// input,
// output,
// inputAmountPerEth,
// outputAmountPerEth,
// ethAmount: fee,
// });
// // targetInput can be less than the order size
// // whilst the penalty is constant, it affects the adjusted output
// // only up until the target has been exhausted.
// // A large order and an order at the exact target should be penalized
// // the same.
// const clippedInput = BigNumber.min(targetInput, input);
// // scale the clipped output inline with the input
// const clippedOutput = clippedInput.dividedBy(input).times(output);
// const adjustedOutput =
// side === MarketOperation.Sell ? clippedOutput.minus(outputPenalty) : clippedOutput.plus(outputPenalty);
// const adjustedRate =
// side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput);
// // Skip orders with rates that are <= 0.
// if (adjustedRate.lte(0)) {
// continue;
// }
// fills.push({
// sourcePathId,
// adjustedRate,
// adjustedOutput,
// input: clippedInput,
// output: clippedOutput,
// flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'],
// index: 0, // TBD
// parent: undefined, // TBD
// source: ERC20BridgeSource.Native,
// type,
// fillData: { ...o },
// });
// }
// // Sort by descending adjusted rate.
// fills = fills.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
// // Re-index fills.
// for (let i = 0; i < fills.length; ++i) {
// fills[i].parent = i === 0 ? undefined : fills[i - 1];
// fills[i].index = i;
// }
// return fills;
}
export function dexSamplesToFills(
@@ -171,10 +172,10 @@ export function dexSamplesToFills(
for (let i = 0; i < nonzeroSamples.length; i++) {
const sample = nonzeroSamples[i];
const prevSample = i === 0 ? undefined : nonzeroSamples[i - 1];
const { source, fillData } = sample;
const { source, encodedFillData } = sample;
const input = sample.input.minus(prevSample ? prevSample.input : 0);
const output = sample.output.minus(prevSample ? prevSample.output : 0);
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData) || 0;
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.encodedFillData) || 0;
let penalty = ZERO_AMOUNT;
if (i === 0) {
// Only the first fill in a DEX path incurs a penalty.
@@ -194,7 +195,7 @@ export function dexSamplesToFills(
output,
adjustedOutput,
source,
fillData,
encodedFillData,
type: FillQuoteTransformerOrderType.Bridge,
index: i,
parent: i !== 0 ? fills[fills.length - 1] : undefined,

View File

@@ -40,7 +40,7 @@ import { getBestTwoHopQuote } from './multihop_utils';
import { createOrdersFromTwoHopSample } from './orders';
import { Path, PathPenaltyOpts } from './path';
import { fillsToSortedPaths, findOptimalPathJSAsync, findOptimalRustPathFromSamples } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler';
import { Sampler } from './sampler';
import { SourceFilters } from './source_filters';
import {
AggregationError,
@@ -65,7 +65,6 @@ export class MarketOperationUtils {
private readonly _buySources: SourceFilters;
private readonly _feeSources: SourceFilters;
private readonly _nativeFeeToken: string;
private readonly _nativeFeeTokenAmount: BigNumber;
private static _computeQuoteReport(
quoteRequestor: QuoteRequestor | undefined,
@@ -73,9 +72,10 @@ export class MarketOperationUtils {
optimizerResult: OptimizerResult,
comparisonPrice?: BigNumber | undefined,
): QuoteReport {
const { side, quotes } = marketSideLiquidity;
const { liquidityDelivered } = optimizerResult;
return generateQuoteReport(side, quotes.nativeOrders, liquidityDelivered, comparisonPrice, quoteRequestor);
throw new Error(`Not implemented`);
// const { side, quotes } = marketSideLiquidity;
// const { liquidityDelivered } = optimizerResult;
// return generateQuoteReport(side, quotes.nativeOrders, liquidityDelivered, comparisonPrice, quoteRequestor);
}
private static _computePriceComparisonsReport(
@@ -83,24 +83,25 @@ export class MarketOperationUtils {
marketSideLiquidity: MarketSideLiquidity,
comparisonPrice?: BigNumber | undefined,
): PriceComparisonsReport {
const { side, quotes } = marketSideLiquidity;
const dexSources = _.flatten(quotes.dexQuotes).map(quote => dexSampleToReportSource(quote, side));
const multiHopSources = quotes.twoHopQuotes.map(quote => multiHopSampleToReportSource(quote, side));
const nativeSources = quotes.nativeOrders.map(order =>
nativeOrderToReportEntry(
order.type,
order as any,
order.fillableTakerAmount,
comparisonPrice,
quoteRequestor,
),
);
return { dexSources, multiHopSources, nativeSources };
throw new Error(`Not implemented`);
// const { side, quotes } = marketSideLiquidity;
// const dexSources = _.flatten(quotes.dexQuotes).map(quote => dexSampleToReportSource(quote, side));
// const multiHopSources = quotes.twoHopQuotes.map(quote => multiHopSampleToReportSource(quote, side));
// const nativeSources = quotes.nativeOrders.map(order =>
// nativeOrderToReportEntry(
// order.type,
// order as any,
// order.fillableTakerAmount,
// comparisonPrice,
// quoteRequestor,
// ),
// );
//
// return { dexSources, multiHopSources, nativeSources };
}
constructor(
private readonly _sampler: DexOrderSampler,
private readonly _sampler: Sampler,
private readonly contractAddresses: AssetSwapperContractAddresses,
private readonly _orderDomain: OrderDomain,
) {
@@ -108,7 +109,6 @@ export class MarketOperationUtils {
this._sellSources = SELL_SOURCE_FILTER_BY_CHAIN_ID[_sampler.chainId];
this._feeSources = new SourceFilters(FEE_QUOTE_SOURCES_BY_CHAIN_ID[_sampler.chainId]);
this._nativeFeeToken = NATIVE_FEE_TOKEN_BY_CHAIN_ID[_sampler.chainId];
this._nativeFeeTokenAmount = NATIVE_FEE_TOKEN_AMOUNT_BY_CHAIN_ID[_sampler.chainId];
}
/**
@@ -125,87 +125,53 @@ export class MarketOperationUtils {
): Promise<MarketSideLiquidity> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const { makerToken, takerToken } = nativeOrders[0].order;
const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const quoteSourceFilters = this._sellSources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
// Used to determine whether the tx origin is an EOA or a contract
const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
// Call the sampler contract.
const samplerPromise = this._sampler.executeAsync(
this._sampler.getTokenDecimals([makerToken, takerToken]),
// Get native order fillable amounts.
this._sampler.getLimitOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// Get ETH -> maker token price.
this._sampler.getMedianSellRate(
feeSourceFilters.sources,
makerToken,
this._nativeFeeToken,
this._nativeFeeTokenAmount,
),
// Get ETH -> taker token price.
this._sampler.getMedianSellRate(
feeSourceFilters.sources,
takerToken,
this._nativeFeeToken,
this._nativeFeeTokenAmount,
),
// Get sell quotes for taker -> maker.
this._sampler.getSellQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
this._sampler.getTwoHopSellQuotes(
quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
makerToken,
takerToken,
takerAmount,
),
this._sampler.isAddressContract(txOrigin),
);
// Refresh the cached pools asynchronously if required
void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken);
const [
[
tokenDecimals,
orderFillableTakerAmounts,
outputAmountPerEth,
inputAmountPerEth,
dexQuotes,
rawTwoHopQuotes,
isTxOriginContract,
],
] = await Promise.all([samplerPromise]);
tokenInfos,
[makerTokenToEthPrice, takerTokenToEthPrice],
dexQuotes,
] = await Promise.all([
this._sampler.getTokenInfosAsync(
[makerToken, takerToken],
),
this._sampler.getPricesAsync(
[
[makerToken, this._nativeFeeToken],
[takerToken, this._nativeFeeToken],
],
feeSourceFilters.sources,
),
this._sampler.getSellLiquidityAsync(
[makerToken, takerToken],
takerAmount,
quoteSourceFilters.sources,
),
]);
// Filter out any invalid two hop quotes where we couldn't find a route
const twoHopQuotes = rawTwoHopQuotes.filter(
q => q && q.fillData && q.fillData.firstHopSource && q.fillData.secondHopSource,
);
const [makerTokenInfo, takerTokenInfo] = tokenInfos;
const makerTokenDecimals = makerTokenInfo.decimals;
const takerTokenDecimals = takerTokenInfo.decimals;
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
const isRfqSupported = !!(_opts.rfqt && !isTxOriginContract);
const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
...order,
...getNativeAdjustedFillableAmountsFromTakerAmount(order, orderFillableTakerAmounts[i]),
}));
const isRfqSupported = !!_opts.rfqt;
return {
side: MarketOperation.Sell,
inputAmount: takerAmount,
inputToken: takerToken,
outputToken: makerToken,
outputAmountPerEth,
inputAmountPerEth,
outputAmountPerEth: makerTokenToEthPrice,
inputAmountPerEth: takerTokenToEthPrice,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
makerTokenDecimals: makerTokenDecimals,
takerTokenDecimals: takerTokenDecimals,
quotes: {
nativeOrders: limitOrdersWithFillableAmounts,
nativeOrders: [],
rfqtIndicativeQuotes: [],
twoHopQuotes,
// twoHopQuotes: [],
dexQuotes,
},
isRfqSupported,
@@ -224,93 +190,94 @@ export class MarketOperationUtils {
makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<MarketSideLiquidity> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const { makerToken, takerToken } = nativeOrders[0].order;
const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const quoteSourceFilters = this._buySources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
// Used to determine whether the tx origin is an EOA or a contract
const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
// Call the sampler contract.
const samplerPromise = this._sampler.executeAsync(
this._sampler.getTokenDecimals([makerToken, takerToken]),
// Get native order fillable amounts.
this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// Get ETH -> makerToken token price.
this._sampler.getMedianSellRate(
feeSourceFilters.sources,
makerToken,
this._nativeFeeToken,
this._nativeFeeTokenAmount,
),
// Get ETH -> taker token price.
this._sampler.getMedianSellRate(
feeSourceFilters.sources,
takerToken,
this._nativeFeeToken,
this._nativeFeeTokenAmount,
),
// Get buy quotes for taker -> maker.
this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
this._sampler.getTwoHopBuyQuotes(
quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
makerToken,
takerToken,
makerAmount,
),
this._sampler.isAddressContract(txOrigin),
);
// Refresh the cached pools asynchronously if required
void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken);
const [
[
tokenDecimals,
orderFillableMakerAmounts,
ethToMakerAssetRate,
ethToTakerAssetRate,
dexQuotes,
rawTwoHopQuotes,
isTxOriginContract,
],
] = await Promise.all([samplerPromise]);
// Filter out any invalid two hop quotes where we couldn't find a route
const twoHopQuotes = rawTwoHopQuotes.filter(
q => q && q.fillData && q.fillData.firstHopSource && q.fillData.secondHopSource,
);
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
const isRfqSupported = !isTxOriginContract;
const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
...order,
...getNativeAdjustedFillableAmountsFromMakerAmount(order, orderFillableMakerAmounts[i]),
}));
return {
side: MarketOperation.Buy,
inputAmount: makerAmount,
inputToken: makerToken,
outputToken: takerToken,
outputAmountPerEth: ethToTakerAssetRate,
inputAmountPerEth: ethToMakerAssetRate,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
quotes: {
nativeOrders: limitOrdersWithFillableAmounts,
rfqtIndicativeQuotes: [],
twoHopQuotes,
dexQuotes,
},
isRfqSupported,
};
throw new Error(`Not implemented`);
// const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
// const { makerToken, takerToken } = nativeOrders[0].order;
// const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase);
//
// const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
// const quoteSourceFilters = this._buySources.merge(requestFilters);
// const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
//
// // Used to determine whether the tx origin is an EOA or a contract
// const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
//
// // Call the sampler contract.
// const samplerPromise = this._sampler.executeAsync(
// this._sampler.getTokenDecimals([makerToken, takerToken]),
// // Get native order fillable amounts.
// this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// // Get ETH -> makerToken token price.
// this._sampler.getMedianSellRate(
// feeSourceFilters.sources,
// makerToken,
// this._nativeFeeToken,
// this._nativeFeeTokenAmount,
// ),
// // Get ETH -> taker token price.
// this._sampler.getMedianSellRate(
// feeSourceFilters.sources,
// takerToken,
// this._nativeFeeToken,
// this._nativeFeeTokenAmount,
// ),
// // Get buy quotes for taker -> maker.
// this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
// this._sampler.getTwoHopBuyQuotes(
// quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
// makerToken,
// takerToken,
// makerAmount,
// ),
// this._sampler.isAddressContract(txOrigin),
// );
//
// // Refresh the cached pools asynchronously if required
// void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken);
//
// const [
// [
// tokenDecimals,
// orderFillableMakerAmounts,
// ethToMakerAssetRate,
// ethToTakerAssetRate,
// dexQuotes,
// rawTwoHopQuotes,
// isTxOriginContract,
// ],
// ] = await Promise.all([samplerPromise]);
//
// // Filter out any invalid two hop quotes where we couldn't find a route
// const twoHopQuotes = rawTwoHopQuotes.filter(
// q => q && q.fillData && q.fillData.firstHopSource && q.fillData.secondHopSource,
// );
//
// const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
// const isRfqSupported = !isTxOriginContract;
//
// const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
// ...order,
// ...getNativeAdjustedFillableAmountsFromMakerAmount(order, orderFillableMakerAmounts[i]),
// }));
//
// return {
// side: MarketOperation.Buy,
// inputAmount: makerAmount,
// inputToken: makerToken,
// outputToken: takerToken,
// outputAmountPerEth: ethToTakerAssetRate,
// inputAmountPerEth: ethToMakerAssetRate,
// quoteSourceFilters,
// makerTokenDecimals: makerTokenDecimals.toNumber(),
// takerTokenDecimals: takerTokenDecimals.toNumber(),
// quotes: {
// nativeOrders: limitOrdersWithFillableAmounts,
// rfqtIndicativeQuotes: [],
// twoHopQuotes,
// dexQuotes,
// },
// isRfqSupported,
// };
}
/**
@@ -329,98 +296,99 @@ export class MarketOperationUtils {
makerAmounts: BigNumber[],
opts: Partial<GetMarketOrdersOpts> & { gasPrice: BigNumber },
): Promise<Array<OptimizerResult | undefined>> {
if (batchNativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts: GetMarketOrdersOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const quoteSourceFilters = this._buySources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
const ops = [
...batchNativeOrders.map(orders =>
this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy),
),
...batchNativeOrders.map(orders =>
this._sampler.getMedianSellRate(
feeSourceFilters.sources,
orders[0].order.takerToken,
this._nativeFeeToken,
this._nativeFeeTokenAmount,
),
),
...batchNativeOrders.map((orders, i) =>
this._sampler.getBuyQuotes(
quoteSourceFilters.sources,
orders[0].order.makerToken,
orders[0].order.takerToken,
[makerAmounts[i]],
),
),
...batchNativeOrders.map(orders =>
this._sampler.getTokenDecimals([orders[0].order.makerToken, orders[0].order.takerToken]),
),
];
const executeResults = await this._sampler.executeBatchAsync(ops);
const batchOrderFillableMakerAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][];
const inputAmountPerEth = ZERO_AMOUNT;
return Promise.all(
batchNativeOrders.map(async (nativeOrders, i) => {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const { makerToken, takerToken } = nativeOrders[0].order;
const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i];
const outputAmountPerEth = batchEthToTakerAssetRate[i];
const dexQuotes = batchDexQuotes[i];
const makerAmount = makerAmounts[i];
try {
const optimizerResult = await this._generateOptimizedOrdersAsync(
{
side: MarketOperation.Buy,
inputToken: makerToken,
outputToken: takerToken,
inputAmount: makerAmount,
outputAmountPerEth,
inputAmountPerEth,
quoteSourceFilters,
makerTokenDecimals: batchTokenDecimals[i][0],
takerTokenDecimals: batchTokenDecimals[i][1],
quotes: {
nativeOrders: nativeOrders.map((o, k) => ({
...o,
...getNativeAdjustedFillableAmountsFromMakerAmount(o, orderFillableMakerAmounts[k]),
})),
dexQuotes,
rfqtIndicativeQuotes: [],
twoHopQuotes: [],
},
isRfqSupported: false,
},
{
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback,
gasPrice: _opts.gasPrice,
},
);
return optimizerResult;
} catch (e) {
// It's possible for one of the pairs to have no path
// rather than throw NO_OPTIMAL_PATH we return undefined
return undefined;
}
}),
);
throw new Error(`Not implemented`);
// if (batchNativeOrders.length === 0) {
// throw new Error(AggregationError.EmptyOrders);
// }
// const _opts: GetMarketOrdersOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
//
// const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
// const quoteSourceFilters = this._buySources.merge(requestFilters);
//
// const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
//
// const ops = [
// ...batchNativeOrders.map(orders =>
// this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy),
// ),
// ...batchNativeOrders.map(orders =>
// this._sampler.getMedianSellRate(
// feeSourceFilters.sources,
// orders[0].order.takerToken,
// this._nativeFeeToken,
// this._nativeFeeTokenAmount,
// ),
// ),
// ...batchNativeOrders.map((orders, i) =>
// this._sampler.getBuyQuotes(
// quoteSourceFilters.sources,
// orders[0].order.makerToken,
// orders[0].order.takerToken,
// [makerAmounts[i]],
// ),
// ),
// ...batchNativeOrders.map(orders =>
// this._sampler.getTokenDecimals([orders[0].order.makerToken, orders[0].order.takerToken]),
// ),
// ];
//
// const executeResults = await this._sampler.executeBatchAsync(ops);
// const batchOrderFillableMakerAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
// const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
// const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
// const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][];
// const inputAmountPerEth = ZERO_AMOUNT;
//
// return Promise.all(
// batchNativeOrders.map(async (nativeOrders, i) => {
// if (nativeOrders.length === 0) {
// throw new Error(AggregationError.EmptyOrders);
// }
// const { makerToken, takerToken } = nativeOrders[0].order;
// const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i];
// const outputAmountPerEth = batchEthToTakerAssetRate[i];
// const dexQuotes = batchDexQuotes[i];
// const makerAmount = makerAmounts[i];
// try {
// const optimizerResult = await this._generateOptimizedOrdersAsync(
// {
// side: MarketOperation.Buy,
// inputToken: makerToken,
// outputToken: takerToken,
// inputAmount: makerAmount,
// outputAmountPerEth,
// inputAmountPerEth,
// quoteSourceFilters,
// makerTokenDecimals: batchTokenDecimals[i][0],
// takerTokenDecimals: batchTokenDecimals[i][1],
// quotes: {
// nativeOrders: nativeOrders.map((o, k) => ({
// ...o,
// ...getNativeAdjustedFillableAmountsFromMakerAmount(o, orderFillableMakerAmounts[k]),
// })),
// dexQuotes,
// rfqtIndicativeQuotes: [],
// twoHopQuotes: [],
// },
// isRfqSupported: false,
// },
// {
// bridgeSlippage: _opts.bridgeSlippage,
// maxFallbackSlippage: _opts.maxFallbackSlippage,
// excludedSources: _opts.excludedSources,
// feeSchedule: _opts.feeSchedule,
// allowFallback: _opts.allowFallback,
// gasPrice: _opts.gasPrice,
// },
// );
// return optimizerResult;
// } catch (e) {
// // It's possible for one of the pairs to have no path
// // rather than throw NO_OPTIMAL_PATH we return undefined
// return undefined;
// }
// }),
// );
}
public async _generateOptimizedOrdersAsync(
@@ -515,7 +483,7 @@ export class MarketOperationUtils {
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts);
return {
optimizedOrders: twoHopOrders,
liquidityDelivered: bestTwoHopQuote,
// liquidityDelivered: bestTwoHopQuote,
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
marketSideLiquidity,
adjustedRate: bestTwoHopRate,
@@ -536,7 +504,7 @@ export class MarketOperationUtils {
return {
optimizedOrders: collapsedPath.orders,
liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[],
// liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[],
sourceFlags: collapsedPath.sourceFlags,
marketSideLiquidity,
adjustedRate: optimalPathRate,
@@ -713,17 +681,6 @@ export class MarketOperationUtils {
return { ...optimizerResult, quoteReport, priceComparisonsReport };
}
private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise<void> {
void Promise.all(
Object.values(this._sampler.poolsCaches).map(async cache => {
if (cache.isFresh(takerToken, makerToken)) {
return Promise.resolve([]);
}
return cache.getFreshPoolsForPairAsync(takerToken, makerToken);
}),
);
}
// tslint:disable-next-line: prefer-function-over-method
private async _addOptionalFallbackAsync(
side: MarketOperation,

View File

@@ -38,41 +38,42 @@ export function getBestTwoHopQuote(
marketSideLiquidity: Omit<MarketSideLiquidity, 'makerTokenDecimals' | 'takerTokenDecimals'>,
feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity;
const { twoHopQuotes } = quotes;
// Ensure the expected data we require exists. In the case where all hops reverted
// or there were no sources included that allowed for multi hop,
// we can end up with empty, but not undefined, fill data
const filteredQuotes = twoHopQuotes.filter(
quote =>
quote &&
quote.fillData &&
quote.fillData.firstHopSource &&
quote.fillData.secondHopSource &&
quote.output.isGreaterThan(ZERO_AMOUNT),
);
if (filteredQuotes.length === 0) {
return { quote: undefined, adjustedRate: ZERO_AMOUNT };
}
const best = filteredQuotes
.map(quote =>
getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead),
)
.reduce(
(prev, curr, i) =>
curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: filteredQuotes[i] } : prev,
{
adjustedRate: getTwoHopAdjustedRate(
side,
filteredQuotes[0],
inputAmount,
outputAmountPerEth,
feeSchedule,
exchangeProxyOverhead,
),
quote: filteredQuotes[0],
},
);
return best;
): { quote: DexSample | undefined; adjustedRate: BigNumber } {
throw new Error(`No implementado`);
// const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity;
// const { twoHopQuotes } = quotes;
// // Ensure the expected data we require exists. In the case where all hops reverted
// // or there were no sources included that allowed for multi hop,
// // we can end up with empty, but not undefined, fill data
// const filteredQuotes = twoHopQuotes.filter(
// quote =>
// quote &&
// quote.fillData &&
// quote.fillData.firstHopSource &&
// quote.fillData.secondHopSource &&
// quote.output.isGreaterThan(ZERO_AMOUNT),
// );
// if (filteredQuotes.length === 0) {
// return { quote: undefined, adjustedRate: ZERO_AMOUNT };
// }
// const best = filteredQuotes
// .map(quote =>
// getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead),
// )
// .reduce(
// (prev, curr, i) =>
// curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: filteredQuotes[i] } : prev,
// {
// adjustedRate: getTwoHopAdjustedRate(
// side,
// filteredQuotes[0],
// inputAmount,
// outputAmountPerEth,
// feeSchedule,
// exchangeProxyOverhead,
// ),
// quote: filteredQuotes[0],
// },
// );
// return best;
}

View File

@@ -48,33 +48,34 @@ export interface CreateOrderFromPathOpts {
}
export function createOrdersFromTwoHopSample(
sample: DexSample<MultiHopFillData>,
sample: DexSample,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData;
const firstHopFill: CollapsedFill = {
sourcePathId: '',
source: firstHopSource.source,
type: FillQuoteTransformerOrderType.Bridge,
input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT,
output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
subFills: [],
fillData: firstHopSource.fillData,
};
const secondHopFill: CollapsedFill = {
sourcePathId: '',
source: secondHopSource.source,
type: FillQuoteTransformerOrderType.Bridge,
input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input,
output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
subFills: [],
fillData: secondHopSource.fillData,
};
return [
createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side),
createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts.side),
];
throw new Error(`Not implemented`);
// const [makerToken, takerToken] = getMakerTakerTokens(opts);
// const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData;
// const firstHopFill: CollapsedFill = {
// sourcePathId: '',
// source: firstHopSource.source,
// type: FillQuoteTransformerOrderType.Bridge,
// input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT,
// output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
// subFills: [],
// fillData: firstHopSource.fillData,
// };
// const secondHopFill: CollapsedFill = {
// sourcePathId: '',
// source: secondHopSource.source,
// type: FillQuoteTransformerOrderType.Bridge,
// input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input,
// output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
// subFills: [],
// fillData: secondHopSource.fillData,
// };
// return [
// createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side),
// createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts.side),
// ];
}
export function getErc20BridgeSourceToBridgeSource(source: ERC20BridgeSource): string {
@@ -348,7 +349,7 @@ export function createBridgeOrder(
takerToken,
makerAmount,
takerAmount,
fillData: createFinalBridgeOrderFillDataFromCollapsedFill(fill),
fillData: fill.encodedFillData,
source: fill.source,
sourcePathId: fill.sourcePathId,
type: FillQuoteTransformerOrderType.Bridge,
@@ -356,36 +357,6 @@ export function createBridgeOrder(
};
}
function createFinalBridgeOrderFillDataFromCollapsedFill(fill: CollapsedFill): FillData {
switch (fill.source) {
case ERC20BridgeSource.UniswapV3: {
const fd = fill.fillData as UniswapV3FillData;
return {
router: fd.router,
tokenAddressPath: fd.tokenAddressPath,
uniswapPath: getBestUniswapV3PathForInputAmount(fd, fill.input),
};
}
default:
break;
}
return fill.fillData;
}
function getBestUniswapV3PathForInputAmount(fillData: UniswapV3FillData, inputAmount: BigNumber): string {
if (fillData.pathAmounts.length === 0) {
throw new Error(`No Uniswap V3 paths`);
}
// Find the best path that can satisfy `inputAmount`.
// Assumes `fillData.pathAmounts` is sorted ascending.
for (const { inputAmount: pathInputAmount, uniswapPath } of fillData.pathAmounts) {
if (pathInputAmount.gte(inputAmount)) {
return uniswapPath;
}
}
return fillData.pathAmounts[fillData.pathAmounts.length - 1].uniswapPath;
}
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
@@ -506,19 +477,20 @@ export function createNativeOptimizedOrder(
fill: NativeCollapsedFill,
side: MarketOperation,
): OptimizedMarketOrderBase<NativeLimitOrderFillData> | OptimizedMarketOrderBase<NativeRfqOrderFillData> {
const fillData = fill.fillData;
const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side);
const base = {
type: fill.type,
source: ERC20BridgeSource.Native,
makerToken: fillData.order.makerToken,
takerToken: fillData.order.takerToken,
makerAmount,
takerAmount,
fills: [fill],
fillData,
};
return fill.type === FillQuoteTransformerOrderType.Rfq
? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData }
: { ...base, type: FillQuoteTransformerOrderType.Limit, fillData: fillData as NativeLimitOrderFillData };
throw new Error(`No implementado`);
// const fillData = fill.fillData;
// const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side);
// const base = {
// type: fill.type,
// source: ERC20BridgeSource.Native,
// makerToken: fillData.order.makerToken,
// takerToken: fillData.order.takerToken,
// makerAmount,
// takerAmount,
// fills: [fill],
// fillData,
// };
// return fill.type === FillQuoteTransformerOrderType.Rfq
// ? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData }
// : { ...base, type: FillQuoteTransformerOrderType.Limit, fillData: fillData as NativeLimitOrderFillData };
}

View File

@@ -257,7 +257,7 @@ export class Path {
if (prevFill.sourcePathId === fill.sourcePathId) {
prevFill.input = prevFill.input.plus(fill.input);
prevFill.output = prevFill.output.plus(fill.output);
prevFill.fillData = fill.fillData;
prevFill.encodedFillData = fill.encodedFillData;
prevFill.subFills.push(fill);
continue;
}
@@ -266,7 +266,7 @@ export class Path {
sourcePathId: fill.sourcePathId,
source: fill.source,
type: fill.type,
fillData: fill.fillData,
encodedFillData: fill.encodedFillData,
input: fill.input,
output: fill.output,
subFills: [fill],

View File

@@ -45,8 +45,8 @@ function calculateOuputFee(
fees: FeeSchedule,
): BigNumber {
if (isDexSample(sampleOrNativeOrder)) {
const { input, output, source, fillData } = sampleOrNativeOrder;
const fee = fees[source]?.(fillData) || 0;
const { input, output, source, encodedFillData } = sampleOrNativeOrder;
const fee = fees[source]?.(encodedFillData) || 0;
const outputFee = ethToOutputAmount({
input,
output,
@@ -259,7 +259,7 @@ function findRoutesAndCreateOptimalPath(
// NOTE: For DexSamples only
let fill = createFill(current);
const routeSamples = routeSamplesAndNativeOrders as Array<DexSample<FillData>>;
const routeSamples = routeSamplesAndNativeOrders as Array<DexSample>;
// Descend to approach a closer fill for fillData which may not be consistent
// throughout the path (UniswapV3) and for a closer guesstimate at
// gas used

View File

@@ -13,25 +13,26 @@ import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, Multi
*/
export function getTwoHopAdjustedRate(
side: MarketOperation,
twoHopQuote: DexSample<MultiHopFillData>,
twoHopQuote: DexSample,
targetInput: BigNumber,
outputAmountPerEth: BigNumber,
fees: FeeSchedule = {},
exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT,
): BigNumber {
const { output, input, fillData } = twoHopQuote;
if (input.isLessThan(targetInput) || output.isZero()) {
return ZERO_AMOUNT;
}
const penalty = outputAmountPerEth.times(
exchangeProxyOverhead(
SOURCE_FLAGS.MultiHop |
SOURCE_FLAGS[fillData.firstHopSource.source] |
SOURCE_FLAGS[fillData.secondHopSource.source],
).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
);
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
throw new Error(`Not implemented`);
// const { output, input, fillData } = twoHopQuote;
// if (input.isLessThan(targetInput) || output.isZero()) {
// return ZERO_AMOUNT;
// }
// const penalty = outputAmountPerEth.times(
// exchangeProxyOverhead(
// SOURCE_FLAGS.MultiHop |
// SOURCE_FLAGS[fillData.firstHopSource.source] |
// SOURCE_FLAGS[fillData.secondHopSource.source],
// ).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
// );
// const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
// return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
}
/**

View File

@@ -1,167 +1,86 @@
import { ChainId } from '@0x/contract-addresses';
import { BigNumber, NULL_BYTES } from '@0x/utils';
import { BigNumber } from '@0x/utils';
import { SamplerOverrides } from '../../types';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { Address } from '../../types';
import { BancorService } from './bancor_service';
import { PoolsCache } from './pools_cache';
import { SamplerOperations } from './sampler_operations';
import { BatchedOperation, ERC20BridgeSource, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types';
import { DexSample, ERC20BridgeSource, TokenAdjacencyGraph } from './types';
import { SamplerServiceRpcClient } from './sampler_service_rpc_client';
/**
* Generate sample amounts up to `maxFillAmount`.
*/
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 stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution)));
const amounts = stepSizes.map((_s, i) => {
if (i === numSamples - 1) {
return maxFillAmount;
}
return maxFillAmount
.times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)]))
.integerValue(BigNumber.ROUND_UP);
});
return amounts;
interface TokenInfo {
decimals: number;
address: Address;
gasCost: number;
symbol: string;
}
type BatchedOperationResult<T> = T extends BatchedOperation<infer TResult> ? TResult : never;
export interface Sampler {
chainId: ChainId;
getTokenInfosAsync(tokens: Address[]): Promise<TokenInfo[]>;
getPricesAsync(paths: Address[][], sources: ERC20BridgeSource[]): Promise<BigNumber[]>;
getSellLiquidityAsync(path: Address[], takerAmount: BigNumber, sources: ERC20BridgeSource[]): Promise<DexSample[][]>;
}
/**
* Encapsulates interactions with the `ERC20BridgeSampler` contract.
*/
export class DexOrderSampler extends SamplerOperations {
constructor(
public readonly chainId: ChainId,
_samplerContract: ERC20BridgeSamplerContract,
private readonly _samplerOverrides?: SamplerOverrides,
poolsCaches?: { [key in ERC20BridgeSource]: PoolsCache },
tokenAdjacencyGraph?: TokenAdjacencyGraph,
liquidityProviderRegistry?: LiquidityProviderRegistry,
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
) {
super(chainId, _samplerContract, poolsCaches, tokenAdjacencyGraph, liquidityProviderRegistry, bancorServiceFn);
export class SamplerClient implements Sampler {
static createFromChainIdAndEndpoint(chainId: ChainId, endpoint: string): SamplerClient {
return new SamplerClient(chainId, new SamplerServiceRpcClient(endpoint));
}
/* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */
// prettier-ignore
public async executeAsync<
T1
>(...ops: [T1]): Promise<[
BatchedOperationResult<T1>
]>;
// prettier-ignore
public async executeAsync<
T1, T2
>(...ops: [T1, T2]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3
>(...ops: [T1, T2, T3]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4
>(...ops: [T1, T2, T3, T4]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5
>(...ops: [T1, T2, T3, T4, T5]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5, T6
>(...ops: [T1, T2, T3, T4, T5, T6]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>,
BatchedOperationResult<T6>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5, T6, T7
>(...ops: [T1, T2, T3, T4, T5, T6, T7]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>,
BatchedOperationResult<T6>,
BatchedOperationResult<T7>
]>;
// prettier-ignore
public async executeAsync<
T1, T2, T3, T4, T5, T6, T7, T8
>(...ops: [T1, T2, T3, T4, T5, T6, T7, T8]): Promise<[
BatchedOperationResult<T1>,
BatchedOperationResult<T2>,
BatchedOperationResult<T3>,
BatchedOperationResult<T4>,
BatchedOperationResult<T5>,
BatchedOperationResult<T6>,
BatchedOperationResult<T7>,
BatchedOperationResult<T8>
]>;
/**
* Run a series of operations from `DexOrderSampler.ops` in a single transaction.
*/
public async executeAsync(...ops: any[]): Promise<any[]> {
return this.executeBatchAsync(ops);
static async createFromEndpointAsync(endpoint: string): Promise<SamplerClient> {
const service = new SamplerServiceRpcClient(endpoint);
const chainId = await service.getChainIdAsync();
return new SamplerClient(
chainId,
service,
);
}
/**
* Run a series of operations from `DexOrderSampler.ops` in a single transaction.
* Takes an arbitrary length array, but is not typesafe.
*/
public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> {
const callDatas = ops.map(o => o.encodeCall());
const { overrides, block } = this._samplerOverrides
? this._samplerOverrides
: { overrides: undefined, block: undefined };
private constructor(
private readonly _chainId: number,
private readonly _service: SamplerServiceRpcClient,
) {}
// All operations are NOOPs
if (callDatas.every(cd => cd === NULL_BYTES)) {
return callDatas.map((_callData, i) => ops[i].handleCallResults(NULL_BYTES));
}
// Execute all non-empty calldatas.
const rawCallResults = await this._samplerContract
.batchCall(callDatas.filter(cd => cd !== NULL_BYTES))
.callAsync({ overrides }, block);
// Return the parsed results.
let rawCallResultsIdx = 0;
return callDatas.map((callData, i) => {
// tslint:disable-next-line:boolean-naming
const { data, success } =
callData !== NULL_BYTES ? rawCallResults[rawCallResultsIdx++] : { success: true, data: NULL_BYTES };
return success ? ops[i].handleCallResults(data) : ops[i].handleRevert(data);
});
public get chainId(): ChainId {
return this._chainId;
}
public async getPricesAsync(
paths: Address[][],
sources: ERC20BridgeSource[],
): Promise<BigNumber[]> {
return this._service.getPricesAsync(paths.map(p => ({
tokenPath: p,
demand: true,
sources,
})));
}
public async getTokenInfosAsync(tokens: Address[]): Promise<TokenInfo[]> {
return this._service.getTokensAsync(tokens);
}
public async getSellLiquidityAsync(
path: Address[],
takerAmount: BigNumber,
sources: ERC20BridgeSource[],
): Promise<DexSample[][]> {
const liquidity = await this._service.getSellLiquidityAsync(
sources.map(s => ({
tokenPath: path,
inputAmount: takerAmount,
source: s,
demand: true,
})),
);
return liquidity.map(
liq => liq.liquidityCurves.map(
pts =>
pts.map(pt => ({
input: pt.sellAmount,
output: pt.buyAmount,
encodedFillData: pt.encodedFillData,
gasCost: pt.gasCost,
source: liq.source,
}) as DexSample),
)).flat(1);
}
}

View File

@@ -0,0 +1,128 @@
import { BigNumber } from '@0x/utils';
import { Client as OpenRpcClient, HTTPTransport, RequestManager } from '@open-rpc/client-js';
import { Address, Bytes } from '../../types';
type DecimalString = string;
export interface LiquidityCurvePoint {
sellAmount: BigNumber;
buyAmount: BigNumber;
encodedFillData: Bytes;
gasCost: number;
}
type RpcLiquidityCurvePoint = Omit<Omit<LiquidityCurvePoint, 'sellAmount'>, 'buyAmount'> & {
sellAmount: DecimalString;
buyAmount: DecimalString;
}
export interface LiquidityRequest {
tokenPath: Address[];
inputAmount: BigNumber;
source: string;
demand?: boolean;
}
type RpcLiquidityRequest = Omit<LiquidityRequest, 'inputAmount'> & {
inputAmount: string;
}
export interface PriceRequest {
tokenPath: Address[];
sources?: string[];
demand?: boolean;
}
type RpcPriceRequest = PriceRequest;
export interface LiquidityResponse {
source: string;
liquidityCurves: LiquidityCurvePoint[][];
}
type RpcLiquidityResponse = & Omit<LiquidityResponse, 'liquidityCurves'> & {
source: string;
liquidityCurves: RpcLiquidityCurvePoint[][];
}
export interface TokenResponse {
address: Address;
symbol: string;
decimals: number;
gasCost: number;
}
type RpcTokenResponse = TokenResponse;
export class SamplerServiceRpcClient {
private _rpcClient: OpenRpcClient;
public constructor(url: string) {
const transport = new HTTPTransport(url);
this._rpcClient = new OpenRpcClient(new RequestManager([transport]));
}
private async _requestAsync<TResult, TArgs = any>(method: string, params: TArgs[] = []): Promise<TResult> {
return this._rpcClient.request({ method, params }) as Promise<TResult>;
}
public async getChainIdAsync(): Promise<number> {
return this._requestAsync<number>('get_chain_id');
}
public async getSellLiquidityAsync(reqs: LiquidityRequest[]): Promise<LiquidityResponse[]> {
const resp = await this._requestAsync<RpcLiquidityResponse[], RpcLiquidityRequest[]>(
'get_sell_liquidity',
[
reqs.map(r => ({
...r,
inputAmount: r.inputAmount.toString(10),
})),
],
);
return resp.map(r => ({
...r,
liquidityCurves: r.liquidityCurves.map(a => a.map(c => ({
...c,
buyAmount: new BigNumber(c.buyAmount),
sellAmount: new BigNumber(c.sellAmount),
}))),
}));
}
public async getBuyLiquidityAsync(reqs: LiquidityRequest[]): Promise<LiquidityResponse[]> {
const resp = await this._requestAsync<RpcLiquidityResponse[], RpcLiquidityRequest[]>(
'get_buy_liquidity',
[
reqs.map(r => ({
...r,
inputAmount: r.inputAmount.toString(10),
})),
],
);
return resp.map(r => ({
...r,
liquidityCurves: r.liquidityCurves.map(a => a.map(c => ({
...c,
buyAmount: new BigNumber(c.buyAmount),
sellAmount: new BigNumber(c.sellAmount),
}))),
}));
}
public async getPricesAsync(reqs: PriceRequest[]): Promise<BigNumber[]> {
const resp = await this._requestAsync<DecimalString[], RpcPriceRequest[]>(
'get_prices',
[ reqs ],
);
return resp.map(r => new BigNumber(r));
}
public async getTokensAsync(addresses: Address[]): Promise<TokenResponse[]> {
return this._requestAsync<RpcTokenResponse[], Address[]>(
'get_tokens',
[ addresses ],
);
}
}

View File

@@ -7,7 +7,7 @@ import { V4RFQIndicativeQuote } from '@0x/quote-server';
import { MarketOperation } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { NativeOrderWithFillableAmounts, RfqFirmQuoteValidator, RfqRequestOpts } from '../../types';
import { Bytes, NativeOrderWithFillableAmounts, RfqFirmQuoteValidator, RfqRequestOpts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor';
import { PriceComparisonsReport, QuoteReport } from '../quote_report_generator';
@@ -172,12 +172,14 @@ export type NativeLimitOrderFillData = FillQuoteTransformerLimitOrderInfo;
export type NativeFillData = NativeRfqOrderFillData | NativeLimitOrderFillData;
// Represents an individual DEX sample from the sampler contract
export interface DexSample<TFillData extends FillData = FillData> {
export interface DexSample {
source: ERC20BridgeSource;
fillData: TFillData;
encodedFillData: Bytes;
input: BigNumber;
output: BigNumber;
gasCost: number;
}
export interface CurveFillData extends FillData {
fromTokenIdx: number;
toTokenIdx: number;
@@ -273,12 +275,12 @@ export interface LidoFillData extends FillData {
/**
* Represents a node on a fill path.
*/
export interface Fill<TFillData extends FillData = FillData> {
export interface Fill {
// basic data for every fill
source: ERC20BridgeSource;
// TODO jacob people seem to agree that orderType here is more readable
type: FillQuoteTransformerOrderType; // should correspond with TFillData
fillData: TFillData;
encodedFillData: Bytes;
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
@@ -300,10 +302,10 @@ export interface Fill<TFillData extends FillData = FillData> {
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill<TFillData extends FillData = FillData> {
export interface CollapsedFill {
source: ERC20BridgeSource;
type: FillQuoteTransformerOrderType; // should correspond with TFillData
fillData: TFillData;
encodedFillData: Bytes;
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
@@ -328,7 +330,7 @@ export interface CollapsedFill<TFillData extends FillData = FillData> {
/**
* A `CollapsedFill` wrapping a native order.
*/
export interface NativeCollapsedFill extends CollapsedFill<NativeFillData> {}
export interface NativeCollapsedFill extends CollapsedFill {}
export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
@@ -481,7 +483,7 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData> ext
export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[];
sourceFlags: bigint;
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
// liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
marketSideLiquidity: MarketSideLiquidity;
adjustedRate: BigNumber;
unoptimizedPath?: CollapsedPath;
@@ -494,7 +496,7 @@ export interface OptimizerResultWithReport extends OptimizerResult {
priceComparisonsReport?: PriceComparisonsReport;
}
export type MarketDepthSide = Array<Array<DexSample<FillData>>>;
export type MarketDepthSide = Array<Array<DexSample>>;
export interface MarketDepth {
bids: MarketDepthSide;
@@ -520,8 +522,8 @@ export interface MarketSideLiquidity {
export interface RawQuotes {
nativeOrders: NativeOrderWithFillableAmounts[];
rfqtIndicativeQuotes: V4RFQIndicativeQuote[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>;
dexQuotes: Array<Array<DexSample<FillData>>>;
// twoHopQuotes: Array<DexSample<MultiHopFillData>>;
dexQuotes: Array<Array<DexSample>>;
}
export interface TokenAdjacencyGraph {

View File

@@ -1,5 +1,6 @@
import { BigNumber } from '@0x/utils';
import * as heartbeats from 'heartbeats';
import fetch from 'axios';
import { constants } from '../constants';
import { SwapQuoterError } from '../types';
@@ -61,7 +62,7 @@ export class ProtocolFeeUtils {
private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> {
try {
const res = await fetch(this._ethGasStationUrl);
const gasInfo = await res.json();
const gasInfo = res.data;
// Eth Gas Station result is gwei * 10
// tslint:disable-next-line:custom-no-magic-numbers
const BASE_TEN = 10;

View File

@@ -73,47 +73,48 @@ export interface PriceComparisonsReport {
export function generateQuoteReport(
marketOperation: MarketOperation,
nativeOrders: NativeOrderWithFillableAmounts[],
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
// liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): QuoteReport {
const nativeOrderSourcesConsidered = nativeOrders.map(order =>
nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor),
);
const sourcesConsidered = [...nativeOrderSourcesConsidered.filter(order => order.isRfqt)];
let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) {
// create easy way to look up fillable amounts
const nativeOrderSignaturesToFillableAmounts = _.fromPairs(
nativeOrders.map(o => {
return [_nativeDataToId(o), o.fillableTakerAmount];
}),
);
// map sources delivered
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
if (_isNativeOrderFromCollapsedFill(collapsedFill)) {
return nativeOrderToReportEntry(
collapsedFill.type,
collapsedFill.fillData,
nativeOrderSignaturesToFillableAmounts[_nativeDataToId(collapsedFill.fillData)],
comparisonPrice,
quoteRequestor,
);
} else {
return dexSampleToReportSource(collapsedFill, marketOperation);
}
});
} else {
sourcesDelivered = [
// tslint:disable-next-line: no-unnecessary-type-assertion
multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
];
}
return {
sourcesConsidered,
sourcesDelivered,
};
throw new Error(`Not implemented`);
// const nativeOrderSourcesConsidered = nativeOrders.map(order =>
// nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor),
// );
// const sourcesConsidered = [...nativeOrderSourcesConsidered.filter(order => order.isRfqt)];
//
// let sourcesDelivered;
// if (Array.isArray(liquidityDelivered)) {
// // create easy way to look up fillable amounts
// const nativeOrderSignaturesToFillableAmounts = _.fromPairs(
// nativeOrders.map(o => {
// return [_nativeDataToId(o), o.fillableTakerAmount];
// }),
// );
// // map sources delivered
// sourcesDelivered = liquidityDelivered.map(collapsedFill => {
// if (_isNativeOrderFromCollapsedFill(collapsedFill)) {
// return nativeOrderToReportEntry(
// collapsedFill.type,
// collapsedFill.fillData,
// nativeOrderSignaturesToFillableAmounts[_nativeDataToId(collapsedFill.fillData)],
// comparisonPrice,
// quoteRequestor,
// );
// } else {
// return dexSampleToReportSource(collapsedFill, marketOperation);
// }
// });
// } else {
// sourcesDelivered = [
// // tslint:disable-next-line: no-unnecessary-type-assertion
// multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
// ];
// }
// return {
// sourcesConsidered,
// sourcesDelivered,
// };
}
function _nativeDataToId(data: { signature: Signature }): string {
@@ -126,31 +127,32 @@ function _nativeDataToId(data: { signature: Signature }): string {
* NOTE: this is used for the QuoteReport and quote price comparison data
*/
export function dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeQuoteReportEntry {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
throw new Error(`Unexpected liquidity source Native`);
}
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return {
makerAmount: ds.input,
takerAmount: ds.output,
liquiditySource,
fillData: ds.fillData,
};
} else if (marketOperation === MarketOperation.Sell) {
return {
makerAmount: ds.output,
takerAmount: ds.input,
liquiditySource,
fillData: ds.fillData,
};
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
throw new Error(`Not implemented`);
// const liquiditySource = ds.source;
//
// if (liquiditySource === ERC20BridgeSource.Native) {
// throw new Error(`Unexpected liquidity source Native`);
// }
//
// // input and output map to different values
// // based on the market operation
// if (marketOperation === MarketOperation.Buy) {
// return {
// makerAmount: ds.input,
// takerAmount: ds.output,
// liquiditySource,
// fillData: ds.fillData,
// };
// } else if (marketOperation === MarketOperation.Sell) {
// return {
// makerAmount: ds.output,
// takerAmount: ds.input,
// liquiditySource,
// fillData: ds.fillData,
// };
// } else {
// throw new Error(`Unexpected marketOperation ${marketOperation}`);
// }
}
/**
@@ -158,31 +160,32 @@ export function dexSampleToReportSource(ds: DexSample, marketOperation: MarketOp
* NOTE: this is used for the QuoteReport and quote price comparison data
*/
export function multiHopSampleToReportSource(
ds: DexSample<MultiHopFillData>,
ds: DexSample,
marketOperation: MarketOperation,
): MultiHopQuoteReportEntry {
const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData;
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.input,
takerAmount: ds.output,
fillData: ds.fillData,
hopSources: [firstHop.source, secondHop.source],
};
} else if (marketOperation === MarketOperation.Sell) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.output,
takerAmount: ds.input,
fillData: ds.fillData,
hopSources: [firstHop.source, secondHop.source],
};
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
throw new Error(`Not implemented`);
// const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData;
// // input and output map to different values
// // based on the market operation
// if (marketOperation === MarketOperation.Buy) {
// return {
// liquiditySource: ERC20BridgeSource.MultiHop,
// makerAmount: ds.input,
// takerAmount: ds.output,
// fillData: ds.fillData,
// hopSources: [firstHop.source, secondHop.source],
// };
// } else if (marketOperation === MarketOperation.Sell) {
// return {
// liquiditySource: ERC20BridgeSource.MultiHop,
// makerAmount: ds.output,
// takerAmount: ds.input,
// fillData: ds.fillData,
// hopSources: [firstHop.source, secondHop.source],
// };
// } else {
// throw new Error(`Unexpected marketOperation ${marketOperation}`);
// }
}
function _isNativeOrderFromCollapsedFill(cf: CollapsedFill): cf is NativeCollapsedFill {

View File

@@ -282,7 +282,7 @@ export class QuoteRequestor {
private readonly _altRfqCreds?: { altRfqApiKey: string; altRfqProfile: string },
private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER,
private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER,
private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs,
private readonly _expiryBufferMs: number = 120e3,
private readonly _metrics?: MetricsProxy,
) {
rfqMakerBlacklist.infoLogger = this._infoLogger;

View File

@@ -155,8 +155,8 @@ export function fillQuoteOrders(
if (remainingInput.lte(0)) {
break;
}
const { source, fillData } = fill;
const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData);
const { source, encodedFillData } = fill;
const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(encodedFillData);
result.gas += new BigNumber(gas).toNumber();
result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT;

View File

@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true, "lib": ["es2019"] },
"include": ["./src/**/*", "./generated-wrappers/**/*"],
"files": [
"generated-artifacts/BalanceChecker.json",
"generated-artifacts/ERC20BridgeSampler.json",

View File

@@ -2607,6 +2607,16 @@
dependencies:
"@types/node" ">= 8"
"@open-rpc/client-js@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@open-rpc/client-js/-/client-js-1.7.1.tgz#763d75c046a40f57428b861e16a9a69aaa630cb1"
integrity sha512-DycSYZUGSUwFl+k9T8wLBSGA8f2hYkvS5A9AB94tBOuU8QlP468NS5ZtAxy72dF4g2WW0genwNJdfeFnHnaxXQ==
dependencies:
isomorphic-fetch "^3.0.0"
isomorphic-ws "^4.0.1"
strict-event-emitter-types "^2.0.0"
ws "^7.0.0"
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -7969,6 +7979,11 @@ isomorphic-fetch@^3.0.0:
node-fetch "^2.6.1"
whatwg-fetch "^3.4.1"
isomorphic-ws@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
isstream@0.1.x, isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -11657,6 +11672,11 @@ stream-to-pull-stream@^1.7.1:
looper "^3.0.0"
pull-stream "^3.2.3"
strict-event-emitter-types@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@@ -13565,6 +13585,11 @@ ws@^5.1.1:
dependencies:
async-limiter "~1.0.0"
ws@^7.0.0:
version "7.5.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
wsrun@^5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/wsrun/-/wsrun-5.2.4.tgz#6eb6c3ccd3327721a8df073a5e3578fb0dea494e"