Merge pull request #2427 from 0xProject/feature/erc20-bridge-sampler/query-multiple-buys-sells

ERC20BridgeSampler: Allow for batching multiple buy/sell samples
This commit is contained in:
Jacob Evans 2020-01-22 11:54:45 +10:00 committed by GitHub
commit 4e46bf4697
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1609 additions and 327 deletions

View File

@ -1,4 +1,17 @@
[
{
"version": "1.1.0",
"changes": [
{
"note": "Add batch functions to query quotes",
"pr": 2427
},
{
"note": "Early exit if a DEX sample fails",
"pr": 2427
}
]
},
{
"version": "1.0.3",
"changes": [

View File

@ -37,9 +37,73 @@ contract ERC20BridgeSampler is
DeploymentConstants
{
bytes4 constant internal ERC20_PROXY_ID = 0xf47261b0; // bytes4(keccak256("ERC20Token(address)"));
uint256 constant internal KYBER_SAMPLE_CALL_GAS = 600e3;
uint256 constant internal KYBER_SAMPLE_CALL_GAS = 1500e3;
uint256 constant internal UNISWAP_SAMPLE_CALL_GAS = 150e3;
uint256 constant internal ETH2DAI_SAMPLE_CALL_GAS = 250e3;
uint256 constant internal ETH2DAI_SAMPLE_CALL_GAS = 1000e3;
/// @dev Query batches of native orders and sample sell quotes on multiple DEXes at once.
/// @param orders Batches of Native orders to query.
/// @param orderSignatures Batches of Signatures for each respective order in `orders`.
/// @param sources Address of each DEX. Passing in an unsupported DEX will throw.
/// @param takerTokenAmounts Batches of Taker token sell amount for each sample.
/// @return ordersAndSamples How much taker asset can be filled
/// by each order in `orders`. Maker amounts bought for each source at
/// each taker token amount. First indexed by source index, then sample
/// index.
function queryBatchOrdersAndSampleSells(
LibOrder.Order[][] memory orders,
bytes[][] memory orderSignatures,
address[] memory sources,
uint256[][] memory takerTokenAmounts
)
public
view
returns (
OrdersAndSample[] memory ordersAndSamples
)
{
ordersAndSamples = new OrdersAndSample[](orders.length);
for (uint256 i = 0; i != orders.length; i++) {
(
uint256[] memory orderFillableAssetAmounts,
uint256[][] memory tokenAmountsBySource
) = queryOrdersAndSampleSells(orders[i], orderSignatures[i], sources, takerTokenAmounts[i]);
ordersAndSamples[i].orderFillableAssetAmounts = orderFillableAssetAmounts;
ordersAndSamples[i].tokenAmountsBySource = tokenAmountsBySource;
}
}
/// @dev Query batches of native orders and sample buy quotes on multiple DEXes at once.
/// @param orders Batches of Native orders to query.
/// @param orderSignatures Batches of Signatures for each respective order in `orders`.
/// @param sources Address of each DEX. Passing in an unsupported DEX will throw.
/// @param makerTokenAmounts Batches of Maker token sell amount for each sample.
/// @return ordersAndSamples How much taker asset can be filled
/// by each order in `orders`. Taker amounts sold for each source at
/// each maker token amount. First indexed by source index, then sample
/// index.
function queryBatchOrdersAndSampleBuys(
LibOrder.Order[][] memory orders,
bytes[][] memory orderSignatures,
address[] memory sources,
uint256[][] memory makerTokenAmounts
)
public
view
returns (
OrdersAndSample[] memory ordersAndSamples
)
{
ordersAndSamples = new OrdersAndSample[](orders.length);
for (uint256 i = 0; i != orders.length; i++) {
(
uint256[] memory orderFillableAssetAmounts,
uint256[][] memory tokenAmountsBySource
) = queryOrdersAndSampleBuys(orders[i], orderSignatures[i], sources, makerTokenAmounts[i]);
ordersAndSamples[i].orderFillableAssetAmounts = orderFillableAssetAmounts;
ordersAndSamples[i].tokenAmountsBySource = tokenAmountsBySource;
}
}
/// @dev Query native orders and sample sell quotes on multiple DEXes at once.
/// @param orders Native orders to query.
@ -281,6 +345,8 @@ contract ERC20BridgeSampler is
uint256 rate = 0;
if (didSucceed) {
rate = abi.decode(resultData, (uint256));
} else {
break;
}
makerTokenAmounts[i] =
rate *
@ -321,6 +387,8 @@ contract ERC20BridgeSampler is
uint256 buyAmount = 0;
if (didSucceed) {
buyAmount = abi.decode(resultData, (uint256));
} else{
break;
}
makerTokenAmounts[i] = buyAmount;
}
@ -356,6 +424,8 @@ contract ERC20BridgeSampler is
uint256 sellAmount = 0;
if (didSucceed) {
sellAmount = abi.decode(resultData, (uint256));
} else {
break;
}
takerTokenAmounts[i] = sellAmount;
}
@ -384,26 +454,28 @@ contract ERC20BridgeSampler is
IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ?
IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken);
for (uint256 i = 0; i < numSamples; i++) {
bool didSucceed = true;
if (makerToken == _getWethAddress()) {
makerTokenAmounts[i] = _callUniswapExchangePriceFunction(
(makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction(
address(takerTokenExchange),
takerTokenExchange.getTokenToEthInputPrice.selector,
takerTokenAmounts[i]
);
} else if (takerToken == _getWethAddress()) {
makerTokenAmounts[i] = _callUniswapExchangePriceFunction(
(makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction(
address(makerTokenExchange),
makerTokenExchange.getEthToTokenInputPrice.selector,
takerTokenAmounts[i]
);
} else {
uint256 ethBought = _callUniswapExchangePriceFunction(
uint256 ethBought;
(ethBought, didSucceed) = _callUniswapExchangePriceFunction(
address(takerTokenExchange),
takerTokenExchange.getTokenToEthInputPrice.selector,
takerTokenAmounts[i]
);
if (ethBought != 0) {
makerTokenAmounts[i] = _callUniswapExchangePriceFunction(
(makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction(
address(makerTokenExchange),
makerTokenExchange.getEthToTokenInputPrice.selector,
ethBought
@ -412,6 +484,9 @@ contract ERC20BridgeSampler is
makerTokenAmounts[i] = 0;
}
}
if (!didSucceed) {
break;
}
}
}
@ -438,26 +513,28 @@ contract ERC20BridgeSampler is
IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ?
IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken);
for (uint256 i = 0; i < numSamples; i++) {
bool didSucceed = true;
if (makerToken == _getWethAddress()) {
takerTokenAmounts[i] = _callUniswapExchangePriceFunction(
(takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction(
address(takerTokenExchange),
takerTokenExchange.getTokenToEthOutputPrice.selector,
makerTokenAmounts[i]
);
} else if (takerToken == _getWethAddress()) {
takerTokenAmounts[i] = _callUniswapExchangePriceFunction(
(takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction(
address(makerTokenExchange),
makerTokenExchange.getEthToTokenOutputPrice.selector,
makerTokenAmounts[i]
);
} else {
uint256 ethSold = _callUniswapExchangePriceFunction(
uint256 ethSold;
(ethSold, didSucceed) = _callUniswapExchangePriceFunction(
address(makerTokenExchange),
makerTokenExchange.getEthToTokenOutputPrice.selector,
makerTokenAmounts[i]
);
if (ethSold != 0) {
takerTokenAmounts[i] = _callUniswapExchangePriceFunction(
(takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction(
address(takerTokenExchange),
takerTokenExchange.getTokenToEthOutputPrice.selector,
ethSold
@ -466,6 +543,9 @@ contract ERC20BridgeSampler is
takerTokenAmounts[i] = 0;
}
}
if (!didSucceed) {
break;
}
}
}
@ -493,12 +573,13 @@ contract ERC20BridgeSampler is
)
private
view
returns (uint256 outputAmount)
returns (uint256 outputAmount, bool didSucceed)
{
if (uniswapExchangeAddress == address(0)) {
return 0;
return (outputAmount, didSucceed);
}
(bool didSucceed, bytes memory resultData) =
bytes memory resultData;
(didSucceed, resultData) =
uniswapExchangeAddress.staticcall.gas(UNISWAP_SAMPLE_CALL_GAS)(
abi.encodeWithSelector(
functionSelector,

View File

@ -23,6 +23,52 @@ import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
interface IERC20BridgeSampler {
struct OrdersAndSample {
uint256[] orderFillableAssetAmounts;
uint256[][] tokenAmountsBySource;
}
/// @dev Query batches of native orders and sample sell quotes on multiple DEXes at once.
/// @param orders Batches of Native orders to query.
/// @param orderSignatures Batches of Signatures for each respective order in `orders`.
/// @param sources Address of each DEX. Passing in an unsupported DEX will throw.
/// @param takerTokenAmounts Batches of Taker token sell amount for each sample.
/// @return ordersAndSamples How much taker asset can be filled
/// by each order in `orders`. Maker amounts bought for each source at
/// each taker token amount. First indexed by source index, then sample
/// index.
function queryBatchOrdersAndSampleSells(
LibOrder.Order[][] calldata orders,
bytes[][] calldata orderSignatures,
address[] calldata sources,
uint256[][] calldata takerTokenAmounts
)
external
view
returns (
OrdersAndSample[] memory ordersAndSamples
);
/// @dev Query batches of native orders and sample buy quotes on multiple DEXes at once.
/// @param orders Batches of Native orders to query.
/// @param orderSignatures Batches of Signatures for each respective order in `orders`.
/// @param sources Address of each DEX. Passing in an unsupported DEX will throw.
/// @param makerTokenAmounts Batches of Maker token sell amount for each sample.
/// @return ordersAndSamples How much taker asset can be filled
/// by each order in `orders`. Taker amounts sold for each source at
/// each maker token amount. First indexed by source index, then sample
/// index
function queryBatchOrdersAndSampleBuys(
LibOrder.Order[][] calldata orders,
bytes[][] calldata orderSignatures,
address[] calldata sources,
uint256[][] calldata makerTokenAmounts
)
external
view
returns (
OrdersAndSample[] memory ordersAndSamples
);
/// @dev Query native orders and sample sell quotes on multiple DEXes at once.
/// @param orders Native orders to query.

View File

@ -338,7 +338,11 @@ contract TestERC20BridgeSampler is
bytes32 orderHash = keccak256(abi.encode(order.salt));
// Everything else is derived from the hash.
orderInfo.orderHash = orderHash;
orderInfo.orderStatus = LibOrder.OrderStatus(uint256(orderHash) % MAX_ORDER_STATUS);
if (uint256(orderHash) % 100 > 90) {
orderInfo.orderStatus = LibOrder.OrderStatus.FULLY_FILLED;
} else {
orderInfo.orderStatus = LibOrder.OrderStatus.FILLABLE;
}
orderInfo.orderTakerAssetFilledAmount = uint256(orderHash) % order.takerAssetAmount;
fillableTakerAssetAmount =
order.takerAssetAmount - orderInfo.orderTakerAssetFilledAmount;

View File

@ -195,7 +195,7 @@ blockchainTests('erc20-bridge-sampler', env => {
function getDeterministicFillableTakerAssetAmount(order: Order): BigNumber {
const hash = getPackedHash(hexUtils.toHex(order.salt, 32));
const orderStatus = new BigNumber(hash).mod(255).toNumber();
const orderStatus = new BigNumber(hash).mod(100).toNumber() > 90 ? 5 : 3;
const isValidSignature = !!new BigNumber(hash).mod(2).toNumber();
if (orderStatus !== 3 || !isValidSignature) {
return constants.ZERO_AMOUNT;
@ -208,7 +208,7 @@ blockchainTests('erc20-bridge-sampler', env => {
return order.makerAssetAmount
.times(takerAmount)
.div(order.takerAssetAmount)
.integerValue(BigNumber.ROUND_DOWN);
.integerValue(BigNumber.ROUND_UP);
}
function getERC20AssetData(tokenAddress: string): string {
@ -255,7 +255,7 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('getOrderFillableTakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const signatures: string[] = _.times(orders.length, hexUtils.random);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const expected = orders.map(getDeterministicFillableTakerAssetAmount);
const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync();
expect(actual).to.deep.eq(expected);
@ -269,7 +269,7 @@ blockchainTests('erc20-bridge-sampler', env => {
it('returns zero for an order with zero maker asset amount', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1);
orders[0].makerAssetAmount = constants.ZERO_AMOUNT;
const signatures: string[] = _.times(orders.length, hexUtils.random);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync();
expect(actual).to.deep.eq([constants.ZERO_AMOUNT]);
});
@ -277,7 +277,7 @@ blockchainTests('erc20-bridge-sampler', env => {
it('returns zero for an order with zero taker asset amount', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1);
orders[0].takerAssetAmount = constants.ZERO_AMOUNT;
const signatures: string[] = _.times(orders.length, hexUtils.random);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync();
expect(actual).to.deep.eq([constants.ZERO_AMOUNT]);
});
@ -293,7 +293,7 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('getOrderFillableMakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const signatures: string[] = _.times(orders.length, hexUtils.random);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const expected = orders.map(getDeterministicFillableMakerAssetAmount);
const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync();
expect(actual).to.deep.eq(expected);
@ -307,7 +307,7 @@ blockchainTests('erc20-bridge-sampler', env => {
it('returns zero for an order with zero maker asset amount', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1);
orders[0].makerAssetAmount = constants.ZERO_AMOUNT;
const signatures: string[] = _.times(orders.length, hexUtils.random);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync();
expect(actual).to.deep.eq([constants.ZERO_AMOUNT]);
});
@ -315,7 +315,7 @@ blockchainTests('erc20-bridge-sampler', env => {
it('returns zero for an order with zero taker asset amount', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1);
orders[0].takerAssetAmount = constants.ZERO_AMOUNT;
const signatures: string[] = _.times(orders.length, hexUtils.random);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync();
expect(actual).to.deep.eq([constants.ZERO_AMOUNT]);
});
@ -330,7 +330,7 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('queryOrdersAndSampleSells()', () => {
const ORDERS = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const SIGNATURES: string[] = _.times(ORDERS.length, hexUtils.random);
const SIGNATURES: string[] = _.times(ORDERS.length, i => hexUtils.random());
before(async () => {
await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync();
@ -436,7 +436,7 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('queryOrdersAndSampleBuys()', () => {
const ORDERS = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const SIGNATURES: string[] = _.times(ORDERS.length, hexUtils.random);
const SIGNATURES: string[] = _.times(ORDERS.length, i => hexUtils.random());
before(async () => {
await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync();

View File

@ -277,10 +277,16 @@ for (const abiFileName of abiFileNames) {
// use command-line tool black to reformat, if its available
try {
execSync(`black --line-length 79 ${outFilePath}`);
} catch {
logUtils.warn(
'Failed to reformat generated Python with black. Do you have it installed? Proceeding anyways...',
);
} catch (e) {
const BLACK_RC_CANNOT_PARSE = 123; // empirical black exit code
if (e.status === BLACK_RC_CANNOT_PARSE) {
logUtils.warn(
'Failed to reformat generated Python with black. Exception thrown by execSync("black ...") follows.',
);
throw e;
} else {
logUtils.warn('Failed to invoke black. Do you have it installed? Proceeding anyways...');
}
}
}

View File

@ -195,7 +195,7 @@ export const utils = {
return tuple.internalType
.replace('struct ', '')
.replace('.', '')
.replace('[]', '');
.replace(/\[\]/g, '');
} else {
const tupleComponents = tuple.components;
const lengthOfHashSuffix = 8;

File diff suppressed because one or more lines are too long

View File

@ -138,6 +138,8 @@ contract AbiGenDummy
}
function methodReturningArrayOfStructs() public pure returns(Struct[] memory) {}
function methodAcceptingArrayOfStructs(Struct[] memory) public pure {}
function methodAcceptingArrayOfArrayOfStructs(Struct[][] memory) public pure {}
struct NestedStruct {
Struct innerStruct;

File diff suppressed because one or more lines are too long

View File

@ -90,7 +90,7 @@ class LibDummy:
try:
for middleware in MIDDLEWARE:
web3.middleware_onion.inject(
middleware["function"], layer=middleware["layer"]
middleware["function"], layer=middleware["layer"],
)
except ValueError as value_error:
if value_error.args == (

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,18 @@
{
"note": "Remove `getSmartContractParamsOrThrow()` from `SwapQuoteConsumer`s.",
"pr": 2432
},
{
"note": "Added `getBatchMarketBuySwapQuoteForAssetDataAsync` on `SwapQuoter`",
"pr": 2427
},
{
"note": "Add exponential sampling distribution and `sampleDistributionBase` option to `SwapQuoter`",
"pr": 2427
},
{
"note": "Compute more accurate best quote price",
"pr": 2427
}
]
},

View File

@ -54,6 +54,11 @@ export {
SwapQuoteConsumerError,
SignedOrderWithFillableAmounts,
} from './types';
export { ERC20BridgeSource } from './utils/market_operation_utils/types';
export {
ERC20BridgeSource,
CollapsedFill,
NativeCollapsedFill,
OptimizedMarketOrder,
} from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';

View File

@ -226,6 +226,58 @@ export class SwapQuoter {
options,
)) as MarketBuySwapQuote;
}
public async getBatchMarketBuySwapQuoteForAssetDataAsync(
makerAssetDatas: string[],
takerAssetData: string,
makerAssetBuyAmount: BigNumber[],
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<Array<MarketBuySwapQuote | undefined>> {
makerAssetBuyAmount.map((a, i) => assert.isBigNumber(`makerAssetBuyAmount[${i}]`, a));
let gasPrice: BigNumber;
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
if (!!options.gasPrice) {
gasPrice = options.gasPrice;
assert.isBigNumber('gasPrice', gasPrice);
} else {
gasPrice = await this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync();
}
const apiOrders = await this.orderbook.getBatchOrdersAsync(makerAssetDatas, [takerAssetData]);
const allOrders = apiOrders.map(orders => orders.map(o => o.order));
const allPrunedOrders = allOrders.map((orders, i) => {
if (orders.length === 0) {
return [
dummyOrderUtils.createDummyOrderForSampler(
makerAssetDatas[i],
takerAssetData,
this._contractAddresses.uniswapBridge,
),
];
} else {
return sortingUtils.sortOrders(
orderPrunerUtils.pruneForUsableSignedOrders(
orders,
this.permittedOrderFeeTypes,
this.expiryBufferMs,
),
);
}
});
const swapQuotes = await this._swapQuoteCalculator.calculateBatchMarketBuySwapQuoteAsync(
allPrunedOrders,
makerAssetBuyAmount,
slippagePercentage,
gasPrice,
calculateSwapQuoteOpts,
);
return swapQuotes;
}
/**
* Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address.
* You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption.

View File

@ -24,12 +24,14 @@ export const SELL_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Da
export const BUY_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
runLimit: 4096,
// tslint:disable-next-line: custom-no-magic-numbers
runLimit: 2 ** 15,
excludedSources: [],
bridgeSlippage: 0.0005,
dustFractionThreshold: 0.01,
numSamples: 10,
dustFractionThreshold: 0.0025,
numSamples: 13,
noConflicts: true,
sampleDistributionBase: 1.05,
};
export const constants = {
@ -40,5 +42,5 @@ export const constants = {
DEFAULT_GET_MARKET_ORDERS_OPTS,
ERC20_PROXY_ID: '0xf47261b0',
WALLET_SIGNATURE: '0x04',
SAMPLER_CONTRACT_GAS_LIMIT: 10e6,
SAMPLER_CONTRACT_GAS_LIMIT: 16e6,
};

View File

@ -3,11 +3,17 @@ import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { SignedOrderWithFillableAmounts } from '../../types';
import { sortingUtils } from '../../utils/sorting_utils';
import { constants as marketOperationUtilConstants } from './constants';
import { AggregationError, ERC20BridgeSource, Fill, FillData, NativeFillData, OrderDomain } from './types';
import {
AggregationError,
CollapsedFill,
ERC20BridgeSource,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
} from './types';
const { NULL_BYTES, NULL_ADDRESS, ZERO_AMOUNT } = constants;
const { INFINITE_TIMESTAMP_SEC, WALLET_SIGNATURE } = marketOperationUtilConstants;
@ -24,23 +30,21 @@ export class CreateOrderUtils {
orderDomain: OrderDomain,
inputToken: string,
outputToken: string,
path: Fill[],
path: CollapsedFill[],
bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] {
const orders: SignedOrderWithFillableAmounts[] = [];
): OptimizedMarketOrder[] {
const orders: OptimizedMarketOrder[] = [];
for (const fill of path) {
const source = (fill.fillData as FillData).source;
if (source === ERC20BridgeSource.Native) {
orders.push((fill.fillData as NativeFillData).order);
if (fill.source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(fill));
} else {
orders.push(
createBridgeOrder(
orderDomain,
this._getBridgeAddressFromSource(source),
fill,
this._getBridgeAddressFromSource(fill.source),
outputToken,
inputToken,
fill.output,
fill.input,
bridgeSlippage,
),
);
@ -54,23 +58,21 @@ export class CreateOrderUtils {
orderDomain: OrderDomain,
inputToken: string,
outputToken: string,
path: Fill[],
path: CollapsedFill[],
bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] {
const orders: SignedOrderWithFillableAmounts[] = [];
): OptimizedMarketOrder[] {
const orders: OptimizedMarketOrder[] = [];
for (const fill of path) {
const source = (fill.fillData as FillData).source;
if (source === ERC20BridgeSource.Native) {
orders.push((fill.fillData as NativeFillData).order);
if (fill.source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(fill));
} else {
orders.push(
createBridgeOrder(
orderDomain,
this._getBridgeAddressFromSource(source),
fill,
this._getBridgeAddressFromSource(fill.source),
inputToken,
outputToken,
fill.input,
fill.output,
bridgeSlippage,
true,
),
@ -97,14 +99,13 @@ export class CreateOrderUtils {
function createBridgeOrder(
orderDomain: OrderDomain,
fill: CollapsedFill,
bridgeAddress: string,
makerToken: string,
takerToken: string,
makerAssetAmount: BigNumber,
takerAssetAmount: BigNumber,
slippage: number,
isBuy: boolean = false,
): SignedOrderWithFillableAmounts {
): OptimizedMarketOrder {
return {
makerAddress: bridgeAddress,
makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(
@ -113,7 +114,7 @@ function createBridgeOrder(
createBridgeData(takerToken),
),
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
...createCommonOrderFields(orderDomain, makerAssetAmount, takerAssetAmount, slippage, isBuy),
...createCommonOrderFields(orderDomain, fill, slippage, isBuy),
};
}
@ -123,24 +124,24 @@ function createBridgeData(tokenAddress: string): string {
}
type CommonOrderFields = Pick<
SignedOrderWithFillableAmounts,
Exclude<keyof SignedOrderWithFillableAmounts, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
OptimizedMarketOrder,
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
>;
function createCommonOrderFields(
orderDomain: OrderDomain,
makerAssetAmount: BigNumber,
takerAssetAmount: BigNumber,
fill: CollapsedFill,
slippage: number,
isBuy: boolean = false,
): CommonOrderFields {
const makerAssetAmountAdjustedWithSlippage = isBuy
? makerAssetAmount
: makerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN);
? fill.totalMakerAssetAmount
: fill.totalMakerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN);
const takerAssetAmountAdjustedWithSlippage = isBuy
? takerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP)
: takerAssetAmount;
? fill.totalTakerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP)
: fill.totalTakerAssetAmount;
return {
fill,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
@ -159,3 +160,15 @@ function createCommonOrderFields(
...orderDomain,
};
}
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
return {
fill: {
source: fill.source,
totalMakerAssetAmount: fill.totalMakerAssetAmount,
totalTakerAssetAmount: fill.totalTakerAssetAmount,
subFills: fill.subFills,
},
...(fill as NativeCollapsedFill).nativeOrder,
};
}

View File

@ -14,12 +14,16 @@ import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimi
import { DexOrderSampler } from './sampler';
import {
AggregationError,
CollapsedFill,
DexSample,
ERC20BridgeSource,
Fill,
FillData,
FillFlags,
GetMarketOrdersOpts,
NativeCollapsedFill,
NativeFillData,
OptimizedMarketOrder,
OrderDomain,
} from './types';
@ -53,7 +57,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[],
takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<SignedOrderWithFillableAmounts[]> {
): Promise<OptimizedMarketOrder[]> {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
@ -63,7 +67,7 @@ export class MarketOperationUtils {
};
const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketSellAsync(
nativeOrders,
DexOrderSampler.getSampleAmounts(takerAmount, _opts.numSamples),
DexOrderSampler.getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase),
difference(SELL_SOURCES, _opts.excludedSources),
);
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
@ -77,8 +81,7 @@ export class MarketOperationUtils {
takerAmount,
_opts.dustFractionThreshold,
);
const clippedNativePath = clipPathToInput(prunedNativePath, takerAmount);
const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath), takerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths);
@ -106,7 +109,7 @@ export class MarketOperationUtils {
this._orderDomain,
inputToken,
outputToken,
simplifyPath(optimalPath),
collapsePath(optimalPath, false),
_opts.bridgeSlippage,
);
}
@ -123,7 +126,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[],
makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<SignedOrderWithFillableAmounts[]> {
): Promise<OptimizedMarketOrder[]> {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
@ -134,29 +137,86 @@ export class MarketOperationUtils {
const [fillableAmounts, dexQuotes] = await this._dexSampler.getFillableAmountsAndSampleMarketBuyAsync(
nativeOrders,
DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples),
DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase),
difference(BUY_SOURCES, _opts.excludedSources),
);
const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists(
nativeOrders,
makerAmount,
fillableAmounts,
dexQuotes,
_opts,
);
if (!signedOrderWithFillableAmounts) {
throw new Error(AggregationError.NoOptimalPath);
}
return signedOrderWithFillableAmounts;
}
/**
* gets the orders required for a batch of market buy operations by (potentially) merging native orders with
* generated bridge orders.
* @param batchNativeOrders Batch of Native orders.
* @param makerAmounts Array amount of maker asset to buy for each batch.
* @param opts Options object.
* @return orders.
*/
public async getBatchMarketBuyOrdersAsync(
batchNativeOrders: SignedOrder[][],
makerAmounts: BigNumber[],
opts?: Partial<GetMarketOrdersOpts>,
): Promise<Array<OptimizedMarketOrder[] | undefined>> {
if (batchNativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const batchSampleResults = await this._dexSampler.getBatchFillableAmountsAndSampleMarketBuyAsync(
batchNativeOrders,
makerAmounts.map(makerAmount => DexOrderSampler.getSampleAmounts(makerAmount, _opts.numSamples)),
difference(BUY_SOURCES, _opts.excludedSources),
);
return batchSampleResults.map(([fillableAmounts, dexQuotes], i) =>
this._createBuyOrdersPathFromSamplerResultIfExists(
batchNativeOrders[i],
makerAmounts[i],
fillableAmounts,
dexQuotes,
_opts,
),
);
}
private _createBuyOrdersPathFromSamplerResultIfExists(
nativeOrders: SignedOrder[],
makerAmount: BigNumber,
nativeOrderFillableAmounts: BigNumber[],
dexQuotes: DexSample[][],
opts: GetMarketOrdersOpts,
): OptimizedMarketOrder[] | undefined {
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
nativeOrders,
fillableAmounts,
nativeOrderFillableAmounts,
MarketOperation.Buy,
);
const prunedNativePath = pruneDustFillsFromNativePath(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts),
makerAmount,
_opts.dustFractionThreshold,
opts.dustFractionThreshold,
);
const clippedNativePath = clipPathToInput(prunedNativePath, makerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath).reverse(), makerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, opts.noConflicts);
const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in.
if (!_opts.excludedSources.includes(ERC20BridgeSource.Native)) {
if (!opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath);
allFills.splice(0, 0, ...clippedNativePath);
}
const optimizer = new FillsOptimizer(_opts.runLimit, true);
const optimizer = new FillsOptimizer(opts.runLimit, true);
const upperBoundPath = pickBestUpperBoundPath(allPaths, makerAmount, true);
const optimalPath = optimizer.optimize(
// Sorting the orders by price effectively causes the optimizer to walk
@ -167,15 +227,15 @@ export class MarketOperationUtils {
upperBoundPath,
);
if (!optimalPath) {
throw new Error(AggregationError.NoOptimalPath);
return undefined;
}
const [inputToken, outputToken] = getOrderTokens(nativeOrders[0]);
return this._createOrderUtils.createBuyOrdersFromPath(
this._orderDomain,
inputToken,
outputToken,
simplifyPath(optimalPath),
_opts.bridgeSlippage,
collapsePath(optimalPath, true),
opts.bridgeSlippage,
);
}
}
@ -186,26 +246,24 @@ function createSignedOrdersWithFillableAmounts(
operation: MarketOperation,
): SignedOrderWithFillableAmounts[] {
return signedOrders
.map(
(order: SignedOrder, i: number): SignedOrderWithFillableAmounts => {
const fillableAmount = fillableAmounts[i];
const fillableMakerAssetAmount =
operation === MarketOperation.Buy
? fillableAmount
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
const fillableTakerAssetAmount =
operation === MarketOperation.Sell
? fillableAmount
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
return {
fillableMakerAssetAmount,
fillableTakerAssetAmount,
fillableTakerFeeAmount,
...order,
};
},
)
.map((order: SignedOrder, i: number) => {
const fillableAmount = fillableAmounts[i];
const fillableMakerAssetAmount =
operation === MarketOperation.Buy
? fillableAmount
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
const fillableTakerAssetAmount =
operation === MarketOperation.Sell
? fillableAmount
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
return {
fillableMakerAssetAmount,
fillableTakerAssetAmount,
fillableTakerFeeAmount,
...order,
};
})
.filter(order => {
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
});
@ -356,23 +414,31 @@ function getPathInput(path: Fill[]): BigNumber {
}
// Merges contiguous fills from the same DEX.
function simplifyPath(path: Fill[]): Fill[] {
const simplified: Fill[] = [];
function collapsePath(path: Fill[], isBuy: boolean): CollapsedFill[] {
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
for (const fill of path) {
const makerAssetAmount = isBuy ? fill.input : fill.output;
const takerAssetAmount = isBuy ? fill.output : fill.input;
const source = (fill.fillData as FillData).source;
if (simplified.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = simplified[simplified.length - 1];
const prevSource = (prevFill.fillData as FillData).source;
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = collapsed[collapsed.length - 1];
// If the last fill is from the same source, merge them.
if (prevSource === source) {
prevFill.input = prevFill.input.plus(fill.input);
prevFill.output = prevFill.output.plus(fill.output);
if (prevFill.source === source) {
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
continue;
}
}
simplified.push(fill);
collapsed.push({
source: fill.fillData.source,
totalMakerAssetAmount: makerAssetAmount,
totalTakerAssetAmount: takerAssetAmount,
subFills: [{ makerAssetAmount, takerAssetAmount }],
nativeOrder: (fill.fillData as NativeFillData).order,
});
}
return simplified;
return collapsed;
}
// Sort fills by descending price.

View File

@ -13,16 +13,14 @@ export class DexOrderSampler {
/**
* Generate sample amounts up to `maxFillAmount`.
*/
public static getSampleAmounts(maxFillAmount: BigNumber, numSamples: number): BigNumber[] {
const amounts = [];
for (let i = 0; i < numSamples; i++) {
amounts.push(
maxFillAmount
.times(i + 1)
.div(numSamples)
.integerValue(BigNumber.ROUND_UP),
);
}
public static 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) => {
return maxFillAmount
.times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)]))
.integerValue(BigNumber.ROUND_UP);
});
return amounts;
}
@ -50,6 +48,36 @@ export class DexOrderSampler {
return [fillableAmount, quotes];
}
public async getBatchFillableAmountsAndSampleMarketBuyAsync(
nativeOrders: SignedOrder[][],
sampleAmounts: BigNumber[][],
sources: ERC20BridgeSource[],
): Promise<Array<[BigNumber[], DexSample[][]]>> {
const signatures = nativeOrders.map(o => o.map(i => i.signature));
const fillableAmountsAndSamples = await this._samplerContract
.queryBatchOrdersAndSampleBuys(
nativeOrders,
signatures,
sources.map(s => SOURCE_TO_ADDRESS[s]),
sampleAmounts,
)
.callAsync();
const batchFillableAmountsAndQuotes: Array<[BigNumber[], DexSample[][]]> = [];
fillableAmountsAndSamples.forEach((sampleResult, i) => {
const { tokenAmountsBySource, orderFillableAssetAmounts } = sampleResult;
const quotes = tokenAmountsBySource.map((rawDexSamples, sourceIdx) => {
const source = sources[sourceIdx];
return rawDexSamples.map((sample, sampleIdx) => ({
source,
input: sampleAmounts[i][sampleIdx],
output: sample,
}));
});
batchFillableAmountsAndQuotes.push([orderFillableAssetAmounts, quotes]);
});
return batchFillableAmountsAndQuotes;
}
public async getFillableAmountsAndSampleMarketSellAsync(
nativeOrders: SignedOrder[],
sampleAmounts: BigNumber[],

View File

@ -79,6 +79,48 @@ export interface Fill {
fillData: FillData | NativeFillData;
}
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill {
/**
* The source DEX.
*/
source: ERC20BridgeSource;
/**
* Total maker asset amount.
*/
totalMakerAssetAmount: BigNumber;
/**
* Total taker asset amount.
*/
totalTakerAssetAmount: BigNumber;
/**
* All the fill asset amounts that were collapsed into this node.
*/
subFills: Array<{
makerAssetAmount: BigNumber;
takerAssetAmount: BigNumber;
}>;
}
/**
* A `CollapsedFill` wrapping a native order.
*/
export interface NativeCollapsedFill extends CollapsedFill {
nativeOrder: SignedOrderWithFillableAmounts;
}
/**
* Optimized orders to fill.
*/
export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts {
/**
* The optimized fills that generated this order.
*/
fill: CollapsedFill;
}
/**
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
*/
@ -114,4 +156,12 @@ export interface GetMarketOrdersOpts {
* Default is 0.01 (100 basis points).
*/
dustFractionThreshold: number;
/**
* The exponential sampling distribution base.
* A value of 1 will result in evenly spaced samples.
* > 1 will result in more samples at lower sizes.
* < 1 will result in more samples at higher sizes.
* Default: 1.25.
*/
sampleDistributionBase: number;
}

View File

@ -9,9 +9,9 @@ export class ProtocolFeeUtils {
public gasPriceEstimation: BigNumber;
private readonly _gasPriceHeart: any;
constructor(gasPricePollingIntervalInMs: number) {
constructor(gasPricePollingIntervalInMs: number, initialGasPrice: BigNumber = constants.ZERO_AMOUNT) {
this._gasPriceHeart = heartbeats.createHeart(gasPricePollingIntervalInMs);
this.gasPriceEstimation = constants.ZERO_AMOUNT;
this.gasPriceEstimation = initialGasPrice;
this._initializeHeartBeat();
}

View File

@ -16,6 +16,7 @@ import {
import { fillableAmountsUtils } from './fillable_amounts_utils';
import { MarketOperationUtils } from './market_operation_utils';
import { ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types';
import { ProtocolFeeUtils } from './protocol_fee_utils';
import { utils } from './utils';
@ -63,6 +64,58 @@ export class SwapQuoteCalculator {
)) as MarketBuySwapQuote;
}
public async calculateBatchMarketBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][],
takerAssetFillAmounts: BigNumber[],
slippagePercentage: number,
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<Array<MarketBuySwapQuote | undefined>> {
return (await this._calculateBatchBuySwapQuoteAsync(
batchPrunedOrders,
takerAssetFillAmounts,
slippagePercentage,
gasPrice,
MarketOperation.Buy,
opts,
)) as Array<MarketBuySwapQuote | undefined>;
}
private async _calculateBatchBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][],
assetFillAmounts: BigNumber[],
slippagePercentage: number,
gasPrice: BigNumber,
operation: MarketOperation,
opts: CalculateSwapQuoteOpts,
): Promise<Array<SwapQuote | undefined>> {
const assetFillAmountsWithSlippage = assetFillAmounts.map(a =>
a.plus(a.multipliedBy(slippagePercentage).integerValue()),
);
const batchSignedOrders = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync(
batchPrunedOrders,
assetFillAmountsWithSlippage,
opts,
);
const batchSwapQuotes = await Promise.all(
batchSignedOrders.map(async (orders, i) => {
if (orders) {
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
return this._createSwapQuoteAsync(
makerAssetData,
takerAssetData,
orders,
operation,
assetFillAmounts[i],
gasPrice,
);
} else {
return undefined;
}
}),
);
return batchSwapQuotes;
}
private async _calculateSwapQuoteAsync(
prunedOrders: SignedOrder[],
assetFillAmount: BigNumber,
@ -74,7 +127,7 @@ export class SwapQuoteCalculator {
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: SignedOrderWithFillableAmounts[] = [];
let resultOrders: OptimizedMarketOrder[] = [];
if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
@ -91,28 +144,43 @@ export class SwapQuoteCalculator {
}
// assetData information for the result
const takerAssetData = prunedOrders[0].takerAssetData;
const makerAssetData = prunedOrders[0].makerAssetData;
const { makerAssetData, takerAssetData } = prunedOrders[0];
return this._createSwapQuoteAsync(
makerAssetData,
takerAssetData,
resultOrders,
operation,
assetFillAmount,
gasPrice,
);
}
private async _createSwapQuoteAsync(
makerAssetData: string,
takerAssetData: string,
resultOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
): Promise<SwapQuote> {
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
createBestCaseOrders(resultOrders, operation, opts.bridgeSlippage),
resultOrders,
assetFillAmount,
gasPrice,
operation,
);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts
// such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
_.reverse(resultOrders.slice()),
resultOrders,
assetFillAmount,
gasPrice,
operation,
true,
);
const quoteBase = {
takerAssetData,
makerAssetData,
orders: resultOrders,
// Remove fill metadata.
orders: resultOrders.map(o => _.omit(o, 'fill')) as SignedOrderWithFillableAmounts[],
bestCaseQuoteInfo,
worstCaseQuoteInfo,
gasPrice,
@ -135,31 +203,39 @@ export class SwapQuoteCalculator {
// tslint:disable-next-line: prefer-function-over-method
private async _calculateQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[],
orders: OptimizedMarketOrder[],
assetFillAmount: BigNumber,
gasPrice: BigNumber,
operation: MarketOperation,
worstCase: boolean = false,
): Promise<SwapQuoteInfo> {
if (operation === MarketOperation.Buy) {
return this._calculateMarketBuyQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice);
return this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase);
} else {
return this._calculateMarketSellQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice);
return this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase);
}
}
private async _calculateMarketSellQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[],
orders: OptimizedMarketOrder[],
takerAssetSellAmount: BigNumber,
gasPrice: BigNumber,
worstCase: boolean = false,
): Promise<SwapQuoteInfo> {
const result = prunedOrders.reduce(
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingTakerAssetFillAmount,
} = acc;
let totalMakerAssetAmount = constants.ZERO_AMOUNT;
let totalTakerAssetAmount = constants.ZERO_AMOUNT;
let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT;
let remainingTakerAssetFillAmount = takerAssetSellAmount;
const filledOrders = [] as OptimizedMarketOrder[];
const _orders = !worstCase ? orders : orders.slice().reverse();
for (const order of _orders) {
let makerAssetAmount = constants.ZERO_AMOUNT;
let takerAssetAmount = constants.ZERO_AMOUNT;
let feeTakerAssetAmount = constants.ZERO_AMOUNT;
if (remainingTakerAssetFillAmount.lte(0)) {
break;
}
if (order.fill.source === ERC20BridgeSource.Native) {
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
order,
);
@ -170,99 +246,155 @@ export class SwapQuoteCalculator {
remainingTakerAssetFillAmount,
adjustedFillableTakerAssetAmount,
);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
const makerAssetAmount = takerAssetAmountWithFees
const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees);
takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount;
feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount;
makerAssetAmount = takerAssetAmountWithFees
.div(adjustedFillableTakerAssetAmount)
.multipliedBy(adjustedFillableMakerAssetAmount)
.times(adjustedFillableMakerAssetAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerAssetAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingTakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount.minus(takerAssetAmountWithFees),
),
};
},
{
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount: takerAssetSellAmount,
},
);
} else {
// This is a collapsed bridge order.
// Because collapsed bridge orders actually fill at different rates,
// we can iterate over the uncollapsed fills to get the actual
// asset amounts transfered.
// We can also assume there are no fees and the order is not
// partially filled.
// Infer the bridge slippage from the difference between the fill
// size and the actual order asset amounts.
const makerAssetBridgeSlippage = !worstCase
? constants.ONE_AMOUNT
: order.makerAssetAmount.div(order.fill.totalMakerAssetAmount);
const takerAssetBridgeSlippage = !worstCase
? constants.ONE_AMOUNT
: order.takerAssetAmount.div(order.fill.totalTakerAssetAmount);
// Consecutively fill the subfills in this order.
const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse();
for (const subFill of subFills) {
if (remainingTakerAssetFillAmount.minus(takerAssetAmount).lte(0)) {
break;
}
const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage);
const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage);
const partialTakerAssetFillAmount = BigNumber.min(
partialTakerAssetAmount,
remainingTakerAssetFillAmount.minus(takerAssetAmount),
);
const partialMakerAssetFillAmount = partialTakerAssetFillAmount
.div(partialTakerAssetAmount)
.times(partialMakerAssetAmount)
.integerValue(BigNumber.ROUND_DOWN);
takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount);
makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount);
}
}
totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount);
totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount);
totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount);
remainingTakerAssetFillAmount = remainingTakerAssetFillAmount
.minus(takerAssetAmount)
.minus(feeTakerAssetAmount);
filledOrders.push(order);
}
const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
prunedOrders,
filledOrders,
gasPrice,
);
return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
feeTakerAssetAmount: totalFeeTakerAssetAmount,
takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount,
};
}
private async _calculateMarketBuyQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[],
orders: OptimizedMarketOrder[],
makerAssetBuyAmount: BigNumber,
gasPrice: BigNumber,
worstCase: boolean = false,
): Promise<SwapQuoteInfo> {
const result = prunedOrders.reduce(
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingMakerAssetFillAmount,
} = acc;
let totalMakerAssetAmount = constants.ZERO_AMOUNT;
let totalTakerAssetAmount = constants.ZERO_AMOUNT;
let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT;
let remainingMakerAssetFillAmount = makerAssetBuyAmount;
const filledOrders = [] as OptimizedMarketOrder[];
const _orders = !worstCase ? orders : orders.slice().reverse();
for (const order of _orders) {
let makerAssetAmount = constants.ZERO_AMOUNT;
let takerAssetAmount = constants.ZERO_AMOUNT;
let feeTakerAssetAmount = constants.ZERO_AMOUNT;
if (remainingMakerAssetFillAmount.lte(0)) {
break;
}
if (order.fill.source === ERC20BridgeSource.Native) {
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
order,
);
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
order,
);
const makerFillAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
const takerAssetAmountWithFees = makerFillAmount
makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
const takerAssetAmountWithFees = makerAssetAmount
.div(adjustedFillableMakerAssetAmount)
.multipliedBy(adjustedFillableTakerAssetAmount)
.integerValue(BigNumber.ROUND_UP);
const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees);
takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount;
feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount;
} else {
// This is a collapsed bridge order.
// Because collapsed bridge orders actually fill at different rates,
// we can iterate over the uncollapsed fills to get the actual
// asset amounts transfered.
// We can also assume there are no fees and the order is not
// partially filled.
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
return {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerFillAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingMakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount.minus(makerFillAmount),
),
};
},
{
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount: makerAssetBuyAmount,
},
);
// Infer the bridge slippage from the difference between the fill
// size and the actual order asset amounts.
const makerAssetBridgeSlippage = !worstCase
? constants.ONE_AMOUNT
: order.makerAssetAmount.div(order.fill.totalMakerAssetAmount);
const takerAssetBridgeSlippage = !worstCase
? constants.ONE_AMOUNT
: order.takerAssetAmount.div(order.fill.totalTakerAssetAmount);
// Consecutively fill the subfills in this order.
const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse();
for (const subFill of subFills) {
if (remainingMakerAssetFillAmount.minus(makerAssetAmount).lte(0)) {
break;
}
const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage);
const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage);
const partialMakerAssetFillAmount = BigNumber.min(
partialMakerAssetAmount,
remainingMakerAssetFillAmount.minus(makerAssetAmount),
);
const partialTakerAssetFillAmount = partialMakerAssetFillAmount
.div(partialMakerAssetAmount)
.times(partialTakerAssetAmount)
.integerValue(BigNumber.ROUND_UP);
takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount);
makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount);
}
}
totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount);
totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount);
totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount);
remainingMakerAssetFillAmount = remainingMakerAssetFillAmount.minus(makerAssetAmount);
filledOrders.push(order);
}
const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
prunedOrders,
filledOrders,
gasPrice,
);
return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
feeTakerAssetAmount: totalFeeTakerAssetAmount,
takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount,
};
}
@ -303,35 +435,3 @@ function getTakerAssetAmountBreakDown(
takerAssetAmount: takerAssetAmountWithFees,
};
}
function createBestCaseOrders(
orders: SignedOrderWithFillableAmounts[],
operation: MarketOperation,
bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] {
// Scale the asset amounts to undo the bridge slippage applied to
// bridge orders.
const bestCaseOrders: SignedOrderWithFillableAmounts[] = [];
for (const order of orders) {
if (!order.makerAssetData.startsWith(constants.BRIDGE_ASSET_DATA_PREFIX)) {
bestCaseOrders.push(order);
continue;
}
bestCaseOrders.push({
...order,
...(operation === MarketOperation.Sell
? {
makerAssetAmount: order.makerAssetAmount
.dividedBy(1 - bridgeSlippage)
.integerValue(BigNumber.ROUND_DOWN),
}
: // Buy operation
{
takerAssetAmount: order.takerAssetAmount
.dividedBy(bridgeSlippage + 1)
.integerValue(BigNumber.ROUND_UP),
}),
});
}
return bestCaseOrders;
}

