Create initial AssetBuyer class

This commit is contained in:
Brandon Millman
2018-09-14 16:18:16 +02:00
parent 8da7d39998
commit 7b46cef83d
10 changed files with 641 additions and 580 deletions

View File

@@ -48,6 +48,8 @@
"@0xproject/types": "^1.0.1", "@0xproject/types": "^1.0.1",
"@0xproject/typescript-typings": "^2.0.0", "@0xproject/typescript-typings": "^2.0.0",
"@0xproject/utils": "^1.0.8", "@0xproject/utils": "^1.0.8",
"@0xproject/web3-wrapper": "^2.0.2",
"ethereum-types": "^1.0.6",
"lodash": "^4.17.10" "lodash": "^4.17.10"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,129 @@
import { ContractWrappers } from '@0xproject/contract-wrappers';
import { marketUtils } from '@0xproject/order-utils';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import { Provider } from 'ethereum-types';
import { constants } from '../constants';
import { AssetBuyerError, BuyQuote, BuyQuoteRequest } from '../types';
const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface
export interface AssetBuyerConfig {
orders: SignedOrder[];
feeOrders: SignedOrder[];
remainingFillableMakerAssetAmounts?: BigNumber[];
remainingFillableFeeAmounts?: BigNumber[];
networkId?: number;
}
export class AssetBuyer {
public readonly provider: Provider;
public readonly config: AssetBuyerConfig;
private _contractWrappers: ContractWrappers;
constructor(provider: Provider, config: AssetBuyerConfig) {
this.provider = provider;
this.config = config;
const networkId = this.config.networkId || constants.MAINNET_NETWORK_ID;
this._contractWrappers = new ContractWrappers(this.provider, {
networkId,
});
}
/**
* Given a BuyQuoteRequest, returns a BuyQuote containing all information relevant to fulfilling the buy. Pass the BuyQuote
* to executeBuyQuoteAsync to execute the buy.
* @param buyQuoteRequest An object that conforms to BuyQuoteRequest. See type definition for more information.
* @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
*/
public getBuyQuote(buyQuoteRequest: BuyQuoteRequest): BuyQuote {
const { assetBuyAmount, feePercentage } = buyQuoteRequest;
const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config;
// TODO: optimization
// make the slippage percentage customizable
const slippageBufferAmount = assetBuyAmount.mul(SLIPPAGE_PERCENTAGE).round();
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
orders,
assetBuyAmount,
{
remainingFillableMakerAssetAmounts,
slippageBufferAmount,
},
);
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientAssetLiquidity);
}
// TODO: optimization
// update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
// finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
resultOrders,
feeOrders,
{
remainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts,
},
);
if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientZrxLiquidity);
}
const assetData = orders[0].makerAssetData;
// TODO: critical
// calculate minRate and maxRate by calculating min and max eth usage and then dividing into
// assetBuyAmount to get assetData / WETH
return {
assetData,
orders: resultOrders,
feeOrders: resultFeeOrders,
minRate: constants.ZERO_AMOUNT,
maxRate: constants.ZERO_AMOUNT,
assetBuyAmount,
feePercentage,
};
}
/**
* Given a BuyQuote and desired rate, attempt to execute the buy.
* @param buyQuote An object that conforms to BuyQuote. See type definition for more information.
* @param rate The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate.
* @param takerAddress The address to perform the buy. Defaults to the first available address from the provider.
* @param feeRecipient The address where affiliate fees are sent. Defaults to null address (0x000...000).
* @return A promise of the txHash.
*/
public async executeBuyQuoteAsync(
buyQuote: BuyQuote,
rate?: BigNumber,
takerAddress?: string,
feeRecipient: string = constants.NULL_ADDRESS,
): Promise<string> {
const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = buyQuote;
// if no takerAddress is provided, try to get one from the provider
let finalTakerAddress;
if (!_.isUndefined(takerAddress)) {
finalTakerAddress = takerAddress;
} else {
const web3Wrapper = new Web3Wrapper(this.provider);
const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
const firstAvailableAddress = _.head(availableAddresses);
if (!_.isUndefined(firstAvailableAddress)) {
finalTakerAddress = firstAvailableAddress;
} else {
throw new Error(AssetBuyerError.NoAddressAvailable);
}
}
// if no rate is provided, default to the maxRate from buyQuote
const desiredRate = rate || maxRate;
// calculate how much eth is required to buy assetBuyAmount at the desired rate
const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate);
const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
orders,
assetBuyAmount,
finalTakerAddress,
ethAmount,
feeOrders,
feePercentage,
feeRecipient,
);
return txHash;
}
}

View File

@@ -3,4 +3,5 @@ import { BigNumber } from '@0xproject/utils';
export const constants = { export const constants = {
ZERO_AMOUNT: new BigNumber(0), ZERO_AMOUNT: new BigNumber(0),
NULL_ADDRESS: '0x0000000000000000000000000000000000000000', NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
MAINNET_NETWORK_ID: 1,
}; };

View File

