Files
protocol/packages/contract-wrappers/src/coordinator_wrapper.ts
Xianny 8ce390be3c switch @0x/contract-wrappers to generated wrappers (#2037)
* switch @0x/contract-wrappers to generated wrappers

- remove TransactionEncoder
- move TokenUtils to @0x/dev-utils
- detailed changes in #2040
2019-08-08 07:29:30 -07:00

912 lines
43 KiB
TypeScript

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 } from 'ethereum-types';
import * as HttpStatus from 'http-status-codes';
import { flatten } from 'lodash';
import { CoordinatorContract, CoordinatorRegistryContract, ExchangeContract } from './index';
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(
web3Wrapper: Web3Wrapper,
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 = web3Wrapper;
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<string> {
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;
}
/**
* No-throw version of fillOrderAsync. This version will not throw if the fill fails. This allows the caller to save gas at the expense of not knowing the reason the fill failed.
* @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 fillOrderNoThrowAsync(
signedOrder: SignedOrder,
takerAssetFillAmount: BigNumber,
takerAddress: string,
orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true },
): Promise<string> {
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(
'fillOrderNoThrow',
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<string> {
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<string> {
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<string> {
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<string> {
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;
}
/**
* Synchronously executes multiple calls to fillOrder until total amount of makerAsset is bought by taker.
* 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 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 marketBuyOrdersAsync(
signedOrders: SignedOrder[],
makerAssetFillAmount: BigNumber,
takerAddress: string,
orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true },
): Promise<string> {
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(
'marketBuyOrders',
signedOrders,
makerAssetFillAmount,
signatures,
);
const txHash = await this._handleFillsAsync(data, takerAddress, signedOrders, orderTransactionOpts);
return txHash;
}
/**
* Synchronously executes multiple calls to fillOrder until total amount of makerAsset is bought by taker.
* 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 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 marketSellOrdersAsync(
signedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
takerAddress: string,
orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true },
): Promise<string> {
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(
'marketSellOrders',
signedOrders,
takerAssetFillAmount,
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<string> {
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<string> {
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<CoordinatorServerCancellationResponse> {
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<CoordinatorServerCancellationResponse[]> {
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<string> {
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<string> {
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<string> {
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<void> {
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<string> {
assert.isHexString('hash', hash);
assert.isHexString('signature', signature);
const signerAddress = await this._contractInstance.getSignerAddress.callAsync(hash, signature);
return signerAddress;
}
private _getAbiEncodedTransactionData<K extends keyof ExchangeContract>(methodName: K, ...args: any[]): string {
return getAbiEncodedTransactionData(this._exchangeInstance, methodName, ...args);
}
private async _handleFillsAsync(
data: string,
takerAddress: string,
signedOrders: SignedOrder[],
orderTransactionOpts: OrderTransactionOpts,
): Promise<string> {
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<string> {
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<string> {
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<SignedZeroExTransaction> {
const transaction: ZeroExTransaction = {
salt: generatePseudoRandomSalt(),
signerAddress,
data,
verifyingContractAddress: this.exchangeAddress,
};
const signedTransaction = await signatureUtils.ecSignTransactionAsync(
this._web3Wrapper.getProvider(),
transaction,
transaction.signerAddress,
);
return signedTransaction;
}
private async _executeServerRequestAsync(
signedTransaction: SignedZeroExTransaction,
txOrigin: string,
endpoint: string,
): Promise<CoordinatorServerResponse> {
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<string> {
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<Order | SignedOrder>): 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