Merge branch 'development' into feature/bignumber-8.0

This commit is contained in:
Leonid Logvinov
2019-01-18 12:53:04 +01:00
committed by GitHub
115 changed files with 1268 additions and 471 deletions

View File

@@ -8,6 +8,24 @@
}
]
},
{
"version": "4.1.0",
"changes": [
{
"note": "Adds new public method getLiquidityForAssetDataAsync, and exposes getOrdersAndFillableAmountsAsync as public method",
"pr": 1512
}
]
},
{
"timestamp": 1547747677,
"version": "4.0.2",
"changes": [
{
"note": "Dependencies updated"
}
]
},
{
"timestamp": 1547561734,
"version": "4.0.1",

View File

@@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only.
CHANGELOG
## v4.0.2 - _January 17, 2019_
* Dependencies updated
## v4.0.1 - _January 15, 2019_
* Dependencies updated

View File

@@ -1,6 +1,6 @@
{
"name": "@0x/asset-buyer",
"version": "4.0.1",
"version": "4.0.2",
"engines": {
"node": ">=6.12"
},
@@ -38,7 +38,7 @@
"dependencies": {
"@0x/assert": "^1.0.23",
"@0x/connect": "^3.0.13",
"@0x/contract-wrappers": "^5.0.0",
"@0x/contract-wrappers": "^5.0.1",
"@0x/json-schemas": "^2.1.7",
"@0x/order-utils": "^3.1.2",
"@0x/subproviders": "^2.1.11",
@@ -65,6 +65,7 @@
"shx": "^0.2.2",
"tslint": "5.11.0",
"typedoc": "0.13.0",
"typemoq": "^2.1.0",
"typescript": "3.0.1"
},
"publishConfig": {

View File

@@ -16,14 +16,16 @@ import {
BuyQuote,
BuyQuoteExecutionOpts,
BuyQuoteRequestOpts,
LiquidityForAssetData,
LiquidityRequestOpts,
OrderProvider,
OrderProviderResponse,
OrdersAndFillableAmounts,
} from './types';
import { assert } from './utils/assert';
import { assetDataUtils } from './utils/asset_data_utils';
import { buyQuoteCalculator } from './utils/buy_quote_calculator';
import { calculateLiquidity } from './utils/calculate_liquidity';
import { orderProviderResponseProcessor } from './utils/order_provider_response_processor';
interface OrdersEntry {
@@ -138,10 +140,10 @@ export class AssetBuyer {
// get the relevant orders for the makerAsset and fees
// if the requested assetData is ZRX, don't get the fee info
const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([
this._getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh),
this.getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh),
isMakerAssetZrxToken
? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS)
: this._getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh),
: this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh),
shouldForceOrderRefresh,
]);
if (ordersAndFillableAmounts.orders.length === 0) {
@@ -177,6 +179,40 @@ export class AssetBuyer {
const buyQuote = this.getBuyQuoteAsync(assetData, assetBuyAmount, options);
return buyQuote;
}
/**
* Returns information about available liquidity for an asset
* Does not factor in slippage or fees
* @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param options Options for the request. See type definition for more information.
*
* @return An object that conforms to LiquidityForAssetData that satisfies the request. See type definition for more information.
*/
public async getLiquidityForAssetDataAsync(
assetData: string,
options: Partial<LiquidityRequestOpts> = {},
): Promise<LiquidityForAssetData> {
const shouldForceOrderRefresh =
options.shouldForceOrderRefresh !== undefined ? options.shouldForceOrderRefresh : false;
assetDataUtils.decodeAssetDataOrThrow(assetData);
assert.isBoolean('options.shouldForceOrderRefresh', shouldForceOrderRefresh);
const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(assetData);
const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow();
if (!assetPairs.includes(etherTokenAssetData)) {
return {
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
};
}
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(
assetData,
shouldForceOrderRefresh,
);
return calculateLiquidity(ordersAndFillableAmounts);
}
/**
* Given a BuyQuote and desired rate, attempt to execute the buy.
* @param buyQuote An object that conforms to BuyQuote. See type definition for more information.
@@ -260,8 +296,10 @@ export class AssetBuyer {
}
/**
* Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders
* @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param shouldForceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs.
*/
private async _getOrdersAndFillableAmountsAsync(
public async getOrdersAndFillableAmountsAsync(
assetData: string,
shouldForceOrderRefresh: boolean,
): Promise<OrdersAndFillableAmounts> {

View File

@@ -19,6 +19,9 @@ export {
BuyQuoteExecutionOpts,
BuyQuoteInfo,
BuyQuoteRequestOpts,
LiquidityForAssetData,
LiquidityRequestOpts,
OrdersAndFillableAmounts,
OrderProvider,
OrderProviderRequest,
OrderProviderResponse,

View File

@@ -75,6 +75,13 @@ export interface BuyQuoteRequestOpts {
slippagePercentage: number;
}
/*
* Options for checking liquidity
*
* shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false.
*/
export type LiquidityRequestOpts = Pick<BuyQuoteRequestOpts, 'shouldForceOrderRefresh'>;
/**
* ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount.
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
@@ -117,7 +124,19 @@ export enum AssetBuyerError {
TransactionValueTooLow = 'TRANSACTION_VALUE_TOO_LOW',
}
/**
* orders: An array of signed orders
* remainingFillableMakerAssetAmounts: A list of fillable amounts for the signed orders. The index of an item in the array associates the amount with the corresponding order.
*/
export interface OrdersAndFillableAmounts {
orders: SignedOrder[];
remainingFillableMakerAssetAmounts: BigNumber[];
}
/**
* Represents available liquidity for a given assetData
*/
export interface LiquidityForAssetData {
tokensAvailableInBaseUnits: BigNumber;
ethValueAvailableInWei: BigNumber;
}

View File

@@ -0,0 +1,34 @@
import { BigNumber } from '@0x/utils';
import { LiquidityForAssetData, OrdersAndFillableAmounts } from '../types';
import { orderUtils } from './order_utils';
export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAmounts): LiquidityForAssetData => {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
const liquidityInBigNumbers = orders.reduce(
(acc, order, curIndex) => {
const availableMakerAssetAmount = remainingFillableMakerAssetAmounts[curIndex];
if (availableMakerAssetAmount === undefined) {
throw new Error(`No corresponding fillableMakerAssetAmounts at index ${curIndex}`);
}
const tokensAvailableForCurrentOrder = availableMakerAssetAmount;
const ethValueAvailableForCurrentOrder = orderUtils.getTakerFillAmount(order, availableMakerAssetAmount);
return {
tokensAvailableInBaseUnits: acc.tokensAvailableInBaseUnits.plus(tokensAvailableForCurrentOrder),
ethValueAvailableInWei: acc.ethValueAvailableInWei.plus(ethValueAvailableForCurrentOrder),
};
},
{
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
},
);
// Turn into regular numbers
return {
tokensAvailableInBaseUnits: liquidityInBigNumbers.tokensAvailableInBaseUnits,
ethValueAvailableInWei: liquidityInBigNumbers.ethValueAvailableInWei,
};
};

View File

@@ -0,0 +1,212 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
import { Web3ProviderEngine } from '@0x/subproviders';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { AssetBuyer } from '../src';
import { constants } from '../src/constants';
import { LiquidityForAssetData, OrderProvider, OrdersAndFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import {
mockAvailableAssetDatas,
mockedAssetBuyerWithOrdersAndFillableAmounts,
orderProviderMock,
} from './utils/mocks';
chaiSetup.configure();
const expect = chai.expect;
const FAKE_SRA_URL = 'https://fakeurl.com';
const FAKE_ASSET_DATA = '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48';
const TOKEN_DECIMALS = 18;
const DAI_ASSET_DATA = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359"';
const WETH_ASSET_DATA = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS;
const baseUnitAmount = (unitAmount: number, decimals = TOKEN_DECIMALS): BigNumber => {
return Web3Wrapper.toBaseUnitAmount(new BigNumber(unitAmount), decimals);
};
const expectLiquidityResult = async (
web3Provider: Web3ProviderEngine,
orderProvider: OrderProvider,
ordersAndFillableAmounts: OrdersAndFillableAmounts,
expectedLiquidityResult: LiquidityForAssetData,
) => {
const mockedAssetBuyer = mockedAssetBuyerWithOrdersAndFillableAmounts(
web3Provider,
orderProvider,
FAKE_ASSET_DATA,
ordersAndFillableAmounts,
);
const liquidityResult = await mockedAssetBuyer.object.getLiquidityForAssetDataAsync(FAKE_ASSET_DATA);
expect(liquidityResult).to.deep.equal(expectedLiquidityResult);
};
// tslint:disable:custom-no-magic-numbers
describe('AssetBuyer', () => {
describe('getLiquidityForAssetDataAsync', () => {
const mockWeb3Provider = TypeMoq.Mock.ofType(Web3ProviderEngine);
const mockOrderProvider = orderProviderMock();
beforeEach(() => {
mockWeb3Provider.reset();
mockOrderProvider.reset();
});
afterEach(() => {
mockWeb3Provider.verifyAll();
mockOrderProvider.verifyAll();
});
describe('validation', () => {
it('should ensure assetData is a string', async () => {
const assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(
mockWeb3Provider.object,
FAKE_SRA_URL,
);
expect(assetBuyer.getLiquidityForAssetDataAsync(false as any)).to.be.rejectedWith(
'Expected assetData to be of type string, encountered: false',
);
});
});
describe('asset pair not supported', () => {
it('should return 0s when no asset pair not supported', async () => {
mockAvailableAssetDatas(mockOrderProvider, FAKE_ASSET_DATA, []);
const assetBuyer = new AssetBuyer(mockWeb3Provider.object, mockOrderProvider.object);
const liquidityResult = await assetBuyer.getLiquidityForAssetDataAsync(FAKE_ASSET_DATA);
expect(liquidityResult).to.deep.equal({
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
});
});
it('should return 0s when only other asset pair supported', async () => {
mockAvailableAssetDatas(mockOrderProvider, FAKE_ASSET_DATA, [DAI_ASSET_DATA]);
const assetBuyer = new AssetBuyer(mockWeb3Provider.object, mockOrderProvider.object);
const liquidityResult = await assetBuyer.getLiquidityForAssetDataAsync(FAKE_ASSET_DATA);
expect(liquidityResult).to.deep.equal({
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
});
});
});
describe('assetData is supported', () => {
// orders
const sellTwoTokensFor1Weth: SignedOrder = orderFactory.createSignedOrderFromPartial({
makerAssetAmount: baseUnitAmount(2),
takerAssetAmount: baseUnitAmount(1, WETH_DECIMALS),
});
const sellTenTokensFor10Weth: SignedOrder = orderFactory.createSignedOrderFromPartial({
makerAssetAmount: baseUnitAmount(10),
takerAssetAmount: baseUnitAmount(10, WETH_DECIMALS),
});
beforeEach(() => {
mockAvailableAssetDatas(mockOrderProvider, FAKE_ASSET_DATA, [WETH_ASSET_DATA]);
});
it('should return 0s when no orders available', async () => {
const ordersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: [],
remainingFillableMakerAssetAmounts: [],
};
const expectedResult = {
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
ordersAndFillableAmounts,
expectedResult,
);
});
it('should return correct computed value when orders provided with full fillableAmounts', async () => {
const orders: SignedOrder[] = [sellTwoTokensFor1Weth, sellTenTokensFor10Weth];
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth],
remainingFillableMakerAssetAmounts: orders.map(o => o.makerAssetAmount),
};
const expectedTokensAvailable = orders[0].makerAssetAmount.plus(orders[1].makerAssetAmount);
const expectedEthValueAvailable = orders[0].takerAssetAmount.plus(orders[1].takerAssetAmount);
const expectedResult = {
tokensAvailableInBaseUnits: expectedTokensAvailable,
ethValueAvailableInWei: expectedEthValueAvailable,
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
ordersAndFillableAmounts,
expectedResult,
);
});
it('should return correct computed value with one partial fillableAmounts', async () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1Weth],
remainingFillableMakerAssetAmounts: [baseUnitAmount(1)],
};
const expectedResult = {
tokensAvailableInBaseUnits: baseUnitAmount(1),
ethValueAvailableInWei: baseUnitAmount(0.5, WETH_DECIMALS),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
ordersAndFillableAmounts,
expectedResult,
);
});
it('should return correct computed value with multiple orders and fillable amounts', async () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth],
remainingFillableMakerAssetAmounts: [baseUnitAmount(1), baseUnitAmount(3)],
};
const expectedResult = {
tokensAvailableInBaseUnits: baseUnitAmount(4),
ethValueAvailableInWei: baseUnitAmount(3.5, WETH_DECIMALS),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
ordersAndFillableAmounts,
expectedResult,
);
});
it('should return 0s when no amounts fillable', async () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth],
remainingFillableMakerAssetAmounts: [baseUnitAmount(0), baseUnitAmount(0)],
};
const expectedResult = {
tokensAvailableInBaseUnits: baseUnitAmount(0),
ethValueAvailableInWei: baseUnitAmount(0, WETH_DECIMALS),
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
ordersAndFillableAmounts,
expectedResult,
);
});
});
});
});