@@ -1,261 +1,261 @@
import { assert } from '@0xproject/assert'; // import { assert } from '@0xproject/assert';
import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect'; // import { APIOrder, HttpClient, OrderbookResponse } from '@0xproject/connect';
import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers'; // import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers';
import { schemas } from '@0xproject/json-schemas'; // import { schemas } from '@0xproject/json-schemas';
import { assetDataUtils } from '@0xproject/order-utils'; // import { assetDataUtils } from '@0xproject/order-utils';
import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator'; // import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator';
import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders'; // import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders';
import { SignedOrder } from '@0xproject/types'; // import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; // import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash'; // import * as _ from 'lodash';
import { constants } from './constants'; // import { constants } from './constants';
import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl'; // import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer';
import { ForwarderHelper, ForwarderHelperFactoryError } from './types'; // import { ForwarderHelper, ForwarderHelperFactoryError } from './types';
import { orderUtils } from './utils/order_utils'; // import { orderUtils } from './utils/order_utils';
export const forwarderHelperFactory = { // export const forwarderHelperFactory = {
/** // /**
* Given an array of orders and an array of feeOrders, get a ForwarderHelper // * Given an array of orders and an array of feeOrders, get a ForwarderHelper
* @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData // * @param orders An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData
* @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData // * @param feeOrders An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData
* @return A ForwarderHelper, see type for definition // * @return A ForwarderHelper, see type for definition
*/ // */
getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper { // getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper {
assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); // assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema); // assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema);
// TODO: Add assertion here for orders all having the same makerAsset and takerAsset // // TODO: Add assertion here for orders all having the same makerAsset and takerAsset
const config: ForwarderHelperImplConfig = { // const config: ForwarderHelperImplConfig = {
orders, // orders,
feeOrders, // feeOrders,
}; // };
const helper = new ForwarderHelperImpl(config); // const helper = new ForwarderHelperImpl(config);
return helper; // return helper;
}, // },
/** // /**
* Given a desired makerAsset and SRA url, get a ForwarderHelper // * Given a desired makerAsset and SRA url, get a ForwarderHelper
* @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData // * @param makerAssetData An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData
* @param sraUrl A url pointing to an SRA v2 compliant endpoint. // * @param sraUrl A url pointing to an SRA v2 compliant endpoint.
* @param rpcUrl A url pointing to an ethereum node. // * @param rpcUrl A url pointing to an ethereum node.
* @param networkId The ethereum networkId, defaults to 1 (mainnet). // * @param networkId The ethereum networkId, defaults to 1 (mainnet).
* @return A ForwarderHelper, see type for definition // * @return A ForwarderHelper, see type for definition
*/ // */
async getForwarderHelperForMakerAssetDataAsync( // async getForwarderHelperForMakerAssetDataAsync(
makerAssetData: string, // makerAssetData: string,
sraUrl: string, // sraUrl: string,
rpcUrl?: string, // rpcUrl?: string,
networkId: number = 1, // networkId: number = 1,
): Promise<ForwarderHelper> { // ): Promise<ForwarderHelper> {
assert.isHexString('makerAssetData', makerAssetData); // assert.isHexString('makerAssetData', makerAssetData);
assert.isWebUri('sraUrl', sraUrl); // assert.isWebUri('sraUrl', sraUrl);
if (!_.isUndefined(rpcUrl)) { // if (!_.isUndefined(rpcUrl)) {
assert.isWebUri('rpcUrl', rpcUrl); // assert.isWebUri('rpcUrl', rpcUrl);
} // }
assert.isNumber('networkId', networkId); // assert.isNumber('networkId', networkId);
// create provider // // create provider
const providerEngine = new Web3ProviderEngine(); // const providerEngine = new Web3ProviderEngine();
if (!_.isUndefined(rpcUrl)) { // if (!_.isUndefined(rpcUrl)) {
providerEngine.addProvider(new RPCSubprovider(rpcUrl)); // providerEngine.addProvider(new RPCSubprovider(rpcUrl));
} // }
providerEngine.start(); // providerEngine.start();
// create contract wrappers given provider and networkId // // create contract wrappers given provider and networkId
const contractWrappers = new ContractWrappers(providerEngine, { networkId }); // const contractWrappers = new ContractWrappers(providerEngine, { networkId });
// find ether token asset data // // find ether token asset data
const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); // const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists();
if (_.isUndefined(etherTokenAddressIfExists)) { // if (_.isUndefined(etherTokenAddressIfExists)) {
throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound); // throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound);
} // }
const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists); // const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists);
// find zrx token asset data // // find zrx token asset data
let zrxTokenAssetData: string; // let zrxTokenAssetData: string;
try { // try {
zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData(); // zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData();
} catch (err) { // } catch (err) {
throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound); // throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound);
} // }
// get orderbooks for makerAsset/WETH and ZRX/WETH // // get orderbooks for makerAsset/WETH and ZRX/WETH
const sraClient = new HttpClient(sraUrl); // const sraClient = new HttpClient(sraUrl);
const orderbookRequests = [ // const orderbookRequests = [
{ baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData }, // { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData },
{ baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData }, // { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData },
]; // ];
const requestOpts = { networkId }; // const requestOpts = { networkId };
let makerAssetOrderbook: OrderbookResponse; // let makerAssetOrderbook: OrderbookResponse;
let zrxOrderbook: OrderbookResponse; // let zrxOrderbook: OrderbookResponse;
try { // try {
[makerAssetOrderbook, zrxOrderbook] = await Promise.all( // [makerAssetOrderbook, zrxOrderbook] = await Promise.all(
_.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)), // _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)),
); // );
} catch (err) { // } catch (err) {
throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError); // throw new Error(ForwarderHelperFactoryError.StandardRelayerApiError);
} // }
// validate orders and find remaining fillable from on chain state or sra api // // validate orders and find remaining fillable from on chain state or sra api
let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; // let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts;
let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts; // let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts;
if (!_.isUndefined(rpcUrl)) { // if (!_.isUndefined(rpcUrl)) {
// if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper // // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper
const ordersFromSra = getOpenAsksFromOrderbook(makerAssetOrderbook); // const ordersFromSra = getOpenAsksFromOrderbook(makerAssetOrderbook);
const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook); // const feeOrdersFromSra = getOpenAsksFromOrderbook(zrxOrderbook);
// TODO: try catch these requests and throw a more domain specific error // // TODO: try catch these requests and throw a more domain specific error
// TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response // // TODO: optimization, reduce this to once RPC call buy combining orders into one array and then splitting up the response
const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all( // const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all(
_.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => { // _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => {
const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS); // const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS);
return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync( // return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync(
ordersToBeValidated, // ordersToBeValidated,
takerAddresses, // takerAddresses,
); // );
}), // }),
); // );
// take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts // // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts
ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( // ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
ordersFromSra, // ordersFromSra,
makerAssetOrdersAndTradersInfo, // makerAssetOrdersAndTradersInfo,
zrxTokenAssetData, // zrxTokenAssetData,
); // );
// take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts // // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts
feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( // feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
feeOrdersFromSra, // feeOrdersFromSra,
feeOrdersAndTradersInfo, // feeOrdersAndTradersInfo,
zrxTokenAssetData, // zrxTokenAssetData,
); // );
} else { // } else {
// if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA // // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA
// if fill amounts are not available from the SRA, assume all orders are completely fillable // // if fill amounts are not available from the SRA, assume all orders are completely fillable
const apiOrdersFromSra = makerAssetOrderbook.asks.records; // const apiOrdersFromSra = makerAssetOrderbook.asks.records;
const feeApiOrdersFromSra = zrxOrderbook.asks.records; // const feeApiOrdersFromSra = zrxOrderbook.asks.records;
// take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts // // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts
ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( // ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
apiOrdersFromSra, // apiOrdersFromSra,
); // );
// take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts // // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts
feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( // feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
feeApiOrdersFromSra, // feeApiOrdersFromSra,
); // );
} // }
// compile final config // // compile final config
const config: ForwarderHelperImplConfig = { // const config: ForwarderHelperImplConfig = {
orders: ordersAndRemainingFillableMakerAssetAmounts.orders, // orders: ordersAndRemainingFillableMakerAssetAmounts.orders,
feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders, // feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders,
remainingFillableMakerAssetAmounts: // remainingFillableMakerAssetAmounts:
ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, // ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts: // remainingFillableFeeAmounts:
feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts, // feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
}; // };
const helper = new ForwarderHelperImpl(config); // const helper = new ForwarderHelperImpl(config);
return helper; // return helper;
}, // },
}; // };
interface OrdersAndRemainingFillableMakerAssetAmounts { // interface OrdersAndRemainingFillableMakerAssetAmounts {
orders: SignedOrder[]; // orders: SignedOrder[];
remainingFillableMakerAssetAmounts: BigNumber[]; // remainingFillableMakerAssetAmounts: BigNumber[];
} // }
/** // /**
* Given an array of APIOrder objects from a standard relayer api, return an array // * Given an array of APIOrder objects from a standard relayer api, return an array
* of fillable orders with their corresponding remainingFillableMakerAssetAmounts // * of fillable orders with their corresponding remainingFillableMakerAssetAmounts
*/ // */
function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi( // function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
apiOrders: APIOrder[], // apiOrders: APIOrder[],
): OrdersAndRemainingFillableMakerAssetAmounts { // ): OrdersAndRemainingFillableMakerAssetAmounts {
const result = _.reduce( // const result = _.reduce(
apiOrders, // apiOrders,
(acc, apiOrder) => { // (acc, apiOrder) => {
// get current accumulations // // get current accumulations
const { orders, remainingFillableMakerAssetAmounts } = acc; // const { orders, remainingFillableMakerAssetAmounts } = acc;
// get order and metadata // // get order and metadata
const { order, metaData } = apiOrder; // const { order, metaData } = apiOrder;
// if the order is expired or not open, move on // // if the order is expired or not open, move on
if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) { // if (orderUtils.isOrderExpired(order) || !orderUtils.isOpenOrder(order)) {
return acc; // return acc;
} // }
// calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable // // calculate remainingFillableMakerAssetAmount from api metadata, else assume order is completely fillable
const remainingFillableTakerAssetAmount = _.get( // const remainingFillableTakerAssetAmount = _.get(
metaData, // metaData,
'remainingTakerAssetAmount', // 'remainingTakerAssetAmount',
order.takerAssetAmount, // order.takerAssetAmount,
); // );
const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( // const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount(
order, // order,
remainingFillableTakerAssetAmount, // remainingFillableTakerAssetAmount,
); // );
// if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations // // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations
// if there is not any maker asset left to fill, do not add // // if there is not any maker asset left to fill, do not add
if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) { // if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) {
return { // return {
orders: _.concat(orders, order), // orders: _.concat(orders, order),
remainingFillableMakerAssetAmounts: _.concat( // remainingFillableMakerAssetAmounts: _.concat(
remainingFillableMakerAssetAmounts, // remainingFillableMakerAssetAmounts,
remainingFillableMakerAssetAmount, // remainingFillableMakerAssetAmount,
), // ),
}; // };
} else { // } else {
return acc; // return acc;
} // }
}, // },
{ orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, // { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] },
); // );
return result; // return result;
} // }
/** // /**
* Given an array of orders and corresponding on-chain infos, return a subset of the orders // * Given an array of orders and corresponding on-chain infos, return a subset of the orders
* that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts // * that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts
*/ // */
function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain( // function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
inputOrders: SignedOrder[], // inputOrders: SignedOrder[],
ordersAndTradersInfo: OrderAndTraderInfo[], // ordersAndTradersInfo: OrderAndTraderInfo[],
zrxAssetData: string, // zrxAssetData: string,
): OrdersAndRemainingFillableMakerAssetAmounts { // ): OrdersAndRemainingFillableMakerAssetAmounts {
// iterate through the input orders and find the ones that are still fillable // // iterate through the input orders and find the ones that are still fillable
// for the orders that are still fillable, calculate the remaining fillable maker asset amount // // for the orders that are still fillable, calculate the remaining fillable maker asset amount
const result = _.reduce( // const result = _.reduce(
inputOrders, // inputOrders,
(acc, order, index) => { // (acc, order, index) => {
// get current accumulations // // get current accumulations
const { orders, remainingFillableMakerAssetAmounts } = acc; // const { orders, remainingFillableMakerAssetAmounts } = acc;
// get corresponding on-chain state for the order // // get corresponding on-chain state for the order
const { orderInfo, traderInfo } = ordersAndTradersInfo[index]; // const { orderInfo, traderInfo } = ordersAndTradersInfo[index];
// if the order IS NOT fillable, do not add anything to the accumulations and continue iterating // // if the order IS NOT fillable, do not add anything to the accumulations and continue iterating
if (orderInfo.orderStatus !== OrderStatus.FILLABLE) { // if (orderInfo.orderStatus !== OrderStatus.FILLABLE) {
return acc; // return acc;
} // }
// if the order IS fillable, add the order and calculate the remaining fillable amount // // if the order IS fillable, add the order and calculate the remaining fillable amount
const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]); // const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]);
const transferrableFeeAssetAmount = BigNumber.min([ // const transferrableFeeAssetAmount = BigNumber.min([
traderInfo.makerZrxAllowance, // traderInfo.makerZrxAllowance,
traderInfo.makerZrxBalance, // traderInfo.makerZrxBalance,
]); // ]);
const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); // const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount);
const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount( // const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount(
order, // order,
remainingTakerAssetAmount, // remainingTakerAssetAmount,
); // );
const remainingFillableCalculator = new RemainingFillableCalculator( // const remainingFillableCalculator = new RemainingFillableCalculator(
order.makerFee, // order.makerFee,
order.makerAssetAmount, // order.makerAssetAmount,
order.makerAssetData === zrxAssetData, // order.makerAssetData === zrxAssetData,
transferrableAssetAmount, // transferrableAssetAmount,
transferrableFeeAssetAmount, // transferrableFeeAssetAmount,
remainingMakerAssetAmount, // remainingMakerAssetAmount,
); // );
const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable(); // const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable();
return { // return {
orders: _.concat(orders, order), // orders: _.concat(orders, order),
remainingFillableMakerAssetAmounts: _.concat( // remainingFillableMakerAssetAmounts: _.concat(
remainingFillableMakerAssetAmounts, // remainingFillableMakerAssetAmounts,
remainingFillableAmount, // remainingFillableAmount,
), // ),
}; // };
}, // },
{ orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] }, // { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] },
); // );
return result; // return result;
} // }
function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] { // function getOpenAsksFromOrderbook(orderbookResponse: OrderbookResponse): SignedOrder[] {
const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order); // const asks = _.map(orderbookResponse.asks.records, apiOrder => apiOrder.order);
const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask)); // const result = _.filter(asks, ask => orderUtils.isOpenOrder(ask));
return result; // return result;
} // }

