Merge pull request #2056 from 0xProject/feature/orderbook

@0x/orderbook
This commit is contained in:
Jacob Evans
2019-09-16 23:05:11 +01:00
committed by GitHub
41 changed files with 4439 additions and 1100 deletions

View File

@@ -1,4 +1,13 @@
[
{
"version": "2.0.0",
"changes": [
{
"note": "AssetSwapper to use `@0x/orderbook` to fetch and subscribe to order updates",
"pr": 2056
}
]
},
{
"timestamp": 1567521715,
"version": "1.0.3",

File diff suppressed because it is too large Load Diff

View File

@@ -33,13 +33,14 @@
"type": "git",
"url": "https://github.com/0xProject/0x-monorepo.git"
},
"author": "",
"author": "David Sun",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/0xProject/0x-monorepo/issues"
},
"homepage": "https://github.com/0xProject/0x-monorepo/packages/asset-buyer/README.md",
"dependencies": {
"@0x/orderbook": "^0.0.1",
"@0x/assert": "^2.1.5",
"@0x/connect": "^5.0.18",
"@0x/contract-addresses": "^3.1.0",
@@ -68,6 +69,7 @@
"chai-bignumber": "^3.0.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
"@0x/mesh-rpc-client": "^4.0.1-beta",
"mocha": "^6.2.0",
"npm-run-all": "^4.1.2",
"nyc": "^11.0.1",

View File

@@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils';
import {
ForwarderSwapQuoteExecutionOpts,
ForwarderSwapQuoteGetOutputOpts,
LiquidityRequestOpts,
OrdersAndFillableAmounts,
SwapQuoteRequestOpts,
SwapQuoterOpts,
@@ -30,7 +29,6 @@ const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: ForwarderSwapQuoteGetOutputOpts = {
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: ForwarderSwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
shouldForceOrderRefresh: false,
shouldDisableRequestingFeeOrders: false,
slippagePercentage: 0.2, // 20% slippage protection,
};
@@ -40,10 +38,6 @@ const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = {
remainingFillableMakerAssetAmounts: [] as BigNumber[],
};
const DEFAULT_LIQUIDITY_REQUEST_OPTS: LiquidityRequestOpts = {
shouldForceOrderRefresh: false,
};
export const constants = {
NULL_BYTES,
ZERO_AMOUNT: new BigNumber(0),
@@ -58,5 +52,4 @@ export const constants = {
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
EMPTY_ORDERS_AND_FILLABLE_AMOUNTS,
DEFAULT_PER_PAGE,
DEFAULT_LIQUIDITY_REQUEST_OPTS,
};

View File

@@ -20,16 +20,13 @@ export {
ConstructorStateMutability,
} from 'ethereum-types';
export { SignedOrder } from '@0x/types';
export { SignedOrder, AssetPairsItem, APIOrder, Asset } from '@0x/types';
export { BigNumber } from '@0x/utils';
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
export { SwapQuoter } from './swap_quoter';
export { InsufficientAssetLiquidityError } from './errors';
export { BasicOrderProvider } from './order_providers/basic_order_provider';
export { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider';
export {
SwapQuoterError,
SwapQuoterOpts,
@@ -50,12 +47,22 @@ export {
MarketBuySwapQuoteWithAffiliateFee,
MarketSellSwapQuoteWithAffiliateFee,
LiquidityForAssetData,
LiquidityRequestOpts,
OrdersAndFillableAmounts,
OrderProvider,
OrderProviderRequest,
OrderProviderResponse,
SignedOrderWithRemainingFillableMakerAssetAmount,
SwapQuoteConsumerBase,
SwapQuoteRequestOpts,
} from './types';
export {
Orderbook,
MeshOrderProviderOpts,
SRAPollingOrderProviderOpts,
SRAWebsocketOrderProviderOpts,
BaseOrderProvider,
OrderStore,
AcceptedRejectedOrders,
RejectedOrder,
AddedRemovedOrders,
OrderSet,
} from '@0x/orderbook';
export { WSOpts } from '@0x/mesh-rpc-client';

View File

@@ -1,50 +0,0 @@
import { schemas } from '@0x/json-schemas';
import { SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { OrderProvider, OrderProviderRequest, OrderProviderResponse } from '../types';
import { assert } from '../utils/assert';
export class BasicOrderProvider implements OrderProvider {
public readonly orders: SignedOrder[];
/**
* Instantiates a new BasicOrderProvider instance
* @param orders An array of objects that conform to SignedOrder to fetch from.
* @return An instance of BasicOrderProvider
*/
constructor(orders: SignedOrder[]) {
assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
this.orders = orders;
}
/**
* Given an object that conforms to OrderFetcherRequest, return the corresponding OrderProviderResponse that satisfies the request.
* @param orderProviderRequest An instance of OrderFetcherRequest. See type for more information.
* @return An instance of OrderProviderResponse. See type for more information.
*/
public async getOrdersAsync(orderProviderRequest: OrderProviderRequest): Promise<OrderProviderResponse> {
assert.isValidOrderProviderRequest('orderProviderRequest', orderProviderRequest);
const { makerAssetData, takerAssetData } = orderProviderRequest;
const orders = _.filter(this.orders, order => {
return order.makerAssetData === makerAssetData && order.takerAssetData === takerAssetData;
});
return { orders };
}
/**
* Given a taker asset data string, return all availabled paired maker asset data strings.
* @param takerAssetData A string representing the taker asset data.
* @return An array of asset data strings that can be purchased using takerAssetData.
*/
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
const ordersWithTakerAssetData = _.filter(this.orders, { takerAssetData });
return _.map(ordersWithTakerAssetData, order => order.makerAssetData);
}
/**
* Given a maker asset data string, return all availabled paired taker asset data strings.
* @param makerAssetData A string representing the maker asset data.
* @return An array of asset data strings that can be used to purchased makerAssetData.
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
const ordersWithMakerAssetData = _.filter(this.orders, { makerAssetData });
return _.map(ordersWithMakerAssetData, order => order.takerAssetData);
}
}

View File

@@ -1,141 +0,0 @@
import { HttpClient } from '@0x/connect';
import { orderCalculationUtils } from '@0x/order-utils';
import { APIOrder, AssetPairsResponse, OrderbookResponse } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
OrderProvider,
OrderProviderRequest,
OrderProviderResponse,
SignedOrderWithRemainingFillableMakerAssetAmount,
SwapQuoterError,
} from '../types';
import { assert } from '../utils/assert';
export class StandardRelayerAPIOrderProvider implements OrderProvider {
public readonly apiUrl: string;
public readonly networkId: number;
private readonly _sraClient: HttpClient;
/**
* Given an array of APIOrder objects from a standard relayer api, return an array
* of SignedOrderWithRemainingFillableMakerAssetAmounts
*/
private static _getSignedOrderWithRemainingFillableMakerAssetAmountFromApi(
apiOrders: APIOrder[],
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
const result = _.map(apiOrders, apiOrder => {
const { order, metaData } = apiOrder;
// The contents of metaData is not explicity defined in the spec
// We check for remainingTakerAssetAmount as a string and use this value if populated
const metaDataRemainingTakerAssetAmount = _.get(metaData, 'remainingTakerAssetAmount') as
| string
| undefined;
const remainingFillableTakerAssetAmount = metaDataRemainingTakerAssetAmount
? new BigNumber(metaDataRemainingTakerAssetAmount)
: order.takerAssetAmount;
const remainingFillableMakerAssetAmount = orderCalculationUtils.getMakerFillAmount(
order,
remainingFillableTakerAssetAmount,
);
const newOrder = {
...order,
remainingFillableMakerAssetAmount,
};
return newOrder;
});
return result;
}
/**
* Instantiates a new StandardRelayerAPIOrderProvider instance
* @param apiUrl The standard relayer API base HTTP url you would like to source orders from.
* @param networkId The ethereum network id.
* @return An instance of StandardRelayerAPIOrderProvider
*/
constructor(apiUrl: string, networkId: number) {
assert.isWebUri('apiUrl', apiUrl);
assert.isNumber('networkId', networkId);
this.apiUrl = apiUrl;
this.networkId = networkId;
this._sraClient = new HttpClient(apiUrl);
}
/**
* Given an object that conforms to OrderProviderRequest, return the corresponding OrderProviderResponse that satisfies the request.
* @param orderProviderRequest An instance of OrderProviderRequest. See type for more information.
* @return An instance of OrderProviderResponse. See type for more information.
*/
public async getOrdersAsync(orderProviderRequest: OrderProviderRequest): Promise<OrderProviderResponse> {
assert.isValidOrderProviderRequest('orderProviderRequest', orderProviderRequest);
const { makerAssetData, takerAssetData } = orderProviderRequest;
const orderbookRequest = { baseAssetData: makerAssetData, quoteAssetData: takerAssetData };
const requestOpts = { networkId: this.networkId };
let orderbook: OrderbookResponse;
try {
orderbook = await this._sraClient.getOrderbookAsync(orderbookRequest, requestOpts);
} catch (err) {
throw new Error(SwapQuoterError.StandardRelayerApiError);
}
const apiOrders = orderbook.asks.records;
const orders = StandardRelayerAPIOrderProvider._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi(
apiOrders,
);
return {
orders,
};
}
/**
* Given a taker asset data string, return all available paired maker asset data strings.
* @param takerAssetData A string representing the taker asset data.
* @return An array of asset data strings that can be purchased using takerAssetData.
*/
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
// Return a maximum of 1000 asset datas
const maxPerPage = 1000;
const requestOpts = { networkId: this.networkId, perPage: maxPerPage };
const assetPairsRequest = { assetDataA: takerAssetData };
const fullRequest = {
...requestOpts,
...assetPairsRequest,
};
let response: AssetPairsResponse;
try {
response = await this._sraClient.getAssetPairsAsync(fullRequest);
} catch (err) {
throw new Error(SwapQuoterError.StandardRelayerApiError);
}
return _.map(response.records, item => {
if (item.assetDataA.assetData === takerAssetData) {
return item.assetDataB.assetData;
} else {
return item.assetDataA.assetData;
}
});
}
/**
* Given a maker asset data string, return all availabled paired taker asset data strings.
* @param makerAssetData A string representing the maker asset data.
* @return An array of asset data strings that can be used to purchased makerAssetData.
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
const requestOpts = { networkId: this.networkId, perPage: constants.DEFAULT_PER_PAGE };
const assetPairsRequest = { assetDataA: makerAssetData };
const fullRequest = {
...requestOpts,
...assetPairsRequest,
};
let response: AssetPairsResponse;
try {
response = await this._sraClient.getAssetPairsAsync(fullRequest);
} catch (err) {
throw new Error(SwapQuoterError.StandardRelayerApiError);
}
return _.map(response.records, item => {
if (item.assetDataA.assetData === makerAssetData) {
return item.assetDataB.assetData;
} else {
return item.assetDataA.assetData;
}
});
}
}

View File

@@ -51,7 +51,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
*/
public async getCalldataOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts>,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(quote, opts);
@@ -65,7 +65,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
*/
public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts>,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<SmartContractParamsInfo<SmartContractParams>> {
assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(quote, opts);
@@ -79,7 +79,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
*/
public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts>,
opts: Partial<SwapQuoteExecutionOpts> = {},
): Promise<string> {
assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(quote, opts);

View File

@@ -1,52 +1,39 @@
import { ContractWrappers } from '@0x/contract-wrappers';
import { schemas } from '@0x/json-schemas';
import { assetDataUtils, SignedOrder } from '@0x/order-utils';
import { MarketOperation, ObjectMap } from '@0x/types';
import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { MarketOperation } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from './constants';
import { BasicOrderProvider } from './order_providers/basic_order_provider';
import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider';
import {
LiquidityForAssetData,
LiquidityRequestOpts,
MarketBuySwapQuote,
MarketSellSwapQuote,
OrderProvider,
OrdersAndFillableAmounts,
SwapQuote,
SwapQuoteRequestOpts,
SwapQuoterError,
SwapQuoterOpts,
} from './types';
import { assert } from './utils/assert';
import { calculateLiquidity } from './utils/calculate_liquidity';
import { orderProviderResponseProcessor } from './utils/order_provider_response_processor';
import { swapQuoteCalculator } from './utils/swap_quote_calculator';
interface OrdersEntry {
ordersAndFillableAmounts: OrdersAndFillableAmounts;
lastRefreshTime: number;
}
export class SwapQuoter {
public readonly provider: ZeroExProvider;
public readonly orderProvider: OrderProvider;
public readonly networkId: number;
public readonly orderRefreshIntervalMs: number;
public readonly orderbook: Orderbook;
public readonly expiryBufferMs: number;
private readonly _contractWrappers: ContractWrappers;
// cache of orders along with the time last updated keyed by assetData
private readonly _ordersEntryMap: ObjectMap<OrdersEntry> = {};
/**
* Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData.
* @param options Initialization options for the SwapQuoter. See type definition for details.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData.
* @param options Initialization options for the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
@@ -57,66 +44,102 @@ export class SwapQuoter {
): SwapQuoter {
assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
assert.assert(orders.length !== 0, `Expected orders to contain at least one order`);
const orderProvider = new BasicOrderProvider(orders);
const swapQuoter = new SwapQuoter(supportedProvider, orderProvider, options);
const orderbook = Orderbook.getOrderbookForProvidedOrders(orders);
const swapQuoter = new SwapQuoter(supportedProvider, orderbook, options);
return swapQuoter;
}
/**
* Instantiates a new SwapQuoter instance given a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from.
* @param options Initialization options for the SwapQuoter. See type definition for details.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from.
* @param options Initialization options for the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
public static getSwapQuoterForStandardRelayerAPIUrl(
supportedProvider: SupportedProvider,
sraApiUrl: string,
options: Partial<SwapQuoterOpts & SRAPollingOrderProviderOpts> = {},
): SwapQuoter {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isWebUri('sraApiUrl', sraApiUrl);
const orderbook = Orderbook.getOrderbookForPollingProvider({
httpEndpoint: sraApiUrl,
pollingIntervalMs:
options.orderRefreshIntervalMs || constants.DEFAULT_SWAP_QUOTER_OPTS.orderRefreshIntervalMs,
networkId: options.networkId || constants.DEFAULT_SWAP_QUOTER_OPTS.networkId,
perPage: options.perPage || constants.DEFAULT_PER_PAGE,
});
const swapQuoter = new SwapQuoter(provider, orderbook, options);
return swapQuoter;
}
/**
* Instantiates a new SwapQuoter instance given a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint
* and a websocket endpoint. This is more effecient than `getSwapQuoterForStandardRelayerAPIUrl` when requesting multiple quotes.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from.
* @param sraWebsocketApiUrl The standard relayer API Websocket url you would like to subscribe to.
* @param options Initialization options for the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
public static getSwapQuoterForStandardRelayerAPIWebsocket(
supportedProvider: SupportedProvider,
sraApiUrl: string,
sraWebsocketAPIUrl: string,
options: Partial<SwapQuoterOpts> = {},
): SwapQuoter {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isWebUri('sraApiUrl', sraApiUrl);
const networkId = options.networkId || constants.DEFAULT_SWAP_QUOTER_OPTS.networkId;
const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl, networkId);
const swapQuoter = new SwapQuoter(provider, orderProvider, options);
assert.isUri('sraWebsocketAPIUrl', sraWebsocketAPIUrl);
const orderbook = Orderbook.getOrderbookForWebsocketProvider({
httpEndpoint: sraApiUrl,
websocketEndpoint: sraWebsocketAPIUrl,
networkId: options.networkId,
});
const swapQuoter = new SwapQuoter(provider, orderbook, options);
return swapQuoter;
}
/**
* Instantiates a new SwapQuoter instance given a 0x Mesh endpoint. This pulls all available liquidity stored in Mesh
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param meshEndpoint The standard relayer API base HTTP url you would like to source orders from.
* @param options Initialization options for the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
public static getSwapQuoterForMeshEndpoint(
supportedProvider: SupportedProvider,
meshEndpoint: string,
options: Partial<SwapQuoterOpts & MeshOrderProviderOpts> = {},
): SwapQuoter {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isUri('meshEndpoint', meshEndpoint);
const orderbook = Orderbook.getOrderbookForMeshProvider({
websocketEndpoint: meshEndpoint,
wsOpts: options.wsOpts,
});
const swapQuoter = new SwapQuoter(provider, orderbook, options);
return swapQuoter;
}
/**
*
* get the key for _orderEntryMap for maker + taker asset pair
*/
private static _getOrdersEntryMapKey(makerAssetData: string, takerAssetData: string): string {
return `${makerAssetData}_${takerAssetData}`;
}
/**
* Instantiates a new SwapQuoter instance
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param orderProvider An object that conforms to OrderProvider, see type for definition.
* @param orderbook An object that conforms to Orderbook, see type for definition.
* @param options Initialization options for the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
constructor(
supportedProvider: SupportedProvider,
orderProvider: OrderProvider,
options: Partial<SwapQuoterOpts> = {},
) {
const { networkId, orderRefreshIntervalMs, expiryBufferMs } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTER_OPTS,
options,
);
constructor(supportedProvider: SupportedProvider, orderbook: Orderbook, options: Partial<SwapQuoterOpts> = {}) {
const { networkId, expiryBufferMs } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderProvider('orderProvider', orderProvider);
assert.isValidOrderbook('orderbook', orderbook);
assert.isNumber('networkId', networkId);
assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
assert.isNumber('expiryBufferMs', expiryBufferMs);
this.provider = provider;
this.orderProvider = orderProvider;
this.networkId = networkId;
this.orderRefreshIntervalMs = orderRefreshIntervalMs;
this.orderbook = orderbook;
this.expiryBufferMs = expiryBufferMs;
this._contractWrappers = new ContractWrappers(this.provider, {
networkId,
@@ -238,23 +261,19 @@ export class SwapQuoter {
* Does not factor in slippage or fees
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (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 getLiquidityForMakerTakerAssetDataPairAsync(
makerAssetData: string,
takerAssetData: string,
options: Partial<LiquidityRequestOpts> = {},
): Promise<LiquidityForAssetData> {
const { shouldForceOrderRefresh } = _.merge({}, constants.DEFAULT_LIQUIDITY_REQUEST_OPTS, options);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assert.isBoolean('options.shouldForceOrderRefresh', shouldForceOrderRefresh);
const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData);
const assetPairs = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
if (!assetPairs.includes(makerAssetData)) {
return {
makerTokensAvailableInBaseUnits: new BigNumber(0),
@@ -262,12 +281,7 @@ export class SwapQuoter {
};
}
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(
makerAssetData,
takerAssetData,
shouldForceOrderRefresh,
);
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData);
return calculateLiquidity(ordersAndFillableAmounts);
}
@@ -279,7 +293,11 @@ export class SwapQuoter {
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
assert.isString('makerAssetData', makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
return this.orderProvider.getAvailableTakerAssetDatasAsync(makerAssetData);
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs
.filter(pair => pair.assetDataA.assetData === makerAssetData)
.map(pair => pair.assetDataB.assetData);
return assetPairs;
}
/**
@@ -290,7 +308,11 @@ export class SwapQuoter {
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
return this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData);
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs
.filter(pair => pair.assetDataB.assetData === takerAssetData)
.map(pair => pair.assetDataA.assetData);
return assetPairs;
}
/**
@@ -314,61 +336,30 @@ export class SwapQuoter {
* Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (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.
*/
public async getOrdersAndFillableAmountsAsync(
makerAssetData: string,
takerAssetData: string,
shouldForceOrderRefresh: boolean,
): Promise<OrdersAndFillableAmounts> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
// try to get ordersEntry from the map
const ordersEntryIfExists = this._ordersEntryMap[
SwapQuoter._getOrdersEntryMapKey(makerAssetData, takerAssetData)
];
// we should refresh if:
// we do not have any orders OR
// we are forced to OR
// we have some last refresh time AND that time was sufficiently long ago
const shouldRefresh =
ordersEntryIfExists === undefined ||
shouldForceOrderRefresh ||
// tslint:disable:restrict-plus-operands
ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now();
if (!shouldRefresh) {
const result = ordersEntryIfExists.ordersAndFillableAmounts;
return result;
}
const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
// construct orderProvider request
const orderProviderRequest = {
makerAssetData,
takerAssetData,
networkId: this.networkId,
};
const request = orderProviderRequest;
// get provider response
const response = await this.orderProvider.getOrdersAsync(request);
// get orders
const response = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData);
const adaptedResponse = { orders: response.map(o => ({ ...o.order, ...o.metaData })) };
// since the order provider is an injected dependency, validate that it respects the API
// ie. it should only return maker/taker assetDatas that are specified
orderProviderResponseProcessor.throwIfInvalidResponse(response, request);
orderProviderResponseProcessor.throwIfInvalidResponse(adaptedResponse, makerAssetData, takerAssetData);
// process the responses into one object
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync(
response,
adaptedResponse,
isMakerAssetZrxToken,
this.expiryBufferMs,
this._contractWrappers.orderValidator,
);
const lastRefreshTime = Date.now();
const updatedOrdersEntry = {
ordersAndFillableAmounts,
lastRefreshTime,
};
this._ordersEntryMap[SwapQuoter._getOrdersEntryMapKey(makerAssetData, takerAssetData)] = updatedOrdersEntry;
return ordersAndFillableAmounts;
}
@@ -393,6 +384,13 @@ export class SwapQuoter {
];
}
/**
* Destroys any subscriptions or connections.
*/
public async destroyAsync(): Promise<void> {
return this.orderbook.destroyAsync();
}
/**
* Get the assetData that represents the ZRX token.
* Will throw if ZRX does not exist for the current network.
@@ -411,25 +409,23 @@ export class SwapQuoter {
marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> {
const { shouldForceOrderRefresh, slippagePercentage, shouldDisableRequestingFeeOrders } = _.merge(
const { slippagePercentage, shouldDisableRequestingFeeOrders } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh);
assert.isNumber('slippagePercentage', slippagePercentage);
const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
// 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(makerAssetData, takerAssetData, shouldForceOrderRefresh),
this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData),
shouldDisableRequestingFeeOrders || isMakerAssetZrxToken
? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS)
: this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, takerAssetData, shouldForceOrderRefresh),
shouldForceOrderRefresh,
: this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, takerAssetData),
]);
if (ordersAndFillableAmounts.orders.length === 0) {

View File

@@ -28,17 +28,6 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed
remainingFillableMakerAssetAmount?: BigNumber;
}
/**
* gerOrdersAsync: Given an OrderProviderRequest, get an OrderProviderResponse.
* getAvailableMakerAssetDatasAsync: Given a taker asset data string, return all availabled paired maker asset data strings.
* getAvailableTakerAssetDatasAsync: Given a maker asset data string, return all availabled paired taker asset data strings.
*/
export interface OrderProvider {
getOrdersAsync: (orderProviderRequest: OrderProviderRequest) => Promise<OrderProviderResponse>;
getAvailableMakerAssetDatasAsync: (takerAssetData: string) => Promise<string[]>;
getAvailableTakerAssetDatasAsync: (makerAssetData: string) => Promise<string[]>;
}
/**
* Represents the metadata to call a smart contract with calldata.
* calldataHexString: The hexstring of the calldata.
@@ -98,8 +87,8 @@ export interface ExchangeMarketSellSmartContractParams extends SmartContractPara
* Represents the varying smart contracts that can consume a valid swap quote
*/
export enum ConsumerType {
Forwarder,
Exchange,
Forwarder = 'FORWARDER',
Exchange = 'EXCHANGE',
}
/**
@@ -269,22 +258,14 @@ export interface SwapQuoteInfo {
}
/**
* shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false.
* shouldDisableRequestingFeeOrders: If set to true, requesting a swapQuote will not perform any computation or requests for fees.
* slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%).
*/
export interface SwapQuoteRequestOpts {
shouldForceOrderRefresh: boolean;
shouldDisableRequestingFeeOrders: boolean;
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<SwapQuoteRequestOpts, 'shouldForceOrderRefresh'>;
/**
* networkId: The ethereum network id. Defaults to 1 (mainnet).
* orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s).

View File

@@ -1,9 +1,10 @@
import { assert as sharedAssert } from '@0x/assert';
import { schemas } from '@0x/json-schemas';
import { Orderbook } from '@0x/orderbook';
import { MarketOperation, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import { OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
export const assert = {
...sharedAssert,
@@ -69,7 +70,7 @@ export const assert = {
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.makerTokenAmount);
},
isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void {
isValidOrderbook(variableName: string, orderFetcher: Orderbook): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);
},
isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void {

View File

@@ -7,7 +7,6 @@ import * as _ from 'lodash';
import { constants } from '../constants';
import {
OrderProviderRequest,
OrderProviderResponse,
OrdersAndFillableAmounts,
SignedOrderWithRemainingFillableMakerAssetAmount,
@@ -15,8 +14,7 @@ import {
} from '../types';
export const orderProviderResponseProcessor = {
throwIfInvalidResponse(response: OrderProviderResponse, request: OrderProviderRequest): void {
const { makerAssetData, takerAssetData } = request;
throwIfInvalidResponse(response: OrderProviderResponse, makerAssetData: string, takerAssetData: string): void {
_.forEach(response.orders, order => {
if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) {
throw new Error(SwapQuoterError.InvalidOrderProviderResponse);
@@ -27,7 +25,7 @@ export const orderProviderResponseProcessor = {
* Take the responses for the target orders to buy and fee orders and process them.
* Processing includes:
* - Drop orders that are expired or not open orders (null taker address)
* - If shouldValidateOnChain, attempt to grab fillable amounts from on-chain otherwise assume completely fillable
* - If an orderValidator is provided, attempt to grab fillable amounts from on-chain otherwise assume completely fillable
* - Sort by rate
*/
async processAsync(

View File

@@ -0,0 +1,177 @@
import { ContractAddresses, ContractWrappers, ERC20TokenContract } from '@0x/contract-wrappers';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote, SwapQuoteConsumer } from '../src';
import { ConsumerType } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_NETWORK_ID = 50;
const FILLABLE_AMOUNTS = [new BigNumber(3), new BigNumber(2), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
describe('SwapQuoteConsumer', () => {
let contractWrappers: ContractWrappers;
let erc20Token: ERC20TokenContract;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
const networkId = TESTRPC_NETWORK_ID;
let orders: SignedOrder[];
let marketSellSwapQuote: SwapQuote;
let swapQuoteConsumer: SwapQuoteConsumer;
let erc20ProxyAddress: string;
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
networkId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20Token = new ERC20TokenContract(makerTokenAddress, provider);
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
erc20ProxyAddress = contractWrappers.erc20Proxy.address;
const totalFillableAmount = FILLABLE_AMOUNTS.reduce(
(a: BigNumber, c: BigNumber) => a.plus(c),
new BigNumber(0),
);
await erc20Token.transfer.sendTransactionAsync(makerAddress, totalFillableAmount, {
from: coinbaseAddress,
});
await erc20Token.approve.sendTransactionAsync(erc20ProxyAddress, UNLIMITED_ALLOWANCE, {
from: makerAddress,
});
orders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
contractAddresses.exchange,
);
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Sell,
);
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
networkId,
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
// TODO(david): write tests to ensure options work for executeSwapQuote
// describe('executeSwapQuoteOrThrowAsync', () => {
// /*
// * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
// * Does not test the validity of the state change performed by the forwarder smart contract
// */
// it('should perform an asset swap with Forwarder contract when provided corresponding useConsumerType option', async () => {
// let makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
// let takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
// expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
// expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress, useConsumerType: ConsumerType.Forwarder });
// makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
// takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
// expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
// expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// });
// it('should perform an asset swap with Exchange contract when provided corresponding useConsumerType option', async () => {
// let makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
// let takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
// expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
// expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress });
// makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
// takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
// expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
// expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// });
// });
describe('getSmartContractParamsOrThrow', () => {
describe('valid swap quote', async () => {
// TODO(david) Check for valid MethodAbi
it('should provide correct and optimized smart contract params for Forwarder contract when provided corresponding useConsumerType option', async () => {
const { toAddress } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
useConsumerType: ConsumerType.Forwarder,
});
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
});
it('should provide correct and optimized smart contract params for Exchange contract when provided corresponding useConsumerType option', async () => {
const { toAddress } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
useConsumerType: ConsumerType.Exchange,
});
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
});
});
});
describe('getCalldataOrThrow', () => {
describe('valid swap quote', async () => {
it('should provide correct and optimized calldata options for Forwarder contract when provided corresponding useConsumerType option', async () => {
const { toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(marketSellSwapQuote, {
useConsumerType: ConsumerType.Forwarder,
});
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
});
it('should provide correct and optimized smart contract params for Exchange contract when provided corresponding useConsumerType option', async () => {
const { toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(marketSellSwapQuote, {
useConsumerType: ConsumerType.Exchange,
});
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
});
});
});
});

View File

@@ -1,6 +1,7 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
import { Orderbook } from '@0x/orderbook';
import { Web3ProviderEngine } from '@0x/subproviders';
import { SignedOrder } from '@0x/types';
import { AssetPairsItem, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai';
@@ -9,14 +10,10 @@ import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../src';
import { constants } from '../src/constants';
import { LiquidityForAssetData, OrderProvider, OrdersAndFillableAmounts } from '../src/types';
import { LiquidityForAssetData, OrdersAndFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import {
mockAvailableMakerAssetDatas,
mockedSwapQuoterWithOrdersAndFillableAmounts,
orderProviderMock,
} from './utils/mocks';
import { mockAvailableAssetDatas, mockedSwapQuoterWithOrdersAndFillableAmounts, orderbookMock } from './utils/mocks';
chaiSetup.configure();
const expect = chai.expect;
@@ -28,20 +25,51 @@ const TOKEN_DECIMALS = 18;
const DAI_ASSET_DATA = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359"';
const WETH_ASSET_DATA = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS;
const ZERO = new BigNumber(0);
const baseUnitAmount = (unitAmount: number, decimals = TOKEN_DECIMALS): BigNumber => {
return Web3Wrapper.toBaseUnitAmount(new BigNumber(unitAmount), decimals);
};
const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string): AssetPairsItem[] => {
const defaultAssetPairItem = {
minAmount: ZERO,
maxAmount: ZERO,
precision: TOKEN_DECIMALS,
};
return [
{
assetDataA: {
...defaultAssetPairItem,
assetData: makerAssetData,
},
assetDataB: {
...defaultAssetPairItem,
assetData: takerAssetData,
},
},
{
assetDataA: {
...defaultAssetPairItem,
assetData: takerAssetData,
},
assetDataB: {
...defaultAssetPairItem,
assetData: makerAssetData,
},
},
];
};
const expectLiquidityResult = async (
web3Provider: Web3ProviderEngine,
orderProvider: OrderProvider,
orderbook: Orderbook,
ordersAndFillableAmounts: OrdersAndFillableAmounts,
expectedLiquidityResult: LiquidityForAssetData,
) => {
const mockedSwapQuoter = mockedSwapQuoterWithOrdersAndFillableAmounts(
web3Provider,
orderProvider,
orderbook,
FAKE_MAKER_ASSET_DATA,
WETH_ASSET_DATA,
ordersAndFillableAmounts,
@@ -57,16 +85,16 @@ const expectLiquidityResult = async (
describe('SwapQuoter', () => {
describe('getLiquidityForMakerTakerAssetDataPairAsync', () => {
const mockWeb3Provider = TypeMoq.Mock.ofType(Web3ProviderEngine);
const mockOrderProvider = orderProviderMock();
const mockOrderbook = orderbookMock();
beforeEach(() => {
mockWeb3Provider.reset();
mockOrderProvider.reset();
mockOrderbook.reset();
});
afterEach(() => {
mockWeb3Provider.verifyAll();
mockOrderProvider.verifyAll();
mockOrderbook.verifyAll();
});
describe('validation', () => {
@@ -94,9 +122,9 @@ describe('SwapQuoter', () => {
describe('asset pair not supported', () => {
it('should return 0s when no asset pair are supported', async () => {
mockAvailableMakerAssetDatas(mockOrderProvider, FAKE_TAKER_ASSET_DATA, []);
mockAvailableAssetDatas(mockOrderbook, []);
const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderProvider.object);
const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderbook.object);
const liquidityResult = await swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
@@ -108,9 +136,9 @@ describe('SwapQuoter', () => {
});
it('should return 0s when only other asset pair supported', async () => {
mockAvailableMakerAssetDatas(mockOrderProvider, FAKE_TAKER_ASSET_DATA, [DAI_ASSET_DATA]);
mockAvailableAssetDatas(mockOrderbook, assetsToAssetPairItems(FAKE_MAKER_ASSET_DATA, DAI_ASSET_DATA));
const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderProvider.object);
const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderbook.object);
const liquidityResult = await swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
@@ -134,7 +162,7 @@ describe('SwapQuoter', () => {
});
beforeEach(() => {
mockAvailableMakerAssetDatas(mockOrderProvider, WETH_ASSET_DATA, [FAKE_MAKER_ASSET_DATA]);
mockAvailableAssetDatas(mockOrderbook, assetsToAssetPairItems(WETH_ASSET_DATA, FAKE_MAKER_ASSET_DATA));
});
it('should return 0s when no orders available', async () => {
@@ -148,7 +176,7 @@ describe('SwapQuoter', () => {
};
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
mockOrderbook.object,
ordersAndFillableAmounts,
expectedResult,
);
@@ -171,7 +199,7 @@ describe('SwapQuoter', () => {
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
mockOrderbook.object,
ordersAndFillableAmounts,
expectedResult,
);
@@ -190,7 +218,7 @@ describe('SwapQuoter', () => {
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
mockOrderbook.object,
ordersAndFillableAmounts,
expectedResult,
);
@@ -209,7 +237,7 @@ describe('SwapQuoter', () => {
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
mockOrderbook.object,
ordersAndFillableAmounts,
expectedResult,
);
@@ -228,7 +256,7 @@ describe('SwapQuoter', () => {
await expectLiquidityResult(
mockWeb3Provider.object,
mockOrderProvider.object,
mockOrderbook.object,
ordersAndFillableAmounts,
expectedResult,
);

View File

@@ -1,62 +1,49 @@
import { AcceptedRejectedOrders, Orderbook } from '@0x/orderbook';
import { Web3ProviderEngine } from '@0x/subproviders';
import { APIOrder, AssetPairsItem, SignedOrder } from '@0x/types';
import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../../src/swap_quoter';
import { OrderProvider, OrderProviderResponse, OrdersAndFillableAmounts } from '../../src/types';
import { 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 {
class OrderbookClass extends Orderbook {
// 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[]> {
public async getOrdersAsync(_makerAssetData: string, _takerAssetData: string): Promise<APIOrder[]> {
return Promise.resolve([]);
}
// tslint:disable-next-line:prefer-function-over-method
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
return Promise.resolve([]);
}
// tslint:disable-next-line:prefer-function-over-method
public async addOrdersAsync(_orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
return Promise.resolve({ accepted: [], rejected: [] });
}
}
export const orderProviderMock = () => {
return TypeMoq.Mock.ofType(OrderProviderClass, TypeMoq.MockBehavior.Strict);
export const orderbookMock = () => {
return TypeMoq.Mock.ofType(OrderbookClass, TypeMoq.MockBehavior.Strict);
};
export const mockAvailableMakerAssetDatas = (
mockOrderProvider: TypeMoq.IMock<OrderProviderClass>,
assetData: string,
availableAssetDatas: string[],
export const mockAvailableAssetDatas = (
mockOrderbook: TypeMoq.IMock<OrderbookClass>,
availableAssetDatas: AssetPairsItem[],
) => {
mockOrderProvider
.setup(op => op.getAvailableMakerAssetDatasAsync(TypeMoq.It.isValue(assetData)))
.returns(() => {
return Promise.resolve(availableAssetDatas);
})
mockOrderbook
.setup(async op => op.getAvailableAssetDatasAsync())
.returns(async () => Promise.resolve(availableAssetDatas))
.verifiable(TypeMoq.Times.once());
mockOrderbook
.setup(o => (o as any)._orderProvider)
.returns(() => undefined)
.verifiable(TypeMoq.Times.atLeast(0));
mockOrderbook
.setup(o => (o as any)._orderStore)
.returns(() => undefined)
.verifiable(TypeMoq.Times.atLeast(0));
};
export const mockAvailableTakerAssetDatas = (
mockOrderProvider: TypeMoq.IMock<OrderProviderClass>,
assetData: string,
availableAssetDatas: string[],
) => {
mockOrderProvider
.setup(op => op.getAvailableTakerAssetDatasAsync(TypeMoq.It.isValue(assetData)))
.returns(() => {
return Promise.resolve(availableAssetDatas);
})
.verifiable(TypeMoq.Times.once());
};
const partiallyMockedSwapQuoter = (
provider: Web3ProviderEngine,
orderProvider: OrderProvider,
): TypeMoq.IMock<SwapQuoter> => {
const rawSwapQuoter = new SwapQuoter(provider, orderProvider);
const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orderbook): TypeMoq.IMock<SwapQuoter> => {
const rawSwapQuoter = new SwapQuoter(provider, orderbook);
const mockedSwapQuoter = TypeMoq.Mock.ofInstance(rawSwapQuoter, TypeMoq.MockBehavior.Loose, false);
mockedSwapQuoter.callBase = true;
return mockedSwapQuoter;
@@ -69,19 +56,19 @@ const mockGetOrdersAndAvailableAmounts = (
ordersAndFillableAmounts: OrdersAndFillableAmounts,
): void => {
mockedSwapQuoter
.setup(a => a.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData, false))
.returns(() => Promise.resolve(ordersAndFillableAmounts))
.setup(async a => a.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData))
.returns(async () => Promise.resolve(ordersAndFillableAmounts))
.verifiable(TypeMoq.Times.once());
};
export const mockedSwapQuoterWithOrdersAndFillableAmounts = (
provider: Web3ProviderEngine,
orderProvider: OrderProvider,
orderbook: Orderbook,
makerAssetData: string,
takerAssetData: string,
ordersAndFillableAmounts: OrdersAndFillableAmounts,
): TypeMoq.IMock<SwapQuoter> => {
const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderProvider);
const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderbook);
mockGetOrdersAndAvailableAmounts(mockedAssetQuoter, makerAssetData, takerAssetData, ordersAndFillableAmounts);
return mockedAssetQuoter;
};

View File

@@ -0,0 +1,63 @@
/**
* This is to generate the umd bundle only
*/
const _ = require('lodash');
const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');
const production = process.env.NODE_ENV === 'production';
let entry = {
index: './src/index.ts',
};
if (production) {
entry = _.assign({}, entry, { 'index.min': './src/index.ts' });
}
module.exports = {
entry,
mode: 'production',
output: {
path: path.resolve(__dirname, '_bundles'),
filename: '[name].js',
libraryTarget: 'umd',
library: 'AssetSwapper',
umdNamedDefine: true,
},
resolve: {
extensions: ['.ts', '.js', '.json'],
},
devtool: 'source-map',
optimization: {
minimizer: [
new TerserPlugin({
sourceMap: true,
terserOptions: {
mangle: {
reserved: ['BigNumber'],
},
},
}),
],
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: 'awesome-typescript-loader',
// tsconfig.json contains some options required for
// project references which do not work with webback.
// We override those options here.
query: {
declaration: false,
declarationMap: false,
composite: false,
},
},
],
exclude: /node_modules/,
},
],
},
};

View File

@@ -24,6 +24,7 @@ export const docGenConfigs: DocGenConfigs = {
// HACK: Asset-swapper specifies marketSell and marketBuy quotes with a descriminant MarketOperation Type to ignore the error, linking Buy and Sell to MarketOperation
Buy: true,
Sell: true,
IterableIterator: true,
},
// Some types are not explicitly part of the public interface like params, return values, etc... But we still
// want them exported. E.g error enum types that can be thrown by methods. These must be manually added to this

View File

@@ -0,0 +1,114 @@
## @0x/orderbook
Package to help fetch orders from a remote source ([Standard Relayer API](https://github.com/0xProject/standard-relayer-api), Mesh) and keep the local orderbook synced and up-to-date.
Supported Order Providers:
- SRA HTTP Polling
- SRA Websocket
- Mesh
## Installation
```bash
yarn add @0x/orderbook
```
**Import**
```typescript
import { Orderbook } from '@0x/orderbook';
```
or
```javascript
var Orderbook = require('@0x/orderbook').Orderbook;
```
If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`:
```json
"compilerOptions": {
"typeRoots": ["node_modules/@0x/typescript-typings/types", "node_modules/@types"],
}
```
## Usage
```typescript
// Create an orderbook for makerAssetData, takerAssetData using the SRA Polling Order Provider
// This Provider polls the SRA endpoint automatically every 5 seconds on the supplied asset pairs
const orderbook = Orderbook.getOrderbookForPollingProvider({
httpEndpoint: 'https://sra.0x.org/v2',
pollingIntervalMs: 5000,
});
const orders = await orderbook.getOrdersAsync(makerAssetData, takerAssetData);
// Create an orderbook for makerAssetData, takerAssetData using the SRA Websocket Order Provider
// This provider subscribes via websocket to receive order updates on the supplied asset pairs
const orderbook = Orderbook.getOrderbookForWebsocketProvider({
httpEndpoint: 'https://sra.0x.org/v2',
websocketEndpoint: 'wss://ws.sra.0x.org',
});
const orders = await orderbook.getOrdersAsync(makerAssetData, takerAssetData);
// Create an orderbook for makerAssetData, takerAssetData using the Mesh Order Provider
// This provider subscribes via websocket to receive order updates on all orders stored in Mesh
const orderbook = Orderbook.getOrderbookForMeshProvider({
websocketEndpoint: 'wss://MESH_ENDPOINT',
});
const orders = await orderbook.getOrdersAsync(makerAssetData, takerAssetData);
```
## Contributing
We welcome improvements and fixes from the wider community! To report bugs within this package, please create an issue in this repository.
Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
### Install dependencies
If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
```bash
yarn config set workspaces-experimental true
```
Then install dependencies
```bash
yarn install
```
### Build
To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory:
```bash
PKG=@0x/orderbook yarn build
```
Or continuously rebuild on change:
```bash
PKG=@0x/orderbook yarn watch
```
### Clean
```bash
yarn clean
```
### Lint
```bash
yarn lint
```
### Run Tests
```bash
yarn test
```

View File

@@ -0,0 +1,10 @@
module.exports = {
roots: ['<rootDir>/test'],
coverageDirectory: 'coverage',
transform: {
'.*.ts?$': 'ts-jest',
},
testRegex: '(/__test__/.*|(\\.|/)(test|spec))\\.(js|ts)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/index.tsx'],
};

View File

@@ -0,0 +1,47 @@
{
"name": "@0x/orderbook",
"version": "0.0.1",
"description": "Library for retrieving and syncing a remote orderbook locally",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"bugs": {
"url": "https://github.com/0xProject/0x-monorepo/issues"
},
"homepage": "https://github.com/0xProject/0x-monorepo/packages/orderbook/README.md",
"repository": {
"type": "git",
"url": "https://github.com/0xProject/0x-monorepo.git"
},
"scripts": {
"clean": "shx rm -rf lib generated_docs",
"test": "jest",
"test:coverage": "jest --coverage",
"build": "tsc -p tsconfig.json",
"build:ci": "yarn build",
"watch": "tsc -w -p tsconfig.json",
"prettier": "prettier --write 'src/**/*.ts' --config .prettierrc",
"test:circleci": "yarn test:coverage",
"lint": "tslint --project . --format stylish"
},
"author": "Jacob Evans",
"license": "Apache-2.0",
"devDependencies": {
"@0x/tslint-config": "^3.0.1",
"@0x/types": "^2.4.2",
"@types/jest": "^24.0.17",
"@types/sinon": "^2.2.2",
"@types/websocket": "^0.0.39",
"jest": "^24.8.0",
"sinon": "^4.0.0",
"ts-jest": "^24.0.2",
"shx": "^0.2.2",
"typescript": "3.0.1"
},
"dependencies": {
"@0x/assert": "^2.1.5",
"@0x/connect": "^5.0.18",
"@0x/mesh-rpc-client": "^4.0.1-beta",
"@0x/order-utils": "^8.3.1",
"@0x/utils": "^4.5.1"
}
}

View File

@@ -0,0 +1,17 @@
export { Orderbook } from './orderbook';
export { OrderStore } from './order_store';
export { OrderSet } from './order_set';
export { SRAWebsocketOrderProvider } from './order_provider/sra_websocket_order_provider';
export { SRAPollingOrderProvider } from './order_provider/sra_polling_order_provider';
export { MeshOrderProvider } from './order_provider/mesh_order_provider';
export { BaseOrderProvider } from './order_provider/base_order_provider';
export {
MeshOrderProviderOpts,
SRAPollingOrderProviderOpts,
SRAWebsocketOrderProviderOpts,
AcceptedRejectedOrders,
AddedRemovedOrders,
RejectedOrder,
} from './types';
export { WSOpts } from '@0x/mesh-rpc-client';

View File

@@ -0,0 +1,34 @@
import { AssetPairsItem, SignedOrder } from '@0x/types';
import { OrderStore } from '../order_store';
import { AcceptedRejectedOrders, AddedRemovedOrders } from '../types';
// AssetPairItem requires precision but some OrderProviders may not
// enforce any precision. This is not the token decimal but the
// maximum precision for an orderbook.
export const DEFAULT_TOKEN_PRECISION = 18;
export abstract class BaseOrderProvider {
public readonly _orderStore: OrderStore;
constructor(orderStore: OrderStore) {
this._orderStore = orderStore;
}
public abstract async createSubscriptionForAssetPairAsync(
makerAssetData: string,
takerAssetData: string,
): Promise<void>;
public abstract async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]>;
public abstract async destroyAsync(): Promise<void>;
public abstract async addOrdersAsync(orders: SignedOrder[]): Promise<AcceptedRejectedOrders>;
protected _updateStore(addedRemoved: AddedRemovedOrders): void {
const orderSet = this._orderStore.getOrderSetForAssetPair(addedRemoved.assetPairKey);
orderSet.addMany(addedRemoved.added);
orderSet.deleteMany(addedRemoved.removed);
}
}

View File

@@ -0,0 +1,108 @@
import { APIOrder, HttpClient } from '@0x/connect';
import { AssetPairsItem, PaginatedCollection, SignedOrder } from '@0x/types';
import { OrderStore } from '../order_store';
import { AcceptedRejectedOrders, RejectedOrder } from '../types';
import { utils } from '../utils';
import { BaseOrderProvider } from './base_order_provider';
export const PER_PAGE_DEFAULT = 100;
export abstract class BaseSRAOrderProvider extends BaseOrderProvider {
protected readonly _httpClient: HttpClient;
protected readonly _networkId?: number;
protected readonly _perPage: number;
/**
* This is an internal class for Websocket and Polling Order Providers
*/
constructor(orderStore: OrderStore, httpEndpoint: string, perPage: number = PER_PAGE_DEFAULT, networkId?: number) {
super(orderStore);
this._httpClient = new HttpClient(httpEndpoint);
this._perPage = perPage;
this._networkId = networkId;
}
/**
* Returns the availale Asset pairs from the SRA endpoint. This response is direct from the endpoint
* so this call blocks until the response arrives.
*/
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
const requestOpts = {
perPage: this._perPage,
networkId: this._networkId,
};
let recordsToReturn: AssetPairsItem[] = [];
let hasMorePages = true;
let page = 1;
while (hasMorePages) {
const { total, records, perPage } = await utils.attemptAsync<PaginatedCollection<AssetPairsItem>>(() =>
this._httpClient.getAssetPairsAsync(requestOpts),
);
recordsToReturn = [...recordsToReturn, ...records];
page += 1;
const lastPage = Math.ceil(total / perPage);
hasMorePages = page <= lastPage;
}
return recordsToReturn;
}
/**
* Submits the SignedOrder to the SRA endpoint
* @param orders the set of signed orders to add
*/
public async addOrdersAsync(orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
const accepted: SignedOrder[] = [];
const rejected: RejectedOrder[] = [];
for (const order of orders) {
try {
await this._httpClient.submitOrderAsync(order, { networkId: this._networkId });
accepted.push(order);
} catch (e) {
rejected.push({ order, message: e.message });
}
}
return { accepted, rejected };
}
protected async _fetchLatestOrdersAsync(makerAssetData: string, takerAssetData: string): Promise<APIOrder[]> {
const [latestSellOrders, latestBuyOrders] = await Promise.all([
this._getAllPaginatedOrdersAsync(makerAssetData, takerAssetData),
this._getAllPaginatedOrdersAsync(takerAssetData, makerAssetData),
]);
return [...latestSellOrders, ...latestBuyOrders];
}
protected async _getAllPaginatedOrdersAsync(makerAssetData: string, takerAssetData: string): Promise<APIOrder[]> {
let recordsToReturn: APIOrder[] = [];
const requestOpts = {
makerAssetData,
takerAssetData,
perPage: this._perPage,
networkId: this._networkId,
};
let hasMorePages = true;
let page = 1;
while (hasMorePages) {
const { total, records, perPage } = await utils.attemptAsync(() =>
this._httpClient.getOrdersAsync({
...requestOpts,
page,
}),
);
recordsToReturn = [...recordsToReturn, ...records];
page += 1;
const lastPage = Math.ceil(total / perPage);
hasMorePages = page <= lastPage;
}
return recordsToReturn;
}
}

View File

@@ -0,0 +1,54 @@
import { Asset, AssetPairsItem, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { OrderStore } from '../order_store';
import { AcceptedRejectedOrders } from '../types';
import { utils } from '../utils';
import { BaseOrderProvider, DEFAULT_TOKEN_PRECISION } from './base_order_provider';
export class CustomOrderProvider extends BaseOrderProvider {
constructor(orders: SignedOrder[], orderStore: OrderStore) {
super(orderStore);
void this.addOrdersAsync(orders);
}
// tslint:disable-next-line:prefer-function-over-method
public async createSubscriptionForAssetPairAsync(_makerAssetData: string, _takerAssetData: string): Promise<void> {
// Do nothing
}
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
const assetPairsItems: AssetPairsItem[] = [];
const minAmount = new BigNumber(0);
const maxAmount = new BigNumber(2).pow(256).minus(1);
const precision = DEFAULT_TOKEN_PRECISION;
for (const assetPairKey of this._orderStore.keys()) {
const [assetA, assetB] = OrderStore.assetPairKeyToAssets(assetPairKey);
const assetDataA: Asset = { assetData: assetA, minAmount, maxAmount, precision };
const assetDataB: Asset = { assetData: assetB, minAmount, maxAmount, precision };
assetPairsItems.push({ assetDataA, assetDataB });
assetPairsItems.push({ assetDataA: assetDataB, assetDataB: assetDataA });
}
return assetPairsItems;
}
// tslint:disable-next-line:prefer-function-over-method
public async destroyAsync(): Promise<void> {
// Do nothing
}
public async addOrdersAsync(orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
for (const order of orders) {
const orderSet = this._orderStore.getOrderSetForAssets(order.makerAssetData, order.takerAssetData);
orderSet.add({
order,
metaData: {
remainingFillableTakerAssetAmount: order.takerAssetAmount,
orderHash: utils.getOrderHash(order),
},
});
}
return { accepted: orders, rejected: [] };
}
}

View File

@@ -0,0 +1,205 @@
import { APIOrder } from '@0x/connect';
import {
AcceptedOrderInfo,
OrderEvent,
OrderEventKind,
OrderInfo,
RejectedOrderInfo,
WSClient,
} from '@0x/mesh-rpc-client';
import { Asset, AssetPairsItem, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { OrderStore } from '../order_store';
import { AcceptedRejectedOrders, AddedRemovedOrders, MeshOrderProviderOpts } from '../types';
import { utils } from '../utils';
import { BaseOrderProvider, DEFAULT_TOKEN_PRECISION } from './base_order_provider';
export class MeshOrderProvider extends BaseOrderProvider {
private readonly _wsClient: WSClient;
private _wsSubscriptionId?: string;
/**
* Converts the OrderEvent or OrderInfo from Mesh into an APIOrder.
* If the OrderInfo is a RejectedOrderInfo the remainingFillableTakerAssetAmount is
* assumed to be 0.
* @param orderEvent The `OrderEvent` from a Mesh subscription update
*/
private static _orderInfoToAPIOrder(
orderEvent: OrderEvent | AcceptedOrderInfo | RejectedOrderInfo | OrderInfo,
): APIOrder {
const remainingFillableTakerAssetAmount = (orderEvent as OrderEvent).fillableTakerAssetAmount
? (orderEvent as OrderEvent).fillableTakerAssetAmount
: new BigNumber(0);
return {
order: orderEvent.signedOrder,
metaData: {
orderHash: orderEvent.orderHash,
remainingFillableTakerAssetAmount,
},
};
}
/**
* Instantiates a [Mesh](https://github.com/0xProject/0x-mesh) Order Provider. This provider writes
* all orders stored in Mesh to the OrderStore and subscribes all Mesh updates.
* @param opts `MeshOrderProviderOpts` containing the websocketEndpoint and additional Mesh options
* @param orderStore The `OrderStore` where orders are added and removed from
*/
constructor(opts: MeshOrderProviderOpts, orderStore: OrderStore) {
super(orderStore);
this._wsClient = new WSClient(opts.websocketEndpoint, opts.wsOpts);
}
/**
* Returns the available asset pairs. If no subscription to Mesh exists (and therefore no orders) it is
* created and awaited on. Once the connection has been initialized the orders in the store are returned
* as asset pairs.
*/
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
await this._initializeIfRequiredAsync();
const assetPairsItems: AssetPairsItem[] = [];
const minAmount = new BigNumber(0);
const maxAmount = new BigNumber(2).pow(256).minus(1);
const precision = DEFAULT_TOKEN_PRECISION;
for (const assetPairKey of this._orderStore.keys()) {
const [assetA, assetB] = OrderStore.assetPairKeyToAssets(assetPairKey);
const assetDataA: Asset = { assetData: assetA, minAmount, maxAmount, precision };
const assetDataB: Asset = { assetData: assetB, minAmount, maxAmount, precision };
assetPairsItems.push({ assetDataA, assetDataB });
assetPairsItems.push({ assetDataA: assetDataB, assetDataB: assetDataA });
}
return assetPairsItems;
}
/**
* Creates a subscription for all asset pairs in Mesh.
* @param makerAssetData the Maker Asset Data
* @param takerAssetData the Taker Asset Data
*/
public async createSubscriptionForAssetPairAsync(_makerAssetData: string, _takerAssetData: string): Promise<void> {
// Create the subscription first to get any updates while waiting for the request
await this._initializeIfRequiredAsync();
}
/**
* Submits the SignedOrder to the Mesh node
* @param orders the set of signed orders to add
*/
public async addOrdersAsync(orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
const { accepted, rejected } = await utils.attemptAsync(() => this._wsClient.addOrdersAsync(orders));
return {
accepted: accepted.map(o => o.signedOrder),
rejected: rejected.map(o => ({ order: o.signedOrder, message: o.status.message })),
};
}
/**
* Destroys the order provider, removing any subscriptions
*/
public async destroyAsync(): Promise<void> {
this._wsClient.destroy();
}
/**
* Creates the order subscription unless one already exists. If one does not exist
* it also handles the reconnection logic.
*/
private async _initializeIfRequiredAsync(): Promise<void> {
if (this._wsSubscriptionId) {
return;
}
this._wsSubscriptionId = await this._wsClient.subscribeToOrdersAsync(this._handleOrderUpdates.bind(this));
await this._fetchOrdersAndStoreAsync();
// On Reconnnect sync all of the orders currently stored
this._wsClient.onReconnected(() => {
void this._syncOrdersInOrderStoreAsync();
});
}
/**
* Syncs the orders currently stored in the OrderStore. This is used when the connection to mesh
* has reconnected. During this outage there are missed OrderEvents so all orders are re-validated
* for every known asset pair.
*/
private async _syncOrdersInOrderStoreAsync(): Promise<void> {
for (const assetPairKey of this._orderStore.keys()) {
const currentOrders = this._orderStore.getOrderSetForAssetPair(assetPairKey);
const { rejected } = await utils.attemptAsync(() =>
this._wsClient.addOrdersAsync(Array.from(currentOrders.values()).map(o => o.order)),
);
// Remove any rejected orders
this._updateStore({
assetPairKey,
added: [],
removed: rejected.map(o => MeshOrderProvider._orderInfoToAPIOrder(o)),
});
}
await this._fetchOrdersAndStoreAsync();
}
/**
* Fetches all of the Orders available in Mesh. All orders are then stored in the
* OrderStore.
*/
private async _fetchOrdersAndStoreAsync(): Promise<void> {
const ordersByAssetPairKey: { [assetPairKey: string]: APIOrder[] } = {};
// Fetch all orders in Mesh
const orders = await utils.attemptAsync(() => this._wsClient.getOrdersAsync());
for (const order of orders) {
const { makerAssetData, takerAssetData } = order.signedOrder;
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
if (!ordersByAssetPairKey[assetPairKey]) {
ordersByAssetPairKey[assetPairKey] = [];
}
ordersByAssetPairKey[assetPairKey].push(MeshOrderProvider._orderInfoToAPIOrder(order));
}
for (const assetPairKey of Object.keys(ordersByAssetPairKey)) {
this._updateStore({
added: ordersByAssetPairKey[assetPairKey],
removed: [],
assetPairKey,
});
}
}
/**
* Handles the order events converting to APIOrders and either adding or removing based on its kind.
* @param orderEvents The set of `OrderEvents` returned from a mesh subscription update
*/
private _handleOrderUpdates(orderEvents: OrderEvent[]): void {
const addedRemovedByAssetPairKey: { [assetPairKey: string]: AddedRemovedOrders } = {};
for (const event of orderEvents) {
const { makerAssetData, takerAssetData } = event.signedOrder;
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
if (!addedRemovedByAssetPairKey[assetPairKey]) {
addedRemovedByAssetPairKey[assetPairKey] = { added: [], removed: [], assetPairKey };
}
const apiOrder = MeshOrderProvider._orderInfoToAPIOrder(event);
switch (event.kind) {
case OrderEventKind.Added: {
addedRemovedByAssetPairKey[assetPairKey].added.push(apiOrder);
break;
}
case OrderEventKind.Cancelled:
case OrderEventKind.Expired:
case OrderEventKind.FullyFilled:
case OrderEventKind.Unfunded: {
addedRemovedByAssetPairKey[assetPairKey].removed.push(apiOrder);
break;
}
case OrderEventKind.FillabilityIncreased:
case OrderEventKind.Filled: {
addedRemovedByAssetPairKey[assetPairKey].added.push(apiOrder);
break;
}
default:
break;
}
}
for (const assetPairKey of Object.keys(addedRemovedByAssetPairKey)) {
this._updateStore(addedRemovedByAssetPairKey[assetPairKey]);
}
}
}

View File

@@ -0,0 +1,85 @@
import { assert } from '@0x/assert';
import { intervalUtils } from '@0x/utils';
import { OrderSet } from '../order_set';
import { OrderStore } from '../order_store';
import { SRAPollingOrderProviderOpts } from '../types';
import { BaseSRAOrderProvider } from './base_sra_order_provider';
export class SRAPollingOrderProvider extends BaseSRAOrderProvider {
private readonly _assetPairKeyToPollingIntervalId: Map<string, number> = new Map();
private readonly _pollingIntervalMs: number;
/**
* Instantiates a HTTP [Standard Relayer API](https://github.com/0xProject/standard-relayer-api)
* Polling Order Provider
* @param opts `SRAPollingOrderProviderOpts` containing the httpEndpoint to an SRA backend and polling options
* @param orderStore The `OrderStore` where orders are added and removed from
*/
constructor(opts: SRAPollingOrderProviderOpts, orderStore: OrderStore) {
super(orderStore, opts.httpEndpoint, opts.perPage, opts.networkId);
assert.isNumber('pollingIntervalMs', opts.pollingIntervalMs);
this._pollingIntervalMs = opts.pollingIntervalMs;
}
/**
* Creates a http polling subscription and fetches the current orders from SRA.
* @param makerAssetData the maker asset Data
* @param takerAssetData the taker asset Data
*/
public async createSubscriptionForAssetPairAsync(makerAssetData: string, takerAssetData: string): Promise<void> {
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
// Do nothing if we already have a polling interval or websocket created for this asset pair
if (this._assetPairKeyToPollingIntervalId.has(assetPairKey)) {
return;
}
await this._fetchAndCreatePollingAsync(makerAssetData, takerAssetData);
}
/**
* Destroys the order provider, removing any subscriptions
*/
public async destroyAsync(): Promise<void> {
for (const [assetPairKey, id] of this._assetPairKeyToPollingIntervalId) {
clearInterval(id);
this._assetPairKeyToPollingIntervalId.delete(assetPairKey);
}
}
/**
* Fetches all of the orders for both sides of the orderbook and stores them. A polling subscription
* is created performing this action every pollingIntervalMs
*/
private async _fetchAndCreatePollingAsync(makerAssetData: string, takerAssetData: string): Promise<void> {
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
// first time we have had this request, preload the local storage
const orders = await this._fetchLatestOrdersAsync(makerAssetData, takerAssetData);
// Set the OrderSet for the polling to diff against
this._updateStore({ added: orders, removed: [], assetPairKey });
// Create a HTTP polling subscription
const pollingIntervalId = (this._createPollingSubscription(makerAssetData, takerAssetData) as any) as number;
this._assetPairKeyToPollingIntervalId.set(assetPairKey, pollingIntervalId);
}
/**
* Creates the polling interval fetching the orders, calculating the diff and updating the store
*/
private _createPollingSubscription(makerAssetData: string, takerAssetData: string): NodeJS.Timer {
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
const pollingIntervalId = intervalUtils.setAsyncExcludingInterval(
async () => {
const previousOrderSet = this._orderStore.getOrderSetForAssetPair(assetPairKey);
const orders = await this._fetchLatestOrdersAsync(makerAssetData, takerAssetData);
const diff = previousOrderSet.diff(new OrderSet(orders));
this._updateStore({ ...diff, assetPairKey });
},
this._pollingIntervalMs,
(_: Error) => {
// TODO(dave4506) Add richer errors
throw new Error(`Fetching latest orders for asset pair ${makerAssetData}/${takerAssetData}`);
},
);
return pollingIntervalId;
}
}

View File

@@ -0,0 +1,178 @@
import { assert } from '@0x/assert';
import {
APIOrder,
OrdersChannel,
ordersChannelFactory,
OrdersChannelHandler,
OrdersChannelSubscriptionOpts,
} from '@0x/connect';
import { BigNumber } from '@0x/utils';
import { OrderSet } from '../order_set';
import { OrderStore } from '../order_store';
import { AddedRemovedOrders, SRAWebsocketOrderProviderOpts } from '../types';
import { utils } from '../utils';
import { BaseSRAOrderProvider, PER_PAGE_DEFAULT } from './base_sra_order_provider';
export class SRAWebsocketOrderProvider extends BaseSRAOrderProvider {
private readonly _websocketEndpoint: string;
private readonly _wsSubscriptions: Map<string, OrdersChannelSubscriptionOpts> = new Map();
private _ordersChannel?: OrdersChannel;
private _isDestroyed = false;
private _isConnecting = false;
/**
* Instantiates a HTTP and WS [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) Order Provider
* @param opts `SRAWebsocketOrderProviderOpts` containing the websocketEndpoint and the httpEndpoint to an SRA backend.
* @param orderStore The `OrderStore` where orders are added and removed from
*/
constructor(opts: SRAWebsocketOrderProviderOpts, orderStore: OrderStore) {
super(orderStore, opts.httpEndpoint, PER_PAGE_DEFAULT, opts.networkId);
assert.isUri('websocketEndpoint', opts.websocketEndpoint);
this._websocketEndpoint = opts.websocketEndpoint;
}
/**
* Creates a websocket subscription and fetches the current orders from SRA. If a websocket
* connection already exists this function is a noop.
* @param makerAssetData the Maker Asset Data
* @param takerAssetData the Taker Asset Data
*/
public async createSubscriptionForAssetPairAsync(makerAssetData: string, takerAssetData: string): Promise<void> {
// If we've previously been destroyed then reset
this._isDestroyed = false;
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
if (this._wsSubscriptions.has(assetPairKey)) {
return;
}
return this._fetchAndCreateSubscriptionAsync(makerAssetData, takerAssetData);
}
/**
* Destroys the order provider, removing any subscriptions
*/
public async destroyAsync(): Promise<void> {
this._isDestroyed = true;
this._wsSubscriptions.clear();
if (this._ordersChannel) {
this._ordersChannel.close();
this._ordersChannel = undefined;
}
}
/**
* Creates a websocket subscription. If the inital websocket connnection
* does not exist, it is created.
* @param makerAssetData the Maker Asset Data
* @param takerAssetData the Taker Asset Data
*/
private async _createWebsocketSubscriptionAsync(makerAssetData: string, takerAssetData: string): Promise<void> {
// Prevent creating multiple channels
while (this._isConnecting && !this._ordersChannel) {
await utils.delayAsync(100);
}
if (!this._ordersChannel) {
this._isConnecting = true;
try {
this._ordersChannel = await this._createOrdersChannelAsync();
} finally {
this._isConnecting = false;
}
}
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
const subscriptionOpts = {
makerAssetData,
takerAssetData,
};
this._wsSubscriptions.set(assetPairKey, subscriptionOpts);
// Subscribe to both sides of the book
this._ordersChannel.subscribe(subscriptionOpts);
this._ordersChannel.subscribe({
...subscriptionOpts,
makerAssetData: takerAssetData,
takerAssetData: makerAssetData,
});
}
private async _fetchAndCreateSubscriptionAsync(makerAssetData: string, takerAssetData: string): Promise<void> {
// Create the subscription first to get any updates while waiting for the request
await this._createWebsocketSubscriptionAsync(makerAssetData, takerAssetData);
// first time we have had this request, preload the local storage
const orders = await this._fetchLatestOrdersAsync(makerAssetData, takerAssetData);
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
const currentOrders = this._orderStore.getOrderSetForAssetPair(assetPairKey);
const newOrders = new OrderSet(orders);
const diff = currentOrders.diff(newOrders);
this._updateStore({
added: diff.added,
removed: diff.removed,
assetPairKey,
});
}
private async _syncOrdersInOrderStoreAsync(): Promise<void> {
for (const assetPairKey of this._orderStore.keys()) {
const [assetDataA, assetDataB] = OrderStore.assetPairKeyToAssets(assetPairKey);
await this._fetchAndCreateSubscriptionAsync(assetDataA, assetDataB);
}
}
/**
* Creates a new websocket orders channel.
*/
private async _createOrdersChannelAsync(): Promise<OrdersChannel> {
const ordersChannelHandler: OrdersChannelHandler = {
onUpdate: async (_channel, _opts, apiOrders) => this._handleOrderUpdates(apiOrders),
// tslint:disable-next-line:no-empty
onError: (_channel, _err) => {},
onClose: async () => {
// Do not reconnect if destroyed
if (this._isDestroyed) {
return;
}
// Re-sync and create subscriptions
await utils.attemptAsync<boolean>(async () => {
this._ordersChannel = undefined;
await this._syncOrdersInOrderStoreAsync();
return true;
});
},
};
try {
return await ordersChannelFactory.createWebSocketOrdersChannelAsync(
this._websocketEndpoint,
ordersChannelHandler,
);
} catch (e) {
throw new Error(`Creating websocket connection to ${this._websocketEndpoint}`);
}
}
/**
* Handles updates from the websocket, adding new orders and removing orders
* which have remainingFillableTakerAssetAmount as 0.
* @param orders the set of API Orders returned from the websocket channel
*/
private _handleOrderUpdates(orders: APIOrder[]): void {
const addedRemovedByKey: { [assetPairKey: string]: AddedRemovedOrders } = {};
for (const order of orders) {
const assetPairKey = OrderStore.getKeyForAssetPair(order.order.makerAssetData, order.order.takerAssetData);
if (!addedRemovedByKey[assetPairKey]) {
addedRemovedByKey[assetPairKey] = { added: [], removed: [], assetPairKey };
}
const addedRemoved = addedRemovedByKey[assetPairKey];
// If we have the metadata informing us that the order cannot be filled for any amount we don't add it
const remainingFillableTakerAssetAmount = (order.metaData as any).remainingFillableTakerAssetAmount;
if (remainingFillableTakerAssetAmount && new BigNumber(remainingFillableTakerAssetAmount).eq(0)) {
addedRemoved.removed.push(order);
} else {
addedRemoved.added.push(order);
}
}
for (const assetPairKey of Object.keys(addedRemovedByKey)) {
this._updateStore(addedRemovedByKey[assetPairKey]);
}
}
}

View File

@@ -0,0 +1,66 @@
import { APIOrder } from '@0x/connect';
import { utils } from './utils';
export class OrderSet {
private readonly _map: Map<string, APIOrder>;
constructor(orders: APIOrder[] = []) {
this._map = new Map();
(this as any)[Symbol.iterator] = this.values;
for (const order of orders) {
this.add(order);
}
}
public size(): number {
return this._map.size;
}
public add(item: APIOrder): void {
const orderHash = utils.getOrderHash(item);
(item.metaData as any).orderHash = orderHash;
this._map.set(orderHash, item);
}
public addMany(items: APIOrder[]): void {
for (const item of items) {
this.add(item);
}
}
public has(order: APIOrder): boolean {
return this._map.has(utils.getOrderHash(order));
}
public diff(other: OrderSet): { added: APIOrder[]; removed: APIOrder[] } {
const added: APIOrder[] = [];
const removed: APIOrder[] = [];
for (const otherItem of other.values()) {
const doesContainItem = this._map.has(utils.getOrderHash(otherItem));
if (!doesContainItem) {
added.push(otherItem);
}
}
for (const item of this.values()) {
const doesContainItem = other._map.has(utils.getOrderHash(item));
if (!doesContainItem) {
removed.push(item);
}
}
return { added, removed };
}
public values(): IterableIterator<APIOrder> {
return this._map.values();
}
public delete(item: APIOrder): boolean {
return this._map.delete(utils.getOrderHash(item));
}
public deleteMany(items: APIOrder[]): void {
for (const item of items) {
this.delete(item);
}
}
}

View File

@@ -0,0 +1,43 @@
import { APIOrder } from '@0x/connect';
import { OrderSet } from './order_set';
import { AddedRemovedOrders } from './types';
export class OrderStore {
// Both bids and asks are stored together in one set
private readonly _orders: Map<string, OrderSet> = new Map();
public static getKeyForAssetPair(makerAssetData: string, takerAssetData: string): string {
return [makerAssetData, takerAssetData].sort().join('-');
}
public static assetPairKeyToAssets(assetPairKey: string): string[] {
return assetPairKey.split('-');
}
public getOrderSetForAssets(makerAssetData: string, takerAssetData: string): OrderSet {
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
return this.getOrderSetForAssetPair(assetPairKey);
}
public getOrderSetForAssetPair(assetPairKey: string): OrderSet {
const orderSet = this._orders.get(assetPairKey);
if (!orderSet) {
const newOrderSet = new OrderSet();
this._orders.set(assetPairKey, newOrderSet);
return newOrderSet;
}
return orderSet;
}
public update(addedRemoved: AddedRemovedOrders): void {
const { added, removed, assetPairKey } = addedRemoved;
const orders = this.getOrderSetForAssetPair(assetPairKey);
orders.addMany(added);
orders.deleteMany(removed);
}
public has(assetPairKey: string): boolean {
return this._orders.has(assetPairKey);
}
public values(assetPairKey: string): APIOrder[] {
return Array.from(this.getOrderSetForAssetPair(assetPairKey).values());
}
public keys(): IterableIterator<string> {
return this._orders.keys();
}
}

View File

@@ -0,0 +1,107 @@
import { assert } from '@0x/assert';
import { APIOrder } from '@0x/connect';
import { AssetPairsItem, SignedOrder } from '@0x/types';
import { BaseOrderProvider } from './order_provider/base_order_provider';
import { CustomOrderProvider } from './order_provider/custom_order_provider';
import { MeshOrderProvider } from './order_provider/mesh_order_provider';
import { SRAPollingOrderProvider } from './order_provider/sra_polling_order_provider';
import { SRAWebsocketOrderProvider } from './order_provider/sra_websocket_order_provider';
import { OrderStore } from './order_store';
import {
AcceptedRejectedOrders,
MeshOrderProviderOpts,
SRAPollingOrderProviderOpts,
SRAWebsocketOrderProviderOpts,
} from './types';
export class Orderbook {
private readonly _orderProvider: BaseOrderProvider;
private readonly _orderStore: OrderStore;
/**
* Creates an Orderbook with the provided orders. This provider simply stores the
* orders and allows querying. No validation or subscriptions occur.
* @param orders the set of SignedOrders
*/
public static getOrderbookForProvidedOrders(orders: SignedOrder[]): Orderbook {
const orderStore = new OrderStore();
return new Orderbook(new CustomOrderProvider(orders, orderStore), orderStore);
}
/**
* Creates an Orderbook with the SRA Websocket Provider. This Provider fetches orders via
* the SRA http endpoint and then subscribes to the asset pair for future updates.
* @param opts the `SRAWebsocketOrderProviderOpts`
*/
public static getOrderbookForWebsocketProvider(opts: SRAWebsocketOrderProviderOpts): Orderbook {
const orderStore = new OrderStore();
return new Orderbook(new SRAWebsocketOrderProvider(opts, orderStore), orderStore);
}
/**
* Creates an Orderbook with SRA Polling Provider. This Provider simply polls every interval.
* @param opts the `SRAPollingOrderProviderOpts`
*/
public static getOrderbookForPollingProvider(opts: SRAPollingOrderProviderOpts): Orderbook {
const orderStore = new OrderStore();
return new Orderbook(new SRAPollingOrderProvider(opts, orderStore), orderStore);
}
/**
* Creates an Orderbook with a Mesh Order Provider. This Provider fetches ALL orders
* and subscribes to updates on ALL orders.
* @param opts the `MeshOrderProviderOpts`
*/
public static getOrderbookForMeshProvider(opts: MeshOrderProviderOpts): Orderbook {
const orderStore = new OrderStore();
return new Orderbook(new MeshOrderProvider(opts, orderStore), orderStore);
}
/**
* Creates an Orderbook with the order provider. All order updates are stored
* in the `OrderStore`.
* @param orderProvider the order provider, e.g SRAWebbsocketOrderProvider
* @param orderStore the order store where orders are added and deleted
*/
constructor(orderProvider: BaseOrderProvider, orderStore: OrderStore) {
this._orderProvider = orderProvider;
this._orderStore = orderStore;
}
/**
* Returns all orders where the order.makerAssetData == makerAssetData and
* order.takerAssetData == takerAssetData. This pair is then subscribed to
* and all future updates will be stored. The first request
* to `getOrdersAsync` might fetch the orders from the Order Provider and create a subscription.
* Subsequent requests will be quick and up to date and synced with the Order Provider state.
* @param makerAssetData the maker asset data
* @param takerAssetData the taker asset data
*/
public async getOrdersAsync(makerAssetData: string, takerAssetData: string): Promise<APIOrder[]> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
const assetPairKey = OrderStore.getKeyForAssetPair(makerAssetData, takerAssetData);
if (!this._orderStore.has(assetPairKey)) {
await this._orderProvider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
}
const orders = this._orderStore.values(assetPairKey);
return orders.filter(
o => o.order.makerAssetData === makerAssetData && o.order.takerAssetData === takerAssetData,
);
}
/**
* Returns all of the Available Asset Pairs for the provided Order Provider.
*/
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
return this._orderProvider.getAvailableAssetDatasAsync();
}
/**
* Adds the orders to the Order Provider. All accepted orders will be returned
* and rejected orders will be returned with an message indicating a reason for its rejection
* @param orders The set of Orders to add to the Order Provider
*/
public async addOrdersAsync(orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
return this._orderProvider.addOrdersAsync(orders);
}
/**
* Destroys any subscriptions or connections.
*/
public async destroyAsync(): Promise<void> {
return this._orderProvider.destroyAsync();
}
}

View File

@@ -0,0 +1,55 @@
import { APIOrder, SignedOrder } from '@0x/connect';
import { WSOpts } from '@0x/mesh-rpc-client';
export interface AddedRemovedOrders {
assetPairKey: string;
added: APIOrder[];
removed: APIOrder[];
}
export interface RejectedOrder {
message: string;
order: SignedOrder;
}
export interface AcceptedRejectedOrders {
accepted: SignedOrder[];
rejected: RejectedOrder[];
}
export type AddedRemovedListeners = (addedRemoved: AddedRemovedOrders) => void;
/**
* Constructor options for a SRA Websocket Order Provider
*/
export interface SRAWebsocketOrderProviderOpts {
// The http endpoint to the SRA service, e.g https://sra.0x.org/v2
httpEndpoint: string;
// The websocket endpoint to the SRA service, e.g wss://ws.sra.0x.org/
websocketEndpoint: string;
// The network Id
networkId?: number;
}
/**
* Constructor options for a SRA Polling Order Provider
*/
export interface SRAPollingOrderProviderOpts {
// The http endpoint to the SRA service, e.g https://sra.0x.org/v2
httpEndpoint: string;
// The interval between polling for each subscription
pollingIntervalMs: number;
// The amount of records to request per request to the SRA endpoint
perPage?: number;
// The network Id
networkId?: number;
}
/**
* Constructor options for a Mesh Order Provider
*/
export interface MeshOrderProviderOpts {
// The websocket endpoint for the Mesh service
websocketEndpoint: string;
// Additional options to configure the Mesh client, e.g reconnectAfter
wsOpts?: WSOpts;
}

View File

@@ -0,0 +1,43 @@
import { APIOrder, SignedOrder } from '@0x/connect';
import { orderHashUtils } from '@0x/order-utils';
export const utils = {
getOrderHash: (order: APIOrder | SignedOrder): string => {
if ((order as APIOrder).metaData) {
const apiOrder = order as APIOrder;
const orderHash = (apiOrder.metaData as any).orderHash || orderHashUtils.getOrderHashHex(apiOrder.order);
return orderHash;
} else {
const orderHash = orderHashUtils.getOrderHashHex(order as SignedOrder);
return orderHash;
}
},
async delayAsync(ms: number): Promise<void> {
// tslint:disable:no-inferred-empty-object-type
return new Promise<void>(resolve => setTimeout(resolve, ms));
},
async attemptAsync<T>(
fn: () => Promise<T>,
opts: { interval: number; maxRetries: number } = { interval: 1000, maxRetries: 10 },
): Promise<T> {
let result: T | undefined;
let attempt = 0;
let error;
let isSuccess = false;
while (!result && attempt < opts.maxRetries) {
attempt++;
try {
result = await fn();
isSuccess = true;
error = undefined;
} catch (err) {
error = err;
await utils.delayAsync(opts.interval);
}
}
if (!isSuccess) {
throw error;
}
return result as T;
},
};

View File

@@ -0,0 +1,275 @@
import { BigNumber, WSClient } from '@0x/mesh-rpc-client';
import { SERVER_PORT, setupServerAsync, stopServer } from '@0x/mesh-rpc-client/lib/test/utils/mock_ws_server';
import * as sinon from 'sinon';
import { MeshOrderProvider } from '../../src';
import { BaseOrderProvider } from '../../src/order_provider/base_order_provider';
import { OrderStore } from '../../src/order_store';
import { utils } from '../../src/utils';
import { createOrder } from '../utils';
describe('MeshOrderProvider', () => {
let orderStore: OrderStore;
let provider: BaseOrderProvider;
const stubs: sinon.SinonStub[] = [];
const websocketEndpoint = `ws://localhost:${SERVER_PORT}`;
const makerAssetData = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359';
const takerAssetData = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const subscriptionId = 'subscriptionId';
const addedResponse = {
jsonrpc: '2.0',
method: 'mesh_subscription',
params: {
subscription: subscriptionId,
result: [
{
orderHash: '0x96e6eb6174dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ecc13fd4',
signedOrder: {
makerAddress: '0x50f84bbee6fb250d6f49e854fa280445369d64d9',
makerAssetData,
makerAssetAmount: '4424020538752105500000',
makerFee: '0',
takerAddress: '0x0000000000000000000000000000000000000000',
takerAssetData,
takerAssetAmount: '1000000000000000061',
takerFee: '0',
senderAddress: '0x0000000000000000000000000000000000000000',
exchangeAddress: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
feeRecipientAddress: '0xa258b39954cef5cb142fd567a46cddb31a670124',
expirationTimeSeconds: '1559422407',
salt: '1559422141994',
signature:
'0x1cf16c2f3a210965b5e17f51b57b869ba4ddda33df92b0017b4d8da9dacd3152b122a73844eaf50ccde29a42950239ba36a525ed7f1698a8a5e1896cf7d651aed203',
},
kind: 'ADDED',
fillableTakerAssetAmount: 1000000000000000061,
txHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
],
},
};
const removedResponse = {
...addedResponse,
...{
params: {
...addedResponse.params,
result: [{ ...addedResponse.params.result[0], kind: 'CANCELLED', fillableTakerAssetAmount: 0 }],
},
},
};
let wsServer: any;
let connection: any;
afterEach(() => {
void provider.destroyAsync();
stubs.forEach(s => s.restore());
stopServer();
});
beforeEach(async () => {
orderStore = new OrderStore();
stubs.push(
sinon
.stub(WSClient.prototype as any, '_startInternalLivenessCheckAsync')
.callsFake(async () => Promise.resolve()),
);
});
describe('#createSubscriptionForAssetPairAsync', () => {
beforeEach(async () => {
wsServer = await setupServerAsync();
wsServer.on('connect', (conn: any) => {
connection = conn;
conn.on('message', (message: any) => {
const jsonRpcRequest = JSON.parse(message.utf8Data);
if (jsonRpcRequest.method === 'mesh_subscribe') {
connection.sendUTF(
JSON.stringify({
id: jsonRpcRequest.id,
jsonrpc: '2.0',
result: subscriptionId,
}),
);
}
});
});
});
test('fetches order on first subscription', async () => {
const getOrdersStub = sinon
.stub(WSClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve([]));
stubs.push(getOrdersStub);
const subscriptionStub = sinon
.stub(WSClient.prototype, 'subscribeToOrdersAsync')
.callsFake(async () => Promise.resolve('suscriptionId'));
stubs.push(subscriptionStub);
provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(getOrdersStub.callCount).toBe(1);
expect(subscriptionStub.callCount).toBe(1);
});
test('fetches once when the same subscription is called', async () => {
const stub = sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([]));
stubs.push(stub);
stubs.push(
sinon
.stub(WSClient.prototype, 'subscribeToOrdersAsync')
.callsFake(async () => Promise.resolve(subscriptionId)),
);
provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(stub.callCount).toBe(1);
});
test('stores the orders', async () => {
const order = createOrder(makerAssetData, takerAssetData);
const orderInfo = {
orderHash: '0x00',
signedOrder: order.order,
fillableTakerAssetAmount: new BigNumber(1),
};
stubs.push(
sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([orderInfo])),
);
stubs.push(
sinon
.stub(WSClient.prototype, 'subscribeToOrdersAsync')
.callsFake(async () => Promise.resolve(subscriptionId)),
);
provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(orders.size()).toBe(1);
});
test('stores the orders from a subscription update', async () => {
const eventResponse = JSON.stringify(addedResponse);
stubs.push(sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([])));
provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
connection.sendUTF(eventResponse);
await utils.delayAsync(5);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(orders.size()).toBe(1);
});
test('stores removed orders on a subscription update', async () => {
const added = JSON.stringify(addedResponse);
const removed = JSON.stringify(removedResponse);
stubs.push(sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([])));
provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
connection.sendUTF(added);
await utils.delayAsync(5);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(orders.size()).toBe(1);
connection.sendUTF(removed);
await utils.delayAsync(5);
expect(orders.size()).toBe(0);
});
});
describe('reconnnect', () => {
test.skip('revalidates all stored orders', async () => {
wsServer = await setupServerAsync();
const orderInfoResponse = {
jsonrpc: '2.0',
result: {
accepted: [],
rejected: [{ ...addedResponse.params.result[0], kind: 'CANCELLED', fillableTakerAssetAmount: 0 }],
},
};
wsServer.on('connect', (conn: any) => {
connection = conn;
conn.on('message', (message: any) => {
const jsonRpcRequest = JSON.parse(message.utf8Data);
if (jsonRpcRequest.method === 'mesh_subscribe') {
connection.sendUTF(
JSON.stringify({
id: jsonRpcRequest.id,
jsonrpc: '2.0',
result: subscriptionId,
}),
);
} else if (jsonRpcRequest.method === 'mesh_addOrders') {
connection.sendUTF(
JSON.stringify({
id: jsonRpcRequest.id,
...orderInfoResponse,
}),
);
}
});
});
const getOrdersStub = sinon
.stub(WSClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve([]));
const addOrdersStub = sinon
.stub(WSClient.prototype, 'addOrdersAsync')
.callsFake(async () => Promise.resolve({ accepted: [], rejected: [] }));
stubs.push(getOrdersStub);
stubs.push(addOrdersStub);
provider = new MeshOrderProvider(
{
websocketEndpoint,
wsOpts: {
reconnectAfter: 1,
clientConfig: {
fragmentOutgoingMessages: false,
},
},
},
orderStore,
);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(getOrdersStub.callCount).toBe(1);
// Orders are not added on a subscription, only during reconnnect
expect(addOrdersStub.callCount).toBe(0);
const added = JSON.stringify(addedResponse);
connection.sendUTF(added);
await utils.delayAsync(5);
expect(orders.size()).toBe(1);
// Drop the connection and check orders are re-validated
connection.drop();
await utils.delayAsync(5);
expect(addOrdersStub.callCount).toBe(1);
});
});
describe('#getAvailableAssetDatasAsync', () => {
test('stores the orders', async () => {
const order = createOrder(makerAssetData, takerAssetData);
const orderInfo = {
orderHash: '0x00',
signedOrder: order.order,
fillableTakerAssetAmount: new BigNumber(1),
};
stubs.push(
sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([orderInfo])),
);
stubs.push(
sinon
.stub(WSClient.prototype, 'subscribeToOrdersAsync')
.callsFake(async () => Promise.resolve(subscriptionId)),
);
provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
const assetPairs = await provider.getAvailableAssetDatasAsync();
expect(assetPairs.length).toBe(2);
const assetDataA = {
assetData: makerAssetData,
maxAmount: new BigNumber(
'115792089237316195423570985008687907853269984665640564039457584007913129639935',
),
minAmount: new BigNumber('0'),
precision: 18,
};
const assetDataB = {
...assetDataA,
assetData: takerAssetData,
};
expect(assetPairs).toMatchObject([
{ assetDataA, assetDataB },
{ assetDataA: assetDataB, assetDataB: assetDataA },
]);
});
});
});

View File

@@ -0,0 +1,99 @@
import { HttpClient } from '@0x/connect';
import * as sinon from 'sinon';
import { SRAPollingOrderProvider } from '../../src';
import { BaseOrderProvider } from '../../src/order_provider/base_order_provider';
import { OrderStore } from '../../src/order_store';
import { utils } from '../../src/utils';
import { createOrder } from '../utils';
describe('SRAPollingOrderProvider', () => {
let orderStore: OrderStore;
let provider: BaseOrderProvider;
const httpEndpoint = 'https://localhost';
const makerAssetData = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359';
const takerAssetData = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const stubs: sinon.SinonStub[] = [];
afterEach(() => {
void provider.destroyAsync();
stubs.forEach(s => s.restore());
});
beforeEach(() => {
orderStore = new OrderStore();
});
describe('#createSubscriptionForAssetPairAsync', () => {
test('fetches order on first subscription', async () => {
const stub = sinon
.stub(HttpClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 }));
stubs.push(stub);
provider = new SRAPollingOrderProvider({ httpEndpoint, pollingIntervalMs: 5 }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(stub.callCount).toBe(2);
});
test('fetches once when the same subscription is created', async () => {
const stub = sinon
.stub(HttpClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 }));
stubs.push(stub);
provider = new SRAPollingOrderProvider({ httpEndpoint, pollingIntervalMs: 5 }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(stub.callCount).toBe(2);
});
test('periodically polls for orders', async () => {
const stub = sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records: [createOrder(makerAssetData, takerAssetData)],
total: 1,
perPage: 1,
page: 1,
}),
);
stubs.push(stub);
provider = new SRAPollingOrderProvider({ httpEndpoint, pollingIntervalMs: 1 }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await utils.delayAsync(5);
expect(stub.callCount).toBeGreaterThan(2);
});
test('stores the orders returned from the API response', async () => {
const records = [createOrder(makerAssetData, takerAssetData)];
stubs.push(
sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records,
total: 1,
perPage: 1,
page: 1,
}),
),
);
provider = new SRAPollingOrderProvider({ httpEndpoint, pollingIntervalMs: 30000 }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(orders.size()).toBe(1);
});
test('removes the order from the set when the API response no longer returns the order', async () => {
const records = [createOrder(makerAssetData, takerAssetData)];
stubs.push(
sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records,
total: 1,
perPage: 1,
page: 1,
}),
),
);
provider = new SRAPollingOrderProvider({ httpEndpoint, pollingIntervalMs: 1 }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(orders.size()).toBe(1);
// Delete the record from the API response
records.splice(0, 1);
await utils.delayAsync(5);
expect(orders.size()).toBe(0);
});
});
});

View File

@@ -0,0 +1,138 @@
import { HttpClient, ordersChannelFactory, OrdersChannelHandler } from '@0x/connect';
import * as sinon from 'sinon';
import { SRAWebsocketOrderProvider } from '../../src';
import { BaseOrderProvider } from '../../src/order_provider/base_order_provider';
import { OrderStore } from '../../src/order_store';
import { createOrder } from '../utils';
// tslint:disable-next-line:no-empty
const NOOP = () => {};
describe('SRAWebsocketOrderProvider', () => {
let orderStore: OrderStore;
let provider: BaseOrderProvider;
const httpEndpoint = 'https://localhost';
const websocketEndpoint = 'wss://localhost';
const makerAssetData = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359';
const takerAssetData = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const stubs: sinon.SinonStub[] = [];
afterEach(() => {
void provider.destroyAsync();
stubs.forEach(s => s.restore());
});
beforeEach(() => {
orderStore = new OrderStore();
});
describe('#createSubscriptionForAssetPairAsync', () => {
test('fetches order on first subscription', async () => {
const httpStub = sinon
.stub(HttpClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 }));
stubs.push(
sinon
.stub(ordersChannelFactory, 'createWebSocketOrdersChannelAsync')
.callsFake(async () => Promise.resolve({ subscribe: NOOP, close: NOOP })),
);
stubs.push(httpStub);
provider = new SRAWebsocketOrderProvider({ httpEndpoint, websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(httpStub.callCount).toBe(2);
});
test('fetches once when the same subscription is called', async () => {
const stub = sinon
.stub(HttpClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 }));
stubs.push(
sinon
.stub(ordersChannelFactory, 'createWebSocketOrdersChannelAsync')
.callsFake(async () => Promise.resolve({ subscribe: NOOP, close: NOOP })),
);
stubs.push(stub);
provider = new SRAWebsocketOrderProvider({ httpEndpoint, websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(stub.callCount).toBe(2);
});
test('adds orders from the subscription', async () => {
const stub = sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records: [],
total: 0,
perPage: 1,
page: 1,
}),
);
stubs.push(stub);
let handler: OrdersChannelHandler | undefined;
const wsStub = sinon
.stub(ordersChannelFactory, 'createWebSocketOrdersChannelAsync')
.callsFake(async (_url, updateHandler) => {
handler = updateHandler;
return Promise.resolve({ subscribe: NOOP, close: NOOP });
});
stubs.push(wsStub);
provider = new SRAWebsocketOrderProvider({ httpEndpoint, websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(handler).not.toBe(undefined);
if (handler) {
const channel = '';
const subscriptionOpts = {};
const orders = [createOrder(makerAssetData, takerAssetData)];
handler.onUpdate(channel as any, subscriptionOpts as any, orders);
}
expect(stub.callCount).toBe(2);
expect(wsStub.callCount).toBe(1);
const storedOrders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(storedOrders.size()).toBe(1);
});
test('stores the orders', async () => {
stubs.push(
sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records: [createOrder(makerAssetData, takerAssetData)],
total: 1,
perPage: 1,
page: 1,
}),
),
);
stubs.push(
sinon
.stub(ordersChannelFactory, 'createWebSocketOrdersChannelAsync')
.callsFake(async () => Promise.resolve({ subscribe: NOOP, close: NOOP })),
);
provider = new SRAWebsocketOrderProvider({ httpEndpoint, websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
expect(orders.size()).toBe(1);
});
test('reconnects on channel close', async () => {
stubs.push(
sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records: [],
total: 0,
perPage: 1,
page: 1,
}),
),
);
let handler: OrdersChannelHandler | undefined;
const wsStub = sinon
.stub(ordersChannelFactory, 'createWebSocketOrdersChannelAsync')
.callsFake(async (_url, updateHandler) => {
handler = updateHandler;
return Promise.resolve({ subscribe: NOOP, close: NOOP });
});
stubs.push(wsStub);
provider = new SRAWebsocketOrderProvider({ httpEndpoint, websocketEndpoint }, orderStore);
await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
expect(handler).not.toBe(undefined);
(handler as OrdersChannelHandler).onClose(undefined as any);
// Creates the new connection
expect(wsStub.callCount).toBe(2);
});
});
});

View File

@@ -0,0 +1,76 @@
import { HttpClient } from '@0x/connect';
import * as sinon from 'sinon';
import { Orderbook } from '../src';
import { createOrder } from './utils';
describe('Orderbook', () => {
const httpEndpoint = 'https://localhost';
const makerAssetData = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359';
const takerAssetData = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const stubs: sinon.SinonStub[] = [];
afterEach(() => {
stubs.forEach(s => s.restore());
});
describe('#getOrdersAsync', () => {
test('returns the orders stored', async () => {
const records = [createOrder(makerAssetData, takerAssetData)];
stubs.push(
sinon.stub(HttpClient.prototype, 'getOrdersAsync').callsFake(async () =>
Promise.resolve({
records,
total: 1,
perPage: 1,
page: 1,
}),
),
);
const orderbook = Orderbook.getOrderbookForPollingProvider({ httpEndpoint, pollingIntervalMs: 5 });
const orders = await orderbook.getOrdersAsync(makerAssetData, takerAssetData);
expect(orders.length).toBe(1);
});
});
describe('#addOrdersAsync', () => {
test('propagates the order rejection', async () => {
stubs.push(
sinon
.stub(HttpClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 })),
);
stubs.push(
sinon
.stub(HttpClient.prototype, 'submitOrderAsync')
.callsFake(async () => Promise.reject('INVALID_ORDER')),
);
const orderbook = Orderbook.getOrderbookForPollingProvider({ httpEndpoint, pollingIntervalMs: 5 });
const result = await orderbook.addOrdersAsync([createOrder(makerAssetData, takerAssetData).order]);
expect(result.rejected.length).toBe(1);
expect(result.accepted.length).toBe(0);
});
test('propagates the order accepted', async () => {
stubs.push(
sinon
.stub(HttpClient.prototype, 'getOrdersAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 })),
);
stubs.push(sinon.stub(HttpClient.prototype, 'submitOrderAsync').callsFake(async () => Promise.resolve()));
const orderbook = Orderbook.getOrderbookForPollingProvider({ httpEndpoint, pollingIntervalMs: 5 });
const result = await orderbook.addOrdersAsync([createOrder(makerAssetData, takerAssetData).order]);
expect(result.rejected.length).toBe(0);
expect(result.accepted.length).toBe(1);
});
});
describe('#getAvailableAssetDatasAsync', () => {
test('gets the available assets', async () => {
stubs.push(
sinon
.stub(HttpClient.prototype, 'getAssetPairsAsync')
.callsFake(async () => Promise.resolve({ records: [], total: 0, perPage: 0, page: 1 })),
);
const orderbook = Orderbook.getOrderbookForPollingProvider({ httpEndpoint, pollingIntervalMs: 5 });
const result = await orderbook.getAvailableAssetDatasAsync();
expect(result.length).toBe(0);
});
});
});

View File

@@ -0,0 +1,41 @@
import { utils } from '../src/utils';
describe('Utils', () => {
describe('.getOrderHash', () => {
const order = {
makerAddress: '0x50f84bbee6fb250d6f49e854fa280445369d64d9',
makerAssetData: '0xf47261b00000000000000000000000000f5d2fb29fb7d3cfee444a200298f468908cc942',
makerAssetAmount: '4424020538752105500000',
makerFee: '0',
takerAddress: '0x0000000000000000000000000000000000000000',
takerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
takerAssetAmount: '1000000000000000061',
takerFee: '0',
senderAddress: '0x0000000000000000000000000000000000000000',
exchangeAddress: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
feeRecipientAddress: '0xa258b39954cef5cb142fd567a46cddb31a670124',
expirationTimeSeconds: '1559422407',
salt: '1559422141994',
signature:
'0x1cf16c2f3a210965b5e17f51b57b869ba4ddda33df92b0017b4d8da9dacd3152b122a73844eaf50ccde29a42950239ba36a525ed7f1698a8a5e1896cf7d651aed203',
};
test('calculates the orderhash if it does not exist', async () => {
const orderHash = utils.getOrderHash(order as any);
const calculatedOrderHash = utils.getOrderHash({ order: order as any, metaData: {} });
expect(orderHash).toBe(calculatedOrderHash);
expect(orderHash).toBe('0x96e6eb6174dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ecc13fd4');
});
});
describe('.attemptAsync', () => {
test('attempts the operation multiple times if the operation throws', async () => {
const success = 'Success';
const mock = jest
.fn()
.mockRejectedValueOnce(new Error('Async Error'))
.mockResolvedValue(success);
const result = await utils.attemptAsync<string>(mock);
expect(result).toBe(success);
expect(mock.mock.calls.length).toBe(2);
});
});
});

View File

@@ -0,0 +1,27 @@
import { APIOrder } from '@0x/connect';
import { BigNumber } from '@0x/utils';
export const createOrder = (makerAssetData: string, takerAssetData: string): APIOrder => {
return {
order: {
makerAddress: '0x00',
takerAddress: '0x00',
makerAssetData,
takerAssetData,
exchangeAddress: '0x0',
senderAddress: '0x00',
makerAssetAmount: new BigNumber(1),
takerAssetAmount: new BigNumber(1),
feeRecipientAddress: '0x00',
makerFee: new BigNumber(0),
takerFee: new BigNumber(0),
salt: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0),
signature: '0xsig',
},
metaData: {
orderHash: '0x12345',
remainingFillableTakerAssetAmount: new BigNumber(1),
},
};
};

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "."
},
"include": ["src", "test"]
}

View File

@@ -0,0 +1,6 @@
{
"extends": ["@0x/tslint-config"],
"rules": {
"custom-no-magic-numbers": false
}
}

1063
yarn.lock

File diff suppressed because it is too large Load Diff