View File

@@ -168,7 +168,7 @@ describe('buyQuoteCalculator', () => {
};
testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(1));
});
it('should throw without amount available to fill if amount rounds to 0', () => {
it('should throw with 0 available to fill if amount rounds to 0', () => {
const smallOrder = orderFactory.createSignedOrderFromPartial({
makerAssetAmount: new BigNumber(1),
takerAssetAmount: new BigNumber(1),
@@ -184,7 +184,7 @@ describe('buyQuoteCalculator', () => {
false,
);
};
testHelpers.expectInsufficientLiquidityError(expect, errorFunction, undefined);
testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(0));
});
});
it('should not throw if order is fillable', () => {

View File

@@ -0,0 +1,68 @@
import { Web3ProviderEngine } from '@0x/subproviders';
import * as TypeMoq from 'typemoq';
import { AssetBuyer } from '../../src/asset_buyer';
import { OrderProvider, OrderProviderResponse, OrdersAndFillableAmounts } from '../../src/types';
// tslint:disable:promise-function-async
// Implementing dummy class for using in mocks, see https://github.com/florinn/typemoq/issues/3
class OrderProviderClass implements OrderProvider {
// tslint:disable-next-line:prefer-function-over-method
public async getOrdersAsync(): Promise<OrderProviderResponse> {
return Promise.resolve({ orders: [] });
}
// tslint:disable-next-line:prefer-function-over-method
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
return Promise.resolve([]);
}
}
export const orderProviderMock = () => {
return TypeMoq.Mock.ofType(OrderProviderClass, TypeMoq.MockBehavior.Strict);
};
export const mockAvailableAssetDatas = (
mockOrderProvider: TypeMoq.IMock<OrderProviderClass>,
assetData: string,
availableAssetDatas: string[],
) => {
mockOrderProvider
.setup(op => op.getAvailableMakerAssetDatasAsync(TypeMoq.It.isValue(assetData)))
.returns(() => {
return Promise.resolve(availableAssetDatas);
})
.verifiable(TypeMoq.Times.once());
};
const partiallyMockedAssetBuyer = (
provider: Web3ProviderEngine,
orderProvider: OrderProvider,
): TypeMoq.IMock<AssetBuyer> => {
const rawAssetBuyer = new AssetBuyer(provider, orderProvider);
const mockedAssetBuyer = TypeMoq.Mock.ofInstance(rawAssetBuyer, TypeMoq.MockBehavior.Loose, false);
mockedAssetBuyer.callBase = true;
return mockedAssetBuyer;
};
const mockGetOrdersAndAvailableAmounts = (
mockedAssetBuyer: TypeMoq.IMock<AssetBuyer>,
assetData: string,
ordersAndFillableAmounts: OrdersAndFillableAmounts,
): void => {
mockedAssetBuyer
.setup(a => a.getOrdersAndFillableAmountsAsync(assetData, false))
.returns(() => Promise.resolve(ordersAndFillableAmounts))
.verifiable(TypeMoq.Times.once());
};
export const mockedAssetBuyerWithOrdersAndFillableAmounts = (
provider: Web3ProviderEngine,
orderProvider: OrderProvider,
assetData: string,
ordersAndFillableAmounts: OrdersAndFillableAmounts,
): TypeMoq.IMock<AssetBuyer> => {
const mockedAssetBuyer = partiallyMockedAssetBuyer(provider, orderProvider);
mockGetOrdersAndAvailableAmounts(mockedAssetBuyer, assetData, ordersAndFillableAmounts);
return mockedAssetBuyer;
};

View File

@@ -6,7 +6,7 @@ export const testHelpers = {
expectInsufficientLiquidityError: (
expect: Chai.ExpectStatic,
functionWhichTriggersError: () => void,
expectedAmountAvailableToFill?: BigNumber,
expectedAmountAvailableToFill: BigNumber,
): void => {
let wasErrorThrown = false;
try {