View File

@ -23,7 +23,7 @@ describe('#calculateLiquidity', () => {
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(10));
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
});
it('should provide correct liquidity result with orders with takerFees in takerAsset', () => {
@ -31,7 +31,7 @@ describe('#calculateLiquidity', () => {
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(10));
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15));
});
it('should provide correct liquidity result with orders with takerFees in makerAsset', () => {
@ -51,7 +51,7 @@ describe('#calculateLiquidity', () => {
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(25));
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(27));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(33));
});
});

View File

@ -123,7 +123,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TakerTokenContract,
makerAddress,

View File

@ -126,7 +126,7 @@ describe('ForwarderSwapQuoteConsumer', () => {
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TokenContract,
makerAddress,

View File

@ -306,7 +306,7 @@ describe('MarketOperationUtils tests', () => {
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]),
);
const DEFAULT_SAMPLER = createSamplerFromSellRates(SOURCE_RATES);
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0 };
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0, sampleDistributionBase: 1 };
const defaultMarketOperationUtils = new MarketOperationUtils(
DEFAULT_SAMPLER,
contractAddresses,
@ -552,7 +552,7 @@ describe('MarketOperationUtils tests', () => {
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]),
);
const DEFAULT_SAMPLER = createSamplerFromBuyRates(SOURCE_RATES);
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0 };
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0, sampleDistributionBase: 1 };
const defaultMarketOperationUtils = new MarketOperationUtils(
DEFAULT_SAMPLER,
contractAddresses,

View File

@ -65,6 +65,9 @@ const createSamplerFromSignedOrdersWithFillableAmounts = (
return sampler;
};
// TODO(dorothy-zbornak): Replace these tests entirely with unit tests because
// omg they're a nightmare to maintain.
// tslint:disable:max-file-line-count
// tslint:disable:custom-no-magic-numbers
describe('swapQuoteCalculator', () => {
@ -272,7 +275,7 @@ describe('swapQuoteCalculator', () => {
});
});
it('calculates a correct swapQuote with slippage (feeless orders)', async () => {
const assetSellAmount = baseUnitAmount(1);
const assetSellAmount = baseUnitAmount(4);
const slippagePercentage = 0.2;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
@ -289,10 +292,10 @@ describe('swapQuoteCalculator', () => {
GAS_PRICE,
CALCULATE_SWAP_QUOTE_OPTS,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
@ -301,15 +304,15 @@ describe('swapQuoteCalculator', () => {
feeTakerAssetAmount: baseUnitAmount(0),
takerAssetAmount: assetSellAmount,
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(6),
makerAssetAmount: baseUnitAmount(9),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(0),
takerAssetAmount: assetSellAmount,
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.4),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
makerAssetAmount: baseUnitAmount(1.6),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
});
it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => {
@ -372,7 +375,7 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
@ -381,14 +384,14 @@ describe('swapQuoteCalculator', () => {
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2.25)),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(4.5),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(0.5),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(0.5)),
feeTakerAssetAmount: baseUnitAmount(1.2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.2)),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(1),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
makerAssetAmount: baseUnitAmount(1.8),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
});
it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => {
@ -411,23 +414,24 @@ describe('swapQuoteCalculator', () => {
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)),
feeTakerAssetAmount: baseUnitAmount(1.5).minus(1),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.8),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
makerAssetAmount: baseUnitAmount(4),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)),
feeTakerAssetAmount: baseUnitAmount(1.5).minus(1),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.8),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
makerAssetAmount: baseUnitAmount(4),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
});
it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => {
@ -451,24 +455,24 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct
// 50 takerAsset units to fill the first order + 100 takerAsset units for fees
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(1.5).minus(1),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(4),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.8),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(3.6),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
});
});
@ -678,7 +682,7 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
@ -692,12 +696,16 @@ describe('swapQuoteCalculator', () => {
takerAssetAmount,
totalTakerAssetAmount: takerAssetAmount,
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(0),
takerAssetAmount: baseUnitAmount(5.5),
totalTakerAssetAmount: baseUnitAmount(5.5),
takerAssetAmount: baseUnitAmount(20)
.div(6)
.integerValue(BigNumber.ROUND_UP),
totalTakerAssetAmount: baseUnitAmount(20)
.div(6)
.integerValue(BigNumber.ROUND_UP),
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
@ -766,7 +774,7 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
@ -775,12 +783,16 @@ describe('swapQuoteCalculator', () => {
takerAssetAmount: fiveSixthEthInWei,
totalTakerAssetAmount: baseUnitAmount(2.5).plus(fiveSixthEthInWei),
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.5),
takerAssetAmount: baseUnitAmount(5.5),
totalTakerAssetAmount: baseUnitAmount(8),
feeTakerAssetAmount: baseUnitAmount(3),
takerAssetAmount: baseUnitAmount(10)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 3.3333...
totalTakerAssetAmount: baseUnitAmount(19)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 6.3333...
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
@ -805,21 +817,33 @@ describe('swapQuoteCalculator', () => {
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.5),
takerAssetAmount: baseUnitAmount(2.5),
totalTakerAssetAmount: baseUnitAmount(5),
feeTakerAssetAmount: baseUnitAmount(0.5)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 0.16666...
takerAssetAmount: baseUnitAmount(0.5)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 0.1666...
totalTakerAssetAmount: baseUnitAmount(1)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 0.3333...
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.5),
takerAssetAmount: baseUnitAmount(2.5),
totalTakerAssetAmount: baseUnitAmount(5),
feeTakerAssetAmount: baseUnitAmount(0.5)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 0.16666...
takerAssetAmount: baseUnitAmount(0.5)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 0.1666...
totalTakerAssetAmount: baseUnitAmount(1)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 0.3333...
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
@ -845,14 +869,14 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2],
]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.75),
takerAssetAmount: baseUnitAmount(2.75),
totalTakerAssetAmount: baseUnitAmount(5.5),
feeTakerAssetAmount: baseUnitAmount(1.25).minus(1),
takerAssetAmount: baseUnitAmount(2.25).plus(1),
totalTakerAssetAmount: baseUnitAmount(3.5),
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
});
@ -879,7 +903,7 @@ describe('swapQuoteCalculator', () => {
.multipliedBy(0.1)
.integerValue(BigNumber.ROUND_CEIL),
makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
});
});

