import { BlockchainBalanceStore, LocalBalanceStore } from '@0x/contracts-exchange'; import { constants, expect, orderHashUtils, OrderStatus } from '@0x/contracts-test-utils'; import { BatchMatchedFillResults, FillResults, MatchedFillResults, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { LogWithDecodedArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { DeploymentManager } from '../deployment_manager'; export interface FillEventArgs { orderHash: string; makerAddress: string; takerAddress: string; makerAssetFilledAmount: BigNumber; takerAssetFilledAmount: BigNumber; makerFeePaid: BigNumber; takerFeePaid: BigNumber; protocolFeePaid: BigNumber; } export interface MatchTransferAmounts { // Assets being traded. leftMakerAssetSoldByLeftMakerAmount: BigNumber; // leftMakerAssetBoughtByRightMakerAmount if omitted. rightMakerAssetSoldByRightMakerAmount: BigNumber; // rightMakerAssetBoughtByLeftMakerAmount if omitted. rightMakerAssetBoughtByLeftMakerAmount: BigNumber; // rightMakerAssetSoldByRightMakerAmount if omitted. leftMakerAssetBoughtByRightMakerAmount: BigNumber; // leftMakerAssetSoldByLeftMakerAmount if omitted. // Taker profit. leftMakerAssetReceivedByTakerAmount: BigNumber; // 0 if omitted. rightMakerAssetReceivedByTakerAmount: BigNumber; // 0 if omitted. // Maker fees. leftMakerFeeAssetPaidByLeftMakerAmount: BigNumber; // 0 if omitted. rightMakerFeeAssetPaidByRightMakerAmount: BigNumber; // 0 if omitted. // Taker fees. leftTakerFeeAssetPaidByTakerAmount: BigNumber; // 0 if omitted. rightTakerFeeAssetPaidByTakerAmount: BigNumber; // 0 if omitted. // Protocol fees. leftProtocolFeePaidByTakerInEthAmount: BigNumber; // 0 if omitted leftProtocolFeePaidByTakerInWethAmount: BigNumber; // 0 if omitted rightProtocolFeePaidByTakerInWethAmount: BigNumber; // 0 if omitted rightProtocolFeePaidByTakerInEthAmount: BigNumber; // 0 if omitted } export interface MatchResults { orders: MatchedOrders; fills: FillEventArgs[]; } export interface BatchMatchResults { matches: MatchResults[]; filledAmounts: Array<[SignedOrder, BigNumber, string]>; leftFilledResults: FillEventArgs[]; rightFilledResults: FillEventArgs[]; } export interface BatchMatchedOrders { leftOrders: SignedOrder[]; rightOrders: SignedOrder[]; leftOrdersTakerAssetFilledAmounts: BigNumber[]; rightOrdersTakerAssetFilledAmounts: BigNumber[]; } export interface MatchedOrders { leftOrder: SignedOrder; rightOrder: SignedOrder; leftOrderTakerAssetFilledAmount?: BigNumber; rightOrderTakerAssetFilledAmount?: BigNumber; } export class MatchOrderTester { /** * Constructs new MatchOrderTester. */ constructor( protected readonly _deployment: DeploymentManager, protected readonly _blockchainBalanceStore: BlockchainBalanceStore, ) {} /** * Performs batch order matching on a set of complementary orders and asserts results. * @param orders The list of orders and filled states * @param takerAddress Address of taker (the address who matched the two orders) * @param value The amount of value that should be sent in the contract call. * @param matchPairs An array of left and right indices that will be used to perform * the expected simulation. * @param expectedTransferAmounts Expected amounts transferred as a result of each round of * order matching. Omitted fields are either set to 0 or their * complementary field. * @param withMaximalFill A boolean that indicates whether the "maximal fill" order matching * strategy should be used. * @return Results of `batchMatchOrders()`. */ public async batchMatchOrdersAndAssertEffectsAsync( orders: BatchMatchedOrders, takerAddress: string, value: BigNumber, matchPairs: Array<[number, number]>, expectedTransferAmounts: Array>, withMaximalFill: boolean, ): Promise { // Ensure that the provided input is valid. expect(matchPairs.length).to.be.eq(expectedTransferAmounts.length); expect(orders.leftOrders.length).to.be.eq(orders.leftOrdersTakerAssetFilledAmounts.length); expect(orders.rightOrders.length).to.be.eq(orders.rightOrdersTakerAssetFilledAmounts.length); // Ensure that the exchange is in the expected state. await this._assertBatchOrderStatesAsync(orders); // Update the blockchain balance store and create a new local balance store // with the same initial balances. await this._blockchainBalanceStore.updateBalancesAsync(); const localBalanceStore = LocalBalanceStore.create(this._deployment.devUtils, this._blockchainBalanceStore); // Execute `batchMatchOrders()` let actualBatchMatchResults; let transactionReceipt; if (withMaximalFill) { const tx = this._deployment.exchange.batchMatchOrdersWithMaximalFill( orders.leftOrders, orders.rightOrders, orders.leftOrders.map(order => order.signature), orders.rightOrders.map(order => order.signature), ); actualBatchMatchResults = await tx.callAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); transactionReceipt = await tx.awaitTransactionSuccessAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); } else { const tx = this._deployment.exchange.batchMatchOrders( orders.leftOrders, orders.rightOrders, orders.leftOrders.map(order => order.signature), orders.rightOrders.map(order => order.signature), ); actualBatchMatchResults = await tx.callAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); transactionReceipt = await tx.awaitTransactionSuccessAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); } // Burn the gas used to execute the transaction in the local balance store. localBalanceStore.burnGas(takerAddress, DeploymentManager.gasPrice.times(transactionReceipt.gasUsed)); // Simulate the batch order match. const expectedBatchMatchResults = await this._simulateBatchMatchOrdersAsync( orders, takerAddress, matchPairs, expectedTransferAmounts, localBalanceStore, ); const expectedResults = convertToBatchMatchResults(expectedBatchMatchResults); expect(actualBatchMatchResults).to.be.eql(expectedResults); // Validate the simulation against reality. await this._assertBatchMatchResultsAsync(expectedBatchMatchResults, transactionReceipt, localBalanceStore); return expectedBatchMatchResults; } /** * Matches two complementary orders and asserts results. * @param orders The matched orders and filled states. * @param expectedTransferAmounts Expected amounts transferred as a result of order matching. * Omitted fields are either set to 0 or their complementary * field. * @param takerAddress Address of taker (the address who matched the two orders) * @param value The amount of value that should be sent in the contract call. * @param withMaximalFill A boolean that indicates whether the "maximal fill" order matching * strategy should be used. * @return Results of `matchOrders()`. */ public async matchOrdersAndAssertEffectsAsync( orders: MatchedOrders, expectedTransferAmounts: Partial, takerAddress: string, value: BigNumber, withMaximalFill: boolean, ): Promise { await this._assertInitialOrderStatesAsync(orders); // Update the blockchain balance store and create a new local balance store // with the same initial balances. await this._blockchainBalanceStore.updateBalancesAsync(); const localBalanceStore = LocalBalanceStore.create(this._deployment.devUtils, this._blockchainBalanceStore); // Execute `matchOrders()` let actualMatchResults; let transactionReceipt; if (withMaximalFill) { const tx = this._deployment.exchange.matchOrdersWithMaximalFill( orders.leftOrder, orders.rightOrder, orders.leftOrder.signature, orders.rightOrder.signature, ); actualMatchResults = await tx.callAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); transactionReceipt = await tx.awaitTransactionSuccessAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); } else { const tx = this._deployment.exchange.matchOrders( orders.leftOrder, orders.rightOrder, orders.leftOrder.signature, orders.rightOrder.signature, ); actualMatchResults = await tx.callAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); transactionReceipt = await tx.awaitTransactionSuccessAsync({ from: takerAddress, gasPrice: DeploymentManager.gasPrice, value, }); } localBalanceStore.burnGas(takerAddress, DeploymentManager.gasPrice.times(transactionReceipt.gasUsed)); // Simulate the fill. const expectedMatchResults = await this._simulateMatchOrdersAsync( orders, takerAddress, toFullMatchTransferAmounts(expectedTransferAmounts), localBalanceStore, ); const expectedResults = convertToMatchResults(expectedMatchResults); expect(actualMatchResults).to.be.eql(expectedResults); // Validate the simulation against reality. await this._assertMatchResultsAsync(expectedMatchResults, transactionReceipt, localBalanceStore); return expectedMatchResults; } /** * Simulates matching two orders by transferring amounts defined in * `transferAmounts` and returns the results. * @param orders The orders being matched and their filled states. * @param takerAddress Address of taker (the address who matched the two orders) * @param transferAmounts Amounts to transfer during the simulation. * @param localBalanceStore The balance store to use for the simulation. * @return The new account balances and fill events that occurred during the match. */ protected async _simulateMatchOrdersAsync( orders: MatchedOrders, takerAddress: string, transferAmounts: MatchTransferAmounts, localBalanceStore: LocalBalanceStore, ): Promise { // prettier-ignore const matchResults = { orders: { leftOrder: orders.leftOrder, leftOrderTakerAssetFilledAmount: (orders.leftOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT).plus( transferAmounts.rightMakerAssetBoughtByLeftMakerAmount, ), rightOrder: orders.rightOrder, rightOrderTakerAssetFilledAmount: (orders.rightOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT).plus( transferAmounts.leftMakerAssetBoughtByRightMakerAmount, ), }, fills: simulateFillEvents(orders, takerAddress, transferAmounts), }; // Right maker asset -> left maker await localBalanceStore.transferAssetAsync( orders.rightOrder.makerAddress, orders.leftOrder.makerAddress, transferAmounts.rightMakerAssetBoughtByLeftMakerAmount, orders.rightOrder.makerAssetData, ); if (orders.leftOrder.makerAddress !== orders.leftOrder.feeRecipientAddress) { // Left maker fees await localBalanceStore.transferAssetAsync( orders.leftOrder.makerAddress, orders.leftOrder.feeRecipientAddress, transferAmounts.leftMakerFeeAssetPaidByLeftMakerAmount, orders.leftOrder.makerFeeAssetData, ); } // Left maker asset -> right maker await localBalanceStore.transferAssetAsync( orders.leftOrder.makerAddress, orders.rightOrder.makerAddress, transferAmounts.leftMakerAssetBoughtByRightMakerAmount, orders.leftOrder.makerAssetData, ); if (orders.rightOrder.makerAddress !== orders.rightOrder.feeRecipientAddress) { // Right maker fees await localBalanceStore.transferAssetAsync( orders.rightOrder.makerAddress, orders.rightOrder.feeRecipientAddress, transferAmounts.rightMakerFeeAssetPaidByRightMakerAmount, orders.rightOrder.makerFeeAssetData, ); } // Left taker profit await localBalanceStore.transferAssetAsync( orders.leftOrder.makerAddress, takerAddress, transferAmounts.leftMakerAssetReceivedByTakerAmount, orders.leftOrder.makerAssetData, ); // Right taker profit await localBalanceStore.transferAssetAsync( orders.rightOrder.makerAddress, takerAddress, transferAmounts.rightMakerAssetReceivedByTakerAmount, orders.rightOrder.makerAssetData, ); // Left taker fees await localBalanceStore.transferAssetAsync( takerAddress, orders.leftOrder.feeRecipientAddress, transferAmounts.leftTakerFeeAssetPaidByTakerAmount, orders.leftOrder.takerFeeAssetData, ); // Right taker fees await localBalanceStore.transferAssetAsync( takerAddress, orders.rightOrder.feeRecipientAddress, transferAmounts.rightTakerFeeAssetPaidByTakerAmount, orders.rightOrder.takerFeeAssetData, ); // Protocol Fee const wethAssetData = this._deployment.assetDataEncoder .ERC20Token(this._deployment.tokens.weth.address) .getABIEncodedTransactionData(); localBalanceStore.sendEth( takerAddress, this._deployment.staking.stakingProxy.address, transferAmounts.leftProtocolFeePaidByTakerInEthAmount, ); localBalanceStore.sendEth( takerAddress, this._deployment.staking.stakingProxy.address, transferAmounts.rightProtocolFeePaidByTakerInEthAmount, ); await localBalanceStore.transferAssetAsync( takerAddress, this._deployment.staking.stakingProxy.address, transferAmounts.leftProtocolFeePaidByTakerInWethAmount, wethAssetData, ); await localBalanceStore.transferAssetAsync( takerAddress, this._deployment.staking.stakingProxy.address, transferAmounts.rightProtocolFeePaidByTakerInWethAmount, wethAssetData, ); return matchResults; } /** * Simulates matching a batch of orders by transferring amounts defined in * `transferAmounts` and returns the results. * @param orders The orders being batch matched and their filled states. * @param takerAddress Address of taker (the address who matched the two orders) * @param matchPairs The pairs of orders that are expected to be matched. * @param transferAmounts Amounts to transfer during the simulation. * @param localBalanceStore The balance store to use for the simulation. * @return The new account balances and fill events that occurred during the match. */ protected async _simulateBatchMatchOrdersAsync( orders: BatchMatchedOrders, takerAddress: string, matchPairs: Array<[number, number]>, transferAmounts: Array>, localBalanceStore: LocalBalanceStore, ): Promise { // Initialize variables let leftIdx = 0; let rightIdx = 0; let lastLeftIdx = -1; let lastRightIdx = -1; let matchedOrders: MatchedOrders; const batchMatchResults: BatchMatchResults = { matches: [], filledAmounts: [], leftFilledResults: [], rightFilledResults: [], }; // Loop over all of the matched pairs from the round for (let i = 0; i < matchPairs.length; i++) { leftIdx = matchPairs[i][0]; rightIdx = matchPairs[i][1]; // Construct a matched order out of the current left and right orders matchedOrders = { leftOrder: orders.leftOrders[leftIdx], rightOrder: orders.rightOrders[rightIdx], leftOrderTakerAssetFilledAmount: orders.leftOrdersTakerAssetFilledAmounts[leftIdx], rightOrderTakerAssetFilledAmount: orders.rightOrdersTakerAssetFilledAmounts[rightIdx], }; // If there has been a match recorded and one or both of the side indices have not changed, // replace the side's taker asset filled amount if (batchMatchResults.matches.length > 0) { if (lastLeftIdx === leftIdx) { matchedOrders.leftOrderTakerAssetFilledAmount = getLastMatch( batchMatchResults, ).orders.leftOrderTakerAssetFilledAmount; } else { batchMatchResults.filledAmounts.push([ orders.leftOrders[lastLeftIdx], getLastMatch(batchMatchResults).orders.leftOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT, 'left', ]); } if (lastRightIdx === rightIdx) { matchedOrders.rightOrderTakerAssetFilledAmount = getLastMatch( batchMatchResults, ).orders.rightOrderTakerAssetFilledAmount; } else { batchMatchResults.filledAmounts.push([ orders.rightOrders[lastRightIdx], getLastMatch(batchMatchResults).orders.rightOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT, 'right', ]); } } // Add the latest match to the batch match results batchMatchResults.matches.push( await this._simulateMatchOrdersAsync( matchedOrders, takerAddress, toFullMatchTransferAmounts(transferAmounts[i]), localBalanceStore, ), ); // Update the left and right fill results if (lastLeftIdx === leftIdx) { addFillResults(batchMatchResults.leftFilledResults[leftIdx], getLastMatch(batchMatchResults).fills[0]); } else { batchMatchResults.leftFilledResults.push({ ...getLastMatch(batchMatchResults).fills[0] }); } if (lastRightIdx === rightIdx) { addFillResults( batchMatchResults.rightFilledResults[rightIdx], getLastMatch(batchMatchResults).fills[1], ); } else { batchMatchResults.rightFilledResults.push({ ...getLastMatch(batchMatchResults).fills[1] }); } lastLeftIdx = leftIdx; lastRightIdx = rightIdx; } for (let i = leftIdx + 1; i < orders.leftOrders.length; i++) { batchMatchResults.leftFilledResults.push(emptyFillEventArgs()); } for (let i = rightIdx + 1; i < orders.rightOrders.length; i++) { batchMatchResults.rightFilledResults.push(emptyFillEventArgs()); } // The two orders indexed by lastLeftIdx and lastRightIdx were potentially // filled; however, the TakerAssetFilledAmounts that pertain to these orders // will not have been added to batchMatchResults, so we need to write them // here. batchMatchResults.filledAmounts.push([ orders.leftOrders[lastLeftIdx], getLastMatch(batchMatchResults).orders.leftOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT, 'left', ]); batchMatchResults.filledAmounts.push([ orders.rightOrders[lastRightIdx], getLastMatch(batchMatchResults).orders.rightOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT, 'right', ]); // Return the batch match results return batchMatchResults; } /** * Checks that the results of `simulateBatchMatchOrders()` agrees with reality. * @param batchMatchResults The results of a `simulateBatchMatchOrders()`. * @param transactionReceipt The transaction receipt of a call to `matchOrders()`. * @param localBalanceStore The balance store to use during the simulation. */ protected async _assertBatchMatchResultsAsync( batchMatchResults: BatchMatchResults, transactionReceipt: TransactionReceiptWithDecodedLogs, localBalanceStore: LocalBalanceStore, ): Promise { // Ensure that the batchMatchResults contain at least one match expect(batchMatchResults.matches.length).to.be.gt(0); // Check the fill events. assertFillEvents( batchMatchResults.matches.map(match => match.fills).reduce((total, fills) => total.concat(fills)), transactionReceipt, ); // Update the blockchain balance store balances. await this._blockchainBalanceStore.updateBalancesAsync(); // Ensure that the actual and expected token balances are equivalent. localBalanceStore.assertEquals(this._blockchainBalanceStore); // Check the Exchange state. await this._assertPostBatchExchangeStateAsync(batchMatchResults); } /** * Checks that the results of `simulateMatchOrders()` agrees with reality. * @param matchResults The results of a `simulateMatchOrders()`. * @param transactionReceipt The transaction receipt of a call to `matchOrders()`. * @param localBalanceStore The balance store to use during the simulation. */ protected async _assertMatchResultsAsync( matchResults: MatchResults, transactionReceipt: TransactionReceiptWithDecodedLogs, localBalanceStore: LocalBalanceStore, ): Promise { // Check the fill events. assertFillEvents(matchResults.fills, transactionReceipt); // Update the blockchain balance store balances. await this._blockchainBalanceStore.updateBalancesAsync(); // Check the token balances. localBalanceStore.assertEquals(this._blockchainBalanceStore); // Check the Exchange state. await this._assertPostExchangeStateAsync(matchResults); } /** * Asserts the initial exchange state for batch matched orders. * @param orders Batch matched orders with intial filled amounts. */ private async _assertBatchOrderStatesAsync(orders: BatchMatchedOrders): Promise { for (let i = 0; i < orders.leftOrders.length; i++) { await this._assertOrderFilledAmountAsync( orders.leftOrders[i], orders.leftOrdersTakerAssetFilledAmounts[i], 'left', ); } for (let i = 0; i < orders.rightOrders.length; i++) { await this._assertOrderFilledAmountAsync( orders.rightOrders[i], orders.rightOrdersTakerAssetFilledAmounts[i], 'right', ); } } /** * Asserts the initial exchange state for matched orders. * @param orders Matched orders with intial filled amounts. */ private async _assertInitialOrderStatesAsync(orders: MatchedOrders): Promise { const pairs = [ [orders.leftOrder, orders.leftOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT], [orders.rightOrder, orders.rightOrderTakerAssetFilledAmount || constants.ZERO_AMOUNT], ] as Array<[SignedOrder, BigNumber]>; await Promise.all( pairs.map(async ([order, expectedFilledAmount]) => { const side = order === orders.leftOrder ? 'left' : 'right'; await this._assertOrderFilledAmountAsync(order, expectedFilledAmount, side); }), ); } /** * Asserts the exchange state after a call to `batchMatchOrders()`. * @param batchMatchResults Results from a call to `simulateBatchMatchOrders()`. */ private async _assertPostBatchExchangeStateAsync(batchMatchResults: BatchMatchResults): Promise { await this._assertTriplesExchangeStateAsync(batchMatchResults.filledAmounts); } /** * Asserts the exchange state after a call to `matchOrders()`. * @param matchResults Results from a call to `simulateMatchOrders()`. */ private async _assertPostExchangeStateAsync(matchResults: MatchResults): Promise { const triples = [ [matchResults.orders.leftOrder, matchResults.orders.leftOrderTakerAssetFilledAmount, 'left'], [matchResults.orders.rightOrder, matchResults.orders.rightOrderTakerAssetFilledAmount, 'right'], ] as Array<[SignedOrder, BigNumber, string]>; await this._assertTriplesExchangeStateAsync(triples); } /** * Asserts the exchange state represented by provided sequence of triples. * @param triples The sequence of triples to verifiy. Each triple consists * of an `order`, a `takerAssetFilledAmount`, and a `side`, * which will be used to determine if the exchange's state * is valid. */ private async _assertTriplesExchangeStateAsync(triples: Array<[SignedOrder, BigNumber, string]>): Promise { await Promise.all( triples.map(async ([order, expectedFilledAmount, side]) => { expect(['left', 'right']).to.include(side); await this._assertOrderFilledAmountAsync(order, expectedFilledAmount, side); }), ); } /** * Asserts that the provided order's fill amount and order status * are the expected values. * @param order The order to verify for a correct state. * @param expectedFilledAmount The amount that the order should * have been filled. * @param side The side that the provided order should be matched on. */ private async _assertOrderFilledAmountAsync( order: SignedOrder, expectedFilledAmount: BigNumber, side: string, ): Promise { const orderInfo = await this._deployment.exchange.getOrderInfo(order).callAsync(); // Check filled amount of order. const actualFilledAmount = orderInfo.orderTakerAssetFilledAmount; expect(actualFilledAmount, `${side} order final filled amount`).to.be.bignumber.equal(expectedFilledAmount); // Check status of order. const expectedStatus = expectedFilledAmount.isGreaterThanOrEqualTo(order.takerAssetAmount) ? OrderStatus.FullyFilled : OrderStatus.Fillable; const actualStatus = orderInfo.orderStatus; expect(actualStatus, `${side} order final status`).to.equal(expectedStatus); } } /** * Converts a `Partial` to a `MatchTransferAmounts` by * filling in missing fields with zero. */ function toFullMatchTransferAmounts(partial: Partial): MatchTransferAmounts { // prettier-ignore return { leftMakerAssetSoldByLeftMakerAmount: partial.leftMakerAssetSoldByLeftMakerAmount || partial.leftMakerAssetBoughtByRightMakerAmount || constants.ZERO_AMOUNT, rightMakerAssetSoldByRightMakerAmount: partial.rightMakerAssetSoldByRightMakerAmount || partial.rightMakerAssetBoughtByLeftMakerAmount || constants.ZERO_AMOUNT, rightMakerAssetBoughtByLeftMakerAmount: partial.rightMakerAssetBoughtByLeftMakerAmount || partial.rightMakerAssetSoldByRightMakerAmount || constants.ZERO_AMOUNT, leftMakerAssetBoughtByRightMakerAmount: partial.leftMakerAssetBoughtByRightMakerAmount || partial.leftMakerAssetSoldByLeftMakerAmount || constants.ZERO_AMOUNT, leftMakerFeeAssetPaidByLeftMakerAmount: partial.leftMakerFeeAssetPaidByLeftMakerAmount || constants.ZERO_AMOUNT, rightMakerFeeAssetPaidByRightMakerAmount: partial.rightMakerFeeAssetPaidByRightMakerAmount || constants.ZERO_AMOUNT, leftMakerAssetReceivedByTakerAmount: partial.leftMakerAssetReceivedByTakerAmount || constants.ZERO_AMOUNT, rightMakerAssetReceivedByTakerAmount: partial.rightMakerAssetReceivedByTakerAmount || constants.ZERO_AMOUNT, leftTakerFeeAssetPaidByTakerAmount: partial.leftTakerFeeAssetPaidByTakerAmount || constants.ZERO_AMOUNT, rightTakerFeeAssetPaidByTakerAmount: partial.rightTakerFeeAssetPaidByTakerAmount || constants.ZERO_AMOUNT, leftProtocolFeePaidByTakerInEthAmount: partial.leftProtocolFeePaidByTakerInEthAmount || constants.ZERO_AMOUNT, leftProtocolFeePaidByTakerInWethAmount: partial.leftProtocolFeePaidByTakerInWethAmount || constants.ZERO_AMOUNT, rightProtocolFeePaidByTakerInEthAmount: partial.rightProtocolFeePaidByTakerInEthAmount || constants.ZERO_AMOUNT, rightProtocolFeePaidByTakerInWethAmount: partial.rightProtocolFeePaidByTakerInWethAmount || constants.ZERO_AMOUNT, }; } /** * Checks values from the logs produced by Exchange.matchOrders against * the expected transfer amounts. * @param orders The matched orders. * @param transactionReceipt Transaction receipt and logs produced by Exchange.matchOrders. */ function assertFillEvents(expectedFills: FillEventArgs[], transactionReceipt: TransactionReceiptWithDecodedLogs): void { // Extract the actual `Fill` events. const actualFills = extractFillEventsfromReceipt(transactionReceipt); expect(actualFills.length, 'wrong number of Fill events').to.be.equal(expectedFills.length); // Validate event arguments. const fillPairs = _.zip(expectedFills, actualFills) as Array<[FillEventArgs, FillEventArgs]>; for (const [expected, actual] of fillPairs) { const side = expected === expectedFills[0] ? 'Left' : 'Right'; expect(actual.orderHash, `${side} order Fill event orderHash`).to.equal(expected.orderHash); expect(actual.makerAddress, `${side} order Fill event makerAddress`).to.equal(expected.makerAddress); expect(actual.takerAddress, `${side} order Fill event takerAddress`).to.equal(expected.takerAddress); expect(actual.makerAssetFilledAmount, `${side} order Fill event makerAssetFilledAmount`).to.bignumber.equal( expected.makerAssetFilledAmount, ); expect(actual.takerAssetFilledAmount, `${side} order Fill event takerAssetFilledAmount`).to.bignumber.equal( expected.takerAssetFilledAmount, ); expect(actual.makerFeePaid, `${side} order Fill event makerFeePaid`).to.bignumber.equal(expected.makerFeePaid); expect(actual.takerFeePaid, `${side} order Fill event takerFeePaid`).to.bignumber.equal(expected.takerFeePaid); } } /** * Create a pair of `Fill` events for a simulated `matchOrder()`. */ function simulateFillEvents( orders: MatchedOrders, takerAddress: string, transferAmounts: MatchTransferAmounts, ): [FillEventArgs, FillEventArgs] { // prettier-ignore return [ // Left order Fill { orderHash: orderHashUtils.getOrderHashHex(orders.leftOrder), makerAddress: orders.leftOrder.makerAddress, takerAddress, makerAssetFilledAmount: transferAmounts.leftMakerAssetSoldByLeftMakerAmount, takerAssetFilledAmount: transferAmounts.rightMakerAssetBoughtByLeftMakerAmount, makerFeePaid: transferAmounts.leftMakerFeeAssetPaidByLeftMakerAmount, takerFeePaid: transferAmounts.leftTakerFeeAssetPaidByTakerAmount, protocolFeePaid: transferAmounts.leftProtocolFeePaidByTakerInEthAmount.plus( transferAmounts.leftProtocolFeePaidByTakerInWethAmount, ), }, // Right order Fill { orderHash: orderHashUtils.getOrderHashHex(orders.rightOrder), makerAddress: orders.rightOrder.makerAddress, takerAddress, makerAssetFilledAmount: transferAmounts.rightMakerAssetSoldByRightMakerAmount, takerAssetFilledAmount: transferAmounts.leftMakerAssetBoughtByRightMakerAmount, makerFeePaid: transferAmounts.rightMakerFeeAssetPaidByRightMakerAmount, takerFeePaid: transferAmounts.rightTakerFeeAssetPaidByTakerAmount, protocolFeePaid: transferAmounts.rightProtocolFeePaidByTakerInEthAmount.plus( transferAmounts.rightProtocolFeePaidByTakerInWethAmount, ), }, ]; } /** * Extract `Fill` events from a transaction receipt. */ function extractFillEventsfromReceipt(receipt: TransactionReceiptWithDecodedLogs): FillEventArgs[] { interface RawFillEventArgs { orderHash: string; makerAddress: string; takerAddress: string; makerAssetFilledAmount: string; takerAssetFilledAmount: string; makerFeePaid: string; takerFeePaid: string; protocolFeePaid: string; } const actualFills = (_.filter(receipt.logs, ['event', 'Fill']) as any) as Array< LogWithDecodedArgs >; // Convert RawFillEventArgs to FillEventArgs. return actualFills.map(fill => ({ orderHash: fill.args.orderHash, makerAddress: fill.args.makerAddress, takerAddress: fill.args.takerAddress, makerAssetFilledAmount: new BigNumber(fill.args.makerAssetFilledAmount), takerAssetFilledAmount: new BigNumber(fill.args.takerAssetFilledAmount), makerFeePaid: new BigNumber(fill.args.makerFeePaid), takerFeePaid: new BigNumber(fill.args.takerFeePaid), protocolFeePaid: new BigNumber(fill.args.protocolFeePaid), })); } /** * Gets the last match in a BatchMatchResults object. * @param batchMatchResults The BatchMatchResults object. * @return The last match of the results. */ function getLastMatch(batchMatchResults: BatchMatchResults): MatchResults { return batchMatchResults.matches[batchMatchResults.matches.length - 1]; } /** * Add a new fill results object to a total fill results object destructively. * @param total The total fill results that should be updated. * @param fill The new fill results that should be used to accumulate. */ function addFillResults(total: FillEventArgs, fill: FillEventArgs): void { // Ensure that the total and fill are compatibe fill events expect(total.orderHash).to.be.eq(fill.orderHash); expect(total.makerAddress).to.be.eq(fill.makerAddress); expect(total.takerAddress).to.be.eq(fill.takerAddress); // Add the fill results together total.makerAssetFilledAmount = total.makerAssetFilledAmount.plus(fill.makerAssetFilledAmount); total.takerAssetFilledAmount = total.takerAssetFilledAmount.plus(fill.takerAssetFilledAmount); total.makerFeePaid = total.makerFeePaid.plus(fill.makerFeePaid); total.takerFeePaid = total.takerFeePaid.plus(fill.takerFeePaid); total.protocolFeePaid = total.protocolFeePaid.plus(fill.protocolFeePaid); } /** * Converts a BatchMatchResults object to the associated value that correspondes to a value that could be * returned by `batchMatchOrders` or `batchMatchOrdersWithMaximalFill`. * @param results The results object to convert * @return The associated object that can be compared to the return value of `batchMatchOrders` */ function convertToBatchMatchResults(results: BatchMatchResults): BatchMatchedFillResults { // Initialize the results object const batchMatchedFillResults: BatchMatchedFillResults = { left: [], right: [], profitInLeftMakerAsset: constants.ZERO_AMOUNT, profitInRightMakerAsset: constants.ZERO_AMOUNT, }; for (const match of results.matches) { const leftSpread = match.fills[0].makerAssetFilledAmount.minus(match.fills[1].takerAssetFilledAmount); // If the left maker spread is positive for match, update the profitInLeftMakerAsset if (leftSpread.isGreaterThan(constants.ZERO_AMOUNT)) { batchMatchedFillResults.profitInLeftMakerAsset = batchMatchedFillResults.profitInLeftMakerAsset.plus( leftSpread, ); } const rightSpread = match.fills[1].makerAssetFilledAmount.minus(match.fills[0].takerAssetFilledAmount); // If the right maker spread is positive for match, update the profitInRightMakerAsset if (rightSpread.isGreaterThan(constants.ZERO_AMOUNT)) { batchMatchedFillResults.profitInRightMakerAsset = batchMatchedFillResults.profitInRightMakerAsset.plus( rightSpread, ); } } for (const fill of results.leftFilledResults) { batchMatchedFillResults.left.push(convertToFillResults(fill)); } for (const fill of results.rightFilledResults) { batchMatchedFillResults.right.push(convertToFillResults(fill)); } return batchMatchedFillResults; } /** * Converts a MatchResults object to the associated value that correspondes to a value that could be * returned by `matchOrders` or `matchOrdersWithMaximalFill`. * @param results The results object to convert * @return The associated object that can be compared to the return value of `matchOrders` */ function convertToMatchResults(result: MatchResults): MatchedFillResults { // If the left spread is negative, set it to zero let profitInLeftMakerAsset = result.fills[0].makerAssetFilledAmount.minus(result.fills[1].takerAssetFilledAmount); if (profitInLeftMakerAsset.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) { profitInLeftMakerAsset = constants.ZERO_AMOUNT; } // If the right spread is negative, set it to zero let profitInRightMakerAsset = result.fills[1].makerAssetFilledAmount.minus(result.fills[0].takerAssetFilledAmount); if (profitInRightMakerAsset.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) { profitInRightMakerAsset = constants.ZERO_AMOUNT; } const matchedFillResults: MatchedFillResults = { left: { makerAssetFilledAmount: result.fills[0].makerAssetFilledAmount, takerAssetFilledAmount: result.fills[0].takerAssetFilledAmount, makerFeePaid: result.fills[0].makerFeePaid, takerFeePaid: result.fills[0].takerFeePaid, protocolFeePaid: result.fills[0].protocolFeePaid, }, right: { makerAssetFilledAmount: result.fills[1].makerAssetFilledAmount, takerAssetFilledAmount: result.fills[1].takerAssetFilledAmount, makerFeePaid: result.fills[1].makerFeePaid, takerFeePaid: result.fills[1].takerFeePaid, protocolFeePaid: result.fills[1].protocolFeePaid, }, profitInLeftMakerAsset, profitInRightMakerAsset, }; return matchedFillResults; } /** * Converts a fill event args object to the associated FillResults object. * @param result The result to be converted to a FillResults object. * @return The converted value. */ function convertToFillResults(result: FillEventArgs): FillResults { const fillResults: FillResults = { makerAssetFilledAmount: result.makerAssetFilledAmount, takerAssetFilledAmount: result.takerAssetFilledAmount, makerFeePaid: result.makerFeePaid, takerFeePaid: result.takerFeePaid, protocolFeePaid: result.protocolFeePaid, }; return fillResults; } /** * Creates an empty FillEventArgs object. * @return The empty FillEventArgs object. */ function emptyFillEventArgs(): FillEventArgs { const empty: FillEventArgs = { orderHash: '', makerAddress: '', takerAddress: '', makerAssetFilledAmount: constants.ZERO_AMOUNT, takerAssetFilledAmount: constants.ZERO_AMOUNT, makerFeePaid: constants.ZERO_AMOUNT, takerFeePaid: constants.ZERO_AMOUNT, protocolFeePaid: constants.ZERO_AMOUNT, }; return empty; } // tslint:disable-line:max-file-line-count