Created tests for batchMatchOrders

This commit is contained in:
James Towle 2019-06-24 17:21:48 -05:00 committed by Amir Bandeali
parent f289b3112b
commit 6cf11554de
6 changed files with 594 additions and 32 deletions

View File

@ -57,20 +57,23 @@ contract MixinMatchOrders is
require(leftOrders.length == leftSignatures.length, "Incompatible leftOrders and leftSignatures");
require(rightOrders.length == rightSignatures.length, "Incompatible rightOrders and rightSignatures");
uint256 minLength = _min256(leftOrders.length, rightOrders.length);
batchMatchedFillResults.left = new LibFillResults.FillResults[](minLength);
batchMatchedFillResults.right = new LibFillResults.FillResults[](minLength);
// Without simulating all of the order matching, this program cannot know how many
// matches there will be. To ensure that batchMatchedFillResults has enough memory
// allocated for the left and the right side, we will allocate enough space for the
// maximum amount of matches (the maximum of the left and the right sides).
uint256 maxLength = _max256(leftOrders.length, rightOrders.length);
batchMatchedFillResults.left = new LibFillResults.FillResults[](maxLength);
batchMatchedFillResults.right = new LibFillResults.FillResults[](maxLength);
// Initialize initial variables
uint256 matchCount;
uint matchCount;
uint256 leftIdx = 0;
uint256 rightIdx = 0;
LibOrder.Order memory leftOrder = leftOrders[0];
LibOrder.Order memory rightOrder = rightOrders[0];
bytes memory leftSignature = leftSignatures[0];
bytes memory rightSignature = leftSignatures[0];
bytes memory rightSignature = rightSignatures[0];
// Loop infinitely (until broken inside of the loop), but keep a counter of how
// many orders have been matched.
@ -93,6 +96,7 @@ contract MixinMatchOrders is
);
// batchMatchedFillResults.profitInRightMakerAsset += 0; // Placeholder for ZEIP 40
// If the leftOrder is filled, update the leftIdx, leftOrder, and leftSignature,
// or break out of the loop if there are no more leftOrders to match.
if (_isFilled(leftOrder, matchResults.left)) {

View File

@ -14,7 +14,7 @@ import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { chaiSetup, constants, OrderFactory, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { assetDataUtils, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils';
import { OrderStatus } from '@0x/types';
import { OrderStatus, SignedOrder } from '@0x/types';
import { BigNumber, providerUtils, ReentrancyGuardRevertErrors } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai';
@ -31,6 +31,7 @@ import {
import { MatchOrderTester, TokenBalances } from './utils/match_order_tester';
const ZERO = new BigNumber(0);
const ONE = new BigNumber(1);
const TWO = new BigNumber(2);
@ -1850,5 +1851,193 @@ describe('matchOrders', () => {
}
});
});
describe.only('batchMatchOrders', () => {
it('should fail if there are zero leftOrders', async () => {
const leftOrders: SignedOrder[] = [];
const rightOrders = [
await orderFactoryRight.newSignedOrderAsync({
makerAddress: makerAddressRight,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
feeRecipientAddress: feeRecipientAddressRight,
}),
];
const tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress);
return expect(tx).to.be.rejected();
});
it('should fail if there are zero rightOrders', async () => {
const leftOrders = [
await orderFactoryLeft.newSignedOrderAsync({
makerAddress: makerAddressLeft,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
feeRecipientAddress: feeRecipientAddressLeft,
}),
];
const rightOrders: SignedOrder[] = [];
const tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress);
return expect(tx).to.be.rejected();
});
it('should correctly match two opposite orders', async () => {
const leftOrders = [
await orderFactoryLeft.newSignedOrderAsync({
makerAddress: makerAddressLeft,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
feeRecipientAddress: feeRecipientAddressLeft,
}),
];
const rightOrders = [
await orderFactoryRight.newSignedOrderAsync({
makerAddress: makerAddressRight,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
feeRecipientAddress: feeRecipientAddressRight,
}),
];
const expectedTransferAmounts = [
{
// Left Maker
leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
// Right Maker
rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
// Taker
leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
},
];
await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync(
{
leftOrders,
rightOrders,
leftOrdersTakerAssetFilledAmounts: [ZERO],
rightOrdersTakerAssetFilledAmounts: [ZERO],
},
takerAddress,
[[0, 0]],
expectedTransferAmounts,
);
});
it('should correctly match two left orders to one complementary right order', async () => {
const leftOrders = [
await orderFactoryLeft.newSignedOrderAsync({
makerAddress: makerAddressLeft,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
feeRecipientAddress: feeRecipientAddressLeft,
}),
await orderFactoryLeft.newSignedOrderAsync({
makerAddress: makerAddressLeft,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
feeRecipientAddress: feeRecipientAddressLeft,
}),
];
const rightOrders = [
await orderFactoryRight.newSignedOrderAsync({
makerAddress: makerAddressRight,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0),
feeRecipientAddress: feeRecipientAddressRight,
}),
];
const expectedTransferAmounts = [
{
// Left Maker
leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
// Right Maker
rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
// Taker
leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50%
rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
},
{
// Left Maker
leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50%
// Right Maker
rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
// Taker
leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50%
rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
},
];
await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync(
{
leftOrders,
rightOrders,
leftOrdersTakerAssetFilledAmounts: [ZERO, ZERO],
rightOrdersTakerAssetFilledAmounts: [ZERO],
},
takerAddress,
[[0, 0], [1, 0]],
expectedTransferAmounts,
);
});
it('should correctly match one left order to two complementary right orders', async () => {
const leftOrders = [
await orderFactoryLeft.newSignedOrderAsync({
makerAddress: makerAddressLeft,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
feeRecipientAddress: feeRecipientAddressLeft,
}),
];
const rightOrders = [
await orderFactoryRight.newSignedOrderAsync({
makerAddress: makerAddressRight,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
feeRecipientAddress: feeRecipientAddressRight,
}),
await orderFactoryRight.newSignedOrderAsync({
makerAddress: makerAddressRight,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
feeRecipientAddress: feeRecipientAddressRight,
}),
];
const expectedTransferAmounts = [
{
// Left Maker
leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
// Right Maker
rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
// Taker
leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
},
{
// Left Maker
leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0),
leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
// Right Maker
rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0),
rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
// Taker
leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50%
rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100%
},
];
await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync(
{
leftOrders,
rightOrders,
leftOrdersTakerAssetFilledAmounts: [ZERO],
rightOrdersTakerAssetFilledAmounts: [ZERO, ZERO],
},
takerAddress,
[[0, 0], [0, 1]],
expectedTransferAmounts,
);
});
});
});
// tslint:disable-line:max-file-line-count

