aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/0x.ts2
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts34
-rw-r--r--src/contract_wrappers/token_wrapper.ts57
-rw-r--r--src/index.ts5
-rw-r--r--src/types.ts22
-rw-r--r--src/utils/event_utils.ts44
-rw-r--r--test/token_wrapper_test.ts113
7 files changed, 240 insertions, 37 deletions
diff --git a/src/0x.ts b/src/0x.ts
index b56a2f949..e266eff7d 100644
--- a/src/0x.ts
+++ b/src/0x.ts
@@ -164,8 +164,8 @@ export class ZeroEx {
this._web3Wrapper.setProvider(provider);
await this.exchange.invalidateContractInstancesAsync();
this.tokenRegistry.invalidateContractInstance();
- this.token.invalidateContractInstances();
this._proxyWrapper.invalidateContractInstance();
+ await this.token.invalidateContractInstancesAsync();
}
/**
* Get user Ethereum addresses available through the supplied web3 instance available for sending transactions.
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 57a116aea..722910a61 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -34,6 +34,7 @@ import {
} from '../types';
import {assert} from '../utils/assert';
import {utils} from '../utils/utils';
+import {eventUtils} from '../utils/event_utils';
import {ContractWrapper} from './contract_wrapper';
import {ProxyWrapper} from './proxy_wrapper';
import {ExchangeArtifactsByName} from '../exchange_artifacts_by_name';
@@ -601,7 +602,7 @@ export class ExchangeWrapper extends ContractWrapper {
}
const logEventObj: ContractEventObj = createLogEvent(indexFilterValues, subscriptionOpts);
- const eventEmitter = this._wrapEventEmitter(logEventObj);
+ const eventEmitter = eventUtils.wrapEventEmitter(logEventObj);
this._exchangeLogEventEmitters.push(eventEmitter);
return eventEmitter;
}
@@ -655,37 +656,6 @@ export class ExchangeWrapper extends ContractWrapper {
const isAuthorized = await this._proxyWrapper.isAuthorizedAsync(exchangeContractAddress);
return isAuthorized;
}
- private _wrapEventEmitter(event: ContractEventObj): ContractEventEmitter {
- const watch = (eventCallback: EventCallback) => {
- const bignumberWrappingEventCallback = this._getBigNumberWrappingEventCallback(eventCallback);
- event.watch(bignumberWrappingEventCallback);
- };
- const zeroExEvent = {
- watch,
- stopWatchingAsync: async () => {
- await promisify(event.stopWatching, event)();
- },
- };
- return zeroExEvent;
- }
- private _getBigNumberWrappingEventCallback(eventCallback: EventCallback): EventCallback {
- const bignumberWrappingEventCallback = (err: Error, event: ContractEvent) => {
- if (_.isNull(err)) {
- const wrapIfBigNumber = (value: ContractEventArg): ContractEventArg => {
- // HACK: The old version of BigNumber used by Web3@0.19.0 does not support the `isBigNumber`
- // and checking for a BigNumber instance using `instanceof` does not work either. We therefore
- // compare the constructor functions of the possible BigNumber instance and the BigNumber used by
- // Web3.
- const web3BigNumber = (Web3.prototype as any).BigNumber;
- const isWeb3BigNumber = web3BigNumber.toString() === value.constructor.toString();
- return isWeb3BigNumber ? new BigNumber(value) : value;
- };
- event.args = _.mapValues(event.args, wrapIfBigNumber);
- }
- eventCallback(err, event);
- };
- return bignumberWrappingEventCallback;
- }
private async _isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature,
signerAddressHex: string,
exchangeContractAddress: string): Promise<boolean> {
diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts
index e34c624ab..d60843cdb 100644
--- a/src/contract_wrappers/token_wrapper.ts
+++ b/src/contract_wrappers/token_wrapper.ts
@@ -2,11 +2,22 @@ import * as _ from 'lodash';
import * as BigNumber from 'bignumber.js';
import {Web3Wrapper} from '../web3_wrapper';
import {assert} from '../utils/assert';
+import {utils} from '../utils/utils';
+import {eventUtils} from '../utils/event_utils';
import {constants} from '../utils/constants';
import {ContractWrapper} from './contract_wrapper';
import * as TokenArtifacts from '../artifacts/Token.json';
import * as ProxyArtifacts from '../artifacts/Proxy.json';
-import {TokenContract, ZeroExError} from '../types';
+import {
+ TokenContract,
+ ZeroExError,
+ TokenEvents,
+ IndexedFilterValues,
+ SubscriptionOpts,
+ CreateContractEvent,
+ ContractEventEmitter,
+ ContractEventObj,
+} from '../types';
const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 45730;
@@ -17,11 +28,14 @@ const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 45730;
*/
export class TokenWrapper extends ContractWrapper {
private _tokenContractsByAddress: {[address: string]: TokenContract};
+ private _tokenLogEventEmitters: ContractEventEmitter[];
constructor(web3Wrapper: Web3Wrapper) {
super(web3Wrapper);
this._tokenContractsByAddress = {};
+ this._tokenLogEventEmitters = [];
}
- public invalidateContractInstances() {
+ public async invalidateContractInstancesAsync(): Promise<void> {
+ await this.stopWatchingAllEventsAsync();
this._tokenContractsByAddress = {};
}
/**
@@ -178,6 +192,45 @@ export class TokenWrapper extends ContractWrapper {
from: senderAddress,
});
}
+ /**
+ * Subscribe to an event type emitted by the Token smart contract
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @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 `{maker: aUserAddressHex}`
+ * @return ContractEventEmitter object
+ */
+ public async subscribeAsync(tokenAddress: string, eventName: TokenEvents, subscriptionOpts: SubscriptionOpts,
+ indexFilterValues: IndexedFilterValues):
+ Promise<ContractEventEmitter> {
+ const tokenContract = await this._getTokenContractAsync(tokenAddress);
+ let createLogEvent: CreateContractEvent;
+ switch (eventName) {
+ case TokenEvents.Approval:
+ createLogEvent = tokenContract.Approval;
+ break;
+ case TokenEvents.Transfer:
+ createLogEvent = tokenContract.Transfer;
+ break;
+ default:
+ throw utils.spawnSwitchErr('TokenEvents', eventName);
+ }
+
+ const logEventObj: ContractEventObj = createLogEvent(indexFilterValues, subscriptionOpts);
+ const eventEmitter = eventUtils.wrapEventEmitter(logEventObj);
+ this._tokenLogEventEmitters.push(eventEmitter);
+ return eventEmitter;
+ }
+ /**
+ * Stops watching for all token events
+ */
+ public async stopWatchingAllEventsAsync(): Promise<void> {
+ const stopWatchingPromises = _.map(this._tokenLogEventEmitters,
+ logEventObj => logEventObj.stopWatchingAsync());
+ await Promise.all(stopWatchingPromises);
+ this._tokenLogEventEmitters = [];
+ }
private async _getTokenContractAsync(tokenAddress: string): Promise<TokenContract> {
let tokenContract = this._tokenContractsByAddress[tokenAddress];
if (!_.isUndefined(tokenContract)) {
diff --git a/src/index.ts b/src/index.ts
index 9133d1db5..81523953e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,6 +12,7 @@ export {
ContractEvent,
Token,
ExchangeEvents,
+ TokenEvents,
IndexedFilterValues,
SubscriptionOpts,
BlockParam,
@@ -22,6 +23,10 @@ export {
LogErrorContractEventArgs,
LogCancelContractEventArgs,
LogFillContractEventArgs,
+ ExchangeContractEventArgs,
+ TransferContractEventArgs,
+ ApprovalContractEventArgs,
+ TokenContractEventArgs,
ContractEventArgs,
Web3Provider,
} from './types';
diff --git a/src/types.ts b/src/types.ts
index b7ee9c946..21352648a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -122,6 +122,8 @@ export interface ExchangeContract extends ContractInstance {
}
export interface TokenContract extends ContractInstance {
+ Transfer: CreateContractEvent;
+ Approval: CreateContractEvent;
balanceOf: {
call: (address: string) => Promise<BigNumber.BigNumber>;
};
@@ -236,7 +238,19 @@ export interface LogErrorContractEventArgs {
errorId: BigNumber.BigNumber;
orderHash: string;
}
-export type ContractEventArgs = LogFillContractEventArgs|LogCancelContractEventArgs|LogErrorContractEventArgs;
+export type ExchangeContractEventArgs = LogFillContractEventArgs|LogCancelContractEventArgs|LogErrorContractEventArgs;
+export interface TransferContractEventArgs {
+ _from: string;
+ _to: string;
+ _value: BigNumber.BigNumber;
+}
+export interface ApprovalContractEventArgs {
+ _owner: string;
+ _spender: string;
+ _value: BigNumber.BigNumber;
+}
+export type TokenContractEventArgs = TransferContractEventArgs|ApprovalContractEventArgs;
+export type ContractEventArgs = ExchangeContractEventArgs|TokenContractEventArgs;
export type ContractEventArg = string|BigNumber.BigNumber;
export interface Order {
@@ -286,6 +300,12 @@ export const ExchangeEvents = strEnum([
]);
export type ExchangeEvents = keyof typeof ExchangeEvents;
+export const TokenEvents = strEnum([
+ 'Transfer',
+ 'Approval',
+]);
+export type TokenEvents = keyof typeof TokenEvents;
+
export interface IndexedFilterValues {
[index: string]: any;
}
diff --git a/src/utils/event_utils.ts b/src/utils/event_utils.ts
new file mode 100644
index 000000000..c9d725a42
--- /dev/null
+++ b/src/utils/event_utils.ts
@@ -0,0 +1,44 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import {EventCallback, ContractEventArg, ContractEvent, ContractEventObj, ContractEventEmitter} from '../types';
+import * as BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+
+export const eventUtils = {
+ /**
+ * Wrappes eventCallback function so that all the BigNumber arguments are wrapped in nwwer version of BigNumber
+ * @param eventCallback event callback function to be wrapped
+ * @return Wrapped event callback function
+ */
+ getBigNumberWrappingEventCallback(eventCallback: EventCallback): EventCallback {
+ const bignumberWrappingEventCallback = (err: Error, event: ContractEvent) => {
+ if (_.isNull(err)) {
+ const wrapIfBigNumber = (value: ContractEventArg): ContractEventArg => {
+ // HACK: The old version of BigNumber used by Web3@0.19.0 does not support the `isBigNumber`
+ // and checking for a BigNumber instance using `instanceof` does not work either. We therefore
+ // compare the constructor functions of the possible BigNumber instance and the BigNumber used by
+ // Web3.
+ const web3BigNumber = (Web3.prototype as any).BigNumber;
+ const isWeb3BigNumber = web3BigNumber.toString() === value.constructor.toString();
+ return isWeb3BigNumber ? new BigNumber(value) : value;
+ };
+ event.args = _.mapValues(event.args, wrapIfBigNumber);
+ }
+ eventCallback(err, event);
+ };
+ return bignumberWrappingEventCallback;
+ },
+ wrapEventEmitter(event: ContractEventObj): ContractEventEmitter {
+ const watch = (eventCallback: EventCallback) => {
+ const bignumberWrappingEventCallback = eventUtils.getBigNumberWrappingEventCallback(eventCallback);
+ event.watch(bignumberWrappingEventCallback);
+ };
+ const zeroExEvent = {
+ watch,
+ stopWatchingAsync: async () => {
+ await promisify(event.stopWatching, event)();
+ },
+ };
+ return zeroExEvent;
+ },
+};
diff --git a/test/token_wrapper_test.ts b/test/token_wrapper_test.ts
index a1c035672..4a20141db 100644
--- a/test/token_wrapper_test.ts
+++ b/test/token_wrapper_test.ts
@@ -5,8 +5,17 @@ import * as Web3 from 'web3';
import * as BigNumber from 'bignumber.js';
import promisify = require('es6-promisify');
import {web3Factory} from './utils/web3_factory';
-import {ZeroEx, ZeroExError, Token} from '../src';
+import {
+ ZeroEx,
+ ZeroExError,
+ Token,
+ SubscriptionOpts,
+ TokenEvents,
+ ContractEvent,
+ TransferContractEventArgs,
+} from '../src';
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {DoneCallback} from '../src/types';
chaiSetup.configure();
const expect = chai.expect;
@@ -231,4 +240,106 @@ describe('TokenWrapper', () => {
return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet);
});
});
+ describe('#subscribeAsync', () => {
+ const indexFilterValues = {};
+ const shouldCheckTransfer = false;
+ let tokenAddress: string;
+ const subscriptionOpts: SubscriptionOpts = {
+ fromBlock: 0,
+ toBlock: 'latest',
+ };
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(async () => {
+ await zeroEx.token.stopWatchingAllEventsAsync();
+ });
+ // Hack: Mocha does not allow a test to be both async and have a `done` callback
+ // Since we need to await the receipt of the event in the `subscribeAsync` callback,
+ // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then
+ // wrap the rest of the test in an async block
+ // Source: https://github.com/mochajs/mocha/issues/2407
+ it('Should receive the Transfer event when an order is filled', (done: DoneCallback) => {
+ (async () => {
+ const zeroExEvent = await zeroEx.token.subscribeAsync(
+ tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues);
+ zeroExEvent.watch((err: Error, event: ContractEvent) => {
+ expect(err).to.be.null();
+ expect(event).to.not.be.undefined();
+ expect(event.args as TransferContractEventArgs).to.be.deep.equal({
+ _from: coinbase,
+ _to: addressWithoutFunds,
+ _value: transferAmount,
+ });
+ done();
+ });
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })();
+ });
+ it('Should receive the Approval event when an order is cancelled', (done: DoneCallback) => {
+ (async () => {
+ const zeroExEvent = await zeroEx.token.subscribeAsync(
+ tokenAddress, TokenEvents.Approval, subscriptionOpts, indexFilterValues);
+ zeroExEvent.watch((err: Error, event: ContractEvent) => {
+ expect(err).to.be.null();
+ expect(event).to.not.be.undefined();
+ expect(event.args as TransferContractEventArgs).to.be.deep.equal({
+ _owner: coinbase,
+ _spender: addressWithoutFunds,
+ _value: allowanceAmount,
+ });
+ done();
+ });
+ await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
+ })();
+ });
+ it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
+ (async () => {
+ const eventSubscriptionToBeCancelled = await zeroEx.token.subscribeAsync(
+ tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues);
+ eventSubscriptionToBeCancelled.watch((err: Error, event: ContractEvent) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ });
+
+ const newProvider = web3Factory.getRpcProvider();
+ await zeroEx.setProviderAsync(newProvider);
+
+ const eventSubscriptionToStay = await zeroEx.token.subscribeAsync(
+ tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues);
+ eventSubscriptionToStay.watch((err: Error, event: ContractEvent) => {
+ expect(err).to.be.null();
+ expect(event).to.not.be.undefined();
+ done();
+ });
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })();
+ });
+ it('Should stop watch for events when stopWatchingAsync called on the eventEmitter', (done: DoneCallback) => {
+ (async () => {
+ const eventSubscriptionToBeStopped = await zeroEx.token.subscribeAsync(
+ tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues);
+ eventSubscriptionToBeStopped.watch((err: Error, event: ContractEvent) => {
+ done(new Error('Expected this subscription to have been stopped'));
+ });
+ await eventSubscriptionToBeStopped.stopWatchingAsync();
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ done();
+ })();
+ });
+ it('Should wrap all event args BigNumber instances in a newer version of BigNumber', (done: DoneCallback) => {
+ (async () => {
+ const zeroExEvent = await zeroEx.token.subscribeAsync(
+ tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues);
+ zeroExEvent.watch((err: Error, event: ContractEvent) => {
+ const args = event.args as TransferContractEventArgs;
+ expect(args._value.isBigNumber).to.be.true();
+ done();
+ });
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })();
+ });
+ });
});