Add initial implementation and tests for zeroEx.token.subscribeAsync

This commit is contained in:
Leonid Logvinov
2017-07-03 11:39:26 -07:00
parent c9edeae6d8
commit 5a8eb77ff0
7 changed files with 240 additions and 37 deletions

View File

@@ -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.

View File

@@ -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> {

View File

@@ -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)) {

View File

@@ -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';

View File

@@ -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;
}

44
src/utils/event_utils.ts Normal file
View File

@@ -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;
},
};

View File

@@ -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);
})();
});
});
});