diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/0x.ts | 25 | ||||
-rw-r--r-- | src/contract_wrappers/contract_wrapper.ts | 66 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 23 | ||||
-rw-r--r-- | src/contract_wrappers/token_wrapper.ts | 24 | ||||
-rw-r--r-- | src/index.ts | 1 | ||||
-rw-r--r-- | src/types.ts | 14 | ||||
-rw-r--r-- | src/utils/abi_decoder.ts | 64 | ||||
-rw-r--r-- | src/web3_wrapper.ts | 22 |
8 files changed, 188 insertions, 51 deletions
@@ -31,6 +31,8 @@ import { DecodedLogArgs, TransactionReceiptWithDecodedLogs, LogWithDecodedArgs, + FilterObject, + RawLog, } from './types'; import {zeroExConfigSchema} from './schemas/zero_ex_config_schema'; @@ -200,10 +202,16 @@ export class ZeroEx { this._web3Wrapper = new Web3Wrapper(provider, defaults); this.token = new TokenWrapper( this._web3Wrapper, + this._abiDecoder, this._getTokenTransferProxyAddressAsync.bind(this), ); const exchageContractAddressIfExists = _.isUndefined(config) ? undefined : config.exchangeContractAddress; - this.exchange = new ExchangeWrapper(this._web3Wrapper, this.token, exchageContractAddressIfExists); + this.exchange = new ExchangeWrapper( + this._web3Wrapper, + this._abiDecoder, + this.token, + exchageContractAddressIfExists, + ); this.proxy = new TokenTransferProxyWrapper( this._web3Wrapper, this._getTokenTransferProxyAddressAsync.bind(this), @@ -300,17 +308,10 @@ export class ZeroEx { const transactionReceipt = await this._web3Wrapper.getTransactionReceiptAsync(txHash); if (!_.isNull(transactionReceipt)) { intervalUtils.clearAsyncExcludingInterval(intervalId); - const logsWithDecodedArgs = _.map(transactionReceipt.logs, (log: Web3.LogEntry) => { - const decodedLog = this._abiDecoder.decodeLog(log); - if (_.isUndefined(decodedLog)) { - return log; - } - const logWithDecodedArgs: LogWithDecodedArgs = { - ...log, - ...decodedLog, - }; - return logWithDecodedArgs; - }); + const logsWithDecodedArgs = _.map( + transactionReceipt.logs, + this._abiDecoder.tryToDecodeLogOrNoop.bind(this._abiDecoder), + ); const transactionReceiptWithDecodedLogArgs: TransactionReceiptWithDecodedLogs = { ...transactionReceipt, logs: logsWithDecodedArgs, diff --git a/src/contract_wrappers/contract_wrapper.ts b/src/contract_wrappers/contract_wrapper.ts index 2a55b53d9..743dfc9b2 100644 --- a/src/contract_wrappers/contract_wrapper.ts +++ b/src/contract_wrappers/contract_wrapper.ts @@ -1,13 +1,52 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; +import * as ethUtil from 'ethereumjs-util'; import {Web3Wrapper} from '../web3_wrapper'; -import {ZeroExError, Artifact} from '../types'; +import {AbiDecoder} from '../utils/abi_decoder'; +import { + InternalZeroExError, + Artifact, + LogWithDecodedArgs, + RawLog, + ContractEvents, + SubscriptionOpts, + IndexedFilterValues, +} from '../types'; import {utils} from '../utils/utils'; +const TOPIC_LENGTH = 32; + export class ContractWrapper { protected _web3Wrapper: Web3Wrapper; - constructor(web3Wrapper: Web3Wrapper) { + private _abiDecoder?: AbiDecoder; + constructor(web3Wrapper: Web3Wrapper, abiDecoder?: AbiDecoder) { this._web3Wrapper = web3Wrapper; + this._abiDecoder = abiDecoder; + } + protected async _getLogsAsync(address: string, eventName: ContractEvents, subscriptionOpts: SubscriptionOpts, + indexFilterValues: IndexedFilterValues, + abi: Web3.ContractAbi): Promise<LogWithDecodedArgs[]> { + const eventAbi = _.find(abi, {name: eventName}) as Web3.EventAbi; + const eventSignature = this._getEventSignatureFromAbiByName(eventAbi, eventName); + const topicForEventSignature = this._web3Wrapper.keccak256(eventSignature); + const topicsForIndexedArgs = this._getTopicsForIndexedArgs(eventAbi, indexFilterValues); + const topics = [topicForEventSignature, ...topicsForIndexedArgs]; + const filter = { + fromBlock: subscriptionOpts.fromBlock, + toBlock: subscriptionOpts.toBlock, + address, + topics, + }; + const logs = await this._web3Wrapper.getLogsAsync(filter); + const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this)); + return logsWithDecodedArguments; + } + protected _tryToDecodeLogOrNoop(log: Web3.LogEntry): LogWithDecodedArgs|RawLog { + if (_.isUndefined(this._abiDecoder)) { + throw new Error(InternalZeroExError.NoAbiDecoder); + } + const logWithDecodedArgs = this._abiDecoder.tryToDecodeLogOrNoop(log); + return logWithDecodedArgs; } protected async _instantiateContractIfExistsAsync<A extends Web3.ContractInstance>(artifact: Artifact, addressIfExists?: string, @@ -16,4 +55,27 @@ export class ContractWrapper { await this._web3Wrapper.getContractInstanceFromArtifactAsync<A>(artifact, addressIfExists); return contractInstance; } + protected _getEventSignatureFromAbiByName(eventAbi: Web3.EventAbi, eventName: ContractEvents): string { + const types = _.map(eventAbi.inputs, 'type'); + const signature = `${eventAbi.name}(${types.join(',')})`; + return signature; + } + private _getTopicsForIndexedArgs(abi: Web3.EventAbi, indexFilterValues: IndexedFilterValues): Array<string|null> { + const topics: Array<string|null> = []; + for (const eventInput of abi.inputs) { + if (!eventInput.indexed) { + continue; + } + if (_.isUndefined(indexFilterValues[eventInput.name])) { + topics.push(null); + } else { + const value = indexFilterValues[eventInput.name] as string; + const buffer = ethUtil.toBuffer(value); + const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH); + const topic = ethUtil.bufferToHex(paddedBuffer); + topics.push(topic); + } + } + return topics; + } } diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index d02a6e642..b3a35d5bf 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -30,6 +30,7 @@ import { MethodOpts, ValidateOrderFillableOpts, OrderTransactionOpts, + RawLog, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; @@ -39,6 +40,7 @@ import {ContractWrapper} from './contract_wrapper'; import {constants} from '../utils/constants'; import {TokenWrapper} from './token_wrapper'; import {decorators} from '../utils/decorators'; +import {AbiDecoder} from '../utils/abi_decoder'; import {artifacts} from '../artifacts'; const SHOULD_VALIDATE_BY_DEFAULT = true; @@ -79,8 +81,9 @@ export class ExchangeWrapper extends ContractWrapper { ]; return [orderAddresses, orderValues]; } - constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper, contractAddressIfExists?: string) { - super(web3Wrapper); + constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, + tokenWrapper: TokenWrapper, contractAddressIfExists?: string) { + super(web3Wrapper, abiDecoder); this._tokenWrapper = tokenWrapper; this._orderValidationUtils = new OrderValidationUtils(tokenWrapper, this); this._exchangeLogEventEmitters = []; @@ -656,6 +659,22 @@ export class ExchangeWrapper extends ContractWrapper { return eventEmitter; } /** + * Gets historical logs without creating a subscription + * @param eventName The exchange contract event you would like to subscribe to. + * @param subscriptionOpts Subscriptions options that let you configure the subscription. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{_from: aUserAddressHex}` + * @return Array of logs that match the parameters + */ + public async getLogsAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, + indexFilterValues: IndexedFilterValues): Promise<LogWithDecodedArgs[]> { + const exchangeContractAddress = await this.getContractAddressAsync(); + const logs = await this._getLogsAsync( + exchangeContractAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.ExchangeArtifact.abi, + ); + return logs; + } + /** * Stops watching for all exchange events */ public async stopWatchingAllEventsAsync(): Promise<void> { diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts index f7f0a0ce3..91af223e4 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -7,6 +7,7 @@ import {utils} from '../utils/utils'; import {eventUtils} from '../utils/event_utils'; import {constants} from '../utils/constants'; import {ContractWrapper} from './contract_wrapper'; +import {AbiDecoder} from '../utils/abi_decoder'; import {artifacts} from '../artifacts'; import { TokenContract, @@ -18,6 +19,8 @@ import { ContractEventEmitter, ContractEventObj, MethodOpts, + LogWithDecodedArgs, + RawLog, } from '../types'; const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 47155; @@ -32,8 +35,9 @@ export class TokenWrapper extends ContractWrapper { private _tokenContractsByAddress: {[address: string]: TokenContract}; private _tokenLogEventEmitters: ContractEventEmitter[]; private _tokenTransferProxyContractAddressFetcher: () => Promise<string>; - constructor(web3Wrapper: Web3Wrapper, tokenTransferProxyContractAddressFetcher: () => Promise<string>) { - super(web3Wrapper); + constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, + tokenTransferProxyContractAddressFetcher: () => Promise<string>) { + super(web3Wrapper, abiDecoder); this._tokenContractsByAddress = {}; this._tokenLogEventEmitters = []; this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher; @@ -277,6 +281,22 @@ export class TokenWrapper extends ContractWrapper { return eventEmitter; } /** + * Gets historical logs without creating a subscription + * @param tokenAddress An address of the token that emmited the logs. + * @param eventName The token contract event you would like to subscribe to. + * @param subscriptionOpts Subscriptions options that let you configure the subscription. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{_from: aUserAddressHex}` + * @return Array of logs that match the parameters + */ + public async getLogsAsync(tokenAddress: string, eventName: TokenEvents, subscriptionOpts: SubscriptionOpts, + indexFilterValues: IndexedFilterValues): Promise<LogWithDecodedArgs[]> { + const logs = await this._getLogsAsync( + tokenAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.TokenArtifact.abi, + ); + return logs; + } + /** * Stops watching for all token events */ public async stopWatchingAllEventsAsync(): Promise<void> { diff --git a/src/index.ts b/src/index.ts index 9730d3fef..3359743e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,4 +35,5 @@ export { DecodedLogArgs, MethodOpts, OrderTransactionOpts, + FilterObject, } from './types'; diff --git a/src/types.ts b/src/types.ts index 02230b0ab..35bb6af78 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,6 @@ export enum ZeroExError { UserHasNoAssociatedAddress = 'USER_HAS_NO_ASSOCIATED_ADDRESSES', InvalidSignature = 'INVALID_SIGNATURE', ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', - ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', InsufficientBalanceForTransfer = 'INSUFFICIENT_BALANCE_FOR_TRANSFER', InsufficientEthBalanceForDeposit = 'INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT', @@ -17,6 +16,11 @@ export enum ZeroExError { NoNetworkId = 'NO_NETWORK_ID', } +export enum InternalZeroExError { + NoAbiDecoder = 'NO_ABI_DECODER', + ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', +} + /** * Elliptic Curve signature */ @@ -198,6 +202,8 @@ export interface TokenTransferProxyContract extends Web3.ContractInstance { export enum SolidityTypes { Address = 'address', Uint256 = 'uint256', + Uint8 = 'uint8', + Uint = 'uint', } export enum ExchangeContractErrCodes { @@ -234,6 +240,8 @@ export enum ExchangeContractErrs { BatchOrdersMustHaveAtLeastOneItem = 'BATCH_ORDERS_MUST_HAVE_AT_LEAST_ONE_ITEM', } +export type RawLog = Web3.LogEntry; + export interface ContractEvent { logIndex: number; transactionIndex: number; @@ -338,6 +346,8 @@ export enum TokenEvents { Approval = 'Approval', } +export type ContractEvents = TokenEvents|ExchangeEvents; + export interface IndexedFilterValues { [index: string]: ContractEventArg; } @@ -460,3 +470,5 @@ export interface MethodOpts { export interface OrderTransactionOpts { shouldValidate: boolean; } + +export type FilterObject = Web3.FilterObject; diff --git a/src/utils/abi_decoder.ts b/src/utils/abi_decoder.ts index 61c8eecd4..542591251 100644 --- a/src/utils/abi_decoder.ts +++ b/src/utils/abi_decoder.ts @@ -1,7 +1,7 @@ import * as Web3 from 'web3'; import * as _ from 'lodash'; import * as BigNumber from 'bignumber.js'; -import {AbiType, DecodedLogArgs, DecodedArgs} from '../types'; +import {AbiType, DecodedLogArgs, DecodedArgs, LogWithDecodedArgs, RawLog, SolidityTypes} from '../types'; import * as SolidityCoder from 'web3/lib/solidity/coder'; export class AbiDecoder { @@ -10,40 +10,40 @@ export class AbiDecoder { constructor(abiArrays: Web3.AbiDefinition[][]) { _.map(abiArrays, this.addABI.bind(this)); } - public decodeLog(logItem: Web3.LogEntry): DecodedArgs|undefined { - const methodId = logItem.topics[0]; + // This method can only decode logs from the 0x smart contracts + public tryToDecodeLogOrNoop(log: Web3.LogEntry): LogWithDecodedArgs|RawLog { + const methodId = log.topics[0]; const event = this.methodIds[methodId]; - if (!_.isUndefined(event)) { - const logData = logItem.data; - const decodedParams: DecodedLogArgs = {}; - let dataIndex = 0; - let topicsIndex = 1; + if (_.isUndefined(event)) { + return log; + } + const logData = log.data; + const decodedParams: DecodedLogArgs = {}; + let dataIndex = 0; + let topicsIndex = 1; - const nonIndexedInputs = _.filter(event.inputs, input => !input.indexed); - const dataTypes = _.map(nonIndexedInputs, input => input.type); - const decodedData = SolidityCoder.decodeParams(dataTypes, logData.slice(2)); - _.map(event.inputs, (param: Web3.EventParameter) => { - let value; - if (param.indexed) { - value = logItem.topics[topicsIndex]; - topicsIndex++; - } else { - value = decodedData[dataIndex]; - dataIndex++; - } - if (param.type === 'address') { - value = this.padZeros(new BigNumber(value).toString(16)); - } else if (param.type === 'uint256' || param.type === 'uint8' || param.type === 'int' ) { - value = new BigNumber(value); - } - decodedParams[param.name] = value; - }); + const nonIndexedInputs = _.filter(event.inputs, input => !input.indexed); + const dataTypes = _.map(nonIndexedInputs, input => input.type); + const decodedData = SolidityCoder.decodeParams(dataTypes, logData.slice('0x'.length)); - return { - event: event.name, - args: decodedParams, - }; - } + _.map(event.inputs, (param: Web3.EventParameter) => { + // Indexed parameters are stored in topics. Non-indexed ones in decodedData + let value = param.indexed ? log.topics[topicsIndex++] : decodedData[dataIndex++]; + if (param.type === SolidityTypes.Address) { + value = this.padZeros(new BigNumber(value).toString(16)); + } else if (param.type === SolidityTypes.Uint256 || + param.type === SolidityTypes.Uint8 || + param.type === SolidityTypes.Uint ) { + value = new BigNumber(value); + } + decodedParams[param.name] = value; + }); + + return { + ...log, + event: event.name, + args: decodedParams, + }; } private addABI(abiArray: Web3.AbiDefinition[]): void { _.map(abiArray, (abi: Web3.AbiDefinition) => { diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index 0bf80a6e4..7576f3d40 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -9,10 +9,12 @@ export class Web3Wrapper { private web3: Web3; private defaults: Partial<Web3.TxData>; private networkIdIfExists?: number; + private jsonRpcRequestId: number; constructor(provider: Web3.Provider, defaults: Partial<Web3.TxData>) { this.web3 = new Web3(); this.web3.setProvider(provider); this.defaults = defaults; + this.jsonRpcRequestId = 0; } public setProvider(provider: Web3.Provider) { delete this.networkIdIfExists; @@ -100,6 +102,20 @@ export class Web3Wrapper { const addresses: string[] = await promisify(this.web3.eth.getAccounts)(); return addresses; } + public async getLogsAsync(filter: Web3.FilterObject): Promise<Web3.LogEntry[]> { + const payload = { + jsonrpc: '2.0', + id: this.jsonRpcRequestId++, + method: 'eth_getLogs', + params: [filter], + }; + const logs = await this.sendRawPayloadAsync(payload); + return logs; + } + public keccak256(data: string): string { + const hash = this.web3.sha3(data); + return hash; + } private getContractInstance<A extends Web3.ContractInstance>(abi: Web3.ContractAbi, address: string): A { const web3ContractInstance = this.web3.eth.contract(abi).at(address); const contractInstance = new Contract(web3ContractInstance, this.defaults) as any as A; @@ -109,4 +125,10 @@ export class Web3Wrapper { const networkId = await promisify(this.web3.version.getNetwork)(); return networkId; } + private async sendRawPayloadAsync(payload: Web3.JSONRPCRequestPayload): Promise<any> { + const sendAsync = this.web3.currentProvider.sendAsync.bind(this.web3.currentProvider); + const response = await promisify(sendAsync)(payload); + const result = response.result; + return result; + } } |