Files
protocol/apps-node/rfq-api/test/utils/quote_comparison_utils_test.ts

623 lines
25 KiB
TypeScript

import { OtcOrder, SignatureType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { DEFAULT_MIN_EXPIRY_DURATION_MS, ONE_SECOND_MS, ZERO } from '../../src/core/constants';
import { FirmOtcQuote, IndicativeQuote } from '../../src/core/types';
import { getBestQuote } from '../../src/utils/quote_comparison_utils';
const NEVER_EXPIRES = new BigNumber(999999999999999);
function createBaseQuote(): FirmOtcQuote {
return {
order: new OtcOrder({
makerAmount: ZERO,
takerAmount: ZERO,
}),
kind: 'otc',
makerSignature: {
signatureType: SignatureType.Invalid,
v: 0,
r: '0x1',
s: '0x2',
},
makerUri: 'someuri.xyz',
};
}
describe('Quote Comparison Utils', () => {
describe('getBestQuote', () => {
const makerToken = 'DAI';
const takerToken = 'SUSD';
const assetFillAmount = new BigNumber(100);
const validityWindowMs = DEFAULT_MIN_EXPIRY_DURATION_MS;
const inThirtySeconds = new BigNumber(Math.round((Date.now() + ONE_SECOND_MS * 30) / ONE_SECOND_MS));
describe('IndicativeQuotes when selling', () => {
// Given
const BASE_INDICATIVE_QUOTE = {
makerUri: 'http://makeruri',
maker: '0xmaker',
makerToken,
takerToken,
expiry: NEVER_EXPIRES,
};
describe('sells', () => {
const isSelling = true;
const partialFillQuote: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(55),
takerAmount: new BigNumber(50),
};
const fullQuoteBadPricing: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(99),
takerAmount: new BigNumber(100),
};
const fullQuoteOkPricing: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(105),
takerAmount: new BigNumber(100),
};
const fullQuoteGreatPricing: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(125),
takerAmount: new BigNumber(100),
};
const wrongPair: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
expiry: NEVER_EXPIRES,
makerAmount: new BigNumber(125),
makerToken: takerToken,
makerUri: 'http://makeruri',
takerAmount: new BigNumber(100),
takerToken: makerToken,
};
const expiresInOneMinute: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(125),
takerAmount: new BigNumber(100),
expiry: inThirtySeconds,
};
const tests = [
{
name: 'should return null when no quotes valid',
quotes: [partialFillQuote],
expectations: {
isNull: true,
makerAmount: undefined,
takerAmount: undefined,
},
},
{
name: 'should only select quotes that are 100% filled',
quotes: [partialFillQuote, fullQuoteBadPricing],
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
{
name: 'should select quote with best pricing',
quotes: [fullQuoteBadPricing, fullQuoteGreatPricing, fullQuoteOkPricing],
expectations: {
isNull: false,
makerAmount: 125,
takerAmount: 100,
},
},
{
name: 'should ignore quotes with the wrong pair',
quotes: [fullQuoteBadPricing, wrongPair],
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
{
name: 'should ignore quotes that expire too soon',
quotes: [fullQuoteBadPricing, expiresInOneMinute],
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
];
tests.forEach(({ name, quotes, expectations }) => {
it(name, () => {
const bestQuote = getBestQuote<IndicativeQuote>(
quotes,
isSelling,
takerToken,
makerToken,
assetFillAmount,
validityWindowMs,
);
if (bestQuote === null) {
expect(expectations?.isNull).toEqual(true);
return;
}
expect(expectations?.isNull).toEqual(false);
expect(bestQuote.makerAmount.toNumber()).toEqual(expectations?.makerAmount);
expect(bestQuote.takerAmount.toNumber()).toEqual(expectations?.takerAmount);
});
});
});
describe('buys', () => {
const isSelling = false;
const partialFillQuote: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(55),
takerAmount: new BigNumber(50),
};
const fullQuoteBadPricing: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(125),
};
const fullQuoteOkPricing: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(120),
};
const fullQuoteGreatPricing: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(80),
};
const wrongPair: IndicativeQuote = {
...BASE_INDICATIVE_QUOTE,
makerToken: takerToken,
takerToken: makerToken,
expiry: NEVER_EXPIRES,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(80),
};
const expiresInOneMinute = {
...BASE_INDICATIVE_QUOTE,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(80),
expiry: inThirtySeconds,
};
const tests = [
{
name: 'should return null when no quotes valid',
quotes: [partialFillQuote],
expectations: {
isNull: true,
makerAmount: undefined,
takerAmount: undefined,
},
},
{
name: 'should only select quotes that are 100% filled',
quotes: [partialFillQuote, fullQuoteBadPricing],
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
{
name: 'should select quote with best pricing',
quotes: [fullQuoteBadPricing, fullQuoteGreatPricing, fullQuoteOkPricing],
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 80,
},
},
{
name: 'should ignore quotes with the wrong pair',
quotes: [fullQuoteBadPricing, wrongPair],
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
{
name: 'should ignore quotes that expire too soon',
quotes: [fullQuoteBadPricing, expiresInOneMinute],
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
];
tests.forEach(({ name, quotes, expectations }) => {
it(name, () => {
const bestQuote = getBestQuote<IndicativeQuote>(
quotes,
isSelling,
takerToken,
makerToken,
assetFillAmount,
validityWindowMs,
);
if (bestQuote === null) {
expect(expectations?.isNull).toEqual(true);
return;
}
expect(expectations?.isNull).toEqual(false);
expect(bestQuote.makerAmount.toNumber()).toEqual(expectations?.makerAmount);
expect(bestQuote.takerAmount.toNumber()).toEqual(expectations?.takerAmount);
});
});
});
});
describe('FirmQuotes', () => {
// Given
const BASE_QUOTE = createBaseQuote();
const BASE_ORDER = new OtcOrder({
makerToken,
takerToken,
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(NEVER_EXPIRES, ZERO, ZERO),
});
describe('sells', () => {
const isSelling = true;
const partialFillQuote: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(55),
takerAmount: new BigNumber(50),
}),
};
const fullQuoteBadPricing: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(99),
takerAmount: new BigNumber(100),
}),
};
const fullQuoteOkPricing: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(105),
takerAmount: new BigNumber(100),
}),
};
const fullQuoteGreatPricing: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(125),
takerAmount: new BigNumber(100),
}),
};
const wrongPair: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerToken: takerToken,
takerToken: makerToken,
makerAmount: new BigNumber(125),
takerAmount: new BigNumber(100),
}),
};
const expiresInOneMinute: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(125),
takerAmount: new BigNumber(100),
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(inThirtySeconds, ZERO, ZERO),
}),
};
// TODO (MKR-671): uncomment once filter is enabled
const validMakerBalances: BigNumber[] = [new BigNumber(150), new BigNumber(150)];
const invalidMakerBalances: BigNumber[] = [new BigNumber(150), new BigNumber(50)];
const tests = [
{
name: 'should return null when no quotes valid',
args: {
quotes: [partialFillQuote],
},
expectations: {
isNull: true,
makerAmount: undefined,
takerAmount: undefined,
},
},
{
name: 'should only select quotes that are 100% filled',
args: {
quotes: [partialFillQuote, fullQuoteBadPricing],
},
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
{
name: 'should select quote with best pricing',
args: {
quotes: [fullQuoteBadPricing, fullQuoteGreatPricing, fullQuoteOkPricing],
},
expectations: {
isNull: false,
makerAmount: 125,
takerAmount: 100,
},
},
{
name: 'should ignore quotes with the wrong pair',
args: {
quotes: [fullQuoteBadPricing, wrongPair],
},
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
{
name: 'should ignore quotes that expire too soon',
args: {
quotes: [fullQuoteBadPricing, expiresInOneMinute],
},
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
// TODO (MKR-671): uncomment once filter is enabled
{
name: 'should not ignore quotes if makers have enough balances',
args: {
quotes: [fullQuoteBadPricing, fullQuoteOkPricing],
quotedMakerBalances: validMakerBalances,
},
expectations: {
isNull: false,
makerAmount: 105,
takerAmount: 100,
},
},
{
name: 'should ignore quotes if makers do not have enough balances',
args: {
quotes: [fullQuoteBadPricing, fullQuoteOkPricing],
quotedMakerBalances: invalidMakerBalances,
},
expectations: {
isNull: false,
makerAmount: 99,
takerAmount: 100,
},
},
];
tests.forEach(({ name, args, expectations }) => {
it(name, () => {
const bestQuote = getBestQuote(
args?.quotes,
isSelling,
takerToken,
makerToken,
assetFillAmount,
validityWindowMs,
args?.quotedMakerBalances,
);
if (bestQuote === null) {
expect(expectations?.isNull).toEqual(true);
return;
}
expect(expectations?.isNull).toEqual(false);
expect(bestQuote.order.makerAmount.toNumber()).toEqual(expectations?.makerAmount);
expect(bestQuote.order.takerAmount.toNumber()).toEqual(expectations?.takerAmount);
});
});
});
describe('buys', () => {
const isSelling = false;
const partialFillQuote: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(55),
takerAmount: new BigNumber(50),
}),
};
const fullQuoteBadPricing: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(125),
}),
};
const fullQuoteOkPricing: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(120),
}),
};
const fullQuoteGreatPricing: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(80),
}),
};
const wrongPair: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerToken: takerToken,
takerToken: makerToken,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(80),
}),
};
const expiresInOneMinute: FirmOtcQuote = {
...BASE_QUOTE,
order: new OtcOrder({
...BASE_ORDER,
makerAmount: new BigNumber(100),
takerAmount: new BigNumber(80),
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(inThirtySeconds, ZERO, ZERO),
}),
};
// TODO (MKR-671): uncomment once filter is enabled
const validMakerBalances: BigNumber[] = [new BigNumber(150), new BigNumber(150)];
const invalidMakerBalances: BigNumber[] = [new BigNumber(150), new BigNumber(50)];
const tests = [
{
name: 'should return null when no quotes valid',
args: {
quotes: [partialFillQuote],
},
expectations: {
isNull: true,
makerAmount: undefined,
takerAmount: undefined,
},
},
{
name: 'should only select quotes that are 100% filled',
args: {
quotes: [partialFillQuote, fullQuoteBadPricing],
},
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
{
name: 'should select quote with best pricing',
args: {
quotes: [fullQuoteBadPricing, fullQuoteGreatPricing, fullQuoteOkPricing],
},
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 80,
},
},
{
name: 'should ignore quotes with the wrong pair',
args: {
quotes: [fullQuoteBadPricing, wrongPair],
},
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
{
name: 'should ignore quotes that expire too soon',
args: {
quotes: [fullQuoteBadPricing, expiresInOneMinute],
},
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
// TODO (MKR-671): uncomment once filter is enabled
{
name: 'should not ignore quotes if makers have enough balances',
args: {
quotes: [fullQuoteBadPricing, fullQuoteOkPricing],
quotedMakerBalances: validMakerBalances,
},
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 120,
},
},
{
name: 'should ignore quotes if makers do not have enough balances',
args: {
quotes: [fullQuoteBadPricing, fullQuoteOkPricing],
quotedMakerBalances: invalidMakerBalances,
},
expectations: {
isNull: false,
makerAmount: 100,
takerAmount: 125,
},
},
];
tests.forEach(({ name, args, expectations }) => {
it(name, () => {
const bestQuote = getBestQuote(
args?.quotes,
isSelling,
takerToken,
makerToken,
assetFillAmount,
validityWindowMs,
args?.quotedMakerBalances,
);
if (bestQuote === null) {
expect(expectations?.isNull).toEqual(true);
return;
}
expect(expectations?.isNull).toEqual(false);
expect(bestQuote.order.makerAmount.toNumber()).toEqual(expectations?.makerAmount);
expect(bestQuote.order.takerAmount.toNumber()).toEqual(expectations?.takerAmount);
});
});
});
});
});
});