250 lines
10 KiB
TypeScript

import {
artifacts as proxyArtifacts,
ERC1155ProxyWrapper,
ERC20Wrapper,
ERC721Wrapper,
} from '@0x/contracts-asset-proxy';
import { artifacts as erc20Artifacts } from '@0x/contracts-erc20';
import { artifacts as erc721Artifacts } from '@0x/contracts-erc721';
import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
constants,
expect,
FillEventArgs,
filterLogsToArguments,
LogDecoder,
OrderStatus,
orderUtils,
Web3ProviderEngine,
} from '@0x/contracts-test-utils';
import { orderHashUtils } from '@0x/order-utils';
import { FillResults, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { TransactionReceiptWithDecodedLogs, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts, ExchangeContract } from '../../src';
import { BalanceStore } from '../balance_stores/balance_store';
import { BlockchainBalanceStore } from '../balance_stores/blockchain_balance_store';
import { LocalBalanceStore } from '../balance_stores/local_balance_store';
export class FillOrderWrapper {
private readonly _exchange: ExchangeContract;
private readonly _blockchainBalanceStore: BlockchainBalanceStore;
private readonly _web3Wrapper: Web3Wrapper;
/**
* 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 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.
*/
public static simulateFillOrder(
signedOrder: SignedOrder,
takerAddress: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
initBalanceStore: BalanceStore,
): [FillResults, FillEventArgs, BalanceStore] {
const balanceStore = LocalBalanceStore.create(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
balanceStore.transferAsset(
takerAddress,
signedOrder.makerAddress,
fillResults.takerAssetFilledAmount,
signedOrder.takerAssetData,
);
// Maker -> Taker
balanceStore.transferAsset(
signedOrder.makerAddress,
takerAddress,
fillResults.makerAssetFilledAmount,
signedOrder.makerAssetData,
);
// Taker -> Fee Recipient
balanceStore.transferAsset(
takerAddress,
signedOrder.feeRecipientAddress,
fillResults.takerFeePaid,
signedOrder.takerFeeAssetData,
);
// Maker -> Fee Recipient
balanceStore.transferAsset(
signedOrder.makerAddress,
signedOrder.feeRecipientAddress,
fillResults.makerFeePaid,
signedOrder.makerFeeAssetData,
);
return [fillResults, fillEvent, balanceStore];
}
/**
* 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[];
}
/**
* Constructor.
* @param exchangeContract Insstance of the deployed exchange contract
* @param erc20Wrapper The ERC20 Wrapper used to interface with deployed erc20 tokens.
* @param erc721Wrapper The ERC721 Wrapper used to interface with deployed erc20 tokens.
* @param erc1155ProxyWrapper The ERC1155 Proxy Wrapper used to interface with deployed erc20 tokens.
* @param provider Web3 provider to be used by a `Web3Wrapper` instance
*/
public constructor(
exchangeContract: ExchangeContract,
erc20Wrapper: ERC20Wrapper,
erc721Wrapper: ERC721Wrapper,
erc1155ProxyWrapper: ERC1155ProxyWrapper,
provider: Web3ProviderEngine | ZeroExProvider,
) {
this._exchange = exchangeContract;
this._blockchainBalanceStore = new BlockchainBalanceStore(erc20Wrapper, erc721Wrapper, erc1155ProxyWrapper);
this._web3Wrapper = new Web3Wrapper(provider);
}
/**
* 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 [
simulatedFillResults,
simulatedFillEvent,
simulatedFinalBalanceStore,
] = FillOrderWrapper.simulateFillOrder(signedOrder, from, opts, this._blockchainBalanceStore);
const [fillResults, fillEvent] = await this._fillOrderAsync(signedOrder, from, opts);
// Assert state transition
expect(simulatedFillResults, 'Fill Results').to.be.deep.equal(fillResults);
expect(simulatedFillEvent, 'Fill Events').to.be.deep.equal(fillEvent);
const areBalancesEqual = BalanceStore.isEqual(simulatedFinalBalanceStore, this._blockchainBalanceStore);
expect(areBalancesEqual, 'Balances After Fill').to.be.true();
// Assert end state of exchange
const finalTakerAssetFilledAmount = initTakerAssetFilledAmount.plus(fillResults.takerAssetFilledAmount);
await this._assertOrderStateAsync(signedOrder, finalTakerAssetFilledAmount);
}
/**
* Fills an order on-chain. As an optimization this function auto-updates the blockchain balance store
* used by this contract.
*/
protected async _fillOrderAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<[FillResults, FillEventArgs]> {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const fillResults = await this._exchange.fillOrder.callAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
// @TODO: Replace with `awaitTransactionAsync` once `development` is merged into `3.0` branch
const txHash = await this._exchange.fillOrder.sendTransactionAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
const logDecoder = new LogDecoder(this._web3Wrapper, {
...artifacts,
...proxyArtifacts,
...erc20Artifacts,
...erc721Artifacts,
});
const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(txHash);
const fillEvent = FillOrderWrapper._extractFillEventsfromReceipt(txReceipt)[0];
await this._blockchainBalanceStore.updateBalancesAsync();
return [fillResults, fillEvent];
}
/**
* 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.
*/
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);
}
}