Compare commits

...

4 Commits

Author SHA1 Message Date
Romain Butteaud
7f8a4101d5 rpc sellLiquidity wrapper async 2021-09-17 16:34:07 -07:00
Romain Butteaud
15b7cf3986 rewrite rpc sampler client 2021-09-17 13:32:26 -07:00
Romain Butteaud
5d4481f0f0 use jayson for JSONRPC 2021-09-15 17:02:16 -07:00
Romain Butteaud
2a9871d706 feat: WIP sampler service client 2021-09-14 11:35:00 -07:00
8 changed files with 238 additions and 624 deletions

View File

@@ -0,0 +1,40 @@
import { Client as RPCClient, JSONRPCVersionOneResponse } from 'jayson';
import {
LiquidityResponse,
RpcLiquidityRequest,
RPCSamplerCallback,
} from './utils/market_operation_utils/sampler_types';
const RPC_SAMPLER_SERVICE_URL = '';
const RPC_SAMPLER_SERVICE_PORT = 7002;
export class RpcSamplerClient {
private readonly _rpcUrl: string;
private readonly _rpcClient: RPCClient;
/**
* @param rpcUrl URL to the Sampler Service to which JSON RPC requests should be sent
*/
constructor() {
this._rpcUrl = RPC_SAMPLER_SERVICE_URL;
this._rpcClient = RPCClient.http({ port: RPC_SAMPLER_SERVICE_PORT });
}
public getSellLiquidity(reqs: RpcLiquidityRequest[], rpcSamplerCallback: RPCSamplerCallback): void {
try {
this._rpcClient.request(`get_sell_liquidity`, [reqs], (err: any, response: JSONRPCVersionOneResponse) => {
return rpcSamplerCallback(err, response.result);
});
} catch (err) {
throw new Error(`error with sampler service: ${err}`);
}
}
public async getSellLiquidityWrapperAsync(reqs: RpcLiquidityRequest[]): Promise<LiquidityResponse[]> {
return new Promise(resolve => {
this.getSellLiquidity(reqs, (err, liquidityResponses: LiquidityResponse[]) => {
return resolve(liquidityResponses);
});
});
}
}

View File

@@ -10,6 +10,7 @@ import * as _ from 'lodash';
import { artifacts } from './artifacts';
import { constants, INVALID_SIGNATURE, KEEP_ALIVE_TTL } from './constants';
import { RpcSamplerClient } from './rpc_sampler_client';
import {
AssetSwapperContractAddresses,
MarketBuySwapQuote,
@@ -143,6 +144,7 @@ export class SwapQuoter {
this._marketOperationUtils = new MarketOperationUtils(
new DexOrderSampler(
this.chainId,
new RpcSamplerClient(),
samplerContract,
samplerOverrides,
undefined, // pools caches for balancer and cream

View File

@@ -124,90 +124,90 @@ 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 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;
// const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
const sellQuotes = this._sampler.getCachedSellQuotesAsync(quoteSourceFilters.sources, makerToken, takerToken, takerAmount);
// 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),
);
// 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.getCachedSellQuotesAsync(quoteSourceFilters.sources, makerToken, takerToken, takerAmount),
// // 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);
// void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken);
const [
[
tokenDecimals,
orderFillableTakerAmounts,
outputAmountPerEth,
inputAmountPerEth,
// tokenDecimals,
// orderFillableTakerAmounts,
// outputAmountPerEth,
// inputAmountPerEth,
dexQuotes,
rawTwoHopQuotes,
isTxOriginContract,
],
] = await Promise.all([samplerPromise]);
// rawTwoHopQuotes,
// isTxOriginContract,
] = await Promise.all([sellQuotes]);
// 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 twoHopQuotes = rawTwoHopQuotes.filter(
// q => q && q.fillData && q.fillData.firstHopSource && q.fillData.secondHopSource,
// );
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
// const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
const isRfqSupported = !!(_opts.rfqt && !isTxOriginContract);
const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
...order,
...getNativeAdjustedFillableAmountsFromTakerAmount(order, orderFillableTakerAmounts[i]),
}));
// const isRfqSupported = !!(_opts.rfqt && !isTxOriginContract);
// const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
// ...order,
// ...getNativeAdjustedFillableAmountsFromTakerAmount(order, orderFillableTakerAmounts[i]),
// }));
return {
side: MarketOperation.Sell,
inputAmount: takerAmount,
inputToken: takerToken,
outputToken: makerToken,
outputAmountPerEth,
inputAmountPerEth,
outputAmountPerEth: new BigNumber(1),
inputAmountPerEth: new BigNumber(1),
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
makerTokenDecimals: 18,
takerTokenDecimals: 18,
quotes: {
nativeOrders: limitOrdersWithFillableAmounts,
nativeOrders: [],
rfqtIndicativeQuotes: [],
twoHopQuotes,
twoHopQuotes: [],
dexQuotes,
},
isRfqSupported,
isRfqSupported: true,
};
}

