import { getContractAddressesForNetworkOrThrow } from '@0x/contract-addresses'; import { Coordinator } from '@0x/contract-artifacts'; import { schemas } from '@0x/json-schemas'; import { generatePseudoRandomSalt, signatureUtils } from '@0x/order-utils'; import { Order, SignedOrder, SignedZeroExTransaction, ZeroExTransaction } from '@0x/types'; import { BigNumber, fetchAsync } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { ContractAbi, SupportedProvider } from 'ethereum-types'; import * as HttpStatus from 'http-status-codes'; import { flatten } from 'lodash'; import { CoordinatorContract, CoordinatorRegistryContract, ExchangeContract } from '@0x/abi-gen-wrappers'; import { orderTxOptsSchema } from './schemas/order_tx_opts_schema'; import { txOptsSchema } from './schemas/tx_opts_schema'; import { CoordinatorTransaction, OrderTransactionOpts } from './types'; import { assert } from './utils/assert'; import { CoordinatorServerApprovalRawResponse, CoordinatorServerApprovalResponse, CoordinatorServerCancellationResponse, CoordinatorServerError, CoordinatorServerErrorMsg, CoordinatorServerResponse, } from './utils/coordinator_server_types'; import { decorators } from './utils/decorators'; import { getAbiEncodedTransactionData } from './utils/getAbiEncodedTransactionData'; /** * This class includes all the functionality related to filling or cancelling orders through * the 0x V2 Coordinator extension contract. */ export class CoordinatorWrapper { public abi: ContractAbi = Coordinator.compilerOutput.abi; public networkId: number; public address: string; public exchangeAddress: string; public registryAddress: string; private readonly _web3Wrapper: Web3Wrapper; private readonly _contractInstance: CoordinatorContract; private readonly _registryInstance: CoordinatorRegistryContract; private readonly _exchangeInstance: ExchangeContract; private readonly _feeRecipientToEndpoint: { [feeRecipient: string]: string } = {}; /** * Instantiate CoordinatorWrapper * @param web3Wrapper Web3Wrapper instance to use. * @param networkId Desired networkId. * @param address The address of the Coordinator contract. If undefined, will * default to the known address corresponding to the networkId. * @param exchangeAddress The address of the Exchange contract. If undefined, will * default to the known address corresponding to the networkId. * @param registryAddress The address of the CoordinatorRegistry contract. If undefined, will * default to the known address corresponding to the networkId. */ constructor( provider: SupportedProvider, networkId: number, address?: string, exchangeAddress?: string, registryAddress?: string, ) { this.networkId = networkId; const contractAddresses = getContractAddressesForNetworkOrThrow(networkId); this.address = address === undefined ? contractAddresses.coordinator : address; this.exchangeAddress = exchangeAddress === undefined ? contractAddresses.coordinator : exchangeAddress; this.registryAddress = registryAddress === undefined ? contractAddresses.coordinatorRegistry : registryAddress; this._web3Wrapper = new Web3Wrapper(provider); this._contractInstance = new CoordinatorContract( this.address, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); this._registryInstance = new CoordinatorRegistryContract( this.registryAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); this._exchangeInstance = new ExchangeContract( this.exchangeAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); } /** * Fills a signed order with an amount denominated in baseUnits of the taker asset. Under-the-hood, this * method uses the `feeRecipientAddress` of the order to look up the coordinator server endpoint registered in the * coordinator registry contract. It requests a signature from that coordinator server before * submitting the order and signature as a 0x transaction to the coordinator extension contract. The coordinator extension * contract validates signatures and then fills the order via the Exchange contract. * @param signedOrder An object that conforms to the SignedOrder interface. * @param takerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill. * @param takerAddress The user Ethereum address who would like to fill this order. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async fillOrderAsync( signedOrder: SignedOrder, takerAssetFillAmount: BigNumber, takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount); assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData( 'fillOrder', signedOrder, takerAssetFillAmount, signedOrder.signature, ); const txHash = await this._handleFillsAsync(data, takerAddress, [signedOrder], orderTransactionOpts); return txHash; } /** * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled, * the fill order is abandoned. * @param signedOrder An object that conforms to the SignedOrder interface. * @param takerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill. * @param takerAddress The user Ethereum address who would like to fill this order. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async fillOrKillOrderAsync( signedOrder: SignedOrder, takerAssetFillAmount: BigNumber, takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount); assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData( 'fillOrKillOrder', signedOrder, takerAssetFillAmount, signedOrder.signature, ); const txHash = await this._handleFillsAsync(data, takerAddress, [signedOrder], orderTransactionOpts); return txHash; } /** * Batch version of fillOrderAsync. Executes multiple fills atomically in a single transaction. * Under-the-hood, this method uses the `feeRecipientAddress`s of the orders to looks up the coordinator server endpoints * registered in the coordinator registry contract. It requests a signature from each coordinator server before * submitting the orders and signatures as a 0x transaction to the coordinator extension contract, which validates the * signatures and then fills the order through the Exchange contract. * If any `feeRecipientAddress` in the batch is not registered to a coordinator server, the whole batch fails. * @param signedOrders An array of signed orders to fill. * @param takerAssetFillAmounts The amounts of the orders (in taker asset baseUnits) that you wish to fill. * @param takerAddress The user Ethereum address who would like to fill these orders. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async batchFillOrdersAsync( signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[], takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); for (const takerAssetFillAmount of takerAssetFillAmounts) { assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount); } assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const signatures = signedOrders.map(o => o.signature); const data = this._getAbiEncodedTransactionData( 'batchFillOrders', signedOrders, takerAssetFillAmounts, signatures, ); const txHash = await this._handleFillsAsync(data, takerAddress, signedOrders, orderTransactionOpts); return txHash; } /** * No throw version of batchFillOrdersAsync * @param signedOrders An array of signed orders to fill. * @param takerAssetFillAmounts The amounts of the orders (in taker asset baseUnits) that you wish to fill. * @param takerAddress The user Ethereum address who would like to fill these orders. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async batchFillOrdersNoThrowAsync( signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[], takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); for (const takerAssetFillAmount of takerAssetFillAmounts) { assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount); } assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const signatures = signedOrders.map(o => o.signature); const data = this._getAbiEncodedTransactionData( 'batchFillOrdersNoThrow', signedOrders, takerAssetFillAmounts, signatures, ); const txHash = await this._handleFillsAsync(data, takerAddress, signedOrders, orderTransactionOpts); return txHash; } /** * Batch version of fillOrKillOrderAsync. Executes multiple fills atomically in a single transaction. * @param signedOrders An array of signed orders to fill. * @param takerAssetFillAmounts The amounts of the orders (in taker asset baseUnits) that you wish to fill. * @param takerAddress The user Ethereum address who would like to fill these orders. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async batchFillOrKillOrdersAsync( signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[], takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); for (const takerAssetFillAmount of takerAssetFillAmounts) { assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount); } assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const signatures = signedOrders.map(o => o.signature); const data = this._getAbiEncodedTransactionData( 'batchFillOrKillOrders', signedOrders, takerAssetFillAmounts, signatures, ); const txHash = await this._handleFillsAsync(data, takerAddress, signedOrders, orderTransactionOpts); return txHash; } /** * No throw version of marketBuyOrdersAsync * @param signedOrders An array of signed orders to fill. * @param makerAssetFillAmount Maker asset fill amount. * @param takerAddress The user Ethereum address who would like to fill these orders. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async marketBuyOrdersNoThrowAsync( signedOrders: SignedOrder[], makerAssetFillAmount: BigNumber, takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount); assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const signatures = signedOrders.map(o => o.signature); const data = this._getAbiEncodedTransactionData( 'marketBuyOrdersNoThrow', signedOrders, makerAssetFillAmount, signatures, ); const txHash = await this._handleFillsAsync(data, takerAddress, signedOrders, orderTransactionOpts); return txHash; } /** * No throw version of marketSellOrdersAsync * @param signedOrders An array of signed orders to fill. * @param takerAssetFillAmount Taker asset fill amount. * @param takerAddress The user Ethereum address who would like to fill these orders. Must be available via the supplied * Provider provided at instantiation. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async marketSellOrdersNoThrowAsync( signedOrders: SignedOrder[], takerAssetFillAmount: BigNumber, takerAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount); assert.isETHAddressHex('takerAddress', takerAddress); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const signatures = signedOrders.map(o => o.signature); const data = this._getAbiEncodedTransactionData( 'marketSellOrdersNoThrow', signedOrders, takerAssetFillAmount, signatures, ); const txHash = await this._handleFillsAsync(data, takerAddress, signedOrders, orderTransactionOpts); return txHash; } /** * Soft cancel a given order. * Soft cancels are recorded only on coordinator operator servers and do not involve an Ethereum transaction. * See [soft cancels](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/coordinator-specification.md#soft-cancels). * @param order An object that conforms to the Order or SignedOrder interface. The order you would like to cancel. * @return CoordinatorServerCancellationResponse. See [Cancellation Response](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/coordinator-specification.md#response). */ public async softCancelOrderAsync(order: Order | SignedOrder): Promise { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.isETHAddressHex('feeRecipientAddress', order.feeRecipientAddress); assert.isSenderAddressAsync('makerAddress', order.makerAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData('cancelOrder', order); const transaction = await this._generateSignedZeroExTransactionAsync(data, order.makerAddress); const endpoint = await this._getServerEndpointOrThrowAsync(order.feeRecipientAddress); const response = await this._executeServerRequestAsync(transaction, order.makerAddress, endpoint); if (response.isError) { const approvedOrders = new Array(); const cancellations = new Array(); const errors = [ { ...response, orders: [order], }, ]; throw new CoordinatorServerError( CoordinatorServerErrorMsg.CancellationFailed, approvedOrders, cancellations, errors, ); } else { return response.body as CoordinatorServerCancellationResponse; } } /** * Batch version of softCancelOrderAsync. Requests multiple soft cancels * @param orders An array of orders to cancel. * @return CoordinatorServerCancellationResponse. See [Cancellation Response](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/coordinator-specification.md#response). */ public async batchSoftCancelOrdersAsync(orders: SignedOrder[]): Promise { assert.doesConformToSchema('orders', orders, schemas.ordersSchema); const makerAddress = getMakerAddressOrThrow(orders); assert.isSenderAddressAsync('makerAddress', makerAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData('batchCancelOrders', orders); const serverEndpointsToOrders = await this._mapServerEndpointsToOrdersAsync(orders); // make server requests const errorResponses: CoordinatorServerResponse[] = []; const successResponses: CoordinatorServerCancellationResponse[] = []; const transaction = await this._generateSignedZeroExTransactionAsync(data, makerAddress); for (const endpoint of Object.keys(serverEndpointsToOrders)) { const response = await this._executeServerRequestAsync(transaction, makerAddress, endpoint); if (response.isError) { errorResponses.push(response); } else { successResponses.push(response.body as CoordinatorServerCancellationResponse); } } // if no errors if (errorResponses.length === 0) { return successResponses; } else { // lookup orders with errors const errorsWithOrders = errorResponses.map(resp => { const endpoint = resp.coordinatorOperator; const _orders = serverEndpointsToOrders[endpoint]; return { ...resp, orders: _orders, }; }); const approvedOrders = new Array(); const cancellations = successResponses; // return errors and approvals throw new CoordinatorServerError( CoordinatorServerErrorMsg.CancellationFailed, approvedOrders, cancellations, errorsWithOrders, ); } } /** * Cancels an order on-chain by submitting an Ethereum transaction. * @param order An object that conforms to the Order or SignedOrder interface. The order you would like to cancel. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async hardCancelOrderAsync( order: Order | SignedOrder, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('makerAddress', order.makerAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData('cancelOrder', order); const transaction = await this._generateSignedZeroExTransactionAsync(data, order.makerAddress); const approvalSignatures = new Array(); const approvalExpirationTimeSeconds = new Array(); const txHash = await this._submitCoordinatorTransactionAsync( transaction, order.makerAddress, transaction.signature, approvalExpirationTimeSeconds, approvalSignatures, orderTransactionOpts, ); return txHash; } /** * Batch version of hardCancelOrderAsync. Cancels orders on-chain by submitting an Ethereum transaction. * Executes multiple cancels atomically in a single transaction. * @param orders An array of orders to cancel. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async batchHardCancelOrdersAsync( orders: SignedOrder[], orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.doesConformToSchema('orders', orders, schemas.ordersSchema); const makerAddress = getMakerAddressOrThrow(orders); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('makerAddress', makerAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData('batchCancelOrders', orders); const transaction = await this._generateSignedZeroExTransactionAsync(data, makerAddress); const approvalSignatures = new Array(); const approvalExpirationTimeSeconds = new Array(); const txHash = await this._submitCoordinatorTransactionAsync( transaction, makerAddress, transaction.signature, approvalExpirationTimeSeconds, approvalSignatures, orderTransactionOpts, ); return txHash; } /** * Cancels orders on-chain by submitting an Ethereum transaction. * Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch * and senderAddress equal to coordinator extension contract address. * @param targetOrderEpoch Target order epoch. * @param senderAddress Address that should send the transaction. * @param orderTransactionOpts Optional arguments this method accepts. * @return Transaction hash. */ @decorators.asyncZeroExErrorHandler public async hardCancelOrdersUpToAsync( targetOrderEpoch: BigNumber, senderAddress: string, orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true }, ): Promise { assert.isBigNumber('targetOrderEpoch', targetOrderEpoch); assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]); await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper); const data = this._getAbiEncodedTransactionData('cancelOrdersUpTo', targetOrderEpoch); const transaction = await this._generateSignedZeroExTransactionAsync(data, senderAddress); const approvalSignatures = new Array(); const approvalExpirationTimeSeconds = new Array(); const txHash = await this._submitCoordinatorTransactionAsync( transaction, senderAddress, transaction.signature, approvalExpirationTimeSeconds, approvalSignatures, orderTransactionOpts, ); return txHash; } /** * Validates that the 0x transaction has been approved by all of the feeRecipients that correspond to each order in the transaction's Exchange calldata. * Throws an error if the transaction approvals are not valid. Will not detect failures that would occur when the transaction is executed on the Exchange contract. * @param transaction 0x transaction containing salt, signerAddress, and data. * @param txOrigin Required signer of Ethereum transaction calling this function. * @param transactionSignature Proof that the transaction has been signed by the signer. * @param approvalExpirationTimeSeconds Array of expiration times in seconds for which each corresponding approval signature expires. * @param approvalSignatures Array of signatures that correspond to the feeRecipients of each order in the transaction's Exchange calldata. */ public async assertValidCoordinatorApprovalsOrThrowAsync( transaction: ZeroExTransaction, txOrigin: string, transactionSignature: string, approvalExpirationTimeSeconds: BigNumber[], approvalSignatures: string[], ): Promise { assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema); assert.isETHAddressHex('txOrigin', txOrigin); assert.isHexString('transactionSignature', transactionSignature); for (const expirationTime of approvalExpirationTimeSeconds) { assert.isBigNumber('expirationTime', expirationTime); } for (const approvalSignature of approvalSignatures) { assert.isHexString('approvalSignature', approvalSignature); } await this._contractInstance.assertValidCoordinatorApprovals.callAsync( transaction, txOrigin, transactionSignature, approvalExpirationTimeSeconds, approvalSignatures, ); } /** * Recovers the address of a signer given a hash and signature. * @param hash Any 32 byte hash. * @param signature Proof that the hash has been signed by signer. * @returns Signer address. */ public async getSignerAddressAsync(hash: string, signature: string): Promise { assert.isHexString('hash', hash); assert.isHexString('signature', signature); const signerAddress = await this._contractInstance.getSignerAddress.callAsync(hash, signature); return signerAddress; } private _getAbiEncodedTransactionData(methodName: K, ...args: any[]): string { return getAbiEncodedTransactionData(this._exchangeInstance, methodName, ...args); } private async _handleFillsAsync( data: string, takerAddress: string, signedOrders: SignedOrder[], orderTransactionOpts: OrderTransactionOpts, ): Promise { const coordinatorOrders = signedOrders.filter(o => o.senderAddress === this.address); const serverEndpointsToOrders = await this._mapServerEndpointsToOrdersAsync(coordinatorOrders); // make server requests const errorResponses: CoordinatorServerResponse[] = []; const approvalResponses: CoordinatorServerResponse[] = []; const transaction = await this._generateSignedZeroExTransactionAsync(data, takerAddress); for (const endpoint of Object.keys(serverEndpointsToOrders)) { const response = await this._executeServerRequestAsync(transaction, takerAddress, endpoint); if (response.isError) { errorResponses.push(response); } else { approvalResponses.push(response); } } // if no errors if (errorResponses.length === 0) { // concatenate all approval responses const allApprovals = approvalResponses.map(resp => formatRawResponse(resp.body as CoordinatorServerApprovalRawResponse), ); const allSignatures = flatten(allApprovals.map(a => a.signatures)); const allExpirationTimes = flatten(allApprovals.map(a => a.expirationTimeSeconds)); // submit transaction with approvals const txHash = await this._submitCoordinatorTransactionAsync( transaction, takerAddress, transaction.signature, allExpirationTimes, allSignatures, orderTransactionOpts, ); return txHash; } else { // format errors and approvals // concatenate approvals const notCoordinatorOrders = signedOrders.filter(o => o.senderAddress !== this.address); const approvedOrdersNested = approvalResponses.map(resp => { const endpoint = resp.coordinatorOperator; const orders = serverEndpointsToOrders[endpoint]; return orders; }); const approvedOrders = flatten(approvedOrdersNested.concat(notCoordinatorOrders)); // lookup orders with errors const errorsWithOrders = errorResponses.map(resp => { const endpoint = resp.coordinatorOperator; const orders = serverEndpointsToOrders[endpoint]; return { ...resp, orders, }; }); // throw informative error const cancellations = new Array(); throw new CoordinatorServerError( CoordinatorServerErrorMsg.FillFailed, approvedOrders, cancellations, errorsWithOrders, ); } function formatRawResponse( rawResponse: CoordinatorServerApprovalRawResponse, ): CoordinatorServerApprovalResponse { return { signatures: ([] as string[]).concat(rawResponse.signatures), expirationTimeSeconds: ([] as BigNumber[]).concat( Array(rawResponse.signatures.length).fill(rawResponse.expirationTimeSeconds), ), }; } } private async _getServerEndpointOrThrowAsync(feeRecipientAddress: string): Promise { const cached = this._feeRecipientToEndpoint[feeRecipientAddress]; const endpoint = cached !== undefined ? cached : await _fetchServerEndpointOrThrowAsync(feeRecipientAddress, this._registryInstance); return endpoint; async function _fetchServerEndpointOrThrowAsync( feeRecipient: string, registryInstance: CoordinatorRegistryContract, ): Promise { const coordinatorOperatorEndpoint = await registryInstance.getCoordinatorEndpoint.callAsync(feeRecipient); if (coordinatorOperatorEndpoint === '' || coordinatorOperatorEndpoint === undefined) { throw new Error( `No Coordinator server endpoint found in Coordinator Registry for feeRecipientAddress: ${feeRecipient}. Registry contract address: ${ registryInstance.address }`, ); } return coordinatorOperatorEndpoint; } } private async _generateSignedZeroExTransactionAsync( data: string, signerAddress: string, ): Promise { const transaction: ZeroExTransaction = { salt: generatePseudoRandomSalt(), signerAddress, data, domain: { verifyingContract: this.exchangeAddress, chainId: await this._web3Wrapper.getChainIdAsync(), }, // HACK (xianny): arbitrary numbers for now expirationTimeSeconds: new BigNumber(5), gasPrice: new BigNumber(1), }; const signedTransaction = await signatureUtils.ecSignTransactionAsync( this._web3Wrapper.getProvider(), transaction, transaction.signerAddress, ); return signedTransaction; } private async _executeServerRequestAsync( signedTransaction: SignedZeroExTransaction, txOrigin: string, endpoint: string, ): Promise { const requestPayload = { signedTransaction, txOrigin, }; const response = await fetchAsync(`${endpoint}/v1/request_transaction?networkId=${this.networkId}`, { body: JSON.stringify(requestPayload), method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, }); const isError = response.status !== HttpStatus.OK; const isValidationError = response.status === HttpStatus.BAD_REQUEST; const json = isError && !isValidationError ? undefined : await response.json(); const result = { isError, status: response.status, body: isError ? undefined : json, error: isError ? json : undefined, request: requestPayload, coordinatorOperator: endpoint, }; return result; } private async _submitCoordinatorTransactionAsync( transaction: CoordinatorTransaction, txOrigin: string, transactionSignature: string, approvalExpirationTimeSeconds: BigNumber[], approvalSignatures: string[], orderTransactionOpts: OrderTransactionOpts, ): Promise { if (orderTransactionOpts.shouldValidate) { await this._contractInstance.executeTransaction.callAsync( transaction, txOrigin, transactionSignature, approvalExpirationTimeSeconds, approvalSignatures, { from: txOrigin, gas: orderTransactionOpts.gasLimit, gasPrice: orderTransactionOpts.gasPrice, nonce: orderTransactionOpts.nonce, }, ); } const txHash = await this._contractInstance.executeTransaction.sendTransactionAsync( transaction, txOrigin, transactionSignature, approvalExpirationTimeSeconds, approvalSignatures, { from: txOrigin, gas: orderTransactionOpts.gasLimit, gasPrice: orderTransactionOpts.gasPrice, nonce: orderTransactionOpts.nonce, }, ); return txHash; } private async _mapServerEndpointsToOrdersAsync( coordinatorOrders: SignedOrder[], ): Promise<{ [endpoint: string]: SignedOrder[] }> { const feeRecipientsToOrders: { [feeRecipient: string]: SignedOrder[] } = {}; for (const order of coordinatorOrders) { const feeRecipient = order.feeRecipientAddress; if (feeRecipientsToOrders[feeRecipient] === undefined) { feeRecipientsToOrders[feeRecipient] = [] as SignedOrder[]; } feeRecipientsToOrders[feeRecipient].push(order); } const serverEndpointsToOrders: { [endpoint: string]: SignedOrder[] } = {}; for (const feeRecipient of Object.keys(feeRecipientsToOrders)) { const endpoint = await this._getServerEndpointOrThrowAsync(feeRecipient); const orders = feeRecipientsToOrders[feeRecipient]; if (serverEndpointsToOrders[endpoint] === undefined) { serverEndpointsToOrders[endpoint] = []; } serverEndpointsToOrders[endpoint] = serverEndpointsToOrders[endpoint].concat(orders); } return serverEndpointsToOrders; } } function getMakerAddressOrThrow(orders: Array): string { const uniqueMakerAddresses = new Set(orders.map(o => o.makerAddress)); if (uniqueMakerAddresses.size > 1) { throw new Error(`All orders in a batch must have the same makerAddress`); } return orders[0].makerAddress; } // tslint:disable:max-file-line-count