aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonid <logvinov.leon@gmail.com>2017-07-05 09:17:57 +0800
committerGitHub <noreply@github.com>2017-07-05 09:17:57 +0800
commit74b2308488832290340f3a6c6473ab7340510dfc (patch)
treebe598e0355a72486125cacfad837a2d06342f170
parent3302d18f6e0a4b7e51b318959c6b2d040ae3c5ed (diff)
parent371acc0ba12197de735dea20e09d50bbfd524118 (diff)
downloaddexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.tar
dexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.tar.gz
dexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.tar.bz2
dexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.tar.lz
dexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.tar.xz
dexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.tar.zst
dexon-sol-tools-74b2308488832290340f3a6c6473ab7340510dfc.zip
Merge pull request #90 from 0xProject/subscribe-token
Add implementation and tests for zeroEx.token.subscribeAsync
-rw-r--r--CHANGELOG.md7
-rw-r--r--src/0x.ts14
-rw-r--r--src/contract_wrappers/ether_token_wrapper.ts3
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts55
-rw-r--r--src/contract_wrappers/proxy_wrapper.ts6
-rw-r--r--src/contract_wrappers/token_registry_wrapper.ts6
-rw-r--r--src/contract_wrappers/token_wrapper.ts66
-rw-r--r--src/index.ts5
-rw-r--r--src/schemas/index_filter_values_schema.ts11
-rw-r--r--src/schemas/order_hash_schema.ts5
-rw-r--r--src/schemas/order_schemas.ts3
-rw-r--r--src/schemas/subscription_opts_schema.ts20
-rw-r--r--src/types.ts29
-rw-r--r--src/utils/assert.ts16
-rw-r--r--src/utils/event_utils.ts44
-rw-r--r--src/utils/schema_validator.ts7
-rw-r--r--src/utils/utils.ts4
-rw-r--r--test/exchange_wrapper_test.ts10
-rw-r--r--test/schema_test.ts59
-rw-r--r--test/token_wrapper_test.ts112
20 files changed, 410 insertions, 72 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5e41bc74..c98c2052a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ v0.8.0 - TBD
* Add the ability to call methods on different authorized versions of the Exchange smart contract (#82)
* Update contract artifacts to reflect latest changes to the smart contracts (0xproject/contracts#59)
* Add `zeroEx.proxy.isAuthorizedAsync` and `zeroEx.proxy.getAuthorizedAddressesAsync` (#89)
+ * Add `zeroEx.token.subscribeAsync` (#90)
+ * Make contract invalidation functions private (#90)
+ * `zeroEx.token.invalidateContractInstancesAsync`
+ * `zeroEx.exchange.invalidateContractInstancesAsync`
+ * `zeroEx.proxy.invalidateContractInstance`
+ * `zeroEx.tokenRegistry.invalidateContractInstance`
+ * Fix the bug where `zeroEx.setProviderAsync` didn't invalidate etherToken contract's instance
v0.7.1 - _Jun. 26, 2017_
------------------------
diff --git a/src/0x.ts b/src/0x.ts
index 938e61805..49bd31f2d 100644
--- a/src/0x.ts
+++ b/src/0x.ts
@@ -17,7 +17,9 @@ import {ecSignatureSchema} from './schemas/ec_signature_schema';
import {TokenWrapper} from './contract_wrappers/token_wrapper';
import {ProxyWrapper} from './contract_wrappers/proxy_wrapper';
import {ECSignature, ZeroExError, Order, SignedOrder, Web3Provider} from './types';
+import {orderHashSchema} from './schemas/order_hash_schema';
import {orderSchema} from './schemas/order_schemas';
+import {SchemaValidator} from './utils/schema_validator';
// Customize our BigNumber instances
bigNumberConfigs.configure();
@@ -110,7 +112,8 @@ export class ZeroEx {
// Since this method can be called to check if any arbitrary string conforms to an orderHash's
// format, we only assert that we were indeed passed a string.
assert.isString('orderHash', orderHash);
- const isValidOrderHash = utils.isValidOrderHash(orderHash);
+ const schemaValidator = new SchemaValidator();
+ const isValidOrderHash = schemaValidator.validate(orderHash, orderHashSchema).valid;
return isValidOrderHash;
}
/**
@@ -166,10 +169,11 @@ export class ZeroEx {
*/
public async setProviderAsync(provider: Web3Provider) {
this._web3Wrapper.setProvider(provider);
- await this.exchange.invalidateContractInstancesAsync();
- this.tokenRegistry.invalidateContractInstance();
- this.token.invalidateContractInstances();
- this.proxy.invalidateContractInstance();
+ await (this.exchange as any)._invalidateContractInstancesAsync();
+ (this.tokenRegistry as any)._invalidateContractInstance();
+ await (this.token as any)._invalidateContractInstancesAsync();
+ (this.proxy as any)._invalidateContractInstance();
+ (this.etherToken as any)._invalidateContractInstance();
}
/**
* Get user Ethereum addresses available through the supplied web3 instance available for sending transactions.
diff --git a/src/contract_wrappers/ether_token_wrapper.ts b/src/contract_wrappers/ether_token_wrapper.ts
index 76e7289b7..03d714bd7 100644
--- a/src/contract_wrappers/ether_token_wrapper.ts
+++ b/src/contract_wrappers/ether_token_wrapper.ts
@@ -64,6 +64,9 @@ export class EtherTokenWrapper extends ContractWrapper {
const wethContract = await this._getEtherTokenContractAsync();
return wethContract.address;
}
+ private _invalidateContractInstance(): void {
+ delete this._etherTokenContractIfExists;
+ }
private async _getEtherTokenContractAsync(): Promise<EtherTokenContract> {
if (!_.isUndefined(this._etherTokenContractIfExists)) {
return this._etherTokenContractIfExists;
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 6726f3eac..5a2da4a98 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -34,14 +34,18 @@ 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';
import {ecSignatureSchema} from '../schemas/ec_signature_schema';
import {signedOrdersSchema} from '../schemas/signed_orders_schema';
+import {subscriptionOptsSchema} from '../schemas/subscription_opts_schema';
+import {indexFilterValuesSchema} from '../schemas/index_filter_values_schema';
import {orderFillRequestsSchema} from '../schemas/order_fill_requests_schema';
import {orderCancellationRequestsSchema} from '../schemas/order_cancel_schema';
import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema';
+import {orderHashSchema} from '../schemas/order_hash_schema';
import {signedOrderSchema, orderSchema} from '../schemas/order_schemas';
import {constants} from '../utils/constants';
import {TokenWrapper} from './token_wrapper';
@@ -89,10 +93,6 @@ export class ExchangeWrapper extends ContractWrapper {
this._exchangeLogEventEmitters = [];
this._exchangeContractByAddress = {};
}
- public async invalidateContractInstancesAsync(): Promise<void> {
- await this.stopWatchingAllEventsAsync();
- this._exchangeContractByAddress = {};
- }
/**
* Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total
* amount that has been filled or cancelled. The remaining takerAmount can be calculated by
@@ -104,7 +104,7 @@ export class ExchangeWrapper extends ContractWrapper {
*/
public async getUnavailableTakerAmountAsync(orderHash: string,
exchangeContractAddress: string): Promise<BigNumber.BigNumber> {
- assert.isValidOrderHash('orderHash', orderHash);
+ assert.doesConformToSchema('orderHash', orderHash, orderHashSchema);
const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress);
let unavailableAmountInBaseUnits = await exchangeContract.getUnavailableValueT.call(orderHash);
@@ -120,7 +120,7 @@ export class ExchangeWrapper extends ContractWrapper {
*/
public async getFilledTakerAmountAsync(orderHash: string,
exchangeContractAddress: string): Promise<BigNumber.BigNumber> {
- assert.isValidOrderHash('orderHash', orderHash);
+ assert.doesConformToSchema('orderHash', orderHash, orderHashSchema);
const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress);
let fillAmountInBaseUnits = await exchangeContract.filled.call(orderHash);
@@ -137,7 +137,7 @@ export class ExchangeWrapper extends ContractWrapper {
*/
public async getCanceledTakerAmountAsync(orderHash: string,
exchangeContractAddress: string): Promise<BigNumber.BigNumber> {
- assert.isValidOrderHash('orderHash', orderHash);
+ assert.doesConformToSchema('orderHash', orderHash, orderHashSchema);
const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress);
let cancelledAmountInBaseUnits = await exchangeContract.cancelled.call(orderHash);
@@ -584,6 +584,10 @@ export class ExchangeWrapper extends ContractWrapper {
public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts,
indexFilterValues: IndexedFilterValues, exchangeContractAddress: string):
Promise<ContractEventEmitter> {
+ assert.isETHAddressHex('exchangeContractAddress', exchangeContractAddress);
+ assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
+ assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, subscriptionOptsSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, indexFilterValuesSchema);
const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress);
let createLogEvent: CreateContractEvent;
switch (eventName) {
@@ -601,7 +605,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;
}
@@ -651,41 +655,14 @@ export class ExchangeWrapper extends ContractWrapper {
await Promise.all(stopWatchingPromises);
this._exchangeLogEventEmitters = [];
}
+ private async _invalidateContractInstancesAsync(): Promise<void> {
+ await this.stopWatchingAllEventsAsync();
+ this._exchangeContractByAddress = {};
+ }
private async _isExchangeContractAddressProxyAuthorizedAsync(exchangeContractAddress: string): Promise<boolean> {
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/proxy_wrapper.ts b/src/contract_wrappers/proxy_wrapper.ts
index bdf163f35..05d4e142c 100644
--- a/src/contract_wrappers/proxy_wrapper.ts
+++ b/src/contract_wrappers/proxy_wrapper.ts
@@ -9,9 +9,6 @@ import {ProxyContract} from '../types';
*/
export class ProxyWrapper extends ContractWrapper {
private _proxyContractIfExists?: ProxyContract;
- public invalidateContractInstance(): void {
- delete this._proxyContractIfExists;
- }
/**
* Check if the Exchange contract address is authorized by the Proxy contract.
* @param exchangeContractAddress The hex encoded address of the Exchange contract to call.
@@ -32,6 +29,9 @@ export class ProxyWrapper extends ContractWrapper {
const authorizedAddresses = await proxyContractInstance.getAuthorizedAddresses.call();
return authorizedAddresses;
}
+ private _invalidateContractInstance(): void {
+ delete this._proxyContractIfExists;
+ }
private async _getProxyContractAsync(): Promise<ProxyContract> {
if (!_.isUndefined(this._proxyContractIfExists)) {
return this._proxyContractIfExists;
diff --git a/src/contract_wrappers/token_registry_wrapper.ts b/src/contract_wrappers/token_registry_wrapper.ts
index 3e87e4852..c9f21e46f 100644
--- a/src/contract_wrappers/token_registry_wrapper.ts
+++ b/src/contract_wrappers/token_registry_wrapper.ts
@@ -13,9 +13,6 @@ export class TokenRegistryWrapper extends ContractWrapper {
constructor(web3Wrapper: Web3Wrapper) {
super(web3Wrapper);
}
- public invalidateContractInstance(): void {
- delete this._tokenRegistryContractIfExists;
- }
/**
* Retrieves all the tokens currently listed in the Token Registry smart contract
* @return An array of objects that conform to the Token interface.
@@ -40,6 +37,9 @@ export class TokenRegistryWrapper extends ContractWrapper {
});
return tokens;
}
+ private _invalidateContractInstance(): void {
+ delete this._tokenRegistryContractIfExists;
+ }
private async _getTokenRegistryContractAsync(): Promise<TokenRegistryContract> {
if (!_.isUndefined(this._tokenRegistryContractIfExists)) {
return this._tokenRegistryContractIfExists;
diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts
index e34c624ab..fdf711823 100644
--- a/src/contract_wrappers/token_wrapper.ts
+++ b/src/contract_wrappers/token_wrapper.ts
@@ -2,11 +2,24 @@ 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 {subscriptionOptsSchema} from '../schemas/subscription_opts_schema';
+import {indexFilterValuesSchema} from '../schemas/index_filter_values_schema';
+import {
+ TokenContract,
+ ZeroExError,
+ TokenEvents,
+ IndexedFilterValues,
+ SubscriptionOpts,
+ CreateContractEvent,
+ ContractEventEmitter,
+ ContractEventObj,
+} from '../types';
const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 45730;
@@ -17,12 +30,11 @@ 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 = {};
- }
- public invalidateContractInstances() {
- this._tokenContractsByAddress = {};
+ this._tokenLogEventEmitters = [];
}
/**
* Retrieves an owner's ERC20 token balance.
@@ -178,6 +190,52 @@ export class TokenWrapper extends ContractWrapper {
from: senderAddress,
});
}
+ /**
+ * Subscribe to an event type emitted by the Token contract.
+ * @param tokenAddress The hex encoded 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> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
+ assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, subscriptionOptsSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, indexFilterValuesSchema);
+ 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 _invalidateContractInstancesAsync(): Promise<void> {
+ await this.stopWatchingAllEventsAsync();
+ this._tokenContractsByAddress = {};
+ }
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/schemas/index_filter_values_schema.ts b/src/schemas/index_filter_values_schema.ts
new file mode 100644
index 000000000..7c8d3f943
--- /dev/null
+++ b/src/schemas/index_filter_values_schema.ts
@@ -0,0 +1,11 @@
+export const indexFilterValuesSchema = {
+ id: '/indexFilterValues',
+ additionalProperties: {
+ oneOf: [
+ {$ref: '/numberSchema'},
+ {$ref: '/addressSchema'},
+ {$ref: '/orderHashSchema'},
+ ],
+ },
+ type: 'object',
+};
diff --git a/src/schemas/order_hash_schema.ts b/src/schemas/order_hash_schema.ts
new file mode 100644
index 000000000..9773a88f9
--- /dev/null
+++ b/src/schemas/order_hash_schema.ts
@@ -0,0 +1,5 @@
+export const orderHashSchema = {
+ id: '/orderHashSchema',
+ type: 'string',
+ pattern: '^0x[0-9a-fA-F]{64}$',
+};
diff --git a/src/schemas/order_schemas.ts b/src/schemas/order_schemas.ts
index 133736b3d..c346687b5 100644
--- a/src/schemas/order_schemas.ts
+++ b/src/schemas/order_schemas.ts
@@ -16,10 +16,11 @@ export const orderSchema = {
salt: {$ref: '/numberSchema'},
feeRecipient: {$ref: '/addressSchema'},
expirationUnixTimestampSec: {$ref: '/numberSchema'},
+ exchangeContractAddress: {$ref: '/addressSchema'},
},
required: [
'maker', 'taker', 'makerFee', 'takerFee', 'makerTokenAmount', 'takerTokenAmount',
- 'salt', 'feeRecipient', 'expirationUnixTimestampSec',
+ 'salt', 'feeRecipient', 'expirationUnixTimestampSec', 'exchangeContractAddress',
],
type: 'object',
};
diff --git a/src/schemas/subscription_opts_schema.ts b/src/schemas/subscription_opts_schema.ts
new file mode 100644
index 000000000..0bb44fecf
--- /dev/null
+++ b/src/schemas/subscription_opts_schema.ts
@@ -0,0 +1,20 @@
+export const blockParamSchema = {
+ id: '/blockParam',
+ oneOf: [
+ {
+ type: 'number',
+ },
+ {
+ enum: ['latest', 'earliest', 'pending'],
+ },
+ ],
+};
+
+export const subscriptionOptsSchema = {
+ id: '/subscriptionOpts',
+ properties: {
+ fromBlock: {$ref: '/blockParam'},
+ toBlock: {$ref: '/blockParam'},
+ },
+ type: 'object',
+};
diff --git a/src/types.ts b/src/types.ts
index 8047f4536..66881e170 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -3,7 +3,10 @@ import * as Web3 from 'web3';
// Utility function to create a K:V from a list of strings
// Adapted from: https://basarat.gitbooks.io/typescript/content/docs/types/literal-types.html
-function strEnum(values: string[]): {[key: string]: string} {
+export interface StringEnum {
+ [key: string]: string;
+}
+function strEnum(values: string[]): StringEnum {
return _.reduce(values, (result, key) => {
result[key] = key;
return result;
@@ -122,6 +125,8 @@ export interface ExchangeContract extends ContractInstance {
}
export interface TokenContract extends ContractInstance {
+ Transfer: CreateContractEvent;
+ Approval: CreateContractEvent;
balanceOf: {
call: (address: string) => Promise<BigNumber.BigNumber>;
};
@@ -239,7 +244,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 {
@@ -289,8 +306,14 @@ 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;
+ [index: string]: ContractEventArg;
}
export type BlockParam = 'latest'|'earliest'|'pending'|number;
diff --git a/src/utils/assert.ts b/src/utils/assert.ts
index 38c1d4aae..b3c30c11d 100644
--- a/src/utils/assert.ts
+++ b/src/utils/assert.ts
@@ -5,6 +5,7 @@ import {Web3Wrapper} from '../web3_wrapper';
import {Schema} from 'jsonschema';
import {SchemaValidator} from './schema_validator';
import {utils} from './utils';
+import {StringEnum} from '../types';
const HEX_REGEX = /^0x[0-9A-F]*$/i;
@@ -27,6 +28,16 @@ export const assert = {
const web3 = new Web3();
this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value));
},
+ doesBelongToStringEnum(variableName: string, value: string, stringEnum: StringEnum): void {
+ const doesBelongToStringEnum = !_.isUndefined(stringEnum[value]);
+ const enumValues = _.keys(stringEnum);
+ const enumValuesAsStrings = _.map(enumValues, enumValue => `'${enumValue}'`);
+ const enumValuesAsString = enumValuesAsStrings.join(', ');
+ assert.assert(
+ doesBelongToStringEnum,
+ `Expected ${variableName} to be one of: ${enumValuesAsString}, encountered: ${value}`,
+ );
+ },
async isSenderAddressAsync(variableName: string, senderAddressHex: string,
web3Wrapper: Web3Wrapper): Promise<void> {
assert.isETHAddressHex(variableName, senderAddressHex);
@@ -45,13 +56,10 @@ export const assert = {
isNumber(variableName: string, value: number): void {
this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value));
},
- isValidOrderHash(variableName: string, value: string): void {
- this.assert(utils.isValidOrderHash(value), this.typeAssertionMessage(variableName, 'orderHash', value));
- },
isBoolean(variableName: string, value: boolean): void {
this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value));
},
- doesConformToSchema(variableName: string, value: object, schema: Schema): void {
+ doesConformToSchema(variableName: string, value: any, schema: Schema): void {
const schemaValidator = new SchemaValidator();
const validationResult = schemaValidator.validate(value, schema);
const hasValidationErrors = validationResult.errors.length > 0;
diff --git a/src/utils/event_utils.ts b/src/utils/event_utils.ts
new file mode 100644
index 000000000..07418cbc4
--- /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 = {
+ 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;
+ },
+ /**
+ * Wraps eventCallback function so that all the BigNumber arguments are wrapped in a newer 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;
+ },
+};
diff --git a/src/utils/schema_validator.ts b/src/utils/schema_validator.ts
index e3f911adb..58450ff20 100644
--- a/src/utils/schema_validator.ts
+++ b/src/utils/schema_validator.ts
@@ -1,8 +1,11 @@
import {Validator, ValidatorResult, Schema} from 'jsonschema';
import {ecSignatureSchema, ecSignatureParameterSchema} from '../schemas/ec_signature_schema';
+import {orderHashSchema} from '../schemas/order_hash_schema';
import {orderSchema, signedOrderSchema} from '../schemas/order_schemas';
import {addressSchema, numberSchema} from '../schemas/basic_type_schemas';
import {tokenSchema} from '../schemas/token_schema';
+import {subscriptionOptsSchema, blockParamSchema} from '../schemas/subscription_opts_schema';
+import {indexFilterValuesSchema} from '../schemas/index_filter_values_schema';
import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema';
export class SchemaValidator {
@@ -13,8 +16,12 @@ export class SchemaValidator {
this.validator.addSchema(orderSchema, orderSchema.id);
this.validator.addSchema(numberSchema, numberSchema.id);
this.validator.addSchema(addressSchema, addressSchema.id);
+ this.validator.addSchema(orderHashSchema, orderHashSchema.id);
+ this.validator.addSchema(blockParamSchema, blockParamSchema.id);
this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id);
this.validator.addSchema(signedOrderSchema, signedOrderSchema.id);
+ this.validator.addSchema(subscriptionOptsSchema, subscriptionOptsSchema.id);
+ this.validator.addSchema(indexFilterValuesSchema, indexFilterValuesSchema.id);
this.validator.addSchema(ecSignatureParameterSchema, ecSignatureParameterSchema.id);
this.validator.addSchema(orderFillOrKillRequestsSchema, orderFillOrKillRequestsSchema.id);
}
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 061e9f99a..ecc171bfe 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -25,10 +25,6 @@ export const utils = {
isTestRpc(nodeVersion: string): boolean {
return _.includes(nodeVersion, 'TestRPC');
},
- isValidOrderHash(orderHashHex: string): boolean {
- const isValid = /^0x[0-9A-F]{64}$/i.test(orderHashHex);
- return isValid;
- },
spawnSwitchErr(name: string, value: any): Error {
return new Error(`Unexpected switch value: ${value} encountered for ${name}`);
},
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index 3a88db5c9..0321eb569 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -721,7 +721,7 @@ describe('ExchangeWrapper', () => {
await zeroEx.exchange.fillOrderAsync(
signedOrder, fillTakerAmountInBaseUnits, shouldCheckTransfer, takerAddress,
);
- })();
+ })().catch(done);
});
it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => {
(async () => {
@@ -735,7 +735,7 @@ describe('ExchangeWrapper', () => {
done();
});
await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits);
- })();
+ })().catch(done);
});
it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
(async () => {
@@ -761,7 +761,7 @@ describe('ExchangeWrapper', () => {
await zeroEx.exchange.fillOrderAsync(
signedOrder, fillTakerAmountInBaseUnits, shouldCheckTransfer, takerAddress,
);
- })();
+ })().catch(done);
});
it('Should stop watch for events when stopWatchingAsync called on the eventEmitter', (done: DoneCallback) => {
(async () => {
@@ -776,7 +776,7 @@ describe('ExchangeWrapper', () => {
signedOrder, fillTakerAmountInBaseUnits, shouldCheckTransfer, takerAddress,
);
done();
- })();
+ })().catch(done);
});
it('Should wrap all event args BigNumber instances in a newer version of BigNumber', (done: DoneCallback) => {
(async () => {
@@ -794,7 +794,7 @@ describe('ExchangeWrapper', () => {
await zeroEx.exchange.fillOrderAsync(
signedOrder, fillTakerAmountInBaseUnits, shouldCheckTransfer, takerAddress,
);
- })();
+ })().catch(done);
});
});
describe('#getOrderHashHexUsingContractCallAsync', () => {
diff --git a/test/schema_test.ts b/test/schema_test.ts
index b251a68f9..c170bebb1 100644
--- a/test/schema_test.ts
+++ b/test/schema_test.ts
@@ -6,12 +6,14 @@ import promisify = require('es6-promisify');
import {constants} from './utils/constants';
import {SchemaValidator} from '../src/utils/schema_validator';
import {tokenSchema} from '../src/schemas/token_schema';
+import {orderHashSchema} from '../src/schemas/order_hash_schema';
import {orderSchema, signedOrderSchema} from '../src/schemas/order_schemas';
import {addressSchema, numberSchema} from '../src/schemas/basic_type_schemas';
import {orderFillOrKillRequestsSchema} from '../src/schemas/order_fill_or_kill_requests_schema';
import {ecSignatureParameterSchema, ecSignatureSchema} from '../src/schemas/ec_signature_schema';
import {orderCancellationRequestsSchema} from '../src/schemas/order_cancel_schema';
import {orderFillRequestsSchema} from '../src/schemas/order_fill_requests_schema';
+import {blockParamSchema, subscriptionOptsSchema} from '../src/schemas/subscription_opts_schema';
chai.config.includeStack = true;
const expect = chai.expect;
@@ -96,6 +98,62 @@ describe('Schema', () => {
validateAgainstSchema(testCases, ecSignatureSchema, shouldFail);
});
});
+ describe('#orderHashSchema', () => {
+ it('should validate valid order hash', () => {
+ const testCases = [
+ '0x61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33',
+ '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ ];
+ validateAgainstSchema(testCases, orderHashSchema);
+ });
+ it('should fail for invalid order hash', () => {
+ const testCases = [
+ {},
+ '0x',
+ '0x8b0292B11a196601eD2ce54B665CaFEca0347D42',
+ '61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33',
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, orderHashSchema, shouldFail);
+ });
+ });
+ describe('#blockParamSchema', () => {
+ it('should validate valid block param', () => {
+ const testCases = [
+ 42,
+ 'latest',
+ 'pending',
+ 'earliest',
+ ];
+ validateAgainstSchema(testCases, blockParamSchema);
+ });
+ it('should fail for invalid block param', () => {
+ const testCases = [
+ {},
+ '42',
+ 'pemding',
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, blockParamSchema, shouldFail);
+ });
+ });
+ describe('#subscriptionOptsSchema', () => {
+ it('should validate valid subscription opts', () => {
+ const testCases = [
+ {fromBlock: 42, toBlock: 'latest'},
+ {fromBlock: 42},
+ {},
+ ];
+ validateAgainstSchema(testCases, subscriptionOptsSchema);
+ });
+ it('should fail for invalid subscription opts', () => {
+ const testCases = [
+ {fromBlock: '42'},
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, subscriptionOptsSchema, shouldFail);
+ });
+ });
describe('#tokenSchema', () => {
const token = {
name: 'Zero Ex',
@@ -143,6 +201,7 @@ describe('Schema', () => {
takerTokenAddress: constants.NULL_ADDRESS,
salt: '256',
feeRecipient: constants.NULL_ADDRESS,
+ exchangeContractAddress: constants.NULL_ADDRESS,
expirationUnixTimestampSec: '42',
};
describe('#orderSchema', () => {
diff --git a/test/token_wrapper_test.ts b/test/token_wrapper_test.ts
index a1c035672..06e373bfa 100644
--- a/test/token_wrapper_test.ts
+++ b/test/token_wrapper_test.ts
@@ -5,8 +5,18 @@ 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,
+ ApprovalContractEventArgs,
+} from '../src';
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {DoneCallback} from '../src/types';
chaiSetup.configure();
const expect = chai.expect;
@@ -231,4 +241,104 @@ 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();
+ const args = event.args as TransferContractEventArgs;
+ expect(args._from).to.be.equal(coinbase);
+ expect(args._to).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(transferAmount);
+ done();
+ });
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ 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();
+ const args = event.args as ApprovalContractEventArgs;
+ expect(args._owner).to.be.equal(coinbase);
+ expect(args._spender).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(allowanceAmount);
+ done();
+ });
+ await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
+ })().catch(done);
+ });
+ 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);
+ })().catch(done);
+ });
+ 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();
+ })().catch(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);
+ })().catch(done);
+ });
+ });
});