@0x/contracts-exchange: More TestIsolatedExchange rework.

This commit is contained in:
Lawrence Forman 2019-07-31 10:24:00 -04:00
parent 039cc6e28b
commit e2bd80253b
3 changed files with 61 additions and 124 deletions

View File

@ -36,38 +36,13 @@ contract TestIsolatedExchange is
uint256 amount uint256 amount
); );
/// @dev Raw asset balances of addresses based on asset data hash.
/// These start at 0 and are allowed to be negative.
/// Updated by `_dispatchTransferFrom()`.
mapping(bytes32 => mapping(address => int256)) public rawAssetBalances;
// solhint-disable no-empty-blocks // solhint-disable no-empty-blocks
constructor () constructor ()
public public
Exchange(1337) Exchange(1337)
{} {}
/// @dev Convenience function to get the `rawAssetBalances` for /// @dev Overriden to only log arguments.
/// multiple assets and addresses.
function getRawAssetBalances(
bytes[] calldata assets,
address[] calldata addresses
)
external
returns (int256[][] memory balances)
{
balances = new int256[][](assets.length);
for (uint assetIdx = 0; assetIdx < assets.length; ++assetIdx) {
balances[assetIdx] = new int256[](addresses.length);
mapping(address => int256) storage assetBalances =
rawAssetBalances[keccak256(assets[assetIdx])];
for (uint addrIdx = 0; addrIdx < addresses.length; ++addrIdx) {
balances[assetIdx][addrIdx] = assetBalances[addresses[addrIdx]];
}
}
}
/// @dev Overriden to only log arguments and track raw asset balances.
function _dispatchTransferFrom( function _dispatchTransferFrom(
bytes32 orderHash, bytes32 orderHash,
bytes memory assetData, bytes memory assetData,
@ -84,11 +59,6 @@ contract TestIsolatedExchange is
to, to,
amount amount
); );
mapping(address => int256) storage assetBalances =
rawAssetBalances[keccak256(assetData)];
assetBalances[from] = _subAssetAmount(assetBalances[from], amount);
assetBalances[to] = _addAssetAmount(assetBalances[to], amount);
} }
/// @dev Overriden to simplify signature validation. /// @dev Overriden to simplify signature validation.
@ -106,14 +76,4 @@ contract TestIsolatedExchange is
// '0x01' in the first byte is valid. // '0x01' in the first byte is valid.
return signature.length == 2 && signature[0] == 0x01; return signature.length == 2 && signature[0] == 0x01;
} }
function _subAssetAmount(int256 a, uint256 b) private pure returns (int256 r) {
r = a - int256(b);
require(r <= a, "ASSET_AMOUNT_UNDERFLOW");
}
function _addAssetAmount(int256 a, uint256 b) private pure returns (int256 r) {
r = a + int256(b);
require(r >= a, "ASSET_AMOUNT_OVERFLOW");
}
} }

View File

@ -4,7 +4,6 @@ import * as _ from 'lodash';
import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper'; import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper';
blockchainTests.resets.only('Isolated fillOrder() tests', env => { blockchainTests.resets.only('Isolated fillOrder() tests', env => {
const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24;
const ERC20_ASSET_DATA_LENGTH = 24; const ERC20_ASSET_DATA_LENGTH = 24;
@ -47,7 +46,6 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => {
takerAssetAmount: toBN(2), takerAssetAmount: toBN(2),
}); });
const results = await testExchange.fillOrderAsync(order, 2); const results = await testExchange.fillOrderAsync(order, 2);
// console.log(results, testExchange.getOrderHash(order));
}); });
} }
}); });

View File

