Improvements and conventions in utils package + abi decoder

This commit is contained in:
Greg Hysen 2019-02-08 14:46:39 -08:00
parent 6d0dc47157
commit 500b4940a3
10 changed files with 57 additions and 69 deletions

View File

@ -1,6 +1,12 @@
export { ContractAddresses } from '@0x/contract-addresses'; export { ContractAddresses } from '@0x/contract-addresses';
export { assetDataUtils, signatureUtils, generatePseudoRandomSalt, orderHashUtils } from '@0x/order-utils'; export {
assetDataUtils,
signatureUtils,
generatePseudoRandomSalt,
orderHashUtils,
transactionHashUtils,
} from '@0x/order-utils';
export { export {
ContractWrappers, ContractWrappers,
@ -68,7 +74,7 @@ export {
MetamaskSubprovider, MetamaskSubprovider,
} from '@0x/subproviders'; } from '@0x/subproviders';
export { AbiDecoder, TransactionData } from '@0x/utils'; export { AbiDecoder, DecodedCalldata } from '@0x/utils';
export { BigNumber } from '@0x/utils'; export { BigNumber } from '@0x/utils';
@ -92,6 +98,8 @@ export {
OrderRelevantState, OrderRelevantState,
Stats, Stats,
DutchAuctionDetails, DutchAuctionDetails,
ZeroExTransaction,
SignedZeroExTransaction,
} from '@0x/types'; } from '@0x/types';
export { export {

View File

@ -172,8 +172,8 @@ export class ContractWrappers {
return this._web3Wrapper.getProvider(); return this._web3Wrapper.getProvider();
} }
/** /**
* Get the provider instance currently used by contract-wrappers * Get the abi decoder instance currently used by contract-wrappers
* @return Web3 provider instance * @return AbiDecoder instance
*/ */
public getAbiDecoder(): AbiDecoder { public getAbiDecoder(): AbiDecoder {
return this._web3Wrapper.abiDecoder; return this._web3Wrapper.abiDecoder;

View File

@ -38,7 +38,7 @@ export { DutchAuctionWrapper } from './contract_wrappers/dutch_auction_wrapper';
export { TransactionEncoder } from './utils/transaction_encoder'; export { TransactionEncoder } from './utils/transaction_encoder';
export { AbiDecoder, TransactionData } from '@0x/utils'; export { AbiDecoder, DecodedCalldata } from '@0x/utils';
export { export {
ContractWrappersError, ContractWrappersError,

View File

@ -248,8 +248,8 @@ export class TransactionEncoder {
* @return Hex encoded abi of the function call. * @return Hex encoded abi of the function call.
*/ */
public matchOrdersTx(leftOrder: SignedOrder, rightOrder: SignedOrder): string { public matchOrdersTx(leftOrder: SignedOrder, rightOrder: SignedOrder): string {
assert.doesConformToSchema('order', leftOrder, schemas.orderSchema); assert.doesConformToSchema('leftOrder', leftOrder, schemas.orderSchema);
assert.doesConformToSchema('order', rightOrder, schemas.orderSchema); assert.doesConformToSchema('rightOrder', rightOrder, schemas.orderSchema);
const abiEncodedData = this._getExchangeContract().matchOrders.getABIEncodedTransactionData( const abiEncodedData = this._getExchangeContract().matchOrders.getABIEncodedTransactionData(
leftOrder, leftOrder,
rightOrder, rightOrder,

View File

@ -33,12 +33,12 @@ describe('ABI Decoding Calldata', () => {
before(async () => { before(async () => {
// Create accounts // Create accounts
const accounts = await web3Wrapper.getAvailableAddressesAsync(); const accounts = await web3Wrapper.getAvailableAddressesAsync();
const [makerAddressLeft, makerAddressRight] = accounts.slice(0, 2); const [makerAddressLeft, makerAddressRight] = accounts;
const [privateKeyLeft, privateKeyRight] = constants.TESTRPC_PRIVATE_KEYS;
const exchangeAddress = addressUtils.generatePseudoRandomAddress(); const exchangeAddress = addressUtils.generatePseudoRandomAddress();
const feeRecipientAddress = addressUtils.generatePseudoRandomAddress(); const feeRecipientAddress = addressUtils.generatePseudoRandomAddress();
const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; // Create orders to match.
const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; // Values are arbitrary, with the exception of maker addresses (generated above).
// Create orders to match
orderLeft = { orderLeft = {
makerAddress: makerAddressLeft, makerAddress: makerAddressLeft,
makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
@ -87,7 +87,9 @@ describe('ABI Decoding Calldata', () => {
describe('decode', () => { describe('decode', () => {
it('should successfully decode DutchAuction.matchOrders calldata', async () => { it('should successfully decode DutchAuction.matchOrders calldata', async () => {
const contractName = 'DutchAuction'; const contractName = 'DutchAuction';
const decodedTxData = contractWrappers.getAbiDecoder().tryDecodeCalldata(matchOrdersTxData, contractName); const decodedTxData = contractWrappers
.getAbiDecoder()
.decodeCalldataOrThrow(matchOrdersTxData, contractName);
const expectedFunctionName = 'matchOrders'; const expectedFunctionName = 'matchOrders';
const expectedFunctionArguments = { const expectedFunctionArguments = {
buyOrder: orderLeft, buyOrder: orderLeft,
@ -101,7 +103,9 @@ describe('ABI Decoding Calldata', () => {
}); });
it('should successfully decode Exchange.matchOrders calldata (and distinguish from DutchAuction.matchOrders)', async () => { it('should successfully decode Exchange.matchOrders calldata (and distinguish from DutchAuction.matchOrders)', async () => {
const contractName = 'Exchange'; const contractName = 'Exchange';
const decodedTxData = contractWrappers.getAbiDecoder().tryDecodeCalldata(matchOrdersTxData, contractName); const decodedTxData = contractWrappers
.getAbiDecoder()
.decodeCalldataOrThrow(matchOrdersTxData, contractName);
const expectedFunctionName = 'matchOrders'; const expectedFunctionName = 'matchOrders';
const expectedFunctionArguments = { const expectedFunctionArguments = {
leftOrder: orderLeft, leftOrder: orderLeft,
@ -116,7 +120,7 @@ describe('ABI Decoding Calldata', () => {
it('should throw if cannot decode calldata', async () => { it('should throw if cannot decode calldata', async () => {
const badTxData = '0x01020304'; const badTxData = '0x01020304';
expect(() => { expect(() => {
contractWrappers.getAbiDecoder().tryDecodeCalldata(badTxData); contractWrappers.getAbiDecoder().decodeCalldataOrThrow(badTxData);
}).to.throw("No functions registered for selector '0x01020304'"); }).to.throw("No functions registered for selector '0x01020304'");
}); });
}); });

View File

@ -16,7 +16,7 @@ import * as _ from 'lodash';
import { AbiEncoder } from '.'; import { AbiEncoder } from '.';
import { addressUtils } from './address_utils'; import { addressUtils } from './address_utils';
import { BigNumber } from './configured_bignumber'; import { BigNumber } from './configured_bignumber';
import { FunctionInfoBySelector, TransactionData } from './types'; import { DecodedCalldata, SelectorToFunctionInfo } from './types';
/** /**
* AbiDecoder allows you to decode event logs given a set of supplied contract ABI's. It takes the contract's event * AbiDecoder allows you to decode event logs given a set of supplied contract ABI's. It takes the contract's event
@ -24,17 +24,17 @@ import { FunctionInfoBySelector, TransactionData } from './types';
*/ */
export class AbiDecoder { export class AbiDecoder {
private readonly _eventIds: { [signatureHash: string]: { [numIndexedArgs: number]: EventAbi } } = {}; private readonly _eventIds: { [signatureHash: string]: { [numIndexedArgs: number]: EventAbi } } = {};
private readonly _functionInfoBySelector: FunctionInfoBySelector = {}; private readonly _selectorToFunctionInfo: SelectorToFunctionInfo = {};
/** /**
* Retrieves the function selector from tranasction data. * Retrieves the function selector from calldata.
* @param calldata hex-encoded transaction data. * @param calldata hex-encoded calldata.
* @return hex-encoded function selector. * @return hex-encoded function selector.
*/ */
private static _getFunctionSelector(calldata: string): string { private static _getFunctionSelector(calldata: string): string {
const functionSelectorLength = 10; const functionSelectorLength = 10;
if (!calldata.startsWith('0x') || calldata.length < functionSelectorLength) { if (!calldata.startsWith('0x') || calldata.length < functionSelectorLength) {
throw new Error( throw new Error(
`Malformed transaction data. Must include a hex prefix '0x' and 4-byte function selector. Got '${calldata}'`, `Malformed calldata. Must include a hex prefix '0x' and 4-byte function selector. Got '${calldata}'`,
); );
} }
const functionSelector = calldata.substr(0, functionSelectorLength); const functionSelector = calldata.substr(0, functionSelectorLength);
@ -56,12 +56,12 @@ export class AbiDecoder {
* @return The decoded log if the requisite ABI was available. Otherwise the log unaltered. * @return The decoded log if the requisite ABI was available. Otherwise the log unaltered.
*/ */
public tryToDecodeLogOrNoop<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog { public tryToDecodeLogOrNoop<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog {
const methodId = log.topics[0]; const eventId = log.topics[0];
const numIndexedArgs = log.topics.length - 1; const numIndexedArgs = log.topics.length - 1;
if (_.isUndefined(this._eventIds[methodId]) || _.isUndefined(this._eventIds[methodId][numIndexedArgs])) { if (_.isUndefined(this._eventIds[eventId]) || _.isUndefined(this._eventIds[eventId][numIndexedArgs])) {
return log; return log;
} }
const event = this._eventIds[methodId][numIndexedArgs]; const event = this._eventIds[eventId][numIndexedArgs];
const ethersInterface = new ethers.utils.Interface([event]); const ethersInterface = new ethers.utils.Interface([event]);
const decodedParams: DecodedLogArgs = {}; const decodedParams: DecodedLogArgs = {};
let topicsIndex = 1; let topicsIndex = 1;
@ -110,19 +110,21 @@ export class AbiDecoder {
} }
} }
/** /**
* Decodes transaction data for a known ABI. * Decodes calldata for a known ABI.
* @param calldata hex-encoded transaction data. * @param calldata hex-encoded calldata.
* @param contractName used to disambiguate similar ABI's (optional). * @param contractName used to disambiguate similar ABI's (optional).
* @return Decoded transaction data. Includes: function name and signature, along with the decoded arguments. * @return Decoded calldata. Includes: function name and signature, along with the decoded arguments.
*/ */
public tryDecodeCalldata(calldata: string, contractName?: string): TransactionData { public decodeCalldataOrThrow(calldata: string, contractName?: string): DecodedCalldata {
const functionSelector = AbiDecoder._getFunctionSelector(calldata); const functionSelector = AbiDecoder._getFunctionSelector(calldata);
const candidateFunctionInfos = this._functionInfoBySelector[functionSelector]; const candidateFunctionInfos = this._selectorToFunctionInfo[functionSelector];
if (_.isUndefined(candidateFunctionInfos)) { if (_.isUndefined(candidateFunctionInfos)) {
throw new Error(`No functions registered for selector '${functionSelector}'`); throw new Error(`No functions registered for selector '${functionSelector}'`);
} }
const functionInfo = _.find(candidateFunctionInfos, txDecoder => { const functionInfo = _.find(candidateFunctionInfos, candidateFunctionInfo => {
return _.isUndefined(contractName) || _.toLower(txDecoder.contractName) === _.toLower(contractName); return (
_.isUndefined(contractName) || _.toLower(contractName) === _.toLower(candidateFunctionInfo.contractName)
);
}); });
if (_.isUndefined(functionInfo)) { if (_.isUndefined(functionInfo)) {
throw new Error( throw new Error(
@ -144,12 +146,14 @@ export class AbiDecoder {
return decodedCalldata; return decodedCalldata;
} }
/** /**
* Adds a set of ABI definitions, after which transaction data targeting these ABI's can be decoded. * Adds a set of ABI definitions, after which calldata and logs targeting these ABI's can be decoded.
* Additional properties can be included to disambiguate similar ABI's. For example, if two functions * Additional properties can be included to disambiguate similar ABI's. For example, if two functions
* have the same signature but different parameter names, then their ABI definitions can be disambiguated * have the same signature but different parameter names, then their ABI definitions can be disambiguated
* by specifying a contract name. * by specifying a contract name.
* @param abiDefinitions ABI definitions for a given contract. * @param abiDefinitions ABI definitions for a given contract.
* @param contractName Name of contract that encapsulates the ABI definitions (optional). * @param contractName Name of contract that encapsulates the ABI definitions (optional).
* This can be used when decoding calldata to disambiguate methods with
* the same signature but different parameter names.
*/ */
public addABI(abiArray: AbiDefinition[], contractName?: string): void { public addABI(abiArray: AbiDefinition[], contractName?: string): void {
if (_.isUndefined(abiArray)) { if (_.isUndefined(abiArray)) {
@ -159,15 +163,13 @@ export class AbiDecoder {
_.map(abiArray, (abi: AbiDefinition) => { _.map(abiArray, (abi: AbiDefinition) => {
switch (abi.type) { switch (abi.type) {
case AbiType.Event: case AbiType.Event:
// tslint:disable no-unnecessary-type-assertion // tslint:disable-next-line:no-unnecessary-type-assertion
this._addEventABI(abi as EventAbi, ethersInterface); this._addEventABI(abi as EventAbi, ethersInterface);
// tslint:enable no-unnecessary-type-assertion
break; break;
case AbiType.Function: case AbiType.Function:
// tslint:disable no-unnecessary-type-assertion // tslint:disable-next-line:no-unnecessary-type-assertion
this._addMethodABI(abi as MethodAbi, contractName); this._addMethodABI(abi as MethodAbi, contractName);
// tslint:enable no-unnecessary-type-assertion
break; break;
default: default:
@ -176,9 +178,7 @@ export class AbiDecoder {
} }
}); });
} }
private _addEventABI(abi: EventAbi, ethersInterface: ethers.utils.Interface): void { private _addEventABI(eventAbi: EventAbi, ethersInterface: ethers.utils.Interface): void {
// tslint:disable-next-line:no-unnecessary-type-assertion
const eventAbi = abi as EventAbi;
const topic = ethersInterface.events[eventAbi.name].topic; const topic = ethersInterface.events[eventAbi.name].topic;
const numIndexedArgs = _.reduce(eventAbi.inputs, (sum, input) => (input.indexed ? sum + 1 : sum), 0); const numIndexedArgs = _.reduce(eventAbi.inputs, (sum, input) => (input.indexed ? sum + 1 : sum), 0);
this._eventIds[topic] = { this._eventIds[topic] = {
@ -189,12 +189,12 @@ export class AbiDecoder {
private _addMethodABI(methodAbi: MethodAbi, contractName?: string): void { private _addMethodABI(methodAbi: MethodAbi, contractName?: string): void {
const abiEncoder = new AbiEncoder.Method(methodAbi); const abiEncoder = new AbiEncoder.Method(methodAbi);
const functionSelector = abiEncoder.getSelector(); const functionSelector = abiEncoder.getSelector();
if (!(functionSelector in this._functionInfoBySelector)) { if (!(functionSelector in this._selectorToFunctionInfo)) {
this._functionInfoBySelector[functionSelector] = []; this._selectorToFunctionInfo[functionSelector] = [];
} }
// Recored a copy of this ABI for each deployment // Recored a copy of this ABI for each deployment
const functionSignature = abiEncoder.getSignature(); const functionSignature = abiEncoder.getSignature();
this._functionInfoBySelector[functionSelector].push({ this._selectorToFunctionInfo[functionSelector].push({
functionSignature, functionSignature,
abiEncoder, abiEncoder,
contractName, contractName,

View File

@ -1,13 +1,10 @@
import { addHexPrefix, sha3, stripHexPrefix } from 'ethereumjs-util'; import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
import * as jsSHA3 from 'js-sha3'; import * as jsSHA3 from 'js-sha3';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { BigNumber } from './configured_bignumber';
const BASIC_ADDRESS_REGEX = /^(0x)?[0-9a-f]{40}$/i; const BASIC_ADDRESS_REGEX = /^(0x)?[0-9a-f]{40}$/i;
const SAME_CASE_ADDRESS_REGEX = /^(0x)?([0-9a-f]{40}|[0-9A-F]{40})$/; const SAME_CASE_ADDRESS_REGEX = /^(0x)?([0-9a-f]{40}|[0-9A-F]{40})$/;
const ADDRESS_LENGTH = 40; const ADDRESS_LENGTH = 40;
const MAX_DIGITS_IN_UNSIGNED_256_INT = 78;
export const addressUtils = { export const addressUtils = {
isChecksumAddress(address: string): boolean { isChecksumAddress(address: string): boolean {
@ -46,25 +43,4 @@ export const addressUtils = {
padZeros(address: string): string { padZeros(address: string): string {
return addHexPrefix(_.padStart(stripHexPrefix(address), ADDRESS_LENGTH, '0')); return addHexPrefix(_.padStart(stripHexPrefix(address), ADDRESS_LENGTH, '0'));
}, },
/**
* Generates a pseudo-random 256-bit salt.
* The salt can be included in a 0x order, ensuring that the order generates a unique orderHash
* and will not collide with other outstanding orders that are identical in all other parameters.
* @return A pseudo-random 256-bit number that can be used as a salt.
*/
generatePseudoRandomSalt(): BigNumber {
// BigNumber.random returns a pseudo-random number between 0 & 1 with a passed in number of decimal places.
// Source: https://mikemcl.github.io/bignumber.js/#random
const randomNumber = BigNumber.random(MAX_DIGITS_IN_UNSIGNED_256_INT);
const factor = new BigNumber(10).pow(MAX_DIGITS_IN_UNSIGNED_256_INT - 1);
const salt = randomNumber.times(factor);
return salt;
},
generatePseudoRandomAddress(): string {
const randomBigNum = addressUtils.generatePseudoRandomSalt();
const randomBuff = sha3(randomBigNum.toString());
const addressLengthInBytes = 20;
const randomAddress = `0x${randomBuff.slice(0, addressLengthInBytes).toString('hex')}`;
return randomAddress;
},
}; };

View File

@ -8,11 +8,11 @@ export interface FunctionInfo {
abiEncoder?: AbiEncoder.Method; abiEncoder?: AbiEncoder.Method;
} }
export interface FunctionInfoBySelector { export interface SelectorToFunctionInfo {
[index: string]: FunctionInfo[]; [index: string]: FunctionInfo[];
} }
export interface TransactionData { export interface DecodedCalldata {
functionName: string; functionName: string;
functionSignature: string; functionSignature: string;
functionArguments: any; functionArguments: any;

View File

@ -40,7 +40,7 @@ describe('AbiDecoder', () => {
const foobarSignature = foobarEncoder.getSignature(); const foobarSignature = foobarEncoder.getSignature();
const foobarTxData = foobarEncoder.encode([testAddress]); const foobarTxData = foobarEncoder.encode([testAddress]);
// Decode tx data using contract name // Decode tx data using contract name
const decodedTxData = abiDecoder.tryDecodeCalldata(foobarTxData, contractName); const decodedTxData = abiDecoder.decodeCalldataOrThrow(foobarTxData, contractName);
const expectedFunctionName = abi.name; const expectedFunctionName = abi.name;
const expectedFunctionArguments = { testAddress }; const expectedFunctionArguments = { testAddress };
expect(decodedTxData.functionName).to.be.equal(expectedFunctionName); expect(decodedTxData.functionName).to.be.equal(expectedFunctionName);

View File

@ -1,7 +1,7 @@
export { Web3Wrapper } from './web3_wrapper'; export { Web3Wrapper } from './web3_wrapper';
export { marshaller } from './marshaller'; export { marshaller } from './marshaller';
export { AbiDecoder, TransactionData } from '@0x/utils'; export { AbiDecoder, DecodedCalldata } from '@0x/utils';
export { export {
BlockParam, BlockParam,