View File

@ -119,7 +119,7 @@ describe('swapQuoteConsumerUtils', () => {
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
swapQuoteConsumer = new SwapQuoteConsumer(provider, {

View File

@ -51,7 +51,7 @@ const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS: Array<Partial<SignedOrderWit
takerAssetAmount: baseUnitAmount(6),
makerAssetAmount: baseUnitAmount(6),
fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(2),
fillableMakerAssetAmount: baseUnitAmount(3),
},
...PARTIAL_ORDER,
},
@ -86,7 +86,7 @@ const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET: Array<Partial<Sig
makerAssetAmount: baseUnitAmount(6),
takerFee: baseUnitAmount(4),
fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(2),
fillableMakerAssetAmount: baseUnitAmount(3),
fillableTakerFeeAmount: baseUnitAmount(2),
},
...PARTIAL_ORDER_FEE_IN_TAKER_ASSET,

View File

@ -9,6 +9,10 @@
{
"note": "Update `DevUtils` mainnet, Ropsten, Rinkeby, Kovan addresses",
"pr": 2436
},
{
"note": "Update `ERC20BridgeSampler` address on kovan and mainnet.",
"pr": 2427
}
]
},

View File

@ -22,7 +22,7 @@
"devUtils": "0x5f53f2aa72cb3a9371bf3c58e8fb3a313478b2f4",
"erc20BridgeProxy": "0x8ed95d1746bf1e4dab58d8ed4724f1ef95b20db0",
"uniswapBridge": "0x533344cfdf2a3e911e2cf4c6f5ed08e791f5355f",
"erc20BridgeSampler": "0x1b402fdb5ee87f989c11e3963557e89cc313b6c0",
"erc20BridgeSampler": "0x25840bf3582cb9e5acabbf45148b3092ac3f6b56",
"kyberBridge": "0xf342f3a80fdc9b48713d58fe97e17f5cc764ee62",
"eth2DaiBridge": "0xe97ea901d034ba2e018155264f77c417ce7717f9",
"chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438",
@ -110,7 +110,7 @@
"erc20BridgeProxy": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64",
"uniswapBridge": "0x0000000000000000000000000000000000000000",
"eth2DaiBridge": "0x0000000000000000000000000000000000000000",
"erc20BridgeSampler": "0x551f0e213dcb71f676558d8b0ab559d1cdd103f2",
"erc20BridgeSampler": "0x8c9f255253bcf2c9539c887fee4d71c69fd035b9",
"kyberBridge": "0x0000000000000000000000000000000000000000",
"chaiBridge": "0x0000000000000000000000000000000000000000",
"dydxBridge": "0x0000000000000000000000000000000000000000"

