refactored ERC <> ERC for asset-buyer

minor update to interface
This commit is contained in:
David Sun
2019-05-31 13:47:38 -07:00
parent 2fdc0426ff
commit 64e3b6f5ee
10 changed files with 287 additions and 187 deletions

View File

@@ -4,7 +4,7 @@
"engines": {
"node": ">=6.12"
},
"description": "Convenience package for discovering and buying assets with Ether.",
"description": "Convenience package for discovering and buying assets for both on chain and off chain needs.",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"scripts": {

View File

@@ -124,39 +124,39 @@ export class AssetBuyer {
* @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
*/
public async getBuyQuoteAsync(
assetData: string,
assetBuyAmount: BigNumber,
makerAssetData: string,
takerAssetData: string,
makerAssetBuyAmount: BigNumber,
options: Partial<BuyQuoteRequestOpts> = {},
): Promise<BuyQuote> {
const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = _.merge(
const { shouldForceOrderRefresh, slippagePercentage } = _.merge(
{},
constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('assetData', assetData);
assert.isBigNumber('assetBuyAmount', assetBuyAmount);
assert.isValidPercentage('feePercentage', feePercentage);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount);
assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh);
assert.isNumber('slippagePercentage', slippagePercentage);
const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
const isMakerAssetZrxToken = assetData === zrxTokenAssetData;
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(assetData, shouldForceOrderRefresh),
this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData, shouldForceOrderRefresh),
isMakerAssetZrxToken
? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS)
: this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh),
: this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, takerAssetData, shouldForceOrderRefresh),
shouldForceOrderRefresh,
]);
if (ordersAndFillableAmounts.orders.length === 0) {
throw new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${assetData}`);
throw new Error(`${AssetBuyerError.AssetUnavailable}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`);
}
const buyQuote = buyQuoteCalculator.calculate(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
assetBuyAmount,
feePercentage,
makerAssetBuyAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
@@ -172,14 +172,17 @@ export class AssetBuyer {
* @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
*/
public async getBuyQuoteForERC20TokenAddressAsync(
tokenAddress: string,
assetBuyAmount: BigNumber,
makerTokenAddress: string,
takerTokenAddress: string,
makerAssetBuyAmount: 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);
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;
}
/**
@@ -190,125 +193,162 @@ export class AssetBuyer {
*
* @return An object that conforms to LiquidityForAssetData that satisfies the request. See type definition for more information.
*/
public async getLiquidityForAssetDataAsync(
assetData: string,
public async getLiquidityForMakerTakerAssetdataPairAsync(
makerAssetData: string,
takerAssetData: string,
options: Partial<LiquidityRequestOpts> = {},
): Promise<LiquidityForAssetData> {
const shouldForceOrderRefresh =
options.shouldForceOrderRefresh !== undefined ? options.shouldForceOrderRefresh : false;
assert.isString('assetData', assetData);
assetDataUtils.decodeAssetDataOrThrow(assetData);
assert.isString('makerAssetDataa', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
assert.isBoolean('options.shouldForceOrderRefresh', shouldForceOrderRefresh);
const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(assetData);
const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow();
if (!assetPairs.includes(etherTokenAssetData)) {
const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData);
if (!assetPairs.includes(makerAssetData)) {
return {
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
makerTokensAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0),
};
}
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(
assetData,
makerAssetData,
takerAssetData,
shouldForceOrderRefresh,
);
return calculateLiquidity(ordersAndFillableAmounts);
}
// /**
// * 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.
// *
// * @return A promise of the txHash.
// */
// public async executeBuyQuoteAsync(
// buyQuote: BuyQuote,
// options: Partial<BuyQuoteExecutionOpts> = {},
// ): Promise<string> {
// const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = _.merge(
// {},
// constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
// options,
// );
// assert.isValidBuyQuote('buyQuote', buyQuote);
// 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 { orders, feeOrders, makerAssetBuyAmount, worstCaseQuoteInfo } = buyQuote;
// // TODO(dave4506) upgrade logic for asset-buyer2.0
// // if no takerAddress is provided, try to get one from the provider
// // 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(AssetBuyerError.NoAddressAvailable);
// // }
// // }
// // try {
// // // if no ethAmount is provided, default to the worst ethAmount from buyQuote
// // const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
// // orders,
// // assetBuyAmount,
// // finalTakerAddress,
// // ethAmount || worstCaseQuoteInfo.totalEthAmount,
// // feeOrders,
// // feePercentage,
// // feeRecipient,
// // {
// // gasLimit,
// // gasPrice,
// // shouldValidate: true,
// // },
// // );
// // return txHash;
// // } catch (err) {
// // if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) {
// // throw new Error(AssetBuyerError.SignatureRequestDenied);
// // } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) {
// // throw new Error(AssetBuyerError.TransactionValueTooLow);
// // } else {
// // throw err;
// // }
// // }
// return Promise.resolve('test');
// }
/**
* 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.
*
* @return A promise of the txHash.
*/
public async executeBuyQuoteAsync(
buyQuote: BuyQuote,
options: Partial<BuyQuoteExecutionOpts> = {},
): Promise<string> {
const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = _.merge(
{},
constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
options,
);
assert.isValidBuyQuote('buyQuote', buyQuote);
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 { orders, feeOrders, feePercentage, assetBuyAmount, worstCaseQuoteInfo } = buyQuote;
// if no takerAddress is provided, try to get one from the provider
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(AssetBuyerError.NoAddressAvailable);
}
}
try {
// if no ethAmount is provided, default to the worst ethAmount from buyQuote
const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
orders,
assetBuyAmount,
finalTakerAddress,
ethAmount || worstCaseQuoteInfo.totalEthAmount,
feeOrders,
feePercentage,
feeRecipient,
{
gasLimit,
gasPrice,
shouldValidate: true,
},
);
return txHash;
} catch (err) {
if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) {
throw new Error(AssetBuyerError.SignatureRequestDenied);
} else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) {
throw new Error(AssetBuyerError.TransactionValueTooLow);
} else {
throw err;
}
}
}
/**
* Get the asset data of all assets that are purchaseable with ether token (wETH) in the order provider passed in at init.
* 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 be purchased using wETH.
*/
public async getAvailableAssetDatasAsync(): Promise<string[]> {
const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow();
return this.orderProvider.getAvailableMakerAssetDatasAsync(etherTokenAssetData);
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
assert.isString('makerAssetDataa', makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
return this.orderProvider.getAvailableTakerAssetDatasAsync(makerAssetData);
}
/**
* Get the asset data of all assets that are purchaseable with makerAssetData in the order provider passed in at init.
*
* @return An array of asset data strings that can be purchased using wETH.
*/
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
return this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData);
}
/**
* validates that the taker + maker asset pair is availalbe from the order provider passed
*
* @return A boolean on if the taker + maker pair exists
*/
public async isTakerMakerAssetDataPairAvailableAsync(makerAssetData: string, takerAssetData: string): Promise<boolean> {
assert.isString('makerAssetDataa', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
return _.findIndex(await this.getAvailableMakerAssetDatasAsync(takerAssetData), makerAssetData) !== -1;
}
/**
* Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders
* @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 shouldForceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs.
*/
public async getOrdersAndFillableAmountsAsync(
assetData: string,
makerAssetData: string,
takerAssetData: string,
shouldForceOrderRefresh: boolean,
): Promise<OrdersAndFillableAmounts> {
assert.isString('makerAssetDataa', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
// try to get ordersEntry from the map
const ordersEntryIfExists = this._ordersEntryMap[assetData];
const ordersEntryIfExists = this._ordersEntryMap[this._getOrdersEntryMapKey(makerAssetData, takerAssetData)];
// we should refresh if:
// we do not have any orders OR
// we are forced to OR
@@ -322,12 +362,11 @@ export class AssetBuyer {
const result = ordersEntryIfExists.ordersAndFillableAmounts;
return result;
}
const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow();
const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow();
// construct orderProvider request
const orderProviderRequest = {
makerAssetData: assetData,
takerAssetData: etherTokenAssetData,
makerAssetData,
takerAssetData,
networkId: this.networkId,
};
const request = orderProviderRequest;
@@ -337,7 +376,7 @@ export class AssetBuyer {
// 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 isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync(
response,
isMakerAssetZrxToken,
@@ -349,16 +388,26 @@ export class AssetBuyer {
ordersAndFillableAmounts,
lastRefreshTime,
};
this._ordersEntryMap[assetData] = updatedOrdersEntry;
this._ordersEntryMap[this._getOrdersEntryMapKey(makerAssetData, takerAssetData)] = updatedOrdersEntry;
return ordersAndFillableAmounts;
}
/**
*
* get the key for _orderEntryMap for maker + taker asset pair
*/
// tslint:disable-next-line: prefer-function-over-method
private _getOrdersEntryMapKey(makerAssetData: string, takerAssetData: string): string {
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);
}
// 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.

View File

@@ -13,13 +13,17 @@ export {
Web3JsV2Provider,
Web3JsV3Provider,
} from 'ethereum-types';
// TODO(dave4506): if this lives under the 0x.js library, then these type exports should be removed in favor of minimizing redundancy
export { SignedOrder } from '@0x/types';
export { BigNumber } from '@0x/utils';
export { AssetBuyer } 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,

View File

@@ -38,4 +38,13 @@ export class BasicOrderProvider implements OrderProvider {
const ordersWithTakerAssetData = _.filter(this.orders, { takerAssetData });
return _.map(ordersWithTakerAssetData, order => order.makerAssetData);
}
/**
* Given a maker asset data string, return all availabled paired taker asset data strings.
* @param makerAssetData A string representing the maker asset data.
* @return An array of asset data strings that can be used to purchased makerAssetData.
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
const ordersWithMakerAssetData = _.filter(this.orders, { makerAssetData });
return _.map(ordersWithMakerAssetData, order => order.takerAssetData);
}
}

View File

@@ -111,4 +111,32 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider {
}
});
}
/**
* Given a maker asset data string, return all availabled paired taker asset data strings.
* @param makerAssetData A string representing the maker asset data.
* @return An array of asset data strings that can be used to purchased makerAssetData.
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
// Return a maximum of 1000 asset datas
const maxPerPage = 1000;
const requestOpts = { networkId: this.networkId, perPage: maxPerPage };
const assetPairsRequest = { assetDataA: makerAssetData };
const fullRequest = {
...requestOpts,
...assetPairsRequest,
};
let response: AssetPairsResponse;
try {
response = await this._sraClient.getAssetPairsAsync(fullRequest);
} catch (err) {
throw new Error(AssetBuyerError.StandardRelayerApiError);
}
return _.map(response.records, item => {
if (item.assetDataA.assetData === makerAssetData) {
return item.assetDataB.assetData;
} else {
return item.assetDataA.assetData;
}
});
}
}

View File

@@ -25,6 +25,7 @@ export interface OrderProviderResponse {
export interface SignedOrderWithRemainingFillableMakerAssetAmount extends SignedOrder {
remainingFillableMakerAssetAmount?: BigNumber;
}
/**
* gerOrdersAsync: Given an OrderProviderRequest, get an OrderProviderResponse.
* getAvailableMakerAssetDatasAsync: Given a taker asset data string, return all availabled paired maker asset data strings.
@@ -32,6 +33,7 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed
export interface OrderProvider {
getOrdersAsync: (orderProviderRequest: OrderProviderRequest) => Promise<OrderProviderResponse>;
getAvailableMakerAssetDatasAsync: (takerAssetData: string) => Promise<string[]>;
getAvailableTakerAssetDatasAsync: (makerAssetData: string) => Promise<string[]>;
}
/**
@@ -44,13 +46,15 @@ export interface OrderProvider {
* worstCaseQuoteInfo: Info about the worst case price for the asset.
*/
export interface BuyQuote {
assetData: string;
assetBuyAmount: BigNumber;
takerAssetData: string;
makerAssetData: string;
makerAssetBuyAmount: BigNumber;
orders: SignedOrder[];
feeOrders: SignedOrder[];
feePercentage?: number;
bestCaseQuoteInfo: BuyQuoteInfo;
worstCaseQuoteInfo: BuyQuoteInfo;
toAddress: string; // exchange address, coordinator address
isUsingCoordinator: boolean;
}
/**
@@ -59,18 +63,16 @@ export interface BuyQuote {
* totalEthAmount: The total amount of eth required to complete the buy (filling orders, feeOrders, and paying affiliate fee).
*/
export interface BuyQuoteInfo {
assetEthAmount: BigNumber;
feeEthAmount: BigNumber;
totalEthAmount: BigNumber;
takerTokenAmount: BigNumber;
feeTakerTokenAmount: BigNumber;
totalTakerTokenAmount: BigNumber;
}
/**
* 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;
}
@@ -137,6 +139,6 @@ export interface OrdersAndFillableAmounts {
* Represents available liquidity for a given assetData
*/
export interface LiquidityForAssetData {
tokensAvailableInBaseUnits: BigNumber;
ethValueAvailableInWei: BigNumber;
makerTokensAvailableInBaseUnits: BigNumber;
takerTokensAvailableInBaseUnits: BigNumber;
}

View File

@@ -6,20 +6,24 @@ import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../
export const assert = {
...sharedAssert,
isValidBuyQuote(variableName: string, buyQuote: BuyQuote): void {
sharedAssert.isHexString(`${variableName}.assetData`, buyQuote.assetData);
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}.assetBuyAmount`, buyQuote.assetBuyAmount);
if (buyQuote.feePercentage !== undefined) {
sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage);
}
sharedAssert.isBigNumber(`${variableName}.makerAssetBuyAmount`, buyQuote.makerAssetBuyAmount);
assert.isETHAddressHex(`${variableName}.toAddress`, buyQuote.toAddress);
assert.isBoolean(`${variableName}.isUsingCoordinator`, buyQuote.isUsingCoordinator);
// 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}.assetEthAmount`, buyQuoteInfo.assetEthAmount);
sharedAssert.isBigNumber(`${variableName}.feeEthAmount`, buyQuoteInfo.feeEthAmount);
sharedAssert.isBigNumber(`${variableName}.totalEthAmount`, buyQuoteInfo.totalEthAmount);
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, buyQuoteInfo.takerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, buyQuoteInfo.feeTakerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, buyQuoteInfo.totalTakerTokenAmount);
},
isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);

