Merge pull request #90 from 0xProject/subscribe-token

Add implementation and tests for zeroEx.token.subscribeAsync
This commit is contained in:
Leonid
2017-07-04 18:17:57 -07:00
committed by GitHub
20 changed files with 410 additions and 72 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,11 @@
export const indexFilterValuesSchema = {
id: '/indexFilterValues',
additionalProperties: {
oneOf: [
{$ref: '/numberSchema'},
{$ref: '/addressSchema'},
{$ref: '/orderHashSchema'},
],
},
type: 'object',
};

View File

@@ -0,0 +1,5 @@
export const orderHashSchema = {
id: '/orderHashSchema',
type: 'string',
pattern: '^0x[0-9a-fA-F]{64}$',
};

View File

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

View File

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

View File

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

View File

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

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 = {
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;
},
};

View File

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

View File

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

View File

@@ -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', () => {

View File

@@ -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', () => {

View File

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