aboutsummaryrefslogtreecommitdiffstats
path: root/packages/asset-buyer/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/asset-buyer/src')
-rw-r--r--packages/asset-buyer/src/asset_buyer.ts327
-rw-r--r--packages/asset-buyer/src/constants.ts40
-rw-r--r--packages/asset-buyer/src/globals.d.ts6
-rw-r--r--packages/asset-buyer/src/index.ts25
-rw-r--r--packages/asset-buyer/src/order_providers/basic_order_provider.ts41
-rw-r--r--packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts105
-rw-r--r--packages/asset-buyer/src/types.ts123
-rw-r--r--packages/asset-buyer/src/utils/assert.ts39
-rw-r--r--packages/asset-buyer/src/utils/asset_data_utils.ts12
-rw-r--r--packages/asset-buyer/src/utils/buy_quote_calculator.ts207
-rw-r--r--packages/asset-buyer/src/utils/order_provider_response_processor.ts169
-rw-r--r--packages/asset-buyer/src/utils/order_utils.ts74
12 files changed, 1168 insertions, 0 deletions
diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts
new file mode 100644
index 000000000..934410c55
--- /dev/null
+++ b/packages/asset-buyer/src/asset_buyer.ts
@@ -0,0 +1,327 @@
+import { ContractWrappers, ContractWrappersError, ForwarderWrapperError } from '@0x/contract-wrappers';
+import { schemas } from '@0x/json-schemas';
+import { SignedOrder } from '@0x/order-utils';
+import { ObjectMap } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider } 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 {
+ AssetBuyerError,
+ AssetBuyerOpts,
+ BuyQuote,
+ BuyQuoteExecutionOpts,
+ BuyQuoteRequestOpts,
+ OrderProvider,
+ OrderProviderResponse,
+ OrdersAndFillableAmounts,
+} from './types';
+
+import { assert } from './utils/assert';
+import { assetDataUtils } from './utils/asset_data_utils';
+import { buyQuoteCalculator } from './utils/buy_quote_calculator';
+import { orderProviderResponseProcessor } from './utils/order_provider_response_processor';
+
+interface OrdersEntry {
+ ordersAndFillableAmounts: OrdersAndFillableAmounts;
+ lastRefreshTime: number;
+}
+
+export class AssetBuyer {
+ public readonly provider: Provider;
+ public readonly orderProvider: OrderProvider;
+ public readonly networkId: number;
+ public readonly orderRefreshIntervalMs: number;
+ public readonly expiryBufferSeconds: 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 AssetBuyer instance given existing liquidity in the form of orders and feeOrders.
+ * @param provider 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 (WETH).
+ * @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array.
+ * @param options Initialization options for the AssetBuyer. See type definition for details.
+ *
+ * @return An instance of AssetBuyer
+ */
+ public static getAssetBuyerForProvidedOrders(
+ provider: Provider,
+ orders: SignedOrder[],
+ options: Partial<AssetBuyerOpts> = {},
+ ): AssetBuyer {
+ assert.isWeb3Provider('provider', provider);
+ 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 assetBuyer = new AssetBuyer(provider, orderProvider, options);
+ return assetBuyer;
+ }
+ /**
+ * Instantiates a new AssetBuyer instance given a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint
+ * @param provider 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 AssetBuyer. See type definition for details.
+ *
+ * @return An instance of AssetBuyer
+ */
+ public static getAssetBuyerForStandardRelayerAPIUrl(
+ provider: Provider,
+ sraApiUrl: string,
+ options: Partial<AssetBuyerOpts> = {},
+ ): AssetBuyer {
+ assert.isWeb3Provider('provider', provider);
+ assert.isWebUri('sraApiUrl', sraApiUrl);
+ const networkId = options.networkId || constants.DEFAULT_ASSET_BUYER_OPTS.networkId;
+ const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl, networkId);
+ const assetBuyer = new AssetBuyer(provider, orderProvider, options);
+ return assetBuyer;
+ }
+ /**
+ * Instantiates a new AssetBuyer instance
+ * @param provider 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 options Initialization options for the AssetBuyer. See type definition for details.
+ *
+ * @return An instance of AssetBuyer
+ */
+ constructor(provider: Provider, orderProvider: OrderProvider, options: Partial<AssetBuyerOpts> = {}) {
+ const { networkId, orderRefreshIntervalMs, expiryBufferSeconds } = _.merge(
+ {},
+ constants.DEFAULT_ASSET_BUYER_OPTS,
+ options,
+ );
+ assert.isWeb3Provider('provider', provider);
+ assert.isValidOrderProvider('orderProvider', orderProvider);
+ assert.isNumber('networkId', networkId);
+ assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
+ assert.isNumber('expiryBufferSeconds', expiryBufferSeconds);
+ this.provider = provider;
+ this.orderProvider = orderProvider;
+ this.networkId = networkId;
+ this.orderRefreshIntervalMs = orderRefreshIntervalMs;
+ this.expiryBufferSeconds = expiryBufferSeconds;
+ this._contractWrappers = new ContractWrappers(this.provider, {
+ networkId,
+ });
+ }
+ /**
+ * Get a `BuyQuote` containing all information relevant to fulfilling a buy given a desired assetData.
+ * You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy.
+ * @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
+ * @param assetBuyAmount The amount of asset to buy.
+ * @param options Options for the request. See type definition for more information.
+ *
+ * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
+ */
+ public async getBuyQuoteAsync(
+ assetData: string,
+ assetBuyAmount: BigNumber,
+ options: Partial<BuyQuoteRequestOpts> = {},
+ ): Promise<BuyQuote> {
+ const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = _.merge(
+ {},
+ constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS,
+ options,
+ );
+ assert.isString('assetData', assetData);
+ assert.isBigNumber('assetBuyAmount', assetBuyAmount);
+ assert.isValidPercentage('feePercentage', feePercentage);
+ assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh);
+ assert.isNumber('slippagePercentage', slippagePercentage);
+ const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
+ const isMakerAssetZrxToken = assetData === 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(assetData, shouldForceOrderRefresh),
+ isMakerAssetZrxToken
+ ? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS)
+ : this._getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh),
+ shouldForceOrderRefresh,
+ ]);
+ if (ordersAndFillableAmounts.orders.length === 0) {
+ throw new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${assetData}`);
+ }
+ const buyQuote = buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ feeOrdersAndFillableAmounts,
+ assetBuyAmount,
+ feePercentage,
+ slippagePercentage,
+ isMakerAssetZrxToken,
+ );
+ return buyQuote;
+ }
+ /**
+ * Get a `BuyQuote` containing all information relevant to fulfilling a buy given a desired ERC20 token address.
+ * You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy.
+ * @param tokenAddress The ERC20 token address.
+ * @param assetBuyAmount The amount of asset to buy.
+ * @param options Options for the request. See type definition for more information.
+ *
+ * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
+ */
+ public async getBuyQuoteForERC20TokenAddressAsync(
+ tokenAddress: string,
+ assetBuyAmount: BigNumber,
+ options: Partial<BuyQuoteRequestOpts> = {},
+ ): Promise<BuyQuote> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isBigNumber('assetBuyAmount', assetBuyAmount);
+ const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress);
+ const buyQuote = this.getBuyQuoteAsync(assetData, assetBuyAmount, options);
+ return buyQuote;
+ }
+ /**
+ * Given a BuyQuote and desired rate, attempt to execute the buy.
+ * @param buyQuote An object that conforms to BuyQuote. See type definition for more information.
+ * @param options Options for the execution of the BuyQuote. See type definition for more information.
+ *
+ * @return A promise of the txHash.
+ */
+ public async executeBuyQuoteAsync(
+ buyQuote: BuyQuote,
+ options: Partial<BuyQuoteExecutionOpts> = {},
+ ): Promise<string> {
+ const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = _.merge(
+ {},
+ constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
+ options,
+ );
+ assert.isValidBuyQuote('buyQuote', buyQuote);
+ if (!_.isUndefined(ethAmount)) {
+ assert.isBigNumber('ethAmount', ethAmount);
+ }
+ if (!_.isUndefined(takerAddress)) {
+ assert.isETHAddressHex('takerAddress', takerAddress);
+ }
+ assert.isETHAddressHex('feeRecipient', feeRecipient);
+ if (!_.isUndefined(gasLimit)) {
+ assert.isNumber('gasLimit', gasLimit);
+ }
+ if (!_.isUndefined(gasPrice)) {
+ assert.isBigNumber('gasPrice', gasPrice);
+ }
+ const { orders, feeOrders, feePercentage, assetBuyAmount, worstCaseQuoteInfo } = buyQuote;
+ // if no takerAddress is provided, try to get one from the provider
+ let finalTakerAddress;
+ if (!_.isUndefined(takerAddress)) {
+ finalTakerAddress = takerAddress;
+ } else {
+ const web3Wrapper = new Web3Wrapper(this.provider);
+ const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ const firstAvailableAddress = _.head(availableAddresses);
+ if (!_.isUndefined(firstAvailableAddress)) {
+ finalTakerAddress = firstAvailableAddress;
+ } else {
+ throw new Error(AssetBuyerError.NoAddressAvailable);
+ }
+ }
+ try {
+ // if no ethAmount is provided, default to the worst ethAmount from buyQuote
+ const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
+ orders,
+ assetBuyAmount,
+ finalTakerAddress,
+ ethAmount || worstCaseQuoteInfo.totalEthAmount,
+ feeOrders,
+ feePercentage,
+ feeRecipient,
+ {
+ gasLimit,
+ gasPrice,
+ shouldValidate: true,
+ },
+ );
+ return txHash;
+ } catch (err) {
+ if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) {
+ throw new Error(AssetBuyerError.SignatureRequestDenied);
+ } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) {
+ throw new Error(AssetBuyerError.TransactionValueTooLow);
+ } else {
+ throw err;
+ }
+ }
+ }
+ /**
+ * Get the asset data of all assets that are purchaseable with ether token (wETH) in the order provider passed in at init.
+ *
+ * @return An array of asset data strings that can be purchased using wETH.
+ */
+ public async getAvailableAssetDatasAsync(): Promise<string[]> {
+ const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow();
+ return this.orderProvider.getAvailableMakerAssetDatasAsync(etherTokenAssetData);
+ }
+ /**
+ * Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders
+ */
+ private async _getOrdersAndFillableAmountsAsync(
+ assetData: string,
+ shouldForceOrderRefresh: boolean,
+ ): Promise<OrdersAndFillableAmounts> {
+ // try to get ordersEntry from the map
+ const ordersEntryIfExists = this._ordersEntryMap[assetData];
+ // 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 =
+ _.isUndefined(ordersEntryIfExists) ||
+ shouldForceOrderRefresh ||
+ // tslint:disable:restrict-plus-operands
+ ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now();
+ if (!shouldRefresh) {
+ const result = ordersEntryIfExists.ordersAndFillableAmounts;
+ return result;
+ }
+ const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow();
+ const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
+ // construct orderProvider request
+ const orderProviderRequest = {
+ makerAssetData: assetData,
+ takerAssetData: etherTokenAssetData,
+ networkId: this.networkId,
+ };
+ const request = orderProviderRequest;
+ // get provider response
+ const response = await this.orderProvider.getOrdersAsync(request);
+ // 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);
+ // process the responses into one object
+ const isMakerAssetZrxToken = assetData === zrxTokenAssetData;
+ const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync(
+ response,
+ isMakerAssetZrxToken,
+ this.expiryBufferSeconds,
+ this._contractWrappers.orderValidator,
+ );
+ const lastRefreshTime = Date.now();
+ const updatedOrdersEntry = {
+ ordersAndFillableAmounts,
+ lastRefreshTime,
+ };
+ this._ordersEntryMap[assetData] = updatedOrdersEntry;
+ return ordersAndFillableAmounts;
+ }
+ /**
+ * Get the assetData that represents the WETH token.
+ * Will throw if WETH does not exist for the current network.
+ */
+ private _getEtherTokenAssetDataOrThrow(): string {
+ return assetDataUtils.getEtherTokenAssetData(this._contractWrappers);
+ }
+ /**
+ * Get the assetData that represents the ZRX token.
+ * Will throw if ZRX does not exist for the current network.
+ */
+ private _getZrxTokenAssetDataOrThrow(): string {
+ return this._contractWrappers.exchange.getZRXAssetData();
+ }
+}
diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts
new file mode 100644
index 000000000..c0e1bf27d
--- /dev/null
+++ b/packages/asset-buyer/src/constants.ts
@@ -0,0 +1,40 @@
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+
+import { AssetBuyerOpts, BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrdersAndFillableAmounts } from './types';
+
+const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
+const MAINNET_NETWORK_ID = 1;
+
+const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = {
+ networkId: MAINNET_NETWORK_ID,
+ orderRefreshIntervalMs: 10000, // 10 seconds
+ expiryBufferSeconds: 120, // 2 minutes
+};
+
+const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = {
+ feePercentage: 0,
+ shouldForceOrderRefresh: false,
+ slippagePercentage: 0.2, // 20% slippage protection
+};
+
+// Other default values are dynamically determined
+const DEFAULT_BUY_QUOTE_EXECUTION_OPTS: BuyQuoteExecutionOpts = {
+ feeRecipient: NULL_ADDRESS,
+};
+
+const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = {
+ orders: [] as SignedOrder[],
+ remainingFillableMakerAssetAmounts: [] as BigNumber[],
+};
+
+export const constants = {
+ ZERO_AMOUNT: new BigNumber(0),
+ NULL_ADDRESS,
+ MAINNET_NETWORK_ID,
+ ETHER_TOKEN_DECIMALS: 18,
+ DEFAULT_ASSET_BUYER_OPTS,
+ DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
+ DEFAULT_BUY_QUOTE_REQUEST_OPTS,
+ EMPTY_ORDERS_AND_FILLABLE_AMOUNTS,
+};
diff --git a/packages/asset-buyer/src/globals.d.ts b/packages/asset-buyer/src/globals.d.ts
new file mode 100644
index 000000000..94e63a32d
--- /dev/null
+++ b/packages/asset-buyer/src/globals.d.ts
@@ -0,0 +1,6 @@
+declare module '*.json' {
+ const json: any;
+ /* tslint:disable */
+ export default json;
+ /* tslint:enable */
+}
diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts
new file mode 100644
index 000000000..8418edb42
--- /dev/null
+++ b/packages/asset-buyer/src/index.ts
@@ -0,0 +1,25 @@
+export {
+ JSONRPCRequestPayload,
+ JSONRPCResponsePayload,
+ JSONRPCResponseError,
+ JSONRPCErrorCallback,
+ Provider,
+} from 'ethereum-types';
+export { SignedOrder } from '@0x/types';
+export { BigNumber } from '@0x/utils';
+
+export { AssetBuyer } from './asset_buyer';
+export { BasicOrderProvider } from './order_providers/basic_order_provider';
+export { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider';
+export {
+ AssetBuyerError,
+ AssetBuyerOpts,
+ BuyQuote,
+ BuyQuoteExecutionOpts,
+ BuyQuoteInfo,
+ BuyQuoteRequestOpts,
+ OrderProvider,
+ OrderProviderRequest,
+ OrderProviderResponse,
+ SignedOrderWithRemainingFillableMakerAssetAmount,
+} from './types';
diff --git a/packages/asset-buyer/src/order_providers/basic_order_provider.ts b/packages/asset-buyer/src/order_providers/basic_order_provider.ts
new file mode 100644
index 000000000..76685f27a
--- /dev/null
+++ b/packages/asset-buyer/src/order_providers/basic_order_provider.ts
@@ -0,0 +1,41 @@
+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);
+ }
+}
diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts
new file mode 100644
index 000000000..be1fc55d6
--- /dev/null
+++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts
@@ -0,0 +1,105 @@
+import { HttpClient } from '@0x/connect';
+import { APIOrder, AssetPairsResponse, OrderbookResponse } from '@0x/types';
+import * as _ from 'lodash';
+
+import {
+ AssetBuyerError,
+ OrderProvider,
+ OrderProviderRequest,
+ OrderProviderResponse,
+ SignedOrderWithRemainingFillableMakerAssetAmount,
+} from '../types';
+import { assert } from '../utils/assert';
+import { orderUtils } from '../utils/order_utils';
+
+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;
+ // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable
+ const remainingFillableTakerAssetAmount = _.get(
+ metaData,
+ 'remainingTakerAssetAmount',
+ order.takerAssetAmount,
+ );
+ const remainingFillableMakerAssetAmount = orderUtils.getRemainingMakerAmount(
+ 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(AssetBuyerError.StandardRelayerApiError);
+ }
+ const apiOrders = orderbook.asks.records;
+ const orders = StandardRelayerAPIOrderProvider._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi(
+ apiOrders,
+ );
+ 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[]> {
+ // 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(AssetBuyerError.StandardRelayerApiError);
+ }
+ return _.map(response.records, item => item.assetDataB.assetData);
+ }
+}
diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts
new file mode 100644
index 000000000..3f1e6ff21
--- /dev/null
+++ b/packages/asset-buyer/src/types.ts
@@ -0,0 +1,123 @@
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+
+/**
+ * makerAssetData: The assetData representing the desired makerAsset.
+ * takerAssetData: The assetData representing the desired takerAsset.
+ * networkId: The networkId that the desired orders should be for.
+ */
+export interface OrderProviderRequest {
+ makerAssetData: string;
+ takerAssetData: string;
+}
+
+/**
+ * orders: An array of orders with optional remaining fillable makerAsset amounts. See type for more info.
+ */
+export interface OrderProviderResponse {
+ orders: SignedOrderWithRemainingFillableMakerAssetAmount[];
+}
+
+/**
+ * A normal SignedOrder with one extra optional property `remainingFillableMakerAssetAmount`
+ * remainingFillableMakerAssetAmount: The amount of the makerAsset that is available to be filled
+ */
+export interface SignedOrderWithRemainingFillableMakerAssetAmount extends SignedOrder {
+ remainingFillableMakerAssetAmount?: BigNumber;
+}
+/**
+ * gerOrdersAsync: Given an OrderProviderRequest, get an OrderProviderResponse.
+ * getAvailableMakerAssetDatasAsync: Given a taker asset data string, return all availabled paired maker asset data strings.
+ */
+export interface OrderProvider {
+ getOrdersAsync: (orderProviderRequest: OrderProviderRequest) => Promise<OrderProviderResponse>;
+ getAvailableMakerAssetDatasAsync: (takerAssetData: string) => Promise<string[]>;
+}
+
+/**
+ * assetData: String that represents a specific asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
+ * assetBuyAmount: The amount of asset to buy.
+ * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage.
+ * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above.
+ * feePercentage: Optional affiliate fee percentage used to calculate the eth amounts above.
+ * bestCaseQuoteInfo: Info about the best case price for the asset.
+ * worstCaseQuoteInfo: Info about the worst case price for the asset.
+ */
+export interface BuyQuote {
+ assetData: string;
+ assetBuyAmount: BigNumber;
+ orders: SignedOrder[];
+ feeOrders: SignedOrder[];
+ feePercentage?: number;
+ bestCaseQuoteInfo: BuyQuoteInfo;
+ worstCaseQuoteInfo: BuyQuoteInfo;
+}
+
+/**
+ * ethPerAssetPrice: The price of one unit of the desired asset in ETH
+ * feeEthAmount: The amount of eth required to pay the affiliate fee.
+ * totalEthAmount: the total amount of eth required to complete the buy. (Filling orders, feeOrders, and paying affiliate fee)
+ */
+export interface BuyQuoteInfo {
+ ethPerAssetPrice: BigNumber;
+ feeEthAmount: BigNumber;
+ totalEthAmount: BigNumber;
+}
+
+/**
+ * feePercentage: The affiliate fee percentage. Defaults to 0.
+ * shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false.
+ * slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%).
+ */
+export interface BuyQuoteRequestOpts {
+ feePercentage: number;
+ shouldForceOrderRefresh: boolean;
+ slippagePercentage: number;
+}
+
+/**
+ * ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount.
+ * takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
+ * gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call.
+ * gasPrice: Gas price in Wei to use for a transaction
+ * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000).
+ */
+export interface BuyQuoteExecutionOpts {
+ ethAmount?: BigNumber;
+ takerAddress?: string;
+ gasLimit?: number;
+ gasPrice?: BigNumber;
+ feeRecipient: string;
+}
+
+/**
+ * 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).
+ * expiryBufferSeconds: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
+ */
+export interface AssetBuyerOpts {
+ networkId: number;
+ orderRefreshIntervalMs: number;
+ expiryBufferSeconds: number;
+}
+
+/**
+ * Possible errors thrown by an AssetBuyer instance or associated static methods.
+ */
+export enum AssetBuyerError {
+ NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
+ NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND',
+ StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR',
+ InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY',
+ InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY',
+ NoAddressAvailable = 'NO_ADDRESS_AVAILABLE',
+ InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE',
+ AssetUnavailable = 'ASSET_UNAVAILABLE',
+ SignatureRequestDenied = 'SIGNATURE_REQUEST_DENIED',
+ TransactionValueTooLow = 'TRANSACTION_VALUE_TOO_LOW',
+}
+
+export interface OrdersAndFillableAmounts {
+ orders: SignedOrder[];
+ remainingFillableMakerAssetAmounts: BigNumber[];
+}
diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts
new file mode 100644
index 000000000..2466f53a4
--- /dev/null
+++ b/packages/asset-buyer/src/utils/assert.ts
@@ -0,0 +1,39 @@
+import { assert as sharedAssert } from '@0x/assert';
+import { schemas } from '@0x/json-schemas';
+import * as _ from 'lodash';
+
+import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../types';
+
+export const assert = {
+ ...sharedAssert,
+ isValidBuyQuote(variableName: string, buyQuote: BuyQuote): void {
+ sharedAssert.isHexString(`${variableName}.assetData`, buyQuote.assetData);
+ sharedAssert.doesConformToSchema(`${variableName}.orders`, buyQuote.orders, schemas.signedOrdersSchema);
+ sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, buyQuote.feeOrders, schemas.signedOrdersSchema);
+ assert.isValidBuyQuoteInfo(`${variableName}.bestCaseQuoteInfo`, buyQuote.bestCaseQuoteInfo);
+ assert.isValidBuyQuoteInfo(`${variableName}.worstCaseQuoteInfo`, buyQuote.worstCaseQuoteInfo);
+ sharedAssert.isBigNumber(`${variableName}.assetBuyAmount`, buyQuote.assetBuyAmount);
+ if (!_.isUndefined(buyQuote.feePercentage)) {
+ sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage);
+ }
+ },
+ isValidBuyQuoteInfo(variableName: string, buyQuoteInfo: BuyQuoteInfo): void {
+ sharedAssert.isBigNumber(`${variableName}.ethPerAssetPrice`, buyQuoteInfo.ethPerAssetPrice);
+ sharedAssert.isBigNumber(`${variableName}.feeEthAmount`, buyQuoteInfo.feeEthAmount);
+ sharedAssert.isBigNumber(`${variableName}.totalEthAmount`, buyQuoteInfo.totalEthAmount);
+ },
+ isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void {
+ sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);
+ },
+ isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void {
+ sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData);
+ sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData);
+ },
+ isValidPercentage(variableName: string, percentage: number): void {
+ assert.isNumber(variableName, percentage);
+ assert.assert(
+ percentage >= 0 && percentage <= 1,
+ `Expected ${variableName} to be between 0 and 1, but is ${percentage}`,
+ );
+ },
+};
diff --git a/packages/asset-buyer/src/utils/asset_data_utils.ts b/packages/asset-buyer/src/utils/asset_data_utils.ts
new file mode 100644
index 000000000..70f646902
--- /dev/null
+++ b/packages/asset-buyer/src/utils/asset_data_utils.ts
@@ -0,0 +1,12 @@
+import { ContractWrappers } from '@0x/contract-wrappers';
+import { assetDataUtils as sharedAssetDataUtils } from '@0x/order-utils';
+import * as _ from 'lodash';
+
+export const assetDataUtils = {
+ ...sharedAssetDataUtils,
+ getEtherTokenAssetData(contractWrappers: ContractWrappers): string {
+ const etherTokenAddress = contractWrappers.forwarder.etherTokenAddress;
+ const etherTokenAssetData = sharedAssetDataUtils.encodeERC20AssetData(etherTokenAddress);
+ return etherTokenAssetData;
+ },
+};
diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
new file mode 100644
index 000000000..6a67ed1ed
--- /dev/null
+++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
@@ -0,0 +1,207 @@
+import { marketUtils, SignedOrder } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { constants } from '../constants';
+import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types';
+
+import { orderUtils } from './order_utils';
+
+// Calculates a buy quote for orders that have WETH as the takerAsset
+export const buyQuoteCalculator = {
+ calculate(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+ feePercentage: number,
+ slippagePercentage: number,
+ isMakerAssetZrxToken: boolean,
+ ): BuyQuote {
+ const orders = ordersAndFillableAmounts.orders;
+ const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts;
+ const feeOrders = feeOrdersAndFillableAmounts.orders;
+ const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
+ const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round();
+ // find the orders that cover the desired assetBuyAmount (with slippage)
+ const {
+ resultOrders,
+ remainingFillAmount,
+ ordersRemainingFillableMakerAssetAmounts,
+ } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetBuyAmount, {
+ remainingFillableMakerAssetAmounts,
+ slippageBufferAmount,
+ });
+ // if we do not have enough orders to cover the desired assetBuyAmount, throw
+ if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
+ throw new Error(AssetBuyerError.InsufficientAssetLiquidity);
+ }
+ // if we are not buying ZRX:
+ // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage)
+ // TODO(bmillman): optimization
+ // update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
+ // finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
+ let resultFeeOrders = [] as SignedOrder[];
+ let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[];
+ if (!isMakerAssetZrxToken) {
+ const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ resultOrders,
+ feeOrders,
+ {
+ remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
+ remainingFillableFeeAmounts,
+ },
+ );
+ // if we do not have enough feeOrders to cover the fees, throw
+ if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
+ throw new Error(AssetBuyerError.InsufficientZrxLiquidity);
+ }
+ resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders;
+ feeOrdersRemainingFillableMakerAssetAmounts =
+ feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts;
+ }
+
+ // assetData information for the result
+ const assetData = orders[0].makerAssetData;
+ // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount
+ const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
+ orders: resultOrders,
+ remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
+ };
+ const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
+ orders: resultFeeOrders,
+ remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts,
+ };
+ const bestCaseQuoteInfo = calculateQuoteInfo(
+ trimmedOrdersAndFillableAmounts,
+ trimmedFeeOrdersAndFillableAmounts,
+ assetBuyAmount,
+ feePercentage,
+ isMakerAssetZrxToken,
+ );
+ // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
+ const worstCaseQuoteInfo = calculateQuoteInfo(
+ reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts),
+ reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts),
+ assetBuyAmount,
+ feePercentage,
+ isMakerAssetZrxToken,
+ );
+ return {
+ assetData,
+ orders: resultOrders,
+ feeOrders: resultFeeOrders,
+ bestCaseQuoteInfo,
+ worstCaseQuoteInfo,
+ assetBuyAmount,
+ feePercentage,
+ };
+ },
+};
+
+function calculateQuoteInfo(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+ feePercentage: number,
+ isMakerAssetZrxToken: boolean,
+): BuyQuoteInfo {
+ // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right
+ let ethAmountToBuyAsset = constants.ZERO_AMOUNT;
+ let ethAmountToBuyZrx = constants.ZERO_AMOUNT;
+ if (isMakerAssetZrxToken) {
+ ethAmountToBuyAsset = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount);
+ } else {
+ // find eth and zrx amounts needed to buy
+ const ethAndZrxAmountToBuyAsset = findEthAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, assetBuyAmount);
+ ethAmountToBuyAsset = ethAndZrxAmountToBuyAsset[0];
+ const zrxAmountToBuyAsset = ethAndZrxAmountToBuyAsset[1];
+ // find eth amount needed to buy zrx
+ ethAmountToBuyZrx = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
+ }
+ /// find the eth amount needed to buy the affiliate fee
+ const ethAmountToBuyAffiliateFee = ethAmountToBuyAsset.mul(feePercentage).ceil();
+ const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyZrx);
+ const ethAmountTotal = totalEthAmountWithoutAffiliateFee.plus(ethAmountToBuyAffiliateFee);
+ // divide into the assetBuyAmount in order to find rate of makerAsset / WETH
+ const ethPerAssetPrice = totalEthAmountWithoutAffiliateFee.div(assetBuyAmount);
+ return {
+ totalEthAmount: ethAmountTotal,
+ feeEthAmount: ethAmountToBuyAffiliateFee,
+ ethPerAssetPrice,
+ };
+}
+
+// given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties
+function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFillableAmounts): OrdersAndFillableAmounts {
+ const ordersCopy = _.clone(ordersAndFillableAmounts.orders);
+ const remainingFillableMakerAssetAmountsCopy = _.clone(ordersAndFillableAmounts.remainingFillableMakerAssetAmounts);
+ return {
+ orders: ordersCopy.reverse(),
+ remainingFillableMakerAssetAmounts: remainingFillableMakerAssetAmountsCopy.reverse(),
+ };
+}
+
+function findEthAmountNeededToBuyZrx(
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ zrxBuyAmount: BigNumber,
+): BigNumber {
+ const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts;
+ const result = _.reduce(
+ orders,
+ (acc, order, index) => {
+ const { totalEthAmount, remainingZrxBuyAmount } = acc;
+ const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
+ const makerFillAmount = BigNumber.min(remainingZrxBuyAmount, remainingFillableMakerAssetAmount);
+ const [takerFillAmount, adjustedMakerFillAmount] = orderUtils.getTakerFillAmountForFeeOrder(
+ order,
+ makerFillAmount,
+ );
+ const extraFeeAmount = remainingFillableMakerAssetAmount.greaterThanOrEqualTo(adjustedMakerFillAmount)
+ ? constants.ZERO_AMOUNT
+ : adjustedMakerFillAmount.sub(makerFillAmount);
+ return {
+ totalEthAmount: totalEthAmount.plus(takerFillAmount),
+ remainingZrxBuyAmount: BigNumber.max(
+ constants.ZERO_AMOUNT,
+ remainingZrxBuyAmount.minus(makerFillAmount).plus(extraFeeAmount),
+ ),
+ };
+ },
+ {
+ totalEthAmount: constants.ZERO_AMOUNT,
+ remainingZrxBuyAmount: zrxBuyAmount,
+ },
+ );
+ return result.totalEthAmount;
+}
+
+function findEthAndZrxAmountNeededToBuyAsset(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+): [BigNumber, BigNumber] {
+ const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
+ const result = _.reduce(
+ orders,
+ (acc, order, index) => {
+ const { totalEthAmount, totalZrxAmount, remainingAssetBuyAmount } = acc;
+ const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
+ const makerFillAmount = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount);
+ const takerFillAmount = orderUtils.getTakerFillAmount(order, makerFillAmount);
+ const takerFeeAmount = orderUtils.getTakerFeeAmount(order, takerFillAmount);
+ return {
+ totalEthAmount: totalEthAmount.plus(takerFillAmount),
+ totalZrxAmount: totalZrxAmount.plus(takerFeeAmount),
+ remainingAssetBuyAmount: BigNumber.max(
+ constants.ZERO_AMOUNT,
+ remainingAssetBuyAmount.minus(makerFillAmount),
+ ),
+ };
+ },
+ {
+ totalEthAmount: constants.ZERO_AMOUNT,
+ totalZrxAmount: constants.ZERO_AMOUNT,
+ remainingAssetBuyAmount: assetBuyAmount,
+ },
+ );
+ return [result.totalEthAmount, result.totalZrxAmount];
+}
diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts
new file mode 100644
index 000000000..28f684f3c
--- /dev/null
+++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts
@@ -0,0 +1,169 @@
+import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0x/contract-wrappers';
+import { sortingUtils } from '@0x/order-utils';
+import { RemainingFillableCalculator } from '@0x/order-utils/lib/src/remaining_fillable_calculator';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { constants } from '../constants';
+import {
+ AssetBuyerError,
+ OrderProviderRequest,
+ OrderProviderResponse,
+ OrdersAndFillableAmounts,
+ SignedOrderWithRemainingFillableMakerAssetAmount,
+} from '../types';
+
+import { orderUtils } from './order_utils';
+
+export const orderProviderResponseProcessor = {
+ throwIfInvalidResponse(response: OrderProviderResponse, request: OrderProviderRequest): void {
+ const { makerAssetData, takerAssetData } = request;
+ _.forEach(response.orders, order => {
+ if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) {
+ throw new Error(AssetBuyerError.InvalidOrderProviderResponse);
+ }
+ });
+ },
+ /**
+ * 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
+ * - Sort by rate
+ */
+ async processAsync(
+ orderProviderResponse: OrderProviderResponse,
+ isMakerAssetZrxToken: boolean,
+ expiryBufferSeconds: number,
+ orderValidator?: OrderValidatorWrapper,
+ ): Promise<OrdersAndFillableAmounts> {
+ // drop orders that are expired or not open
+ const filteredOrders = filterOutExpiredAndNonOpenOrders(orderProviderResponse.orders, expiryBufferSeconds);
+ // set the orders to be sorted equal to the filtered orders
+ let unsortedOrders = filteredOrders;
+ // if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts
+ if (!_.isUndefined(orderValidator)) {
+ // TODO(bmillman): improvement
+ // try/catch this request and throw a more domain specific error
+ const takerAddresses = _.map(filteredOrders, () => constants.NULL_ADDRESS);
+ const ordersAndTradersInfo = await orderValidator.getOrdersAndTradersInfoAsync(
+ filteredOrders,
+ takerAddresses,
+ );
+ // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts
+ unsortedOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
+ filteredOrders,
+ ordersAndTradersInfo,
+ isMakerAssetZrxToken,
+ );
+ }
+ // sort orders by rate
+ // TODO(bmillman): optimization
+ // provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens
+ const sortedOrders = isMakerAssetZrxToken
+ ? sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedOrders)
+ : sortingUtils.sortOrdersByFeeAdjustedRate(unsortedOrders);
+ // unbundle orders and fillable amounts and compile final result
+ const result = unbundleOrdersWithAmounts(sortedOrders);
+ return result;
+ },
+};
+
+/**
+ * Given an array of orders, return a new array with expired and non open orders filtered out.
+ */
+function filterOutExpiredAndNonOpenOrders(
+ orders: SignedOrderWithRemainingFillableMakerAssetAmount[],
+ expiryBufferSeconds: number,
+): SignedOrderWithRemainingFillableMakerAssetAmount[] {
+ const result = _.filter(orders, order => {
+ return orderUtils.isOpenOrder(order) && !orderUtils.willOrderExpire(order, expiryBufferSeconds);
+ });
+ return result;
+}
+
+/**
+ * Given an array of orders and corresponding on-chain infos, return a subset of the orders
+ * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts.
+ */
+function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
+ inputOrders: SignedOrder[],
+ ordersAndTradersInfo: OrderAndTraderInfo[],
+ isMakerAssetZrxToken: boolean,
+): SignedOrderWithRemainingFillableMakerAssetAmount[] {
+ // iterate through the input orders and find the ones that are still fillable
+ // for the orders that are still fillable, calculate the remaining fillable maker asset amount
+ const result = _.reduce(
+ inputOrders,
+ (accOrders, order, index) => {
+ // get corresponding on-chain state for the order
+ const { orderInfo, traderInfo } = ordersAndTradersInfo[index];
+ // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating
+ if (orderInfo.orderStatus !== OrderStatus.FILLABLE) {
+ return accOrders;
+ }
+ // if the order IS fillable, add the order and calculate the remaining fillable amount
+ const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]);
+ const transferrableFeeAssetAmount = BigNumber.min([
+ traderInfo.makerZrxAllowance,
+ traderInfo.makerZrxBalance,
+ ]);
+ const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount);
+ const remainingMakerAssetAmount = orderUtils.getRemainingMakerAmount(order, remainingTakerAssetAmount);
+ const remainingFillableCalculator = new RemainingFillableCalculator(
+ order.makerFee,
+ order.makerAssetAmount,
+ isMakerAssetZrxToken,
+ transferrableAssetAmount,
+ transferrableFeeAssetAmount,
+ remainingMakerAssetAmount,
+ );
+ const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable();
+ // if the order does not have any remaining fillable makerAsset, do not add anything to the accumulations and continue iterating
+ if (remainingFillableAmount.lte(constants.ZERO_AMOUNT)) {
+ return accOrders;
+ }
+ const orderWithRemainingFillableMakerAssetAmount = {
+ ...order,
+ remainingFillableMakerAssetAmount: remainingFillableAmount,
+ };
+ const newAccOrders = _.concat(accOrders, orderWithRemainingFillableMakerAssetAmount);
+ return newAccOrders;
+ },
+ [] as SignedOrderWithRemainingFillableMakerAssetAmount[],
+ );
+ return result;
+}
+
+/**
+ * Given an array of orders with remaining fillable maker asset amounts. Unbundle into an instance of OrdersAndRemainingFillableMakerAssetAmounts.
+ * If an order is missing a corresponding remainingFillableMakerAssetAmount, assume it is completely fillable.
+ */
+function unbundleOrdersWithAmounts(
+ ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[],
+): OrdersAndFillableAmounts {
+ const result = _.reduce(
+ ordersWithAmounts,
+ (acc, orderWithAmount) => {
+ const { orders, remainingFillableMakerAssetAmounts } = acc;
+ const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount;
+ // if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable
+ const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount;
+ // if remaining amount is less than or equal to zero, do not add it
+ if (newRemainingAmount.lte(constants.ZERO_AMOUNT)) {
+ return acc;
+ }
+ const newAcc = {
+ orders: _.concat(orders, order),
+ remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount),
+ };
+ return newAcc;
+ },
+ {
+ orders: [] as SignedOrder[],
+ remainingFillableMakerAssetAmounts: [] as BigNumber[],
+ },
+ );
+ return result;
+}
diff --git a/packages/asset-buyer/src/utils/order_utils.ts b/packages/asset-buyer/src/utils/order_utils.ts
new file mode 100644
index 000000000..1cc2cf95f
--- /dev/null
+++ b/packages/asset-buyer/src/utils/order_utils.ts
@@ -0,0 +1,74 @@
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+
+import { constants } from '../constants';
+
+export const orderUtils = {
+ isOrderExpired(order: SignedOrder): boolean {
+ return orderUtils.willOrderExpire(order, 0);
+ },
+ willOrderExpire(order: SignedOrder, secondsFromNow: number): boolean {
+ const millisecondsInSecond = 1000;
+ const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round();
+ return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec.plus(secondsFromNow));
+ },
+ isOpenOrder(order: SignedOrder): boolean {
+ return order.takerAddress === constants.NULL_ADDRESS;
+ },
+ // given a remaining amount of takerAsset, calculate how much makerAsset is available
+ getRemainingMakerAmount(order: SignedOrder, remainingTakerAmount: BigNumber): BigNumber {
+ const remainingMakerAmount = remainingTakerAmount
+ .times(order.makerAssetAmount)
+ .div(order.takerAssetAmount)
+ .floor();
+ return remainingMakerAmount;
+ },
+ // given a desired amount of makerAsset, calculate how much takerAsset is required to fill that amount
+ getTakerFillAmount(order: SignedOrder, makerFillAmount: BigNumber): BigNumber {
+ // Round up because exchange rate favors Maker
+ const takerFillAmount = makerFillAmount
+ .mul(order.takerAssetAmount)
+ .div(order.makerAssetAmount)
+ .ceil();
+ return takerFillAmount;
+ },
+ // given a desired amount of takerAsset to fill, calculate how much fee is required by the taker to fill that amount
+ getTakerFeeAmount(order: SignedOrder, takerFillAmount: BigNumber): BigNumber {
+ // Round down because Taker fee rate favors Taker
+ const takerFeeAmount = takerFillAmount
+ .mul(order.takerFee)
+ .div(order.takerAssetAmount)
+ .floor();
+ return takerFeeAmount;
+ },
+ // given a desired amount of takerAsset to fill, calculate how much makerAsset will be filled
+ getMakerFillAmount(order: SignedOrder, takerFillAmount: BigNumber): BigNumber {
+ // Round down because exchange rate favors Maker
+ const makerFillAmount = takerFillAmount
+ .mul(order.makerAssetAmount)
+ .div(order.takerAssetAmount)
+ .floor();
+ return makerFillAmount;
+ },
+ // given a desired amount of makerAsset, calculate how much fee is required by the maker to fill that amount
+ getMakerFeeAmount(order: SignedOrder, makerFillAmount: BigNumber): BigNumber {
+ // Round down because Maker fee rate favors Maker
+ const makerFeeAmount = makerFillAmount
+ .mul(order.makerFee)
+ .div(order.makerAssetAmount)
+ .floor();
+ return makerFeeAmount;
+ },
+ // given a desired amount of ZRX from a fee order, calculate how much takerAsset is required to fill that amount
+ // also calculate how much ZRX needs to be bought in order fill the desired amount + takerFee
+ getTakerFillAmountForFeeOrder(order: SignedOrder, makerFillAmount: BigNumber): [BigNumber, BigNumber] {
+ // For each unit of TakerAsset we buy (MakerAsset - TakerFee)
+ const adjustedTakerFillAmount = makerFillAmount
+ .mul(order.takerAssetAmount)
+ .div(order.makerAssetAmount.sub(order.takerFee))
+ .ceil();
+ // The amount that we buy will be greater than makerFillAmount, since we buy some amount for fees.
+ const adjustedMakerFillAmount = orderUtils.getMakerFillAmount(order, adjustedTakerFillAmount);
+ return [adjustedTakerFillAmount, adjustedMakerFillAmount];
+ },
+};