@0x/contracts-exchange: Allow fetching of balance of multiple assets

in `TestIsolatedExchange` contract.
`@0x/contracts-exchange`: Refactor `IsolatedExchangeWrapper` to be more
extensible.
This commit is contained in:
Lawrence Forman 2019-07-30 18:58:33 -04:00
parent 1030c96eec
commit 039cc6e28b
3 changed files with 123 additions and 55 deletions

View File

@ -48,19 +48,22 @@ contract TestIsolatedExchange is
{}
/// @dev Convenience function to get the `rawAssetBalances` for
/// multiple addresses.
/// multiple assets and addresses.
function getRawAssetBalances(
bytes calldata assetData,
bytes[] calldata assets,
address[] calldata addresses
)
external
returns (int256[] memory balances)
returns (int256[][] memory balances)
{
balances = new int256[](addresses.length);
mapping(address => int256) storage assetBalances =
rawAssetBalances[keccak256(assetData)];
for (uint i = 0; i < addresses.length; i++) {
balances[i] = assetBalances[addresses[i]];
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]];
}
}
}

View File

@ -4,8 +4,10 @@ import * as _ from 'lodash';
import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper';
blockchainTests.resets.only('Isolated fillOrder() tests', env => {
const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24;
const ERC20_ASSET_DATA_LENGTH = 24;
const DEFAULT_ORDER: Order = {
senderAddress: constants.NULL_ADDRESS,
makerAddress: randomAddress(),
@ -14,13 +16,13 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => {
takerFee: constants.ZERO_AMOUNT,
makerAssetAmount: constants.ZERO_AMOUNT,
takerAssetAmount: constants.ZERO_AMOUNT,
makerAssetData: constants.NULL_BYTES,
takerAssetData: constants.NULL_BYTES,
makerFeeAssetData: constants.NULL_BYTES,
takerFeeAssetData: constants.NULL_BYTES,
salt: constants.ZERO_AMOUNT,
feeRecipientAddress: constants.NULL_ADDRESS,
expirationTimeSeconds: toBN(TOMORROW),
makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
};
let takerAddress: string;
let testExchange: IsolatedExchangeWrapper;
@ -38,14 +40,16 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => {
return _.assign({}, DEFAULT_ORDER, { salt: toBN(nextSaltValue++) }, details);
}
it('works', async () => {
const order = createOrder({
makerAssetAmount: toBN(1),
takerAssetAmount: toBN(2),
for (const i of _.times(100)) {
it('works', async () => {
const order = createOrder({
makerAssetAmount: toBN(1),
takerAssetAmount: toBN(2),
});
const results = await testExchange.fillOrderAsync(order, 2);
// console.log(results, testExchange.getOrderHash(order));
});
const results = await testExchange.fillOrderAsync(order, 2);
console.log(results, testExchange.getOrderHash(order));
});
}
});
function toBN(num: BigNumber | string | number): BigNumber {

View File

@ -3,6 +3,7 @@ import { orderHashUtils } from '@0x/order-utils';
import { OrderWithoutDomain, SignatureType } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TxData, Web3Wrapper } from '@0x/web3-wrapper';
import { LogEntry } from 'ethereum-types';
import * as _ from 'lodash';
import {
@ -26,15 +27,22 @@ export function createBadSignature(type: SignatureType = SignatureType.EIP712):
return `0x00${Buffer.from([type]).toString('hex')}`;
}
export interface IsolatedAssetBalances {
export interface AssetBalances {
[assetData: string]: { [address: string]: BigNumber };
}
export interface IsolatedFillOrderResults {
fillResults: FillResults;
fillEventArgs: FillEventArgs;
export interface IsolatedExchangeEvents {
fillEvents: FillEventArgs[];
transferFromCalls: DispatchTransferFromCallArgs[];
balances: IsolatedAssetBalances;
}
export interface EventsAndBalances {
events: IsolatedExchangeEvents;
balances: AssetBalances;
}
export interface IsolatedFillOrderResults extends EventsAndBalances {
fillResults: FillResults;
}
export type Order = OrderWithoutDomain;
@ -42,10 +50,20 @@ export type Order = OrderWithoutDomain;
export const DEFAULT_GOOD_SIGNATURE = createGoodSignature();
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.
*/
export class IsolatedExchangeWrapper {
public static readonly CHAIN_ID = 1337;
public instance: TestIsolatedExchangeContract;
public logDecoder: LogDecoder;
@ -84,46 +102,89 @@ export class IsolatedExchangeWrapper {
txOpts?: TxData,
): Promise<IsolatedFillOrderResults> {
const _takerAssetFillAmount = new BigNumber(takerAssetFillAmount);
// Call to get the return value.
const fillResults = await this.instance.fillOrder.callAsync(order, _takerAssetFillAmount, signature, txOpts);
// Transact to execute it.
const receipt = await this.logDecoder.getTxWithDecodedLogsAsync(
await this.instance.fillOrder.sendTransactionAsync(order, _takerAssetFillAmount, signature, txOpts),
const results = await this._callAndSendExchangeFunctionAsync<FillResults>(
this.instance.fillOrder,
order,
_takerAssetFillAmount,
signature,
txOpts,
);
// Parse logs.
const fillEventArgs = filterLogsToArguments<FillEventArgs>(receipt.logs, 'Fill')[0];
const transferFromCalls = filterLogsToArguments<DispatchTransferFromCallArgs>(
receipt.logs,
'DispatchTransferFromCalled',
);
// Extract addresses involved in transfers.
const addresses = _.uniq(_.flatten(transferFromCalls.map(c => [c.from, c.to])));
// Extract assets involved in transfers.
const assets = _.uniq(transferFromCalls.map(c => c.assetData));
// Query balances of addresses and assets involved in transfers.
const balances = await (async () => {
const result: IsolatedAssetBalances = {};
for (const assetData of assets) {
result[assetData] = _.zipObject(
addresses,
await this.instance.getRawAssetBalances.callAsync(assetData, addresses),
);
}
return result;
})();
return {
fillResults,
fillEventArgs,
transferFromCalls,
balances,
fillResults: results.result,
events: results.events,
balances: results.balances,
};
}
public getOrderHash(order: Order): string {
const domain = {
verifyingContractAddress: this.instance.address,
chainId: 1337,
chainId: IsolatedExchangeWrapper.CHAIN_ID,
};
return orderHashUtils.getOrderHashHex(_.assign(order, { domain }));
}
public async getAssetBalanceAsync(assetData: string, address: string): Promise<BigNumber[]> {
return this.getAssetBalancesAsync(assetData, [ address ]);
}
public async getAssetBalancesAsync(assetData: string, addresses: string[]): Promise<BigNumber[]> {
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;
}
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>(
instanceMethod: TransactionContractFunction<TResult>,
// tslint:disable-next-line: trailing-comma
...args: any[]
): Promise<CallAndSendResult<TResult>> {
// Call to get the return value.
const result = await instanceMethod.callAsync.call(this.instance, ...args);
// Transact to execute it.
const receipt = await this.logDecoder.getTxWithDecodedLogsAsync(
await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args),
);
const events = extractEvents(receipt.logs);
const balances = await this._getBalancesFromTransferFromCallsAsync(events.transferFromCalls);
return {
result,
events,
balances,
};
}
}
function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents {
return {
fillEvents: filterLogsToArguments<FillEventArgs>(logs, 'Fill'),
transferFromCalls: filterLogsToArguments<DispatchTransferFromCallArgs>(
logs,
'DispatchTransferFromCalled',
),
};
}