From 7b46cef83dca0a743bd598a70076004983cbf294 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 14 Sep 2018 16:18:16 +0200 Subject: Create initial AssetBuyer class --- .../asset-buyer/src/asset_buyers/asset_buyer.ts | 129 ++++++ packages/asset-buyer/src/constants.ts | 1 + .../asset-buyer/src/forwarder_helper_factory.ts | 510 ++++++++++----------- packages/asset-buyer/src/forwarder_helper_impl.ts | 64 --- packages/asset-buyer/src/index.ts | 4 +- packages/asset-buyer/src/types.ts | 63 ++- .../utils/forwarder_helper_impl_config_utils.ts | 170 +++---- 7 files changed, 500 insertions(+), 441 deletions(-) create mode 100644 packages/asset-buyer/src/asset_buyers/asset_buyer.ts delete mode 100644 packages/asset-buyer/src/forwarder_helper_impl.ts (limited to 'packages/asset-buyer/src') diff --git a/packages/asset-buyer/src/asset_buyers/asset_buyer.ts b/packages/asset-buyer/src/asset_buyers/asset_buyer.ts new file mode 100644 index 000000000..eb7f85e2b --- /dev/null +++ b/packages/asset-buyer/src/asset_buyers/asset_buyer.ts @@ -0,0 +1,129 @@ +import { ContractWrappers } from '@0xproject/contract-wrappers'; +import { marketUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; +import { Provider } from 'ethereum-types'; + +import { constants } from '../constants'; +import { AssetBuyerError, BuyQuote, BuyQuoteRequest } from '../types'; + +const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface + +export interface AssetBuyerConfig { + orders: SignedOrder[]; + feeOrders: SignedOrder[]; + remainingFillableMakerAssetAmounts?: BigNumber[]; + remainingFillableFeeAmounts?: BigNumber[]; + networkId?: number; +} + +export class AssetBuyer { + public readonly provider: Provider; + public readonly config: AssetBuyerConfig; + private _contractWrappers: ContractWrappers; + constructor(provider: Provider, config: AssetBuyerConfig) { + this.provider = provider; + this.config = config; + const networkId = this.config.networkId || constants.MAINNET_NETWORK_ID; + this._contractWrappers = new ContractWrappers(this.provider, { + networkId, + }); + } + /** + * Given a BuyQuoteRequest, returns a BuyQuote containing all information relevant to fulfilling the buy. Pass the BuyQuote + * to executeBuyQuoteAsync to execute the buy. + * @param buyQuoteRequest An object that conforms to BuyQuoteRequest. See type definition for more information. + * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. + */ + public getBuyQuote(buyQuoteRequest: BuyQuoteRequest): BuyQuote { + const { assetBuyAmount, feePercentage } = buyQuoteRequest; + const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config; + // TODO: optimization + // make the slippage percentage customizable + const slippageBufferAmount = assetBuyAmount.mul(SLIPPAGE_PERCENTAGE).round(); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + orders, + assetBuyAmount, + { + remainingFillableMakerAssetAmounts, + slippageBufferAmount, + }, + ); + if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientAssetLiquidity); + } + // TODO: 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 + const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( + resultOrders, + feeOrders, + { + remainingFillableMakerAssetAmounts, + remainingFillableFeeAmounts, + }, + ); + if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { + throw new Error(AssetBuyerError.InsufficientZrxLiquidity); + } + const assetData = orders[0].makerAssetData; + // TODO: critical + // calculate minRate and maxRate by calculating min and max eth usage and then dividing into + // assetBuyAmount to get assetData / WETH + return { + assetData, + orders: resultOrders, + feeOrders: resultFeeOrders, + minRate: constants.ZERO_AMOUNT, + maxRate: constants.ZERO_AMOUNT, + assetBuyAmount, + feePercentage, + }; + } + /** + * 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 rate The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate. + * @param takerAddress The address to perform the buy. Defaults to the first available address from the provider. + * @param feeRecipient The address where affiliate fees are sent. Defaults to null address (0x000...000). + * @return A promise of the txHash. + */ + public async executeBuyQuoteAsync( + buyQuote: BuyQuote, + rate?: BigNumber, + takerAddress?: string, + feeRecipient: string = constants.NULL_ADDRESS, + ): Promise { + const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = 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); + } + } + // if no rate is provided, default to the maxRate from buyQuote + const desiredRate = rate || maxRate; + // calculate how much eth is required to buy assetBuyAmount at the desired rate + const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate); + const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + orders, + assetBuyAmount, + finalTakerAddress, + ethAmount, + feeOrders, + feePercentage, + feeRecipient, + ); + return txHash; + } +} diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index c0a1b090e..5785e705b 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -3,4 +3,5 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { ZERO_AMOUNT: new BigNumber(0), NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + MAINNET_NETWORK_ID: 1, }; diff --git a/packages/asset-buyer/src/forwarder_helper_factory.ts b/packages/asset-buyer/src/forwarder_helper_factory.ts index 2b37ac98f..9a3832e81 100644 --- a/packages/asset-buyer/src/forwarder_helper_factory.ts +++ b/packages/asset-buyer/src/forwarder_helper_factory.ts @@ -1,261 +1,261 @@ -import { assert } from '@0xproject/assert'; -import { APIOrder, HttpClient, OrderbookResponse } 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 { assert } from '@0xproject/assert'; +// import { APIOrder, HttpClient, OrderbookResponse } 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, ForwarderHelperFactoryError } from './types'; -import { orderUtils } from './utils/order_utils'; +// import { constants } from './constants'; +// import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; +// import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; +// import { orderUtils } from './utils/order_utils'; -export const forwarderHelperFactory = { - /** - * 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 - */ - getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { - assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); - assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); - // TODO: Add assertion here for orders all having the same makerAsset and takerAsset - const config: ForwarderHelperImplConfig = { - orders, - feeOrders, - }; - 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, - sraUrl: string, - rpcUrl?: string, - networkId: number = 1, - ): Promise { - assert.isHexString('makerAssetData', makerAssetData); - 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 }; - let makerAssetOrderbook: OrderbookResponse; - let zrxOrderbook: OrderbookResponse; - try { - [makerAssetOrderbook, zrxOrderbook] = await Promise.all( - _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), - ); - } catch (err) { - throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); - } - // 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 = getOpenAsksFromOrderbook(makerAssetOrderbook); - const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); - // TODO: try catch these requests and throw a more domain specific error - // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response - const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( - _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { - const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); - 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; - }, -}; +// export const forwarderHelperFactory = { +// /** +// * 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 +// */ +// getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { +// assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); +// assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); +// // TODO: Add assertion here for orders all having the same makerAsset and takerAsset +// const config: ForwarderHelperImplConfig = { +// orders, +// feeOrders, +// }; +// 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, +// sraUrl: string, +// rpcUrl?: string, +// networkId: number = 1, +// ): Promise { +// assert.isHexString('makerAssetData', makerAssetData); +// 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 }; +// let makerAssetOrderbook: OrderbookResponse; +// let zrxOrderbook: OrderbookResponse; +// try { +// [makerAssetOrderbook, zrxOrderbook] = await Promise.all( +// _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), +// ); +// } catch (err) { +// throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); +// } +// // 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 = getOpenAsksFromOrderbook(makerAssetOrderbook); +// const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); +// // TODO: try catch these requests and throw a more domain specific error +// // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response +// const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( +// _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { +// const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); +// 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[]; -} +// 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) => { - // get current accumulations - const { orders, remainingFillableMakerAssetAmounts } = acc; - // get order and metadata - const { order, metaData } = apiOrder; - // if the order is expired or not open, move on - if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { - return acc; - } - // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable - 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 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) => { +// // get current accumulations +// const { orders, remainingFillableMakerAssetAmounts } = acc; +// // get order and metadata +// const { order, metaData } = apiOrder; +// // if the order is expired or not open, move on +// if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { +// return acc; +// } +// // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable +// 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 to the accumulations 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; -} +// /** +// * 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 to the accumulations 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; +// } -function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { - const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); - const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); - return result; -} +// function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { +// const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); +// const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); +// return result; +// } diff --git a/packages/asset-buyer/src/forwarder_helper_impl.ts b/packages/asset-buyer/src/forwarder_helper_impl.ts deleted file mode 100644 index a90edb0bb..000000000 --- a/packages/asset-buyer/src/forwarder_helper_impl.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { marketUtils } from '@0xproject/order-utils'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; - -import { constants } from './constants'; -import { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfo, MarketBuyOrdersInfoRequest } from './types'; -import { forwarderHelperImplConfigUtils } from './utils/forwarder_helper_impl_config_utils'; - -const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface - -export interface ForwarderHelperImplConfig { - orders: SignedOrder[]; - feeOrders: SignedOrder[]; - remainingFillableMakerAssetAmounts?: BigNumber[]; - remainingFillableFeeAmounts?: BigNumber[]; -} - -export class ForwarderHelperImpl implements ForwarderHelper { - public readonly config: ForwarderHelperImplConfig; - constructor(config: ForwarderHelperImplConfig) { - this.config = forwarderHelperImplConfigUtils.sortedConfig(config); - } - public getMarketBuyOrdersInfo(request: MarketBuyOrdersInfoRequest): MarketBuyOrdersInfo { - const { makerAssetFillAmount, feePercentage } = request; - const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config; - // TODO: make the slippage percentage customizable - const slippageBufferAmount = makerAssetFillAmount.mul(SLIPPAGE_PERCENTAGE).round(); - const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( - orders, - makerAssetFillAmount, - { - remainingFillableMakerAssetAmounts, - slippageBufferAmount, - }, - ); - if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(ForwarderHelperError.InsufficientMakerAssetLiquidity); - } - // TODO: 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 - const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( - resultOrders, - feeOrders, - { - remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - }, - ); - if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(ForwarderHelperError.InsufficientZrxLiquidity); - } - // TODO: calculate min and max eth usage - // TODO: optimize orders call data - return { - makerAssetFillAmount, - orders: resultOrders, - feeOrders: resultFeeOrders, - minEthAmount: constants.ZERO_AMOUNT, - maxEthAmount: constants.ZERO_AMOUNT, - feePercentage, - }; - } -} diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index eb3a34bd5..299b32edd 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -1,2 +1,2 @@ -export { forwarderHelperFactory } from './forwarder_helper_factory'; -export { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfoRequest, MarketBuyOrdersInfo } from './types'; +export { AssetBuyerError, BuyQuote, BuyQuoteRequest } from './types'; +export { AssetBuyer } from './asset_buyers/asset_buyer'; diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index a7f02ff8d..8a12d0cf8 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -1,49 +1,42 @@ 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', - StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', -} - -export interface ForwarderHelper { - /** - * Given a MarketBuyOrdersInfoRequest, returns a MarketBuyOrdersInfo containing all information relevant to fulfilling the request - * using the ForwarderContract marketBuyOrdersWithEth function. - * @param request An object that conforms to MarketBuyOrdersInfoRequest. See type definition for more information. - * @return An object that conforms to MarketBuyOrdersInfo that satisfies the request. See type definition for more information. - */ - getMarketBuyOrdersInfo: (request: MarketBuyOrdersInfoRequest) => MarketBuyOrdersInfo; -} - -export enum ForwarderHelperError { - InsufficientMakerAssetLiquidity = 'INSUFFICIENT_MAKER_ASSET_LIQUIDITY', - InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', -} - /** - * makerAssetFillAmount: The amount of makerAsset requesting to be filled - * feePercentage: Optional affiliate percentage amount factoring into eth amount calculations + * assetBuyAmount: The amount of asset to buy. + * feePercentage: Optional affiliate percentage amount factoring into eth amount calculations. */ -export interface MarketBuyOrdersInfoRequest { - makerAssetFillAmount: BigNumber; +export interface BuyQuoteRequest { + assetBuyAmount: BigNumber; feePercentage?: BigNumber; } /** - * makerAssetFillAmount: The amount of makerAsset requesting to be filled - * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested makerAssetFillAmount plus slippage - * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above - * minEthAmount: Amount of eth in wei to send with the tx for the most optimistic case - * maxEthAmount: Amount of eth in wei to send with the tx for the worst case - * feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed thru directly from the request + * assetData: The asset information. + * 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. + * minRate: Min rate that needs to be paid in order to execute the buy. + * maxRate: Max rate that can be paid in order to execute the buy. + * assetBuyAmount: The amount of asset to buy. Passed through directly from the request. + * feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed through directly from the request. */ -export interface MarketBuyOrdersInfo { - makerAssetFillAmount: BigNumber; +export interface BuyQuote { + assetData: string; orders: SignedOrder[]; feeOrders: SignedOrder[]; - minEthAmount: BigNumber; - maxEthAmount: BigNumber; + minRate: BigNumber; + maxRate: BigNumber; + assetBuyAmount: BigNumber; feePercentage?: BigNumber; } + +/** + * 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', +} diff --git a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts index 253384f65..d3cbb651a 100644 --- a/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts +++ b/packages/asset-buyer/src/utils/forwarder_helper_impl_config_utils.ts @@ -1,92 +1,92 @@ -import { sortingUtils } from '@0xproject/order-utils'; -import { SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; +// import { sortingUtils } from '@0xproject/order-utils'; +// import { SignedOrder } from '@0xproject/types'; +// import { BigNumber } from '@0xproject/utils'; +// import * as _ from 'lodash'; -import { ForwarderHelperImplConfig } from '../forwarder_helper_impl'; +// import { ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer'; -interface SignedOrderWithAmount extends SignedOrder { - remainingFillAmount: BigNumber; -} +// interface SignedOrderWithAmount extends SignedOrder { +// remainingFillAmount: BigNumber; +// } -export const forwarderHelperImplConfigUtils = { - sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { - const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; - // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens - const orderSorter = (ordersToSort: SignedOrder[]) => { - return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); - }; - const sortOrdersResult = sortOrdersAndRemainingFillAmounts( - orderSorter, - orders, - remainingFillableMakerAssetAmounts, - ); - const feeOrderSorter = (ordersToSort: SignedOrder[]) => { - return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); - }; - const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( - feeOrderSorter, - feeOrders, - remainingFillableFeeAmounts, - ); - return { - orders: sortOrdersResult.orders, - feeOrders: sortFeeOrdersResult.orders, - remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, - remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, - }; - }, -}; +// export const forwarderHelperImplConfigUtils = { +// sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { +// const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; +// // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens +// const orderSorter = (ordersToSort: SignedOrder[]) => { +// return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); +// }; +// const sortOrdersResult = sortOrdersAndRemainingFillAmounts( +// orderSorter, +// orders, +// remainingFillableMakerAssetAmounts, +// ); +// const feeOrderSorter = (ordersToSort: SignedOrder[]) => { +// return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); +// }; +// const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( +// feeOrderSorter, +// feeOrders, +// remainingFillableFeeAmounts, +// ); +// return { +// orders: sortOrdersResult.orders, +// feeOrders: sortFeeOrdersResult.orders, +// remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, +// remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, +// }; +// }, +// }; -type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; +// type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; -function sortOrdersAndRemainingFillAmounts( - orderSorter: OrderSorter, - orders: SignedOrder[], - remainingFillAmounts?: BigNumber[], -): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { - if (!_.isUndefined(remainingFillAmounts)) { - // Bundle orders together with their remainingFillAmounts so that we can sort them together - const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); - // Sort - const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; - // Unbundle after sorting - const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); - return { - orders: unbundledSortedOrderWithAmounts.orders, - remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, - }; - } else { - const sortedOrders = orderSorter(orders); - return { - orders: sortedOrders, - }; - } -} +// function sortOrdersAndRemainingFillAmounts( +// orderSorter: OrderSorter, +// orders: SignedOrder[], +// remainingFillAmounts?: BigNumber[], +// ): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { +// if (!_.isUndefined(remainingFillAmounts)) { +// // Bundle orders together with their remainingFillAmounts so that we can sort them together +// const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); +// // Sort +// const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; +// // Unbundle after sorting +// const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); +// return { +// orders: unbundledSortedOrderWithAmounts.orders, +// remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, +// }; +// } else { +// const sortedOrders = orderSorter(orders); +// return { +// orders: sortedOrders, +// }; +// } +// } -function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { - const ordersAndAmounts = _.map(orders, (order, index) => { - return { - ...order, - remainingFillAmount: amounts[index], - }; - }); - return ordersAndAmounts; -} +// function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { +// const ordersAndAmounts = _.map(orders, (order, index) => { +// return { +// ...order, +// remainingFillAmount: amounts[index], +// }; +// }); +// return ordersAndAmounts; +// } -function unbundleSignedOrderWithAmounts( - signedOrderWithAmounts: SignedOrderWithAmount[], -): { orders: SignedOrder[]; amounts: BigNumber[] } { - const orders = _.map(signedOrderWithAmounts, order => { - const { remainingFillAmount, ...rest } = order; - return rest; - }); - const amounts = _.map(signedOrderWithAmounts, order => { - const { remainingFillAmount } = order; - return remainingFillAmount; - }); - return { - orders, - amounts, - }; -} +// function unbundleSignedOrderWithAmounts( +// signedOrderWithAmounts: SignedOrderWithAmount[], +// ): { orders: SignedOrder[]; amounts: BigNumber[] } { +// const orders = _.map(signedOrderWithAmounts, order => { +// const { remainingFillAmount, ...rest } = order; +// return rest; +// }); +// const amounts = _.map(signedOrderWithAmounts, order => { +// const { remainingFillAmount } = order; +// return remainingFillAmount; +// }); +// return { +// orders, +// amounts, +// }; +// } -- cgit v1.2.3