View File

@ -9,6 +9,10 @@
{
"note": "Update all artifacts.",
"pr": 2432
},
{
"note": "Update `IERC20BridgeSampler artifact",
"pr": 2427
}
]
},

View File

@ -71,6 +71,94 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{ "internalType": "address", "name": "makerAddress", "type": "address" },
{ "internalType": "address", "name": "takerAddress", "type": "address" },
{ "internalType": "address", "name": "feeRecipientAddress", "type": "address" },
{ "internalType": "address", "name": "senderAddress", "type": "address" },
{ "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "makerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "takerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" },
{ "internalType": "bytes", "name": "makerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" }
],
"internalType": "struct LibOrder.Order[][]",
"name": "orders",
"type": "tuple[][]"
},
{ "internalType": "bytes[][]", "name": "orderSignatures", "type": "bytes[][]" },
{ "internalType": "address[]", "name": "sources", "type": "address[]" },
{ "internalType": "uint256[][]", "name": "makerTokenAmounts", "type": "uint256[][]" }
],
"name": "queryBatchOrdersAndSampleBuys",
"outputs": [
{
"components": [
{ "internalType": "uint256[]", "name": "orderFillableAssetAmounts", "type": "uint256[]" },
{ "internalType": "uint256[][]", "name": "tokenAmountsBySource", "type": "uint256[][]" }
],
"internalType": "struct IERC20BridgeSampler.OrdersAndSample[]",
"name": "ordersAndSamples",
"type": "tuple[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{ "internalType": "address", "name": "makerAddress", "type": "address" },
{ "internalType": "address", "name": "takerAddress", "type": "address" },
{ "internalType": "address", "name": "feeRecipientAddress", "type": "address" },
{ "internalType": "address", "name": "senderAddress", "type": "address" },
{ "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "makerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "takerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" },
{ "internalType": "bytes", "name": "makerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" }
],
"internalType": "struct LibOrder.Order[][]",
"name": "orders",
"type": "tuple[][]"
},
{ "internalType": "bytes[][]", "name": "orderSignatures", "type": "bytes[][]" },
{ "internalType": "address[]", "name": "sources", "type": "address[]" },
{ "internalType": "uint256[][]", "name": "takerTokenAmounts", "type": "uint256[][]" }
],
"name": "queryBatchOrdersAndSampleSells",
"outputs": [
{
"components": [
{ "internalType": "uint256[]", "name": "orderFillableAssetAmounts", "type": "uint256[]" },
{ "internalType": "uint256[][]", "name": "tokenAmountsBySource", "type": "uint256[][]" }
],
"internalType": "struct IERC20BridgeSampler.OrdersAndSample[]",
"name": "ordersAndSamples",
"type": "tuple[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -196,6 +284,26 @@
},
"return": "orderFillableTakerAssetAmounts How much taker asset can be filled by each order in `orders`."
},
"queryBatchOrdersAndSampleBuys((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[][],bytes[][],address[],uint256[][])": {
"details": "Query batches of native orders and sample buy quotes on multiple DEXes at once.",
"params": {
"makerTokenAmounts": "Batches of Maker token sell amount for each sample.",
"orderSignatures": "Batches of Signatures for each respective order in `orders`.",
"orders": "Batches of Native orders to query.",
"sources": "Address of each DEX. Passing in an unsupported DEX will throw."
},
"return": "ordersAndSamples How much taker asset can be filled by each order in `orders`. Taker amounts sold for each source at each maker token amount. First indexed by source index, then sample index"
},
"queryBatchOrdersAndSampleSells((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[][],bytes[][],address[],uint256[][])": {
"details": "Query batches of native orders and sample sell quotes on multiple DEXes at once.",
"params": {
"orderSignatures": "Batches of Signatures for each respective order in `orders`.",
"orders": "Batches of Native orders to query.",
"sources": "Address of each DEX. Passing in an unsupported DEX will throw.",
"takerTokenAmounts": "Batches of Taker token sell amount for each sample."
},
"return": "ordersAndSamples How much taker asset can be filled by each order in `orders`. Maker amounts bought for each source at each taker token amount. First indexed by source index, then sample index."
},
"queryOrdersAndSampleBuys((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address[],uint256[])": {
"details": "Query native orders and sample buy quotes on multiple DEXes at once.",
"params": {

View File

@ -13,6 +13,10 @@
{
"note": "Regenerate wrappers to catch empty reverts on live networks.",
"pr": 2433
},
{
"note": "Update `IERC20BridgeSampler`",
"pr": 2427
}
]
},

