265 lines
11 KiB
TypeScript
265 lines
11 KiB
TypeScript
import {
|
|
blockchainTests,
|
|
constants,
|
|
describe,
|
|
expect,
|
|
hexRandom,
|
|
TransactionHelper,
|
|
} from '@0x/contracts-test-utils';
|
|
import { ExchangeRevertErrors } from '@0x/order-utils';
|
|
import { FillResults, OrderInfo, OrderStatus, OrderWithoutDomain as Order } from '@0x/types';
|
|
import { BigNumber, StringRevertError } from '@0x/utils';
|
|
import { LogEntry, LogWithDecodedArgs } from 'ethereum-types';
|
|
import * as ethjs from 'ethereumjs-util';
|
|
import * as _ from 'lodash';
|
|
|
|
import {
|
|
artifacts,
|
|
TestWrapperFunctionsContract,
|
|
TestWrapperFunctionsFillOrderCalledEventArgs as FillOrderCalledEventArgs,
|
|
} from '../src';
|
|
|
|
blockchainTests.only('Exchange wrapper functions unit tests.', env => {
|
|
const { ONE_ETHER } = constants;
|
|
const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH);
|
|
const randomAssetData = () => hexRandom(34);
|
|
const randomSignature = () => `${hexRandom(65)}02`;
|
|
const randomAmount = (maxAmount: BigNumber = ONE_ETHER) => maxAmount.times(_.random(0, 100, true).toFixed(12));
|
|
const randomTimestamp = () => new BigNumber(Math.floor(_.now() / 1000) + _.random(0, 34560));
|
|
const randomSalt = () => new BigNumber(hexRandom(constants.WORD_LENGTH).substr(2), 16);
|
|
const ALWAYS_FAILING_SALT = constants.MAX_UINT256;
|
|
const FILL_ORDER_FAILED_REVERT_ERROR = new StringRevertError('FILL_ORDER_FAILED');
|
|
const EMPTY_FILL_RESULTS = {
|
|
makerAssetFilledAmount: constants.ZERO_AMOUNT,
|
|
takerAssetFilledAmount: constants.ZERO_AMOUNT,
|
|
makerFeePaid: constants.ZERO_AMOUNT,
|
|
takerFeePaid: constants.ZERO_AMOUNT,
|
|
};
|
|
let testContract: TestWrapperFunctionsContract;
|
|
let txHelper: TransactionHelper;
|
|
|
|
before(async () => {
|
|
txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
|
|
testContract = await TestWrapperFunctionsContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestWrapperFunctions,
|
|
env.provider,
|
|
env.txDefaults,
|
|
);
|
|
});
|
|
|
|
function randomOrder(fields?: Partial<Order>): Order {
|
|
return _.assign({
|
|
makerAddress: randomAddress(),
|
|
takerAddress: randomAddress(),
|
|
feeRecipientAddress: randomAddress(),
|
|
senderAddress: randomAddress(),
|
|
takerAssetAmount: randomAmount(),
|
|
makerAssetAmount: randomAmount(),
|
|
makerFee: randomAmount(),
|
|
takerFee: randomAmount(),
|
|
expirationTimeSeconds: randomTimestamp(),
|
|
salt: randomSalt(),
|
|
makerAssetData: randomAssetData(),
|
|
takerAssetData: randomAssetData(),
|
|
makerFeeAssetData: randomAssetData(),
|
|
takerFeeAssetData: randomAssetData(),
|
|
}, fields);
|
|
}
|
|
|
|
// Computes the expected (fake) order hash generated by the `TestWrapperFunctions` contract.
|
|
function getExpectedOrderHash(order: Order): string {
|
|
// It's just `keccak256(order.salt)`.
|
|
return ethjs.bufferToHex(ethjs.sha3(ethjs.setLengthLeft(
|
|
`0x${order.salt.toString(16)}`, constants.WORD_LENGTH)));
|
|
}
|
|
|
|
// Computes the expected (fake) fill results from `TestWrapperFunctions` `_fillOrder` implementation.
|
|
function getExpectedFillResults(order: Order): FillResults {
|
|
return {
|
|
makerAssetFilledAmount: order.makerAssetAmount,
|
|
takerAssetFilledAmount: order.takerAssetAmount,
|
|
makerFeePaid: order.makerFee,
|
|
takerFeePaid: order.takerFee,
|
|
};
|
|
}
|
|
|
|
// Asserts that `_fillOrder()` was called in the same order and with the same
|
|
// arguments as given by examining receipt logs.
|
|
function assertFillOrderCallsFromLogs(
|
|
logs: LogEntry[],
|
|
calls: Array<[Order, BigNumber, string]>,
|
|
): void {
|
|
expect(logs.length).to.eq(calls.length);
|
|
for (const i of _.times(calls.length)) {
|
|
const log = logs[i] as LogWithDecodedArgs<FillOrderCalledEventArgs>;
|
|
const [
|
|
expectedOrder,
|
|
expectedTakerAssetFillAmount,
|
|
expectedSignature,
|
|
] = calls[i];
|
|
expect(log.event).to.eq('FillOrderCalled');
|
|
assertSameOrderFromEvent(log.args.order as any, expectedOrder);
|
|
expect(log.args.takerAssetFillAmount).to.bignumber.eq(expectedTakerAssetFillAmount);
|
|
expect(log.args.signature).to.eq(expectedSignature);
|
|
}
|
|
}
|
|
|
|
function assertSameOrderFromEvent(actual: any[], expected: Order): void {
|
|
expect(actual.length === 14);
|
|
expect(actual[0].toLowerCase()).to.be.eq(expected.makerAddress);
|
|
expect(actual[1].toLowerCase()).to.be.eq(expected.takerAddress);
|
|
expect(actual[2].toLowerCase()).to.be.eq(expected.feeRecipientAddress);
|
|
expect(actual[3].toLowerCase()).to.be.eq(expected.senderAddress);
|
|
expect(actual[4]).to.be.bignumber.eq(expected.makerAssetAmount);
|
|
expect(actual[5]).to.be.bignumber.eq(expected.takerAssetAmount);
|
|
expect(actual[6]).to.be.bignumber.eq(expected.makerFee);
|
|
expect(actual[7]).to.be.bignumber.eq(expected.takerFee);
|
|
expect(actual[8]).to.be.bignumber.eq(expected.expirationTimeSeconds);
|
|
expect(actual[9]).to.be.bignumber.eq(expected.salt);
|
|
expect(actual[10]).to.be.eq(expected.makerAssetData);
|
|
expect(actual[11]).to.be.eq(expected.takerAssetData);
|
|
expect(actual[12]).to.be.eq(expected.makerFeeAssetData);
|
|
expect(actual[13]).to.be.eq(expected.takerFeeAssetData);
|
|
}
|
|
|
|
describe('fillOrKillOrder', () => {
|
|
it('works if the order is filled by exactly `takerAssetFillAmount`', async () => {
|
|
const fillAmount = randomAmount();
|
|
const order = randomOrder({
|
|
// `_fillOrder()` is overridden to always return `order.takerAssetAmount` as
|
|
// the `takerAssetFilledAmount`.
|
|
takerAssetAmount: fillAmount,
|
|
});
|
|
const signature = randomSignature();
|
|
const expected = getExpectedFillResults(order);
|
|
const [ actual, receipt ] = await txHelper.getResultAndReceiptAsync(
|
|
testContract.fillOrKillOrder,
|
|
order,
|
|
fillAmount,
|
|
signature,
|
|
);
|
|
expect(actual).to.deep.eq(expected);
|
|
assertFillOrderCallsFromLogs(receipt.logs, [[ order, fillAmount, signature ]]);
|
|
});
|
|
|
|
it('reverts if the order is filled by less than `takerAssetFillAmount`', async () => {
|
|
const fillAmount = randomAmount();
|
|
const order = randomOrder({
|
|
// `_fillOrder()` is overridden to always return `order.takerAssetAmount` as
|
|
// the `takerAssetFilledAmount`.
|
|
takerAssetAmount: fillAmount.minus(1),
|
|
});
|
|
const expectedError = new ExchangeRevertErrors.IncompleteFillError(
|
|
getExpectedOrderHash(order),
|
|
);
|
|
const tx = testContract.fillOrKillOrder.awaitTransactionSuccessAsync(
|
|
order,
|
|
fillAmount,
|
|
randomSignature(),
|
|
);
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
it('reverts if the order is filled by greater than `takerAssetFillAmount`', async () => {
|
|
const fillAmount = randomAmount();
|
|
const order = randomOrder({
|
|
// `_fillOrder()` is overridden to always return `order.takerAssetAmount` as
|
|
// the `takerAssetFilledAmount`.
|
|
takerAssetAmount: fillAmount.plus(1),
|
|
});
|
|
const expectedError = new ExchangeRevertErrors.IncompleteFillError(
|
|
getExpectedOrderHash(order),
|
|
);
|
|
const tx = testContract.fillOrKillOrder.awaitTransactionSuccessAsync(
|
|
order,
|
|
fillAmount,
|
|
randomSignature(),
|
|
);
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
it('reverts if `_fillOrder()` reverts', async () => {
|
|
const fillAmount = randomAmount();
|
|
const order = randomOrder({
|
|
salt: ALWAYS_FAILING_SALT,
|
|
});
|
|
const expectedError = FILL_ORDER_FAILED_REVERT_ERROR;
|
|
const tx = testContract.fillOrKillOrder.awaitTransactionSuccessAsync(
|
|
order,
|
|
fillAmount,
|
|
randomSignature(),
|
|
);
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
});
|
|
|
|
describe('fillOrderNoThrow', () => {
|
|
it('calls `fillOrder()` and returns its result', async () => {
|
|
const fillAmount = randomAmount();
|
|
const order = randomOrder();
|
|
const signature = randomSignature();
|
|
const expected = getExpectedFillResults(order);
|
|
const [ actual, receipt ] = await txHelper.getResultAndReceiptAsync(
|
|
testContract.fillOrderNoThrow,
|
|
order,
|
|
fillAmount,
|
|
signature,
|
|
);
|
|
expect(actual).to.deep.eq(expected);
|
|
assertFillOrderCallsFromLogs(receipt.logs, [[ order, fillAmount, signature ]]);
|
|
});
|
|
|
|
it('does not revert if `fillOrder()` reverts', async () => {
|
|
const fillAmount = randomAmount();
|
|
const order = randomOrder({
|
|
salt: ALWAYS_FAILING_SALT,
|
|
});
|
|
const signature = randomSignature();
|
|
const expected = EMPTY_FILL_RESULTS;
|
|
const [ actual, receipt ] = await txHelper.getResultAndReceiptAsync(
|
|
testContract.fillOrderNoThrow,
|
|
order,
|
|
fillAmount,
|
|
signature,
|
|
);
|
|
expect(actual).to.deep.eq(expected);
|
|
assertFillOrderCallsFromLogs(receipt.logs, []);
|
|
});
|
|
});
|
|
|
|
describe('getOrdersInfo', () => {
|
|
// Computes the expected (fake) order info generated by the `TestWrapperFunctions` contract.
|
|
function getExpectedOrderInfo(order: Order): OrderInfo {
|
|
const MAX_ORDER_STATUS = OrderStatus.Cancelled;
|
|
return {
|
|
orderHash: getExpectedOrderHash(order),
|
|
// Lower uint128 of `order.salt` is the `orderTakerAssetFilledAmount`.
|
|
orderTakerAssetFilledAmount: order.salt.mod(new BigNumber(2).pow(128)),
|
|
// High byte of `order.salt` is the `orderStatus`.
|
|
orderStatus: order.salt.dividedToIntegerBy(
|
|
new BigNumber(2).pow(248)).toNumber() % (MAX_ORDER_STATUS + 1),
|
|
};
|
|
}
|
|
|
|
it('works with no orders', async () => {
|
|
const infos = await testContract.getOrdersInfo.callAsync([]);
|
|
expect(infos.length).to.eq(0);
|
|
});
|
|
|
|
it('works with one order', async () => {
|
|
const orders = [ randomOrder() ];
|
|
const expected = orders.map(getExpectedOrderInfo);
|
|
const actual = await testContract.getOrdersInfo.callAsync(orders);
|
|
expect(actual).to.deep.eq(expected);
|
|
});
|
|
|
|
it('works with many orders', async () => {
|
|
const NUM_ORDERS = 16;
|
|
const orders = _.times(NUM_ORDERS, () => randomOrder());
|
|
const expected = orders.map(getExpectedOrderInfo);
|
|
const actual = await testContract.getOrdersInfo.callAsync(orders);
|
|
expect(actual).to.deep.eq(expected);
|
|
});
|
|
});
|
|
});
|