From 8687b9533c67a551df928b28a776bfdbe6e3e84f Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 24 Aug 2018 11:11:29 -0700 Subject: Add getForwarderHelperForMakerAssetDataAsync method to forwarderHelperFactory --- packages/connect/src/http_client.ts | 2 +- packages/forwarder-helper/package.json | 5 +- .../src/forwarder_helper_factory.ts | 229 ++++++++++++++++++++- packages/forwarder-helper/src/types.ts | 5 + packages/forwarder-helper/src/utils/order_utils.ts | 16 ++ 5 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 packages/forwarder-helper/src/utils/order_utils.ts diff --git a/packages/connect/src/http_client.ts b/packages/connect/src/http_client.ts index 87d5c30be..bdcfdd8d2 100644 --- a/packages/connect/src/http_client.ts +++ b/packages/connect/src/http_client.ts @@ -29,7 +29,7 @@ const TRAILING_SLASHES_REGEX = /\/+$/; /** * This class includes all the functionality related to interacting with a set of HTTP endpoints - * that implement the standard relayer API v0 + * that implement the standard relayer API v2 */ export class HttpClient implements Client { private readonly _apiEndpointUrl: string; diff --git a/packages/forwarder-helper/package.json b/packages/forwarder-helper/package.json index 410323f76..f849fb4ed 100644 --- a/packages/forwarder-helper/package.json +++ b/packages/forwarder-helper/package.json @@ -40,18 +40,21 @@ "homepage": "https://github.com/0xProject/0x-monorepo/packages/forwarder-helper/README.md", "dependencies": { "@0xproject/assert": "^1.0.8", + "@0xproject/connect": "^2.0.0", + "@0xproject/contract-wrappers": "^1.0.1", "@0xproject/json-schemas": "^1.0.1", "@0xproject/order-utils": "^1.0.1", + "@0xproject/subproviders": "^2.0.2", "@0xproject/types": "^1.0.1", "@0xproject/typescript-typings": "^2.0.0", "@0xproject/utils": "^1.0.8", - "@types/node": "^8.0.53", "lodash": "^4.17.10" }, "devDependencies": { "@0xproject/tslint-config": "^1.0.7", "@types/lodash": "^4.14.116", "@types/mocha": "^2.2.42", + "@types/node": "^8.0.53", "chai": "^4.0.1", "chai-as-promised": "^7.1.0", "chai-bignumber": "^2.0.1", diff --git a/packages/forwarder-helper/src/forwarder_helper_factory.ts b/packages/forwarder-helper/src/forwarder_helper_factory.ts index 95f11f555..e3ef59388 100644 --- a/packages/forwarder-helper/src/forwarder_helper_factory.ts +++ b/packages/forwarder-helper/src/forwarder_helper_factory.ts @@ -1,13 +1,22 @@ import { assert } from '@0xproject/assert'; +import { APIOrder, HttpClient } from '@0xproject/connect'; +import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers'; import { schemas } from '@0xproject/json-schemas'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; +import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders'; import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import { constants } from './constants'; import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl'; -import { ForwarderHelper } from './types'; +import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; +import { orderUtils } from './utils/order_utils'; export const forwarderHelperFactory = { /** - * Given an array of orders and an array of feeOrders + * Given an array of orders and an array of feeOrders, get a ForwarderHelper * @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData * @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData * @return A ForwarderHelper, see type for definition @@ -22,4 +31,220 @@ export const forwarderHelperFactory = { const helper = new ForwarderHelperImpl(config); return helper; }, + /** + * Given a desired makerAsset and SRA url, get a ForwarderHelper + * @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData + * @param sraUrl A url pointing to an SRA v2 compliant endpoint. + * @param rpcUrl A url pointing to an ethereum node. + * @param networkId The ethereum networkId, defaults to 1 (mainnet). + * @return A ForwarderHelper, see type for definition + */ + async getForwarderHelperForMakerAssetDataAsync( + makerAssetData: string, + takerAddress: string, + sraUrl: string, + rpcUrl?: string, + networkId: number = 1, + ): Promise { + assert.isHexString('makerAssetData', makerAssetData); + assert.isETHAddressHex('takerAddress', takerAddress); + assert.isWebUri('sraUrl', sraUrl); + if (!_.isUndefined(rpcUrl)) { + assert.isWebUri('rpcUrl', rpcUrl); + } + assert.isNumber('networkId', networkId); + // create provider + const providerEngine = new Web3ProviderEngine(); + if (!_.isUndefined(rpcUrl)) { + providerEngine.addProvider(new RPCSubprovider(rpcUrl)); + } + providerEngine.start(); + // create contract wrappers given provider and networkId + const contractWrappers = new ContractWrappers(providerEngine, { networkId }); + // find ether token asset data + const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); + if (_.isUndefined(etherTokenAddressIfExists)) { + throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound); + } + const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); + // find zrx token asset data + let zrxTokenAssetData: string; + try { + zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); + } catch (err) { + throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound); + } + // get orderbooks for makerAsset/WETH and ZRX/WETH + const sraClient = new HttpClient(sraUrl); + const orderbookRequests = [ + { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData }, + { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData }, + ]; + const requestOpts = { networkId }; + // TODO: try catch these requests and throw a more domain specific error + const [makerAssetOrderbook, zrxOrderbook] = await Promise.all( + _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), + ); + // validate orders and find remaining fillable from on chain state or sra api + let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; + let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; + if (!_.isUndefined(rpcUrl)) { + // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper + const ordersFromSra = _.map(makerAssetOrderbook.asks.records, apiOrder => apiOrder.order); + const feeOrdersFromSra = _.map(zrxOrderbook.asks.records, apiOrder => apiOrder.order); + // TODO: try catch these requests and throw a more domain specific error + const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( + _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { + const takerAddresses = _.map(ordersToBeValidated, () => takerAddress); + return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( + ordersToBeValidated, + takerAddresses, + ); + }), + ); + // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts + ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( + ordersFromSra, + makerAssetOrdersAndTradersInfo, + zrxTokenAssetData, + ); + // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts + feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( + feeOrdersFromSra, + feeOrdersAndTradersInfo, + zrxTokenAssetData, + ); + } else { + // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA + // if fill amounts are not available from the SRA, assume all orders are completely fillable + const apiOrdersFromSra = makerAssetOrderbook.asks.records; + const feeApiOrdersFromSra = zrxOrderbook.asks.records; + // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts + ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( + apiOrdersFromSra, + ); + // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts + feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( + feeApiOrdersFromSra, + ); + } + // compile final config + const config: ForwarderHelperImplConfig = { + orders: ordersAndRemainingFillableMakerAssetAmounts.orders, + feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, + remainingFillableMakerAssetAmounts: + ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts: + feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, + }; + const helper = new ForwarderHelperImpl(config); + return helper; + }, }; + +interface OrdersAndRemainingFillableMakerAssetAmounts { + orders: SignedOrder[]; + remainingFillableMakerAssetAmounts: BigNumber[]; +} + +/** + * Given an array of APIOrder objects from a standard relayer api, return an array + * of fillable orders with their corresponding remainingFillableMakerAssetAmounts + */ +function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( + apiOrders: APIOrder[], +): OrdersAndRemainingFillableMakerAssetAmounts { + const result = _.reduce( + apiOrders, + (acc, apiOrder, index) => { + // get current accumulations + const { orders, remainingFillableMakerAssetAmounts } = acc; + // get order and metadata + const { order, metaData } = apiOrder; + // if the order is expired, move on + if (orderUtils.isOrderExpired(order)) { + return acc; + } + // calculate remainingFillableMakerAssetAmount from api metadata + const remainingFillableTakerAssetAmount = _.get( + metaData, + 'remainingTakerAssetAmount', + order.takerAssetAmount, + ); + const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( + order, + remainingFillableTakerAssetAmount, + ); + // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations + // if there is not any maker asset left to fill, do not add + if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) { + return { + orders: _.concat(orders, order), + remainingFillableMakerAssetAmounts: _.concat( + remainingFillableMakerAssetAmounts, + remainingFillableMakerAssetAmount, + ), + }; + } else { + return acc; + } + }, + { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, + ); + 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 getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( + inputOrders: SignedOrder[], + ordersAndTradersInfo: OrderAndTraderInfo[], + zrxAssetData: string, +): OrdersAndRemainingFillableMakerAssetAmounts { + // 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, + (acc, order, index) => { + // get current accumulations + const { orders, remainingFillableMakerAssetAmounts } = acc; + // get corresponding on-chain state for the order + const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; + // if the order IS NOT fillable, do not add anything and continue iterating + if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { + return acc; + } + // 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.calculateRemainingMakerAssetAmount( + order, + remainingTakerAssetAmount, + ); + const remainingFillableCalculator = new RemainingFillableCalculator( + order.makerFee, + order.makerAssetAmount, + order.makerAssetData === zrxAssetData, + transferrableAssetAmount, + transferrableFeeAssetAmount, + remainingMakerAssetAmount, + ); + const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); + return { + orders: _.concat(orders, order), + remainingFillableMakerAssetAmounts: _.concat( + remainingFillableMakerAssetAmounts, + remainingFillableAmount, + ), + }; + }, + { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, + ); + return result; +} diff --git a/packages/forwarder-helper/src/types.ts b/packages/forwarder-helper/src/types.ts index fb171cc90..5a8439257 100644 --- a/packages/forwarder-helper/src/types.ts +++ b/packages/forwarder-helper/src/types.ts @@ -1,6 +1,11 @@ import { SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; +export enum ForwarderHelperFactoryError { + NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', + NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND', +} + export interface ForwarderHelper { /** * Given a MarketBuyOrdersInfoRequest, returns a MarketBuyOrdersInfo containing all information relevant to fulfilling the request diff --git a/packages/forwarder-helper/src/utils/order_utils.ts b/packages/forwarder-helper/src/utils/order_utils.ts new file mode 100644 index 000000000..d14c6a6d7 --- /dev/null +++ b/packages/forwarder-helper/src/utils/order_utils.ts @@ -0,0 +1,16 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +export const orderUtils = { + isOrderExpired(order: SignedOrder): boolean { + const millisecondsInSecond = 1000; + const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round(); + return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec); + }, + calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { + const result = remainingTakerAssetAmount.eq(0) + ? new BigNumber(0) + : remainingTakerAssetAmount.times(order.makerAssetAmount).dividedToIntegerBy(order.takerAssetAmount); + return result; + }, +}; -- cgit