View File

@@ -1,5 +1,6 @@
import { ChainId } from '@0x/contract-addresses';
import { BigNumber, NULL_BYTES } from '@0x/utils';
import { RpcSamplerClient } from '../../rpc_sampler_client';
import { SamplerOverrides } from '../../types';
import { ERC20BridgeSamplerContract } from '../../wrappers';
@@ -34,6 +35,7 @@ type BatchedOperationResult<T> = T extends BatchedOperation<infer TResult> ? TRe
export class DexOrderSampler extends SamplerOperations {
constructor(
public readonly chainId: ChainId,
rpcSamplerClient: RpcSamplerClient,
_samplerContract: ERC20BridgeSamplerContract,
private readonly _samplerOverrides?: SamplerOverrides,
poolsCaches?: { [key in ERC20BridgeSource]: PoolsCache },
@@ -41,7 +43,7 @@ export class DexOrderSampler extends SamplerOperations {
liquidityProviderRegistry?: LiquidityProviderRegistry,
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
) {
super(chainId, _samplerContract, poolsCaches, tokenAdjacencyGraph, liquidityProviderRegistry, bancorServiceFn);
super(chainId, rpcSamplerClient, _samplerContract, poolsCaches, tokenAdjacencyGraph, liquidityProviderRegistry, bancorServiceFn);
}
/* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */

View File

@@ -3,6 +3,7 @@ import { LimitOrderFields } from '@0x/protocol-utils';
import { BigNumber, logUtils } from '@0x/utils';
import * as _ from 'lodash';
import { RpcSamplerClient } from '../../rpc_sampler_client';
import { SamplerCallResult, SignedNativeOrder } from '../../types';
import { ERC20BridgeSamplerContract } from '../../wrappers';
@@ -44,6 +45,7 @@ import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
import { getIntermediateTokens } from './multihop_utils';
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
import { SamplerContractOperation } from './sampler_contract_operation';
import { Address, LiquidityResponse, RpcLiquidityRequest } from './sampler_types';
import { SourceFilters } from './source_filters';
import {
BalancerFillData,
@@ -56,6 +58,7 @@ import {
DexSample,
DODOFillData,
ERC20BridgeSource,
FillData,
GenericRouterFillData,
HopInfo,
KyberDmmFillData,
@@ -95,6 +98,8 @@ export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSour
* Composable operations that can be batched in a single transaction,
* for use with `DexOrderSampler.executeAsync()`.
*/
export declare type JSONRPCQuoteCallback = (err: Error | null, dexQuotes: Array<Array<DexSample<FillData>>>) => void;
export class SamplerOperations {
public readonly liquidityProviderRegistry: LiquidityProviderRegistry;
public readonly poolsCaches: { [key in SourcesWithPoolsCache]: PoolsCache };
@@ -109,6 +114,7 @@ export class SamplerOperations {
constructor(
public readonly chainId: ChainId,
protected readonly rpcSamplerClient: RpcSamplerClient,
protected readonly _samplerContract: ERC20BridgeSamplerContract,
poolsCaches?: { [key in SourcesWithPoolsCache]: PoolsCache },
protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [] },
@@ -130,8 +136,101 @@ export class SamplerOperations {
bancorServiceFn()
.then(service => (this._bancorService = service))
.catch(/* do nothing */);
this.rpcSamplerClient = new RpcSamplerClient();
}
public async getTokenDecimalsAsync(tokens: Address[]): Promise<number[]> {
return [];
// return (await this.rpcSamplerClient.getTokensAsync(tokens)).map(t => t.decimals);
}
public async getCachedSellQuotesAsync(
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
takerAmount: BigNumber,
// callback: JSONRPCQuoteCallback,
): Promise<Array<Array<DexSample<FillData>>>> {
// ): Promise<void> {
const rpcLiquidityRequests: RpcLiquidityRequest[] = sources.map(source => {
return {
tokenPath: [makerToken, takerToken],
inputAmount: takerAmount.toString(),
source,
demand: true,
};
});
const liquidityResponses: LiquidityResponse[] = await this.rpcSamplerClient.getSellLiquidityWrapperAsync(rpcLiquidityRequests);
const dexQuotes: Array<Array<DexSample<FillData>>> = liquidityResponses.map((liquidityResponse: LiquidityResponse) => {
const dexSample: Array<DexSample<FillData>> = liquidityResponse.liquidityCurves.map((point, j) => {
const fillData: DexSample = {
source: liquidityResponse.source,
fillData: point[j].encodedFillData,
input: new BigNumber(point[j].sellAmount.toString()), // TODO Romain: prob a better way
output: new BigNumber(point[j].buyAmount.toString()),
// gasCost: new BigNumber(point[j].gasCost.toString()),
};
return fillData;
});
return dexSample;
});
return dexQuotes;
}
public async getBuyQuotesAsync(
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
takerAmount: BigNumber,
): Promise<Array<Array<DexSample<FillData>>>> {
return [];
// const rpcLiquidityRequests: RpcLiquidityRequest[] = sources.map(source => {
// return {
// tokenPath: [makerToken, takerToken],
// inputAmount: takerAmount.toString(),
// source,
// demand: true,
// };
// });
// const rpcLiquidityResponse = await this.rpcSamplerClient.getBuyLiquidityAsync(rpcLiquidityRequests);
// const dexQuotes: Array<Array<DexSample<FillData>>> = rpcLiquidityResponse.map((response, i) => {
// const dexSample: Array<DexSample<FillData>> = response.liquidityCurve.map((point, j) => {
// const fillData: DexSample = {
// source: sources[i],
// fillData: point.encodedFillData,
// input: point.sellAmount,
// output: point.buyAmount,
// };
// return fillData;
// });
// return dexSample;
// });
// return dexQuotes;
}
public async getMedianSellRateAsync(
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
takerFillAmount: BigNumber,
): Promise<BigNumber> {
return new BigNumber(0);
// const samples = await this.getSellQuotesAsync(sources, makerToken, takerToken, takerFillAmount);
// if (samples.length === 0) {
// return ZERO_AMOUNT;
// }
// const flatSortedSamples = samples
// .reduce((acc, v) => acc.concat(...v))
// .filter(v => !v.output.isZero())
// .sort((a, b) => a.output.comparedTo(b.output));
// if (flatSortedSamples.length === 0) {
// return ZERO_AMOUNT;
// }
// const medianSample = flatSortedSamples[Math.floor(flatSortedSamples.length / 2)];
// return medianSample.output.div(takerFillAmount);
}
// Legacy
public getTokenDecimals(tokens: string[]): BatchedOperation<BigNumber[]> {
return new SamplerContractOperation({
source: ERC20BridgeSource.Native,

View File

@@ -0,0 +1,36 @@
import { BigNumber } from '@0x/utils';
import { ERC20BridgeSource } from './types';
export type Bytes = string;
export type Address = Bytes;
export type LiquiditySource = ERC20BridgeSource;
export declare type RPCSamplerCallback = (err: Error | null, liquidityResponses: LiquidityResponse[]) => void;
export interface RpcLiquidityRequest {
tokenPath: Address[];
inputAmount: string;
source: LiquiditySource;
demand: boolean;
}
export interface LiquidityCurvePoint {
sellAmount: bigint;
buyAmount: bigint;
encodedFillData: Bytes;
gasCost: number;
}
export interface LiquidityResponse {
source: LiquiditySource;
liquidityCurves: LiquidityCurvePoint[][];
}
export interface TokenResponse {
address: Address;
symbol: string;
decimals: number;
gasCost: number;
}

View File

@@ -12,6 +12,7 @@ import { QuoteRequestor } from '../../utils/quote_requestor';
import { PriceComparisonsReport, QuoteReport } from '../quote_report_generator';
import { CollapsedPath } from './path';
import { LiquiditySource } from './sampler_types';
import { SourceFilters } from './source_filters';
/**
@@ -175,6 +176,7 @@ export interface DexSample<TFillData extends FillData = FillData> {
fillData: TFillData;
input: BigNumber;
output: BigNumber;
// gasCost: BigNumber;
}
export interface CurveFillData extends FillData {
fromTokenIdx: number;

View File

@@ -1,567 +0,0 @@
import { ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import {
constants,
expect,
getRandomFloat,
getRandomInteger,
randomAddress,
toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { FillQuoteTransformerOrderType, LimitOrderFields, SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash';
import { SignedOrder } from '../src/types';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types';
import { MockSamplerContract } from './utils/mock_sampler_contract';
import { generatePseudoRandomSalt } from './utils/utils';
const CHAIN_ID = 1;
const EMPTY_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
// tslint:disable: custom-no-magic-numbers
describe('DexSampler tests', () => {
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const chainId = ChainId.Mainnet;
const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken;
const exchangeProxyAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchangeProxy;
const tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [wethAddress] };
describe('getSampleAmounts()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const NUM_SAMPLES = 16;
it('generates the correct number of amounts', () => {
const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
expect(amounts).to.be.length(NUM_SAMPLES);
});
it('first amount is nonzero', () => {
const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
expect(amounts[0]).to.not.bignumber.eq(0);
});
it('last amount is the fill amount', () => {
const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
expect(amounts[NUM_SAMPLES - 1]).to.bignumber.eq(FILL_AMOUNT);
});
it('can generate a single amount', () => {
const amounts = getSampleAmounts(FILL_AMOUNT, 1);
expect(amounts).to.be.length(1);
expect(amounts[0]).to.bignumber.eq(FILL_AMOUNT);
});
it('generates ascending amounts', () => {
const amounts = getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
for (const i of _.times(NUM_SAMPLES).slice(1)) {
const prev = amounts[i - 1];
const amount = amounts[i];
expect(prev).to.bignumber.lt(amount);
}
});
});
function createOrder(overrides?: Partial<LimitOrderFields>): SignedOrder<LimitOrderFields> {
const o: SignedOrder<LimitOrderFields> = {
order: {
salt: generatePseudoRandomSalt(),
expiry: getRandomInteger(0, 2 ** 64),
makerToken: MAKER_TOKEN,
takerToken: TAKER_TOKEN,
makerAmount: getRandomInteger(1, 1e18),
takerAmount: getRandomInteger(1, 1e18),
takerTokenFeeAmount: constants.ZERO_AMOUNT,
chainId: CHAIN_ID,
pool: EMPTY_BYTES32,
feeRecipient: NULL_ADDRESS,
sender: NULL_ADDRESS,
maker: NULL_ADDRESS,
taker: NULL_ADDRESS,
verifyingContract: exchangeProxyAddress,
...overrides,
},
signature: { v: 1, r: hexUtils.random(), s: hexUtils.random(), signatureType: SignatureType.EthSign },
type: FillQuoteTransformerOrderType.Limit,
};
return o;
}
const ORDERS = _.times(4, () => createOrder());
const SIMPLE_ORDERS = ORDERS.map(o => _.omit(o.order, ['chainId', 'verifyingContract']));
describe('operations', () => {
it('getLimitOrderFillableMakerAssetAmounts()', async () => {
const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const sampler = new MockSamplerContract({
getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableAmounts;
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeProxyAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
it('getLimitOrderFillableTakerAssetAmounts()', async () => {
const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const sampler = new MockSamplerContract({
getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableAmounts;
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeProxyAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
it('getKyberSellQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const sampler = new MockSamplerContract({
sampleSellsFromKyberNetwork: (_reserveOffset, takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return ['0x', '0x', expectedMakerFillAmounts];
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getKyberSellQuotes(
{ hintHandler: randomAddress(), networkProxy: randomAddress(), weth: randomAddress() },
new BigNumber(0),
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getLiquidityProviderSellQuotes()', async () => {
const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress();
const poolAddress = randomAddress();
const gasCost = 123;
const sampler = new MockSamplerContract({
sampleSellsFromLiquidityProvider: (providerAddress, takerToken, makerToken, _fillAmounts) => {
expect(providerAddress).to.eq(poolAddress);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(1001)];
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
{
[poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost },
},
async () => undefined,
);
const [result] = await dexOrderSampler.executeAsync(
dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
),
);
expect(result).to.deep.equal([
[
{
source: 'LiquidityProvider',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: { poolAddress, gasCost },
},
],
]);
});
it('getLiquidityProviderBuyQuotes()', async () => {
const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress();
const poolAddress = randomAddress();
const gasCost = 321;
const sampler = new MockSamplerContract({
sampleBuysFromLiquidityProvider: (providerAddress, takerToken, makerToken, _fillAmounts) => {
expect(providerAddress).to.eq(poolAddress);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(999)];
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
{
[poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost },
},
async () => undefined,
);
const [result] = await dexOrderSampler.executeAsync(
dexOrderSampler.getBuyQuotes(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
),
);
expect(result).to.deep.equal([
[
{
source: 'LiquidityProvider',
output: toBaseUnitAmount(999),
input: toBaseUnitAmount(1000),
fillData: { poolAddress, gasCost },
},
],
]);
});
it('getUniswapSellQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const sampler = new MockSamplerContract({
sampleSellsFromUniswap: (_router, takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return expectedMakerFillAmounts;
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getUniswapSellQuotes(
randomAddress(),
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getUniswapV2SellQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const sampler = new MockSamplerContract({
sampleSellsFromUniswapV2: (_router, path, fillAmounts) => {
expect(path).to.deep.eq([expectedMakerToken, expectedTakerToken]);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return expectedMakerFillAmounts;
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getUniswapV2SellQuotes(
NULL_ADDRESS,
[expectedMakerToken, expectedTakerToken],
expectedTakerFillAmounts,
),
);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getUniswapBuyQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const sampler = new MockSamplerContract({
sampleBuysFromUniswap: (_router, takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return expectedTakerFillAmounts;
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getUniswapBuyQuotes(
randomAddress(),
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
);
expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
});
interface RatesBySource {
[src: string]: BigNumber;
}
it('getSellQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.Kyber]: getRandomFloat(0, 100),
[ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100),
[ERC20BridgeSource.UniswapV2]: getRandomFloat(0, 100),
};
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
let uniswapRouter: string;
let uniswapV2Router: string;
const sampler = new MockSamplerContract({
sampleSellsFromUniswap: (router, takerToken, makerToken, fillAmounts) => {
uniswapRouter = router;
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Uniswap]).integerValue());
},
sampleSellsFromUniswapV2: (router, path, fillAmounts) => {
uniswapV2Router = router;
if (path.length === 2) {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]);
} else if (path.length === 3) {
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]);
} else {
expect(path).to.have.lengthOf.within(2, 3);
}
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue());
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
tokenAdjacencyGraph,
undefined,
async () => undefined,
);
const [quotes] = await dexOrderSampler.executeAsync(
dexOrderSampler.getSellQuotes(
sources,
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
);
const expectedQuotes = sources.map(s =>
expectedTakerFillAmounts.map(a => ({
source: s,
input: a,
output: a.times(ratesBySource[s]).integerValue(),
fillData: (() => {
if (s === ERC20BridgeSource.UniswapV2) {
return {
router: uniswapV2Router,
tokenAddressPath: [expectedTakerToken, expectedMakerToken],
};
}
// TODO jacob pass through
if (s === ERC20BridgeSource.Uniswap) {
return { router: uniswapRouter };
}
return {};
})(),
})),
);
const uniswapV2ETHQuotes = [
expectedTakerFillAmounts.map(a => ({
source: ERC20BridgeSource.UniswapV2,
input: a,
output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(),
fillData: {
router: uniswapV2Router,
tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken],
},
})),
];
// extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB)
const additionalSourceCount = 1;
expect(quotes).to.have.lengthOf(sources.length + additionalSourceCount);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes));
});
it('getBuyQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const sources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100),
[ERC20BridgeSource.UniswapV2]: getRandomFloat(0, 100),
};
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
let uniswapRouter: string;
let uniswapV2Router: string;
const sampler = new MockSamplerContract({
sampleBuysFromUniswap: (router, takerToken, makerToken, fillAmounts) => {
uniswapRouter = router;
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Uniswap]).integerValue());
},
sampleBuysFromUniswapV2: (router, path, fillAmounts) => {
uniswapV2Router = router;
if (path.length === 2) {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]);
} else if (path.length === 3) {
expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]);
} else {
expect(path).to.have.lengthOf.within(2, 3);
}
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue());
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
tokenAdjacencyGraph,
undefined,
async () => undefined,
);
const [quotes] = await dexOrderSampler.executeAsync(
dexOrderSampler.getBuyQuotes(sources, expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
);
const expectedQuotes = sources.map(s =>
expectedMakerFillAmounts.map(a => ({
source: s,
input: a,
output: a.times(ratesBySource[s]).integerValue(),
fillData: (() => {
if (s === ERC20BridgeSource.UniswapV2) {
return {
router: uniswapV2Router,
tokenAddressPath: [expectedTakerToken, expectedMakerToken],
};
}
if (s === ERC20BridgeSource.Uniswap) {
return { router: uniswapRouter };
}
return {};
})(),
})),
);
const uniswapV2ETHQuotes = [
expectedMakerFillAmounts.map(a => ({
source: ERC20BridgeSource.UniswapV2,
input: a,
output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(),
fillData: {
router: uniswapV2Router,
tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken],
},
})),
];
// extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB)
expect(quotes).to.have.lengthOf(sources.length + 1);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes));
});
describe('batched operations', () => {
it('getLimitOrderFillableMakerAssetAmounts(), getLimitOrderFillableTakerAssetAmounts()', async () => {
const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const sampler = new MockSamplerContract({
getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableMakerAmounts;
},
getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableTakerAmounts;
},
});
const dexOrderSampler = new DexOrderSampler(
chainId,
sampler,
undefined,
undefined,
undefined,
undefined,
async () => undefined,
);
const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeProxyAddress),
dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeProxyAddress),
);
expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts);
expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts);
});
});
});
});
// tslint:disable-next-line: max-file-line-count