From de0e436aad5d6b1c8a9425f075dbc3074b2fe31f Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 26 Jun 2017 19:33:49 -0700 Subject: Allow multiple exchange versions t be functional at the same time --- src/0x.ts | 5 +- src/contract_wrappers/exchange_wrapper.ts | 155 +++++++++++++++++++++--------- src/exchange_artifacts_by_name.ts | 6 ++ src/types.ts | 13 +++ 4 files changed, 127 insertions(+), 52 deletions(-) create mode 100644 src/exchange_artifacts_by_name.ts (limited to 'src') diff --git a/src/0x.ts b/src/0x.ts index d7a01ba70..472a55102 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -16,7 +16,6 @@ import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper'; import {ecSignatureSchema} from './schemas/ec_signature_schema'; import {TokenWrapper} from './contract_wrappers/token_wrapper'; import {ECSignature, ZeroExError, Order, SignedOrder, Web3Provider} from './types'; -import * as ExchangeArtifacts from './artifacts/Exchange.json'; import {orderSchema} from './schemas/order_schemas'; // Customize our BigNumber instances @@ -179,9 +178,7 @@ export class ZeroEx { */ public async getOrderHashHexAsync(order: Order|SignedOrder): Promise { assert.doesConformToSchema('order', order, orderSchema); - - const exchangeContractAddr = await this._getExchangeAddressAsync(); - const orderHashHex = utils.getOrderHashHex(order, exchangeContractAddr); + const orderHashHex = utils.getOrderHashHex(order, order.exchangeContractAddress); return orderHashHex; } /** diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 622b937c2..6138d4ce9 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -7,6 +7,7 @@ import { ExchangeContract, ExchangeContractErrCodes, ExchangeContractErrs, + ZeroExError, OrderValues, OrderAddresses, Order, @@ -25,11 +26,13 @@ import { LogErrorContractEventArgs, LogFillContractEventArgs, LogCancelContractEventArgs, + ExchangeContractByAddress, + ContractArtifact, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; import {ContractWrapper} from './contract_wrapper'; -import * as ExchangeArtifacts from '../artifacts/exchange/Exchange.json'; +import {ExchangeArtifactsByName} from '../exchange_artifacts_by_name'; import {ecSignatureSchema} from '../schemas/ec_signature_schema'; import {signedOrdersSchema} from '../schemas/signed_orders_schema'; import {orderFillRequestsSchema} from '../schemas/order_fill_requests_schema'; @@ -53,7 +56,7 @@ export class ExchangeWrapper extends ContractWrapper { [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR, [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FILL_BALANCE_ALLOWANCE_ERROR, }; - private _exchangeContractIfExists?: ExchangeContract; + private _exchangeContractByAddress: ExchangeContractByAddress; private _exchangeLogEventEmitters: ContractEventEmitter[]; private _tokenWrapper: TokenWrapper; private static _getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] { @@ -78,23 +81,26 @@ export class ExchangeWrapper extends ContractWrapper { super(web3Wrapper); this._tokenWrapper = tokenWrapper; this._exchangeLogEventEmitters = []; + this._exchangeContractByAddress = {}; } public async invalidateContractInstanceAsync(): Promise { await this.stopWatchingAllEventsAsync(); - delete this._exchangeContractIfExists; + 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 * subtracting the unavailable amount from the total order takerAmount. - * @param orderHash The hex encoded orderHash for which you would like to retrieve the - * unavailable takerAmount. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the + * unavailable takerAmount. + * @param exchangeContractAddress The hex encoded address of the Exchange contract to use. * @return The amount of the order (in taker tokens) that has either been filled or canceled. */ - public async getUnavailableTakerAmountAsync(orderHash: string): Promise { + public async getUnavailableTakerAmountAsync(orderHash: string, + exchangeContractAddress: string): Promise { assert.isValidOrderHash('orderHash', orderHash); - const exchangeContract = await this._getExchangeContractAsync(); + const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress); let unavailableAmountInBaseUnits = await exchangeContract.getUnavailableValueT.call(orderHash); // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber unavailableAmountInBaseUnits = new BigNumber(unavailableAmountInBaseUnits); @@ -103,12 +109,14 @@ export class ExchangeWrapper extends ContractWrapper { /** * Retrieve the takerAmount of an order that has already been filled. * @param orderHash The hex encoded orderHash for which you would like to retrieve the filled takerAmount. + * @param exchangeContractAddress The hex encoded address of the Exchange contract to use. * @return The amount of the order (in taker tokens) that has already been filled. */ - public async getFilledTakerAmountAsync(orderHash: string): Promise { + public async getFilledTakerAmountAsync(orderHash: string, + exchangeContractAddress: string): Promise { assert.isValidOrderHash('orderHash', orderHash); - const exchangeContract = await this._getExchangeContractAsync(); + const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress); let fillAmountInBaseUnits = await exchangeContract.filled.call(orderHash); // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits); @@ -118,12 +126,14 @@ export class ExchangeWrapper extends ContractWrapper { * Retrieve the takerAmount of an order that has been cancelled. * @param orderHash The hex encoded orderHash for which you would like to retrieve the * cancelled takerAmount. + * @param exchangeContractAddress The hex encoded address of the Exchange contract to use. * @return The amount of the order (in taker tokens) that has been cancelled. */ - public async getCanceledTakerAmountAsync(orderHash: string): Promise { + public async getCanceledTakerAmountAsync(orderHash: string, + exchangeContractAddress: string): Promise { assert.isValidOrderHash('orderHash', orderHash); - const exchangeContract = await this._getExchangeContractAsync(); + const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress); let cancelledAmountInBaseUnits = await exchangeContract.cancelled.call(orderHash); // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits); @@ -152,7 +162,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(signedOrder.exchangeContractAddress); await this._validateFillOrderAndThrowIfInvalidAsync(signedOrder, takerTokenFillAmount, takerAddress); const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); @@ -207,7 +217,10 @@ export class ExchangeWrapper extends ContractWrapper { shouldCheckTransfer: boolean, takerAddress: string): Promise { const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress); assert.hasAtMostOneUniqueValue(takerTokenAddresses, - ExchangeContractErrs.MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO_DISALLOWED); + ExchangeContractErrs.MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO_DISALLOWED); + const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BATCH_ORDERS_MUST_HAVE_SAME_EXCHANGE_ADDRESS); assert.isBigNumber('takerTokenFillAmount', takerTokenFillAmount); assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); assert.doesConformToSchema('signedOrders', signedOrders, signedOrdersSchema); @@ -233,7 +246,7 @@ export class ExchangeWrapper extends ContractWrapper { orderAddressesValuesAndSignatureArray, ); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddresses[0]); const gas = await exchangeInstance.fillUpTo.estimateGas( orderAddressesArray, orderValuesArray, @@ -282,6 +295,12 @@ export class ExchangeWrapper extends ContractWrapper { @decorators.contractCallErrorHandler public async batchFillOrderAsync(orderFillRequests: OrderFillRequest[], shouldCheckTransfer: boolean, takerAddress: string): Promise { + const exchangeContractAddresses = _.map( + orderFillRequests, + orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BATCH_ORDERS_MUST_HAVE_SAME_EXCHANGE_ADDRESS); assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); assert.doesConformToSchema('orderFillRequests', orderFillRequests, orderFillRequestsSchema); @@ -307,7 +326,7 @@ export class ExchangeWrapper extends ContractWrapper { orderAddressesValuesAmountsAndSignatureArray, ); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddresses[0]); const gas = await exchangeInstance.batchFill.estimateGas( orderAddressesArray, orderValuesArray, @@ -351,7 +370,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBigNumber('takerTokenFillAmount', takerTokenFillAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(signedOrder.exchangeContractAddress); await this._validateFillOrderAndThrowIfInvalidAsync(signedOrder, takerTokenFillAmount, takerAddress); await this._validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder, exchangeInstance.address, @@ -394,9 +413,18 @@ export class ExchangeWrapper extends ContractWrapper { @decorators.contractCallErrorHandler public async batchFillOrKillAsync(orderFillOrKillRequests: OrderFillOrKillRequest[], takerAddress: string): Promise { + const exchangeContractAddresses = _.map( + orderFillOrKillRequests, + orderFillOrKillRequest => orderFillOrKillRequest.signedOrder.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BATCH_ORDERS_MUST_HAVE_SAME_EXCHANGE_ADDRESS); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); assert.doesConformToSchema('orderFillOrKillRequests', orderFillOrKillRequests, orderFillOrKillRequestsSchema); - const exchangeInstance = await this._getExchangeContractAsync(); + if (_.isEmpty(orderFillOrKillRequests)) { + return; // no-op + } + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddresses[0]); for (const request of orderFillOrKillRequests) { await this._validateFillOrKillOrderAndThrowIfInvalidAsync(request.signedOrder, exchangeInstance.address, request.fillTakerAmount); @@ -455,7 +483,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.isBigNumber('takerTokenCancelAmount', takerTokenCancelAmount); await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(order.exchangeContractAddress); await this._validateCancelOrderAndThrowIfInvalidAsync(order, takerTokenCancelAmount); const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); @@ -489,6 +517,12 @@ export class ExchangeWrapper extends ContractWrapper { */ @decorators.contractCallErrorHandler public async batchCancelOrderAsync(orderCancellationRequests: OrderCancellationRequest[]): Promise { + const exchangeContractAddresses = _.map( + orderCancellationRequests, + orderCancellationRequest => orderCancellationRequest.order.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BATCH_ORDERS_MUST_HAVE_SAME_EXCHANGE_ADDRESS); const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker); assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH_DISALLOWED); const maker = makers[0]; @@ -503,7 +537,7 @@ export class ExchangeWrapper extends ContractWrapper { if (_.isEmpty(orderCancellationRequests)) { return; // no-op } - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddresses[0]); const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => { return [ ...ExchangeWrapper._getOrderAddressesAndValues(cancellationRequest.order), @@ -534,16 +568,17 @@ export class ExchangeWrapper extends ContractWrapper { } /** * Subscribe to an event type emitted by the Exchange smart contract - * @param eventName The exchange 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}` + * @param eventName The exchange 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}` + * @param exchangeContractAddress The hex encoded address of the Exchange contract to use. * @return ContractEventEmitter object */ public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, - indexFilterValues: IndexedFilterValues): + indexFilterValues: IndexedFilterValues, exchangeContractAddress: string): Promise { - const exchangeContract = await this._getExchangeContractAsync(); + const exchangeContract = await this._getExchangeContractAsync(exchangeContractAddress); let createLogEvent: CreateContractEvent; switch (eventName) { case ExchangeEvents.LogFill: @@ -565,13 +600,21 @@ export class ExchangeWrapper extends ContractWrapper { return eventEmitter; } /** - * Returns the ethereum address of the current exchange contract + * Returns the ethereum addresses of all available exchange contracts * on the network that the provided web3 instance is connected to - * @return The ethereum address of the current exchange contract. + * @return The ethereum addresses of all available exchange contract. */ - public async getContractAddressAsync(): Promise { - const exchangeContract = await this._getExchangeContractAsync(); - return exchangeContract.address; + public async getAvailableContractAddressedAsync(): Promise { + const networkId = await this._web3Wrapper.getNetworkIdIfExistsAsync(); + if (_.isUndefined(networkId)) { + return []; + } else { + const exchangeAddresses = _.map( + _.values(ExchangeArtifactsByName), + exchangeArfifact => exchangeArfifact.networks[networkId].address, + ); + return exchangeAddresses; + } } /** * Stops watching for all exchange events @@ -592,12 +635,13 @@ export class ExchangeWrapper extends ContractWrapper { return zeroExEvent; } private async _isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature, - signerAddressHex: string): Promise { + signerAddressHex: string, + exchangeContractAddress: string): Promise { assert.isHexString('dataHex', dataHex); assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema); assert.isETHAddressHex('signerAddressHex', signerAddressHex); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddress); const isValidSignature = await exchangeInstance.isValidSignature.call( signerAddressHex, @@ -609,12 +653,12 @@ export class ExchangeWrapper extends ContractWrapper { return isValidSignature; } private async _getOrderHashHexAsync(order: Order|SignedOrder): Promise { - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(order.exchangeContractAddress); const orderHashHex = utils.getOrderHashHex(order, exchangeInstance.address); return orderHashHex; } private async _getOrderHashHexUsingContractCallAsync(order: Order|SignedOrder): Promise { - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(order.exchangeContractAddress); const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); const orderHashHex = await exchangeInstance.getOrderHash.call(orderAddresses, orderValues); return orderHashHex; @@ -632,12 +676,13 @@ export class ExchangeWrapper extends ContractWrapper { if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.ORDER_FILL_EXPIRED); } - const zrxTokenAddress = await this._getZRXTokenAddressAsync(); + const zrxTokenAddress = await this._getZRXTokenAddressAsync(signedOrder.exchangeContractAddress); await this._validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, senderAddress, zrxTokenAddress); const wouldRoundingErrorOccur = await this._isRoundingErrorAsync( signedOrder.takerTokenAmount, fillTakerAmount, signedOrder.makerTokenAmount, + signedOrder.exchangeContractAddress, ); if (wouldRoundingErrorOccur) { throw new Error(ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR); @@ -649,7 +694,7 @@ export class ExchangeWrapper extends ContractWrapper { throw new Error(ExchangeContractErrs.ORDER_CANCEL_AMOUNT_ZERO); } const orderHash = await this._getOrderHashHexAsync(order); - const unavailableAmount = await this.getUnavailableTakerAmountAsync(orderHash); + const unavailableAmount = await this.getUnavailableTakerAmountAsync(orderHash, order.exchangeContractAddress); if (order.takerTokenAmount.minus(unavailableAmount).eq(0)) { throw new Error(ExchangeContractErrs.ORDER_ALREADY_CANCELLED_OR_FILLED); } @@ -659,11 +704,11 @@ export class ExchangeWrapper extends ContractWrapper { } } private async _validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder, - exchangeAddress: string, + exchangeContractAddress: string, fillTakerAmount: BigNumber.BigNumber) { // Check that fillValue available >= fillTakerAmount - const orderHashHex = utils.getOrderHashHex(signedOrder, exchangeAddress); - const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex); + const orderHashHex = utils.getOrderHashHex(signedOrder, exchangeContractAddress); + const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex, exchangeContractAddress); const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount); if (remainingTakerAmount < fillTakerAmount) { throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT); @@ -740,24 +785,38 @@ export class ExchangeWrapper extends ContractWrapper { } private async _isRoundingErrorAsync(takerTokenAmount: BigNumber.BigNumber, fillTakerAmount: BigNumber.BigNumber, - makerTokenAmount: BigNumber.BigNumber): Promise { + makerTokenAmount: BigNumber.BigNumber, + exchangeContractAddress: string): Promise { await assert.isUserAddressAvailableAsync(this._web3Wrapper); - const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddress); const isRoundingError = await exchangeInstance.isRoundingError.call( takerTokenAmount, fillTakerAmount, makerTokenAmount, ); return isRoundingError; } - private async _getExchangeContractAsync(): Promise { - if (!_.isUndefined(this._exchangeContractIfExists)) { - return this._exchangeContractIfExists; + private async _getExchangeContractAsync(exchangeContractAddress: string): Promise { + if (!_.isUndefined(this._exchangeContractByAddress[exchangeContractAddress])) { + return this._exchangeContractByAddress[exchangeContractAddress]; } + const ExchangeArtifacts = this._getExchangeArtifactsByAddressOrThrow(exchangeContractAddress); const contractInstance = await this._instantiateContractIfExistsAsync((ExchangeArtifacts as any)); - this._exchangeContractIfExists = contractInstance as ExchangeContract; - return this._exchangeContractIfExists; + this._exchangeContractByAddress[exchangeContractAddress] = contractInstance as ExchangeContract; + return this._exchangeContractByAddress[exchangeContractAddress]; + } + private _getExchangeArtifactsByAddressOrThrow(exchangeContractAddress: string): ContractArtifact { + for (const exchangeArtifact of _.values(ExchangeArtifactsByName)) { + const exchangeAddressesInAftifact = _.map( + _.values(exchangeArtifact.networks), + artifactsByNetwork => artifactsByNetwork.address, + ); + if (_.includes(exchangeAddressesInAftifact, exchangeContractAddress)) { + return exchangeArtifact; + } + } + throw new Error(ZeroExError.CONTRACT_DOES_NOT_EXIST); } - private async _getZRXTokenAddressAsync(): Promise { - const exchangeInstance = await this._getExchangeContractAsync(); + private async _getZRXTokenAddressAsync(exchangeContractAddress: string): Promise { + const exchangeInstance = await this._getExchangeContractAsync(exchangeContractAddress); return exchangeInstance.ZRX.call(); } } diff --git a/src/exchange_artifacts_by_name.ts b/src/exchange_artifacts_by_name.ts new file mode 100644 index 000000000..4ecd28d43 --- /dev/null +++ b/src/exchange_artifacts_by_name.ts @@ -0,0 +1,6 @@ +import {ContractArtifact} from './types'; +import * as Exchange_v1 from './artifacts/exchange/Exchange.json'; + +export const ExchangeArtifactsByName = { + Exchange_v1: Exchange_v1 as any as ContractArtifact, +}; diff --git a/src/types.ts b/src/types.ts index 200e65d56..f12d6f6c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -240,6 +240,7 @@ export interface Order { makerTokenAddress: string; takerTokenAddress: string; salt: BigNumber.BigNumber; + exchangeContractAddress: string; feeRecipient: string; expirationUnixTimestampSec: BigNumber.BigNumber; } @@ -325,3 +326,15 @@ export interface ContractEventEmitter { * It is however a `Web3` library type, not a native `0x.js` type. */ export type Web3Provider = Web3.Provider; + +export interface ExchangeContractByAddress { + [address: string]: ExchangeContract; +} + +export interface ContractArtifact { + networks: { + [networkId: number]: { + address: string; + }; + }; +} -- cgit v1.2.3