aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/0x.js.ts10
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts68
-rw-r--r--src/types.ts9
-rw-r--r--src/utils/constants.ts1
-rw-r--r--test/exchange_wrapper_test.ts103
5 files changed, 182 insertions, 9 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts
index 7b53b70ea..7be6922fc 100644
--- a/src/0x.js.ts
+++ b/src/0x.js.ts
@@ -130,11 +130,13 @@ export class ZeroEx {
* Computes the orderHash for a given order and returns it as a hex encoded string.
*/
public async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> {
- assert.doesConformToSchema(
- 'order', SchemaValidator.convertToJSONSchemaCompatibleObject(order as object), orderSchema);
+ assert.doesConformToSchema('order',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
+ orderSchema);
+
const exchangeContractAddr = await this.getExchangeAddressAsync();
- const hashHex = utils.getOrderHashHex(order, exchangeContractAddr);
- return hashHex;
+ const orderHash = utils.getOrderHashHex(order, exchangeContractAddr);
+ return orderHash;
}
/**
* Signs an orderHash and returns it's elliptic curve signature
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 6f62934dc..c24a518a4 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -145,6 +145,7 @@ export class ExchangeWrapper extends ContractWrapper {
await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress);
const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
+
const gas = await exchangeInstance.fill.estimateGas(
orderAddresses,
orderValues,
@@ -173,9 +174,70 @@ export class ExchangeWrapper extends ContractWrapper {
this.throwErrorLogsAsErrors(response.logs);
}
/**
+ * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled,
+ * the fill order is abandoned.
+ */
+ public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber,
+ shouldCheckTransfer: boolean, takerAddress: string) {
+ assert.doesConformToSchema('signedOrder',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object),
+ signedOrderSchema);
+ assert.isBigNumber('fillTakerAmount', fillTakerAmount);
+ assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper);
+
+ const exchangeInstance = await this.getExchangeContractAsync();
+ await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress);
+
+ // Check that fillValue available >= fillTakerAmount
+ const orderHashHex = utils.getOrderHashHex(signedOrder, exchangeInstance.address);
+ const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex);
+ const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount);
+ if (remainingTakerAmount < fillTakerAmount) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
+ }
+
+ const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder);
+
+ const gas = await exchangeInstance.fillOrKill.estimateGas(
+ orderAddresses,
+ orderValues,
+ fillTakerAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ },
+ );
+ try {
+ const response: ContractResponse = await exchangeInstance.fillOrKill(
+ orderAddresses,
+ orderValues,
+ fillTakerAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ this.throwErrorLogsAsErrors(response.logs);
+ } catch (err) {
+ // There is a potential race condition where when the cancellation is broadcasted, a sufficient
+ // fillAmount is available, but by the time the transaction gets mined, it no longer is. Instead of
+ // throwing an invalid jump exception, we would rather give the user a more helpful error message.
+ if (_.includes(err, constants.INVALID_JUMP_IDENTIFIER)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
+ }
+ }
+ }
+ /**
* Cancel a given fill amount of an order. Cancellations are cumulative.
*/
- public async cancelOrderAsync(order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber): Promise<void> {
+ public async cancelOrderAsync(order: Order|SignedOrder, takerTokenCancelAmount: BigNumber.BigNumber):
+ Promise<void> {
assert.doesConformToSchema('order',
SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
orderSchema);
@@ -289,8 +351,8 @@ export class ExchangeWrapper extends ContractWrapper {
* Handling the edge-cases that arise when this happens would require making sure that the user has sufficient
* funds to pay both the fees and the transfer amount. We decided to punt on this for now as the contracts
* will throw for these edge-cases.
- * TODO: Throw errors before calling the smart contract for these edge-cases
- * TODO: in order to minimize the callers gas costs.
+ * TODO: Throw errors before calling the smart contract for these edge-cases in order to minimize
+ * the callers gas costs.
*/
private async validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder: SignedOrder,
fillTakerAmount: BigNumber.BigNumber,
diff --git a/src/types.ts b/src/types.ts
index 5407b0121..1ee8a5bd6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -44,6 +44,7 @@ export interface ContractEventObj {
}
export type CreateContractEvent = (indexFilterValues: IndexFilterValues,
subscriptionOpts: SubscriptionOpts) => ContractEventObj;
+
export interface ExchangeContract extends ContractInstance {
isValidSignature: {
call: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string,
@@ -74,6 +75,12 @@ export interface ExchangeContract extends ContractInstance {
estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, cancelAmount: BigNumber.BigNumber,
txOpts?: TxOpts) => number;
};
+ fillOrKill: {
+ (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
+ v: number, r: string, s: string, txOpts: TxOpts): ContractResponse;
+ estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
+ v: number, r: string, s: string, txOpts: TxOpts) => number;
+ };
filled: {
call: (orderHash: string) => BigNumber.BigNumber;
};
@@ -139,7 +146,7 @@ export const ExchangeContractErrs = strEnum([
'INSUFFICIENT_MAKER_FEE_BALANCE',
'INSUFFICIENT_MAKER_FEE_ALLOWANCE',
'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER',
-
+ 'INSUFFICIENT_REMAINING_FILL_AMOUNT',
]);
export type ExchangeContractErrs = keyof typeof ExchangeContractErrs;
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 5a5ba0e0a..ebc8586f3 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,4 +1,5 @@
export const constants = {
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
TESTRPC_NETWORK_ID: 50,
+ INVALID_JUMP_IDENTIFIER: 'invalid JUMP at',
};
diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts
index c0068c58a..ff2121de1 100644
--- a/test/exchange_wrapper_test.ts
+++ b/test/exchange_wrapper_test.ts
@@ -120,6 +120,84 @@ describe('ExchangeWrapper', () => {
expect(isValid).to.be.true();
});
});
+ describe('#fillOrKillOrderAsync', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipient: string;
+ const fillTakerAmount = new BigNumber(5);
+ const shouldCheckTransfer = false;
+ before(async () => {
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ describe('failed fillOrKill', () => {
+ it('should throw if remaining fillAmount is less then the desired fillAmount', async () => {
+ const fillableAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const tooLargeFillAmount = new BigNumber(7);
+ const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount);
+ await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount);
+ await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount);
+
+ return expect(zeroEx.exchange.fillOrKillOrderAsync(
+ signedOrder, tooLargeFillAmount, shouldCheckTransfer, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT);
+ });
+ });
+ describe('successful fills', () => {
+ it('should fill a valid order', async () => {
+ const fillableAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, fillTakerAmount,
+ shouldCheckTransfer, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(fillTakerAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillTakerAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillTakerAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(fillTakerAmount));
+ });
+ it('should partially fill a valid order', async () => {
+ const fillableAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const partialFillAmount = new BigNumber(3);
+ await zeroEx.exchange.fillOrderAsync(signedOrder, partialFillAmount,
+ shouldCheckTransfer, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ });
+ });
+ });
describe('#fillOrderAsync', () => {
let makerTokenAddress: string;
let takerTokenAddress: string;
@@ -210,7 +288,7 @@ describe('ExchangeWrapper', () => {
)).to.be.rejectedWith(ExchangeContractErrs.INSUFFICIENT_MAKER_ALLOWANCE);
});
});
- it('should throw when there a rounding error would have occurred', async () => {
+ it('should throw when a rounding error would have occurred', async () => {
const makerAmount = new BigNumber(3);
const takerAmount = new BigNumber(5);
const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
@@ -309,6 +387,29 @@ describe('ExchangeWrapper', () => {
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
.to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
});
+ it('should fill up to remaining amount if desired fillAmount greater than available amount', async () => {
+ const fillableAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const tooLargeFillAmount = new BigNumber(7);
+ const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount);
+ await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount);
+ await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount);
+
+ await zeroEx.exchange.fillOrderAsync(signedOrder, tooLargeFillAmount, shouldCheckTransfer,
+ takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillAmountDifference);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillAmountDifference);
+ });
it('should fill the valid orders with fees', async () => {
const fillableAmount = new BigNumber(5);
const makerFee = new BigNumber(1);