Add initial implementation and tests for zeroEx.token.subscribeAsync
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
22
src/types.ts
22
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;
|
||||
}
|
||||
|
||||
44
src/utils/event_utils.ts
Normal file
44
src/utils/event_utils.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
})();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user