View File

@@ -11,8 +11,7 @@ export const buyQuoteCalculator = {
calculate(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
assetBuyAmount: BigNumber,
feePercentage: number,
makerAssetBuyAmount: BigNumber,
slippagePercentage: number,
isMakerAssetZrxToken: boolean,
): BuyQuote {
@@ -20,20 +19,20 @@ export const buyQuoteCalculator = {
const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const feeOrders = feeOrdersAndFillableAmounts.orders;
const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const slippageBufferAmount = assetBuyAmount.multipliedBy(slippagePercentage).integerValue();
const slippageBufferAmount = makerAssetBuyAmount.multipliedBy(slippagePercentage).integerValue();
// find the orders that cover the desired assetBuyAmount (with slippage)
const {
resultOrders,
remainingFillAmount,
ordersRemainingFillableMakerAssetAmounts,
} = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetBuyAmount, {
} = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, makerAssetBuyAmount, {
remainingFillableMakerAssetAmounts,
slippageBufferAmount,
});
// if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
// We needed the amount they requested to buy, plus the amount for slippage
const totalAmountRequested = assetBuyAmount.plus(slippageBufferAmount);
const totalAmountRequested = makerAssetBuyAmount.plus(slippageBufferAmount);
const amountAbleToFill = totalAmountRequested.minus(remainingFillAmount);
// multiplierNeededWithSlippage represents what we need to multiply the assetBuyAmount by
// in order to get the total amount needed considering slippage
@@ -73,7 +72,9 @@ export const buyQuoteCalculator = {
}
// assetData information for the result
const assetData = orders[0].makerAssetData;
const takerAssetData = orders[0].takerAssetData;
const makerAssetData = orders[0].makerAssetData;
// compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount
const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultOrders,
@@ -86,26 +87,28 @@ export const buyQuoteCalculator = {
const bestCaseQuoteInfo = calculateQuoteInfo(
trimmedOrdersAndFillableAmounts,
trimmedFeeOrdersAndFillableAmounts,
assetBuyAmount,
feePercentage,
makerAssetBuyAmount,
isMakerAssetZrxToken,
);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = calculateQuoteInfo(
reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts),
reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts),
assetBuyAmount,
feePercentage,
makerAssetBuyAmount,
isMakerAssetZrxToken,
);
return {
assetData,
takerAssetData,
makerAssetData,
makerAssetBuyAmount,
orders: resultOrders,
feeOrders: resultFeeOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
assetBuyAmount,
feePercentage,
// TODO(dave4506): coordinator metadata for buy quote
toAddress: constants.NULL_ADDRESS,
isUsingCoordinator: false,
};
},
};
@@ -113,33 +116,29 @@ export const buyQuoteCalculator = {
function calculateQuoteInfo(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
assetBuyAmount: BigNumber,
feePercentage: number,
makserAssetBuyAmount: BigNumber,
isMakerAssetZrxToken: boolean,
): BuyQuoteInfo {
// find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right
let assetEthAmount = constants.ZERO_AMOUNT;
let zrxEthAmount = constants.ZERO_AMOUNT;
let takerTokenAmount = constants.ZERO_AMOUNT;
let zrxTakerTokenAmount = constants.ZERO_AMOUNT;
if (isMakerAssetZrxToken) {
assetEthAmount = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount);
takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makserAssetBuyAmount);
} else {
// find eth and zrx amounts needed to buy
const ethAndZrxAmountToBuyAsset = findEthAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, assetBuyAmount);
assetEthAmount = ethAndZrxAmountToBuyAsset[0];
const zrxAmountToBuyAsset = ethAndZrxAmountToBuyAsset[1];
const takerTokenAndZrxAmountToBuyAsset = findTakerTokenAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, makserAssetBuyAmount);
takerTokenAmount = takerTokenAndZrxAmountToBuyAsset[0];
const zrxAmountToBuyAsset = takerTokenAndZrxAmountToBuyAsset[1];
// find eth amount needed to buy zrx
zrxEthAmount = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
}
// eth amount needed to buy the affiliate fee
const affiliateFeeEthAmount = assetEthAmount.multipliedBy(feePercentage).integerValue(BigNumber.ROUND_CEIL);
// eth amount needed for fees is the sum of affiliate fee and zrx fee
const feeEthAmount = affiliateFeeEthAmount.plus(zrxEthAmount);
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 totalEthAmount = assetEthAmount.plus(feeEthAmount);
const totalTakerTokenAmount = takerTokenAmount.plus(feeTakerTokenAmount);
return {
assetEthAmount,
feeEthAmount,
totalEthAmount,
takerTokenAmount,
feeTakerTokenAmount,
totalTakerTokenAmount,
};
}
@@ -153,7 +152,7 @@ function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFill
};
}
function findEthAmountNeededToBuyZrx(
function findTakerTokenAmountNeededToBuyZrx(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
zrxBuyAmount: BigNumber,
): BigNumber {
@@ -161,18 +160,19 @@ function findEthAmountNeededToBuyZrx(
const result = _.reduce(
orders,
(acc, order, index) => {
const { totalEthAmount, remainingZrxBuyAmount } = acc;
const { totalTakerTokenAmount, remainingZrxBuyAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const makerFillAmount = BigNumber.min(remainingZrxBuyAmount, remainingFillableMakerAssetAmount);
const [takerFillAmount, adjustedMakerFillAmount] = orderCalculationUtils.getTakerFillAmountForFeeOrder(
order,
makerFillAmount,
);
// TODO(dave4506) may remove if this is for affiliate fees (asset-buyer2.0)
const extraFeeAmount = remainingFillableMakerAssetAmount.isGreaterThanOrEqualTo(adjustedMakerFillAmount)
? constants.ZERO_AMOUNT
: adjustedMakerFillAmount.minus(makerFillAmount);
return {
totalEthAmount: totalEthAmount.plus(takerFillAmount),
totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount),
remainingZrxBuyAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingZrxBuyAmount.minus(makerFillAmount).plus(extraFeeAmount),
@@ -180,40 +180,40 @@ function findEthAmountNeededToBuyZrx(
};
},
{
totalEthAmount: constants.ZERO_AMOUNT,
totalTakerTokenAmount: constants.ZERO_AMOUNT,
remainingZrxBuyAmount: zrxBuyAmount,
},
);
return result.totalEthAmount;
return result.totalTakerTokenAmount;
}
function findEthAndZrxAmountNeededToBuyAsset(
function findTakerTokenAndZrxAmountNeededToBuyAsset(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
assetBuyAmount: BigNumber,
makerAssetBuyAmount: BigNumber,
): [BigNumber, BigNumber] {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
const result = _.reduce(
orders,
(acc, order, index) => {
const { totalEthAmount, totalZrxAmount, remainingAssetBuyAmount } = acc;
const { totalTakerTokenAmount, totalZrxAmount, remainingMakerAssetBuyAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const makerFillAmount = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount);
const makerFillAmount = BigNumber.min(acc.remainingMakerAssetBuyAmount, remainingFillableMakerAssetAmount);
const takerFillAmount = orderCalculationUtils.getTakerFillAmount(order, makerFillAmount);
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
return {
totalEthAmount: totalEthAmount.plus(takerFillAmount),
totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount),
totalZrxAmount: totalZrxAmount.plus(takerFeeAmount),
remainingAssetBuyAmount: BigNumber.max(
remainingMakerAssetBuyAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingAssetBuyAmount.minus(makerFillAmount),
remainingMakerAssetBuyAmount.minus(makerFillAmount),
),
};
},
{
totalEthAmount: constants.ZERO_AMOUNT,
totalTakerTokenAmount: constants.ZERO_AMOUNT,
totalZrxAmount: constants.ZERO_AMOUNT,
remainingAssetBuyAmount: assetBuyAmount,
remainingMakerAssetBuyAmount: makerAssetBuyAmount,
},
);
return [result.totalEthAmount, result.totalZrxAmount];
return [result.totalTakerTokenAmount, result.totalZrxAmount];
}

View File

@@ -12,25 +12,25 @@ export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAm
throw new Error(`No corresponding fillableMakerAssetAmounts at index ${curIndex}`);
}
const tokensAvailableForCurrentOrder = availableMakerAssetAmount;
const ethValueAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount(
const makerTokensAvailableForCurrentOrder = availableMakerAssetAmount;
const takerTokensAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount(
order,
availableMakerAssetAmount,
makerTokensAvailableForCurrentOrder,
);
return {
tokensAvailableInBaseUnits: acc.tokensAvailableInBaseUnits.plus(tokensAvailableForCurrentOrder),
ethValueAvailableInWei: acc.ethValueAvailableInWei.plus(ethValueAvailableForCurrentOrder),
makerTokensAvailableInBaseUnits: acc.makerTokensAvailableInBaseUnits.plus(makerTokensAvailableForCurrentOrder),
takerTokensAvailableInBaseUnits: acc.takerTokensAvailableInBaseUnits.plus(takerTokensAvailableForCurrentOrder),
};
},
{
tokensAvailableInBaseUnits: new BigNumber(0),
ethValueAvailableInWei: new BigNumber(0),
makerTokensAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0),
},
);
// Turn into regular numbers
return {
tokensAvailableInBaseUnits: liquidityInBigNumbers.tokensAvailableInBaseUnits,
ethValueAvailableInWei: liquidityInBigNumbers.ethValueAvailableInWei,
makerTokensAvailableInBaseUnits: liquidityInBigNumbers.makerTokensAvailableInBaseUnits,
takerTokensAvailableInBaseUnits: liquidityInBigNumbers.takerTokensAvailableInBaseUnits,
};
};

View File

@@ -16,6 +16,10 @@ class OrderProviderClass implements OrderProvider {
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
return Promise.resolve([]);
}
// tslint:disable-next-line:prefer-function-over-method
public async getAvailableTakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
return Promise.resolve([]);
}
}
export const orderProviderMock = () => {