Merge pull request #2056 from 0xProject/feature/orderbook
@0x/orderbook
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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) {
|
||||
|
@@ -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).
|
||||
|
@@ -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 {
|
||||
|
@@ -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(
|
||||
|
177
packages/asset-swapper/test/swap_quote_consumer_test.ts
Normal file
177
packages/asset-swapper/test/swap_quote_consumer_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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,
|
||||
);
|
||||
|
@@ -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;
|
||||
};
|
||||
|
63
packages/asset-swapper/webpack.config.js
Normal file
63
packages/asset-swapper/webpack.config.js
Normal 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/,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@@ -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
|
||||
|
114
packages/orderbook/README.md
Normal file
114
packages/orderbook/README.md
Normal 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
|
||||
```
|
10
packages/orderbook/jest.config.js
Normal file
10
packages/orderbook/jest.config.js
Normal 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'],
|
||||
};
|
47
packages/orderbook/package.json
Normal file
47
packages/orderbook/package.json
Normal 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"
|
||||
}
|
||||
}
|
17
packages/orderbook/src/index.ts
Normal file
17
packages/orderbook/src/index.ts
Normal 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';
|
34
packages/orderbook/src/order_provider/base_order_provider.ts
Normal file
34
packages/orderbook/src/order_provider/base_order_provider.ts
Normal 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);
|
||||
}
|
||||
}
|
108
packages/orderbook/src/order_provider/base_sra_order_provider.ts
Normal file
108
packages/orderbook/src/order_provider/base_sra_order_provider.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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: [] };
|
||||
}
|
||||
}
|
205
packages/orderbook/src/order_provider/mesh_order_provider.ts
Normal file
205
packages/orderbook/src/order_provider/mesh_order_provider.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
66
packages/orderbook/src/order_set.ts
Normal file
66
packages/orderbook/src/order_set.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
43
packages/orderbook/src/order_store.ts
Normal file
43
packages/orderbook/src/order_store.ts
Normal 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();
|
||||
}
|
||||
}
|
107
packages/orderbook/src/orderbook.ts
Normal file
107
packages/orderbook/src/orderbook.ts
Normal 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();
|
||||
}
|
||||
}
|
55
packages/orderbook/src/types.ts
Normal file
55
packages/orderbook/src/types.ts
Normal 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;
|
||||
}
|
43
packages/orderbook/src/utils.ts
Normal file
43
packages/orderbook/src/utils.ts
Normal 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;
|
||||
},
|
||||
};
|
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
76
packages/orderbook/test/orderbook.test.ts
Normal file
76
packages/orderbook/test/orderbook.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
41
packages/orderbook/test/utils.test.ts
Normal file
41
packages/orderbook/test/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
27
packages/orderbook/test/utils.ts
Normal file
27
packages/orderbook/test/utils.ts
Normal 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),
|
||||
},
|
||||
};
|
||||
};
|
8
packages/orderbook/tsconfig.json
Normal file
8
packages/orderbook/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
6
packages/orderbook/tslint.json
Normal file
6
packages/orderbook/tslint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["@0x/tslint-config"],
|
||||
"rules": {
|
||||
"custom-no-magic-numbers": false
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user