aboutsummaryrefslogtreecommitdiffstats
path: root/packages/asset-buyer
diff options
context:
space:
mode:
Diffstat (limited to 'packages/asset-buyer')
-rw-r--r--packages/asset-buyer/CHANGELOG.json9
-rw-r--r--packages/asset-buyer/src/errors.ts22
-rw-r--r--packages/asset-buyer/src/index.ts1
-rw-r--r--packages/asset-buyer/src/types.ts2
-rw-r--r--packages/asset-buyer/src/utils/buy_quote_calculator.ts14
-rw-r--r--packages/asset-buyer/test/buy_quote_calculator_test.ts143
-rw-r--r--packages/asset-buyer/test/utils/test_helpers.ts26
7 files changed, 206 insertions, 11 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json
index fcdb655fc..a7f8d3418 100644
--- a/packages/asset-buyer/CHANGELOG.json
+++ b/packages/asset-buyer/CHANGELOG.json
@@ -1,5 +1,14 @@
[
{
+ "version": "4.0.0",
+ "changes": [
+ {
+ "note": "Raise custom InsufficientAssetLiquidityError error with amountAvailableToFill attribute",
+ "pr": 1437
+ }
+ ]
+ },
+ {
"timestamp": 1547040760,
"version": "3.0.5",
"changes": [
diff --git a/packages/asset-buyer/src/errors.ts b/packages/asset-buyer/src/errors.ts
new file mode 100644
index 000000000..ec5fe548c
--- /dev/null
+++ b/packages/asset-buyer/src/errors.ts
@@ -0,0 +1,22 @@
+import { BigNumber } from '@0x/utils';
+
+import { AssetBuyerError } from './types';
+
+/**
+ * Error class representing insufficient asset liquidity
+ */
+export class InsufficientAssetLiquidityError extends Error {
+ /**
+ * The amount availabe to fill (in base units) factoring in slippage.
+ */
+ public amountAvailableToFill: BigNumber;
+ /**
+ * @param amountAvailableToFill The amount availabe to fill (in base units) factoring in slippage
+ */
+ constructor(amountAvailableToFill: BigNumber) {
+ super(AssetBuyerError.InsufficientAssetLiquidity);
+ this.amountAvailableToFill = amountAvailableToFill;
+ // Setting prototype so instanceof works. See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
+ Object.setPrototypeOf(this, InsufficientAssetLiquidityError.prototype);
+ }
+}
diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts
index 8418edb42..a42d7e272 100644
--- a/packages/asset-buyer/src/index.ts
+++ b/packages/asset-buyer/src/index.ts
@@ -9,6 +9,7 @@ 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 {
diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts
index 3b573edca..d5d6be695 100644
--- a/packages/asset-buyer/src/types.ts
+++ b/packages/asset-buyer/src/types.ts
@@ -102,7 +102,7 @@ export interface AssetBuyerOpts {
}
/**
- * Possible errors thrown by an AssetBuyer instance or associated static methods.
+ * Possible error messages thrown by an AssetBuyer instance or associated static methods.
*/
export enum AssetBuyerError {
NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
index b15b880c2..fcded6ab1 100644
--- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts
+++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
@@ -3,6 +3,7 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
+import { InsufficientAssetLiquidityError } from '../errors';
import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types';
import { orderUtils } from './order_utils';
@@ -33,7 +34,18 @@ export const buyQuoteCalculator = {
});
// if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
- throw new Error(AssetBuyerError.InsufficientAssetLiquidity);
+ // We needed the amount they requested to buy, plus the amount for slippage
+ const totalAmountRequested = assetBuyAmount.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
+ // i.e. if slippagePercent was 0.2 (20%), multiplierNeededWithSlippage would be 1.2
+ const multiplierNeededWithSlippage = new BigNumber(1).plus(slippagePercentage);
+ // Given amountAvailableToFillConsideringSlippage * multiplierNeededWithSlippage = amountAbleToFill
+ // We divide amountUnableToFill by multiplierNeededWithSlippage to determine amountAvailableToFillConsideringSlippage
+ const amountAvailableToFillConsideringSlippage = amountAbleToFill.div(multiplierNeededWithSlippage).floor();
+
+ throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage);
}
// if we are not buying ZRX:
// given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage)
diff --git a/packages/asset-buyer/test/buy_quote_calculator_test.ts b/packages/asset-buyer/test/buy_quote_calculator_test.ts
index a30017b72..fdc17ef25 100644
--- a/packages/asset-buyer/test/buy_quote_calculator_test.ts
+++ b/packages/asset-buyer/test/buy_quote_calculator_test.ts
@@ -1,4 +1,5 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
+import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
@@ -8,6 +9,7 @@ import { AssetBuyerError, OrdersAndFillableAmounts } from '../src/types';
import { buyQuoteCalculator } from '../src/utils/buy_quote_calculator';
import { chaiSetup } from './utils/chai_setup';
+import { testHelpers } from './utils/test_helpers';
chaiSetup.configure();
const expect = chai.expect;
@@ -15,6 +17,10 @@ const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
describe('buyQuoteCalculator', () => {
describe('#calculate', () => {
+ let firstOrder: SignedOrder;
+ let firstRemainingFillAmount: BigNumber;
+ let secondOrder: SignedOrder;
+ let secondRemainingFillAmount: BigNumber;
let ordersAndFillableAmounts: OrdersAndFillableAmounts;
let smallFeeOrderAndFillableAmount: OrdersAndFillableAmounts;
let allFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts;
@@ -24,18 +30,18 @@ describe('buyQuoteCalculator', () => {
// the second order has a rate of 2 makerAsset / WETH with a takerFee of 100 ZRX and has 200 / 200 makerAsset units left to fill (completely fillable)
// generate one order for fees
// the fee order has a rate of 1 ZRX / WETH with no taker fee and has 100 ZRX left to fill (completely fillable)
- const firstOrder = orderFactory.createSignedOrderFromPartial({
+ firstOrder = orderFactory.createSignedOrderFromPartial({
makerAssetAmount: new BigNumber(400),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(200),
});
- const firstRemainingFillAmount = new BigNumber(200);
- const secondOrder = orderFactory.createSignedOrderFromPartial({
+ firstRemainingFillAmount = new BigNumber(200);
+ secondOrder = orderFactory.createSignedOrderFromPartial({
makerAssetAmount: new BigNumber(200),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(100),
});
- const secondRemainingFillAmount = secondOrder.makerAssetAmount;
+ secondRemainingFillAmount = secondOrder.makerAssetAmount;
ordersAndFillableAmounts = {
orders: [firstOrder, secondOrder],
remainingFillableMakerAssetAmounts: [firstRemainingFillAmount, secondRemainingFillAmount],
@@ -61,18 +67,137 @@ describe('buyQuoteCalculator', () => {
],
};
});
- it('should throw if not enough maker asset liquidity', () => {
- // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
+ describe('InsufficientLiquidityError', () => {
+ it('should throw if not enough maker asset liquidity (multiple orders)', () => {
+ // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(500),
+ 0,
+ 0,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(400));
+ });
+ it('should throw if not enough maker asset liquidity (multiple orders with 20% slippage)', () => {
+ // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(500),
+ 0,
+ 0.2,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(333));
+ });
+ it('should throw if not enough maker asset liquidity (multiple orders with 5% slippage)', () => {
+ // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(600),
+ 0,
+ 0.05,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(380));
+ });
+ it('should throw if not enough maker asset liquidity (partially filled order)', () => {
+ const firstOrderAndFillableAmount: OrdersAndFillableAmounts = {
+ orders: [firstOrder],
+ remainingFillableMakerAssetAmounts: [firstRemainingFillAmount],
+ };
+
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ firstOrderAndFillableAmount,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(201),
+ 0,
+ 0,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(200));
+ });
+ it('should throw if not enough maker asset liquidity (completely fillable order)', () => {
+ const completelyFillableOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(123),
+ takerAssetAmount: new BigNumber(100),
+ takerFee: new BigNumber(200),
+ });
+ const completelyFillableOrdersAndFillableAmount: OrdersAndFillableAmounts = {
+ orders: [completelyFillableOrder],
+ remainingFillableMakerAssetAmounts: [completelyFillableOrder.makerAssetAmount],
+ };
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ completelyFillableOrdersAndFillableAmount,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(124),
+ 0,
+ 0,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(123));
+ });
+ it('should throw with 1 amount available if no slippage', () => {
+ const smallOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ takerFee: new BigNumber(0),
+ });
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ { orders: [smallOrder], remainingFillableMakerAssetAmounts: [smallOrder.makerAssetAmount] },
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(600),
+ 0,
+ 0,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(1));
+ });
+ it('should throw without amount available to fill if amount rounds to 0', () => {
+ const smallOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ takerFee: new BigNumber(0),
+ });
+ const errorFunction = () => {
+ buyQuoteCalculator.calculate(
+ { orders: [smallOrder], remainingFillableMakerAssetAmounts: [smallOrder.makerAssetAmount] },
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(600),
+ 0,
+ 0.2,
+ false,
+ );
+ };
+ testHelpers.expectInsufficientLiquidityError(expect, errorFunction, undefined);
+ });
+ });
+ it('should not throw if order is fillable', () => {
expect(() =>
buyQuoteCalculator.calculate(
ordersAndFillableAmounts,
- smallFeeOrderAndFillableAmount,
- new BigNumber(500),
+ allFeeOrdersAndFillableAmounts,
+ new BigNumber(300),
0,
0,
false,
),
- ).to.throw(AssetBuyerError.InsufficientAssetLiquidity);
+ ).to.not.throw();
});
it('should throw if not enough ZRX liquidity', () => {
// we request 300 makerAsset units but the ZRX order is only enough to fill the first order, which only has 200 makerAssetUnits available
diff --git a/packages/asset-buyer/test/utils/test_helpers.ts b/packages/asset-buyer/test/utils/test_helpers.ts
new file mode 100644
index 000000000..9c7c244af
--- /dev/null
+++ b/packages/asset-buyer/test/utils/test_helpers.ts
@@ -0,0 +1,26 @@
+import { BigNumber } from '@0x/utils';
+
+import { InsufficientAssetLiquidityError } from '../../src/errors';
+
+export const testHelpers = {
+ expectInsufficientLiquidityError: (
+ expect: Chai.ExpectStatic,
+ functionWhichTriggersError: () => void,
+ expectedAmountAvailableToFill?: BigNumber,
+ ): void => {
+ let wasErrorThrown = false;
+ try {
+ functionWhichTriggersError();
+ } catch (e) {
+ wasErrorThrown = true;
+ expect(e).to.be.instanceOf(InsufficientAssetLiquidityError);
+ if (expectedAmountAvailableToFill) {
+ expect(e.amountAvailableToFill).to.be.bignumber.equal(expectedAmountAvailableToFill);
+ } else {
+ expect(e.amountAvailableToFill).to.be.undefined();
+ }
+ }
+
+ expect(wasErrorThrown).to.be.true();
+ },
+};