Created tests for batchMatchOrders
This commit is contained in:
parent
f289b3112b
commit
6cf11554de
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user