diff options
-rw-r--r-- | src/0x.js.ts | 4 | ||||
-rw-r--r-- | src/artifacts/EtherToken.json | 2 | ||||
-rw-r--r-- | src/artifacts/Mintable.json | 2 | ||||
-rw-r--r-- | src/artifacts/Proxy.json | 2 | ||||
-rw-r--r-- | src/artifacts/Token.json | 2 | ||||
-rw-r--r-- | src/artifacts/TokenRegistry.json | 2 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 46 | ||||
-rw-r--r-- | src/types.ts | 45 | ||||
-rw-r--r-- | src/utils/utils.ts | 3 | ||||
-rw-r--r-- | test/0x.js_test.ts | 6 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 86 |
11 files changed, 187 insertions, 13 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts index 40290467a..850827fee 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -115,9 +115,9 @@ export class ZeroEx { /** * Sets a new provider for the web3 instance used by 0x.js */ - public setProvider(provider: Web3.Provider) { + public async setProviderAsync(provider: Web3.Provider) { this.web3Wrapper.setProvider(provider); - this.exchange.invalidateContractInstance(); + await this.exchange.invalidateContractInstanceAsync(); this.tokenRegistry.invalidateContractInstance(); this.token.invalidateContractInstances(); } diff --git a/src/artifacts/EtherToken.json b/src/artifacts/EtherToken.json index 0593cdc95..ae79d6bf5 100644 --- a/src/artifacts/EtherToken.json +++ b/src/artifacts/EtherToken.json @@ -336,4 +336,4 @@ }, "schema_version": "0.0.5", "updated_at": 1495042008609 -}
\ No newline at end of file +} diff --git a/src/artifacts/Mintable.json b/src/artifacts/Mintable.json index 026f294f2..b508eaee7 100644 --- a/src/artifacts/Mintable.json +++ b/src/artifacts/Mintable.json @@ -186,4 +186,4 @@ "networks": {}, "schema_version": "0.0.5", "updated_at": 1495030728122 -}
\ No newline at end of file +} diff --git a/src/artifacts/Proxy.json b/src/artifacts/Proxy.json index 0d6faef35..d02804096 100644 --- a/src/artifacts/Proxy.json +++ b/src/artifacts/Proxy.json @@ -264,4 +264,4 @@ }, "schema_version": "0.0.5", "updated_at": 1495042008598 -}
\ No newline at end of file +} diff --git a/src/artifacts/Token.json b/src/artifacts/Token.json index 3bbcead41..4843cc2ca 100644 --- a/src/artifacts/Token.json +++ b/src/artifacts/Token.json @@ -173,4 +173,4 @@ "networks": {}, "schema_version": "0.0.5", "updated_at": 1495030728125 -}
\ No newline at end of file +} diff --git a/src/artifacts/TokenRegistry.json b/src/artifacts/TokenRegistry.json index f9f4fafbc..6ecdfa079 100644 --- a/src/artifacts/TokenRegistry.json +++ b/src/artifacts/TokenRegistry.json @@ -999,4 +999,4 @@ }, "schema_version": "0.0.5", "updated_at": 1495042008599 -}
\ No newline at end of file +} diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index cb869b498..b67fd33ac 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; import * as BigNumber from 'bignumber.js'; +import promisify = require('es6-promisify'); import {Web3Wrapper} from '../web3_wrapper'; import { ECSignature, @@ -10,9 +11,17 @@ import { OrderAddresses, SignedOrder, ContractEvent, + ZeroExError, + ExchangeEvents, + SubscriptionOpts, + IndexFilterValues, + CreateContractEvent, + ContractEventObj, + EventCallback, ContractResponse, } from '../types'; import {assert} from '../utils/assert'; +import {utils} from '../utils/utils'; import {ContractWrapper} from './contract_wrapper'; import * as ExchangeArtifacts from '../artifacts/Exchange.json'; import {ecSignatureSchema} from '../schemas/ec_signature_schema'; @@ -31,12 +40,15 @@ export class ExchangeWrapper extends ContractWrapper { [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FILL_BALANCE_ALLOWANCE_ERROR, }; private exchangeContractIfExists?: ExchangeContract; + private exchangeLogEventObjs: ContractEventObj[]; private tokenWrapper: TokenWrapper; constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) { super(web3Wrapper); this.tokenWrapper = tokenWrapper; + this.exchangeLogEventObjs = []; } - public invalidateContractInstance(): void { + public async invalidateContractInstanceAsync(): Promise<void> { + await this.stopWatchingExchangeLogEventsAsync(); delete this.exchangeContractIfExists; } public async isValidSignatureAsync(dataHex: string, ecSignature: ECSignature, @@ -160,6 +172,38 @@ export class ExchangeWrapper extends ContractWrapper { ); this.throwErrorLogsAsErrors(response.logs); } + /** + * Subscribe to an event type emitted by the Exchange smart contract + */ + public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, + indexFilterValues: IndexFilterValues, callback: EventCallback) { + const exchangeContract = await this.getExchangeContractAsync(); + let createLogEvent: CreateContractEvent; + switch (eventName) { + case ExchangeEvents.LogFill: + createLogEvent = exchangeContract.LogFill; + break; + case ExchangeEvents.LogError: + createLogEvent = exchangeContract.LogError; + break; + case ExchangeEvents.LogCancel: + createLogEvent = exchangeContract.LogCancel; + break; + default: + utils.spawnSwitchErr('ExchangeEvents', eventName); + return; + } + + const logEventObj: ContractEventObj = createLogEvent(indexFilterValues, subscriptionOpts); + logEventObj.watch(callback); + this.exchangeLogEventObjs.push(logEventObj); + } + private async stopWatchingExchangeLogEventsAsync() { + for (const logEventObj of this.exchangeLogEventObjs) { + await promisify(logEventObj.stopWatching, logEventObj)(); + } + this.exchangeLogEventObjs = []; + } private async validateFillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, senderAddress: string): Promise<void> { diff --git a/src/types.ts b/src/types.ts index 46156b155..b5430a783 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,22 @@ export interface ExchangeContract { cancelled: { call: (orderHash: string) => BigNumber.BigNumber; }; +} + +export type EventCallbackAsync = (err: Error, event: ContractEvent) => Promise<void>; +export type EventCallbackSync = (err: Error, event: ContractEvent) => void; +export type EventCallback = EventCallbackSync|EventCallbackAsync; +export interface ContractEventObj { + watch: (eventWatch: EventCallback) => void; + stopWatching: () => void; +} +export type CreateContractEvent = (indexFilterValues: IndexFilterValues, + subscriptionOpts: SubscriptionOpts) => ContractEventObj; +export interface ExchangeContract { + isValidSignature: any; + LogFill: CreateContractEvent; + LogCancel: CreateContractEvent; + LogError: CreateContractEvent; ZRX: { call: () => Promise<string>; }; @@ -117,6 +133,13 @@ export interface ContractResponse { } export interface ContractEvent { + logIndex: number; + transactionIndex: number; + transactionHash: string; + blockHash: string; + blockNumber: number; + address: string; + type: string; event: string; args: any; } @@ -154,3 +177,25 @@ export interface TxOpts { from: string; gas?: number; } + +export interface TokenAddressBySymbol { + [symbol: string]: string; +} + +export const ExchangeEvents = strEnum([ + 'LogFill', + 'LogCancel', + 'LogError', +]); +export type ExchangeEvents = keyof typeof ExchangeEvents; + +export interface IndexFilterValues { + [index: string]: any; +} + +export interface SubscriptionOpts { + fromBlock: string|number; + toBlock: string|number; +} + +export type DoneCallback = (err?: Error) => void; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e6840a624..114b46f6c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -22,4 +22,7 @@ export const utils = { const isValid = /^0x[0-9A-F]{64}$/i.test(orderHashHex); return isValid; }, + spawnSwitchErr(name: string, value: any) { + return new Error(`Unexpected switch value: ${value} encountered for ${name}`); + }, }; diff --git a/test/0x.js_test.ts b/test/0x.js_test.ts index 9196d3df3..8686b42eb 100644 --- a/test/0x.js_test.ts +++ b/test/0x.js_test.ts @@ -8,7 +8,7 @@ import * as Sinon from 'sinon'; import {ZeroEx} from '../src/0x.js'; import {constants} from './utils/constants'; import {web3Factory} from './utils/web3_factory'; -import {Order} from '../src/types'; +import {Order, DoneCallback} from '../src/types'; chai.config.includeStack = true; chai.use(ChaiBigNumber()); @@ -17,7 +17,7 @@ const expect = chai.expect; describe('ZeroEx library', () => { describe('#setProvider', () => { - it('overrides the provider in the nested web3 instance and invalidates contractInstances', async () => { + it('overrides provider in nested web3s and invalidates contractInstances', async () => { const web3 = web3Factory.create(); const zeroEx = new ZeroEx(web3); // Instantiate the contract instances with the current provider @@ -29,7 +29,7 @@ describe('ZeroEx library', () => { const newProvider = web3Factory.getRpcProvider(); // Add property to newProvider so that we can differentiate it from old provider (newProvider as any).zeroExTestId = 1; - zeroEx.setProvider(newProvider); + await zeroEx.setProviderAsync(newProvider); // Check that contractInstances with old provider are removed after provider update expect((zeroEx.exchange as any).exchangeContractIfExists).to.be.undefined(); diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 0f683f811..a44d2728a 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -9,8 +9,15 @@ import promisify = require('es6-promisify'); import {web3Factory} from './utils/web3_factory'; import {ZeroEx} from '../src/0x.js'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; -import {orderFactory} from './utils/order_factory'; -import {Token, SignedOrder, ExchangeContractErrs} from '../src/types'; +import { + Token, + SignedOrder, + SubscriptionOpts, + ExchangeEvents, + ContractEvent, + DoneCallback, + ExchangeContractErrs, +} from '../src/types'; import {FillScenarios} from './utils/fill_scenarios'; import {TokenUtils} from './utils/token_utils'; @@ -126,6 +133,7 @@ describe('ExchangeWrapper', () => { const shouldCheckTransfer = false; before(async () => { [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await zeroEx.tokenRegistry.getTokensAsync(); const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); makerTokenAddress = makerToken.address; takerTokenAddress = takerToken.address; @@ -396,4 +404,78 @@ describe('ExchangeWrapper', () => { }); }); }); + describe('#subscribeAsync', () => { + const indexFilterValues = {}; + const shouldCheckTransfer = false; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let takerAddress: string; + let makerAddress: string; + let fillableAmount: BigNumber.BigNumber; + let signedOrder: SignedOrder; + before(() => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokens; + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + fillableAmount = new BigNumber(5); + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + }); + afterEach(async () => { + (zeroEx.exchange as any).stopWatchingExchangeLogEventsAsync(); + }); + // 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 LogFill event when an order is filled', (done: DoneCallback) => { + (async () => { + const subscriptionOpts: SubscriptionOpts = { + fromBlock: 0, + toBlock: 'latest', + }; + await zeroEx.exchange.subscribeAsync(ExchangeEvents.LogFill, subscriptionOpts, + indexFilterValues, (err: Error, event: ContractEvent) => { + expect(err).to.be.null(); + expect(event).to.not.be.undefined(); + done(); + }); + const fillTakerAmountInBaseUnits = new BigNumber(1); + zeroEx.setTransactionSenderAccount(takerAddress); + await zeroEx.exchange.fillOrderAsync(signedOrder, fillTakerAmountInBaseUnits, shouldCheckTransfer); + })(); + }); + it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => { + (async () => { + const subscriptionOpts: SubscriptionOpts = { + fromBlock: 0, + toBlock: 'latest', + }; + await zeroEx.exchange.subscribeAsync(ExchangeEvents.LogFill, subscriptionOpts, + indexFilterValues, (err: Error, event: ContractEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }); + + const newProvider = web3Factory.getRpcProvider(); + await zeroEx.setProviderAsync(newProvider); + + await zeroEx.exchange.subscribeAsync(ExchangeEvents.LogFill, subscriptionOpts, + indexFilterValues, (err: Error, event: ContractEvent) => { + expect(err).to.be.null(); + expect(event).to.not.be.undefined(); + done(); + }); + + const fillTakerAmountInBaseUnits = new BigNumber(1); + zeroEx.setTransactionSenderAccount(takerAddress); + await zeroEx.exchange.fillOrderAsync(signedOrder, fillTakerAmountInBaseUnits, shouldCheckTransfer); + })(); + }); + }); }); |