Files
protocol/packages/asset-buyer/src/swap_quoter.ts
David Sun 11d5fec59b changes on affiliate fee and other changes
bumped version to 7 and added changelog

updated package.json

revert change to package.json

trying this package.json config
2019-07-11 15:15:08 -07:00

347 lines
18 KiB
TypeScript

import { ContractWrappers, ContractWrappersError, ForwarderWrapperError } from '@0x/contract-wrappers';
import { schemas } from '@0x/json-schemas';
import { SignedOrder } from '@0x/order-utils';
import { ObjectMap } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from './constants';
import { BasicOrderProvider } from './order_providers/basic_order_provider';
import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider';
import {
LiquidityForAssetData,
LiquidityRequestOpts,
OrderProvider,
OrdersAndFillableAmounts,
SwapQuote,
SwapQuoteRequestOpts,
SwapQuoterError,
SwapQuoterOpts,
} from './types';
import { assert } from './utils/assert';
import { assetDataUtils } from './utils/asset_data_utils';
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 SwapQuoter {
public readonly provider: ZeroExProvider;
public readonly orderProvider: OrderProvider;
public readonly networkId: number;
public readonly orderRefreshIntervalMs: number;
public readonly expiryBufferMs: number;
private readonly _contractWrappers: ContractWrappers;
// cache of orders along with the time last updated keyed by assetData
private readonly _ordersEntryMap: ObjectMap<OrdersEntry> = {};
/**
* Instantiates a new SwapQuoter 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 orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData.
* @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData. Defaults to an empty array.
* @param options Initialization options for the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
public static getSwapQuoterForProvidedOrders(
supportedProvider: SupportedProvider,
orders: SignedOrder[],
options: Partial<SwapQuoterOpts> = {},
): SwapQuoter {
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 swapQuoter = new SwapQuoter(supportedProvider, orderProvider, options);
return swapQuoter;
}
/**
* Instantiates a new SwapQuoter instance given a [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) endpoint
* @param supportedProvider 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 the SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
public static getSwapQuoterForStandardRelayerAPIUrl(
supportedProvider: SupportedProvider,
sraApiUrl: string,
options: Partial<SwapQuoterOpts> = {},
): SwapQuoter {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isWebUri('sraApiUrl', sraApiUrl);
const networkId = options.networkId || constants.DEFAULT_SWAP_QUOTER_OPTS.networkId;
const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl, networkId);
const swapQuoter = new SwapQuoter(provider, orderProvider, options);
return swapQuoter;
}
/**
*
* get the key for _orderEntryMap for maker + taker asset pair
*/
private static _getOrdersEntryMapKey(makerAssetData: string, takerAssetData: string): string {
return `${makerAssetData}_${takerAssetData}`;
}
/**
* Instantiates a new SwapQuoter instance
* @param supportedProvider 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 SwapQuoter. See type definition for details.
*
* @return An instance of SwapQuoter
*/
constructor(
supportedProvider: SupportedProvider,
orderProvider: OrderProvider,
options: Partial<SwapQuoterOpts> = {},
) {
const { networkId, orderRefreshIntervalMs, expiryBufferMs } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTER_OPTS,
options,
);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderProvider('orderProvider', orderProvider);
assert.isNumber('networkId', networkId);
assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
assert.isNumber('expiryBufferMs', expiryBufferMs);
this.provider = provider;
this.orderProvider = orderProvider;
this.networkId = networkId;
this.orderRefreshIntervalMs = orderRefreshIntervalMs;
this.expiryBufferMs = expiryBufferMs;
this._contractWrappers = new ContractWrappers(this.provider, {
networkId,
});
}
/**
* Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address.
* You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption.
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param makerAssetSwapAmount The amount of maker asset to swap for.
* @param options Options for 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 getSwapQuoteAsync(
makerAssetData: string,
takerAssetData: string,
makerAssetSwapAmount: BigNumber,
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<SwapQuote> {
const { shouldForceOrderRefresh, slippagePercentage } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assert.isBigNumber('makerAssetSwapAmount', makerAssetSwapAmount);
assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh);
assert.isNumber('slippagePercentage', slippagePercentage);
const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
// get the relevant orders for the makerAsset and fees
// if the requested assetData is ZRX, don't get the fee info
const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([
this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData, shouldForceOrderRefresh),
isMakerAssetZrxToken
? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS)
: this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, takerAssetData, shouldForceOrderRefresh),
shouldForceOrderRefresh,
]);
if (ordersAndFillableAmounts.orders.length === 0) {
throw new Error(
`${
SwapQuoterError.AssetUnavailable
}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`,
);
}
const swapQuote = swapQuoteCalculator.calculate(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
makerAssetSwapAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
return swapQuote;
}
/**
* Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address.
* You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption.
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param makerAssetSwapAmount The amount of maker asset to swap for.
* @param options Options for 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 getSwapQuoteForERC20TokenAddressAsync(
makerTokenAddress: string,
takerTokenAddress: string,
makerAssetSwapAmount: BigNumber,
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<SwapQuote> {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('makerAssetSwapAmount', makerAssetSwapAmount);
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
const swapQuote = this.getSwapQuoteAsync(makerAssetData, takerAssetData, makerAssetSwapAmount, options);
return swapQuote;
}
/**
* Returns information about available liquidity for an asset
* Does not factor in slippage or fees
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param options Options for the request. See type definition for more information.
*
* @return An object that conforms to LiquidityForAssetData that satisfies the request. See type definition for more information.
*/
public async getLiquidityForMakerTakerAssetDataPairAsync(
makerAssetData: string,
takerAssetData: string,
options: Partial<LiquidityRequestOpts> = {},
): Promise<LiquidityForAssetData> {
const shouldForceOrderRefresh =
options.shouldForceOrderRefresh !== undefined ? options.shouldForceOrderRefresh : false;
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assert.isBoolean('options.shouldForceOrderRefresh', shouldForceOrderRefresh);
const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData);
if (!assetPairs.includes(makerAssetData)) {
return {
makerTokensAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0),
};
}
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(
makerAssetData,
takerAssetData,
shouldForceOrderRefresh,
);
return calculateLiquidity(ordersAndFillableAmounts);
}
/**
* Get the asset data of all assets that can be used to purchase makerAssetData in the order provider passed in at init.
*
* @return An array of asset data strings that can purchase makerAssetData.
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
assert.isString('makerAssetData', makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
return this.orderProvider.getAvailableTakerAssetDatasAsync(makerAssetData);
}
/**
* Get the asset data of all assets that are purchaseable with takerAssetData in the order provider passed in at init.
*
* @return An array of asset data strings that are purchaseable with takerAssetData.
*/
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
return this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData);
}
/**
* Validates the taker + maker asset pair is available from the order provider provided to `SwapQuote`.
*
* @return A boolean on if the taker, maker pair exists
*/
public async isTakerMakerAssetDataPairAvailableAsync(
makerAssetData: string,
takerAssetData: string,
): Promise<boolean> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
const availableMakerAssetDatas = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
return _.includes(availableMakerAssetDatas, makerAssetData);
}
/**
* Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param shouldForceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs.
*/
public async getOrdersAndFillableAmountsAsync(
makerAssetData: string,
takerAssetData: string,
shouldForceOrderRefresh: boolean,
): Promise<OrdersAndFillableAmounts> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
// try to get ordersEntry from the map
const ordersEntryIfExists = this._ordersEntryMap[SwapQuoter._getOrdersEntryMapKey(makerAssetData, takerAssetData)];
// 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 =
ordersEntryIfExists === undefined ||
shouldForceOrderRefresh ||
// tslint:disable:restrict-plus-operands
ordersEntryIfExists.lastRefreshTime + this.orderRefreshIntervalMs < Date.now();
if (!shouldRefresh) {
const result = ordersEntryIfExists.ordersAndFillableAmounts;
return result;
}
const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
// construct orderProvider request
const orderProviderRequest = {
makerAssetData,
takerAssetData,
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 = makerAssetData === zrxTokenAssetData;
const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync(
response,
isMakerAssetZrxToken,
this.expiryBufferMs,
this._contractWrappers.orderValidator,
);
const lastRefreshTime = Date.now();
const updatedOrdersEntry = {
ordersAndFillableAmounts,
lastRefreshTime,
};
this._ordersEntryMap[SwapQuoter._getOrdersEntryMapKey(makerAssetData, takerAssetData)] = updatedOrdersEntry;
return ordersAndFillableAmounts;
}
/**
* Get the assetData that represents the ZRX token.
* Will throw if ZRX does not exist for the current network.
*/
private _getZrxTokenAssetDataOrThrow(): string {
return this._contractWrappers.exchange.getZRXAssetData();
}
}