232 lines
9.4 KiB
TypeScript
232 lines
9.4 KiB
TypeScript
import { DevUtilsContract } from '@0x/contracts-dev-utils';
|
|
import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs';
|
|
import {
|
|
constants,
|
|
expect,
|
|
FillEventArgs,
|
|
filterLogsToArguments,
|
|
OrderStatus,
|
|
orderUtils,
|
|
} from '@0x/contracts-test-utils';
|
|
import { orderHashUtils } from '@0x/order-utils';
|
|
import { FillResults, SignedOrder } from '@0x/types';
|
|
import { BigNumber } from '@0x/utils';
|
|
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
|
import * as _ from 'lodash';
|
|
|
|
import { ExchangeContract } from '../wrappers';
|
|
|
|
import {
|
|
BalanceStore,
|
|
BlockchainBalanceStore,
|
|
LocalBalanceStore,
|
|
TokenContractsByName,
|
|
TokenIds,
|
|
TokenOwnersByName,
|
|
} from './../balance_stores';
|
|
|
|
export class FillOrderWrapper {
|
|
private readonly _blockchainBalanceStore: BlockchainBalanceStore;
|
|
|
|
/**
|
|
* Simulates the event emitted by the exchange contract when an order is filled.
|
|
*/
|
|
public static simulateFillEvent(order: SignedOrder, takerAddress: string, fillResults: FillResults): FillEventArgs {
|
|
// prettier-ignore
|
|
return {
|
|
orderHash: orderHashUtils.getOrderHashHex(order),
|
|
makerAddress: order.makerAddress,
|
|
takerAddress,
|
|
makerAssetFilledAmount: fillResults.makerAssetFilledAmount,
|
|
takerAssetFilledAmount: fillResults.takerAssetFilledAmount,
|
|
makerFeePaid: fillResults.makerFeePaid,
|
|
takerFeePaid: fillResults.takerFeePaid,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract the exchanges `Fill` event from a transaction receipt.
|
|
*/
|
|
private static _extractFillEventsfromReceipt(receipt: TransactionReceiptWithDecodedLogs): FillEventArgs[] {
|
|
const events = filterLogsToArguments<FillEventArgs>(receipt.logs, 'Fill');
|
|
const fieldsOfInterest = [
|
|
'orderHash',
|
|
'makerAddress',
|
|
'takerAddress',
|
|
'makerAssetFilledAmount',
|
|
'takerAssetFilledAmount',
|
|
'makerFeePaid',
|
|
'takerFeePaid',
|
|
];
|
|
return events.map(event => _.pick(event, fieldsOfInterest)) as FillEventArgs[];
|
|
}
|
|
|
|
/**
|
|
* Locally simulates filling an order.
|
|
* @param txReceipt Transaction receipt from the actual fill, needed to update eth balance
|
|
* @param signedOrder The order being filled.
|
|
* @param takerAddress Address of taker (the address who matched the two orders)
|
|
* @param opts Optionally specifies the amount to fill.
|
|
* @param initBalanceStore Account balances prior to the fill.
|
|
* @return The expected account balances, fill results, and fill events.
|
|
*/
|
|
public async simulateFillOrderAsync(
|
|
txReceipt: TransactionReceiptWithDecodedLogs,
|
|
signedOrder: SignedOrder,
|
|
takerAddress: string,
|
|
initBalanceStore: BalanceStore,
|
|
opts: { takerAssetFillAmount?: BigNumber } = {},
|
|
): Promise<[FillResults, FillEventArgs, BalanceStore]> {
|
|
const balanceStore = LocalBalanceStore.create(this._devUtils, initBalanceStore);
|
|
const takerAssetFillAmount =
|
|
opts.takerAssetFillAmount !== undefined ? opts.takerAssetFillAmount : signedOrder.takerAssetAmount;
|
|
// TODO(jalextowle): Change this if the integration tests take protocol fees into account.
|
|
const fillResults = LibReferenceFunctions.calculateFillResults(
|
|
signedOrder,
|
|
takerAssetFillAmount,
|
|
constants.ZERO_AMOUNT,
|
|
constants.ZERO_AMOUNT,
|
|
);
|
|
const fillEvent = FillOrderWrapper.simulateFillEvent(signedOrder, takerAddress, fillResults);
|
|
// Taker -> Maker
|
|
await balanceStore.transferAssetAsync(
|
|
takerAddress,
|
|
signedOrder.makerAddress,
|
|
fillResults.takerAssetFilledAmount,
|
|
signedOrder.takerAssetData,
|
|
);
|
|
// Maker -> Taker
|
|
await balanceStore.transferAssetAsync(
|
|
signedOrder.makerAddress,
|
|
takerAddress,
|
|
fillResults.makerAssetFilledAmount,
|
|
signedOrder.makerAssetData,
|
|
);
|
|
// Taker -> Fee Recipient
|
|
await balanceStore.transferAssetAsync(
|
|
takerAddress,
|
|
signedOrder.feeRecipientAddress,
|
|
fillResults.takerFeePaid,
|
|
signedOrder.takerFeeAssetData,
|
|
);
|
|
// Maker -> Fee Recipient
|
|
await balanceStore.transferAssetAsync(
|
|
signedOrder.makerAddress,
|
|
signedOrder.feeRecipientAddress,
|
|
fillResults.makerFeePaid,
|
|
signedOrder.makerFeeAssetData,
|
|
);
|
|
balanceStore.burnGas(txReceipt.from, constants.DEFAULT_GAS_PRICE * txReceipt.gasUsed);
|
|
return [fillResults, fillEvent, balanceStore];
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
* @param exchangeContract Instance of the deployed exchange contract.
|
|
* @param tokenOwnersByName The addresses of token owners to assert the balances of.
|
|
* @param tokenContractsByName The contracts of tokens to assert the balances of.
|
|
* @param tokenIds The tokenIds of ERC721 and ERC1155 assets to assert the balances of.
|
|
*/
|
|
public constructor(
|
|
private readonly _exchange: ExchangeContract,
|
|
private readonly _devUtils: DevUtilsContract,
|
|
tokenOwnersByName: TokenOwnersByName,
|
|
tokenContractsByName: Partial<TokenContractsByName>,
|
|
tokenIds: Partial<TokenIds>,
|
|
) {
|
|
this._blockchainBalanceStore = new BlockchainBalanceStore(tokenOwnersByName, tokenContractsByName, tokenIds);
|
|
}
|
|
|
|
/**
|
|
* Returns the balance store used by this wrapper.
|
|
*/
|
|
public getBlockchainBalanceStore(): BlockchainBalanceStore {
|
|
return this._blockchainBalanceStore;
|
|
}
|
|
|
|
/**
|
|
* Fills an order and asserts the effects. This includes
|
|
* 1. The order info (via `getOrderInfo`)
|
|
* 2. The fill results returned by making an `eth_call` to `exchange.fillOrder`
|
|
* 3. The events emitted by the exchange when the order is filled.
|
|
* 4. The balance changes as a result of filling the order.
|
|
*/
|
|
public async fillOrderAndAssertEffectsAsync(
|
|
signedOrder: SignedOrder,
|
|
from: string,
|
|
opts: { takerAssetFillAmount?: BigNumber } = {},
|
|
): Promise<void> {
|
|
// Get init state
|
|
await this._blockchainBalanceStore.updateBalancesAsync();
|
|
const initTakerAssetFilledAmount = await this._exchange.filled.callAsync(
|
|
orderHashUtils.getOrderHashHex(signedOrder),
|
|
);
|
|
// Assert init state of exchange
|
|
await this._assertOrderStateAsync(signedOrder, initTakerAssetFilledAmount);
|
|
// Simulate and execute fill then assert outputs
|
|
const [fillResults, fillEvent, txReceipt] = await this._fillOrderAsync(signedOrder, from, opts);
|
|
const [
|
|
simulatedFillResults,
|
|
simulatedFillEvent,
|
|
simulatedFinalBalanceStore,
|
|
] = await this.simulateFillOrderAsync(txReceipt, signedOrder, from, this._blockchainBalanceStore, opts);
|
|
// Assert state transition
|
|
expect(simulatedFillResults, 'Fill Results').to.be.deep.equal(fillResults);
|
|
expect(simulatedFillEvent, 'Fill Events').to.be.deep.equal(fillEvent);
|
|
|
|
await this._blockchainBalanceStore.updateBalancesAsync();
|
|
this._blockchainBalanceStore.assertEquals(simulatedFinalBalanceStore);
|
|
|
|
// Assert end state of exchange
|
|
const finalTakerAssetFilledAmount = initTakerAssetFilledAmount.plus(fillResults.takerAssetFilledAmount);
|
|
await this._assertOrderStateAsync(signedOrder, finalTakerAssetFilledAmount);
|
|
}
|
|
|
|
/**
|
|
* Fills an order on-chain.
|
|
*/
|
|
protected async _fillOrderAsync(
|
|
signedOrder: SignedOrder,
|
|
from: string,
|
|
opts: { takerAssetFillAmount?: BigNumber } = {},
|
|
): Promise<[FillResults, FillEventArgs, TransactionReceiptWithDecodedLogs]> {
|
|
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
|
|
const fillResults = await this._exchange.fillOrder.callAsync(
|
|
params.order,
|
|
params.takerAssetFillAmount,
|
|
params.signature,
|
|
{ from },
|
|
);
|
|
const txReceipt = await this._exchange.fillOrder.awaitTransactionSuccessAsync(
|
|
params.order,
|
|
params.takerAssetFillAmount,
|
|
params.signature,
|
|
{ from },
|
|
);
|
|
const fillEvent = FillOrderWrapper._extractFillEventsfromReceipt(txReceipt)[0];
|
|
return [fillResults, fillEvent, txReceipt];
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private async _assertOrderStateAsync(
|
|
order: SignedOrder,
|
|
expectedFilledAmount: BigNumber = new BigNumber(0),
|
|
): Promise<void> {
|
|
const orderInfo = await this._exchange.getOrderInfo.callAsync(order);
|
|
// Check filled amount of order.
|
|
const actualFilledAmount = orderInfo.orderTakerAssetFilledAmount;
|
|
expect(actualFilledAmount, 'order 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, 'order status').to.equal(expectedStatus);
|
|
}
|
|
}
|