diff options
Diffstat (limited to 'packages/order-utils/src')
-rw-r--r-- | packages/order-utils/src/constants.ts | 3 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 11 | ||||
-rw-r--r-- | packages/order-utils/src/market_utils.ts | 133 | ||||
-rw-r--r-- | packages/order-utils/src/order_factory.ts | 78 | ||||
-rw-r--r-- | packages/order-utils/src/types.ts | 12 |
5 files changed, 214 insertions, 23 deletions
diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index bb7482184..c23578c20 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + NULL_BYTES: '0x', // tslint:disable-next-line:custom-no-magic-numbers UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), TESTRPC_NETWORK_ID: 50, @@ -10,4 +11,6 @@ export const constants = { ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, SELECTOR_LENGTH: 4, BASE_16: 16, + INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite + ZERO_AMOUNT: new BigNumber(0), }; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 76be63bb8..858f500c6 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -13,7 +13,15 @@ export { orderFactory } from './order_factory'; export { constants } from './constants'; export { crypto } from './crypto'; export { generatePseudoRandomSalt } from './salt'; -export { OrderError, MessagePrefixType, MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types } from './types'; +export { + CreateOrderOpts, + OrderError, + MessagePrefixType, + MessagePrefixOpts, + EIP712Parameter, + EIP712Schema, + EIP712Types, +} from './types'; export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; @@ -24,3 +32,4 @@ export { assetDataUtils } from './asset_data_utils'; export { EIP712Utils } from './eip712_utils'; export { OrderValidationUtils } from './order_validation_utils'; export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; +export { marketUtils } from './market_utils'; diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts new file mode 100644 index 000000000..681059ddf --- /dev/null +++ b/packages/order-utils/src/market_utils.ts @@ -0,0 +1,133 @@ +import { schemas } from '@0xproject/json-schemas'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { assert } from './assert'; +import { constants } from './constants'; + +export const marketUtils = { + /** + * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, + * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. + * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. + * All orders should specify WETH as the takerAsset. + * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fill amount that could not be covered by the input. + */ + findOrdersThatCoverMakerAssetFillAmount( + signedOrders: SignedOrder[], + remainingFillableMakerAssetAmounts: BigNumber[], + makerAssetFillAmount: BigNumber, + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), + ); + assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); + assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + ); + // calculate total amount of makerAsset needed to be filled + const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); + // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount + const result = _.reduce( + signedOrders, + ({ resultOrders, remainingFillAmount }, order, index) => { + if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { + return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; + } else { + const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; + // if there is no makerAssetAmountAvailable do not append order to resultOrders + // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 + return { + resultOrders: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT) + ? _.concat(resultOrders, order) + : resultOrders, + remainingFillAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingFillAmount.minus(makerAssetAmountAvailable), + ), + }; + } + }, + { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount }, + ); + return result; + }, + /** + * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account + * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a + * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of + * feeOrders that will cost the least ETH. + * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups + * for these values. + * @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills. + * @return Resulting orders and remaining fee amount that could not be covered by the input. + */ + findFeeOrdersThatCoverFeesForTargetOrders( + signedOrders: SignedOrder[], + remainingFillableMakerAssetAmounts: BigNumber[], + signedFeeOrders: SignedOrder[], + remainingFillableFeeAmounts: BigNumber[], + slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, + ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } { + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), + ); + assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + _.forEach(remainingFillableFeeAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount), + ); + assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + ); + assert.assert( + signedOrders.length === remainingFillableMakerAssetAmounts.length, + 'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length', + ); + // calculate total amount of ZRX needed to fill signedOrders + const totalFeeAmount = _.reduce( + signedOrders, + (accFees, order, index) => { + const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; + const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable + .mul(order.takerFee) + .div(order.makerAssetAmount); + return accFees.plus(feeToFillMakerAssetAmountAvailable); + }, + constants.ZERO_AMOUNT, + ); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( + signedFeeOrders, + remainingFillableFeeAmounts, + totalFeeAmount, + slippageBufferAmount, + ); + return { + resultOrders, + remainingFeeAmount: remainingFillAmount, + }; + // TODO: add more orders here to cover rounding + // https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx + }, +}; diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 803cb82b1..14727fd97 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -1,49 +1,63 @@ -import { ECSignature, SignedOrder } from '@0xproject/types'; +import { ECSignature, Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Provider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; +import { constants } from './constants'; import { orderHashUtils } from './order_hash'; import { generatePseudoRandomSalt } from './salt'; import { ecSignOrderHashAsync } from './signature_utils'; -import { MessagePrefixType } from './types'; +import { CreateOrderOpts, MessagePrefixType } from './types'; export const orderFactory = { - async createSignedOrderAsync( - provider: Provider, + createOrder( makerAddress: string, - takerAddress: string, - senderAddress: string, - makerFee: BigNumber, - takerFee: BigNumber, makerAssetAmount: BigNumber, makerAssetData: string, takerAssetAmount: BigNumber, takerAssetData: string, exchangeAddress: string, - feeRecipientAddress: string, - expirationTimeSecondsIfExists?: BigNumber, - ): Promise<SignedOrder> { - const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite - const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists) - ? defaultExpirationUnixTimestampSec - : expirationTimeSecondsIfExists; + createOrderOpts: CreateOrderOpts = generateDefaultCreateOrderOpts(), + ): Order { + const defaultCreateOrderOpts = generateDefaultCreateOrderOpts(); const order = { makerAddress, - takerAddress, - senderAddress, - makerFee, - takerFee, makerAssetAmount, takerAssetAmount, makerAssetData, takerAssetData, - salt: generatePseudoRandomSalt(), exchangeAddress, - feeRecipientAddress, - expirationTimeSeconds, + takerAddress: createOrderOpts.takerAddress || defaultCreateOrderOpts.takerAddress, + senderAddress: createOrderOpts.senderAddress || defaultCreateOrderOpts.senderAddress, + makerFee: createOrderOpts.makerFee || defaultCreateOrderOpts.makerFee, + takerFee: createOrderOpts.takerFee || defaultCreateOrderOpts.takerFee, + feeRecipientAddress: createOrderOpts.feeRecipientAddress || defaultCreateOrderOpts.feeRecipientAddress, + salt: createOrderOpts.salt || defaultCreateOrderOpts.salt, + expirationTimeSeconds: + createOrderOpts.expirationTimeSeconds || defaultCreateOrderOpts.expirationTimeSeconds, }; + return order; + }, + async createSignedOrderAsync( + provider: Provider, + makerAddress: string, + makerAssetAmount: BigNumber, + makerAssetData: string, + takerAssetAmount: BigNumber, + takerAssetData: string, + exchangeAddress: string, + createOrderOpts?: CreateOrderOpts, + ): Promise<SignedOrder> { + const order = orderFactory.createOrder( + makerAddress, + makerAssetAmount, + makerAssetData, + takerAssetAmount, + takerAssetData, + exchangeAddress, + createOrderOpts, + ); const orderHash = orderHashUtils.getOrderHashHex(order); const messagePrefixOpts = { prefixType: MessagePrefixType.EthSign, @@ -56,6 +70,26 @@ export const orderFactory = { }, }; +function generateDefaultCreateOrderOpts(): { + takerAddress: string; + senderAddress: string; + makerFee: BigNumber; + takerFee: BigNumber; + feeRecipientAddress: string; + salt: BigNumber; + expirationTimeSeconds: BigNumber; +} { + return { + takerAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + salt: generatePseudoRandomSalt(), + expirationTimeSeconds: constants.INFINITE_TIMESTAMP_SEC, + }; +} + function getVRSHexString(ecSignature: ECSignature): string { const ETH_SIGN_SIGNATURE_TYPE = '03'; const vrs = `${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix( diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index b08e74e71..f44e94349 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -1,3 +1,5 @@ +import { BigNumber } from '@0xproject/utils'; + export enum OrderError { InvalidSignature = 'INVALID_SIGNATURE', } @@ -51,3 +53,13 @@ export enum EIP712Types { String = 'string', Uint256 = 'uint256', } + +export interface CreateOrderOpts { + takerAddress?: string; + senderAddress?: string; + makerFee?: BigNumber; + takerFee?: BigNumber; + feeRecipientAddress?: string; + salt?: BigNumber; + expirationTimeSeconds?: BigNumber; +} |