Merge pull request #226 from dekz/feature/calculate-remaining-proportions
Calculate remaining proportions from fee to ratio proportions
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
import {SignedOrder} from '../types';
|
||||||
|
import {BigNumber} from 'bignumber.js';
|
||||||
|
|
||||||
|
export class RemainingFillableCalculator {
|
||||||
|
private signedOrder: SignedOrder;
|
||||||
|
private isMakerTokenZRX: boolean;
|
||||||
|
// Transferrable Amount is the minimum of Approval and Balance
|
||||||
|
private transferrableMakerTokenAmount: BigNumber;
|
||||||
|
private transferrableMakerFeeTokenAmount: BigNumber;
|
||||||
|
private remainingMakerTokenAmount: BigNumber;
|
||||||
|
private remainingMakerFeeAmount: BigNumber;
|
||||||
|
constructor(signedOrder: SignedOrder,
|
||||||
|
isMakerTokenZRX: boolean,
|
||||||
|
transferrableMakerTokenAmount: BigNumber,
|
||||||
|
transferrableMakerFeeTokenAmount: BigNumber,
|
||||||
|
remainingMakerTokenAmount: BigNumber) {
|
||||||
|
this.signedOrder = signedOrder;
|
||||||
|
this.isMakerTokenZRX = isMakerTokenZRX;
|
||||||
|
this.transferrableMakerTokenAmount = transferrableMakerTokenAmount;
|
||||||
|
this.transferrableMakerFeeTokenAmount = transferrableMakerFeeTokenAmount;
|
||||||
|
this.remainingMakerTokenAmount = remainingMakerTokenAmount;
|
||||||
|
this.remainingMakerFeeAmount = remainingMakerTokenAmount.times(signedOrder.makerFee)
|
||||||
|
.dividedToIntegerBy(signedOrder.makerTokenAmount);
|
||||||
|
}
|
||||||
|
public computeRemainingMakerFillable(): BigNumber {
|
||||||
|
if (this.hasSufficientFundsForFeeAndTransferAmount()) {
|
||||||
|
return this.remainingMakerTokenAmount;
|
||||||
|
}
|
||||||
|
if (this.signedOrder.makerFee.isZero()) {
|
||||||
|
return BigNumber.min(this.remainingMakerTokenAmount, this.transferrableMakerTokenAmount);
|
||||||
|
}
|
||||||
|
return this.calculatePartiallyFillableMakerTokenAmount();
|
||||||
|
}
|
||||||
|
public computeRemainingTakerFillable(): BigNumber {
|
||||||
|
return this.computeRemainingMakerFillable().times(this.signedOrder.takerTokenAmount)
|
||||||
|
.dividedToIntegerBy(this.signedOrder.makerTokenAmount);
|
||||||
|
}
|
||||||
|
private hasSufficientFundsForFeeAndTransferAmount(): boolean {
|
||||||
|
if (this.isMakerTokenZRX) {
|
||||||
|
const totalZRXTransferAmountRequired = this.remainingMakerTokenAmount.plus(this.remainingMakerFeeAmount);
|
||||||
|
const hasSufficientFunds = this.transferrableMakerTokenAmount.greaterThanOrEqualTo(
|
||||||
|
totalZRXTransferAmountRequired);
|
||||||
|
return hasSufficientFunds;
|
||||||
|
} else {
|
||||||
|
const hasSufficientFundsForTransferAmount = this.transferrableMakerTokenAmount.greaterThanOrEqualTo(
|
||||||
|
this.remainingMakerTokenAmount);
|
||||||
|
const hasSufficientFundsForFeeAmount = this.transferrableMakerFeeTokenAmount.greaterThanOrEqualTo(
|
||||||
|
this.remainingMakerFeeAmount);
|
||||||
|
const hasSufficientFunds = hasSufficientFundsForTransferAmount && hasSufficientFundsForFeeAmount;
|
||||||
|
return hasSufficientFunds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private calculatePartiallyFillableMakerTokenAmount(): BigNumber {
|
||||||
|
// Given an order for 200 wei for 2 ZRXwei fee, find 100 wei for 1 ZRXwei. Order ratio is then 100:1
|
||||||
|
const orderToFeeRatio = this.signedOrder.makerTokenAmount.dividedBy(this.signedOrder.makerFee);
|
||||||
|
// The number of times the maker can fill the order, if each fill only required the transfer of a single
|
||||||
|
// baseUnit of fee tokens.
|
||||||
|
// Given 2 ZRXwei, the maximum amount of times Maker can fill this order, in terms of fees, is 2
|
||||||
|
const fillableTimesInFeeTokenBaseUnits = BigNumber.min(this.transferrableMakerFeeTokenAmount,
|
||||||
|
this.remainingMakerFeeAmount);
|
||||||
|
// The number of times the Maker can fill the order, given the Maker Token Balance
|
||||||
|
// Assuming a balance of 150 wei, and an orderToFeeRatio of 100:1, maker can fill this order 1 time.
|
||||||
|
let fillableTimesInMakerTokenUnits = this.transferrableMakerTokenAmount.dividedBy(orderToFeeRatio);
|
||||||
|
if (this.isMakerTokenZRX) {
|
||||||
|
// If ZRX is the maker token, the Fee and the Maker amount need to be removed from the same pool;
|
||||||
|
// 200 ZRXwei for 2ZRXwei fee can only be filled once (need 202 ZRXwei)
|
||||||
|
const totalZRXTokenPooled = this.transferrableMakerTokenAmount;
|
||||||
|
// The purchasing power here is less as the tokens are taken from the same Pool
|
||||||
|
// For every one number of fills, we have to take an extra ZRX out of the pool
|
||||||
|
fillableTimesInMakerTokenUnits = totalZRXTokenPooled.dividedBy(
|
||||||
|
orderToFeeRatio.plus(new BigNumber(1)));
|
||||||
|
|
||||||
|
}
|
||||||
|
// When Ratio is not fully divisible there can be remainders which cannot be represented, so they are floored.
|
||||||
|
// This can result in a RoundingError being thrown by the Exchange Contract.
|
||||||
|
const partiallyFillableMakerTokenAmount = fillableTimesInMakerTokenUnits
|
||||||
|
.times(this.signedOrder.makerTokenAmount)
|
||||||
|
.dividedToIntegerBy(this.signedOrder.makerFee);
|
||||||
|
const partiallyFillableFeeTokenAmount = fillableTimesInFeeTokenBaseUnits
|
||||||
|
.times(this.signedOrder.makerTokenAmount)
|
||||||
|
.dividedToIntegerBy(this.signedOrder.makerFee);
|
||||||
|
const partiallyFillableAmount = BigNumber.min(partiallyFillableMakerTokenAmount,
|
||||||
|
partiallyFillableFeeTokenAmount);
|
||||||
|
return partiallyFillableAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {utils} from '../utils/utils';
|
|||||||
import {constants} from '../utils/constants';
|
import {constants} from '../utils/constants';
|
||||||
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
||||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||||
|
import {RemainingFillableCalculator} from '../order_watcher/remaining_fillable_calculator';
|
||||||
|
|
||||||
const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001;
|
const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001;
|
||||||
|
|
||||||
@@ -78,12 +79,17 @@ export class OrderStateUtils {
|
|||||||
const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount);
|
const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount);
|
||||||
const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount)
|
const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount)
|
||||||
.dividedToIntegerBy(totalTakerTokenAmount);
|
.dividedToIntegerBy(totalTakerTokenAmount);
|
||||||
const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
|
const transferrableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
|
||||||
const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount);
|
const transferrableFeeTokenAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]);
|
||||||
const remainingFillableTakerTokenAmount = remainingFillableMakerTokenAmount
|
|
||||||
.times(totalTakerTokenAmount)
|
const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress;
|
||||||
.dividedToIntegerBy(totalMakerTokenAmount);
|
const remainingFillableCalculator = new RemainingFillableCalculator(signedOrder,
|
||||||
// TODO: Handle edge case where maker token is ZRX with fee
|
isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount,
|
||||||
|
transferrableFeeTokenAmount,
|
||||||
|
remainingMakerTokenAmount);
|
||||||
|
const remainingFillableMakerTokenAmount = remainingFillableCalculator.computeRemainingMakerFillable();
|
||||||
|
const remainingFillableTakerTokenAmount = remainingFillableCalculator.computeRemainingTakerFillable();
|
||||||
const orderRelevantState = {
|
const orderRelevantState = {
|
||||||
makerBalance,
|
makerBalance,
|
||||||
makerProxyAllowance,
|
makerProxyAllowance,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { FillScenarios } from './utils/fill_scenarios';
|
|||||||
import {DoneCallback} from '../src/types';
|
import {DoneCallback} from '../src/types';
|
||||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||||
import {reportCallbackErrors} from './utils/report_callback_errors';
|
import {reportCallbackErrors} from './utils/report_callback_errors';
|
||||||
|
import {constants as constants} from './utils/constants';
|
||||||
|
|
||||||
const TIMEOUT_MS = 150;
|
const TIMEOUT_MS = 150;
|
||||||
|
|
||||||
@@ -47,7 +48,8 @@ describe('OrderStateWatcher', () => {
|
|||||||
let taker: string;
|
let taker: string;
|
||||||
let web3Wrapper: Web3Wrapper;
|
let web3Wrapper: Web3Wrapper;
|
||||||
let signedOrder: SignedOrder;
|
let signedOrder: SignedOrder;
|
||||||
const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), 18);
|
const decimals = constants.ZRX_DECIMALS;
|
||||||
|
const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals);
|
||||||
before(async () => {
|
before(async () => {
|
||||||
web3 = web3Factory.create();
|
web3 = web3Factory.create();
|
||||||
zeroEx = new ZeroEx(web3.currentProvider);
|
zeroEx = new ZeroEx(web3.currentProvider);
|
||||||
@@ -254,15 +256,15 @@ describe('OrderStateWatcher', () => {
|
|||||||
describe('remainingFillable(M|T)akerTokenAmount', () => {
|
describe('remainingFillable(M|T)akerTokenAmount', () => {
|
||||||
it('should calculate correct remaining fillable', (done: DoneCallback) => {
|
it('should calculate correct remaining fillable', (done: DoneCallback) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const takerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(10), 18);
|
const takerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(10), decimals);
|
||||||
const makerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(20), 18);
|
const makerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(20), decimals);
|
||||||
signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||||
makerToken.address, takerToken.address, maker, taker, makerFillableAmount,
|
makerToken.address, takerToken.address, maker, taker, makerFillableAmount,
|
||||||
takerFillableAmount,
|
takerFillableAmount,
|
||||||
);
|
);
|
||||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||||
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
||||||
const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18);
|
const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
|
||||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||||
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
@@ -273,9 +275,9 @@ describe('OrderStateWatcher', () => {
|
|||||||
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
||||||
const orderRelevantState = validOrderState.orderRelevantState;
|
const orderRelevantState = validOrderState.orderRelevantState;
|
||||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||||
ZeroEx.toBaseUnitAmount(new BigNumber(16), 18));
|
ZeroEx.toBaseUnitAmount(new BigNumber(16), decimals));
|
||||||
expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
|
expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
|
||||||
ZeroEx.toBaseUnitAmount(new BigNumber(8), 18));
|
ZeroEx.toBaseUnitAmount(new BigNumber(8), decimals));
|
||||||
if (eventCount === 2) {
|
if (eventCount === 2) {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
@@ -295,7 +297,7 @@ describe('OrderStateWatcher', () => {
|
|||||||
|
|
||||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||||
|
|
||||||
const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), 18);
|
const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals);
|
||||||
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
||||||
|
|
||||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||||
@@ -319,11 +321,12 @@ describe('OrderStateWatcher', () => {
|
|||||||
|
|
||||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||||
|
|
||||||
const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18);
|
const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals);
|
||||||
const transferAmount = makerBalance.sub(remainingAmount);
|
const transferAmount = makerBalance.sub(remainingAmount);
|
||||||
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
||||||
|
|
||||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||||
|
expect(orderState.isValid).to.be.true();
|
||||||
const validOrderState = orderState as OrderStateValid;
|
const validOrderState = orderState as OrderStateValid;
|
||||||
const orderRelevantState = validOrderState.orderRelevantState;
|
const orderRelevantState = validOrderState.orderRelevantState;
|
||||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||||
@@ -337,6 +340,88 @@ describe('OrderStateWatcher', () => {
|
|||||||
makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
|
makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
|
||||||
})().catch(done);
|
})().catch(done);
|
||||||
});
|
});
|
||||||
|
it('should equal remaining amount when partially cancelled and order has fees', (done: DoneCallback) => {
|
||||||
|
(async () => {
|
||||||
|
const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), decimals);
|
||||||
|
const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals);
|
||||||
|
const feeRecipient = taker;
|
||||||
|
signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||||
|
makerToken.address, takerToken.address, makerFee, takerFee, maker,
|
||||||
|
taker, fillableAmount, feeRecipient);
|
||||||
|
|
||||||
|
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||||
|
|
||||||
|
const remainingTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(4), decimals);
|
||||||
|
const transferTokenAmount = makerFee.sub(remainingTokenAmount);
|
||||||
|
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
||||||
|
|
||||||
|
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||||
|
expect(orderState.isValid).to.be.true();
|
||||||
|
const validOrderState = orderState as OrderStateValid;
|
||||||
|
const orderRelevantState = validOrderState.orderRelevantState;
|
||||||
|
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||||
|
remainingTokenAmount);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
zeroEx.orderStateWatcher.subscribe(callback);
|
||||||
|
await zeroEx.exchange.cancelOrderAsync(signedOrder, transferTokenAmount);
|
||||||
|
})().catch(done);
|
||||||
|
});
|
||||||
|
it('should equal ratio amount when fee balance is lowered', (done: DoneCallback) => {
|
||||||
|
(async () => {
|
||||||
|
const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), decimals);
|
||||||
|
const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals);
|
||||||
|
const feeRecipient = taker;
|
||||||
|
signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||||
|
makerToken.address, takerToken.address, makerFee, takerFee, maker,
|
||||||
|
taker, fillableAmount, feeRecipient);
|
||||||
|
|
||||||
|
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||||
|
|
||||||
|
const remainingFeeAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals);
|
||||||
|
const transferFeeAmount = makerFee.sub(remainingFeeAmount);
|
||||||
|
|
||||||
|
const remainingTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(4), decimals);
|
||||||
|
const transferTokenAmount = makerFee.sub(remainingTokenAmount);
|
||||||
|
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
||||||
|
|
||||||
|
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||||
|
const validOrderState = orderState as OrderStateValid;
|
||||||
|
const orderRelevantState = validOrderState.orderRelevantState;
|
||||||
|
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||||
|
remainingFeeAmount);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
zeroEx.orderStateWatcher.subscribe(callback);
|
||||||
|
await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, maker, remainingFeeAmount);
|
||||||
|
await zeroEx.token.transferAsync(
|
||||||
|
makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferTokenAmount);
|
||||||
|
})().catch(done);
|
||||||
|
});
|
||||||
|
it('should calculate full amount when all available and non-divisible', (done: DoneCallback) => {
|
||||||
|
(async () => {
|
||||||
|
const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), decimals);
|
||||||
|
const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
|
||||||
|
const feeRecipient = taker;
|
||||||
|
signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||||
|
makerToken.address, takerToken.address, makerFee, takerFee, maker,
|
||||||
|
taker, fillableAmount, feeRecipient);
|
||||||
|
|
||||||
|
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||||
|
await zeroEx.orderStateWatcher.addOrderAsync(signedOrder);
|
||||||
|
|
||||||
|
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||||
|
const validOrderState = orderState as OrderStateValid;
|
||||||
|
const orderRelevantState = validOrderState.orderRelevantState;
|
||||||
|
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||||
|
fillableAmount);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
zeroEx.orderStateWatcher.subscribe(callback);
|
||||||
|
await zeroEx.token.setProxyAllowanceAsync(
|
||||||
|
makerToken.address, maker, ZeroEx.toBaseUnitAmount(new BigNumber(100), decimals));
|
||||||
|
})().catch(done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
|
it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
176
packages/0x.js/test/remaining_fillable_calculator_test.ts
Normal file
176
packages/0x.js/test/remaining_fillable_calculator_test.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import * as chai from 'chai';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { chaiSetup } from './utils/chai_setup';
|
||||||
|
import { RemainingFillableCalculator } from '../src/order_watcher/remaining_fillable_calculator';
|
||||||
|
import { SignedOrder, ECSignature } from '../src/types';
|
||||||
|
import { TokenUtils } from './utils/token_utils';
|
||||||
|
import { ZeroEx } from '../src/0x';
|
||||||
|
|
||||||
|
chaiSetup.configure();
|
||||||
|
const expect = chai.expect;
|
||||||
|
|
||||||
|
describe('RemainingFillableCalculator', () => {
|
||||||
|
let calculator: RemainingFillableCalculator;
|
||||||
|
let signedOrder: SignedOrder;
|
||||||
|
let transferrableMakerTokenAmount: BigNumber;
|
||||||
|
let transferrableMakerFeeTokenAmount: BigNumber;
|
||||||
|
let remainingMakerTokenAmount: BigNumber;
|
||||||
|
let makerAmount: BigNumber;
|
||||||
|
let takerAmount: BigNumber;
|
||||||
|
let makerFeeAmount: BigNumber;
|
||||||
|
let isMakerTokenZRX: boolean;
|
||||||
|
const makerToken: string = '0x1';
|
||||||
|
const takerToken: string = '0x2';
|
||||||
|
const decimals: number = 4;
|
||||||
|
const zero: BigNumber = new BigNumber(0);
|
||||||
|
const zeroAddress = '0x0';
|
||||||
|
const signature: ECSignature = { v: 27, r: '', s: ''};
|
||||||
|
beforeEach(async () => {
|
||||||
|
[makerAmount, takerAmount, makerFeeAmount] = [ZeroEx.toBaseUnitAmount(new BigNumber(50), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals)];
|
||||||
|
[transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount] = [
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(50), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals)];
|
||||||
|
});
|
||||||
|
function buildSignedOrder(): SignedOrder {
|
||||||
|
return { ecSignature: signature,
|
||||||
|
exchangeContractAddress: zeroAddress,
|
||||||
|
feeRecipient: zeroAddress,
|
||||||
|
maker: zeroAddress,
|
||||||
|
taker: zeroAddress,
|
||||||
|
makerFee: makerFeeAmount,
|
||||||
|
takerFee: zero,
|
||||||
|
makerTokenAmount: makerAmount,
|
||||||
|
takerTokenAmount: takerAmount,
|
||||||
|
makerTokenAddress: makerToken,
|
||||||
|
takerTokenAddress: takerToken,
|
||||||
|
salt: zero,
|
||||||
|
expirationUnixTimestampSec: zero };
|
||||||
|
}
|
||||||
|
describe('Maker token is NOT ZRX', () => {
|
||||||
|
before(async () => {
|
||||||
|
isMakerTokenZRX = false;
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when unfilled and funds available', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when partially filled and funds available', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
remainingMakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals);
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
|
||||||
|
});
|
||||||
|
it('calculates the amount to be 0 when all fee funds are transferred', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
transferrableMakerFeeTokenAmount = zero;
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero);
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when balance is less than remaining fillable', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
const partiallyFilledAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount);
|
||||||
|
transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount);
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount);
|
||||||
|
});
|
||||||
|
describe('Order to Fee Ratio is < 1', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
[makerAmount, takerAmount, makerFeeAmount] = [ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(6), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(6), decimals)];
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when funds unavailable', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||||
|
const transferredAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
|
||||||
|
transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount);
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount,
|
||||||
|
remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Ratio is not evenly divisble', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
[makerAmount, takerAmount, makerFeeAmount] = [ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(7), decimals),
|
||||||
|
ZeroEx.toBaseUnitAmount(new BigNumber(7), decimals)];
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when funds unavailable', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||||
|
const transferredAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
|
||||||
|
transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount);
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount,
|
||||||
|
remainingMakerTokenAmount);
|
||||||
|
const calculatedFillableAmount = calculator.computeRemainingMakerFillable();
|
||||||
|
expect(calculatedFillableAmount.lessThanOrEqualTo(transferrableMakerTokenAmount)).to.be.true();
|
||||||
|
expect(calculatedFillableAmount).to.be.bignumber.greaterThan(new BigNumber(0));
|
||||||
|
const orderToFeeRatio = signedOrder.makerTokenAmount.dividedBy(signedOrder.makerFee);
|
||||||
|
const calculatedFeeAmount = calculatedFillableAmount.dividedBy(orderToFeeRatio);
|
||||||
|
expect(calculatedFeeAmount).to.be.bignumber.lessThan(transferrableMakerFeeTokenAmount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Maker Token is ZRX', () => {
|
||||||
|
before(async () => {
|
||||||
|
isMakerTokenZRX = true;
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when unfilled and funds available', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
transferrableMakerTokenAmount = makerAmount.plus(makerFeeAmount);
|
||||||
|
transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount;
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when partially filled and funds available', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
remainingMakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals);
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
|
||||||
|
});
|
||||||
|
it('calculates the amount to be 0 when all fee funds are transferred', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
transferrableMakerTokenAmount = zero;
|
||||||
|
transferrableMakerFeeTokenAmount = zero;
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero);
|
||||||
|
});
|
||||||
|
it('calculates the correct amount when balance is less than remaining fillable', () => {
|
||||||
|
signedOrder = buildSignedOrder();
|
||||||
|
const partiallyFilledAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
|
||||||
|
remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount);
|
||||||
|
transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount);
|
||||||
|
transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount;
|
||||||
|
|
||||||
|
const orderToFeeRatio = signedOrder.makerTokenAmount.dividedToIntegerBy(signedOrder.makerFee);
|
||||||
|
const expectedFillableAmount = new BigNumber(450980);
|
||||||
|
calculator = new RemainingFillableCalculator(signedOrder, isMakerTokenZRX,
|
||||||
|
transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount, remainingMakerTokenAmount);
|
||||||
|
const calculatedFillableAmount = calculator.computeRemainingMakerFillable();
|
||||||
|
const numberOfFillsInRatio = calculatedFillableAmount.dividedToIntegerBy(orderToFeeRatio);
|
||||||
|
const calculatedFillableAmountPlusFees = calculatedFillableAmount.plus(numberOfFillsInRatio);
|
||||||
|
expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(transferrableMakerTokenAmount);
|
||||||
|
expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(remainingMakerTokenAmount);
|
||||||
|
expect(calculatedFillableAmount).to.be.bignumber.equal(expectedFillableAmount);
|
||||||
|
expect(numberOfFillsInRatio.decimalPlaces()).to.be.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,4 +5,5 @@ export const constants = {
|
|||||||
TESTRPC_NETWORK_ID: 50,
|
TESTRPC_NETWORK_ID: 50,
|
||||||
KOVAN_RPC_URL: 'https://kovan.infura.io',
|
KOVAN_RPC_URL: 'https://kovan.infura.io',
|
||||||
ROPSTEN_RPC_URL: 'https://ropsten.infura.io',
|
ROPSTEN_RPC_URL: 'https://ropsten.infura.io',
|
||||||
|
ZRX_DECIMALS: 18,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user