Add getForwarderHelperForMakerAssetDataAsync method to forwarderHelperFactory

This commit is contained in:
Brandon Millman
2018-08-24 11:11:29 -07:00
parent 07942a7aec
commit 8687b9533c
5 changed files with 253 additions and 4 deletions

View File

@@ -29,7 +29,7 @@ const TRAILING_SLASHES_REGEX = /\/+$/;
/**
* This class includes all the functionality related to interacting with a set of HTTP endpoints
* that implement the standard relayer API v0
* that implement the standard relayer API v2
*/
export class HttpClient implements Client {
private readonly _apiEndpointUrl: string;

View File

@@ -40,18 +40,21 @@
"homepage": "https://github.com/0xProject/0x-monorepo/packages/forwarder-helper/README.md",
"dependencies": {
"@0xproject/assert": "^1.0.8",
"@0xproject/connect": "^2.0.0",
"@0xproject/contract-wrappers": "^1.0.1",
"@0xproject/json-schemas": "^1.0.1",
"@0xproject/order-utils": "^1.0.1",
"@0xproject/subproviders": "^2.0.2",
"@0xproject/types": "^1.0.1",
"@0xproject/typescript-typings": "^2.0.0",
"@0xproject/utils": "^1.0.8",
"@types/node": "^8.0.53",
"lodash": "^4.17.10"
},
"devDependencies": {
"@0xproject/tslint-config": "^1.0.7",
"@types/lodash": "^4.14.116",
"@types/mocha": "^2.2.42",
"@types/node": "^8.0.53",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^2.0.1",

View File

@@ -1,13 +1,22 @@
import { assert } from '@0xproject/assert';
import { APIOrder, HttpClient } from '@0xproject/connect';
import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers';
import { schemas } from '@0xproject/json-schemas';
import { assetDataUtils } from '@0xproject/order-utils';
import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator';
import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { constants } from './constants';
import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl';
import { ForwarderHelper } from './types';
import { ForwarderHelper, ForwarderHelperFactoryError } from './types';
import { orderUtils } from './utils/order_utils';
export const forwarderHelperFactory = {
/**
* Given an array of orders and an array of feeOrders
* 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 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
@@ -22,4 +31,220 @@ export const forwarderHelperFactory = {
const helper = new ForwarderHelperImpl(config);
return helper;
},
/**
* 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 sraUrl A url pointing to an SRA v2 compliant endpoint.
* @param rpcUrl A url pointing to an ethereum node.
* @param networkId The ethereum networkId, defaults to 1 (mainnet).
* @return A ForwarderHelper, see type for definition
*/
async getForwarderHelperForMakerAssetDataAsync(
makerAssetData: string,
takerAddress: string,
sraUrl: string,
rpcUrl?: string,
networkId: number = 1,
): Promise<ForwarderHelper> {
assert.isHexString('makerAssetData', makerAssetData);
assert.isETHAddressHex('takerAddress', takerAddress);
assert.isWebUri('sraUrl', sraUrl);
if (!_.isUndefined(rpcUrl)) {
assert.isWebUri('rpcUrl', rpcUrl);
}
assert.isNumber('networkId', networkId);
// create provider
const providerEngine = new Web3ProviderEngine();
if (!_.isUndefined(rpcUrl)) {
providerEngine.addProvider(new RPCSubprovider(rpcUrl));
}
providerEngine.start();
// create contract wrappers given provider and networkId
const contractWrappers = new ContractWrappers(providerEngine, { networkId });
// find ether token asset data
const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists();
if (_.isUndefined(etherTokenAddressIfExists)) {
throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound);
}
const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists);
// find zrx token asset data
let zrxTokenAssetData: string;
try {
zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData();
} catch (err) {
throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound);
}
// get orderbooks for makerAsset/WETH and ZRX/WETH
const sraClient = new HttpClient(sraUrl);
const orderbookRequests = [
{ baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData },
{ baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData },
];
const requestOpts = { networkId };
// TODO: try catch these requests and throw a more domain specific error
const [makerAssetOrderbook, zrxOrderbook] = await Promise.all(
_.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)),
);
// validate orders and find remaining fillable from on chain state or sra api
let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts;
let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts;
if (!_.isUndefined(rpcUrl)) {
// if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper
const ordersFromSra = _.map(makerAssetOrderbook.asks.records, apiOrder => apiOrder.order);
const feeOrdersFromSra = _.map(zrxOrderbook.asks.records, apiOrder => apiOrder.order);
// TODO: try catch these requests and throw a more domain specific error
const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all(
_.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => {
const takerAddresses = _.map(ordersToBeValidated, () => takerAddress);
return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync(
ordersToBeValidated,
takerAddresses,
);
}),
);
// take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts
ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
ordersFromSra,
makerAssetOrdersAndTradersInfo,
zrxTokenAssetData,
);
// take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts
feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
feeOrdersFromSra,
feeOrdersAndTradersInfo,
zrxTokenAssetData,
);
} else {
// 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
const apiOrdersFromSra = makerAssetOrderbook.asks.records;
const feeApiOrdersFromSra = zrxOrderbook.asks.records;
// take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts
ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
apiOrdersFromSra,
);
// take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts
feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
feeApiOrdersFromSra,
);
}
// compile final config
const config: ForwarderHelperImplConfig = {
orders: ordersAndRemainingFillableMakerAssetAmounts.orders,
feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders,
remainingFillableMakerAssetAmounts:
ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts:
feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
};
const helper = new ForwarderHelperImpl(config);
return helper;
},
};
interface OrdersAndRemainingFillableMakerAssetAmounts {
orders: SignedOrder[];
remainingFillableMakerAssetAmounts: BigNumber[];
}
/**
* Given an array of APIOrder objects from a standard relayer api, return an array
* of fillable orders with their corresponding remainingFillableMakerAssetAmounts
*/
function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
apiOrders: APIOrder[],
): OrdersAndRemainingFillableMakerAssetAmounts {
const result = _.reduce(
apiOrders,
(acc, apiOrder, index) => {
// get current accumulations
const { orders, remainingFillableMakerAssetAmounts } = acc;
// get order and metadata
const { order, metaData } = apiOrder;
// if the order is expired, move on
if (orderUtils.isOrderExpired(order)) {
return acc;
}
// calculate remainingFillableMakerAssetAmount from api metadata
const remainingFillableTakerAssetAmount = _.get(
metaData,
'remainingTakerAssetAmount',
order.takerAssetAmount,
);
const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount(
order,
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 not any maker asset left to fill, do not add
if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) {
return {
orders: _.concat(orders, order),
remainingFillableMakerAssetAmounts: _.concat(
remainingFillableMakerAssetAmounts,
remainingFillableMakerAssetAmount,
),
};
} else {
return acc;
}
},
{ orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] },
);
return result;
}
/**
* 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
*/
function getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
inputOrders: SignedOrder[],
ordersAndTradersInfo: OrderAndTraderInfo[],
zrxAssetData: string,
): OrdersAndRemainingFillableMakerAssetAmounts {
// 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
const result = _.reduce(
inputOrders,
(acc, order, index) => {
// get current accumulations
const { orders, remainingFillableMakerAssetAmounts } = acc;
// get corresponding on-chain state for the order
const { orderInfo, traderInfo } = ordersAndTradersInfo[index];
// if the order IS NOT fillable, do not add anything and continue iterating
if (orderInfo.orderStatus !== OrderStatus.FILLABLE) {
return acc;
}
// if the order IS fillable, add the order and calculate the remaining fillable amount
const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]);
const transferrableFeeAssetAmount = BigNumber.min([
traderInfo.makerZrxAllowance,
traderInfo.makerZrxBalance,
]);
const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount);
const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount(
order,
remainingTakerAssetAmount,
);
const remainingFillableCalculator = new RemainingFillableCalculator(
order.makerFee,
order.makerAssetAmount,
order.makerAssetData === zrxAssetData,
transferrableAssetAmount,
transferrableFeeAssetAmount,
remainingMakerAssetAmount,
);
const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable();
return {
orders: _.concat(orders, order),
remainingFillableMakerAssetAmounts: _.concat(
remainingFillableMakerAssetAmounts,
remainingFillableAmount,
),
};
},
{ orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] },
);
return result;
}

View File

@@ -1,6 +1,11 @@
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
export enum ForwarderHelperFactoryError {
NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND',
}
export interface ForwarderHelper {
/**
* Given a MarketBuyOrdersInfoRequest, returns a MarketBuyOrdersInfo containing all information relevant to fulfilling the request

View File

@@ -0,0 +1,16 @@
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
export const orderUtils = {
isOrderExpired(order: SignedOrder): boolean {
const millisecondsInSecond = 1000;
const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round();
return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec);
},
calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber {
const result = remainingTakerAssetAmount.eq(0)
? new BigNumber(0)
: remainingTakerAssetAmount.times(order.makerAssetAmount).dividedToIntegerBy(order.takerAssetAmount);
return result;
},
};