View File

@@ -1,64 +0,0 @@
import { marketUtils } from '@0xproject/order-utils';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { constants } from './constants';
import { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfo, MarketBuyOrdersInfoRequest } from './types';
import { forwarderHelperImplConfigUtils } from './utils/forwarder_helper_impl_config_utils';
const SLIPPAGE_PERCENTAGE = new BigNumber(0.2); // 20% slippage protection, possibly move this into request interface
export interface ForwarderHelperImplConfig {
orders: SignedOrder[];
feeOrders: SignedOrder[];
remainingFillableMakerAssetAmounts?: BigNumber[];
remainingFillableFeeAmounts?: BigNumber[];
}
export class ForwarderHelperImpl implements ForwarderHelper {
public readonly config: ForwarderHelperImplConfig;
constructor(config: ForwarderHelperImplConfig) {
this.config = forwarderHelperImplConfigUtils.sortedConfig(config);
}
public getMarketBuyOrdersInfo(request: MarketBuyOrdersInfoRequest): MarketBuyOrdersInfo {
const { makerAssetFillAmount, feePercentage } = request;
const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = this.config;
// TODO: make the slippage percentage customizable
const slippageBufferAmount = makerAssetFillAmount.mul(SLIPPAGE_PERCENTAGE).round();
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
orders,
makerAssetFillAmount,
{
remainingFillableMakerAssetAmounts,
slippageBufferAmount,
},
);
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(ForwarderHelperError.InsufficientMakerAssetLiquidity);
}
// TODO: update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
// finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
const { resultFeeOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
resultOrders,
feeOrders,
{
remainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts,
},
);
if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(ForwarderHelperError.InsufficientZrxLiquidity);
}
// TODO: calculate min and max eth usage
// TODO: optimize orders call data
return {
makerAssetFillAmount,
orders: resultOrders,
feeOrders: resultFeeOrders,
minEthAmount: constants.ZERO_AMOUNT,
maxEthAmount: constants.ZERO_AMOUNT,
feePercentage,
};
}
}

