// tslint:disable:custom-no-magic-numbers import { SignedOrder } from '@0x/types'; import { BigNumber, hexUtils } from '@0x/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; import 'mocha'; import * as TypeMoq from 'typemoq'; import { MarketOperation } from '../src/types'; import { CollapsedFill, DexSample, ERC20BridgeSource, MultiHopFillData, NativeCollapsedFill, } from '../src/utils/market_operation_utils/types'; import { QuoteRequestor } from '../src/utils/quote_requestor'; import { BridgeReportSource, generateQuoteReport, MultiHopReportSource, NativeOrderbookReportSource, NativeRFQTReportSource, QuoteReportSource, } from './../src/utils/quote_report_generator'; import { chaiSetup } from './utils/chai_setup'; import { testOrderFactory } from './utils/test_order_factory'; chaiSetup.configure(); const expect = chai.expect; const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill => { return { sourcePathId: hexUtils.random(), source: ERC20BridgeSource.Native, input: order.takerAssetAmount, output: order.makerAssetAmount, fillData: { order: { ...order, fillableMakerAssetAmount: new BigNumber(1), fillableTakerAssetAmount: new BigNumber(1), fillableTakerFeeAmount: new BigNumber(1), }, }, 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 orderbookOrder1FillableAmount = new BigNumber(1000); const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ signature: 'orderbookOrder1', takerAssetAmount: orderbookOrder1FillableAmount, }); const orderbookOrder2FillableAmount = new BigNumber(99); const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({ signature: 'orderbookOrder2', takerAssetAmount: orderbookOrder2FillableAmount.plus(99), }); const rfqtOrder1FillableAmount = new BigNumber(100); const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({ signature: 'rfqtOrder1', takerAssetAmount: rfqtOrder1FillableAmount, }); const rfqtOrder2FillableAmount = new BigNumber(1001); const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({ signature: 'rfqtOrder2', takerAssetAmount: rfqtOrder2FillableAmount.plus(100), }); const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2]; const orderFillableAmounts: BigNumber[] = [ orderbookOrder1FillableAmount, rfqtOrder1FillableAmount, rfqtOrder2FillableAmount, orderbookOrder2FillableAmount, ]; // generate path const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [], sourcePathId: hexUtils.random() }; const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [], sourcePathId: hexUtils.random() }; 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 .setup(qr => qr.getMakerUriForOrderSignature(orderbookOrder2.signature)) .returns(() => { return undefined; }) .verifiable(TypeMoq.Times.atLeastOnce()); quoteRequestor .setup(qr => qr.getMakerUriForOrderSignature(rfqtOrder1.signature)) .returns(() => { return 'https://rfqt1.provider.club'; }) .verifiable(TypeMoq.Times.atLeastOnce()); quoteRequestor .setup(qr => qr.getMakerUriForOrderSignature(rfqtOrder2.signature)) .returns(() => { return 'https://rfqt2.provider.club'; }) .verifiable(TypeMoq.Times.atLeastOnce()); const orderReport = generateQuoteReport( marketOperation, dexQuotes, [], nativeOrders, orderFillableAmounts, pathGenerated, quoteRequestor.object, ); const rfqtOrder1Source: NativeRFQTReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: rfqtOrder1.makerAssetAmount, takerAmount: rfqtOrder1.takerAssetAmount, nativeOrder: rfqtOrder1, fillableTakerAmount: rfqtOrder1FillableAmount, isRfqt: true, makerUri: 'https://rfqt1.provider.club', }; const rfqtOrder2Source: NativeRFQTReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: rfqtOrder2.makerAssetAmount, takerAmount: rfqtOrder2.takerAssetAmount, nativeOrder: rfqtOrder2, fillableTakerAmount: rfqtOrder2FillableAmount, isRfqt: true, makerUri: 'https://rfqt2.provider.club', }; const orderbookOrder1Source: NativeOrderbookReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: orderbookOrder1.makerAssetAmount, takerAmount: orderbookOrder1.takerAssetAmount, nativeOrder: orderbookOrder1, fillableTakerAmount: orderbookOrder1FillableAmount, isRfqt: false, }; const orderbookOrder2Source: NativeOrderbookReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: orderbookOrder2.makerAssetAmount, takerAmount: orderbookOrder2.takerAssetAmount, nativeOrder: orderbookOrder2, fillableTakerAmount: orderbookOrder2FillableAmount, isRfqt: false, }; const uniswap1Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.UniswapV2, makerAmount: uniswapSample1.output, takerAmount: uniswapSample1.input, fillData: {}, }; const uniswap2Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.UniswapV2, makerAmount: uniswapSample2.output, takerAmount: uniswapSample2.input, fillData: {}, }; const kyber1Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample1.output, takerAmount: kyberSample1.input, fillData: {}, }; const kyber2Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample2.output, takerAmount: kyberSample2.input, fillData: {}, }; const expectedSourcesConsidered: QuoteReportSource[] = [ kyber1Source, kyber2Source, uniswap1Source, uniswap2Source, orderbookOrder1Source, rfqtOrder1Source, rfqtOrder2Source, orderbookOrder2Source, ]; expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { const expectedSourceConsidered = expectedSourcesConsidered[idx]; expect(actualSourcesConsidered).to.eql( expectedSourceConsidered, `sourceConsidered incorrect at index ${idx}`, ); }); const expectedSourcesDelivered: QuoteReportSource[] = [ rfqtOrder2Source, orderbookOrder2Source, uniswap2Source, kyber2Source, ]; expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { const expectedSourceDelivered = expectedSourcesDelivered[idx]; // remove fillable values if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ 'fillableMakerAssetAmount', 'fillableTakerAssetAmount', 'fillableTakerFeeAmount', ]) as SignedOrder; } expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`); }); 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 orderbookOrder1FillableAmount = new BigNumber(1000); const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ signature: 'orderbookOrder1', takerAssetAmount: orderbookOrder1FillableAmount.plus(101), }); const orderbookOrder2FillableAmount = new BigNumber(5000); const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({ signature: 'orderbookOrder2', takerAssetAmount: orderbookOrder2FillableAmount.plus(101), }); const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2]; const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount]; // generate path const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1); const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [], sourcePathId: hexUtils.random() }; const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() }; const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill]; const orderReport = generateQuoteReport( marketOperation, dexQuotes, [], nativeOrders, orderFillableAmounts, pathGenerated, ); const orderbookOrder1Source: NativeOrderbookReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: orderbookOrder1.makerAssetAmount, takerAmount: orderbookOrder1.takerAssetAmount, nativeOrder: orderbookOrder1, fillableTakerAmount: orderbookOrder1FillableAmount, isRfqt: false, }; const orderbookOrder2Source: NativeOrderbookReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: orderbookOrder2.makerAssetAmount, takerAmount: orderbookOrder2.takerAssetAmount, nativeOrder: orderbookOrder2, fillableTakerAmount: orderbookOrder2FillableAmount, isRfqt: false, }; const uniswap1Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.UniswapV2, makerAmount: uniswapSample1.input, takerAmount: uniswapSample1.output, fillData: {}, }; const kyber1Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample1.input, takerAmount: kyberSample1.output, fillData: {}, }; const expectedSourcesConsidered: QuoteReportSource[] = [ kyber1Source, uniswap1Source, orderbookOrder1Source, orderbookOrder2Source, ]; expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { const expectedSourceConsidered = expectedSourcesConsidered[idx]; expect(actualSourcesConsidered).to.eql( expectedSourceConsidered, `sourceConsidered incorrect at index ${idx}`, ); }); const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source]; expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { const expectedSourceDelivered = expectedSourcesDelivered[idx]; // remove fillable values if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ 'fillableMakerAssetAmount', 'fillableTakerAssetAmount', 'fillableTakerFeeAmount', ]) as SignedOrder; } expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`); }); }); 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 orderbookOrder1FillableAmount = new BigNumber(1000); const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ signature: 'orderbookOrder1', takerAssetAmount: orderbookOrder1FillableAmount.plus(101), }); const twoHopFillData: MultiHopFillData = { intermediateToken: hexUtils.random(20), firstHopSource: { source: ERC20BridgeSource.Balancer, encodeCall: () => '', handleCallResults: _callResults => [new BigNumber(1337)], }, secondHopSource: { source: ERC20BridgeSource.Curve, encodeCall: () => '', handleCallResults: _callResults => [new BigNumber(1337)], }, }; const twoHopSample: DexSample = { source: ERC20BridgeSource.MultiHop, input: new BigNumber(3005), output: new BigNumber(3006), fillData: twoHopFillData, }; const orderReport = generateQuoteReport( marketOperation, [kyberSample1], [twoHopSample], [orderbookOrder1], [orderbookOrder1FillableAmount], twoHopSample, ); const orderbookOrder1Source: NativeOrderbookReportSource = { liquiditySource: ERC20BridgeSource.Native, makerAmount: orderbookOrder1.makerAssetAmount, takerAmount: orderbookOrder1.takerAssetAmount, nativeOrder: orderbookOrder1, fillableTakerAmount: orderbookOrder1FillableAmount, isRfqt: false, }; const kyber1Source: BridgeReportSource = { liquiditySource: ERC20BridgeSource.Kyber, makerAmount: kyberSample1.output, takerAmount: kyberSample1.input, fillData: {}, }; const twoHopSource: MultiHopReportSource = { liquiditySource: ERC20BridgeSource.MultiHop, makerAmount: twoHopSample.output, takerAmount: twoHopSample.input, hopSources: [ERC20BridgeSource.Balancer, ERC20BridgeSource.Curve], fillData: twoHopFillData, }; const expectedSourcesConsidered: QuoteReportSource[] = [kyber1Source, orderbookOrder1Source, twoHopSource]; expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { const expectedSourceConsidered = expectedSourcesConsidered[idx]; expect(actualSourcesConsidered).to.eql( expectedSourceConsidered, `sourceConsidered incorrect at index ${idx}`, ); }); expect(orderReport.sourcesDelivered.length).to.eql(1); expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource); }); });