@0x/asset-swapper: hack together basic sampler service integration
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 ],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
25
yarn.lock
25
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user