View File

@@ -1,2 +1,2 @@
export { forwarderHelperFactory } from './forwarder_helper_factory'; export { AssetBuyerError, BuyQuote, BuyQuoteRequest } from './types';
export { ForwarderHelper, ForwarderHelperError, MarketBuyOrdersInfoRequest, MarketBuyOrdersInfo } from './types'; export { AssetBuyer } from './asset_buyers/asset_buyer';

View File

@@ -1,49 +1,42 @@
import { SignedOrder } from '@0xproject/types'; import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
export enum ForwarderHelperFactoryError { /**
* assetBuyAmount: The amount of asset to buy.
* feePercentage: Optional affiliate percentage amount factoring into eth amount calculations.
*/
export interface BuyQuoteRequest {
assetBuyAmount: BigNumber;
feePercentage?: BigNumber;
}
/**
* assetData: The asset information.
* 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.
* minRate: Min rate that needs to be paid in order to execute the buy.
* maxRate: Max rate that can be paid in order to execute the buy.
* assetBuyAmount: The amount of asset to buy. Passed through directly from the request.
* feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed through directly from the request.
*/
export interface BuyQuote {
assetData: string;
orders: SignedOrder[];
feeOrders: SignedOrder[];
minRate: BigNumber;
maxRate: BigNumber;
assetBuyAmount: BigNumber;
feePercentage?: BigNumber;
}
/**
* Possible errors thrown by an AssetBuyer instance or associated static methods
*/
export enum AssetBuyerError {
NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND', NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND',
StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR',
} InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY',
export interface ForwarderHelper {
/**
* Given a MarketBuyOrdersInfoRequest, returns a MarketBuyOrdersInfo containing all information relevant to fulfilling the request
* using the ForwarderContract marketBuyOrdersWithEth function.
* @param request An object that conforms to MarketBuyOrdersInfoRequest. See type definition for more information.
* @return An object that conforms to MarketBuyOrdersInfo that satisfies the request. See type definition for more information.
*/
getMarketBuyOrdersInfo: (request: MarketBuyOrdersInfoRequest) => MarketBuyOrdersInfo;
}
export enum ForwarderHelperError {
InsufficientMakerAssetLiquidity = 'INSUFFICIENT_MAKER_ASSET_LIQUIDITY',
InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY', InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY',
} NoAddressAvailable = 'NO_ADDRESS_AVAILABLE',
/**
* makerAssetFillAmount: The amount of makerAsset requesting to be filled
* feePercentage: Optional affiliate percentage amount factoring into eth amount calculations
*/
export interface MarketBuyOrdersInfoRequest {
makerAssetFillAmount: BigNumber;
feePercentage?: BigNumber;
}
/**
* makerAssetFillAmount: The amount of makerAsset requesting to be filled
* orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested makerAssetFillAmount plus slippage
* feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above
* minEthAmount: Amount of eth in wei to send with the tx for the most optimistic case
* maxEthAmount: Amount of eth in wei to send with the tx for the worst case
* feePercentage: Affiliate fee percentage used to calculate the eth amounts above. Passed thru directly from the request
*/
export interface MarketBuyOrdersInfo {
makerAssetFillAmount: BigNumber;
orders: SignedOrder[];
feeOrders: SignedOrder[];
minEthAmount: BigNumber;
maxEthAmount: BigNumber;
feePercentage?: BigNumber;
} }