View File

@ -18,7 +18,7 @@
"rebuild": "yarn wrappers:clean && yarn wrappers:generate && yarn wrappers:prettier && yarn build",
"build:ci": "yarn build",
"lint": "tslint --format stylish --project . --exclude **/lib/**/*",
"fix": "tslint --fix --format stylish --project .--exclude **/lib/**/*",
"fix": "tslint --fix --format stylish --project . --exclude **/lib/**/*",
"prettier": "prettier --write **/* --config ../../.prettierrc",
"clean": "shx rm -rf lib generated_docs",
"docs_test": "typedoc --excludePrivate --excludeExternals --target ES5 --tsconfig typedoc-tsconfig.json --out generated_docs ./src/generated-wrappers/*",

View File

@ -281,6 +281,204 @@ export class IERC20BridgeSamplerContract extends BaseContract {
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [
{
name: 'orders',
type: 'tuple[][]',
components: [
{
name: 'makerAddress',
type: 'address',
},
{
name: 'takerAddress',
type: 'address',
},
{
name: 'feeRecipientAddress',
type: 'address',
},
{
name: 'senderAddress',
type: 'address',
},
{
name: 'makerAssetAmount',
type: 'uint256',
},
{
name: 'takerAssetAmount',
type: 'uint256',
},
{
name: 'makerFee',
type: 'uint256',
},
{
name: 'takerFee',
type: 'uint256',
},
{
name: 'expirationTimeSeconds',
type: 'uint256',
},
{
name: 'salt',
type: 'uint256',
},
{
name: 'makerAssetData',
type: 'bytes',
},
{
name: 'takerAssetData',
type: 'bytes',
},
{
name: 'makerFeeAssetData',
type: 'bytes',
},
{
name: 'takerFeeAssetData',
type: 'bytes',
},
],
},
{
name: 'orderSignatures',
type: 'bytes[][]',
},
{
name: 'sources',
type: 'address[]',
},
{
name: 'makerTokenAmounts',
type: 'uint256[][]',
},
],
name: 'queryBatchOrdersAndSampleBuys',
outputs: [
{
name: 'ordersAndSamples',
type: 'tuple[]',
components: [
{
name: 'orderFillableAssetAmounts',
type: 'uint256[]',
},
{
name: 'tokenAmountsBySource',
type: 'uint256[][]',
},
],
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [
{
name: 'orders',
type: 'tuple[][]',
components: [
{
name: 'makerAddress',
type: 'address',
},
{
name: 'takerAddress',
type: 'address',
},
{
name: 'feeRecipientAddress',
type: 'address',
},
{
name: 'senderAddress',
type: 'address',
},
{
name: 'makerAssetAmount',
type: 'uint256',
},
{
name: 'takerAssetAmount',
type: 'uint256',
},
{
name: 'makerFee',
type: 'uint256',
},
{
name: 'takerFee',
type: 'uint256',
},
{
name: 'expirationTimeSeconds',
type: 'uint256',
},
{
name: 'salt',
type: 'uint256',
},
{
name: 'makerAssetData',
type: 'bytes',
},
{
name: 'takerAssetData',
type: 'bytes',
},
{
name: 'makerFeeAssetData',
type: 'bytes',
},
{
name: 'takerFeeAssetData',
type: 'bytes',
},
],
},
{
name: 'orderSignatures',
type: 'bytes[][]',
},
{
name: 'sources',
type: 'address[]',
},
{
name: 'takerTokenAmounts',
type: 'uint256[][]',
},
],
name: 'queryBatchOrdersAndSampleSells',
outputs: [
{
name: 'ordersAndSamples',
type: 'tuple[]',
components: [
{
name: 'orderFillableAssetAmounts',
type: 'uint256[]',
},
{
name: 'tokenAmountsBySource',
type: 'uint256[][]',
},
],
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [
@ -654,6 +852,118 @@ export class IERC20BridgeSamplerContract extends BaseContract {
},
};
}
public queryBatchOrdersAndSampleBuys(
orders: Array<
Array<{
makerAddress: string;
takerAddress: string;
feeRecipientAddress: string;
senderAddress: string;
makerAssetAmount: BigNumber;
takerAssetAmount: BigNumber;
makerFee: BigNumber;
takerFee: BigNumber;
expirationTimeSeconds: BigNumber;
salt: BigNumber;
makerAssetData: string;
takerAssetData: string;
makerFeeAssetData: string;
takerFeeAssetData: string;
}>
>,
orderSignatures: string[][],
sources: string[],
makerTokenAmounts: BigNumber[][],
): ContractFunctionObj<Array<{ orderFillableAssetAmounts: BigNumber[]; tokenAmountsBySource: BigNumber[][] }>> {
const self = (this as any) as IERC20BridgeSamplerContract;
assert.isArray('orders', orders);
assert.isArray('orderSignatures', orderSignatures);
assert.isArray('sources', sources);
assert.isArray('makerTokenAmounts', makerTokenAmounts);
const functionSignature =
'queryBatchOrdersAndSampleBuys((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[][],bytes[][],address[],uint256[][])';
return {
async callAsync(
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<Array<{ orderFillableAssetAmounts: BigNumber[]; tokenAmountsBySource: BigNumber[][] }>> {
BaseContract._assertCallParams(callData, defaultBlock);
const rawCallResult = await self._performCallAsync(
{ ...callData, data: this.getABIEncodedTransactionData() },
defaultBlock,
);
const abiEncoder = self._lookupAbiEncoder(functionSignature);
return abiEncoder.strictDecodeReturnValue<
Array<{ orderFillableAssetAmounts: BigNumber[]; tokenAmountsBySource: BigNumber[][] }>
>(rawCallResult);
},
getABIEncodedTransactionData(): string {
return self._strictEncodeArguments(functionSignature, [
orders,
orderSignatures,
sources,
makerTokenAmounts,
]);
},
};
}
public queryBatchOrdersAndSampleSells(
orders: Array<
Array<{
makerAddress: string;
takerAddress: string;
feeRecipientAddress: string;
senderAddress: string;
makerAssetAmount: BigNumber;
takerAssetAmount: BigNumber;
makerFee: BigNumber;
takerFee: BigNumber;
expirationTimeSeconds: BigNumber;
salt: BigNumber;
makerAssetData: string;
takerAssetData: string;
makerFeeAssetData: string;
takerFeeAssetData: string;
}>
>,
orderSignatures: string[][],
sources: string[],
takerTokenAmounts: BigNumber[][],
): ContractFunctionObj<Array<{ orderFillableAssetAmounts: BigNumber[]; tokenAmountsBySource: BigNumber[][] }>> {
const self = (this as any) as IERC20BridgeSamplerContract;
assert.isArray('orders', orders);
assert.isArray('orderSignatures', orderSignatures);
assert.isArray('sources', sources);
assert.isArray('takerTokenAmounts', takerTokenAmounts);
const functionSignature =
'queryBatchOrdersAndSampleSells((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[][],bytes[][],address[],uint256[][])';
return {
async callAsync(
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<Array<{ orderFillableAssetAmounts: BigNumber[]; tokenAmountsBySource: BigNumber[][] }>> {
BaseContract._assertCallParams(callData, defaultBlock);
const rawCallResult = await self._performCallAsync(
{ ...callData, data: this.getABIEncodedTransactionData() },
defaultBlock,
);
const abiEncoder = self._lookupAbiEncoder(functionSignature);
return abiEncoder.strictDecodeReturnValue<
Array<{ orderFillableAssetAmounts: BigNumber[]; tokenAmountsBySource: BigNumber[][] }>
>(rawCallResult);
},
getABIEncodedTransactionData(): string {
return self._strictEncodeArguments(functionSignature, [
orders,
orderSignatures,
sources,
takerTokenAmounts,
]);
},
};
}
/**
* Query native orders and sample buy quotes on multiple DEXes at once.
* @param orders Native orders to query.

View File

@ -1,4 +1,13 @@
[
{
"version": "2.1.0",
"changes": [
{
"note": "Added `getBatchOrdersAsync` for fetching batches of orders",
"pr": 2427
}
]
},
{
"timestamp": 1578272714,
"version": "2.0.1",

View File

@ -25,6 +25,19 @@ export class OrderStore {
}
return orderSet;
}
public async getBatchOrderSetsForAssetsAsync(
makerAssetDatas: string[],
takerAssetDatas: string[],
): Promise<OrderSet[]> {
const orderSets: OrderSet[] = [];
for (const makerAssetData of makerAssetDatas) {
for (const takerAssetData of takerAssetDatas) {
const orderSet = await this.getOrderSetForAssetsAsync(makerAssetData, takerAssetData);
orderSets.push(orderSet);
}
}
return orderSets;
}
public async updateAsync(addedRemoved: AddedRemovedOrders): Promise<void> {
const { added, removed, assetPairKey } = addedRemoved;
const orders = await this.getOrderSetForAssetPairAsync(assetPairKey);

View File

@ -84,6 +84,26 @@ export class Orderbook {
o => o.order.makerAssetData === makerAssetData && o.order.takerAssetData === takerAssetData,
);
}
public async getBatchOrdersAsync(makerAssetDatas: string[], takerAssetDatas: string[]): Promise<APIOrder[][]> {
for (const [mi, makerAssetData] of makerAssetDatas.entries()) {
for (const [ti, takerAssetData] of makerAssetDatas.entries()) {
assert.isString(`makerAssetDatas[${mi}]`, makerAssetData);
assert.isString(`takerAssetDatas[${ti}]`, takerAssetData);
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
if (!(await this._orderStore.hasAsync(assetPairKey))) {
await this._orderProvider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
}
}
}
const orderSets = await this._orderStore.getBatchOrderSetsForAssetsAsync(makerAssetDatas, takerAssetDatas);
return orderSets.map(orderSet =>
Array.from(orderSet.values()).filter(
o =>
makerAssetDatas.includes(o.order.makerAssetData) &&
takerAssetDatas.includes(o.order.takerAssetData),
),
);
}
/**
* Returns all of the Available Asset Pairs for the provided Order Provider.
*/

View File

@ -117,17 +117,11 @@ class CleanCommandExtension(clean):
)
):
try:
remove(
path.join(
"src",
"zero_ex",
"contract_wrappers",
contract,
"__init__.py",
)
)
print(f"Removing {contract}...", end="")
remove(contract)
print("done")
except FileNotFoundError:
pass
print("file not found")
class TestPublishCommand(distutils.command.build_py.build_py):