Files
protocol/packages/asset-swapper/test/quote_report_generator_test.ts
Jacob Evans 3f4bb933d1 feat: v4 final (#136)
* v4 FillQuoteTransformer (#104)

* Update FQT to support v4 orders

* `@0x/contracts-zero-ex`: Tweak FQT
`@0x/contracts-zero-ex`: Drop `ERC20BridgeTransfer` event and add `PartialQuoteFill` event.

* `@0x/contracts-utils`: Add `LibSafeMathV06.downcastToUint128()`

* `@0x/protocol-utils`: Update transformer utils for V4 FQT

* `@0x/contracts-zero-ex`: Fixing FQT tests...

* `@0x/contracts-zero-ex`: rename FQT bridge event

* `@0x/contracts-zero-ex`: Un-`only` tests

* `@0x/migrations`: Update `BridgeAdapter` deployment

* `@0x/contracts-integrations`: Delete `mtx_tests`

* `@0x/protocol-utils`: Address review comments

* `@0x/contracts-zero-ex`: Address review comments

* `@0x/migrations`: Update migrations

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* v4: Asset-swapper (main branch) (#113)

* refactor quote_requestor

* WIP v4/asset-swapper: Clean up SwapQuoter and remove @0x/orderbook

* Start replacing SignedOrder everywhere

* wip: new order type

* wip

* remove order-utils from most places

* hack: Play around with VerboseX types (#119)

* hack: Play around with VerboseX types

* More hacks

* Fix up the bridgeData encodings

* Rework Orderbook return type

* feat: Don't charge a protocol fee for RFQ orders WIP (#121)

* fix simple build errors

* simplify types a little

* remove SwapQuoteCalculator: unnecessary abstraction

* Fix all ./src build errors; make types consistent

* export more types for use in 0x API; modify Orderbook interface

* stop overriding APIOrder

* feat: RFQ v4 + consolidated bridge encoders (#125)

* feat: check if taker address is contract

* Rework bridge data

* Worst case adjustments

* RFQT v4

* Future/v4 validate orders (#126)

* RFQT v4

* v4 validate native orders

* use default invalid signature

* refactor rfqt validations in swap quoter

* fix types

* fix RFQT unlisted api key

* remove priceAwareRFQFlag

* adjust maker/taker amounts

* update JSON schemas

* filter zero fillable orders

Co-authored-by: xianny <xianny@gmail.com>

* fix type export

Co-authored-by: xianny <xianny@gmail.com>

* remove order-utils as much as possible

* work on tests compile

* Comment out quote reporter test

* updated tests

* restore order-utils accidental changes

* some lints

* Remove old fill_test

* ts lint disable for now

* update quote report

* Re-enable quote report tests

* make fill data required field

* fix lint

* type guards

* force fillData as required

* fix lint

* fix naming

* exports

* adjust MultiBridge by slippage

* cleanups (checkpoint 1)

* cleanup types (checkpoint #2)

* remove unused deps

* `@0x/contract-addresses`: Deploy new FQT (#129)

Co-authored-by: Lawrence Forman <me@merklejerk.com>

* commit bump to republish

* DRY up the rfqt mocker

* fix: Balancer load top pools (#131)

* fix: Balancer load top 250 pools

* refetch top pools on an interval

Co-authored-by: Jacob Evans <jacob@dekz.net>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* Update post rebase

* prettier

* Remove test helpers exported in asset-swapper

* Clean up from review comments

* prettier

* lint

* recreate rfqt mocker

* change merge and INVALID_SIGNATURE

Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: Xianny <8582774+xianny@users.noreply.github.com>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
2021-02-10 19:20:15 +10:00

455 lines
18 KiB
TypeScript

// tslint:disable:custom-no-magic-numbers
// tslint:disable:no-object-literal-type-assertion
import { FillQuoteTransformerOrderType, LimitOrder, LimitOrderFields, RfqOrder } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill,
NativeFillData,
NativeLimitOrderFillData,
NativeRfqOrderFillData,
} from '../src/utils/market_operation_utils/types';
import { QuoteRequestor } from '../src/utils/quote_requestor';
import {
BridgeQuoteReportEntry,
generateQuoteReport,
MultiHopQuoteReportEntry,
NativeLimitOrderQuoteReportEntry,
NativeRfqOrderQuoteReportEntry,
QuoteReportEntry,
} from './../src/utils/quote_report_generator';
import { chaiSetup } from './utils/chai_setup';
import { getRandomAmount, getRandomSignature } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): NativeCollapsedFill {
const fillData = {
order: order.order,
signature: order.signature,
maxTakerTokenFillAmount: order.fillableTakerAmount,
};
return {
sourcePathId: hexUtils.random(),
source: ERC20BridgeSource.Native,
type: order.type,
input: order.order.takerAmount,
output: order.order.makerAmount,
fillData:
order.type === FillQuoteTransformerOrderType.Limit
? (fillData as NativeLimitOrderFillData)
: (fillData as NativeRfqOrderFillData),
subFills: [],
};
}
describe('generateQuoteReport', async () => {
it('should generate report properly for sell', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
fillData: {},
};
const kyberSample2: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10003),
output: new BigNumber(10004),
fillData: {},
};
const uniswapSample1: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10003),
output: new BigNumber(10004),
fillData: {},
};
const uniswapSample2: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10005),
output: new BigNumber(10006),
fillData: {},
};
const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2];
const orderbookOrder1: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(1000) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(1000),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const orderbookOrder2: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(198) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(99), // takerAmount minus 99
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const rfqtOrder1: NativeOrderWithFillableAmounts = {
order: new RfqOrder({ takerAmount: new BigNumber(100) }),
type: FillQuoteTransformerOrderType.Rfq,
fillableTakerAmount: new BigNumber(100),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const rfqtOrder2: NativeOrderWithFillableAmounts = {
order: new RfqOrder({ takerAmount: new BigNumber(1101) }),
type: FillQuoteTransformerOrderType.Rfq,
fillableTakerAmount: new BigNumber(1001),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const nativeOrders: NativeOrderWithFillableAmounts[] = [
orderbookOrder1,
rfqtOrder1,
rfqtOrder2,
orderbookOrder2,
];
// generate path
const uniswap2Fill: CollapsedFill = {
...uniswapSample2,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const kyber2Fill: CollapsedFill = {
...kyberSample2,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2);
const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2);
const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill];
// quote generator mock
const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>();
quoteRequestor
.setup(qr => qr.getMakerUriForSignature(rfqtOrder1.signature))
.returns(() => {
return 'https://rfqt1.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForSignature(rfqtOrder2.signature))
.returns(() => {
return 'https://rfqt2.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
const orderReport = generateQuoteReport(
marketOperation,
dexQuotes,
[],
nativeOrders,
pathGenerated,
undefined,
quoteRequestor.object,
);
const rfqtOrder1Source: NativeRfqOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder1.order.makerAmount,
takerAmount: rfqtOrder1.order.takerAmount,
fillableTakerAmount: rfqtOrder1.fillableTakerAmount,
isRfqt: true,
makerUri: 'https://rfqt1.provider.club',
fillData: {
order: rfqtOrder1.order,
} as NativeRfqOrderFillData,
};
const rfqtOrder2Source: NativeRfqOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder2.order.makerAmount,
takerAmount: rfqtOrder2.order.takerAmount,
fillableTakerAmount: rfqtOrder2.fillableTakerAmount,
isRfqt: true,
makerUri: 'https://rfqt2.provider.club',
fillData: {
order: rfqtOrder2.order,
} as NativeRfqOrderFillData,
};
const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder1.order,
} as NativeLimitOrderFillData,
};
const orderbookOrder2Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.order.makerAmount,
takerAmount: orderbookOrder2.order.takerAmount,
fillableTakerAmount: orderbookOrder2.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder2.order,
} as NativeLimitOrderFillData,
};
const uniswap1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.output,
takerAmount: uniswapSample1.input,
fillData: {},
};
const uniswap2Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample2.output,
takerAmount: uniswapSample2.input,
fillData: {},
};
const kyber1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
fillData: {},
};
const kyber2Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample2.output,
takerAmount: kyberSample2.input,
fillData: {},
};
const expectedSourcesConsidered: QuoteReportEntry[] = [
kyber1Source,
kyber2Source,
uniswap1Source,
uniswap2Source,
orderbookOrder1Source,
rfqtOrder1Source,
rfqtOrder2Source,
orderbookOrder2Source,
];
const expectedSourcesDelivered: QuoteReportEntry[] = [
rfqtOrder2Source,
orderbookOrder2Source,
uniswap2Source,
kyber2Source,
];
expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`);
expectEqualQuoteReportEntries(orderReport.sourcesDelivered, expectedSourcesDelivered, `sourcesDelivered`);
quoteRequestor.verifyAll();
});
it('should handle properly for buy without quoteRequestor', () => {
const marketOperation: MarketOperation = MarketOperation.Buy;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
fillData: {},
};
const uniswapSample1: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10003),
output: new BigNumber(10004),
fillData: {},
};
const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1];
const orderbookOrder1: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(1101) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(1000),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const orderbookOrder2: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(5101) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(5000), // takerAmount minus 99
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const nativeOrders = [orderbookOrder1, orderbookOrder2];
// generate path
const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1);
const uniswap1Fill: CollapsedFill = {
...uniswapSample1,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const kyber1Fill: CollapsedFill = {
...kyberSample1,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill];
const orderReport = generateQuoteReport(marketOperation, dexQuotes, [], nativeOrders, pathGenerated);
const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder1.order,
} as NativeLimitOrderFillData,
};
const orderbookOrder2Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.order.makerAmount,
takerAmount: orderbookOrder2.order.takerAmount,
fillableTakerAmount: orderbookOrder2.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder2.order,
} as NativeLimitOrderFillData,
};
const uniswap1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.input,
takerAmount: uniswapSample1.output,
fillData: {},
};
const kyber1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.input,
takerAmount: kyberSample1.output,
fillData: {},
};
const expectedSourcesConsidered: QuoteReportEntry[] = [
kyber1Source,
uniswap1Source,
orderbookOrder1Source,
orderbookOrder2Source,
];
const expectedSourcesDelivered: QuoteReportEntry[] = [orderbookOrder1Source, uniswap1Source, kyber1Source];
expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`);
expectEqualQuoteReportEntries(orderReport.sourcesDelivered, expectedSourcesDelivered, `sourcesDelivered`);
});
it('should correctly generate report for a two-hop quote', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
fillData: {},
};
const orderbookOrder1: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(1101) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(1000),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const twoHopFillData: MultiHopFillData = {
intermediateToken: hexUtils.random(20),
firstHopSource: {
source: ERC20BridgeSource.Balancer,
fillData: {},
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
handleRevert: _c => [],
},
secondHopSource: {
source: ERC20BridgeSource.Curve,
fillData: {},
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
handleRevert: _c => [],
},
};
const twoHopSample: DexSample<MultiHopFillData> = {
source: ERC20BridgeSource.MultiHop,
input: new BigNumber(3005),
output: new BigNumber(3006),
fillData: twoHopFillData,
};
const orderReport = generateQuoteReport(
marketOperation,
[kyberSample1],
[twoHopSample],
[orderbookOrder1],
twoHopSample,
);
const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder1.order,
} as NativeLimitOrderFillData,
};
const kyber1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
fillData: {},
};
const twoHopSource: MultiHopQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: twoHopSample.output,
takerAmount: twoHopSample.input,
hopSources: [ERC20BridgeSource.Balancer, ERC20BridgeSource.Curve],
fillData: twoHopFillData,
};
const expectedSourcesConsidered: QuoteReportEntry[] = [kyber1Source, orderbookOrder1Source, twoHopSource];
expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`);
expect(orderReport.sourcesDelivered.length).to.eql(1);
expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource);
});
});
function expectEqualQuoteReportEntries(
actual: QuoteReportEntry[],
expected: QuoteReportEntry[],
variableName: string = 'quote report entries',
): void {
expect(actual.length).to.eql(expected.length);
actual.forEach((actualEntry, idx) => {
const expectedEntry = expected[idx];
// remove fillable values
if (actualEntry.liquiditySource === ERC20BridgeSource.Native) {
actualEntry.fillData.order = _.omit(actualEntry.fillData.order, [
'fillableMakerAmount',
'fillableTakerAmount',
'fillableTakerFeeAmount',
]) as LimitOrderFields;
expect(actualEntry.fillData.order).to.eql(
// tslint:disable-next-line:no-unnecessary-type-assertion
(expectedEntry.fillData as NativeFillData).order,
`${variableName} incorrect at index ${idx}`,
);
}
expect(_.omit(actualEntry, 'fillData')).to.eql(
_.omit(expectedEntry, 'fillData'),
`${variableName} incorrect at index ${idx}`,
);
});
}