810 lines
38 KiB
TypeScript
810 lines
38 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, 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<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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
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<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
|