From 264b25c58de7170dd1e28afe94b7a8e70170f207 Mon Sep 17 00:00:00 2001 From: fragosti Date: Mon, 24 Sep 2018 22:21:25 +0200 Subject: Rename StandardRelayerAPIAssetBuyerManager to just AssetBuyerManager and generalize it --- packages/asset-buyer/src/asset_buyer_manager.ts | 166 +++++++++++++++++++++ packages/asset-buyer/src/index.ts | 4 +- .../standard_relayer_api_asset_buyer_manager.ts | 133 ----------------- packages/asset-buyer/src/types.ts | 4 +- yarn.lock | 6 +- 5 files changed, 173 insertions(+), 140 deletions(-) create mode 100644 packages/asset-buyer/src/asset_buyer_manager.ts delete mode 100644 packages/asset-buyer/src/standard_relayer_api_asset_buyer_manager.ts diff --git a/packages/asset-buyer/src/asset_buyer_manager.ts b/packages/asset-buyer/src/asset_buyer_manager.ts new file mode 100644 index 000000000..3f96a79bb --- /dev/null +++ b/packages/asset-buyer/src/asset_buyer_manager.ts @@ -0,0 +1,166 @@ +import { HttpClient } from '@0xproject/connect'; +import { ContractWrappers } from '@0xproject/contract-wrappers'; +import { SignedOrder } from '@0xproject/order-utils'; +import { ObjectMap } from '@0xproject/types'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { AssetBuyer } from './asset_buyer'; +import { constants } from './constants'; +import { BasicOrderProvider } from './order_providers/basic_order_provider'; +import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; +import { assert } from './utils/assert'; +import { assetDataUtils } from './utils/asset_data_utils'; + +import { AssetBuyerManagerError, OrderProvider } from './types'; + +export class AssetBuyerManager { + // Map of assetData to AssetBuyer for that assetData + private readonly _assetBuyerMap: ObjectMap; + /** + * 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 { + 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 AssetBuyerManager 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 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 promise of an instance of AssetBuyerManager + */ + public static async getAssetBuyerManagerFromStandardRelayerApiAsync( + provider: Provider, + sraApiUrl: string, + networkId: number = constants.MAINNET_NETWORK_ID, + orderRefreshIntervalMs?: number, + expiryBufferSeconds?: number, + ): Promise { + const contractWrappers = new ContractWrappers(provider, { networkId }); + const etherTokenAssetData = assetDataUtils.getEtherTokenAssetDataOrThrow(contractWrappers); + const assetDatas = await AssetBuyerManager.getAllAvailableAssetDatasAsync(sraApiUrl, etherTokenAssetData); + const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); + return new AssetBuyerManager( + provider, + assetDatas, + orderProvider, + networkId, + orderRefreshIntervalMs, + expiryBufferSeconds, + ); + } + /** + * Instantiates a new AssetBuyerManager 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. + * + * @return An instance of AssetBuyerManager + */ + public static getAssetBuyerManagerFromProvidedOrders( + provider: Provider, + orders: SignedOrder[], + feeOrders: SignedOrder[] = [], + networkId?: number, + orderRefreshIntervalMs?: number, + expiryBufferSeconds?: number, + ): AssetBuyerManager { + const assetDatas = _.map(orders, order => order.makerAssetData); + const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); + return new AssetBuyerManager( + provider, + assetDatas, + orderProvider, + networkId, + orderRefreshIntervalMs, + expiryBufferSeconds, + ); + } + /** + * Instantiates a new AssetBuyerManager 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). + * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * + * @return An instance of AssetBuyerManager + */ + constructor( + provider: Provider, + assetDatas: string[], + orderProvider: OrderProvider, + networkId?: number, + orderRefreshIntervalMs?: number, + expiryBufferSeconds?: number, + ) { + assert.assert(assetDatas.length > 0, `Expected 'assetDatas' to be a non-empty array.`); + this._assetBuyerMap = _.reduce( + assetDatas, + (accAssetBuyerMap: ObjectMap, assetData: string) => { + accAssetBuyerMap[assetData] = new AssetBuyer( + provider, + assetData, + orderProvider, + networkId, + orderRefreshIntervalMs, + expiryBufferSeconds, + ); + 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(`${AssetBuyerManagerError.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/index.ts b/packages/asset-buyer/src/index.ts index 8ef529ac0..cedb3bbf4 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -5,7 +5,7 @@ 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 { AssetBuyerManager } from './asset_buyer_manager'; export { AssetBuyerError, BuyQuote, @@ -13,5 +13,5 @@ export { OrderProviderRequest, OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, - StandardRelayerApiAssetBuyerManagerError, + AssetBuyerManagerError, } 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; - /** - * 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 { - 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 { - 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, 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..141193b7b 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -72,9 +72,9 @@ export enum AssetBuyerError { } /** - * Possible errors thrown by an StandardRelayerApiAssetBuyerManager instance or associated static methods. + * Possible errors thrown by an AssetBuyerManager instance or associated static methods. */ -export enum StandardRelayerApiAssetBuyerManagerError { +export enum AssetBuyerManagerError { AssetBuyerNotFound = 'ASSET_BUYER_NOT_FOUND', } diff --git a/yarn.lock b/yarn.lock index 7464a9c5d..aa30946a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5296,9 +5296,9 @@ ethereumjs-wallet@~0.6.0: utf8 "^3.0.0" uuid "^3.3.2" -ethers@3.0.22: - version "3.0.22" - resolved "https://registry.npmjs.org/ethers/-/ethers-3.0.22.tgz#7fab1ea16521705837aa43c15831877b2716b436" +ethers@0xproject/ethers.js#eip-838-reasons, ethers@3.0.22: + version "3.0.18" + resolved "https://codeload.github.com/0xproject/ethers.js/tar.gz/b91342bd200d142af0165d6befddf783c8ae8447" dependencies: aes-js "3.0.0" bn.js "^4.4.0" -- cgit From 89033e01e81fba91aecf62822b582f6f007f5494 Mon Sep 17 00:00:00 2001 From: fragosti Date: Mon, 24 Sep 2018 23:14:07 +0200 Subject: Provide convenience methods on AssetBuyerManager --- packages/asset-buyer/src/asset_buyer.ts | 1 + packages/asset-buyer/src/asset_buyer_manager.ts | 43 ++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 409e34e74..39b0aa619 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -225,6 +225,7 @@ export class AssetBuyer { * @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( diff --git a/packages/asset-buyer/src/asset_buyer_manager.ts b/packages/asset-buyer/src/asset_buyer_manager.ts index 3f96a79bb..1e2c5db5e 100644 --- a/packages/asset-buyer/src/asset_buyer_manager.ts +++ b/packages/asset-buyer/src/asset_buyer_manager.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@0xproject/connect'; import { ContractWrappers } from '@0xproject/contract-wrappers'; import { SignedOrder } from '@0xproject/order-utils'; import { ObjectMap } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; @@ -12,7 +13,7 @@ import { StandardRelayerAPIOrderProvider } from './order_providers/standard_rela import { assert } from './utils/assert'; import { assetDataUtils } from './utils/asset_data_utils'; -import { AssetBuyerManagerError, OrderProvider } from './types'; +import { AssetBuyerManagerError, BuyQuote, BuyQuoteRequestOpts, OrderProvider } from './types'; export class AssetBuyerManager { // Map of assetData to AssetBuyer for that assetData @@ -163,4 +164,44 @@ export class AssetBuyerManager { public getAssetDatas(): string[] { return _.keys(this._assetBuyerMap); } + /** + * Get a `BuyQuote` containing all information relevant to fulfilling a buy. + * You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy. + * + * @param assetData The assetData that identifies the desired asset to buy. + * @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. + * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. + */ + public async getBuyQuoteAsync( + assetData: string, + assetBuyAmount: BigNumber, + options: Partial, + ): Promise { + return this.getAssetBuyerFromAssetData(assetData).getBuyQuoteAsync(assetBuyAmount, options); + } + /** + * 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, + ): Promise { + return this.getAssetBuyerFromAssetData(buyQuote.assetData).executeBuyQuoteAsync( + buyQuote, + rate, + takerAddress, + feeRecipient, + ); + } } -- cgit From 47f8b5d6fc941b3f9c5cf8bb00c3be6123be629f Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 25 Sep 2018 13:55:07 +0200 Subject: Use options object convention everywhere, and make package private --- packages/asset-buyer/README.md | 2 +- packages/asset-buyer/package.json | 4 +- packages/asset-buyer/src/asset_buyer.ts | 106 +++++++----------------- packages/asset-buyer/src/asset_buyer_manager.ts | 82 +++++------------- packages/asset-buyer/src/constants.ts | 24 ++++-- packages/asset-buyer/src/index.ts | 3 + packages/asset-buyer/src/types.ts | 27 ++++++ 7 files changed, 106 insertions(+), 142 deletions(-) diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index 5f7f26f30..294653fc3 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -1,6 +1,6 @@ ## @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. +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. diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index 2b5136e31..940658d4d 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": { @@ -69,6 +69,6 @@ "typescript": "3.0.1" }, "publishConfig": { - "access": "public" + "access": "private" } } diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 39b0aa619..afef0d070 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -11,8 +11,10 @@ import { BasicOrderProvider } from './order_providers/basic_order_provider'; import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; import { AssetBuyerError, + AssetBuyerOpts, AssetBuyerOrdersAndFillableAmounts, BuyQuote, + BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrderProvider, OrderProviderResponse, @@ -38,9 +40,7 @@ export class AssetBuyer { * @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,28 +48,17 @@ 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, ): 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, assetData, orderProvider, options); return assetBuyer; } /** @@ -77,9 +66,7 @@ export class AssetBuyer { * @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 */ @@ -87,24 +74,13 @@ export class AssetBuyer { 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, ): 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, - ); + const assetBuyer = new AssetBuyer(provider, assetData, orderProvider, options); return assetBuyer; } /** @@ -112,59 +88,43 @@ export class AssetBuyer { * @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. + * @param options Initialization options for the AssetBuyer. See type definition for details. + * * @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, + options: Partial, ): 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 = AssetBuyer.getAssetBuyerForAssetData(provider, assetData, sraApiUrl, 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 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 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, assetData: string, orderProvider: OrderProvider, options: Partial) { + 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; @@ -179,15 +139,14 @@ export class AssetBuyer { * Get a `BuyQuote` containing all information relevant to fulfilling a buy. * You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy. * @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): Promise { const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = { - ...options, ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS, + ...options, }; assert.isBigNumber('assetBuyAmount', assetBuyAmount); assert.isValidPercentage('feePercentage', feePercentage); @@ -222,18 +181,15 @@ export class AssetBuyer { /** * 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 { + public async executeBuyQuoteAsync(buyQuote: BuyQuote, options: Partial): Promise { + const { rate, takerAddress, feeRecipient } = { + ...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS, + ...options, + }; assert.isValidBuyQuote('buyQuote', buyQuote); if (!_.isUndefined(rate)) { assert.isBigNumber('rate', rate); diff --git a/packages/asset-buyer/src/asset_buyer_manager.ts b/packages/asset-buyer/src/asset_buyer_manager.ts index 1e2c5db5e..1bde55eff 100644 --- a/packages/asset-buyer/src/asset_buyer_manager.ts +++ b/packages/asset-buyer/src/asset_buyer_manager.ts @@ -13,7 +13,14 @@ import { StandardRelayerAPIOrderProvider } from './order_providers/standard_rela import { assert } from './utils/assert'; import { assetDataUtils } from './utils/asset_data_utils'; -import { AssetBuyerManagerError, BuyQuote, BuyQuoteRequestOpts, OrderProvider } from './types'; +import { + AssetBuyerManagerError, + AssetBuyerOpts, + BuyQuote, + BuyQuoteExecutionOpts, + BuyQuoteRequestOpts, + OrderProvider, +} from './types'; export class AssetBuyerManager { // Map of assetData to AssetBuyer for that assetData @@ -41,40 +48,28 @@ export class AssetBuyerManager { * Instantiates a new AssetBuyerManager 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 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 an AssetBuyer. See type definition for details. * * @return An promise of an instance of AssetBuyerManager */ public static async getAssetBuyerManagerFromStandardRelayerApiAsync( provider: Provider, sraApiUrl: string, - networkId: number = constants.MAINNET_NETWORK_ID, - orderRefreshIntervalMs?: number, - expiryBufferSeconds?: number, + options: Partial, ): Promise { + const networkId = options.networkId || constants.MAINNET_NETWORK_ID; const contractWrappers = new ContractWrappers(provider, { networkId }); const etherTokenAssetData = assetDataUtils.getEtherTokenAssetDataOrThrow(contractWrappers); const assetDatas = await AssetBuyerManager.getAllAvailableAssetDatasAsync(sraApiUrl, etherTokenAssetData); const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); - return new AssetBuyerManager( - provider, - assetDatas, - orderProvider, - networkId, - orderRefreshIntervalMs, - expiryBufferSeconds, - ); + return new AssetBuyerManager(provider, assetDatas, orderProvider, options); } /** * Instantiates a new AssetBuyerManager 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 an AssetBuyer. See type definition for details. * * @return An instance of AssetBuyerManager */ @@ -82,29 +77,18 @@ export class AssetBuyerManager { provider: Provider, orders: SignedOrder[], feeOrders: SignedOrder[] = [], - networkId?: number, - orderRefreshIntervalMs?: number, - expiryBufferSeconds?: number, + options: Partial, ): AssetBuyerManager { const assetDatas = _.map(orders, order => order.makerAssetData); const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); - return new AssetBuyerManager( - provider, - assetDatas, - orderProvider, - networkId, - orderRefreshIntervalMs, - expiryBufferSeconds, - ); + return new AssetBuyerManager(provider, assetDatas, orderProvider, options); } /** * Instantiates a new AssetBuyerManager 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). - * @param expiryBufferSeconds The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. + * @param orderProvider An object that conforms to OrderProvider, see type for definition. + * @param options Initialization options for an AssetBuyer. See type definition for details. * * @return An instance of AssetBuyerManager */ @@ -112,22 +96,13 @@ export class AssetBuyerManager { provider: Provider, assetDatas: string[], orderProvider: OrderProvider, - networkId?: number, - orderRefreshIntervalMs?: number, - expiryBufferSeconds?: number, + options: Partial, ) { assert.assert(assetDatas.length > 0, `Expected 'assetDatas' to be a non-empty array.`); this._assetBuyerMap = _.reduce( assetDatas, (accAssetBuyerMap: ObjectMap, assetData: string) => { - accAssetBuyerMap[assetData] = new AssetBuyer( - provider, - assetData, - orderProvider, - networkId, - orderRefreshIntervalMs, - expiryBufferSeconds, - ); + accAssetBuyerMap[assetData] = new AssetBuyer(provider, assetData, orderProvider, options); return accAssetBuyerMap; }, {}, @@ -170,9 +145,8 @@ export class AssetBuyerManager { * * @param assetData The assetData that identifies the desired asset to buy. * @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 execution of the BuyQuote. 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( @@ -191,17 +165,7 @@ export class AssetBuyerManager { * * @return A promise of the txHash. */ - public async executeBuyQuoteAsync( - buyQuote: BuyQuote, - rate?: BigNumber, - takerAddress?: string, - feeRecipient?: string, - ): Promise { - return this.getAssetBuyerFromAssetData(buyQuote.assetData).executeBuyQuoteAsync( - buyQuote, - rate, - takerAddress, - feeRecipient, - ); + public async executeBuyQuoteAsync(buyQuote: BuyQuote, options: Partial): Promise { + return this.getAssetBuyerFromAssetData(buyQuote.assetData).executeBuyQuoteAsync(buyQuote, options); } } 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 cedb3bbf4..830b4d8b8 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -8,7 +8,10 @@ export { StandardRelayerAPIOrderProvider } from './order_providers/standard_rela export { AssetBuyerManager } from './asset_buyer_manager'; export { AssetBuyerError, + AssetBuyerOpts, BuyQuote, + BuyQuoteExecutionOpts, + BuyQuoteRequestOpts, OrderProvider, OrderProviderRequest, OrderProviderResponse, diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 141193b7b..67baa51c7 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -52,12 +52,39 @@ 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; slippagePercentage: number; } +/** + * 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. */ -- cgit From 49cdd85b1d0ca32eb650baecf2f148d807fefa77 Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 25 Sep 2018 13:58:48 +0200 Subject: Add AssetBuyerManager to README --- packages/asset-buyer/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index 294653fc3..082d3e603 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -12,16 +12,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 -- cgit From b853f04972d797431a5e3f40155fc77c17f913c6 Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 25 Sep 2018 14:01:16 +0200 Subject: Make package public again, but add warning --- packages/asset-buyer/README.md | 2 ++ packages/asset-buyer/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index 082d3e603..555b90957 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -1,5 +1,7 @@ ## @0xproject/asset-buyer +**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. diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index 940658d4d..b75f09cd3 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -69,6 +69,6 @@ "typescript": "3.0.1" }, "publishConfig": { - "access": "private" + "access": "public" } } -- cgit From 04dd4ce6d1d0ca9703610579632d6989f9959277 Mon Sep 17 00:00:00 2001 From: fragosti Date: Tue, 25 Sep 2018 14:10:38 +0200 Subject: Add to CHANGELOG --- packages/asset-buyer/CHANGELOG.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 50b965f2d..ff11aa126 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.0.0", + "changes": [ + { + "note": "Rename StandardRelayerAPIAssetBuyerManager to AssetBuyerManager and other API improvements.", + "pr": 1086 + } + ] + }, { "timestamp": 1537875740, "version": "1.0.0", -- cgit From b3a868da0e77da04b123f4ff397d8f1a3a4d3810 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 2 Oct 2018 10:17:09 -0700 Subject: Merge AssetBuyer and AssetBuyerManager --- packages/asset-buyer/src/asset_buyer.ts | 208 ++++++++++++--------- packages/asset-buyer/src/asset_buyer_manager.ts | 171 ----------------- packages/asset-buyer/src/types.ts | 4 +- .../asset-buyer/src/utils/buy_quote_calculator.ts | 15 +- .../src/utils/order_provider_response_processor.ts | 76 +++----- 5 files changed, 148 insertions(+), 326 deletions(-) delete mode 100644 packages/asset-buyer/src/asset_buyer_manager.ts diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index afef0d070..990369615 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'; @@ -12,12 +14,12 @@ import { StandardRelayerAPIOrderProvider } from './order_providers/standard_rela import { AssetBuyerError, AssetBuyerOpts, - AssetBuyerOrdersAndFillableAmounts, BuyQuote, BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrderProvider, OrderProviderResponse, + OrdersAndFillableAmounts, } from './types'; import { assert } from './utils/assert'; @@ -25,16 +27,39 @@ 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 = {}; + /** + * 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 { + 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 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. @@ -48,7 +73,7 @@ export class AssetBuyer { provider: Provider, orders: SignedOrder[], feeOrders: SignedOrder[] = [], - options: Partial, + options: Partial = {}, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); @@ -56,9 +81,8 @@ export class AssetBuyer { 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, options); + const assetBuyer = new AssetBuyer(provider, orderProvider, options); return assetBuyer; } /** @@ -70,63 +94,36 @@ export class AssetBuyer { * * @return An instance of AssetBuyer */ - public static getAssetBuyerForAssetData( + public static getAssetBuyerForSraApiUrl( provider: Provider, - assetData: string, sraApiUrl: string, - options: Partial, + options: Partial = {}, ): AssetBuyer { assert.isWeb3Provider('provider', provider); - assert.isHexString('assetData', assetData); assert.isWebUri('sraApiUrl', sraApiUrl); const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); - const assetBuyer = new AssetBuyer(provider, assetData, orderProvider, options); - 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 options Initialization options for the AssetBuyer. See type definition for details. - * - * @return An instance of AssetBuyer - */ - public static getAssetBuyerForERC20TokenAddress( - provider: Provider, - tokenAddress: string, - sraApiUrl: string, - options: Partial, - ): AssetBuyer { - assert.isWeb3Provider('provider', provider); - assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isWebUri('sraApiUrl', sraApiUrl); - const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); - const assetBuyer = AssetBuyer.getAssetBuyerForAssetData(provider, assetData, sraApiUrl, options); + 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 options Initialization options for the AssetBuyer. See type definition for details. * * @return An instance of AssetBuyer */ - constructor(provider: Provider, assetData: string, orderProvider: OrderProvider, options: Partial) { + constructor(provider: Provider, orderProvider: OrderProvider, options: Partial = {}) { 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; @@ -136,48 +133,62 @@ export class AssetBuyer { }); } /** - * 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 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): Promise { + public async getBuyQuoteAsync( + assetData: string, + assetBuyAmount: BigNumber, + options: Partial, + ): Promise { const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = { ...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; - } + const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); + const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([ + this._getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh), + this._getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh), + shouldForceOrderRefresh, + ]); const buyQuote = buyQuoteCalculator.calculate( ordersAndFillableAmounts, + feeOrdersAndFillableAmounts, assetBuyAmount, feePercentage, slippagePercentage, ); 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, + ): Promise { + 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. @@ -229,39 +240,54 @@ export class AssetBuyer { return txHash; } /** - * Ask the order Provider for orders and process them. + * Grab orders from the cache, if there is a miss or it is time to refresh, fetch and process the orders */ - private async _getLatestOrdersAndFillableAmountsAsync(): Promise { - 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, - 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)), - ); - // 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); - // process the responses into one object - const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( - targetOrderProviderResponse, - feeOrderProviderResponse, - zrxTokenAssetData, - this.expiryBufferSeconds, - this._contractWrappers.orderValidator, - ); + private async _getOrdersAndFillableAmountsAsync( + assetData: string, + shouldForceOrderRefresh: boolean, + ): Promise { + // 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 ordersEntryIfExists = this._ordersEntryMap[assetData]; + const shouldRefresh = + _.isUndefined(ordersEntryIfExists) || + shouldForceOrderRefresh || + ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now(); + let ordersAndFillableAmounts: OrdersAndFillableAmounts; + if (shouldRefresh) { + const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); + const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); + // construct orderProvider request + const orderProviderRequest = { + makerAssetData: assetData, + takerAssetData: etherTokenAssetData, + networkId: this.networkId, + }; + const request = orderProviderRequest; + // get provider response + const response = await this.orderProvider.getOrdersAsync(request); + // since the order provider is an injected dependency, validate that it respects the API + // ie. it should only return maker/taker assetDatas that are specified + orderProviderResponseProcessor.throwIfInvalidResponse(response, request); + // process the responses into one object + const isMakerAssetZrxToken = assetData === zrxTokenAssetData; + ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( + response, + isMakerAssetZrxToken, + this.expiryBufferSeconds, + this._contractWrappers.orderValidator, + ); + const lastRefreshTime = Date.now(); + const updatedOrdersEntry = { + ordersAndFillableAmounts, + lastRefreshTime, + }; + this._ordersEntryMap[assetData] = updatedOrdersEntry; + } else { + ordersAndFillableAmounts = ordersEntryIfExists.ordersAndFillableAmounts; + } return ordersAndFillableAmounts; } /** diff --git a/packages/asset-buyer/src/asset_buyer_manager.ts b/packages/asset-buyer/src/asset_buyer_manager.ts deleted file mode 100644 index 1bde55eff..000000000 --- a/packages/asset-buyer/src/asset_buyer_manager.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { HttpClient } from '@0xproject/connect'; -import { ContractWrappers } from '@0xproject/contract-wrappers'; -import { SignedOrder } from '@0xproject/order-utils'; -import { ObjectMap } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import { Provider } from 'ethereum-types'; -import * as _ from 'lodash'; - -import { AssetBuyer } from './asset_buyer'; -import { constants } from './constants'; -import { BasicOrderProvider } from './order_providers/basic_order_provider'; -import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; -import { assert } from './utils/assert'; -import { assetDataUtils } from './utils/asset_data_utils'; - -import { - AssetBuyerManagerError, - AssetBuyerOpts, - BuyQuote, - BuyQuoteExecutionOpts, - BuyQuoteRequestOpts, - OrderProvider, -} from './types'; - -export class AssetBuyerManager { - // Map of assetData to AssetBuyer for that assetData - private readonly _assetBuyerMap: ObjectMap; - /** - * 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 { - 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 AssetBuyerManager 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 options Initialization options for an AssetBuyer. See type definition for details. - * - * @return An promise of an instance of AssetBuyerManager - */ - public static async getAssetBuyerManagerFromStandardRelayerApiAsync( - provider: Provider, - sraApiUrl: string, - options: Partial, - ): Promise { - const networkId = options.networkId || constants.MAINNET_NETWORK_ID; - const contractWrappers = new ContractWrappers(provider, { networkId }); - const etherTokenAssetData = assetDataUtils.getEtherTokenAssetDataOrThrow(contractWrappers); - const assetDatas = await AssetBuyerManager.getAllAvailableAssetDatasAsync(sraApiUrl, etherTokenAssetData); - const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl); - return new AssetBuyerManager(provider, assetDatas, orderProvider, options); - } - /** - * Instantiates a new AssetBuyerManager instance given existing liquidity in the form of orders and feeOrders. - * @param provider The Provider instance you would like to use for interacting with the Ethereum network. - * @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData (WETH). - * @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array. - * @param options Initialization options for an AssetBuyer. See type definition for details. - * - * @return An instance of AssetBuyerManager - */ - public static getAssetBuyerManagerFromProvidedOrders( - provider: Provider, - orders: SignedOrder[], - feeOrders: SignedOrder[] = [], - options: Partial, - ): AssetBuyerManager { - const assetDatas = _.map(orders, order => order.makerAssetData); - const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); - return new AssetBuyerManager(provider, assetDatas, orderProvider, options); - } - /** - * Instantiates a new AssetBuyerManager 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 options Initialization options for an AssetBuyer. See type definition for details. - * - * @return An instance of AssetBuyerManager - */ - constructor( - provider: Provider, - assetDatas: string[], - orderProvider: OrderProvider, - options: Partial, - ) { - assert.assert(assetDatas.length > 0, `Expected 'assetDatas' to be a non-empty array.`); - this._assetBuyerMap = _.reduce( - assetDatas, - (accAssetBuyerMap: ObjectMap, assetData: string) => { - accAssetBuyerMap[assetData] = new AssetBuyer(provider, assetData, orderProvider, options); - 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(`${AssetBuyerManagerError.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); - } - /** - * Get a `BuyQuote` containing all information relevant to fulfilling a buy. - * You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy. - * - * @param assetData The assetData that identifies the desired asset to buy. - * @param assetBuyAmount The amount of asset to buy. - * @param options Options for the execution of the BuyQuote. See type definition for more information. - * - * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. - */ - public async getBuyQuoteAsync( - assetData: string, - assetBuyAmount: BigNumber, - options: Partial, - ): Promise { - return this.getAssetBuyerFromAssetData(assetData).getBuyQuoteAsync(assetBuyAmount, options); - } - /** - * 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, options: Partial): Promise { - return this.getAssetBuyerFromAssetData(buyQuote.assetData).executeBuyQuoteAsync(buyQuote, options); - } -} diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 67baa51c7..074bcd453 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -105,9 +105,7 @@ export enum AssetBuyerManagerError { 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 { + ): Promise { // 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) => { -- cgit From 75d6970e6c9bdeb6004228084bb541994b18e8c3 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 2 Oct 2018 15:44:11 -0700 Subject: Add AssetUnavailable error --- packages/asset-buyer/src/asset_buyer.ts | 3 +++ packages/asset-buyer/src/types.ts | 8 +------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 990369615..587e4e941 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -160,6 +160,9 @@ export class AssetBuyer { this._getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh), shouldForceOrderRefresh, ]); + if (ordersAndFillableAmounts.orders.length === 0) { + throw new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${assetData}`); + } const buyQuote = buyQuoteCalculator.calculate( ordersAndFillableAmounts, feeOrdersAndFillableAmounts, diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 074bcd453..8d3dcbfe6 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -96,13 +96,7 @@ export enum AssetBuyerError { InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE', -} - -/** - * Possible errors thrown by an AssetBuyerManager instance or associated static methods. - */ -export enum AssetBuyerManagerError { - AssetBuyerNotFound = 'ASSET_BUYER_NOT_FOUND', + AssetUnavailable = 'ASSET_UNAVAILABLE', } export interface OrdersAndFillableAmounts { -- cgit From 75679835a72f4a011943ea5f4d1bada9a298e653 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 2 Oct 2018 15:52:27 -0700 Subject: Remove static methods to retrieve assetDatas from SRA --- packages/asset-buyer/src/asset_buyer.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 587e4e941..0fe765325 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -41,25 +41,6 @@ export class AssetBuyer { private readonly _contractWrappers: ContractWrappers; // cache of orders along with the time last updated keyed by assetData private readonly _ordersEntryMap: ObjectMap = {}; - /** - * 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 { - 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 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. -- cgit From 6deb027bdf4e57f8918fd2413f0fdc55311508d3 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 2 Oct 2018 16:10:41 -0700 Subject: Clean up interfaces and exports --- packages/asset-buyer/src/asset_buyer.ts | 76 ++++++++++++++++----------------- packages/asset-buyer/src/index.ts | 2 - 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 0fe765325..53bbe427b 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -67,15 +67,14 @@ export class AssetBuyer { 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 options Initialization options for the AssetBuyer. See type definition for details. * * @return An instance of AssetBuyer */ - public static getAssetBuyerForSraApiUrl( + public static getAssetBuyerForStandardRelayerAPIUrl( provider: Provider, sraApiUrl: string, options: Partial = {}, @@ -107,8 +106,8 @@ export class AssetBuyer { this.provider = provider; this.orderProvider = orderProvider; this.networkId = networkId; - this.expiryBufferSeconds = expiryBufferSeconds; this.orderRefreshIntervalMs = orderRefreshIntervalMs; + this.expiryBufferSeconds = expiryBufferSeconds; this._contractWrappers = new ContractWrappers(this.provider, { networkId, }); @@ -135,6 +134,7 @@ export class AssetBuyer { assert.isBigNumber('assetBuyAmount', assetBuyAmount); assert.isValidPercentage('feePercentage', feePercentage); assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh); + assert.isNumber('slippagePercentage', slippagePercentage); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([ this._getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh), @@ -224,54 +224,54 @@ export class AssetBuyer { return txHash; } /** - * Grab orders from the cache, if there is a miss or it is time to refresh, fetch and process the orders + * Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders */ private async _getOrdersAndFillableAmountsAsync( assetData: string, shouldForceOrderRefresh: boolean, ): Promise { + // 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 ordersEntryIfExists = this._ordersEntryMap[assetData]; const shouldRefresh = _.isUndefined(ordersEntryIfExists) || shouldForceOrderRefresh || ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now(); - let ordersAndFillableAmounts: OrdersAndFillableAmounts; - if (shouldRefresh) { - const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); - const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); - // construct orderProvider request - const orderProviderRequest = { - makerAssetData: assetData, - takerAssetData: etherTokenAssetData, - networkId: this.networkId, - }; - const request = orderProviderRequest; - // get provider response - const response = await this.orderProvider.getOrdersAsync(request); - // since the order provider is an injected dependency, validate that it respects the API - // ie. it should only return maker/taker assetDatas that are specified - orderProviderResponseProcessor.throwIfInvalidResponse(response, request); - // process the responses into one object - const isMakerAssetZrxToken = assetData === zrxTokenAssetData; - ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( - response, - isMakerAssetZrxToken, - this.expiryBufferSeconds, - this._contractWrappers.orderValidator, - ); - const lastRefreshTime = Date.now(); - const updatedOrdersEntry = { - ordersAndFillableAmounts, - lastRefreshTime, - }; - this._ordersEntryMap[assetData] = updatedOrdersEntry; - } else { - ordersAndFillableAmounts = ordersEntryIfExists.ordersAndFillableAmounts; + if (!shouldRefresh) { + const result = ordersEntryIfExists.ordersAndFillableAmounts; + return result; } + const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); + const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); + // construct orderProvider request + const orderProviderRequest = { + makerAssetData: assetData, + takerAssetData: etherTokenAssetData, + networkId: this.networkId, + }; + const request = orderProviderRequest; + // get provider response + const response = await this.orderProvider.getOrdersAsync(request); + // since the order provider is an injected dependency, validate that it respects the API + // ie. it should only return maker/taker assetDatas that are specified + orderProviderResponseProcessor.throwIfInvalidResponse(response, request); + // process the responses into one object + const isMakerAssetZrxToken = assetData === zrxTokenAssetData; + const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( + response, + isMakerAssetZrxToken, + this.expiryBufferSeconds, + this._contractWrappers.orderValidator, + ); + const lastRefreshTime = Date.now(); + const updatedOrdersEntry = { + ordersAndFillableAmounts, + lastRefreshTime, + }; + this._ordersEntryMap[assetData] = updatedOrdersEntry; return ordersAndFillableAmounts; } /** diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 830b4d8b8..2da2724d7 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -5,7 +5,6 @@ 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 { AssetBuyerManager } from './asset_buyer_manager'; export { AssetBuyerError, AssetBuyerOpts, @@ -16,5 +15,4 @@ export { OrderProviderRequest, OrderProviderResponse, SignedOrderWithRemainingFillableMakerAssetAmount, - AssetBuyerManagerError, } from './types'; -- cgit From 42083e140833fa39d62158fe2bdc9bfd21ed8f17 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 2 Oct 2018 16:23:54 -0700 Subject: Fix linter errors --- packages/asset-buyer/src/asset_buyer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 53bbe427b..0bb757f52 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,4 +1,3 @@ -import { HttpClient } from '@0xproject/connect'; import { ContractWrappers } from '@0xproject/contract-wrappers'; import { schemas } from '@0xproject/json-schemas'; import { SignedOrder } from '@0xproject/order-utils'; @@ -239,6 +238,7 @@ export class AssetBuyer { const shouldRefresh = _.isUndefined(ordersEntryIfExists) || shouldForceOrderRefresh || + // tslint:disable:restrict-plus-operands ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now(); if (!shouldRefresh) { const result = ordersEntryIfExists.ordersAndFillableAmounts; -- cgit From efe173e4f709eb3a702acf77adf75bae8c1c9813 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 2 Oct 2018 16:30:46 -0700 Subject: Update README --- packages/asset-buyer/README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/asset-buyer/README.md b/packages/asset-buyer/README.md index 555b90957..81c7e1e85 100644 --- a/packages/asset-buyer/README.md +++ b/packages/asset-buyer/README.md @@ -14,30 +14,16 @@ 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 -- cgit