View File

@@ -1,92 +1,92 @@
import { sortingUtils } from '@0xproject/order-utils'; // import { sortingUtils } from '@0xproject/order-utils';
import { SignedOrder } from '@0xproject/types'; // import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; // import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash'; // import * as _ from 'lodash';
import { ForwarderHelperImplConfig } from '../forwarder_helper_impl'; // import { ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer';
interface SignedOrderWithAmount extends SignedOrder { // interface SignedOrderWithAmount extends SignedOrder {
remainingFillAmount: BigNumber; // remainingFillAmount: BigNumber;
} // }
export const forwarderHelperImplConfigUtils = { // export const forwarderHelperImplConfigUtils = {
sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig { // sortedConfig(config: ForwarderHelperImplConfig): ForwarderHelperImplConfig {
const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config; // const { orders, feeOrders, remainingFillableMakerAssetAmounts, remainingFillableFeeAmounts } = config;
// TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens // // TODO: provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens
const orderSorter = (ordersToSort: SignedOrder[]) => { // const orderSorter = (ordersToSort: SignedOrder[]) => {
return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort); // return sortingUtils.sortOrdersByFeeAdjustedRate(ordersToSort);
}; // };
const sortOrdersResult = sortOrdersAndRemainingFillAmounts( // const sortOrdersResult = sortOrdersAndRemainingFillAmounts(
orderSorter, // orderSorter,
orders, // orders,
remainingFillableMakerAssetAmounts, // remainingFillableMakerAssetAmounts,
); // );
const feeOrderSorter = (ordersToSort: SignedOrder[]) => { // const feeOrderSorter = (ordersToSort: SignedOrder[]) => {
return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort); // return sortingUtils.sortFeeOrdersByFeeAdjustedRate(ordersToSort);
}; // };
const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts( // const sortFeeOrdersResult = sortOrdersAndRemainingFillAmounts(
feeOrderSorter, // feeOrderSorter,
feeOrders, // feeOrders,
remainingFillableFeeAmounts, // remainingFillableFeeAmounts,
); // );
return { // return {
orders: sortOrdersResult.orders, // orders: sortOrdersResult.orders,
feeOrders: sortFeeOrdersResult.orders, // feeOrders: sortFeeOrdersResult.orders,
remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts, // remainingFillableMakerAssetAmounts: sortOrdersResult.remainingFillAmounts,
remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts, // remainingFillableFeeAmounts: sortFeeOrdersResult.remainingFillAmounts,
}; // };
}, // },
}; // };
type OrderSorter = (orders: SignedOrder[]) => SignedOrder[]; // type OrderSorter = (orders: SignedOrder[]) => SignedOrder[];
function sortOrdersAndRemainingFillAmounts( // function sortOrdersAndRemainingFillAmounts(
orderSorter: OrderSorter, // orderSorter: OrderSorter,
orders: SignedOrder[], // orders: SignedOrder[],
remainingFillAmounts?: BigNumber[], // remainingFillAmounts?: BigNumber[],
): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } { // ): { orders: SignedOrder[]; remainingFillAmounts?: BigNumber[] } {
if (!_.isUndefined(remainingFillAmounts)) { // if (!_.isUndefined(remainingFillAmounts)) {
// Bundle orders together with their remainingFillAmounts so that we can sort them together // // Bundle orders together with their remainingFillAmounts so that we can sort them together
const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts); // const orderWithAmounts = bundleSignedOrderWithAmounts(orders, remainingFillAmounts);
// Sort // // Sort
const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[]; // const sortedOrderWithAmounts = orderSorter(orderWithAmounts) as SignedOrderWithAmount[];
// Unbundle after sorting // // Unbundle after sorting
const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts); // const unbundledSortedOrderWithAmounts = unbundleSignedOrderWithAmounts(sortedOrderWithAmounts);
return { // return {
orders: unbundledSortedOrderWithAmounts.orders, // orders: unbundledSortedOrderWithAmounts.orders,
remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts, // remainingFillAmounts: unbundledSortedOrderWithAmounts.amounts,
}; // };
} else { // } else {
const sortedOrders = orderSorter(orders); // const sortedOrders = orderSorter(orders);
return { // return {
orders: sortedOrders, // orders: sortedOrders,
}; // };
} // }
} // }
function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] { // function bundleSignedOrderWithAmounts(orders: SignedOrder[], amounts: BigNumber[]): SignedOrderWithAmount[] {
const ordersAndAmounts = _.map(orders, (order, index) => { // const ordersAndAmounts = _.map(orders, (order, index) => {
return { // return {
...order, // ...order,
remainingFillAmount: amounts[index], // remainingFillAmount: amounts[index],
}; // };
}); // });
return ordersAndAmounts; // return ordersAndAmounts;
} // }
function unbundleSignedOrderWithAmounts( // function unbundleSignedOrderWithAmounts(
signedOrderWithAmounts: SignedOrderWithAmount[], // signedOrderWithAmounts: SignedOrderWithAmount[],
): { orders: SignedOrder[]; amounts: BigNumber[] } { // ): { orders: SignedOrder[]; amounts: BigNumber[] } {
const orders = _.map(signedOrderWithAmounts, order => { // const orders = _.map(signedOrderWithAmounts, order => {
const { remainingFillAmount, ...rest } = order; // const { remainingFillAmount, ...rest } = order;
return rest; // return rest;
}); // });
const amounts = _.map(signedOrderWithAmounts, order => { // const amounts = _.map(signedOrderWithAmounts, order => {
const { remainingFillAmount } = order; // const { remainingFillAmount } = order;
return remainingFillAmount; // return remainingFillAmount;
}); // });
return { // return {
orders, // orders,
amounts, // amounts,
}; // };
} // }