@ -1,4 +1,4 @@
import { FillResults, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; import { constants, FillResults, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils';
import { orderHashUtils } from '@0x/order-utils'; import { orderHashUtils } from '@0x/order-utils';
import { OrderWithoutDomain, SignatureType } from '@0x/types'; import { OrderWithoutDomain, SignatureType } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
@ -13,20 +13,6 @@ import {
TestIsolatedExchangeFillEventArgs as FillEventArgs, TestIsolatedExchangeFillEventArgs as FillEventArgs,
} from '../../src'; } from '../../src';
/**
* @dev Create a signature for the `TestIsolatedExchange` contract that will pass.
*/
export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string {
return `0x01${Buffer.from([type]).toString('hex')}`;
}
/**
* @dev Create a signature for the `TestIsolatedExchange` contract that will fail.
*/
export function createBadSignature(type: SignatureType = SignatureType.EIP712): string {
return `0x00${Buffer.from([type]).toString('hex')}`;
}
export interface AssetBalances { export interface AssetBalances {
[assetData: string]: { [address: string]: BigNumber }; [assetData: string]: { [address: string]: BigNumber };
} }
@ -36,29 +22,11 @@ export interface IsolatedExchangeEvents {
transferFromCalls: DispatchTransferFromCallArgs[]; transferFromCalls: DispatchTransferFromCallArgs[];
} }
export interface EventsAndBalances {
events: IsolatedExchangeEvents;
balances: AssetBalances;
}
export interface IsolatedFillOrderResults extends EventsAndBalances {
fillResults: FillResults;
}
export type Order = OrderWithoutDomain; export type Order = OrderWithoutDomain;
export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); export const DEFAULT_GOOD_SIGNATURE = createGoodSignature();
export const DEFAULT_BAD_SIGNATURE = createBadSignature(); export const DEFAULT_BAD_SIGNATURE = createBadSignature();
interface CallAndSendResult<TResult> extends EventsAndBalances {
result: TResult;
}
interface TransactionContractFunction<TResult> {
callAsync: (...args: any[]) => Promise<TResult>;
sendTransactionAsync: (...args: any[]) => Promise<string>;
}
/** /**
* @dev Convenience wrapper for the `TestIsolatedExchange` contract. * @dev Convenience wrapper for the `TestIsolatedExchange` contract.
*/ */
@ -66,6 +34,8 @@ export class IsolatedExchangeWrapper {
public static readonly CHAIN_ID = 1337; public static readonly CHAIN_ID = 1337;
public instance: TestIsolatedExchangeContract; public instance: TestIsolatedExchangeContract;
public logDecoder: LogDecoder; public logDecoder: LogDecoder;
public lastTxEvents: IsolatedExchangeEvents = createEmptyEvents();
public lastTxBalanceChanges: AssetBalances = {};
public static async deployAsync( public static async deployAsync(
web3Wrapper: Web3Wrapper, web3Wrapper: Web3Wrapper,
@ -100,20 +70,14 @@ export class IsolatedExchangeWrapper {
takerAssetFillAmount: BigNumber | number, takerAssetFillAmount: BigNumber | number,
signature: string = DEFAULT_GOOD_SIGNATURE, signature: string = DEFAULT_GOOD_SIGNATURE,
txOpts?: TxData, txOpts?: TxData,
): Promise<IsolatedFillOrderResults> { ): Promise<FillResults> {
const _takerAssetFillAmount = new BigNumber(takerAssetFillAmount); return this._callAndSendExchangeFunctionAsync<FillResults>(
const results = await this._callAndSendExchangeFunctionAsync<FillResults>(
this.instance.fillOrder, this.instance.fillOrder,
order, order,
_takerAssetFillAmount, new BigNumber(takerAssetFillAmount),
signature, signature,
txOpts, txOpts,
); );
return {
fillResults: results.result,
events: results.events,
balances: results.balances,
};
} }
public getOrderHash(order: Order): string { public getOrderHash(order: Order): string {
@ -124,61 +88,60 @@ export class IsolatedExchangeWrapper {
return orderHashUtils.getOrderHashHex(_.assign(order, { domain })); return orderHashUtils.getOrderHashHex(_.assign(order, { domain }));
} }
public async getAssetBalanceAsync(assetData: string, address: string): Promise<BigNumber[]> { public getBalanceChange(assetData: string, address: string): BigNumber {
return this.getAssetBalancesAsync(assetData, [ address ]); if (assetData in this.lastTxBalanceChanges) {
} const balances = this.lastTxBalanceChanges[assetData];
if (address in balances) {
public async getAssetBalancesAsync(assetData: string, addresses: string[]): Promise<BigNumber[]> { return balances[address];
return (await this.instance.getRawAssetBalances.callAsync([ assetData ], addresses))[0];
}
public async getBalancesAsync(assets: string[], addresses: string[]):
Promise<AssetBalances> {
const callResults = await this.instance.getRawAssetBalances.callAsync(assets, addresses);
const result: AssetBalances = {};
for (const i of _.times(assets.length)) {
const assetData = assets[i];
result[assetData] = {};
for (const j of _.times(addresses.length)) {
const address = addresses[j];
result[assetData][address] = callResults[i][j];
} }
} }
return result; return constants.ZERO_AMOUNT;
}
protected async _getBalancesFromTransferFromCallsAsync(
calls: DispatchTransferFromCallArgs[],
): Promise<AssetBalances> {
// Extract addresses involved in transfers.
const addresses = _.uniq(_.flatten(calls.map(c => [c.from, c.to])));
// Extract assets involved in transfers.
const assets = _.uniq(calls.map(c => c.assetData));
// Query balances of addresses and assets involved in transfers.
return this.getBalancesAsync(assets, addresses);
} }
protected async _callAndSendExchangeFunctionAsync<TResult>( protected async _callAndSendExchangeFunctionAsync<TResult>(
instanceMethod: TransactionContractFunction<TResult>, instanceMethod: TransactionContractFunction<TResult>,
// tslint:disable-next-line: trailing-comma // tslint:disable-next-line: trailing-comma
...args: any[] ...args: any[]
): Promise<CallAndSendResult<TResult>> { ): Promise<TResult> {
this.lastTxEvents = createEmptyEvents();
this.lastTxBalanceChanges = {};
// Call to get the return value. // Call to get the return value.
const result = await instanceMethod.callAsync.call(this.instance, ...args); const result = await instanceMethod.callAsync(...args);
// Transact to execute it. // Transact to execute it.
const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( const receipt = await this.logDecoder.getTxWithDecodedLogsAsync(
await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args), await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args),
); );
const events = extractEvents(receipt.logs); this.lastTxEvents = extractEvents(receipt.logs);
const balances = await this._getBalancesFromTransferFromCallsAsync(events.transferFromCalls); this.lastTxBalanceChanges = getBalanceChangesFromTransferFromCalls(
return { this.lastTxEvents.transferFromCalls,
result, );
events, return result;
balances,
};
} }
} }
interface TransactionContractFunction<TResult> {
callAsync: (...args: any[]) => Promise<TResult>;
sendTransactionAsync: (...args: any[]) => Promise<string>;
}
/**
* @dev Create a signature for the `TestIsolatedExchange` contract that will pass.
*/
export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string {
return `0x01${Buffer.from([type]).toString('hex')}`;
}
/**
* @dev Create a signature for the `TestIsolatedExchange` contract that will fail.
*/
export function createBadSignature(type: SignatureType = SignatureType.EIP712): string {
return `0x00${Buffer.from([type]).toString('hex')}`;
}
function createEmptyEvents(): IsolatedExchangeEvents {
return { fillEvents: [], transferFromCalls: [] };
}
function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents { function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents {
return { return {
fillEvents: filterLogsToArguments<FillEventArgs>(logs, 'Fill'), fillEvents: filterLogsToArguments<FillEventArgs>(logs, 'Fill'),
@ -188,3 +151,19 @@ function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents {
), ),
}; };
} }
// Executes transferFrom calls to compute relative balances for addresses.
function getBalanceChangesFromTransferFromCalls(
calls: DispatchTransferFromCallArgs[],
): AssetBalances {
const changes: AssetBalances = {};
for (const call of calls) {
const { assetData, from, to, amount } = call;
const balances = changes[assetData] = changes[assetData ] || {};
const fromBalance = balances[from] || constants.ZERO_AMOUNT;
const toBalance = balances[to] || constants.ZERO_AMOUNT;
balances[from] = fromBalance.minus(amount);
balances[to] = toBalance.plus(amount);
}
return changes;
}