diff options
author | Steve Klebanoff <steve@0xproject.com> | 2019-01-10 08:29:11 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-10 08:29:11 +0800 |
commit | 6487fae1131eb6e9215c918f23bac317157f368b (patch) | |
tree | bc02d4b3b0b556b2e911f4ba2c60f4035fdd717e /packages/asset-buyer | |
parent | 87c287a5e26e89ee25dd43793415d00d4ddfd5fa (diff) | |
parent | fb3605026ef63ef6897010a52bf3f4c116cdf271 (diff) | |
download | dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.tar dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.tar.gz dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.tar.bz2 dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.tar.lz dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.tar.xz dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.tar.zst dexon-sol-tools-6487fae1131eb6e9215c918f23bac317157f368b.zip |
Merge pull request #1437 from 0xProject/feature/instant/tell-amount-available
[instant] Tell user how much of an asset is available
Diffstat (limited to 'packages/asset-buyer')
-rw-r--r-- | packages/asset-buyer/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/asset-buyer/src/errors.ts | 22 | ||||
-rw-r--r-- | packages/asset-buyer/src/index.ts | 1 | ||||
-rw-r--r-- | packages/asset-buyer/src/types.ts | 2 | ||||
-rw-r--r-- | packages/asset-buyer/src/utils/buy_quote_calculator.ts | 14 | ||||
-rw-r--r-- | packages/asset-buyer/test/buy_quote_calculator_test.ts | 143 | ||||
-rw-r--r-- | packages/asset-buyer/test/utils/test_helpers.ts | 26 |
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(); + }, +}; |