diff options
author | Leonid <logvinov.leon@gmail.com> | 2017-06-09 00:31:21 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-09 00:31:21 +0800 |
commit | 743ba29918715e21c7891b3c7426dcb5fdc14e17 (patch) | |
tree | 6053e4d717a0d0f5dcc96124b27fffaec072421c | |
parent | 31cc75bd6d2651466ebf50e9374d5cd19de6dd5e (diff) | |
parent | b1b879e217f383fa694214e731969647f9f6fd78 (diff) | |
download | dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.tar dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.tar.gz dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.tar.bz2 dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.tar.lz dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.tar.xz dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.tar.zst dexon-sol-tools-743ba29918715e21c7891b3c7426dcb5fdc14e17.zip |
Merge pull request #44 from 0xProject/batchFillOrKill
Implement BatchFillOrKill and tests
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 113 | ||||
-rw-r--r-- | src/schemas/basic_type_schemas.ts | 11 | ||||
-rw-r--r-- | src/schemas/order_fill_or_kill_requests_schema.ts | 12 | ||||
-rw-r--r-- | src/schemas/order_schemas.ts | 12 | ||||
-rw-r--r-- | src/types.ts | 11 | ||||
-rw-r--r-- | src/utils/schema_validator.ts | 7 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 127 | ||||
-rw-r--r-- | test/schema_test.ts | 2 |
8 files changed, 205 insertions, 90 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 72ae0d07e..73ec0886c 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -10,6 +10,7 @@ import { OrderValues, OrderAddresses, Order, + OrderFillOrKillRequest, SignedOrder, ContractEvent, ExchangeEvents, @@ -27,6 +28,7 @@ import {utils} from '../utils/utils'; import {ContractWrapper} from './contract_wrapper'; import * as ExchangeArtifacts from '../artifacts/Exchange.json'; import {ecSignatureSchema} from '../schemas/ec_signature_schema'; +import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema'; import {signedOrderSchema, orderSchema} from '../schemas/order_schemas'; import {SchemaValidator} from '../utils/schema_validator'; import {constants} from '../utils/constants'; @@ -71,23 +73,6 @@ export class ExchangeWrapper extends ContractWrapper { await this.stopWatchingExchangeLogEventsAsync(); delete this.exchangeContractIfExists; } - private async isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature, - signerAddressHex: string): Promise<boolean> { - assert.isHexString('dataHex', dataHex); - assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema); - assert.isETHAddressHex('signerAddressHex', signerAddressHex); - - const exchangeInstance = await this.getExchangeContractAsync(); - - const isValidSignature = await exchangeInstance.isValidSignature.call( - signerAddressHex, - dataHex, - ecSignature.v, - ecSignature.r, - ecSignature.s, - ); - return isValidSignature; - } /** * Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total * amount that has been filled or cancelled. The remaining takerAmount can be calculated by @@ -255,13 +240,8 @@ export class ExchangeWrapper extends ContractWrapper { 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); - } + await this.validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder, exchangeInstance.address, + fillTakerAmount); const [orderAddresses, orderValues] = ExchangeWrapper.getOrderAddressesAndValues(signedOrder); @@ -291,6 +271,63 @@ export class ExchangeWrapper extends ContractWrapper { this.throwErrorLogsAsErrors(response.logs); } /** + * Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically + * filled (each to the specified fillAmount) or aborted. + */ + public async batchFillOrKillAsync(orderFillOrKillRequests: OrderFillOrKillRequest[], + takerAddress: string) { + await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper); + assert.doesConformToSchema('orderFillOrKillRequests', + SchemaValidator.convertToJSONSchemaCompatibleObject(orderFillOrKillRequests), + orderFillOrKillRequestsSchema, + ); + const exchangeInstance = await this.getExchangeContractAsync(); + _.each(orderFillOrKillRequests, request => { + this.validateFillOrKillOrderAndThrowIfInvalidAsync(request.signedOrder, + exchangeInstance.address, + request.fillTakerAmount); + }); + + const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillOrKillRequests, request => { + return [ + ...ExchangeWrapper.getOrderAddressesAndValues(request.signedOrder), + request.fillTakerAmount, + request.signedOrder.ecSignature.v, + request.signedOrder.ecSignature.r, + request.signedOrder.ecSignature.s, + ]; + }); + + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddresses, orderValues, fillTakerAmounts, vParams, rParams, sParams] = + _.unzip<any>(orderAddressesValuesAndTakerTokenFillAmounts); + + const gas = await exchangeInstance.batchFillOrKill.estimateGas( + orderAddresses, + orderValues, + fillTakerAmounts, + vParams, + rParams, + sParams, + { + from: takerAddress, + }, + ); + const response: ContractResponse = await exchangeInstance.batchFillOrKill( + orderAddresses, + orderValues, + fillTakerAmounts, + vParams, + rParams, + sParams, + { + from: takerAddress, + gas, + }, + ); + this.throwErrorLogsAsErrors(response.logs); + } + /** * Cancel a given fill amount of an order. Cancellations are cumulative. */ public async cancelOrderAsync( @@ -403,6 +440,23 @@ export class ExchangeWrapper extends ContractWrapper { logEventObj.watch(callback); this.exchangeLogEventObjs.push(logEventObj); } + private async isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature, + signerAddressHex: string): Promise<boolean> { + assert.isHexString('dataHex', dataHex); + assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema); + assert.isETHAddressHex('signerAddressHex', signerAddressHex); + + const exchangeInstance = await this.getExchangeContractAsync(); + + const isValidSignature = await exchangeInstance.isValidSignature.call( + signerAddressHex, + dataHex, + ecSignature.v, + ecSignature.r, + ecSignature.s, + ); + return isValidSignature; + } private async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> { const exchangeInstance = await this.getExchangeContractAsync(); const orderHashHex = utils.getOrderHashHex(order, exchangeInstance.address); @@ -460,6 +514,17 @@ export class ExchangeWrapper extends ContractWrapper { throw new Error(ExchangeContractErrs.ORDER_CANCEL_EXPIRED); } } + private async validateFillOrKillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder, + exchangeAddress: string, + fillTakerAmount: BigNumber.BigNumber) { + // Check that fillValue available >= fillTakerAmount + const orderHashHex = utils.getOrderHashHex(signedOrder, exchangeAddress); + const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex); + const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount); + if (remainingTakerAmount < fillTakerAmount) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT); + } + } /** * This method does not currently validate the edge-case where the makerToken or takerToken is also the token used * to pay fees (ZRX). It is possible for them to have enough for fees and the transfer but not both. diff --git a/src/schemas/basic_type_schemas.ts b/src/schemas/basic_type_schemas.ts new file mode 100644 index 000000000..c3b81185d --- /dev/null +++ b/src/schemas/basic_type_schemas.ts @@ -0,0 +1,11 @@ +export const addressSchema = { + id: '/addressSchema', + type: 'string', + pattern: '^0[xX][0-9A-Fa-f]{40}$', +}; + +export const numberSchema = { + id: '/numberSchema', + type: 'string', + pattern: '^\\d+(\\.\\d+)?$', +}; diff --git a/src/schemas/order_fill_or_kill_requests_schema.ts b/src/schemas/order_fill_or_kill_requests_schema.ts new file mode 100644 index 000000000..4db7113de --- /dev/null +++ b/src/schemas/order_fill_or_kill_requests_schema.ts @@ -0,0 +1,12 @@ +export const orderFillOrKillRequestsSchema = { + id: '/OrderFillOrKillRequests', + type: 'array', + items: { + properties: { + signedOrder: {$ref: '/signedOrderSchema'}, + fillTakerAmount: {type: '/numberSchema'}, + }, + required: ['signedOrder', 'fillTakerAmount'], + type: 'object', + }, +}; diff --git a/src/schemas/order_schemas.ts b/src/schemas/order_schemas.ts index 4999f3e9d..133736b3d 100644 --- a/src/schemas/order_schemas.ts +++ b/src/schemas/order_schemas.ts @@ -1,15 +1,3 @@ -export const addressSchema = { - id: '/addressSchema', - type: 'string', - pattern: '^0[xX][0-9A-Fa-f]{40}$', -}; - -export const numberSchema = { - id: '/numberSchema', - type: 'string', - pattern: '^\\d+(\\.\\d+)?$', -}; - export const orderSchema = { id: '/orderSchema', properties: { diff --git a/src/types.ts b/src/types.ts index edd7f2d33..dc5a5e6b3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,6 +92,12 @@ export interface ExchangeContract extends ContractInstance { estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, v: number, r: string, s: string, txOpts?: TxOpts) => number; }; + batchFillOrKill: { + (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillValuesT: BigNumber.BigNumber[], + v: number[], r: string[], s: string[], txOpts: TxOpts): ContractResponse; + estimateGas: (orderAddresses: OrderAddresses[], orderValues: OrderValues[], fillValuesT: BigNumber.BigNumber[], + v: number[], r: string[], s: string[], txOpts?: TxOpts) => number; + }; filled: { call: (orderHash: string) => BigNumber.BigNumber; }; @@ -236,6 +242,11 @@ export interface SubscriptionOpts { export type DoneCallback = (err?: Error) => void; +export interface OrderFillOrKillRequest { + signedOrder: SignedOrder; + fillTakerAmount: BigNumber.BigNumber; +} + export interface OrderCancellationRequest { order: Order|SignedOrder; takerTokenCancelAmount: BigNumber.BigNumber; diff --git a/src/utils/schema_validator.ts b/src/utils/schema_validator.ts index 932ddf62a..72f6afffa 100644 --- a/src/utils/schema_validator.ts +++ b/src/utils/schema_validator.ts @@ -1,7 +1,9 @@ import {Validator, ValidatorResult} from 'jsonschema'; import {ecSignatureSchema, ecSignatureParameter} from '../schemas/ec_signature_schema'; -import {addressSchema, numberSchema, orderSchema, signedOrderSchema} from '../schemas/order_schemas'; +import {orderSchema, signedOrderSchema} from '../schemas/order_schemas'; +import {addressSchema, numberSchema} from '../schemas/basic_type_schemas'; import {tokenSchema} from '../schemas/token_schema'; +import {orderFillOrKillRequestsSchema} from '../schemas/order_fill_or_kill_requests_schema'; export class SchemaValidator { private validator: Validator; @@ -9,7 +11,7 @@ export class SchemaValidator { // sub-types (e.g BigNumber) with a simpler string representation. Since BigNumber and other // complex types implement the `toString` method, we can stringify the object and // then parse it. The resultant object can then be checked using jsonschema. - public static convertToJSONSchemaCompatibleObject(obj: object): object { + public static convertToJSONSchemaCompatibleObject(obj: any): any { return JSON.parse(JSON.stringify(obj)); } constructor() { @@ -21,6 +23,7 @@ export class SchemaValidator { this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id); this.validator.addSchema(signedOrderSchema, signedOrderSchema.id); this.validator.addSchema(ecSignatureParameter, ecSignatureParameter.id); + this.validator.addSchema(orderFillOrKillRequestsSchema, orderFillOrKillRequestsSchema.id); } public validate(instance: object, schema: Schema): ValidatorResult { return this.validator.validate(instance, schema); diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index e3d8be1c0..2cd8af7f0 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -52,7 +52,7 @@ describe('ExchangeWrapper', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('#fillOrKillOrderAsync', () => { + describe('fillOrKill order(s)', () => { let makerTokenAddress: string; let takerTokenAddress: string; let coinbase: string; @@ -67,63 +67,88 @@ describe('ExchangeWrapper', () => { makerTokenAddress = makerToken.address; takerTokenAddress = takerToken.address; }); - describe('failed fillOrKill', () => { - it('should throw if remaining fillAmount is less then the desired fillAmount', async () => { + describe('#batchFillOrKillAsync', () => { + it('successfuly batch fillOrKill', async () => { const fillableAmount = new BigNumber(5); + const partialFillTakerAmount = new BigNumber(2); 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( + const anotherSignedOrder = 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)); + const orderFillOrKillRequests = [ + { + signedOrder, + fillTakerAmount: partialFillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + fillTakerAmount: partialFillTakerAmount, + }, + ]; + await zeroEx.exchange.batchFillOrKillAsync(orderFillOrKillRequests, takerAddress); }); - 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('#fillOrKillOrderAsync', () => { + 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)); + }); }); }); }); diff --git a/test/schema_test.ts b/test/schema_test.ts index 1c4216496..d35ed4516 100644 --- a/test/schema_test.ts +++ b/test/schema_test.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as BigNumber from 'bignumber.js'; import promisify = require('es6-promisify'); -import {numberSchema} from '../src/schemas/order_schemas'; +import {numberSchema} from '../src/schemas/basic_type_schemas'; import {SchemaValidator} from '../src/utils/schema_validator'; chai.config.includeStack = true; |