View File

@ -266,6 +266,22 @@ export class ExchangeWrapper {
const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[];
return ordersInfo;
}
public async batchMatchOrdersAsync(
signedOrdersLeft: SignedOrder[],
signedOrdersRight: SignedOrder[],
from: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createBatchMatchOrders(signedOrdersLeft, signedOrdersRight);
const txHash = await this._exchange.batchMatchOrders.sendTransactionAsync(
params.leftOrders,
params.rightOrders,
params.leftSignatures,
params.rightSignatures,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async matchOrdersAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,

View File

@ -56,6 +56,11 @@ export interface MatchResults {
balances: TokenBalances;
}
export interface BatchMatchResults {
matches: MatchResults[];
filledAmounts: Array<[SignedOrder, BigNumber, string]>;
}
export interface ERC1155Holdings {
[owner: string]: {
[contract: string]: {
@ -81,6 +86,13 @@ export interface TokenBalances {
erc1155: ERC1155Holdings;
}
export interface BatchMatchedOrders {
leftOrders: SignedOrder[];
rightOrders: SignedOrder[];
leftOrdersTakerAssetFilledAmounts: BigNumber[];
rightOrdersTakerAssetFilledAmounts: BigNumber[];
}
export interface MatchedOrders {
leftOrder: SignedOrder;
rightOrder: SignedOrder;
@ -88,6 +100,12 @@ export interface MatchedOrders {
rightOrderTakerAssetFilledAmount?: BigNumber;
}
export type BatchMatchOrdersAsyncCall = (
leftOrders: SignedOrder[],
rightOrders: SignedOrder[],
takerAddress: string,
) => Promise<TransactionReceiptWithDecodedLogs>;
export type MatchOrdersAsyncCall = (
leftOrder: SignedOrder,
rightOrder: SignedOrder,
@ -99,6 +117,7 @@ export class MatchOrderTester {
public erc20Wrapper: ERC20Wrapper;
public erc721Wrapper: ERC721Wrapper;
public erc1155ProxyWrapper: ERC1155ProxyWrapper;
public batchMatchOrdersCallAsync?: BatchMatchOrdersAsyncCall;
public matchOrdersCallAsync?: MatchOrdersAsyncCall;
private readonly _initialTokenBalancesPromise: Promise<TokenBalances>;
@ -108,6 +127,8 @@ export class MatchOrderTester {
* @param erc20Wrapper Used to fetch ERC20 balances.
* @param erc721Wrapper Used to fetch ERC721 token owners.
* @param erc1155Wrapper Used to fetch ERC1155 token owners.
* @param batchMatchOrdersCallAsync Optional, custom caller for
* `ExchangeWrapper.batchMatchOrdersAsync()`.
* @param matchOrdersCallAsync Optional, custom caller for
* `ExchangeWrapper.matchOrdersAsync()`.
*/
@ -117,6 +138,7 @@ export class MatchOrderTester {
erc721Wrapper: ERC721Wrapper,
erc1155ProxyWrapper: ERC1155ProxyWrapper,
matchOrdersCallAsync?: MatchOrdersAsyncCall,
batchMatchOrdersCallAsync?: BatchMatchOrdersAsyncCall,
) {
this.exchangeWrapper = exchangeWrapper;
this.erc20Wrapper = erc20Wrapper;
@ -126,6 +148,59 @@ export class MatchOrderTester {
this._initialTokenBalancesPromise = this.getBalancesAsync();
}
/**
* Performs batch order matching on a set of complementary orders and asserts results.
* @param orders The list of orders and filled states
* @param matchPairs An array of left and right indices that will be used to perform
* the expected simulation.
* @param takerAddress Address of taker (the address who matched the two orders)
* @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.
* @return Results of `batchMatchOrders()`.
*/
public async batchMatchOrdersAndAssertEffectsAsync(
orders: BatchMatchedOrders,
takerAddress: string,
matchPairs: Array<[number, number]>,
expectedTransferAmounts: Array<Partial<MatchTransferAmounts>>,
initialTokenBalances?: TokenBalances,
): Promise<BatchMatchResults> {
// 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 assertBatchOrderStatesAsync(orders, this.exchangeWrapper);
// Get the token balances before executing `batchMatchOrders()`.
const _initialTokenBalances = initialTokenBalances
? initialTokenBalances
: await this._initialTokenBalancesPromise;
// Execute `batchMatchOrders()`
const transactionReceipt = await this._executeBatchMatchOrdersAsync(
orders.leftOrders,
orders.rightOrders,
takerAddress,
);
// Simulate the batch order match.
const batchMatchResults = simulateBatchMatchOrders(
orders,
takerAddress,
_initialTokenBalances,
matchPairs,
expectedTransferAmounts,
);
// Validate the simulation against reality.
await assertBatchMatchResultsAsync(
batchMatchResults,
transactionReceipt,
await this.getBalancesAsync(),
_initialTokenBalances,
this.exchangeWrapper,
);
return batchMatchResults;
}
/**
* Matches two complementary orders and asserts results.
* @param orders The matched orders and filled states.
@ -159,7 +234,7 @@ export class MatchOrderTester {
_initialTokenBalances,
toFullMatchTransferAmounts(expectedTransferAmounts),
);
// Validate the simulation against realit.
// Validate the simulation against reality.
await assertMatchResultsAsync(
matchResults,
transactionReceipt,
@ -176,6 +251,18 @@ export class MatchOrderTester {
return getTokenBalancesAsync(this.erc20Wrapper, this.erc721Wrapper, this.erc1155ProxyWrapper);
}
private async _executeBatchMatchOrdersAsync(
leftOrders: SignedOrder[],
rightOrders: SignedOrder[],
takerAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const caller =
this.batchMatchOrdersCallAsync ||
(async (_leftOrders: SignedOrder[], _rightOrders: SignedOrder[], _takerAddress: string) =>
this.exchangeWrapper.batchMatchOrdersAsync(_leftOrders, _rightOrders, _takerAddress));
return caller(leftOrders, rightOrders, takerAddress);
}
private async _executeMatchOrdersAsync(
leftOrder: SignedOrder,
rightOrder: SignedOrder,
@ -227,6 +314,97 @@ function toFullMatchTransferAmounts(partial: Partial<MatchTransferAmounts>): Mat
};
}
/**
* 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 tokenBalances Current token balances.
* @param transferAmounts Amounts to transfer during the simulation.
* @return The new account balances and fill events that occurred during the match.
*/
function simulateBatchMatchOrders(
orders: BatchMatchedOrders,
takerAddress: string,
tokenBalances: TokenBalances,
matchPairs: Array<[number, number]>,
transferAmounts: Array<Partial<MatchTransferAmounts>>,
): BatchMatchResults {
// Initialize variables
let lastLeftIdx = 0;
let lastRightIdx = 0;
let matchedOrders: MatchedOrders;
const batchMatchResults: BatchMatchResults = {
matches: [],
filledAmounts: [],
};
// Loop over all of the matched pairs from the round
for (let i = 0; i < matchPairs.length; i++) {
const leftIdx = matchPairs[i][0];
const 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 || ZERO,
'left',
]);
}
if (lastRightIdx === rightIdx) {
matchedOrders.rightOrderTakerAssetFilledAmount = getLastMatch(
batchMatchResults,
).orders.rightOrderTakerAssetFilledAmount;
} else {
batchMatchResults.filledAmounts.push([
orders.rightOrders[lastRightIdx],
getLastMatch(batchMatchResults).orders.rightOrderTakerAssetFilledAmount || ZERO,
'right',
]);
}
}
lastLeftIdx = leftIdx;
lastRightIdx = rightIdx;
// Add the latest match to the batch match results
batchMatchResults.matches.push(
simulateMatchOrders(
matchedOrders,
takerAddress,
tokenBalances,
toFullMatchTransferAmounts(transferAmounts[i]),
),
);
}
// 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 || ZERO,
'left',
]);
batchMatchResults.filledAmounts.push([
orders.rightOrders[lastRightIdx],
getLastMatch(batchMatchResults).orders.rightOrderTakerAssetFilledAmount || ZERO,
'right',
]);
// Return the batch match results
return batchMatchResults;
}
/**
* Simulates matching two orders by transferring amounts defined in
* `transferAmounts` and returns the results.
@ -406,6 +584,34 @@ function transferAsset(
}
}
/**
* 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 actualTokenBalances The actual, on-chain token balances of known addresses.
* @param exchangeWrapper The ExchangeWrapper instance.
*/
async function assertBatchMatchResultsAsync(
batchMatchResults: BatchMatchResults,
transactionReceipt: TransactionReceiptWithDecodedLogs,
actualTokenBalances: TokenBalances,
initialTokenBalances: TokenBalances,
exchangeWrapper: ExchangeWrapper,
): Promise<void> {
// 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,
);
// Check the token balances.
const newBalances = getUpdatedBalances(batchMatchResults, initialTokenBalances);
assertBalances(newBalances, actualTokenBalances);
// Check the Exchange state.
await assertPostBatchExchangeStateAsync(batchMatchResults, exchangeWrapper);
}
/**
* Checks that the results of `simulateMatchOrders()` agrees with reality.
* @param matchResults The results of a `simulateMatchOrders()`.
@ -528,9 +734,36 @@ function assertBalances(expectedBalances: TokenBalances, actualBalances: TokenBa
}
/**
* Asserts initial exchange state for matched orders.
* Asserts the initial exchange state for batch matched orders.
* @param orders Batch matched orders with intial filled amounts.
* @param exchangeWrapper ExchangeWrapper instance.
*/
async function assertBatchOrderStatesAsync(
orders: BatchMatchedOrders,
exchangeWrapper: ExchangeWrapper,
): Promise<void> {
for (let i = 0; i < orders.leftOrders.length; i++) {
await assertOrderFilledAmountAsync(
orders.leftOrders[i],
orders.leftOrdersTakerAssetFilledAmounts[i],
'left',
exchangeWrapper,
);
}
for (let i = 0; i < orders.rightOrders.length; i++) {
await assertOrderFilledAmountAsync(
orders.rightOrders[i],
orders.rightOrdersTakerAssetFilledAmounts[i],
'right',
exchangeWrapper,
);
}
}
/**
* Asserts the initial exchange state for matched orders.
* @param orders Matched orders with intial filled amounts.
* @param exchangeWrapper ExchangeWrapper isntance.
* @param exchangeWrapper ExchangeWrapper instance.
*/
async function assertInitialOrderStatesAsync(orders: MatchedOrders, exchangeWrapper: ExchangeWrapper): Promise<void> {
const pairs = [
@ -540,13 +773,23 @@ async function assertInitialOrderStatesAsync(orders: MatchedOrders, exchangeWrap
await Promise.all(
pairs.map(async ([order, expectedFilledAmount]) => {
const side = order === orders.leftOrder ? 'left' : 'right';
const orderHash = orderHashUtils.getOrderHashHex(order);
const actualFilledAmount = await exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
expect(actualFilledAmount, `${side} order initial filled amount`).to.bignumber.equal(expectedFilledAmount);
await assertOrderFilledAmountAsync(order, expectedFilledAmount, side, exchangeWrapper);
}),
);
}
/**
* Asserts the exchange state after a call to `batchMatchOrders()`.
* @param batchMatchResults Results from a call to `simulateBatchMatchOrders()`.
* @param exchangeWrapper The ExchangeWrapper instance.
*/
async function assertPostBatchExchangeStateAsync(
batchMatchResults: BatchMatchResults,
exchangeWrapper: ExchangeWrapper,
): Promise<void> {
await assertTriplesExchangeStateAsync(batchMatchResults.filledAmounts, exchangeWrapper);
}
/**
* Asserts the exchange state after a call to `matchOrders()`.
* @param matchResults Results from a call to `simulateMatchOrders()`.
@ -556,13 +799,48 @@ async function assertPostExchangeStateAsync(
matchResults: MatchResults,
exchangeWrapper: ExchangeWrapper,
): Promise<void> {
const pairs = [
[matchResults.orders.leftOrder, matchResults.orders.leftOrderTakerAssetFilledAmount],
[matchResults.orders.rightOrder, matchResults.orders.rightOrderTakerAssetFilledAmount],
] as Array<[SignedOrder, BigNumber]>;
const triples = [
[matchResults.orders.leftOrder, matchResults.orders.leftOrderTakerAssetFilledAmount, 'left'],
[matchResults.orders.rightOrder, matchResults.orders.rightOrderTakerAssetFilledAmount, 'right'],
] as Array<[SignedOrder, BigNumber, string]>;
await assertTriplesExchangeStateAsync(triples, exchangeWrapper);
}
/**
* 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.
* @param exchangeWrapper The ExchangeWrapper instance.
*/
async function assertTriplesExchangeStateAsync(
triples: Array<[SignedOrder, BigNumber, string]>,
exchangeWrapper: ExchangeWrapper,
): Promise<void> {
await Promise.all(
pairs.map(async ([order, expectedFilledAmount]) => {
const side = order === matchResults.orders.leftOrder ? 'left' : 'right';
triples.map(async ([order, expectedFilledAmount, side]) => {
expect(['left', 'right']).to.include(side);
await assertOrderFilledAmountAsync(order, expectedFilledAmount, side, exchangeWrapper);
}),
);
}
/**
* 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.
* @param exchangeWrapper The ExchangeWrapper instance.
*/
async function assertOrderFilledAmountAsync(
order: SignedOrder,
expectedFilledAmount: BigNumber,
side: string,
exchangeWrapper: ExchangeWrapper,
): Promise<void> {
const orderInfo = await exchangeWrapper.getOrderInfoAsync(order);
// Check filled amount of order.
const actualFilledAmount = orderInfo.orderTakerAssetFilledAmount;
@ -573,12 +851,10 @@ async function assertPostExchangeStateAsync(
: OrderStatus.Fillable;
const actualStatus = orderInfo.orderStatus;
expect(actualStatus, `${side} order final status`).to.equal(expectedStatus);
}),
);
}
/**
* Retrive the current token balances of all known addresses.
* Retrieve the current token balances of all known addresses.
* @param erc20Wrapper The ERC20Wrapper instance.
* @param erc721Wrapper The ERC721Wrapper instance.
* @param erc1155Wrapper The ERC1155ProxyWrapper instance.
@ -634,4 +910,61 @@ function encodeTokenBalances(obj: any): any {
const keys = _.keys(obj).sort();
return _.zip(keys, keys.map(k => encodeTokenBalances(obj[k])));
}
/**
* 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];
}
/**
* Get the token balances
* @param batchMatchResults The results of a batch order match
* @return The token balances results from after the batch
*/
function getUpdatedBalances(batchMatchResults: BatchMatchResults, initialTokenBalances: TokenBalances): TokenBalances {
return batchMatchResults.matches
.map(match => match.balances)
.reduce((totalBalances, balances) => aggregateBalances(totalBalances, balances, initialTokenBalances));
}
/**
* Takes a `totalBalances`, a `balances`, and an `initialBalances`, subtracts the `initialBalances
* from the `balances`, and then adds the result to `totalBalances`.
* @param totalBalances A set of balances to be updated with new results.
* @param balances A new set of results that deviate from the `initialBalances` by one matched
* order. Subtracting away the `initialBalances` leaves behind a diff of the
* matched orders effect on the `initialBalances`.
* @param initialBalances The token balances from before the call to `batchMatchOrders()`.
* @return The updated total balances using the derived balance difference.
*/
function aggregateBalances(
totalBalances: TokenBalances,
balances: TokenBalances,
initialBalances: TokenBalances,
): TokenBalances {
// ERC20
for (const owner of _.keys(totalBalances.erc20)) {
for (const contract of _.keys(totalBalances.erc20[owner])) {
const difference = balances.erc20[owner][contract].minus(initialBalances.erc20[owner][contract]);
totalBalances.erc20[owner][contract] = totalBalances.erc20[owner][contract].plus(difference);
}
}
// ERC721
for (const owner of _.keys(totalBalances.erc721)) {
for (const contract of _.keys(totalBalances.erc721[owner])) {
totalBalances.erc721[owner][contract] = _.zipWith(
totalBalances.erc721[owner][contract],
balances.erc721[owner][contract],
initialBalances.erc721[owner][contract],
(a: BigNumber, b: BigNumber, c: BigNumber) => a.plus(b.minus(c)),
);
}
}
// TODO(jalextowle): Implement the same as the above for ERC1155
return totalBalances;
}
// tslint:disable-line:max-file-line-count

View File

@ -5,7 +5,7 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from './constants';
import { CancelOrder, MatchOrder } from './types';
import { BatchMatchOrder, CancelOrder, MatchOrder } from './types';
export const orderUtils = {
getPartialAmountFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
@ -33,6 +33,19 @@ export const orderUtils = {
getOrderWithoutDomain(signedOrder: SignedOrder): OrderWithoutDomain {
return _.omit(signedOrder, ['signature', 'domain']) as OrderWithoutDomain;
},
createBatchMatchOrders(signedOrdersLeft: SignedOrder[], signedOrdersRight: SignedOrder[]): BatchMatchOrder {
return {
leftOrders: signedOrdersLeft.map(order => orderUtils.getOrderWithoutDomain(order)),
rightOrders: signedOrdersRight.map(order => {
const right = orderUtils.getOrderWithoutDomain(order);
right.makerAssetData = constants.NULL_BYTES;
right.takerAssetData = constants.NULL_BYTES;
return right;
}),
leftSignatures: signedOrdersLeft.map(order => order.signature),
rightSignatures: signedOrdersRight.map(order => order.signature),
};
},
createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder): MatchOrder {
const fill = {
left: orderUtils.getOrderWithoutDomain(signedOrderLeft),

View File

@ -130,6 +130,13 @@ export interface CancelOrder {
takerAssetCancelAmount: BigNumber;
}
export interface BatchMatchOrder {
leftOrders: OrderWithoutDomain[];
rightOrders: OrderWithoutDomain[];
leftSignatures: string[];
rightSignatures: string[];
}
export interface MatchOrder {
left: OrderWithoutDomain;
right: OrderWithoutDomain;