diff options
-rw-r--r-- | packages/asset-buyer/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/asset-buyer/README.md | 18 | ||||
-rw-r--r-- | packages/asset-buyer/package.json | 2 | ||||
-rw-r--r-- | packages/asset-buyer/src/asset_buyer.ts | 259 | ||||
-rw-r--r-- | packages/asset-buyer/src/constants.ts | 24 | ||||
-rw-r--r-- | packages/asset-buyer/src/index.ts | 5 | ||||
-rw-r--r-- | packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts | 133 | ||||
-rw-r--r-- | packages/asset-buyer/src/types.ts | 39 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/buy_quote_calculator.ts | 15 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/order_provider_response_processor.ts | 76 |
10 files changed, 221 insertions, 359 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 63eafb528..8c2c7a8d2 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "2.0.0", + "changes": [ + { + "note": "Expand AssetBuyer to work with multiple assets at once", + "pr": 1086 + } + ] + }, + { "timestamp": 1538475601, "version": "1.0.3", "changes": [ diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index 5f7f26f30..555b90957 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -1,6 +1,8 @@ ## @0xproject/asset-buyer -Convenience package for buying assets represented on the Ethereum blockchain using 0x. In its simplest form, the package helps in the usage of the [0x forwarder contract](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md), which allows users to execute [Wrapped Ether](https://weth.io/) based 0x orders without having to set allowances, wrap Ether or buy ZRX, meaning they can buy tokens with Ether alone. Given some liquidity (0x signed orders), it helps estimate the Ether cost of buying a certain asset (giving a range) and then buying that asset. +**Warning: In Beta, has not been extensively tested.** + +Convenience package for buying assets represented on the Ethereum blockchain using 0x. In its simplest form, the package helps in the usage of the [0x forwarder contract](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md), which allows users to execute [Wrapped Ether](https://weth.io/) based 0x orders without having to set allowances, wrap Ether or own ZRX, meaning they can buy tokens with Ether alone. Given some liquidity (0x signed orders), it helps estimate the Ether cost of buying a certain asset (giving a range) and then buying that asset. In its more advanced and useful form, it integrates with the [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) and takes care of sourcing liquidity for you given an SRA compliant endpoint. The final result is a library that tells you what assets are available, provides an Ether based quote for any asset desired, and allows you to buy that asset using Ether alone. @@ -12,16 +14,30 @@ yarn add @0xproject/asset-buyer **Import** +For single-asset price estimation and purchase: + ```typescript import { AssetBuyer } from '@0xproject/asset-buyer'; ``` +For multiple assets: + +```typescript +import { AssetBuyerManager } from '@0xproject/asset-buyer'; +``` + or ```javascript var AssetBuyer = require('@0xproject/asset-buyer').AssetBuyer; ``` +and + +```javascript +var AssetBuyerManager = require('@0xproject/asset-buyer').AssetBuyerManager; +``` + If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: ```json diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index 4f63f0a40..dacfbf730 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -4,7 +4,7 @@ "engines": { "node": ">=6.12" }, - "description": "Convenience package for buying assets", + "description": "Convenience package for discovering and buying assets with Ether.", "main": "lib/src/index.js", "types": "lib/src/index.d.ts", "scripts": { diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 409e34e74..53bbe427b 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,6 +1,8 @@ +import { HttpClient } from '@0xproject/connect'; import { ContractWrappers } from '@0xproject/contract-wrappers'; import { schemas } from '@0xproject/json-schemas'; import { SignedOrder } from '@0xproject/order-utils'; +import { ObjectMap } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; @@ -11,11 +13,13 @@ import { BasicOrderProvider } from './order_providers/basic_order_provider'; import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; import { AssetBuyerError, - AssetBuyerOrdersAndFillableAmounts, + AssetBuyerOpts, BuyQuote, + BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrderProvider, OrderProviderResponse, + OrdersAndFillableAmounts, } from './types'; import { assert } from './utils/assert'; @@ -23,24 +27,26 @@ 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 assetData: string; public readonly orderProvider: OrderProvider; public readonly networkId: number; public readonly orderRefreshIntervalMs: number; public readonly expiryBufferSeconds: number; private readonly _contractWrappers: ContractWrappers; - private _lastRefreshTimeIfExists?: number; - private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts; + // 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 networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). - * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * @param options Initialization options for the AssetBuyer. See type definition for details. * * @return An instance of AssetBuyer */ @@ -48,171 +54,99 @@ export class AssetBuyer { provider: Provider, orders: SignedOrder[], feeOrders: SignedOrder[] = [], - networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, - expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, + options: Partial<AssetBuyerOpts> = {}, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); assert.doesConformToSchema('feeOrders', feeOrders, schemas.signedOrdersSchema); - assert.isNumber('networkId', networkId); - assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); assert.areValidProvidedOrders('orders', orders); assert.areValidProvidedOrders('feeOrders', feeOrders); assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); - const assetData = orders[0].makerAssetData; const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); - const assetBuyer = new AssetBuyer( - provider, - assetData, - orderProvider, - networkId, - orderRefreshIntervalMs, - expiryBufferSeconds, - ); + const assetBuyer = new AssetBuyer(provider, orderProvider, options); return assetBuyer; } /** - * Instantiates a new AssetBuyer instance given the desired assetData and a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint + * 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 assetData The assetData that identifies the desired asset to buy. * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. - * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). - * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * @param options Initialization options for the AssetBuyer. See type definition for details. * * @return An instance of AssetBuyer */ - public static getAssetBuyerForAssetData( + public static getAssetBuyerForStandardRelayerAPIUrl( provider: Provider, - assetData: string, sraApiUrl: string, - networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, - expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, + options: Partial<AssetBuyerOpts> = {}, ): AssetBuyer { assert.isWeb3Provider('provider', provider); - assert.isHexString('assetData', assetData); assert.isWebUri('sraApiUrl', sraApiUrl); - assert.isNumber('networkId', networkId); - assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); - const assetBuyer = new AssetBuyer( - provider, - assetData, - orderProvider, - networkId, - orderRefreshIntervalMs, - expiryBufferSeconds, - ); - return assetBuyer; - } - /** - * Instantiates a new AssetBuyer instance given the desired ERC20 token address and 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 tokenAddress The ERC20 token address that identifies the desired asset to buy. - * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. - * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). - * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. - * @return An instance of AssetBuyer - */ - public static getAssetBuyerForERC20TokenAddress( - provider: Provider, - tokenAddress: string, - sraApiUrl: string, - networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, - expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, - ): AssetBuyer { - assert.isWeb3Provider('provider', provider); - assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isWebUri('sraApiUrl', sraApiUrl); - assert.isNumber('networkId', networkId); - assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); - const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); - const assetBuyer = AssetBuyer.getAssetBuyerForAssetData( - provider, - assetData, - sraApiUrl, - networkId, - orderRefreshIntervalMs, - expiryBufferSeconds, - ); + 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 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 orderProvider An object that conforms to OrderProvider, see type for definition. - * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). - * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * @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, - assetData: string, - orderProvider: OrderProvider, - networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS, - expiryBufferSeconds: number = constants.DEFAULT_EXPIRY_BUFFER_SECONDS, - ) { + constructor(provider: Provider, orderProvider: OrderProvider, options: Partial<AssetBuyerOpts> = {}) { + const { networkId, orderRefreshIntervalMs, expiryBufferSeconds } = { + ...constants.DEFAULT_ASSET_BUYER_OPTS, + ...options, + }; assert.isWeb3Provider('provider', provider); - assert.isString('assetData', assetData); assert.isValidOrderProvider('orderProvider', orderProvider); assert.isNumber('networkId', networkId); assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs); + assert.isNumber('expiryBufferSeconds', expiryBufferSeconds); this.provider = provider; - this.assetData = assetData; this.orderProvider = orderProvider; this.networkId = networkId; - this.expiryBufferSeconds = expiryBufferSeconds; this.orderRefreshIntervalMs = orderRefreshIntervalMs; + this.expiryBufferSeconds = expiryBufferSeconds; this._contractWrappers = new ContractWrappers(this.provider, { networkId, }); } /** - * Get a `BuyQuote` containing all information relevant to fulfilling a buy. + * 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 feePercentage The affiliate fee percentage. Defaults to 0. - * @param forceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for - * the next orderRefreshIntervalMs. Defaults to false. + * @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(assetBuyAmount: BigNumber, options: Partial<BuyQuoteRequestOpts>): Promise<BuyQuote> { + public async getBuyQuoteAsync( + assetData: string, + assetBuyAmount: BigNumber, + options: Partial<BuyQuoteRequestOpts>, + ): Promise<BuyQuote> { const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = { - ...options, ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS, + ...options, }; + assert.isString('assetData', assetData); assert.isBigNumber('assetBuyAmount', assetBuyAmount); assert.isValidPercentage('feePercentage', feePercentage); assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh); - // 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(this._currentOrdersAndFillableAmountsIfExists) || - shouldForceOrderRefresh || - (!_.isUndefined(this._lastRefreshTimeIfExists) && - this._lastRefreshTimeIfExists + this.orderRefreshIntervalMs < Date.now()); - let ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts; - if (shouldRefresh) { - ordersAndFillableAmounts = await this._getLatestOrdersAndFillableAmountsAsync(); - this._lastRefreshTimeIfExists = Date.now(); - this._currentOrdersAndFillableAmountsIfExists = ordersAndFillableAmounts; - } else { - // it is safe to cast to AssetBuyerOrdersAndFillableAmounts because shouldRefresh catches the undefined case above - ordersAndFillableAmounts = this - ._currentOrdersAndFillableAmountsIfExists as AssetBuyerOrdersAndFillableAmounts; + assert.isNumber('slippagePercentage', slippagePercentage); + const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); + const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([ + this._getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh), + 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, @@ -220,19 +154,37 @@ export class AssetBuyer { 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 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). + * @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, - rate?: BigNumber, - takerAddress?: string, - feeRecipient: string = constants.NULL_ADDRESS, - ): Promise<string> { + public async executeBuyQuoteAsync(buyQuote: BuyQuote, options: Partial<BuyQuoteExecutionOpts>): Promise<string> { + const { rate, takerAddress, feeRecipient } = { + ...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS, + ...options, + }; assert.isValidBuyQuote('buyQuote', buyQuote); if (!_.isUndefined(rate)) { assert.isBigNumber('rate', rate); @@ -272,39 +224,54 @@ export class AssetBuyer { return txHash; } /** - * Ask the order Provider for orders and process them. + * Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders */ - private async _getLatestOrdersAndFillableAmountsAsync(): Promise<AssetBuyerOrdersAndFillableAmounts> { + 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 || + ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now(); + if (!shouldRefresh) { + const result = ordersEntryIfExists.ordersAndFillableAmounts; + return result; + } const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); - // construct order Provider requests - const targetOrderProviderRequest = { - makerAssetData: this.assetData, - takerAssetData: etherTokenAssetData, - networkId: this.networkId, - }; - const feeOrderProviderRequest = { - makerAssetData: zrxTokenAssetData, + // construct orderProvider request + const orderProviderRequest = { + makerAssetData: assetData, takerAssetData: etherTokenAssetData, networkId: this.networkId, }; - const requests = [targetOrderProviderRequest, feeOrderProviderRequest]; - // fetch orders and possible fillable amounts - const [targetOrderProviderResponse, feeOrderProviderResponse] = await Promise.all( - _.map(requests, async request => this.orderProvider.getOrdersAsync(request)), - ); + 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(targetOrderProviderResponse, targetOrderProviderRequest); - orderProviderResponseProcessor.throwIfInvalidResponse(feeOrderProviderResponse, feeOrderProviderRequest); + orderProviderResponseProcessor.throwIfInvalidResponse(response, request); // process the responses into one object + const isMakerAssetZrxToken = assetData === zrxTokenAssetData; const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( - targetOrderProviderResponse, - feeOrderProviderResponse, - zrxTokenAssetData, + response, + isMakerAssetZrxToken, this.expiryBufferSeconds, this._contractWrappers.orderValidator, ); + const lastRefreshTime = Date.now(); + const updatedOrdersEntry = { + ordersAndFillableAmounts, + lastRefreshTime, + }; + this._ordersEntryMap[assetData] = updatedOrdersEntry; return ordersAndFillableAmounts; } /** diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index 79b5d9052..e1056e39b 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -1,6 +1,15 @@ import { BigNumber } from '@0xproject/utils'; -import { BuyQuoteRequestOpts } from './types'; +import { AssetBuyerOpts, BuyQuoteExecutionOpts, BuyQuoteRequestOpts } 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: 15, +}; const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = { feePercentage: 0, @@ -8,13 +17,18 @@ const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = { slippagePercentage: 0.2, // 20% slippage protection }; +// Other default values are dynamically determined +const DEFAULT_BUY_QUOTE_EXECUTION_OPTS: BuyQuoteExecutionOpts = { + feeRecipient: NULL_ADDRESS, +}; + export const constants = { ZERO_AMOUNT: new BigNumber(0), - NULL_ADDRESS: '0x0000000000000000000000000000000000000000', - MAINNET_NETWORK_ID: 1, - DEFAULT_ORDER_REFRESH_INTERVAL_MS: 10000, // 10 seconds + NULL_ADDRESS, + MAINNET_NETWORK_ID, ETHER_TOKEN_DECIMALS: 18, + DEFAULT_ASSET_BUYER_OPTS, + DEFAULT_BUY_QUOTE_EXECUTION_OPTS, DEFAULT_BUY_QUOTE_REQUEST_OPTS, MAX_PER_PAGE: 10000, - DEFAULT_EXPIRY_BUFFER_SECONDS: 15, }; diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 8ef529ac0..2da2724d7 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -5,13 +5,14 @@ export { BigNumber } from '@0xproject/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 { StandardRelayerAPIAssetBuyerManager } from './standard_relayer_api_asset_buyer_manager'; export { AssetBuyerError, + AssetBuyerOpts, BuyQuote, + BuyQuoteExecutionOpts, + BuyQuoteRequestOpts, OrderProvider, OrderProviderRequest, OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, - StandardRelayerApiAssetBuyerManagerError, } from './types'; diff --git a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts b/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts deleted file mode 100644 index 947c738a1..000000000 --- a/packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { HttpClient } from '@0xproject/connect'; -import { ContractWrappers } from '@0xproject/contract-wrappers'; -import { ObjectMap } from '@0xproject/types'; -import { Provider } from 'ethereum-types'; -import * as _ from 'lodash'; - -import { AssetBuyer } from './asset_buyer'; -import { constants } from './constants'; -import { assert } from './utils/assert'; -import { assetDataUtils } from './utils/asset_data_utils'; - -import { OrderProvider, StandardRelayerApiAssetBuyerManagerError } from './types'; - -export class StandardRelayerAPIAssetBuyerManager { - // Map of assetData to AssetBuyer for that assetData - private readonly _assetBuyerMap: ObjectMap<AssetBuyer>; - /** - * Returns an array of all assetDatas available at the provided sraApiUrl - * @param sraApiUrl The standard relayer API base HTTP url you would like to source orders from. - * @param pairedWithAssetData Optional filter argument to return assetDatas that only pair with this assetData value. - * - * @return An array of all assetDatas available at the provider sraApiUrl - */ - public static async getAllAvailableAssetDatasAsync( - sraApiUrl: string, - pairedWithAssetData?: string, - ): Promise<string[]> { - const client = new HttpClient(sraApiUrl); - const params = { - assetDataA: pairedWithAssetData, - perPage: constants.MAX_PER_PAGE, - }; - const assetPairsResponse = await client.getAssetPairsAsync(params); - return _.uniq(_.map(assetPairsResponse.records, pairsItem => pairsItem.assetDataB.assetData)); - } - /** - * Instantiates a new StandardRelayerAPIAssetBuyerManager instance with all available assetDatas at the provided sraApiUrl - * @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 orderProvider An object that conforms to OrderProvider, see type for definition. - * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. - * Defaults to 10000ms (10s). - * @return An promise of an instance of StandardRelayerAPIAssetBuyerManager - */ - public static async getAssetBuyerManagerWithAllAvailableAssetDatasAsync( - provider: Provider, - sraApiUrl: string, - orderProvider: OrderProvider, - networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs?: number, - ): Promise<StandardRelayerAPIAssetBuyerManager> { - const contractWrappers = new ContractWrappers(provider, { networkId }); - const etherTokenAssetData = assetDataUtils.getEtherTokenAssetDataOrThrow(contractWrappers); - const assetDatas = await StandardRelayerAPIAssetBuyerManager.getAllAvailableAssetDatasAsync( - sraApiUrl, - etherTokenAssetData, - ); - return new StandardRelayerAPIAssetBuyerManager( - provider, - assetDatas, - orderProvider, - networkId, - orderRefreshIntervalMs, - ); - } - /** - * Instantiates a new StandardRelayerAPIAssetBuyerManager instance - * @param provider The Provider instance you would like to use for interacting with the Ethereum network. - * @param assetDatas The assetDatas of the desired assets to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). - * @param orderProvider An object that conforms to OrderProvider, see type for definition. - * @param networkId The ethereum network id. Defaults to 1 (mainnet). - * @param orderRefreshIntervalMs The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. - * Defaults to 10000ms (10s). - * @return An instance of StandardRelayerAPIAssetBuyerManager - */ - constructor( - provider: Provider, - assetDatas: string[], - orderProvider: OrderProvider, - networkId?: number, - orderRefreshIntervalMs?: number, - ) { - assert.assert(assetDatas.length > 0, `Expected 'assetDatas' to be a non-empty array.`); - this._assetBuyerMap = _.reduce( - assetDatas, - (accAssetBuyerMap: ObjectMap<AssetBuyer>, assetData: string) => { - accAssetBuyerMap[assetData] = new AssetBuyer( - provider, - assetData, - orderProvider, - networkId, - orderRefreshIntervalMs, - ); - return accAssetBuyerMap; - }, - {}, - ); - } - /** - * Get an AssetBuyer for the provided assetData - * @param assetData The desired assetData. - * - * @return An instance of AssetBuyer - */ - public getAssetBuyerFromAssetData(assetData: string): AssetBuyer { - const assetBuyer = this._assetBuyerMap[assetData]; - if (_.isUndefined(assetBuyer)) { - throw new Error( - `${StandardRelayerApiAssetBuyerManagerError.AssetBuyerNotFound}: For assetData ${assetData}`, - ); - } - return assetBuyer; - } - /** - * Get an AssetBuyer for the provided ERC20 tokenAddress - * @param tokenAddress The desired tokenAddress. - * - * @return An instance of AssetBuyer - */ - public getAssetBuyerFromERC20TokenAddress(tokenAddress: string): AssetBuyer { - const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); - return this.getAssetBuyerFromAssetData(assetData); - } - /** - * Get a list of all the assetDatas that the instance supports - * - * @return An array of assetData strings - */ - public getAssetDatas(): string[] { - return _.keys(this._assetBuyerMap); - } -} diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index ee6858525..8d3dcbfe6 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -52,6 +52,11 @@ export interface BuyQuote { feePercentage?: number; } +/** + * 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; @@ -59,6 +64,28 @@ export interface BuyQuoteRequestOpts { } /** + * rate: The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate. + * takerAddress: The address to perform the buy. Defaults to the first available address from the provider. + * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000). + */ +export interface BuyQuoteExecutionOpts { + rate?: BigNumber; + takerAddress?: string; + 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 15s. + */ +export interface AssetBuyerOpts { + networkId: number; + orderRefreshIntervalMs: number; + expiryBufferSeconds: number; +} + +/** * Possible errors thrown by an AssetBuyer instance or associated static methods. */ export enum AssetBuyerError { @@ -69,18 +96,10 @@ export enum AssetBuyerError { InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE', + AssetUnavailable = 'ASSET_UNAVAILABLE', } -/** - * Possible errors thrown by an StandardRelayerApiAssetBuyerManager instance or associated static methods. - */ -export enum StandardRelayerApiAssetBuyerManagerError { - AssetBuyerNotFound = 'ASSET_BUYER_NOT_FOUND', -} - -export interface AssetBuyerOrdersAndFillableAmounts { +export interface OrdersAndFillableAmounts { orders: SignedOrder[]; - feeOrders: SignedOrder[]; remainingFillableMakerAssetAmounts: BigNumber[]; - remainingFillableFeeAmounts: BigNumber[]; } diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index 9946924ef..b706ea143 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -3,24 +3,23 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import { constants } from '../constants'; -import { AssetBuyerError, AssetBuyerOrdersAndFillableAmounts, BuyQuote } from '../types'; +import { AssetBuyerError, BuyQuote, 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: AssetBuyerOrdersAndFillableAmounts, + ordersAndFillableAmounts: OrdersAndFillableAmounts, + feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, assetBuyAmount: BigNumber, feePercentage: number, slippagePercentage: number, ): BuyQuote { - const { - orders, - feeOrders, - remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts, - } = ordersAndFillableAmounts; + const orders = ordersAndFillableAmounts.orders; + const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; + const feeOrders = feeOrdersAndFillableAmounts.orders; + const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts; const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round(); const { resultOrders, diff --git a/packages/asset-buyer/src/utils/order_provider_response_processor.ts b/packages/asset-buyer/src/utils/order_provider_response_processor.ts index 31fdcc182..74eec162d 100644 --- a/packages/asset-buyer/src/utils/order_provider_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -8,19 +8,14 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { AssetBuyerError, - AssetBuyerOrdersAndFillableAmounts, OrderProviderRequest, OrderProviderResponse, + OrdersAndFillableAmounts, SignedOrderWithRemainingFillableMakerAssetAmount, } from '../types'; import { orderUtils } from './order_utils'; -interface OrdersAndRemainingFillableMakerAssetAmounts { - orders: SignedOrder[]; - remainingFillableMakerAssetAmounts: BigNumber[]; -} - export const orderProviderResponseProcessor = { throwIfInvalidResponse(response: OrderProviderResponse, request: OrderProviderRequest): void { const { makerAssetData, takerAssetData } = request; @@ -38,65 +33,40 @@ export const orderProviderResponseProcessor = { * - Sort by rate */ async processAsync( - targetOrderProviderResponse: OrderProviderResponse, - feeOrderProviderResponse: OrderProviderResponse, - zrxTokenAssetData: string, + orderProviderResponse: OrderProviderResponse, + isMakerAssetZrxToken: boolean, expiryBufferSeconds: number, orderValidator?: OrderValidatorWrapper, - ): Promise<AssetBuyerOrdersAndFillableAmounts> { + ): Promise<OrdersAndFillableAmounts> { // drop orders that are expired or not open - const filteredTargetOrders = filterOutExpiredAndNonOpenOrders( - targetOrderProviderResponse.orders, - expiryBufferSeconds, - ); - const filteredFeeOrders = filterOutExpiredAndNonOpenOrders( - feeOrderProviderResponse.orders, - expiryBufferSeconds, - ); + const filteredOrders = filterOutExpiredAndNonOpenOrders(orderProviderResponse.orders, expiryBufferSeconds); // set the orders to be sorted equal to the filtered orders - let unsortedTargetOrders = filteredTargetOrders; - let unsortedFeeOrders = filteredFeeOrders; + 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 these requests and throw a more domain specific error - // TODO(bmillman): optimization - // reduce this to once RPC call buy combining orders into one array and then splitting up the response - const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( - _.map([filteredTargetOrders, filteredFeeOrders], ordersToBeValidated => { - const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); - return orderValidator.getOrdersAndTradersInfoAsync(ordersToBeValidated, takerAddresses); - }), - ); - // take orders + on chain information and find the valid orders and remaining fillable maker asset amounts - unsortedTargetOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( - filteredTargetOrders, - targetOrdersAndTradersInfo, - zrxTokenAssetData, + // 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 - unsortedFeeOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( - filteredFeeOrders, - feeOrdersAndTradersInfo, - zrxTokenAssetData, + 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 sortedTargetOrders = sortingUtils.sortOrdersByFeeAdjustedRate(unsortedTargetOrders); - const sortedFeeOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedFeeOrders); + const sortedOrders = isMakerAssetZrxToken + ? sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedOrders) + : sortingUtils.sortOrdersByFeeAdjustedRate(unsortedOrders); // unbundle orders and fillable amounts and compile final result - const targetOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedTargetOrders); - const feeOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedFeeOrders); - return { - orders: targetOrdersAndRemainingFillableMakerAssetAmounts.orders, - feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, - remainingFillableMakerAssetAmounts: - targetOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, - remainingFillableFeeAmounts: - feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, - }; + const result = unbundleOrdersWithAmounts(sortedOrders); + return result; }, }; @@ -120,7 +90,7 @@ function filterOutExpiredAndNonOpenOrders( function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( inputOrders: SignedOrder[], ordersAndTradersInfo: OrderAndTraderInfo[], - zrxAssetData: string, + 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 @@ -147,7 +117,7 @@ function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( const remainingFillableCalculator = new RemainingFillableCalculator( order.makerFee, order.makerAssetAmount, - order.makerAssetData === zrxAssetData, + isMakerAssetZrxToken, transferrableAssetAmount, transferrableFeeAssetAmount, remainingMakerAssetAmount, @@ -175,7 +145,7 @@ function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain( */ function unbundleOrdersWithAmounts( ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[], -): OrdersAndRemainingFillableMakerAssetAmounts { +): OrdersAndFillableAmounts { const result = _.reduce( ordersWithAmounts, (acc, orderWithAmount) => { |