diff options
author | Fabio B <kandinsky454@protonmail.ch> | 2019-02-09 22:29:20 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-09 22:29:20 +0800 |
commit | 2a772b71f16179af16d549b847ae8343ee65e913 (patch) | |
tree | 97fa73654ad9b394050b134d91b4b5ed6ed552d1 | |
parent | 629a8d632801c4861824b7d0423792d167b8564d (diff) | |
parent | 233869ed80a1f2836834d20adbc478d2e9b26b54 (diff) | |
download | dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.tar dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.tar.gz dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.tar.bz2 dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.tar.lz dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.tar.xz dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.tar.zst dexon-0x-contracts-2a772b71f16179af16d549b847ae8343ee65e913.zip |
Merge pull request #1569 from 0xProject/feature/utils/decodeArbitrary0xCalldata
0x Transaction Data Decoder
-rw-r--r-- | packages/0x.js/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/0x.js/src/index.ts | 12 | ||||
-rw-r--r-- | packages/contract-wrappers/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/contract-wrappers/src/contract_wrappers.ts | 12 | ||||
-rw-r--r-- | packages/contract-wrappers/src/index.ts | 7 | ||||
-rw-r--r-- | packages/contract-wrappers/src/utils/transaction_encoder.ts | 17 | ||||
-rw-r--r-- | packages/contract-wrappers/test/calldata_decoder_test.ts | 127 | ||||
-rw-r--r-- | packages/order-utils/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/order-utils/src/salt.ts | 10 | ||||
-rw-r--r-- | packages/utils/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/utils/package.json | 2 | ||||
-rw-r--r-- | packages/utils/src/abi_decoder.ts | 124 | ||||
-rw-r--r-- | packages/utils/src/address_utils.ts | 11 | ||||
-rw-r--r-- | packages/utils/src/index.ts | 2 | ||||
-rw-r--r-- | packages/utils/src/random.ts | 16 | ||||
-rw-r--r-- | packages/utils/src/types.ts | 19 | ||||
-rw-r--r-- | packages/utils/test/abi_decoder_test.ts | 50 | ||||
-rw-r--r-- | packages/web3-wrapper/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/web3-wrapper/src/index.ts | 2 |
19 files changed, 425 insertions, 31 deletions
diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index 69381d7a0..e94d87de1 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "5.0.0", + "changes": [ + { + "note": "Export `transactionHashUtils`, `DecodedCalldata`, `ZeroExTransaction`, and `SignedZeroExTransaction`", + "pr": 1569 + } + ] + }, + { "version": "4.0.3", "changes": [ { diff --git a/packages/0x.js/src/index.ts b/packages/0x.js/src/index.ts index 006e4cf29..082b09727 100644 --- a/packages/0x.js/src/index.ts +++ b/packages/0x.js/src/index.ts @@ -1,6 +1,12 @@ 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 { ContractWrappers, @@ -68,7 +74,7 @@ export { MetamaskSubprovider, } from '@0x/subproviders'; -export { AbiDecoder } from '@0x/utils'; +export { AbiDecoder, DecodedCalldata } from '@0x/utils'; export { BigNumber } from '@0x/utils'; @@ -92,6 +98,8 @@ export { OrderRelevantState, Stats, DutchAuctionDetails, + ZeroExTransaction, + SignedZeroExTransaction, } from '@0x/types'; export { diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 73c8e6070..6e0d1ca4b 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "7.1.0", + "changes": [ + { + "note": "Added calldata decoding to ContractWrappers", + "pr": 1569 + } + ] + }, + { "version": "7.0.2", "changes": [ { diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts index 4e594593e..f43dc5d26 100644 --- a/packages/contract-wrappers/src/contract_wrappers.ts +++ b/packages/contract-wrappers/src/contract_wrappers.ts @@ -1,4 +1,5 @@ import { + DutchAuction, ERC20Proxy, ERC20Token, ERC721Proxy, @@ -8,6 +9,7 @@ import { OrderValidator, WETH9, } from '@0x/contract-artifacts'; +import { AbiDecoder } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; @@ -87,6 +89,7 @@ export class ContractWrappers { }; this._web3Wrapper = new Web3Wrapper(provider, txDefaults); const artifactsArray = [ + DutchAuction, ERC20Proxy, ERC20Token, ERC721Proxy, @@ -97,7 +100,7 @@ export class ContractWrappers { WETH9, ]; _.forEach(artifactsArray, artifact => { - this._web3Wrapper.abiDecoder.addABI(artifact.compilerOutput.abi); + this._web3Wrapper.abiDecoder.addABI(artifact.compilerOutput.abi, artifact.contractName); }); const blockPollingIntervalMs = _.isUndefined(config.blockPollingIntervalMs) ? constants.DEFAULT_BLOCK_POLLING_INTERVAL @@ -168,4 +171,11 @@ export class ContractWrappers { public getProvider(): Provider { return this._web3Wrapper.getProvider(); } + /** + * Get the abi decoder instance currently used by contract-wrappers + * @return AbiDecoder instance + */ + public getAbiDecoder(): AbiDecoder { + return this._web3Wrapper.abiDecoder; + } } diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index 69bbe3c91..5fc400edf 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -38,6 +38,8 @@ export { DutchAuctionWrapper } from './contract_wrappers/dutch_auction_wrapper'; export { TransactionEncoder } from './utils/transaction_encoder'; +export { AbiDecoder, DecodedCalldata } from '@0x/utils'; + export { ContractWrappersError, ForwarderWrapperError, @@ -83,6 +85,11 @@ export { JSONRPCResponseError, AbiDefinition, LogWithDecodedArgs, + LogEntry, + DecodedLogEntry, + DecodedLogEntryEvent, + LogEntryEvent, + RawLog, FunctionAbi, EventAbi, EventParameter, diff --git a/packages/contract-wrappers/src/utils/transaction_encoder.ts b/packages/contract-wrappers/src/utils/transaction_encoder.ts index 307487a9b..0832ee73a 100644 --- a/packages/contract-wrappers/src/utils/transaction_encoder.ts +++ b/packages/contract-wrappers/src/utils/transaction_encoder.ts @@ -242,6 +242,23 @@ export class TransactionEncoder { return abiEncodedData; } /** + * Encodes a matchOrders transaction. + * @param leftOrder First order to match. + * @param rightOrder Second order to match. + * @return Hex encoded abi of the function call. + */ + public matchOrdersTx(leftOrder: SignedOrder, rightOrder: SignedOrder): string { + assert.doesConformToSchema('leftOrder', leftOrder, schemas.orderSchema); + assert.doesConformToSchema('rightOrder', rightOrder, schemas.orderSchema); + const abiEncodedData = this._getExchangeContract().matchOrders.getABIEncodedTransactionData( + leftOrder, + rightOrder, + leftOrder.signature, + rightOrder.signature, + ); + return abiEncodedData; + } + /** * Encodes a preSign transaction. * @param hash Hash to pre-sign * @param signerAddress Address that should have signed the given hash. diff --git a/packages/contract-wrappers/test/calldata_decoder_test.ts b/packages/contract-wrappers/test/calldata_decoder_test.ts new file mode 100644 index 000000000..ba1539ef5 --- /dev/null +++ b/packages/contract-wrappers/test/calldata_decoder_test.ts @@ -0,0 +1,127 @@ +import { constants, OrderFactory } from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { SignedOrder } from '@0x/types'; +import { addressUtils, BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { ContractAddresses, ContractWrappers } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { migrateOnceAsync } from './utils/migrate'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ABI Decoding Calldata', () => { + const defaultERC20MakerAssetAddress = addressUtils.generatePseudoRandomAddress(); + const matchOrdersSignature = + 'matchOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),(address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),bytes,bytes)'; + let signedOrderLeft: SignedOrder; + let signedOrderRight: SignedOrder; + let orderLeft = {}; + let orderRight = {}; + let matchOrdersTxData: string; + let contractAddresses: ContractAddresses; + let contractWrappers: ContractWrappers; + + before(async () => { + // Create accounts + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const [makerAddressLeft, makerAddressRight] = accounts; + const [privateKeyLeft, privateKeyRight] = constants.TESTRPC_PRIVATE_KEYS; + const exchangeAddress = addressUtils.generatePseudoRandomAddress(); + const feeRecipientAddress = addressUtils.generatePseudoRandomAddress(); + // Create orders to match. + // Values are arbitrary, with the exception of maker addresses (generated above). + orderLeft = { + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: new BigNumber(10), + takerAddress: '0x0000000000000000000000000000000000000000', + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress, + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + senderAddress: '0x0000000000000000000000000000000000000000', + expirationTimeSeconds: new BigNumber(1549498915), + salt: new BigNumber(217), + }; + orderRight = { + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAddress: '0x0000000000000000000000000000000000000000', + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetAmount: new BigNumber(8), + feeRecipientAddress, + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + senderAddress: '0x0000000000000000000000000000000000000000', + expirationTimeSeconds: new BigNumber(1549498915), + salt: new BigNumber(50010), + }; + const orderFactoryLeft = new OrderFactory(privateKeyLeft, orderLeft); + signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ exchangeAddress }); + const orderFactoryRight = new OrderFactory(privateKeyRight, orderRight); + signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ exchangeAddress }); + // Encode match orders transaction + contractAddresses = await migrateOnceAsync(); + await blockchainLifecycle.startAsync(); + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + contractAddresses, + blockPollingIntervalMs: 10, + }; + contractWrappers = new ContractWrappers(provider, config); + const transactionEncoder = await contractWrappers.exchange.transactionEncoderAsync(); + matchOrdersTxData = transactionEncoder.matchOrdersTx(signedOrderLeft, signedOrderRight); + }); + + describe('decode', () => { + it('should successfully decode DutchAuction.matchOrders calldata', async () => { + const contractName = 'DutchAuction'; + const decodedTxData = contractWrappers + .getAbiDecoder() + .decodeCalldataOrThrow(matchOrdersTxData, contractName); + const expectedFunctionName = 'matchOrders'; + const expectedFunctionArguments = { + buyOrder: orderLeft, + sellOrder: orderRight, + buySignature: signedOrderLeft.signature, + sellSignature: signedOrderRight.signature, + }; + expect(decodedTxData.functionName).to.be.equal(expectedFunctionName); + expect(decodedTxData.functionSignature).to.be.equal(matchOrdersSignature); + expect(decodedTxData.functionArguments).to.be.deep.equal(expectedFunctionArguments); + }); + it('should successfully decode Exchange.matchOrders calldata (and distinguish from DutchAuction.matchOrders)', async () => { + const contractName = 'Exchange'; + const decodedTxData = contractWrappers + .getAbiDecoder() + .decodeCalldataOrThrow(matchOrdersTxData, contractName); + const expectedFunctionName = 'matchOrders'; + const expectedFunctionArguments = { + leftOrder: orderLeft, + rightOrder: orderRight, + leftSignature: signedOrderLeft.signature, + rightSignature: signedOrderRight.signature, + }; + expect(decodedTxData.functionName).to.be.equal(expectedFunctionName); + expect(decodedTxData.functionSignature).to.be.equal(matchOrdersSignature); + expect(decodedTxData.functionArguments).to.be.deep.equal(expectedFunctionArguments); + }); + it('should throw if cannot decode calldata', async () => { + const badTxData = '0x01020304'; + expect(() => { + contractWrappers.getAbiDecoder().decodeCalldataOrThrow(badTxData); + }).to.throw("No functions registered for selector '0x01020304'"); + }); + }); +}); diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 0028ea0c7..8f984538b 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "6.1.0", + "changes": [ + { + "note": "Updated implementation of `generatePseudoRandomSalt` to use generator from @0x/utils", + "pr": 1569 + } + ] + }, + { "version": "6.0.1", "changes": [ { diff --git a/packages/order-utils/src/salt.ts b/packages/order-utils/src/salt.ts index 95df66c99..a7cc4aea0 100644 --- a/packages/order-utils/src/salt.ts +++ b/packages/order-utils/src/salt.ts @@ -1,6 +1,4 @@ -import { BigNumber } from '@0x/utils'; - -const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; +import { BigNumber, generatePseudoRandom256BitNumber } from '@0x/utils'; /** * Generates a pseudo-random 256-bit salt. @@ -9,10 +7,6 @@ const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; * @return A pseudo-random 256-bit number that can be used as a salt. */ export function 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).integerValue(); + const salt = generatePseudoRandom256BitNumber(); return salt; } diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 9ce2a4c52..95f61a43c 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "4.1.0", + "changes": [ + { + "note": "Added method decoding to AbiDecoder", + "pr": 1569 + } + ] + }, + { "version": "4.0.4", "changes": [ { diff --git a/packages/utils/package.json b/packages/utils/package.json index 86fecbc7c..895560961 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -14,7 +14,7 @@ "lint": "tslint --format stylish --project .", "test": "yarn run_mocha", "test:circleci": "yarn test:coverage", - "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*_test.js' 'lib/test/*_test.js' --bail --exit", "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info" }, diff --git a/packages/utils/src/abi_decoder.ts b/packages/utils/src/abi_decoder.ts index 28b6418d8..b764e45b8 100644 --- a/packages/utils/src/abi_decoder.ts +++ b/packages/utils/src/abi_decoder.ts @@ -6,28 +6,49 @@ import { EventParameter, LogEntry, LogWithDecodedArgs, + MethodAbi, RawLog, SolidityTypes, } from 'ethereum-types'; import * as ethers from 'ethers'; import * as _ from 'lodash'; +import { AbiEncoder } from '.'; import { addressUtils } from './address_utils'; import { BigNumber } from './configured_bignumber'; +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 * signature from the ABI and attempts to decode the logs using it. */ export class AbiDecoder { - private readonly _methodIds: { [signatureHash: string]: { [numIndexedArgs: number]: EventAbi } } = {}; + private readonly _eventIds: { [signatureHash: string]: { [numIndexedArgs: number]: EventAbi } } = {}; + private readonly _selectorToFunctionInfo: SelectorToFunctionInfo = {}; + /** + * Retrieves the function selector from calldata. + * @param calldata hex-encoded calldata. + * @return hex-encoded function selector. + */ + private static _getFunctionSelector(calldata: string): string { + const functionSelectorLength = 10; + if (!calldata.startsWith('0x') || calldata.length < functionSelectorLength) { + throw new Error( + `Malformed calldata. Must include a hex prefix '0x' and 4-byte function selector. Got '${calldata}'`, + ); + } + const functionSelector = calldata.substr(0, functionSelectorLength); + return functionSelector; + } /** * Instantiate an AbiDecoder * @param abiArrays An array of contract ABI's * @return AbiDecoder instance */ constructor(abiArrays: AbiDefinition[][]) { - _.forEach(abiArrays, this.addABI.bind(this)); + _.each(abiArrays, abi => { + this.addABI(abi); + }); } /** * Attempt to decode a log given the ABI's the AbiDecoder knows about. @@ -35,12 +56,12 @@ export class AbiDecoder { * @return The decoded log if the requisite ABI was available. Otherwise the log unaltered. */ 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; - if (_.isUndefined(this._methodIds[methodId]) || _.isUndefined(this._methodIds[methodId][numIndexedArgs])) { + if (_.isUndefined(this._eventIds[eventId]) || _.isUndefined(this._eventIds[eventId][numIndexedArgs])) { return log; } - const event = this._methodIds[methodId][numIndexedArgs]; + const event = this._eventIds[eventId][numIndexedArgs]; const ethersInterface = new ethers.utils.Interface([event]); const decodedParams: DecodedLogArgs = {}; let topicsIndex = 1; @@ -89,25 +110,94 @@ export class AbiDecoder { } } /** - * Add additional ABI definitions to the AbiDecoder - * @param abiArray An array of ABI definitions to add to the AbiDecoder + * Decodes calldata for a known ABI. + * @param calldata hex-encoded calldata. + * @param contractName used to disambiguate similar ABI's (optional). + * @return Decoded calldata. Includes: function name and signature, along with the decoded arguments. */ - public addABI(abiArray: AbiDefinition[]): void { + public decodeCalldataOrThrow(calldata: string, contractName?: string): DecodedCalldata { + const functionSelector = AbiDecoder._getFunctionSelector(calldata); + const candidateFunctionInfos = this._selectorToFunctionInfo[functionSelector]; + if (_.isUndefined(candidateFunctionInfos)) { + throw new Error(`No functions registered for selector '${functionSelector}'`); + } + const functionInfo = _.find(candidateFunctionInfos, candidateFunctionInfo => { + return ( + _.isUndefined(contractName) || _.toLower(contractName) === _.toLower(candidateFunctionInfo.contractName) + ); + }); + if (_.isUndefined(functionInfo)) { + throw new Error( + `No function registered with selector ${functionSelector} and contract name ${contractName}.`, + ); + } else if (_.isUndefined(functionInfo.abiEncoder)) { + throw new Error( + `Function ABI Encoder is not defined, for function registered with selector ${functionSelector} and contract name ${contractName}.`, + ); + } + const functionName = functionInfo.abiEncoder.getDataItem().name; + const functionSignature = functionInfo.abiEncoder.getSignatureType(); + const functionArguments = functionInfo.abiEncoder.decode(calldata); + const decodedCalldata = { + functionName, + functionSignature, + functionArguments, + }; + return decodedCalldata; + } + /** + * 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 + * have the same signature but different parameter names, then their ABI definitions can be disambiguated + * by specifying a contract name. + * @param abiDefinitions ABI definitions for a given contract. + * @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 { if (_.isUndefined(abiArray)) { return; } const ethersInterface = new ethers.utils.Interface(abiArray); _.map(abiArray, (abi: AbiDefinition) => { - if (abi.type === AbiType.Event) { - // tslint:disable-next-line:no-unnecessary-type-assertion - const eventAbi = abi as EventAbi; - const topic = ethersInterface.events[eventAbi.name].topic; - const numIndexedArgs = _.reduce(eventAbi.inputs, (sum, input) => (input.indexed ? sum + 1 : sum), 0); - this._methodIds[topic] = { - ...this._methodIds[topic], - [numIndexedArgs]: eventAbi, - }; + switch (abi.type) { + case AbiType.Event: + // tslint:disable-next-line:no-unnecessary-type-assertion + this._addEventABI(abi as EventAbi, ethersInterface); + break; + + case AbiType.Function: + // tslint:disable-next-line:no-unnecessary-type-assertion + this._addMethodABI(abi as MethodAbi, contractName); + break; + + default: + // ignore other types + break; } }); } + private _addEventABI(eventAbi: EventAbi, ethersInterface: ethers.utils.Interface): void { + const topic = ethersInterface.events[eventAbi.name].topic; + const numIndexedArgs = _.reduce(eventAbi.inputs, (sum, input) => (input.indexed ? sum + 1 : sum), 0); + this._eventIds[topic] = { + ...this._eventIds[topic], + [numIndexedArgs]: eventAbi, + }; + } + private _addMethodABI(methodAbi: MethodAbi, contractName?: string): void { + const abiEncoder = new AbiEncoder.Method(methodAbi); + const functionSelector = abiEncoder.getSelector(); + if (!(functionSelector in this._selectorToFunctionInfo)) { + this._selectorToFunctionInfo[functionSelector] = []; + } + // Recored a copy of this ABI for each deployment + const functionSignature = abiEncoder.getSignature(); + this._selectorToFunctionInfo[functionSelector].push({ + functionSignature, + abiEncoder, + contractName, + }); + } } diff --git a/packages/utils/src/address_utils.ts b/packages/utils/src/address_utils.ts index 1fc960408..361e35cd8 100644 --- a/packages/utils/src/address_utils.ts +++ b/packages/utils/src/address_utils.ts @@ -1,7 +1,9 @@ -import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; +import { addHexPrefix, sha3, stripHexPrefix } from 'ethereumjs-util'; import * as jsSHA3 from 'js-sha3'; import * as _ from 'lodash'; +import { generatePseudoRandom256BitNumber } from './random'; + 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 ADDRESS_LENGTH = 40; @@ -43,4 +45,11 @@ export const addressUtils = { padZeros(address: string): string { return addHexPrefix(_.padStart(stripHexPrefix(address), ADDRESS_LENGTH, '0')); }, + generatePseudoRandomAddress(): string { + const randomBigNum = generatePseudoRandom256BitNumber(); + const randomBuff = sha3(randomBigNum.toString()); + const addressLengthInBytes = 20; + const randomAddress = `0x${randomBuff.slice(0, addressLengthInBytes).toString('hex')}`; + return randomAddress; + }, }; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 082aff6bb..f9c2693fe 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -11,3 +11,5 @@ export { errorUtils } from './error_utils'; export { fetchAsync } from './fetch_async'; export { signTypedDataUtils } from './sign_typed_data_utils'; export import AbiEncoder = require('./abi_encoder'); +export * from './types'; +export { generatePseudoRandom256BitNumber } from './random'; diff --git a/packages/utils/src/random.ts b/packages/utils/src/random.ts new file mode 100644 index 000000000..69243bab8 --- /dev/null +++ b/packages/utils/src/random.ts @@ -0,0 +1,16 @@ +import { BigNumber } from './configured_bignumber'; + +const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; + +/** + * Generates a pseudo-random 256-bit number. + * @return A pseudo-random 256-bit number. + */ +export function generatePseudoRandom256BitNumber(): 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 randomNumberScaledTo256Bits = randomNumber.times(factor).integerValue(); + return randomNumberScaledTo256Bits; +} diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts new file mode 100644 index 000000000..32e11efa2 --- /dev/null +++ b/packages/utils/src/types.ts @@ -0,0 +1,19 @@ +import { AbiEncoder } from '.'; + +export interface FunctionInfo { + functionSignature: string; + contractName?: string; + contractAddress?: string; + networkId?: number; + abiEncoder?: AbiEncoder.Method; +} + +export interface SelectorToFunctionInfo { + [index: string]: FunctionInfo[]; +} + +export interface DecodedCalldata { + functionName: string; + functionSignature: string; + functionArguments: any; +} diff --git a/packages/utils/test/abi_decoder_test.ts b/packages/utils/test/abi_decoder_test.ts new file mode 100644 index 000000000..81fed1060 --- /dev/null +++ b/packages/utils/test/abi_decoder_test.ts @@ -0,0 +1,50 @@ +import * as chai from 'chai'; +import { MethodAbi } from 'ethereum-types'; +import 'mocha'; + +import { AbiDecoder, AbiEncoder } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('AbiDecoder', () => { + it('should successfully add a new ABI and decode calldata for it', async () => { + // Add new ABI + const abi: MethodAbi = { + name: 'foobar', + type: 'function', + inputs: [ + { + name: 'testAddress', + type: 'address', + }, + ], + outputs: [ + { + name: 'butter', + type: 'string', + }, + ], + constant: false, + payable: false, + stateMutability: 'pure', + }; + const contractName = 'newContract'; + const testAddress = '0x0001020304050607080900010203040506070809'; + const abiDecoder = new AbiDecoder([]); + abiDecoder.addABI([abi], contractName); + // Create some tx data + const foobarEncoder = new AbiEncoder.Method(abi); + const foobarSignature = foobarEncoder.getSignature(); + const foobarTxData = foobarEncoder.encode([testAddress]); + // Decode tx data using contract name + const decodedTxData = abiDecoder.decodeCalldataOrThrow(foobarTxData, contractName); + const expectedFunctionName = abi.name; + const expectedFunctionArguments = { testAddress }; + expect(decodedTxData.functionName).to.be.equal(expectedFunctionName); + expect(decodedTxData.functionSignature).to.be.equal(foobarSignature); + expect(decodedTxData.functionArguments).to.be.deep.equal(expectedFunctionArguments); + }); +}); diff --git a/packages/web3-wrapper/CHANGELOG.json b/packages/web3-wrapper/CHANGELOG.json index 49dbe5a64..bf1dedc00 100644 --- a/packages/web3-wrapper/CHANGELOG.json +++ b/packages/web3-wrapper/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "5.0.0", + "changes": [ + { + "note": "Export `DecodedCalldata` from @0x/utils", + "pr": 1569 + } + ] + }, + { "version": "4.0.2", "changes": [ { diff --git a/packages/web3-wrapper/src/index.ts b/packages/web3-wrapper/src/index.ts index 4d20ba9be..a63408455 100644 --- a/packages/web3-wrapper/src/index.ts +++ b/packages/web3-wrapper/src/index.ts @@ -1,7 +1,7 @@ export { Web3Wrapper } from './web3_wrapper'; export { marshaller } from './marshaller'; -export { AbiDecoder } from '@0x/utils'; +export { AbiDecoder, DecodedCalldata } from '@0x/utils'; export { BlockParam, |