From 979e374270ffb3a754acee024518783d5737d71f Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Thu, 12 Jul 2018 02:52:43 -0400 Subject: [PATCH] Added new-style events (removed old-style) to contracts and added filters to contracts and interfaces. --- contracts/contract.js | 258 ++++++++++++++++++++------- contracts/interface.js | 42 +++++ src.ts/contracts/contract.ts | 327 +++++++++++++++++++++++++--------- src.ts/contracts/interface.ts | 43 ++++- tests/test-contract.js | 12 +- 5 files changed, 531 insertions(+), 151 deletions(-) diff --git a/contracts/contract.js b/contracts/contract.js index d448285d5..0ef69caab 100644 --- a/contracts/contract.js +++ b/contracts/contract.js @@ -12,8 +12,8 @@ var provider_1 = require("../providers/provider"); var wallet_1 = require("../wallet/wallet"); var abi_coder_1 = require("../utils/abi-coder"); var address_1 = require("../utils/address"); -var bytes_1 = require("../utils/bytes"); var bignumber_1 = require("../utils/bignumber"); +var bytes_1 = require("../utils/bytes"); var properties_1 = require("../utils/properties"); var web_1 = require("../utils/web"); var errors = __importStar(require("../utils/errors")); @@ -151,6 +151,9 @@ function runMethod(contract, functionName, estimateOnly) { }); }; } +function getEventTag(filter) { + return (filter.address || '') + (filter.topics ? filter.topics.join(':') : ''); +} var Contract = /** @class */ (function () { // https://github.com/Microsoft/TypeScript/issues/5453 // Once this issue is resolved (there are open PR) we can do this nicer @@ -178,14 +181,28 @@ var Contract = /** @class */ (function () { errors.throwError('invalid signer or provider', errors.INVALID_ARGUMENT, { arg: 'signerOrProvider', value: signerOrProvider }); } properties_1.defineReadOnly(this, 'estimate', {}); - properties_1.defineReadOnly(this, 'events', {}); properties_1.defineReadOnly(this, 'functions', {}); + properties_1.defineReadOnly(this, 'filters', {}); + Object.keys(this.interface.events).forEach(function (eventName) { + var event = _this.interface.events[eventName]; + properties_1.defineReadOnly(_this.filters, eventName, function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return { + address: _this.address, + topics: event.encodeTopics(args) + }; + }); + }); // Not connected to an on-chain instance, so do not connect functions and events if (!addressOrName) { properties_1.defineReadOnly(this, 'address', null); properties_1.defineReadOnly(this, 'addressPromise', Promise.resolve(null)); return; } + this._events = []; properties_1.defineReadOnly(this, 'address', addressOrName); properties_1.defineReadOnly(this, 'addressPromise', this.provider.resolveName(addressOrName).then(function (address) { if (address == null) { @@ -209,68 +226,6 @@ var Contract = /** @class */ (function () { properties_1.defineReadOnly(_this.estimate, name, runMethod(_this, name, true)); } }); - Object.keys(this.interface.events).forEach(function (eventName) { - var eventInfo = _this.interface.events[eventName]; - var eventCallback = null; - var contract = _this; - function handleEvent(log) { - contract.addressPromise.then(function (address) { - // Not meant for us (the topics just has the same name) - if (address != log.address) { - return null; - } - try { - var result = eventInfo.decode(log.data, log.topics); - // Some useful things to have with the log - log.args = result; - log.event = eventName; - log.decode = eventInfo.decode; - log.removeListener = function () { - contract.provider.removeListener([eventInfo.topic], handleEvent); - }; - log.getBlock = function () { return contract.provider.getBlock(log.blockHash); ; }; - log.getTransaction = function () { return contract.provider.getTransaction(log.transactionHash); }; - log.getTransactionReceipt = function () { return contract.provider.getTransactionReceipt(log.transactionHash); }; - log.eventSignature = eventInfo.signature; - eventCallback.apply(log, Array.prototype.slice.call(result)); - } - catch (error) { - console.log(error); - var onerror_1 = contract._onerror; - if (onerror_1) { - setTimeout(function () { onerror_1(error); }); - } - } - return null; - }).catch(function (error) { }); - } - var property = { - enumerable: true, - get: function () { - return eventCallback; - }, - set: function (value) { - if (!value) { - value = null; - } - if (!contract.provider) { - errors.throwError('events require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'events' }); - } - if (!value && eventCallback) { - contract.provider.removeListener([eventInfo.topic], handleEvent); - } - else if (value && !eventCallback) { - contract.provider.on([eventInfo.topic], handleEvent); - } - eventCallback = value; - } - }; - var propertyName = 'on' + eventName.toLowerCase(); - if (_this[propertyName] == null) { - Object.defineProperty(_this, propertyName, property); - } - Object.defineProperty(_this.events, eventName, property); - }, this); } Object.defineProperty(Contract.prototype, "onerror", { get: function () { return this._onerror; }, @@ -371,6 +326,181 @@ var Contract = /** @class */ (function () { return contract; }); }; + Contract.prototype._getEventFilter = function (eventName) { + var _this = this; + if (typeof (eventName) === 'string') { + // Listen for any event + if (eventName === '*') { + return { + decode: function (log) { + return [_this.interface.parseLog(log)]; + }, + eventTag: '*', + filter: { address: this.address }, + }; + } + // Normalize the eventName + if (eventName.indexOf('(') !== -1) { + eventName = abi_coder_1.formatSignature(abi_coder_1.parseSignature('event ' + eventName)); + } + var event_1 = this.interface.events[eventName]; + if (!event_1) { + errors.throwError('unknown event - ' + eventName, errors.INVALID_ARGUMENT, { argumnet: 'eventName', value: eventName }); + } + var filter_1 = { + address: this.address, + topics: [event_1.topic] + }; + return { + decode: function (log) { + return event_1.decode(log.data, log.topics); + }, + event: event_1, + eventTag: getEventTag(filter_1), + filter: filter_1 + }; + } + var filter = { + address: this.address + }; + // Find the matching event in the ABI; if none, we still allow filtering + // since it may be a filter for an otherwise unknown event + var event = null; + if (eventName.topics && eventName.topics[0]) { + filter.topics = eventName.topics; + for (var name in this.interface.events) { + if (name.indexOf('(') === -1) { + continue; + } + var e = this.interface.events[name]; + if (e.topic === eventName.topics[0].toLowerCase()) { + event = e; + break; + } + } + } + return { + decode: function (log) { + if (event) { + return event.decode(log.data, log.topics); + } + return [log]; + }, + event: event, + eventTag: getEventTag(filter), + filter: filter + }; + }; + Contract.prototype._addEventListener = function (eventFilter, listener, once) { + var _this = this; + if (!this.provider) { + errors.throwError('events require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'once' }); + } + var wrappedListener = function (log) { + var decoded = Array.prototype.slice.call(eventFilter.decode(log)); + var event = properties_1.jsonCopy(log); + event.args = decoded; + event.decode = eventFilter.event.decode; + event.event = eventFilter.event.name; + event.eventSignature = eventFilter.event.signature; + event.removeListener = function () { _this.removeListener(eventFilter.filter, listener); }; + event.getBlock = function () { return _this.provider.getBlock(log.blockHash); }; + event.getTransaction = function () { return _this.provider.getTransactionReceipt(log.transactionHash); }; + event.getTransactionReceipt = function () { return _this.provider.getTransactionReceipt(log.transactionHash); }; + decoded.push(event); + _this.emit.apply(_this, [eventFilter.filter].concat(decoded)); + }; + this.provider.on(eventFilter.filter, wrappedListener); + this._events.push({ eventFilter: eventFilter, listener: listener, wrappedListener: wrappedListener, once: once }); + }; + Contract.prototype.on = function (event, listener) { + this._addEventListener(this._getEventFilter(event), listener, false); + return this; + }; + Contract.prototype.once = function (event, listener) { + this._addEventListener(this._getEventFilter(event), listener, true); + return this; + }; + Contract.prototype.addEventLisener = function (eventName, listener) { + return this.on(eventName, listener); + }; + Contract.prototype.emit = function (eventName) { + var _this = this; + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + if (!this.provider) { + return false; + } + var result = false; + var eventFilter = this._getEventFilter(eventName); + this._events = this._events.filter(function (event) { + if (event.eventFilter.eventTag !== eventFilter.eventTag) { + return true; + } + setTimeout(function () { + event.listener.apply(_this, args); + }, 0); + result = true; + return !(event.once); + }); + return result; + }; + Contract.prototype.listenerCount = function (eventName) { + if (!this.provider) { + return 0; + } + var eventFilter = this._getEventFilter(eventName); + return this._events.filter(function (event) { + return event.eventFilter.eventTag === eventFilter.eventTag; + }).length; + }; + Contract.prototype.listeners = function (eventName) { + if (!this.provider) { + return []; + } + var eventFilter = this._getEventFilter(eventName); + return this._events.filter(function (event) { + return event.eventFilter.eventTag === eventFilter.eventTag; + }).map(function (event) { return event.listener; }); + }; + Contract.prototype.removeAllListeners = function (eventName) { + if (!this.provider) { + return this; + } + var eventFilter = this._getEventFilter(eventName); + this._events = this._events.filter(function (event) { + return event.eventFilter.eventTag !== eventFilter.eventTag; + }); + return this; + }; + Contract.prototype.removeListener = function (eventName, listener) { + var _this = this; + if (!this.provider) { + return this; + } + var found = false; + var eventFilter = this._getEventFilter(eventName); + this._events = this._events.filter(function (event) { + // Make sure this event and listener match + if (event.eventFilter.eventTag !== eventFilter.eventTag) { + return true; + } + if (event.listener !== listener) { + return true; + } + _this.provider.removeListener(event.eventFilter.filter, event.wrappedListener); + // Already found a matching event in a previous loop + if (found) { + return true; + } + // REmove this event (returning false filters us out) + found = true; + return false; + }); + return this; + }; return Contract; }()); exports.Contract = Contract; diff --git a/contracts/interface.js b/contracts/interface.js index f37e32120..73a48434f 100644 --- a/contracts/interface.js +++ b/contracts/interface.js @@ -18,10 +18,12 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); // See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI +var address_1 = require("../utils/address"); var abi_coder_1 = require("../utils/abi-coder"); var bignumber_1 = require("../utils/bignumber"); var bytes_1 = require("../utils/bytes"); var hash_1 = require("../utils/hash"); +var keccak256_1 = require("../utils/keccak256"); var properties_1 = require("../utils/properties"); var errors = __importStar(require("../utils/errors")); var Description = /** @class */ (function () { @@ -123,6 +125,46 @@ var EventDescription = /** @class */ (function (_super) { function EventDescription() { return _super !== null && _super.apply(this, arguments) || this; } + EventDescription.prototype.encodeTopics = function (params) { + var _this = this; + if (params.length > this.inputs.length) { + errors.throwError('too many arguments for ' + this.name, errors.UNEXPECTED_ARGUMENT, { maxCount: params.length, expectedCount: this.inputs.length }); + } + var topics = []; + if (!this.anonymous) { + topics.push(this.topic); + } + params.forEach(function (arg, index) { + if (arg === null) { + topics.push(null); + return; + } + var param = _this.inputs[index]; + if (!param.indexed) { + errors.throwError('cannot filter non-indexed parameters; must be null', errors.INVALID_ARGUMENT, { argument: (param.name || index), value: arg }); + } + if (param.type === 'string') { + topics.push(hash_1.id(arg)); + } + else if (param.type === 'bytes') { + topics.push(keccak256_1.keccak256(arg)); + } + else if (param.type.indexOf('[') !== -1 || param.type.substring(0, 5) === 'tuple') { + errors.throwError('filtering with tuples or arrays not implemented yet; bug us on GitHub', errors.NOT_IMPLEMENTED, { operation: 'filter(array|tuple)' }); + } + else { + if (param.type === 'address') { + address_1.getAddress(arg); + } + topics.push(bytes_1.hexZeroPad(bytes_1.hexlify(arg), 32).toLowerCase()); + } + }); + // Trim off trailing nulls + while (topics.length && topics[topics.length - 1] === null) { + topics.pop(); + } + return topics; + }; EventDescription.prototype.decode = function (data, topics) { // Strip the signature off of non-anonymous topics if (topics != null && !this.anonymous) { diff --git a/src.ts/contracts/contract.ts b/src.ts/contracts/contract.ts index 639a95d18..772b5aa57 100644 --- a/src.ts/contracts/contract.ts +++ b/src.ts/contracts/contract.ts @@ -2,15 +2,14 @@ import { EventDescription, Interface } from './interface'; -import { Provider, TransactionRequest, TransactionResponse } from '../providers/provider'; +import { Block, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse } from '../providers/provider'; import { Signer } from '../wallet/wallet'; -import { defaultAbiCoder } from '../utils/abi-coder'; +import { defaultAbiCoder, formatSignature, ParamType, parseSignature } from '../utils/abi-coder'; import { getContractAddress } from '../utils/address'; -import { hexDataLength, hexDataSlice, isHexString } from '../utils/bytes'; -import { ParamType } from '../utils/abi-coder'; import { BigNumber, ConstantZero } from '../utils/bignumber'; -import { defineReadOnly, shallowCopy } from '../utils/properties'; +import { hexDataLength, hexDataSlice, isHexString } from '../utils/bytes'; +import { defineReadOnly, jsonCopy, shallowCopy } from '../utils/properties'; import { poll } from '../utils/web'; import * as errors from '../utils/errors'; @@ -48,7 +47,6 @@ function resolveAddresses(provider: Provider, value: any, paramType: ParamType | return Promise.resolve(value); } - type RunFunction = (...params: Array) => Promise; function runMethod(contract: Contract, functionName: string, estimateOnly: boolean): RunFunction { @@ -173,14 +171,55 @@ function runMethod(contract: Contract, functionName: string, estimateOnly: boole } } +export type Listener = (...args: Array) => void; + +export type EventFilter = { + address?: string; + topics?: Array; + // @TODO: Support OR-style topcis; backwards compatible to make this change + //topics?: Array> +}; + export type ContractEstimate = (...params: Array) => Promise; export type ContractFunction = (...params: Array) => Promise; -export type ContractEvent = (...params: Array) => void; +export type ContractFilter = (...params: Array) => EventFilter; + +export interface Event extends Log { + args: Array; + decode: (data: string, topics?: Array) => any; + event: string; + eventSignature: string; + + removeListener: () => void; + + getBlock: () => Promise; + getTransaction: () => Promise; + getTransactionReceipt: () => Promise; +} + +function getEventTag(filter: EventFilter): string { + return (filter.address || '') + (filter.topics ? filter.topics.join(':'): ''); +} interface Bucket { [name: string]: T; } +type _EventFilter = { + decode: (log: Log) => Array; + event?: EventDescription; + eventTag: string; + filter: EventFilter; +}; + +type _Event = { + eventFilter: _EventFilter; + listener: Listener; + once: boolean; + wrappedListener: Listener; +}; + + export type ErrorCallback = (error: Error) => void; export type Contractish = Array | Interface | string; export class Contract { @@ -192,7 +231,8 @@ export class Contract { readonly estimate: Bucket; readonly functions: Bucket; - readonly events: Bucket; + + readonly filters: Bucket; readonly addressPromise: Promise; @@ -227,9 +267,20 @@ export class Contract { } defineReadOnly(this, 'estimate', { }); - defineReadOnly(this, 'events', { }); defineReadOnly(this, 'functions', { }); + defineReadOnly(this, 'filters', { }); + + Object.keys(this.interface.events).forEach((eventName) => { + let event = this.interface.events[eventName]; + defineReadOnly(this.filters, eventName, (...args: Array) => { + return { + address: this.address, + topics: event.encodeTopics(args) + } + }); + }); + // Not connected to an on-chain instance, so do not connect functions and events if (!addressOrName) { defineReadOnly(this, 'address', null); @@ -237,6 +288,8 @@ export class Contract { return; } + this._events = []; + defineReadOnly(this, 'address', addressOrName); defineReadOnly(this, 'addressPromise', this.provider.resolveName(addressOrName).then((address) => { if (address == null) { throw new Error('name not found'); } @@ -260,77 +313,6 @@ export class Contract { defineReadOnly(this.estimate, name, runMethod(this, name, true)); } }); - - Object.keys(this.interface.events).forEach((eventName) => { - let eventInfo: EventDescription = this.interface.events[eventName]; - - type Callback = (...args: Array) => void; - let eventCallback: Callback = null; - - let contract = this; - function handleEvent(log: any): void { - contract.addressPromise.then((address) => { - // Not meant for us (the topics just has the same name) - if (address != log.address) { return null; } - - try { - let result = eventInfo.decode(log.data, log.topics); - - // Some useful things to have with the log - log.args = result; - log.event = eventName; - log.decode = eventInfo.decode; - log.removeListener = function() { - contract.provider.removeListener([ eventInfo.topic ], handleEvent); - } - - log.getBlock = function() { return contract.provider.getBlock(log.blockHash);; } - log.getTransaction = function() { return contract.provider.getTransaction(log.transactionHash); } - log.getTransactionReceipt = function() { return contract.provider.getTransactionReceipt(log.transactionHash); } - log.eventSignature = eventInfo.signature; - - eventCallback.apply(log, Array.prototype.slice.call(result)); - } catch (error) { - console.log(error); - let onerror = contract._onerror; - if (onerror) { setTimeout(() => { onerror(error); }); } - } - - return null; - }).catch((error) => { }); - } - - var property = { - enumerable: true, - get: function() { - return eventCallback; - }, - set: function(value: Callback) { - if (!value) { value = null; } - - if (!contract.provider) { - errors.throwError('events require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'events' }) - } - - if (!value && eventCallback) { - contract.provider.removeListener([ eventInfo.topic ], handleEvent); - - } else if (value && !eventCallback) { - contract.provider.on([ eventInfo.topic ], handleEvent); - } - - eventCallback = value; - } - }; - - var propertyName = 'on' + eventName.toLowerCase(); - if ((this)[propertyName] == null) { - Object.defineProperty(this, propertyName, property); - } - - Object.defineProperty(this.events, eventName, property); - - }, this); } get onerror() { return this._onerror; } @@ -436,4 +418,189 @@ export class Contract { return contract; }); } + + private _events: Array<_Event>; + + _getEventFilter(eventName: EventFilter | string): _EventFilter { + if (typeof(eventName) === 'string') { + + // Listen for any event + if (eventName === '*') { + return { + decode: (log: Log) => { + return [ this.interface.parseLog(log) ]; + }, + eventTag: '*', + filter: { address: this.address }, + }; + } + + // Normalize the eventName + if (eventName.indexOf('(') !== -1) { + eventName = formatSignature(parseSignature('event ' + eventName)); + } + + let event = this.interface.events[eventName]; + if (!event) { + errors.throwError('unknown event - ' + eventName, errors.INVALID_ARGUMENT, { argumnet: 'eventName', value: eventName }); + } + + let filter = { + address: this.address, + topics: [ event.topic ] + } + + return { + decode: (log: Log) => { + return event.decode(log.data, log.topics) + }, + event: event, + eventTag: getEventTag(filter), + filter: filter + }; + } + + let filter: EventFilter = { + address: this.address + } + + // Find the matching event in the ABI; if none, we still allow filtering + // since it may be a filter for an otherwise unknown event + let event: EventDescription = null; + if (eventName.topics && eventName.topics[0]) { + filter.topics = eventName.topics; + for (var name in this.interface.events) { + if (name.indexOf('(') === -1) { continue; } + let e = this.interface.events[name]; + if (e.topic === eventName.topics[0].toLowerCase()) { + event = e; + break; + } + } + } + + return { + decode: (log: Log) => { + if (event) { return event.decode(log.data, log.topics) } + return [ log ] + }, + event: event, + eventTag: getEventTag(filter), + filter: filter + } + } + + _addEventListener(eventFilter: _EventFilter, listener: Listener, once: boolean): void { + if (!this.provider) { + errors.throwError('events require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'once' }) + } + + let wrappedListener = (log: Log) => { + let decoded = Array.prototype.slice.call(eventFilter.decode(log)); + + let event = jsonCopy(log); + event.args = decoded; + event.decode = eventFilter.event.decode; + event.event = eventFilter.event.name; + event.eventSignature = eventFilter.event.signature; + + event.removeListener = () => { this.removeListener(eventFilter.filter, listener); }; + + event.getBlock = () => { return this.provider.getBlock(log.blockHash); } + event.getTransaction = () => { return this.provider.getTransactionReceipt(log.transactionHash); } + event.getTransactionReceipt = () => { return this.provider.getTransactionReceipt(log.transactionHash); } + + decoded.push(event); + this.emit(eventFilter.filter, ...decoded); + }; + + this.provider.on(eventFilter.filter, wrappedListener); + this._events.push({ eventFilter: eventFilter, listener: listener, wrappedListener: wrappedListener, once: once }); + } + + on(event: EventFilter | string, listener: Listener): Contract { + this._addEventListener(this._getEventFilter(event), listener, false); + return this; + } + + once(event: EventFilter | string, listener: Listener): Contract { + this._addEventListener(this._getEventFilter(event), listener, true); + return this; + } + + addEventLisener(eventName: EventFilter | string, listener: Listener): Contract { + return this.on(eventName, listener); + } + + emit(eventName: EventFilter | string, ...args: Array): boolean { + if (!this.provider) { return false; } + + let result = false; + + let eventFilter = this._getEventFilter(eventName); + this._events = this._events.filter((event) => { + if (event.eventFilter.eventTag !== eventFilter.eventTag) { return true; } + setTimeout(() => { + event.listener.apply(this, args); + }, 0); + result = true; + return !(event.once); + }); + + return result; + } + + listenerCount(eventName?: EventFilter | string): number { + if (!this.provider) { return 0; } + + let eventFilter = this._getEventFilter(eventName); + return this._events.filter((event) => { + return event.eventFilter.eventTag === eventFilter.eventTag + }).length; + } + + listeners(eventName: EventFilter | string): Array { + if (!this.provider) { return []; } + + let eventFilter = this._getEventFilter(eventName); + return this._events.filter((event) => { + return event.eventFilter.eventTag === eventFilter.eventTag + }).map((event) => { return event.listener; }); + } + + removeAllListeners(eventName: EventFilter | string): Contract { + if (!this.provider) { return this; } + + let eventFilter = this._getEventFilter(eventName); + this._events = this._events.filter((event) => { + return event.eventFilter.eventTag !== eventFilter.eventTag + }); + + return this; + } + + removeListener(eventName: any, listener: Listener): Contract { + if (!this.provider) { return this; } + + let found = false; + + let eventFilter = this._getEventFilter(eventName); + this._events = this._events.filter((event) => { + + // Make sure this event and listener match + if (event.eventFilter.eventTag !== eventFilter.eventTag) { return true; } + if (event.listener !== listener) { return true; } + this.provider.removeListener(event.eventFilter.filter, event.wrappedListener); + + // Already found a matching event in a previous loop + if (found) { return true; } + + // REmove this event (returning false filters us out) + found = true; + return false; + }); + + return this; + } + } diff --git a/src.ts/contracts/interface.ts b/src.ts/contracts/interface.ts index 6ed609a0b..605828f34 100644 --- a/src.ts/contracts/interface.ts +++ b/src.ts/contracts/interface.ts @@ -2,10 +2,12 @@ // See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI +import { getAddress } from '../utils/address'; import { defaultAbiCoder, EventFragment, formatSignature, FunctionFragment, ParamType, parseSignature } from '../utils/abi-coder'; import { BigNumber, bigNumberify, BigNumberish } from '../utils/bignumber'; -import { arrayify, concat, isHexString } from '../utils/bytes'; +import { arrayify, concat, hexlify, hexZeroPad, isHexString } from '../utils/bytes'; import { id } from '../utils/hash'; +import { keccak256 } from '../utils/keccak256'; import { defineReadOnly, defineFrozen } from '../utils/properties'; import * as errors from '../utils/errors'; @@ -109,6 +111,45 @@ export class EventDescription extends Description { readonly anonymous: boolean; readonly topic: string; + encodeTopics(params: Array): Array { + if (params.length > this.inputs.length) { + errors.throwError('too many arguments for ' + this.name, errors.UNEXPECTED_ARGUMENT, { maxCount: params.length, expectedCount: this.inputs.length }) + } + + let topics: Array = []; + if (!this.anonymous) { topics.push(this.topic); } + params.forEach((arg, index) => { + if (arg === null) { + topics.push(null); + return; + } + + let param = this.inputs[index]; + + if (!param.indexed) { + errors.throwError('cannot filter non-indexed parameters; must be null', errors.INVALID_ARGUMENT, { argument: (param.name || index), value: arg }); + } + + if (param.type === 'string') { + topics.push(id(arg)); + } else if (param.type === 'bytes') { + topics.push(keccak256(arg)); + } else if (param.type.indexOf('[') !== -1 || param.type.substring(0, 5) === 'tuple') { + errors.throwError('filtering with tuples or arrays not implemented yet; bug us on GitHub', errors.NOT_IMPLEMENTED, { operation: 'filter(array|tuple)' }); + } else { + if (param.type === 'address') { getAddress(arg); } + topics.push(hexZeroPad(hexlify(arg), 32).toLowerCase()); + } + }); + + // Trim off trailing nulls + while (topics.length && topics[topics.length - 1] === null) { + topics.pop(); + } + + return topics; + } + decode(data: string, topics?: Array): any { // Strip the signature off of non-anonymous topics if (topics != null && !this.anonymous) { topics = topics.slice(1); } diff --git a/tests/test-contract.js b/tests/test-contract.js index c08bbef38..39b8c938e 100644 --- a/tests/test-contract.js +++ b/tests/test-contract.js @@ -49,13 +49,13 @@ function TestContractEvents() { function waitForEvent(eventName, expected) { return new Promise(function(resolve, reject) { - contract['on' + eventName.toLowerCase()] = function() { - //console.dir(this, { depth: null }); - //console.log(this.event); - this.removeListener(); - equals(this.event, Array.prototype.slice.call(arguments), expected); + contract.on(eventName, function() { + var args = Array.prototype.slice.call(arguments); + var event = args.pop(); + event.removeListener(); + equals(event.event, args, expected); resolve(); - }; + }); }); }