View File

@@ -1,136 +1,136 @@
import { testOrderFactory } from '@0xproject/order-utils/lib/test/utils/test_order_factory'; // import { testOrderFactory } from '@0xproject/order-utils/lib/test/utils/test_order_factory';
import { BigNumber } from '@0xproject/utils'; // import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai'; // import * as chai from 'chai';
import * as _ from 'lodash'; // import * as _ from 'lodash';
import 'mocha'; // import 'mocha';
import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '../src/forwarder_helper_impl'; // import { ForwarderHelperImpl, ForwarderHelperImplConfig } from '@0xproject/asset-buyer/src/asset_buyer';
import { ForwarderHelperError } from '../src/types'; // import { ForwarderHelperError } from '../src/types';
import { chaiSetup } from './utils/chai_setup'; // import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure(); // chaiSetup.configure();
const expect = chai.expect; // const expect = chai.expect;
describe('ForwarderHelperImpl', () => { // describe('ForwarderHelperImpl', () => {
// rate: 2 takerAsset / makerAsset // // rate: 2 takerAsset / makerAsset
const testOrder1 = testOrderFactory.generateTestSignedOrder({ // const testOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100), // makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200), // takerAssetAmount: new BigNumber(200),
}); // });
// rate: 1 takerAsset / makerAsset // // rate: 1 takerAsset / makerAsset
const testOrder2 = testOrderFactory.generateTestSignedOrder({ // const testOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100), // makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100), // takerAssetAmount: new BigNumber(100),
}); // });
// rate: 3 takerAsset / makerAsset // // rate: 3 takerAsset / makerAsset
const testOrder3 = testOrderFactory.generateTestSignedOrder({ // const testOrder3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100), // makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(300), // takerAssetAmount: new BigNumber(300),
takerFee: new BigNumber(1), // takerFee: new BigNumber(1),
}); // });
// rate: 3 WETH / ZRX // // rate: 3 WETH / ZRX
const testFeeOrder1 = testOrderFactory.generateTestSignedOrder({ // const testFeeOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100), // makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(300), // takerAssetAmount: new BigNumber(300),
}); // });
// rate: 2 WETH / ZRX // // rate: 2 WETH / ZRX
const testFeeOrder2 = testOrderFactory.generateTestSignedOrder({ // const testFeeOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100), // makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200), // takerAssetAmount: new BigNumber(200),
}); // });
// rate: 1 WETH / ZRX // // rate: 1 WETH / ZRX
const testFeeOrder3 = testOrderFactory.generateTestSignedOrder({ // const testFeeOrder3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100), // makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100), // takerAssetAmount: new BigNumber(100),
}); // });
const inputForwarderHelperConfig: ForwarderHelperImplConfig = { // const inputForwarderHelperConfig: ForwarderHelperImplConfig = {
orders: [testOrder1, testOrder2, testOrder3], // orders: [testOrder1, testOrder2, testOrder3],
feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], // feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3],
remainingFillableMakerAssetAmounts: [new BigNumber(1), new BigNumber(2), new BigNumber(3)], // remainingFillableMakerAssetAmounts: [new BigNumber(1), new BigNumber(2), new BigNumber(3)],
remainingFillableFeeAmounts: [new BigNumber(4), new BigNumber(5), new BigNumber(6)], // remainingFillableFeeAmounts: [new BigNumber(4), new BigNumber(5), new BigNumber(6)],
}; // };
describe('#constructor', () => { // describe('#constructor', () => {
const inputForwarderHelperConfigNoRemainingAmounts: ForwarderHelperImplConfig = { // const inputForwarderHelperConfigNoRemainingAmounts: ForwarderHelperImplConfig = {
orders: [testOrder1, testOrder2, testOrder3], // orders: [testOrder1, testOrder2, testOrder3],
feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3], // feeOrders: [testFeeOrder1, testFeeOrder2, testFeeOrder3],
}; // };
it('sorts orders', () => { // it('sorts orders', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
expect(forwarderHelper.config.orders).deep.equals([testOrder2, testOrder1, testOrder3]); // expect(forwarderHelper.config.orders).deep.equals([testOrder2, testOrder1, testOrder3]);
}); // });
it('sorts fee orders', () => { // it('sorts fee orders', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
expect(forwarderHelper.config.feeOrders).deep.equals([testFeeOrder3, testFeeOrder2, testFeeOrder1]); // expect(forwarderHelper.config.feeOrders).deep.equals([testFeeOrder3, testFeeOrder2, testFeeOrder1]);
}); // });
it('sorts remainingFillableMakerAssetAmounts', () => { // it('sorts remainingFillableMakerAssetAmounts', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.not.undefined(); // expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.not.undefined();
expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 0)).to.bignumber.equal( // expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 0)).to.bignumber.equal(
new BigNumber(2), // new BigNumber(2),
); // );
expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 1)).to.bignumber.equal( // expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 1)).to.bignumber.equal(
new BigNumber(1), // new BigNumber(1),
); // );
expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 2)).to.bignumber.equal( // expect(_.nth(forwarderHelper.config.remainingFillableMakerAssetAmounts, 2)).to.bignumber.equal(
new BigNumber(3), // new BigNumber(3),
); // );
}); // });
it('sorts remainingFillableFeeAmounts', () => { // it('sorts remainingFillableFeeAmounts', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.not.undefined(); // expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.not.undefined();
expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 0)).to.bignumber.equal(new BigNumber(6)); // expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 0)).to.bignumber.equal(new BigNumber(6));
expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 1)).to.bignumber.equal(new BigNumber(5)); // expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 1)).to.bignumber.equal(new BigNumber(5));
expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 2)).to.bignumber.equal(new BigNumber(4)); // expect(_.nth(forwarderHelper.config.remainingFillableFeeAmounts, 2)).to.bignumber.equal(new BigNumber(4));
}); // });
it('remainingFillableMakerAssetAmounts is undefined if none provided', () => { // it('remainingFillableMakerAssetAmounts is undefined if none provided', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts);
expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.undefined(); // expect(forwarderHelper.config.remainingFillableMakerAssetAmounts).to.be.undefined();
}); // });
it('remainingFillableFeeAmounts is undefined if none provided', () => { // it('remainingFillableFeeAmounts is undefined if none provided', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoRemainingAmounts);
expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.undefined(); // expect(forwarderHelper.config.remainingFillableFeeAmounts).to.be.undefined();
}); // });
}); // });
describe('#getMarketBuyOrdersInfo', () => { // describe('#getMarketBuyOrdersInfo', () => {
it('throws if not enough makerAsset liquidity', () => { // it('throws if not enough makerAsset liquidity', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
expect(() => { // expect(() => {
// request for 6 makerAsset units, because we have exactly 6 available we should throw because there is a built in slippage buffer // // request for 6 makerAsset units, because we have exactly 6 available we should throw because there is a built in slippage buffer
forwarderHelper.getMarketBuyOrdersInfo({ // forwarderHelper.getMarketBuyOrdersInfo({
makerAssetFillAmount: new BigNumber(6), // makerAssetFillAmount: new BigNumber(6),
}); // });
}).to.throw(ForwarderHelperError.InsufficientMakerAssetLiquidity); // }).to.throw(ForwarderHelperError.InsufficientMakerAssetLiquidity);
}); // });
it('throws if not enough ZRX liquidity', () => { // it('throws if not enough ZRX liquidity', () => {
const inputForwarderHelperConfigNoFees: ForwarderHelperImplConfig = { // const inputForwarderHelperConfigNoFees: ForwarderHelperImplConfig = {
orders: [testOrder1, testOrder2, testOrder3], // orders: [testOrder1, testOrder2, testOrder3],
feeOrders: [], // feeOrders: [],
}; // };
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoFees); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfigNoFees);
expect(() => { // expect(() => {
// request for 4 makerAsset units, we need fees but no fee orders exist, show we should throw // // request for 4 makerAsset units, we need fees but no fee orders exist, show we should throw
forwarderHelper.getMarketBuyOrdersInfo({ // forwarderHelper.getMarketBuyOrdersInfo({
makerAssetFillAmount: new BigNumber(250), // makerAssetFillAmount: new BigNumber(250),
}); // });
}).to.throw(ForwarderHelperError.InsufficientZrxLiquidity); // }).to.throw(ForwarderHelperError.InsufficientZrxLiquidity);
}); // });
it('passes the makerAssetFillAmount from the request to the info response', () => { // it('passes the makerAssetFillAmount from the request to the info response', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
const makerAssetFillAmount = new BigNumber(4); // const makerAssetFillAmount = new BigNumber(4);
const info = forwarderHelper.getMarketBuyOrdersInfo({ // const info = forwarderHelper.getMarketBuyOrdersInfo({
makerAssetFillAmount, // makerAssetFillAmount,
}); // });
expect(info.makerAssetFillAmount).to.bignumber.equal(makerAssetFillAmount); // expect(info.makerAssetFillAmount).to.bignumber.equal(makerAssetFillAmount);
}); // });
it('passes the feePercentage from the request to the info response', () => { // it('passes the feePercentage from the request to the info response', () => {
const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig); // const forwarderHelper = new ForwarderHelperImpl(inputForwarderHelperConfig);
const feePercentage = new BigNumber(0.2); // const feePercentage = new BigNumber(0.2);
const info = forwarderHelper.getMarketBuyOrdersInfo({ // const info = forwarderHelper.getMarketBuyOrdersInfo({
makerAssetFillAmount: new BigNumber(4), // makerAssetFillAmount: new BigNumber(4),
feePercentage, // feePercentage,
}); // });
expect(info.feePercentage).to.bignumber.equal(feePercentage); // expect(info.feePercentage).to.bignumber.equal(feePercentage);
}); // });
}); // });
}); // });

View File

@@ -23,6 +23,7 @@
{ "path": "./packages/0x.js" }, { "path": "./packages/0x.js" },
{ "path": "./packages/abi-gen" }, { "path": "./packages/abi-gen" },
{ "path": "./packages/assert" }, { "path": "./packages/assert" },
{ "path": "./packages/asset-buyer" },
{ "path": "./packages/base-contract" }, { "path": "./packages/base-contract" },
{ "path": "./packages/connect" }, { "path": "./packages/connect" },
{ "path": "./packages/contract-wrappers" }, { "path": "./packages/contract-wrappers" },
@@ -30,7 +31,6 @@
{ "path": "./packages/dev-utils" }, { "path": "./packages/dev-utils" },
{ "path": "./packages/ethereum-types" }, { "path": "./packages/ethereum-types" },
{ "path": "./packages/fill-scenarios" }, { "path": "./packages/fill-scenarios" },
{ "path": "./packages/forwarder-helper" },
{ "path": "./packages/json-schemas" }, { "path": "./packages/json-schemas" },
{ "path": "./packages/metacoin" }, { "path": "./packages/metacoin" },
{ "path": "./packages/migrations" }, { "path": "./packages/migrations" },