import { APIOrder } from '@0x/connect'; import { AcceptedOrderInfo, OrderEvent, OrderEventEndState, 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); // TODO(dekz): Remove the any hack when mesh is published v3 return { // tslint:disable:no-unnecessary-type-assertion order: orderEvent.signedOrder as any, 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 { 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 await this._orderStore.keysAsync()) { 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 { // 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 { const { accepted, rejected } = await utils.attemptAsync(() => this._wsClient.addOrdersAsync(orders)); // TODO(dekz): Remove the any hack when mesh is published v3 return { accepted: accepted.map(o => o.signedOrder) as any, rejected: rejected.map(o => ({ order: o.signedOrder, message: o.status.message })) as any, }; } /** * Destroys the order provider, removing any subscriptions */ public async destroyAsync(): Promise { 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 { if (this._wsSubscriptionId) { return; } this._wsSubscriptionId = await this._wsClient.subscribeToOrdersAsync(this._handleOrderUpdatesAsync.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 { for (const assetPairKey of await this._orderStore.keysAsync()) { const currentOrders = await this._orderStore.getOrderSetForAssetPairAsync(assetPairKey); const { rejected } = await utils.attemptAsync(() => this._wsClient.addOrdersAsync(Array.from(currentOrders.values()).map(o => o.order)), ); // Remove any rejected orders await this._updateStoreAsync({ 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 { 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)) { await this._updateStoreAsync({ 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 async _handleOrderUpdatesAsync(orderEvents: OrderEvent[]): Promise { 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.endState) { case OrderEventEndState.Added: case OrderEventEndState.Unexpired: { addedRemovedByAssetPairKey[assetPairKey].added.push(apiOrder); break; } case OrderEventEndState.Cancelled: case OrderEventEndState.Expired: case OrderEventEndState.FullyFilled: case OrderEventEndState.Unfunded: case OrderEventEndState.StoppedWatching: { addedRemovedByAssetPairKey[assetPairKey].removed.push(apiOrder); break; } case OrderEventEndState.FillabilityIncreased: case OrderEventEndState.Filled: { addedRemovedByAssetPairKey[assetPairKey].added.push(apiOrder); break; } default: break; } } for (const assetPairKey of Object.keys(addedRemovedByAssetPairKey)) { await this._updateStoreAsync(addedRemovedByAssetPairKey[assetPairKey]); } } }