Merged tx decoder into AbiDecoder in utils and merged zeroex tx decoder into ContractWrappers.
This commit is contained in:
@@ -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 provider instance currently used by contract-wrappers
|
||||
* @return Web3 provider instance
|
||||
*/
|
||||
public getAbiDecoder(): AbiDecoder {
|
||||
return this._web3Wrapper.abiDecoder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export { OrderValidatorWrapper } from './contract_wrappers/order_validator_wrapp
|
||||
export { DutchAuctionWrapper } from './contract_wrappers/dutch_auction_wrapper';
|
||||
|
||||
export { TransactionEncoder } from './utils/transaction_encoder';
|
||||
export { ZeroExTransactionDecoder } from './utils/zeroex_transaction_decoder';
|
||||
|
||||
export {
|
||||
ContractWrappersError,
|
||||
@@ -72,12 +71,6 @@ export {
|
||||
AssetProxyId,
|
||||
} from '@0x/types';
|
||||
|
||||
export {
|
||||
DeployedContractInfo,
|
||||
TransactionData,
|
||||
TransactionProperties
|
||||
} from '@0x/utils';
|
||||
|
||||
export {
|
||||
BlockParamLiteral,
|
||||
BlockParam,
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { getContractAddressesForNetworkOrThrow, NetworkId } from '@0x/contract-addresses';
|
||||
import * as ContractArtifacts from '@0x/contract-artifacts';
|
||||
import { SimpleContractArtifact } from '@0x/types';
|
||||
import { AbiDefinition, ContractAbi } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import {
|
||||
DeployedContractInfo,
|
||||
DeployedContractInfoByName,
|
||||
TransactionData,
|
||||
TransactionDecoder,
|
||||
TransactionProperties,
|
||||
} from '@0x/utils';
|
||||
|
||||
export class ZeroExTransactionDecoder extends TransactionDecoder {
|
||||
private static _instance: ZeroExTransactionDecoder;
|
||||
/**
|
||||
* Adds a set of ABI definitions, after which transaction data 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).
|
||||
* @param deploymentInfos A collection of network/address pairs where this contract is deployed (optional).
|
||||
*/
|
||||
public static addABI(
|
||||
abiDefinitions: AbiDefinition[],
|
||||
contractName: string,
|
||||
deploymentInfos?: DeployedContractInfo[],
|
||||
): void {
|
||||
const instance = ZeroExTransactionDecoder._getInstance();
|
||||
instance.addABI(abiDefinitions, contractName, deploymentInfos);
|
||||
}
|
||||
/**
|
||||
* Decodes transaction data for a known ABI.
|
||||
* @param txData hex-encoded transaction data.
|
||||
* @param txProperties Properties about the transaction used to disambiguate similar ABI's (optional).
|
||||
* @return Decoded transaction data. Includes: function name and signature, along with the decoded arguments.
|
||||
*/
|
||||
public static decode(calldata: string, txProperties?: TransactionProperties): TransactionData {
|
||||
const instance = ZeroExTransactionDecoder._getInstance();
|
||||
const decodedCalldata = instance.decode(calldata, txProperties);
|
||||
return decodedCalldata;
|
||||
}
|
||||
/**
|
||||
* Gets instance for singleton.
|
||||
* @return singleton instance.
|
||||
*/
|
||||
private static _getInstance(): ZeroExTransactionDecoder {
|
||||
if (!ZeroExTransactionDecoder._instance) {
|
||||
ZeroExTransactionDecoder._instance = new ZeroExTransactionDecoder();
|
||||
}
|
||||
return ZeroExTransactionDecoder._instance;
|
||||
}
|
||||
/**
|
||||
* Adds all known contract ABI's defined by the @0x/Artifacts package, along with known 0x
|
||||
* contract addresses.
|
||||
*/
|
||||
private constructor() {
|
||||
super();
|
||||
// Load addresses by contract name
|
||||
const deployedContractInfoByName: DeployedContractInfoByName = {};
|
||||
_.each(NetworkId, (networkId: any) => {
|
||||
if (typeof networkId !== 'number') {
|
||||
return;
|
||||
}
|
||||
const contractAddressesForNetwork = getContractAddressesForNetworkOrThrow(networkId);
|
||||
_.each(contractAddressesForNetwork, (contractAddress: string, contractName: string) => {
|
||||
const contractNameLowercase = _.toLower(contractName);
|
||||
if (_.isUndefined(deployedContractInfoByName[contractNameLowercase])) {
|
||||
deployedContractInfoByName[contractNameLowercase] = [];
|
||||
}
|
||||
deployedContractInfoByName[contractNameLowercase].push({
|
||||
contractAddress,
|
||||
networkId,
|
||||
});
|
||||
});
|
||||
});
|
||||
// Load contract artifacts
|
||||
_.each(ContractArtifacts, (contractArtifactAsJson: any) => {
|
||||
const conractArtifact = contractArtifactAsJson as SimpleContractArtifact;
|
||||
const contractName = conractArtifact.contractName;
|
||||
const contractNameLowercase = _.toLower(contractName);
|
||||
const contractAbi: ContractAbi = conractArtifact.compilerOutput.abi;
|
||||
this.addABI(contractAbi, contractName, deployedContractInfoByName[contractNameLowercase]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,12 @@ 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 { AbiEncoder, addressUtils, BigNumber } from '@0x/utils';
|
||||
import { addressUtils, BigNumber } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import { MethodAbi } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
import 'mocha';
|
||||
|
||||
import { ContractAddresses, ContractWrappers } from '../src';
|
||||
import { ZeroExTransactionDecoder } from '../src/utils/zeroex_transaction_decoder';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { migrateOnceAsync } from './utils/migrate';
|
||||
@@ -20,7 +18,7 @@ const expect = chai.expect;
|
||||
|
||||
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
|
||||
|
||||
describe.only('ZeroExTransactionDecoder', () => {
|
||||
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)';
|
||||
@@ -30,6 +28,7 @@ describe.only('ZeroExTransactionDecoder', () => {
|
||||
let orderRight = {};
|
||||
let matchOrdersTxData: string;
|
||||
let contractAddresses: ContractAddresses;
|
||||
let contractWrappers: ContractWrappers;
|
||||
|
||||
before(async () => {
|
||||
// Create accounts
|
||||
@@ -80,15 +79,15 @@ describe.only('ZeroExTransactionDecoder', () => {
|
||||
contractAddresses,
|
||||
blockPollingIntervalMs: 10,
|
||||
};
|
||||
const contractWrappers = new ContractWrappers(provider, config);
|
||||
contractWrappers = new ContractWrappers(provider, config);
|
||||
const transactionEncoder = await contractWrappers.exchange.transactionEncoderAsync();
|
||||
matchOrdersTxData = transactionEncoder.matchOrdersTx(signedOrderLeft, signedOrderRight);
|
||||
});
|
||||
|
||||
describe('decode', () => {
|
||||
it('should successfully decode DutchAuction.matchOrders txData', async () => {
|
||||
it('should successfully decode DutchAuction.matchOrders calldata', async () => {
|
||||
const contractName = 'DutchAuction';
|
||||
const decodedTxData = ZeroExTransactionDecoder.decode(matchOrdersTxData, { contractName });
|
||||
const decodedTxData = contractWrappers.getAbiDecoder().tryDecodeCalldata(matchOrdersTxData, contractName);
|
||||
const expectedFunctionName = 'matchOrders';
|
||||
const expectedFunctionArguments = {
|
||||
buyOrder: orderLeft,
|
||||
@@ -100,9 +99,9 @@ describe.only('ZeroExTransactionDecoder', () => {
|
||||
expect(decodedTxData.functionSignature).to.be.equal(matchOrdersSignature);
|
||||
expect(decodedTxData.functionArguments).to.be.deep.equal(expectedFunctionArguments);
|
||||
});
|
||||
it('should successfully decode Exchange.matchOrders txData (and distinguish from DutchAuction.matchOrders)', async () => {
|
||||
it('should successfully decode Exchange.matchOrders calldata (and distinguish from DutchAuction.matchOrders)', async () => {
|
||||
const contractName = 'Exchange';
|
||||
const decodedTxData = ZeroExTransactionDecoder.decode(matchOrdersTxData, { contractName });
|
||||
const decodedTxData = contractWrappers.getAbiDecoder().tryDecodeCalldata(matchOrdersTxData, contractName);
|
||||
const expectedFunctionName = 'matchOrders';
|
||||
const expectedFunctionArguments = {
|
||||
leftOrder: orderLeft,
|
||||
@@ -114,77 +113,11 @@ describe.only('ZeroExTransactionDecoder', () => {
|
||||
expect(decodedTxData.functionSignature).to.be.equal(matchOrdersSignature);
|
||||
expect(decodedTxData.functionArguments).to.be.deep.equal(expectedFunctionArguments);
|
||||
});
|
||||
it('should successfully decode Exchange.matchOrders, using exchange address to identify the exchange contract', async () => {
|
||||
const contractAddress = contractAddresses.exchange;
|
||||
const decodedTxData = ZeroExTransactionDecoder.decode(matchOrdersTxData, { contractAddress });
|
||||
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 txData', async () => {
|
||||
const contractAddress = contractAddresses.exchange;
|
||||
it('should throw if cannot decode calldata', async () => {
|
||||
const badTxData = '0x01020304';
|
||||
expect(() => {
|
||||
ZeroExTransactionDecoder.decode(badTxData, { contractAddress });
|
||||
contractWrappers.getAbiDecoder().tryDecodeCalldata(badTxData);
|
||||
}).to.throw("No functions registered for selector '0x01020304'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('addABI', () => {
|
||||
it('should successfully add a new ABI', async () => {
|
||||
// Add new ABI
|
||||
const abi: MethodAbi = {
|
||||
name: 'foobar',
|
||||
type: 'function',
|
||||
inputs: [
|
||||
{
|
||||
name: 'addr',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'butter',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
constant: false,
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
};
|
||||
const contractName = 'newContract';
|
||||
const contractAddress = addressUtils.generatePseudoRandomAddress();
|
||||
const networkId = 1;
|
||||
const contractInfo = [
|
||||
{
|
||||
contractAddress,
|
||||
networkId,
|
||||
},
|
||||
];
|
||||
ZeroExTransactionDecoder.addABI([abi], contractName, contractInfo);
|
||||
// Create some tx data
|
||||
const foobarEncoder = new AbiEncoder.Method(abi);
|
||||
const foobarSignature = foobarEncoder.getSignature();
|
||||
const foobarTxData = foobarEncoder.encode([contractAddress]);
|
||||
// Decode tx data using contract name
|
||||
const decodedTxData = ZeroExTransactionDecoder.decode(foobarTxData, { contractName });
|
||||
const expectedFunctionName = abi.name;
|
||||
const expectedFunctionArguments = {
|
||||
addr: contractAddress,
|
||||
};
|
||||
expect(decodedTxData.functionName).to.be.equal(expectedFunctionName);
|
||||
expect(decodedTxData.functionSignature).to.be.equal(foobarSignature);
|
||||
expect(decodedTxData.functionArguments).to.be.deep.equal(expectedFunctionArguments);
|
||||
// Decode tx data using contract address
|
||||
const decodedTxDataDecodedWithAddress = ZeroExTransactionDecoder.decode(foobarTxData, { contractAddress });
|
||||
expect(decodedTxDataDecodedWithAddress).to.be.deep.equal(decodedTxData);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,13 @@
|
||||
[
|
||||
{
|
||||
"version": "4.1.0",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Added method decoding to AbiDecoder",
|
||||
"pr": 1569
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "4.0.4",
|
||||
"changes": [
|
||||
|
||||
@@ -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 { FunctionInfoBySelector, TransactionData } 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 _functionInfoBySelector: FunctionInfoBySelector = {};
|
||||
/**
|
||||
* Retrieves the function selector from tranasction data.
|
||||
* @param calldata hex-encoded transaction data.
|
||||
* @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 transaction data. 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.
|
||||
@@ -37,10 +58,10 @@ export class AbiDecoder {
|
||||
public tryToDecodeLogOrNoop<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog {
|
||||
const methodId = log.topics[0];
|
||||
const numIndexedArgs = log.topics.length - 1;
|
||||
if (_.isUndefined(this._methodIds[methodId]) || _.isUndefined(this._methodIds[methodId][numIndexedArgs])) {
|
||||
if (_.isUndefined(this._eventIds[methodId]) || _.isUndefined(this._eventIds[methodId][numIndexedArgs])) {
|
||||
return log;
|
||||
}
|
||||
const event = this._methodIds[methodId][numIndexedArgs];
|
||||
const event = this._eventIds[methodId][numIndexedArgs];
|
||||
const ethersInterface = new ethers.utils.Interface([event]);
|
||||
const decodedParams: DecodedLogArgs = {};
|
||||
let topicsIndex = 1;
|
||||
@@ -89,25 +110,93 @@ export class AbiDecoder {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add additional ABI definitions to the AbiDecoder
|
||||
* @param abiArray An array of ABI definitions to add to the AbiDecoder
|
||||
* Decodes transaction data for a known ABI.
|
||||
* @param calldata hex-encoded transaction data.
|
||||
* @param contractName used to disambiguate similar ABI's (optional).
|
||||
* @return Decoded transaction data. Includes: function name and signature, along with the decoded arguments.
|
||||
*/
|
||||
public addABI(abiArray: AbiDefinition[]): void {
|
||||
public tryDecodeCalldata(calldata: string, contractName?: string): TransactionData {
|
||||
const functionSelector = AbiDecoder._getFunctionSelector(calldata);
|
||||
const candidateFunctionInfos = this._functionInfoBySelector[functionSelector];
|
||||
if (_.isUndefined(candidateFunctionInfos)) {
|
||||
throw new Error(`No functions registered for selector '${functionSelector}'`);
|
||||
}
|
||||
const functionInfo = _.find(candidateFunctionInfos, txDecoder => {
|
||||
return (
|
||||
(_.isUndefined(contractName) ||
|
||||
_.toLower(txDecoder.contractName) === _.toLower(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 transaction data 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).
|
||||
*/
|
||||
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:
|
||||
this._addEventABI(abi as EventAbi, ethersInterface);
|
||||
break;
|
||||
|
||||
case AbiType.Function:
|
||||
this._addMethodABI(abi as MethodAbi, contractName);
|
||||
break;
|
||||
|
||||
default:
|
||||
// ignore other types
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
private _addEventABI(abi: 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 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._functionInfoBySelector)) {
|
||||
this._functionInfoBySelector[functionSelector] = [];
|
||||
}
|
||||
// Recored a copy of this ABI for each deployment
|
||||
const functionSignature = abiEncoder.getSignature();
|
||||
this._functionInfoBySelector[functionSelector].push({
|
||||
functionSignature,
|
||||
abiEncoder,
|
||||
contractName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,3 @@ export { fetchAsync } from './fetch_async';
|
||||
export { signTypedDataUtils } from './sign_typed_data_utils';
|
||||
export import AbiEncoder = require('./abi_encoder');
|
||||
export * from './types';
|
||||
export { TransactionDecoder } from './transaction_decoder';
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { AbiDefinition, MethodAbi } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AbiEncoder } from '.';
|
||||
import { DeployedContractInfo, FunctionInfoBySelector, TransactionData, TransactionProperties } from './types';
|
||||
|
||||
export class TransactionDecoder {
|
||||
private readonly _functionInfoBySelector: FunctionInfoBySelector = {};
|
||||
/**
|
||||
* Retrieves the function selector from tranasction data.
|
||||
* @param txData hex-encoded transaction data.
|
||||
* @return hex-encoded function selector.
|
||||
*/
|
||||
private static _getFunctionSelector(txData: string): string {
|
||||
const functionSelectorLength = 10;
|
||||
if (!txData.startsWith('0x') || txData.length < functionSelectorLength) {
|
||||
throw new Error(
|
||||
`Malformed transaction data. Must include a hex prefix '0x' and 4-byte function selector. Got '${txData}'`,
|
||||
);
|
||||
}
|
||||
const functionSelector = txData.substr(0, functionSelectorLength);
|
||||
return functionSelector;
|
||||
}
|
||||
/**
|
||||
* Adds a set of ABI definitions, after which transaction data 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).
|
||||
* @param deploymentInfos A collection of network/address pairs where this contract is deployed (optional).
|
||||
*/
|
||||
public addABI(
|
||||
abiDefinitions: AbiDefinition[],
|
||||
contractName?: string,
|
||||
deploymentInfos?: DeployedContractInfo[],
|
||||
): void {
|
||||
// Disregard definitions that are not functions
|
||||
// tslint:disable no-unnecessary-type-assertion
|
||||
const functionAbis = _.filter(abiDefinitions, abiEntry => {
|
||||
return abiEntry.type === 'function';
|
||||
}) as MethodAbi[];
|
||||
// tslint:enable no-unnecessary-type-assertion
|
||||
// Record function ABI's
|
||||
_.each(functionAbis, functionAbi => {
|
||||
const abiEncoder = new AbiEncoder.Method(functionAbi);
|
||||
const functionSelector = abiEncoder.getSelector();
|
||||
if (!(functionSelector in this._functionInfoBySelector)) {
|
||||
this._functionInfoBySelector[functionSelector] = [];
|
||||
}
|
||||
// Recored a copy of this ABI for each deployment
|
||||
const functionSignature = abiEncoder.getSignature();
|
||||
_.each(deploymentInfos, deploymentInfo => {
|
||||
this._functionInfoBySelector[functionSelector].push({
|
||||
functionSignature,
|
||||
abiEncoder,
|
||||
contractName,
|
||||
contractAddress: deploymentInfo.contractAddress,
|
||||
networkId: deploymentInfo.networkId,
|
||||
});
|
||||
});
|
||||
// There is no deployment info for this contract; record it without an address/network id
|
||||
if (_.isEmpty(deploymentInfos)) {
|
||||
this._functionInfoBySelector[functionSelector].push({
|
||||
functionSignature,
|
||||
abiEncoder,
|
||||
contractName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Decodes transaction data for a known ABI.
|
||||
* @param txData hex-encoded transaction data.
|
||||
* @param txProperties Properties about the transaction used to disambiguate similar ABI's (optional).
|
||||
* @return Decoded transaction data. Includes: function name and signature, along with the decoded arguments.
|
||||
*/
|
||||
public decode(txData: string, txProperties_?: TransactionProperties): TransactionData {
|
||||
// Lookup
|
||||
const functionSelector = TransactionDecoder._getFunctionSelector(txData);
|
||||
const txProperties = _.isUndefined(txProperties_) ? {} : txProperties_;
|
||||
const candidateFunctionInfos = this._functionInfoBySelector[functionSelector];
|
||||
if (_.isUndefined(candidateFunctionInfos)) {
|
||||
throw new Error(`No functions registered for selector '${functionSelector}'`);
|
||||
}
|
||||
const functionInfo = _.find(candidateFunctionInfos, txDecoder => {
|
||||
return (
|
||||
(_.isUndefined(txProperties.contractName) ||
|
||||
_.toLower(txDecoder.contractName) === _.toLower(txProperties.contractName)) &&
|
||||
(_.isUndefined(txProperties.contractAddress) ||
|
||||
txDecoder.contractAddress === txProperties.contractAddress) &&
|
||||
(_.isUndefined(txProperties.networkId) || txDecoder.networkId === txProperties.networkId)
|
||||
);
|
||||
});
|
||||
if (_.isUndefined(functionInfo)) {
|
||||
throw new Error(`No function registered with properties: ${JSON.stringify(txProperties)}.`);
|
||||
} else if (_.isUndefined(functionInfo.abiEncoder)) {
|
||||
throw new Error(
|
||||
`Function ABI Encoder is not defined, for function with properties: ${JSON.stringify(txProperties)}.`,
|
||||
);
|
||||
}
|
||||
const functionName = functionInfo.abiEncoder.getDataItem().name;
|
||||
const functionSignature = functionInfo.abiEncoder.getSignatureType();
|
||||
const functionArguments = functionInfo.abiEncoder.decode(txData);
|
||||
const decodedCalldata = {
|
||||
functionName,
|
||||
functionSignature,
|
||||
functionArguments,
|
||||
};
|
||||
return decodedCalldata;
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,3 @@ export interface TransactionData {
|
||||
functionSignature: string;
|
||||
functionArguments: any;
|
||||
}
|
||||
|
||||
export interface TransactionProperties {
|
||||
contractName?: string;
|
||||
contractAddress?: string;
|
||||
networkId?: number;
|
||||
}
|
||||
|
||||
export interface DeployedContractInfo {
|
||||
contractAddress: string;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
export interface DeployedContractInfoByName {
|
||||
[index: string]: DeployedContractInfo[];
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@ import * as chai from 'chai';
|
||||
import { MethodAbi } from 'ethereum-types';
|
||||
import 'mocha';
|
||||
|
||||
import { AbiEncoder, TransactionDecoder } from '../src';
|
||||
import { AbiEncoder, AbiDecoder } from '../src';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('TransactionDecoder', () => {
|
||||
it('should successfully add a new ABI and decode tx data for it', async () => {
|
||||
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: 'addr',
|
||||
name: 'testAddress',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
@@ -32,31 +32,19 @@ describe('TransactionDecoder', () => {
|
||||
stateMutability: 'pure',
|
||||
};
|
||||
const contractName = 'newContract';
|
||||
const contractAddress = '0x0001020304050607080900010203040506070809';
|
||||
const networkId = 1;
|
||||
const contractInfo = [
|
||||
{
|
||||
contractAddress,
|
||||
networkId,
|
||||
},
|
||||
];
|
||||
const transactionDecoder = new TransactionDecoder();
|
||||
transactionDecoder.addABI([abi], contractName, contractInfo);
|
||||
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([contractAddress]);
|
||||
const foobarTxData = foobarEncoder.encode([testAddress]);
|
||||
// Decode tx data using contract name
|
||||
const decodedTxData = transactionDecoder.decode(foobarTxData, { contractName });
|
||||
const decodedTxData = abiDecoder.tryDecodeCalldata(foobarTxData, contractName);
|
||||
const expectedFunctionName = abi.name;
|
||||
const expectedFunctionArguments = {
|
||||
addr: contractAddress,
|
||||
};
|
||||
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);
|
||||
// Decode tx data using contract address
|
||||
const decodedTxDataDecodedWithAddress = transactionDecoder.decode(foobarTxData, { contractAddress });
|
||||
expect(decodedTxDataDecodedWithAddress).to.be.deep.equal(decodedTxData);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user