diff options
Diffstat (limited to 'packages/asset-buyer/src/asset_buyer.ts')
-rw-r--r-- | packages/asset-buyer/src/asset_buyer.ts | 259 |
1 files changed, 113 insertions, 146 deletions
diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 409e34e74..0bb757f52 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,6 +1,7 @@ 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 +12,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 +26,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 +53,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 +153,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 +223,55 @@ 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 || + // tslint:disable:restrict-plus-operands + ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now(); + if (!shouldRefresh) { + const result = ordersEntryIfExists.ordersAndFillableAmounts; + return result; + } const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); - // construct 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; } /** |