diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index b0c951d035..0a4f6568f8 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -11,29 +11,28 @@ import { constants } from './constants'; import { BasicOrderProvider } from './order_providers/basic_order_provider'; import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; import { - AssetBuyerError, - AssetBuyerOpts, - BuyQuote, - BuyQuoteExecutionOpts, - BuyQuoteRequestOpts, + AssetSwapQuoterError, + AssetSwapQuoterOpts, LiquidityForAssetData, LiquidityRequestOpts, OrderProvider, OrdersAndFillableAmounts, + SwapQuote, + SwapQuoteRequestOpts, } from './types'; import { assert } from './utils/assert'; import { assetDataUtils } from './utils/asset_data_utils'; -import { buyQuoteCalculator } from './utils/buy_quote_calculator'; import { calculateLiquidity } from './utils/calculate_liquidity'; import { orderProviderResponseProcessor } from './utils/order_provider_response_processor'; +import { swapQuoteCalculator } from './utils/swap_quote_calculator'; interface OrdersEntry { ordersAndFillableAmounts: OrdersAndFillableAmounts; lastRefreshTime: number; } -export class AssetBuyer { +export class AssetSwapQuoter { public readonly provider: ZeroExProvider; public readonly orderProvider: OrderProvider; public readonly networkId: number; @@ -44,7 +43,7 @@ export class AssetBuyer { private readonly _ordersEntryMap: ObjectMap = {}; /** * Instantiates a new AssetBuyer instance given existing liquidity in the form of orders and feeOrders. - * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. + * @param supportedProvider The Provider instance you would like to use for ikknteracting 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 the AssetBuyer. See type definition for details. @@ -54,12 +53,12 @@ export class AssetBuyer { public static getAssetBuyerForProvidedOrders( supportedProvider: SupportedProvider, orders: SignedOrder[], - options: Partial = {}, - ): AssetBuyer { + options: Partial = {}, + ): AssetSwapQuoter { assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); const orderProvider = new BasicOrderProvider(orders); - const assetBuyer = new AssetBuyer(supportedProvider, orderProvider, options); + const assetBuyer = new AssetSwapQuoter(supportedProvider, orderProvider, options); return assetBuyer; } /** @@ -73,13 +72,13 @@ export class AssetBuyer { public static getAssetBuyerForStandardRelayerAPIUrl( supportedProvider: SupportedProvider, sraApiUrl: string, - options: Partial = {}, - ): AssetBuyer { + options: Partial = {}, + ): AssetSwapQuoter { const provider = providerUtils.standardizeOrThrow(supportedProvider); assert.isWebUri('sraApiUrl', sraApiUrl); - const networkId = options.networkId || constants.DEFAULT_ASSET_BUYER_OPTS.networkId; + const networkId = options.networkId || constants.DEFAULT_ASSET_SWAP_QUOTER_OPTS.networkId; const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl, networkId); - const assetBuyer = new AssetBuyer(provider, orderProvider, options); + const assetBuyer = new AssetSwapQuoter(provider, orderProvider, options); return assetBuyer; } /** @@ -93,11 +92,11 @@ export class AssetBuyer { constructor( supportedProvider: SupportedProvider, orderProvider: OrderProvider, - options: Partial = {}, + options: Partial = {}, ) { const { networkId, orderRefreshIntervalMs, expiryBufferSeconds } = _.merge( {}, - constants.DEFAULT_ASSET_BUYER_OPTS, + constants.DEFAULT_ASSET_SWAP_QUOTER_OPTS, options, ); const provider = providerUtils.standardizeOrThrow(supportedProvider); @@ -115,23 +114,23 @@ export class AssetBuyer { }); } /** - * 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. + * Get a `SwapQuote` containing all information relevant to fulfilling a buy given a desired assetData. + * You can then pass the `SwapQuote` to `executeSwapQuoteAsync` 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. + * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. */ - public async getBuyQuoteAsync( + public async getSwapQuoteAsync( makerAssetData: string, takerAssetData: string, makerAssetBuyAmount: BigNumber, - options: Partial = {}, - ): Promise { + options: Partial = {}, + ): Promise { const { shouldForceOrderRefresh, slippagePercentage } = _.merge( {}, - constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS, + constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options, ); assert.isString('makerAssetData', makerAssetData); @@ -151,39 +150,39 @@ export class AssetBuyer { shouldForceOrderRefresh, ]); if (ordersAndFillableAmounts.orders.length === 0) { - throw new Error(`${AssetBuyerError.AssetUnavailable}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`); + throw new Error(`${AssetSwapQuoterError.AssetUnavailable}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`); } - const buyQuote = buyQuoteCalculator.calculate( + const swapQuote = swapQuoteCalculator.calculate( ordersAndFillableAmounts, feeOrdersAndFillableAmounts, makerAssetBuyAmount, slippagePercentage, isMakerAssetZrxToken, ); - return buyQuote; + return swapQuote; } /** - * 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. + * Get a `SwapQuote` containing all information relevant to fulfilling a buy given a desired ERC20 token address. + * You can then pass the `SwapQuote` to `executeSwapQuoteAsync` 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. + * @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information. */ - public async getBuyQuoteForERC20TokenAddressAsync( + public async getSwapQuoteForERC20TokenAddressAsync( makerTokenAddress: string, takerTokenAddress: string, makerAssetBuyAmount: BigNumber, - options: Partial = {}, - ): Promise { + options: Partial = {}, + ): Promise { assert.isETHAddressHex('makerTokenAddress', makerTokenAddress); assert.isETHAddressHex('takerTokenAddress', takerTokenAddress); assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount); const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); - const buyQuote = this.getBuyQuoteAsync(makerAssetData, takerAssetData, makerAssetBuyAmount, options); - return buyQuote; + const swapQuote = this.getSwapQuoteAsync(makerAssetData, takerAssetData, makerAssetBuyAmount, options); + return swapQuote; } /** * Returns information about available liquidity for an asset @@ -224,13 +223,13 @@ 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 options Options for the execution of the BuyQuote. See type definition for more information. + // * Given a SwapQuote and desired rate, attempt to execute the buy. + // * @param SwapQuote An object that conforms to SwapQuote. See type definition for more information. + // * @param options Options for the execution of the SwapQuote. See type definition for more information. // * // * @return A promise of the txHash. // */ - // public async executeBuyQuoteAsync( + // public async executeSwapQuoteAsync( // buyQuote: BuyQuote, // options: Partial = {}, // ): Promise { @@ -401,13 +400,6 @@ export class AssetBuyer { return `${makerAssetData}_${takerAssetData}`; } - /** - * Get the assetData that represents the WETH token. - * Will throw if WETH does not exist for the current network. - */ - // private _getEtherTokenAssetDataOrThrow(): string { - // return assetDataUtils.getEtherTokenAssetData(this._contractWrappers); - // } /** * Get the assetData that represents the ZRX token. * Will throw if ZRX does not exist for the current network. diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index c0e1bf27d6..ad3a19ecb4 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -1,40 +1,35 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { AssetBuyerOpts, BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrdersAndFillableAmounts } from './types'; +import { AssetSwapQuoterOpts, ForwarderSwapQuoteExecutionOpts, OrdersAndFillableAmounts, SwapQuoteRequestOpts, SwapQuoteExecutionOpts } from './types'; const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; const MAINNET_NETWORK_ID = 1; -const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = { +const DEFAULT_ASSET_SWAP_QUOTER_OPTS: AssetSwapQuoterOpts = { networkId: MAINNET_NETWORK_ID, orderRefreshIntervalMs: 10000, // 10 seconds expiryBufferSeconds: 120, // 2 minutes }; -const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = { - feePercentage: 0, +const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = { shouldForceOrderRefresh: false, - slippagePercentage: 0.2, // 20% slippage protection -}; - -// Other default values are dynamically determined -const DEFAULT_BUY_QUOTE_EXECUTION_OPTS: BuyQuoteExecutionOpts = { - feeRecipient: NULL_ADDRESS, + slippagePercentage: 0.2, // 20% slippage protection, + allowMarketBuyOrders: true, }; const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[], }; - + export const constants = { ZERO_AMOUNT: new BigNumber(0), NULL_ADDRESS, MAINNET_NETWORK_ID, ETHER_TOKEN_DECIMALS: 18, - DEFAULT_ASSET_BUYER_OPTS, - DEFAULT_BUY_QUOTE_EXECUTION_OPTS, - DEFAULT_BUY_QUOTE_REQUEST_OPTS, + ONE_AMOUNT: new BigNumber(1), + DEFAULT_ASSET_SWAP_QUOTER_OPTS, + DEFAULT_SWAP_QUOTE_REQUEST_OPTS, EMPTY_ORDERS_AND_FILLABLE_AMOUNTS, }; diff --git a/packages/asset-buyer/src/errors.ts b/packages/asset-buyer/src/errors.ts index ec5fe548c6..35eecf6a3d 100644 --- a/packages/asset-buyer/src/errors.ts +++ b/packages/asset-buyer/src/errors.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@0x/utils'; -import { AssetBuyerError } from './types'; +import { AssetSwapQuoterError } from './types'; /** * Error class representing insufficient asset liquidity @@ -14,7 +14,7 @@ export class InsufficientAssetLiquidityError extends Error { * @param amountAvailableToFill The amount availabe to fill (in base units) factoring in slippage */ constructor(amountAvailableToFill: BigNumber) { - super(AssetBuyerError.InsufficientAssetLiquidity); + super(AssetSwapQuoterError.InsufficientAssetLiquidity); this.amountAvailableToFill = amountAvailableToFill; // Setting prototype so instanceof works. See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work Object.setPrototypeOf(this, InsufficientAssetLiquidityError.prototype); diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 9006dab307..70c6e853a3 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -18,19 +18,19 @@ export { export { SignedOrder } from '@0x/types'; export { BigNumber } from '@0x/utils'; -export { AssetBuyer } from './asset_buyer'; +export { AssetSwapQuoter } from './asset_buyer'; export { InsufficientAssetLiquidityError } from './errors'; export { BasicOrderProvider } from './order_providers/basic_order_provider'; export { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; export { - AssetBuyerError, - AssetBuyerOpts, - BuyQuote, - BuyQuoteExecutionOpts, - BuyQuoteInfo, - BuyQuoteRequestOpts, + AssetSwapQuoterError, + AssetSwapQuoterOpts, + SwapQuote, + SwapQuoteExecutionOpts, + SwapQuoteInfo, + SwapQuoteRequestOpts, LiquidityForAssetData, LiquidityRequestOpts, OrdersAndFillableAmounts, diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts index 10b631431c..b8ab7449a4 100644 --- a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts +++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts @@ -5,7 +5,7 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { - AssetBuyerError, + AssetSwapQuoterError, OrderProvider, OrderProviderRequest, OrderProviderResponse, @@ -73,7 +73,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider { try { orderbook = await this._sraClient.getOrderbookAsync(orderbookRequest, requestOpts); } catch (err) { - throw new Error(AssetBuyerError.StandardRelayerApiError); + throw new Error(AssetSwapQuoterError.StandardRelayerApiError); } const apiOrders = orderbook.asks.records; const orders = StandardRelayerAPIOrderProvider._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi( @@ -101,7 +101,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider { try { response = await this._sraClient.getAssetPairsAsync(fullRequest); } catch (err) { - throw new Error(AssetBuyerError.StandardRelayerApiError); + throw new Error(AssetSwapQuoterError.StandardRelayerApiError); } return _.map(response.records, item => { if (item.assetDataA.assetData === takerAssetData) { @@ -129,7 +129,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider { try { response = await this._sraClient.getAssetPairsAsync(fullRequest); } catch (err) { - throw new Error(AssetBuyerError.StandardRelayerApiError); + throw new Error(AssetSwapQuoterError.StandardRelayerApiError); } return _.map(response.records, item => { if (item.assetDataA.assetData === makerAssetData) { diff --git a/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts b/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts new file mode 100644 index 0000000000..6a14599183 --- /dev/null +++ b/packages/asset-buyer/src/quote_consumers/forwarder_swap_quote_consumer.ts @@ -0,0 +1,172 @@ +import { ContractWrappers, ContractWrappersError, ForwarderWrapperError } from '@0x/contract-wrappers'; +import { BigNumber, providerUtils } from '@0x/utils'; +import { SupportedProvider, Web3Wrapper, ZeroExProvider } from '@0x/web3-wrapper'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { + AssetSwapQuoterError, + CalldataInformation, + SmartContractParams, + SwapQuote, + SwapQuoteConsumer, + SwapQuoteConsumerOpts, + SwapQuoteExecutionOpts, + SwapQuoteGetOutputOpts, + SwapQuoteInfo, + Web3TransactionParams} from '../types'; +import { assert } from '../utils/assert'; + +export interface ForwarderSwapQuoteGetOutputOpts extends SwapQuoteGetOutputOpts { + feePercentage: number; + feeRecipient: string; +} + +export const FORWARDER_SWAP_QUOTE_CONSUMER_OPTS = { + feePercentage: 0, + feeRecipient: constants.NULL_ADDRESS, +}; + +const addAffiliateFeeToSwapQuoteInfo = (quoteInfo: SwapQuoteInfo, feePercentage: number): SwapQuoteInfo => { + const newQuoteInfo = _.clone(quoteInfo); + const affiliateFeeAmount = newQuoteInfo.takerTokenAmount.multipliedBy(feePercentage).integerValue(BigNumber.ROUND_CEIL); + const newFeeAmount = newQuoteInfo.feeTakerTokenAmount.plus(affiliateFeeAmount); + newQuoteInfo.feeTakerTokenAmount = newFeeAmount; + newQuoteInfo.totalTakerTokenAmount = newFeeAmount.plus(newQuoteInfo.takerTokenAmount); + return newQuoteInfo; +}; + +const addAffiliateFeeToSwapQuote = (quote: SwapQuote, feePercentage: number): SwapQuote => { + const newQuote = _.clone(quote); + newQuote.bestCaseQuoteInfo = addAffiliateFeeToSwapQuoteInfo(newQuote.bestCaseQuoteInfo, feePercentage); + newQuote.worstCaseQuoteInfo = addAffiliateFeeToSwapQuoteInfo(newQuote.worstCaseQuoteInfo, feePercentage); + return newQuote; +}; + +export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer { + + public readonly provider: ZeroExProvider; + public readonly networkId: number; + + private readonly _contractWrappers: ContractWrappers; + + constructor( + supportedProvider: SupportedProvider, + options: Partial = {}, + ) { + const { networkId } = _.merge( + {}, + constants.DEFAULT_ASSET_SWAP_QUOTER_OPTS, + options, + ); + const provider = providerUtils.standardizeOrThrow(supportedProvider); + assert.isNumber('networkId', networkId); + this.provider = provider; + this.networkId = networkId; + this._contractWrappers = new ContractWrappers(this.provider, { + networkId, + }); + } + + public getCalldataOrThrowAsync = async (quote: SwapQuote, opts: Partial): Promise => { + + } + + public getWeb3TransactionParamsOrThrowAsync = async (quote: SwapQuote, opts: Partial): Promise => { + + } + + public getSmartContractParamsOrThrowAsync = async (quote: SwapQuote, opts: Partial): Promise => { + const { feeRecipient, feePercentage } = _.merge( + {}, + FORWARDER_SWAP_QUOTE_CONSUMER_OPTS, + opts, + ); + + assert.isValidSwapQuote('quote', quote); + assert.isNumber('feePercentage', feePercentage); + assert.isETHAddressHex('feeRecipient', feeRecipient); + + const swapQuoteWithFeeAdded = addAffiliateFeeToSwapQuote(quote, feePercentage); + + const { orders, feeOrders, makerAssetBuyAmount, worstCaseQuoteInfo } = swapQuoteWithFeeAdded; + + const params = { + orders: [], + makerAssetFillAmount: makerAssetBuyAmount, + signatures: [], + feeOrders: [], + feeSignatures: [], + feePercentage: [], + feeRecipient: [], + }; + } + + public executeSwapQuoteOrThrowAsync = async (quote: SwapQuote, opts: Partial): Promise => { + const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice, feePercentage } = _.merge( + {}, + FORWARDER_SWAP_QUOTE_CONSUMER_OPTS, + opts, + ); + + assert.isValidSwapQuote('quote', quote); + + if (ethAmount !== undefined) { + assert.isBigNumber('ethAmount', ethAmount); + } + if (takerAddress !== undefined) { + assert.isETHAddressHex('takerAddress', takerAddress); + } + assert.isETHAddressHex('feeRecipient', feeRecipient); + if (gasLimit !== undefined) { + assert.isNumber('gasLimit', gasLimit); + } + if (gasPrice !== undefined) { + assert.isBigNumber('gasPrice', gasPrice); + } + + const swapQuoteWithFeeAdded = addAffiliateFeeToSwapQuote(quote, feePercentage); + + const { orders, feeOrders, makerAssetBuyAmount, worstCaseQuoteInfo } = swapQuoteWithFeeAdded; + + let finalTakerAddress; + if (takerAddress !== undefined) { + finalTakerAddress = takerAddress; + } else { + const web3Wrapper = new Web3Wrapper(this.provider); + const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + const firstAvailableAddress = _.head(availableAddresses); + if (firstAvailableAddress !== undefined) { + finalTakerAddress = firstAvailableAddress; + } else { + throw new Error(AssetSwapQuoterError.NoAddressAvailable); + } + } + try { + const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + orders, + makerAssetBuyAmount, + finalTakerAddress, + ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount, + feeOrders, + feePercentage, + feeRecipient, + { + gasLimit, + gasPrice, + shouldValidate: true, + }, + ); + return txHash; + } catch (err) { + if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) { + throw new Error(AssetSwapQuoterError.SignatureRequestDenied); + } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) { + throw new Error(AssetSwapQuoterError.TransactionValueTooLow); + } else { + throw err; + } + } + } + +} diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 40d9d7bab0..be9b8b0aba 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -36,25 +36,70 @@ export interface OrderProvider { getAvailableTakerAssetDatasAsync: (makerAssetData: string) => Promise; } +export interface CalldataInformation { + calldataHexString: string; + to: string; + value: BigNumber; +} + +export interface Web3TransactionParams { + from: string; + to?: string; + value?: string; + gas?: string; + gasPrice?: string; + data?: string; + nonce?: string; +} + +export interface SmartContractParams { + params: { [name: string]: any }; + to: string; + value: BigNumber; +} + +export interface SwapQuoteConsumer { + getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial): Promise; + getWeb3TransactionParamsOrThrowAsync(quote: SwapQuote, opts: Partial): Promise; + getSmartContractParamsOrThrowAsync(quote: SwapQuote, opts: Partial): Promise; + executeSwapQuoteOrThrowAsync(quote: SwapQuote, opts: Partial): Promise; +} + +export interface SwapQuoteConsumerOpts { + networkId: number; +} + +export interface SwapQuoteGetOutputOpts {} + +/** + * ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount. + * takerAddress: The address to perform the buy. Defaults to the first available address from the provider. + * gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call. + * gasPrice: Gas price in Wei to use for a transaction + */ +export interface SwapQuoteExecutionOpts extends SwapQuoteConsumerOpts { + ethAmount?: BigNumber; + takerAddress?: string; + gasLimit?: number; + gasPrice?: BigNumber; +} + /** * assetData: String that represents a specific asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * assetBuyAmount: The amount of asset to buy. * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above. - * feePercentage: Optional affiliate fee percentage used to calculate the eth amounts above. * bestCaseQuoteInfo: Info about the best case price for the asset. * worstCaseQuoteInfo: Info about the worst case price for the asset. */ -export interface BuyQuote { +export interface SwapQuote { takerAssetData: string; makerAssetData: string; makerAssetBuyAmount: BigNumber; orders: SignedOrder[]; feeOrders: SignedOrder[]; - bestCaseQuoteInfo: BuyQuoteInfo; - worstCaseQuoteInfo: BuyQuoteInfo; - toAddress: string; // exchange address, coordinator address - isUsingCoordinator: boolean; + bestCaseQuoteInfo: SwapQuoteInfo; + worstCaseQuoteInfo: SwapQuoteInfo; } /** @@ -62,7 +107,7 @@ export interface BuyQuote { * feeEthAmount: The amount of eth required to pay the affiliate fee. * totalEthAmount: The total amount of eth required to complete the buy (filling orders, feeOrders, and paying affiliate fee). */ -export interface BuyQuoteInfo { +export interface SwapQuoteInfo { takerTokenAmount: BigNumber; feeTakerTokenAmount: BigNumber; totalTakerTokenAmount: BigNumber; @@ -72,9 +117,10 @@ export interface BuyQuoteInfo { * 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 { +export interface SwapQuoteRequestOpts { shouldForceOrderRefresh: boolean; slippagePercentage: number; + allowMarketBuyOrders: boolean; } /* @@ -82,21 +128,10 @@ export interface BuyQuoteRequestOpts { * * shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false. */ -export type LiquidityRequestOpts = Pick; +export type LiquidityRequestOpts = Pick; -/** - * ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount. - * takerAddress: The address to perform the buy. Defaults to the first available address from the provider. - * gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call. - * gasPrice: Gas price in Wei to use for a transaction - * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000). - */ -export interface BuyQuoteExecutionOpts { - ethAmount?: BigNumber; - takerAddress?: string; - gasLimit?: number; - gasPrice?: BigNumber; - feeRecipient: string; +export interface ForwarderSwapQuoteExecutionOpts extends SwapQuoteExecutionOpts { + feeRecipient?: string; } /** @@ -104,7 +139,7 @@ export interface BuyQuoteExecutionOpts { * 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 300s (5m). */ -export interface AssetBuyerOpts { +export interface AssetSwapQuoterOpts { networkId: number; orderRefreshIntervalMs: number; expiryBufferSeconds: number; @@ -113,7 +148,7 @@ export interface AssetBuyerOpts { /** * Possible error messages thrown by an AssetBuyer instance or associated static methods. */ -export enum AssetBuyerError { +export enum AssetSwapQuoterError { NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND', StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 7bbceeaf82..444e61de47 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -1,29 +1,27 @@ import { assert as sharedAssert } from '@0x/assert'; import { schemas } from '@0x/json-schemas'; -import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../types'; +import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types'; export const assert = { ...sharedAssert, - isValidBuyQuote(variableName: string, buyQuote: BuyQuote): void { - sharedAssert.isHexString(`${variableName}.takerAssetData`, buyQuote.takerAssetData); - sharedAssert.isHexString(`${variableName}.makerAssetData`, buyQuote.makerAssetData); - sharedAssert.doesConformToSchema(`${variableName}.orders`, buyQuote.orders, schemas.signedOrdersSchema); - sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, buyQuote.feeOrders, schemas.signedOrdersSchema); - assert.isValidBuyQuoteInfo(`${variableName}.bestCaseQuoteInfo`, buyQuote.bestCaseQuoteInfo); - assert.isValidBuyQuoteInfo(`${variableName}.worstCaseQuoteInfo`, buyQuote.worstCaseQuoteInfo); - sharedAssert.isBigNumber(`${variableName}.makerAssetBuyAmount`, buyQuote.makerAssetBuyAmount); - assert.isETHAddressHex(`${variableName}.toAddress`, buyQuote.toAddress); - assert.isBoolean(`${variableName}.isUsingCoordinator`, buyQuote.isUsingCoordinator); + isValidSwapQuote(variableName: string, swapQuote: SwapQuote): void { + sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData); + sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData); + sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema); + sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, swapQuote.feeOrders, schemas.signedOrdersSchema); + assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo); + assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo); + sharedAssert.isBigNumber(`${variableName}.makerAssetBuyAmount`, swapQuote.makerAssetBuyAmount); // TODO(dave4506) Remove once forwarder features are reimplemented // if (buyQuote.feePercentage !== undefined) { // sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); // } }, - isValidBuyQuoteInfo(variableName: string, buyQuoteInfo: BuyQuoteInfo): void { - sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, buyQuoteInfo.takerTokenAmount); - sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, buyQuoteInfo.feeTakerTokenAmount); - sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, buyQuoteInfo.totalTakerTokenAmount); + isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void { + sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount); + sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, swapQuoteInfo.feeTakerTokenAmount); + sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, swapQuoteInfo.totalTakerTokenAmount); }, isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void { sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync); 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 cb25f98701..633df1227a 100644 --- a/packages/asset-buyer/src/utils/order_provider_response_processor.ts +++ b/packages/asset-buyer/src/utils/order_provider_response_processor.ts @@ -7,7 +7,7 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { - AssetBuyerError, + AssetSwapQuoterError, OrderProviderRequest, OrderProviderResponse, OrdersAndFillableAmounts, @@ -19,7 +19,7 @@ export const orderProviderResponseProcessor = { const { makerAssetData, takerAssetData } = request; _.forEach(response.orders, order => { if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) { - throw new Error(AssetBuyerError.InvalidOrderProviderResponse); + throw new Error(AssetSwapQuoterError.InvalidOrderProviderResponse); } }); }, diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/swap_quote_calculator.ts similarity index 95% rename from packages/asset-buyer/src/utils/buy_quote_calculator.ts rename to packages/asset-buyer/src/utils/swap_quote_calculator.ts index 875fbfc9d9..2f7b273c52 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/swap_quote_calculator.ts @@ -4,17 +4,17 @@ import * as _ from 'lodash'; import { constants } from '../constants'; import { InsufficientAssetLiquidityError } from '../errors'; -import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types'; +import { AssetSwapQuoterError, OrdersAndFillableAmounts, SwapQuote, SwapQuoteInfo } from '../types'; -// Calculates a buy quote for orders that have WETH as the takerAsset -export const buyQuoteCalculator = { +// Calculates a swap quote for orders +export const swapQuoteCalculator = { calculate( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, makerAssetBuyAmount: BigNumber, slippagePercentage: number, isMakerAssetZrxToken: boolean, - ): BuyQuote { + ): SwapQuote { const orders = ordersAndFillableAmounts.orders; const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; const feeOrders = feeOrdersAndFillableAmounts.orders; @@ -64,7 +64,7 @@ export const buyQuoteCalculator = { ); // if we do not have enough feeOrders to cover the fees, throw if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { - throw new Error(AssetBuyerError.InsufficientZrxLiquidity); + throw new Error(AssetSwapQuoterError.InsufficientZrxLiquidity); } resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders; feeOrdersRemainingFillableMakerAssetAmounts = @@ -106,9 +106,6 @@ export const buyQuoteCalculator = { feeOrders: resultFeeOrders, bestCaseQuoteInfo, worstCaseQuoteInfo, - // TODO(dave4506): coordinator metadata for buy quote - toAddress: constants.NULL_ADDRESS, - isUsingCoordinator: false, }; }, }; @@ -118,7 +115,7 @@ function calculateQuoteInfo( feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, makserAssetBuyAmount: BigNumber, isMakerAssetZrxToken: boolean, -): BuyQuoteInfo { +): SwapQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right let takerTokenAmount = constants.ZERO_AMOUNT; let zrxTakerTokenAmount = constants.ZERO_AMOUNT; @@ -132,7 +129,9 @@ function calculateQuoteInfo( // find eth amount needed to buy zrx zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); } + const feeTakerTokenAmount = zrxTakerTokenAmount; + // eth amount needed in total is the sum of the amount needed for the asset and the amount needed for fees const totalTakerTokenAmount = takerTokenAmount.plus(feeTakerTokenAmount); return { diff --git a/packages/asset-buyer/src/utils/utils.ts b/packages/asset-buyer/src/utils/utils.ts new file mode 100644 index 0000000000..8d7df1d8d2 --- /dev/null +++ b/packages/asset-buyer/src/utils/utils.ts @@ -0,0 +1,12 @@ +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; + +import { constants } from '../constants'; + +export const utils = { + numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber { + return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy( + percentage, + ); + }, +};