Merge pull request #90 from 0xProject/subscribe-token
Add implementation and tests for zeroEx.token.subscribeAsync
This commit is contained in:
@@ -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_
|
||||
------------------------
|
||||
|
||||
14
src/0x.ts
14
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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
11
src/schemas/index_filter_values_schema.ts
Normal file
11
src/schemas/index_filter_values_schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const indexFilterValuesSchema = {
|
||||
id: '/indexFilterValues',
|
||||
additionalProperties: {
|
||||
oneOf: [
|
||||
{$ref: '/numberSchema'},
|
||||
{$ref: '/addressSchema'},
|
||||
{$ref: '/orderHashSchema'},
|
||||
],
|
||||
},
|
||||
type: 'object',
|
||||
};
|
||||
5
src/schemas/order_hash_schema.ts
Normal file
5
src/schemas/order_hash_schema.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const orderHashSchema = {
|
||||
id: '/orderHashSchema',
|
||||
type: 'string',
|
||||
pattern: '^0x[0-9a-fA-F]{64}$',
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
20
src/schemas/subscription_opts_schema.ts
Normal file
20
src/schemas/subscription_opts_schema.ts
Normal 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',
|
||||
};
|
||||
29
src/types.ts
29
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;
|
||||
|
||||
@@ -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
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 = {
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user