diff options
-rw-r--r-- | src/0x.js.ts | 9 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 64 | ||||
-rw-r--r-- | src/types.ts | 8 | ||||
-rw-r--r-- | test/0x.js_test.ts | 2 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 100 |
5 files changed, 169 insertions, 14 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts index 7b53b70ea..8f1178b2a 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -130,11 +130,12 @@ 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 orderHashHex = utils.getOrderHashHex(order, exchangeContractAddr); + return orderHashHex; } /** * 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 af8b8db24..47c49fad6 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -146,6 +146,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, @@ -174,6 +175,56 @@ 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, + takerAddress: string) { + assert.doesConformToSchema('signedOrder', + SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object), + signedOrderSchema); + assert.isBigNumber('fillTakerAmount', fillTakerAmount); + 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 = await this.getOrderHashHexAsync(signedOrder); + 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, + }, + ); + 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); + } + /** * Cancel a given fill amount of an order. Cancellations are cumulative. */ public async cancelOrderAsync( @@ -258,11 +309,10 @@ export class ExchangeWrapper extends ContractWrapper { logEventObj.watch(callback); this.exchangeLogEventObjs.push(logEventObj); } - private async getOrderHashAsync(order: Order|SignedOrder): Promise<string> { - const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(order); + private async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> { const exchangeInstance = await this.getExchangeContractAsync(); - const orderHash = utils.getOrderHashHex(order, exchangeInstance.address); - return orderHash; + const orderHashHex = utils.getOrderHashHex(order, exchangeInstance.address); + return orderHashHex; } private async stopWatchingExchangeLogEventsAsync() { const stopWatchingPromises = _.map(this.exchangeLogEventObjs, logEventObj => { @@ -300,7 +350,7 @@ export class ExchangeWrapper extends ContractWrapper { if (takerTokenCancelAmount.eq(0)) { throw new Error(ExchangeContractErrs.ORDER_CANCEL_AMOUNT_ZERO); } - const orderHash = await this.getOrderHashAsync(order); + const orderHash = await this.getOrderHashHexAsync(order); const unavailableAmount = await this.getUnavailableTakerAmountAsync(orderHash); if (order.takerTokenAmount.minus(unavailableAmount).eq(0)) { throw new Error(ExchangeContractErrs.ORDER_ALREADY_CANCELLED_OR_FILLED); @@ -316,8 +366,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 ebd427ae9..00ef17189 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,6 +80,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; }; @@ -146,7 +152,7 @@ export const ExchangeContractErrs = strEnum([ 'INSUFFICIENT_MAKER_FEE_ALLOWANCE', 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER', 'MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH', - + 'INSUFFICIENT_REMAINING_FILL_AMOUNT', ]); export type ExchangeContractErrs = keyof typeof ExchangeContractErrs; diff --git a/test/0x.js_test.ts b/test/0x.js_test.ts index 5096d5df2..58f259a11 100644 --- a/test/0x.js_test.ts +++ b/test/0x.js_test.ts @@ -151,7 +151,7 @@ describe('ZeroEx library', () => { expect(baseUnitAmount).to.be.bignumber.equal(expectedUnitAmount); }); }); - describe('#getOrderHashAsync', () => { + describe('#getOrderHashHexAsync', () => { const exchangeContractAddress = constants.NULL_ADDRESS; const expectedOrderHash = '0x103a5e97dab5dbeb8f385636f86a7d1e458a7ccbe1bd194727f0b2f85ab116c7'; const order: Order = { diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 88a2e2277..a9a93fc71 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -120,6 +120,81 @@ 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); + 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, 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, 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.fillOrKillOrderAsync(signedOrder, partialFillAmount, 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 +285,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 +384,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); |