diff options
author | Leonid <logvinov.leon@gmail.com> | 2017-06-03 00:53:21 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-03 00:53:21 +0800 |
commit | c83587a16d016d1efafaf31abb9b39eb54128568 (patch) | |
tree | ac53dfb35344c644096f574802eb64eab5955f90 /src | |
parent | b8ff2468776e1c784ff50e5ada1c633ee0d3aeda (diff) | |
parent | 3fad55d118b6a2f8f44ba5dec7fdae276c806eb3 (diff) | |
download | dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.tar dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.tar.gz dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.tar.bz2 dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.tar.lz dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.tar.xz dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.tar.zst dexon-sol-tools-c83587a16d016d1efafaf31abb9b39eb54128568.zip |
Merge pull request #30 from 0xProject/fillOrderAsync
fillOrderAsync
Diffstat (limited to 'src')
-rw-r--r-- | src/0x.js.ts | 124 | ||||
-rw-r--r-- | src/bignumber_config.ts | 11 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 209 | ||||
-rw-r--r-- | src/globals.d.ts | 1 | ||||
-rw-r--r-- | src/schemas/order_schemas.ts | 50 | ||||
-rw-r--r-- | src/types.ts | 83 | ||||
-rw-r--r-- | src/utils/assert.ts | 3 | ||||
-rw-r--r-- | src/utils/schema_validator.ts | 16 | ||||
-rw-r--r-- | src/web3_wrapper.ts | 27 |
9 files changed, 449 insertions, 75 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts index d231c579e..7cf313666 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -1,71 +1,39 @@ import * as _ from 'lodash'; import * as BigNumber from 'bignumber.js'; +import {bigNumberConfigs} from './bignumber_config'; import * as ethUtil from 'ethereumjs-util'; import contract = require('truffle-contract'); import * as Web3 from 'web3'; import * as ethABI from 'ethereumjs-abi'; +import findVersions = require('find-versions'); +import compareVersions = require('compare-versions'); import {Web3Wrapper} from './web3_wrapper'; import {constants} from './utils/constants'; import {utils} from './utils/utils'; import {assert} from './utils/assert'; -import findVersions = require('find-versions'); -import compareVersions = require('compare-versions'); +import {SchemaValidator} from './utils/schema_validator'; import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; import {ecSignatureSchema} from './schemas/ec_signature_schema'; import {TokenWrapper} from './contract_wrappers/token_wrapper'; import {SolidityTypes, ECSignature, ZeroExError} from './types'; +import {Order} from './types'; +import {orderSchema} from './schemas/order_schemas'; +import * as ExchangeArtifacts from './artifacts/Exchange.json'; + +// Customize our BigNumber instances +bigNumberConfigs.configure(); const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; export class ZeroEx { + public static NULL_ADDRESS = constants.NULL_ADDRESS; + public exchange: ExchangeWrapper; public tokenRegistry: TokenRegistryWrapper; public token: TokenWrapper; private web3Wrapper: Web3Wrapper; /** - * Computes the orderHash given the order parameters and returns it as a hex encoded string. - */ - public static getOrderHashHex(exchangeContractAddr: string, makerAddr: string, takerAddr: string, - tokenMAddress: string, tokenTAddress: string, feeRecipient: string, - valueM: BigNumber.BigNumber, valueT: BigNumber.BigNumber, - makerFee: BigNumber.BigNumber, takerFee: BigNumber.BigNumber, - expiration: BigNumber.BigNumber, salt: BigNumber.BigNumber): string { - takerAddr = _.isEmpty(takerAddr) ? constants.NULL_ADDRESS : takerAddr ; - assert.isETHAddressHex('exchangeContractAddr', exchangeContractAddr); - assert.isETHAddressHex('makerAddr', makerAddr); - assert.isETHAddressHex('takerAddr', takerAddr); - assert.isETHAddressHex('tokenMAddress', tokenMAddress); - assert.isETHAddressHex('tokenTAddress', tokenTAddress); - assert.isETHAddressHex('feeRecipient', feeRecipient); - assert.isBigNumber('valueM', valueM); - assert.isBigNumber('valueT', valueT); - assert.isBigNumber('makerFee', makerFee); - assert.isBigNumber('takerFee', takerFee); - assert.isBigNumber('expiration', expiration); - assert.isBigNumber('salt', salt); - - const orderParts = [ - {value: exchangeContractAddr, type: SolidityTypes.address}, - {value: makerAddr, type: SolidityTypes.address}, - {value: takerAddr, type: SolidityTypes.address}, - {value: tokenMAddress, type: SolidityTypes.address}, - {value: tokenTAddress, type: SolidityTypes.address}, - {value: feeRecipient, type: SolidityTypes.address}, - {value: utils.bigNumberToBN(valueM), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(valueT), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(makerFee), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(takerFee), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(expiration), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(salt), type: SolidityTypes.uint256}, - ]; - const types = _.map(orderParts, o => o.type); - const values = _.map(orderParts, o => o.value); - const hashBuff = ethABI.soliditySHA3(types, values); - const hashHex = ethUtil.bufferToHex(hashBuff); - return hashHex; - } - /** * Verifies that the elliptic curve signature `signature` was generated * by signing `data` with the private key corresponding to the `signerAddressHex` address. */ @@ -135,9 +103,9 @@ export class ZeroEx { } constructor(web3: Web3) { this.web3Wrapper = new Web3Wrapper(web3); - this.exchange = new ExchangeWrapper(this.web3Wrapper); - this.tokenRegistry = new TokenRegistryWrapper(this.web3Wrapper); this.token = new TokenWrapper(this.web3Wrapper); + this.exchange = new ExchangeWrapper(this.web3Wrapper, this.token); + this.tokenRegistry = new TokenRegistryWrapper(this.web3Wrapper); } /** * Sets a new provider for the web3 instance used by 0x.js @@ -149,12 +117,56 @@ export class ZeroEx { this.token.invalidateContractInstances(); } /** + * Sets default account for sending transactions. + */ + public setTransactionSenderAccount(account: string): void { + this.web3Wrapper.setDefaultAccount(account); + } + /** + * Get the default account set for sending transactions. + */ + public async getTransactionSenderAccountIfExistsAsync(): Promise<string|undefined> { + const senderAccountIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync(); + return senderAccountIfExists; + } + /** + * Computes the orderHash for a given order and returns it as a hex encoded string. + */ + public async getOrderHashHexAsync(order: Order): Promise<string> { + const exchangeContractAddr = await this.getExchangeAddressAsync(); + assert.doesConformToSchema('order', + SchemaValidator.convertToJSONSchemaCompatibleObject(order as object), + orderSchema); + + const orderParts = [ + {value: exchangeContractAddr, type: SolidityTypes.address}, + {value: order.maker, type: SolidityTypes.address}, + {value: order.taker, type: SolidityTypes.address}, + {value: order.makerTokenAddress, type: SolidityTypes.address}, + {value: order.takerTokenAddress, type: SolidityTypes.address}, + {value: order.feeRecipient, type: SolidityTypes.address}, + {value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.salt), type: SolidityTypes.uint256}, + ]; + const types = _.map(orderParts, o => o.type); + const values = _.map(orderParts, o => o.value); + const hashBuff = ethABI.soliditySHA3(types, values); + const hashHex = ethUtil.bufferToHex(hashBuff); + return hashHex; + } + /** * Signs an orderHash and returns it's elliptic curve signature * This method currently supports TestRPC, Geth and Parity above and below V1.6.6 */ public async signOrderHashAsync(orderHashHex: string): Promise<ECSignature> { assert.isHexString('orderHashHex', orderHashHex); + const makerAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + let msgHashHex; const nodeVersion = await this.web3Wrapper.getNodeVersionAsync(); const isParityNode = utils.isParityNode(nodeVersion); @@ -167,12 +179,7 @@ export class ZeroEx { msgHashHex = ethUtil.bufferToHex(msgHashBuff); } - const makerAddressIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync(); - if (_.isUndefined(makerAddressIfExists)) { - throw new Error(ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES); - } - - const signature = await this.web3Wrapper.signTransactionAsync(makerAddressIfExists, msgHashHex); + const signature = await this.web3Wrapper.signTransactionAsync(makerAddress, msgHashHex); let signatureData; const [nodeVersionNumber] = findVersions(nodeVersion); @@ -202,10 +209,21 @@ export class ZeroEx { r: ethUtil.bufferToHex(r), s: ethUtil.bufferToHex(s), }; - const isValidSignature = ZeroEx.isValidSignature(orderHashHex, ecSignature, makerAddressIfExists); + const isValidSignature = ZeroEx.isValidSignature(orderHashHex, ecSignature, makerAddress); if (!isValidSignature) { throw new Error(ZeroExError.INVALID_SIGNATURE); } return ecSignature; } + private async getExchangeAddressAsync() { + const networkIdIfExists = await this.web3Wrapper.getNetworkIdIfExistsAsync(); + const exchangeNetworkConfigsIfExists = _.isUndefined(networkIdIfExists) ? + undefined : + (ExchangeArtifacts as any).networks[networkIdIfExists]; + if (_.isUndefined(exchangeNetworkConfigsIfExists)) { + throw new Error(ZeroExError.CONTRACT_NOT_DEPLOYED_ON_NETWORK); + } + const exchangeAddress = exchangeNetworkConfigsIfExists.address; + return exchangeAddress; + } } diff --git a/src/bignumber_config.ts b/src/bignumber_config.ts new file mode 100644 index 000000000..9c1715f86 --- /dev/null +++ b/src/bignumber_config.ts @@ -0,0 +1,11 @@ +import * as BigNumber from 'bignumber.js'; + +export const bigNumberConfigs = { + configure() { + // By default BigNumber's `toString` method converts to exponential notation if the value has + // more then 20 digits. We want to avoid this behavior, so we set EXPONENTIAL_AT to a high number + BigNumber.config({ + EXPONENTIAL_AT: 1000, + }); + }, +}; diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 3f6eb0dab..4aa532bdd 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -1,15 +1,39 @@ import * as _ from 'lodash'; import {Web3Wrapper} from '../web3_wrapper'; -import {ECSignature, ZeroExError, ExchangeContract} from '../types'; +import { + ECSignature, + ExchangeContract, + ExchangeContractErrCodes, + ExchangeContractErrs, + OrderValues, + OrderAddresses, + SignedOrder, + ContractEvent, + ContractResponse, +} from '../types'; import {assert} from '../utils/assert'; import {ContractWrapper} from './contract_wrapper'; import * as ExchangeArtifacts from '../artifacts/Exchange.json'; import {ecSignatureSchema} from '../schemas/ec_signature_schema'; +import {signedOrderSchema} from '../schemas/order_schemas'; +import {SchemaValidator} from '../utils/schema_validator'; +import {constants} from '../utils/constants'; +import {TokenWrapper} from './token_wrapper'; export class ExchangeWrapper extends ContractWrapper { + private exchangeContractErrCodesToMsg = { + [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.ORDER_FILL_EXPIRED, + [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.ORDER_FILL_EXPIRED, + [ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO, + [ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO, + [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR, + [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FILL_BALANCE_ALLOWANCE_ERROR, + }; private exchangeContractIfExists?: ExchangeContract; - constructor(web3Wrapper: Web3Wrapper) { + private tokenWrapper: TokenWrapper; + constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) { super(web3Wrapper); + this.tokenWrapper = tokenWrapper; } public invalidateContractInstance(): void { delete this.exchangeContractIfExists; @@ -20,23 +44,188 @@ export class ExchangeWrapper extends ContractWrapper { assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema); assert.isETHAddressHex('signerAddressHex', signerAddressHex); - const senderAddressIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync(); - assert.assert(!_.isUndefined(senderAddressIfExists), ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES); + const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + const exchangeInstance = await this.getExchangeContractAsync(); - const exchangeContract = await this.getExchangeContractAsync(); - - const isValidSignature = await exchangeContract.isValidSignature.call( + const isValidSignature = await exchangeInstance.isValidSignature.call( signerAddressHex, dataHex, ecSignature.v, ecSignature.r, ecSignature.s, { - from: senderAddressIfExists, + from: senderAddress, }, ); return isValidSignature; } + /** + * Fills a signed order with a fillAmount denominated in baseUnits of the taker token. + * Since the order in which transactions are included in the next block is indeterminate, race-conditions + * could arise where a users balance or allowance changes before the fillOrder executes. Because of this, + * we allow you to specify `shouldCheckTransfer`. If true, the smart contract will not throw if while + * executing, the parties do not have sufficient balances/allowances, preserving gas costs. Setting it to + * false forgoes this check and causes the smart contract to throw instead. + */ + public async fillOrderAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean): Promise<void> { + assert.doesConformToSchema('signedOrder', + SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object), + signedOrderSchema); + assert.isBigNumber('fillTakerAmount', fillTakerAmount); + assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); + + const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + const exchangeInstance = await this.getExchangeContractAsync(); + await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, senderAddress); + + const orderAddresses: OrderAddresses = [ + signedOrder.maker, + signedOrder.taker, + signedOrder.makerTokenAddress, + signedOrder.takerTokenAddress, + signedOrder.feeRecipient, + ]; + const orderValues: OrderValues = [ + signedOrder.makerTokenAmount, + signedOrder.takerTokenAmount, + signedOrder.makerFee, + signedOrder.takerFee, + signedOrder.expirationUnixTimestampSec, + signedOrder.salt, + ]; + const gas = await exchangeInstance.fill.estimateGas( + orderAddresses, + orderValues, + fillTakerAmount, + shouldCheckTransfer, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: senderAddress, + }, + ); + const response: ContractResponse = await exchangeInstance.fill( + orderAddresses, + orderValues, + fillTakerAmount, + shouldCheckTransfer, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: senderAddress, + gas, + }, + ); + this.throwErrorLogsAsErrors(response.logs); + } + private async validateFillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerAmount: BigNumber.BigNumber, + senderAddress: string): Promise<void> { + if (fillTakerAmount.eq(0)) { + throw new Error(ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO); + } + if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== senderAddress) { + throw new Error(ExchangeContractErrs.TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER); + } + const currentUnixTimestampSec = Date.now() / 1000; + if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.ORDER_FILL_EXPIRED); + } + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + await this.validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, + senderAddress, zrxTokenAddress); + + const wouldRoundingErrorOccur = await this.isRoundingErrorAsync( + signedOrder.takerTokenAmount, fillTakerAmount, signedOrder.makerTokenAmount, + ); + if (wouldRoundingErrorOccur) { + throw new Error(ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR); + } + } + + /** + * 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. + * 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. + */ + private async validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerAmount: BigNumber.BigNumber, + senderAddress: string, + zrxTokenAddress: string): Promise<void> { + + const makerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.makerTokenAddress, + signedOrder.maker); + const takerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.takerTokenAddress, senderAddress); + const makerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(signedOrder.makerTokenAddress, + signedOrder.maker); + const takerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(signedOrder.takerTokenAddress, + senderAddress); + + // exchangeRate is the price of one maker token denominated in taker tokens + const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount); + const fillMakerAmountInBaseUnits = fillTakerAmount.div(exchangeRate); + + if (fillTakerAmount.greaterThan(takerBalance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_BALANCE); + } + if (fillTakerAmount.greaterThan(takerAllowance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_ALLOWANCE); + } + if (fillMakerAmountInBaseUnits.greaterThan(makerBalance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_BALANCE); + } + if (fillMakerAmountInBaseUnits.greaterThan(makerAllowance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_ALLOWANCE); + } + + const makerFeeBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, + signedOrder.maker); + const takerFeeBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, senderAddress); + const makerFeeAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress, + signedOrder.maker); + const takerFeeAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress, + senderAddress); + + if (signedOrder.takerFee.greaterThan(takerFeeBalance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_FEE_BALANCE); + } + if (signedOrder.takerFee.greaterThan(takerFeeAllowance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_FEE_ALLOWANCE); + } + if (signedOrder.makerFee.greaterThan(makerFeeBalance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_FEE_BALANCE); + } + if (signedOrder.makerFee.greaterThan(makerFeeAllowance)) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_FEE_ALLOWANCE); + } + } + private throwErrorLogsAsErrors(logs: ContractEvent[]): void { + const errEvent = _.find(logs, {event: 'LogError'}); + if (!_.isUndefined(errEvent)) { + const errCode = errEvent.args.errorId.toNumber(); + const errMessage = this.exchangeContractErrCodesToMsg[errCode]; + throw new Error(errMessage); + } + } + private async isRoundingErrorAsync(takerTokenAmount: BigNumber.BigNumber, + fillTakerAmount: BigNumber.BigNumber, + makerTokenAmount: BigNumber.BigNumber): Promise<boolean> { + const exchangeInstance = await this.getExchangeContractAsync(); + const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + const isRoundingError = await exchangeInstance.isRoundingError.call( + takerTokenAmount, fillTakerAmount, makerTokenAmount, { + from: senderAddress, + }, + ); + return isRoundingError; + } private async getExchangeContractAsync(): Promise<ExchangeContract> { if (!_.isUndefined(this.exchangeContractIfExists)) { return this.exchangeContractIfExists; @@ -45,4 +234,8 @@ export class ExchangeWrapper extends ContractWrapper { this.exchangeContractIfExists = contractInstance as ExchangeContract; return this.exchangeContractIfExists; } + private async getZRXTokenAddressAsync(): Promise<string> { + const exchangeInstance = await this.getExchangeContractAsync(); + return exchangeInstance.ZRX.call(); + } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 0f2fe0f2f..d86f54dfc 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,4 +1,5 @@ declare module 'chai-bignumber'; +declare module 'dirty-chai'; declare module 'bn.js'; declare module 'request-promise-native'; declare module 'web3-provider-engine'; diff --git a/src/schemas/order_schemas.ts b/src/schemas/order_schemas.ts new file mode 100644 index 000000000..72012dc26 --- /dev/null +++ b/src/schemas/order_schemas.ts @@ -0,0 +1,50 @@ +export const addressSchema = { + id: '/addressSchema', + type: 'string', + pattern: '^0[xX][0-9A-Fa-f]{40}$', +}; + +export const numberSchema = { + id: '/numberSchema', + type: 'string', + format: '\d+(\.\d+)?', +}; + +export const orderSchema = { + id: '/orderSchema', + properties: { + maker: {$ref: '/addressSchema'}, + taker: {$ref: '/addressSchema'}, + + makerFee: {$ref: '/numberSchema'}, + takerFee: {$ref: '/numberSchema'}, + + makerTokenAmount: {$ref: '/numberSchema'}, + takerTokenAmount: {$ref: '/numberSchema'}, + + makerTokenAddress: {$ref: '/addressSchema'}, + takerTokenAddress: {$ref: '/addressSchema'}, + + salt: {$ref: '/numberSchema'}, + feeRecipient: {$ref: '/addressSchema'}, + expirationUnixTimestampSec: {$ref: '/numberSchema'}, + }, + required: [ + 'maker', 'taker', 'makerFee', 'takerFee', 'makerTokenAmount', 'takerTokenAmount', + 'salt', 'feeRecipient', 'expirationUnixTimestampSec', + ], + type: 'object', +}; + +export const signedOrderSchema = { + id: '/signedOrderSchema', + allOf: [ + { $ref: '/orderSchema' }, + { + properties: { + ecSignature: {$ref: '/ECSignature'}, + }, + required: ['ecSignature'], + }, + ], +}; diff --git a/src/types.ts b/src/types.ts index 717257492..3da24abc1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,11 +10,12 @@ function strEnum(values: string[]): {[key: string]: string} { } export const ZeroExError = strEnum([ - 'CONTRACT_DOES_NOT_EXIST', - 'UNHANDLED_ERROR', - 'USER_HAS_NO_ASSOCIATED_ADDRESSES', - 'INVALID_SIGNATURE', - 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', + 'CONTRACT_DOES_NOT_EXIST', + 'UNHANDLED_ERROR', + 'USER_HAS_NO_ASSOCIATED_ADDRESSES', + 'INVALID_SIGNATURE', + 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', + 'ZRX_NOT_IN_TOKEN_REGISTRY', ]); export type ZeroExError = keyof typeof ZeroExError; @@ -27,8 +28,26 @@ export interface ECSignature { s: string; } +export type OrderAddresses = [string, string, string, string, string]; + +export type OrderValues = [BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber, + BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber]; + export interface ExchangeContract { isValidSignature: any; + isRoundingError: { + call: (takerTokenAmount: BigNumber.BigNumber, fillTakerAmount: BigNumber.BigNumber, + makerTokenAmount: BigNumber.BigNumber, txOpts: TxOpts) => Promise<boolean>; + }; + fill: { + (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts): ContractResponse; + estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts) => number; + }; + ZRX: { + call: () => Promise<string>; + }; } export interface TokenContract { @@ -57,6 +76,60 @@ export const SolidityTypes = strEnum([ ]); export type SolidityTypes = keyof typeof SolidityTypes; +export enum ExchangeContractErrCodes { + ERROR_FILL_EXPIRED, // Order has already expired + ERROR_FILL_NO_VALUE, // Order has already been fully filled or cancelled + ERROR_FILL_TRUNCATION, // Rounding error too large + ERROR_FILL_BALANCE_ALLOWANCE, // Insufficient balance or allowance for token transfer + ERROR_CANCEL_EXPIRED, // Order has already expired + ERROR_CANCEL_NO_VALUE, // Order has already been fully filled or cancelled +} + +export const ExchangeContractErrs = strEnum([ + 'ORDER_FILL_EXPIRED', + 'ORDER_REMAINING_FILL_AMOUNT_ZERO', + 'ORDER_FILL_ROUNDING_ERROR', + 'FILL_BALANCE_ALLOWANCE_ERROR', + 'INSUFFICIENT_TAKER_BALANCE', + 'INSUFFICIENT_TAKER_ALLOWANCE', + 'INSUFFICIENT_MAKER_BALANCE', + 'INSUFFICIENT_MAKER_ALLOWANCE', + 'INSUFFICIENT_TAKER_FEE_BALANCE', + 'INSUFFICIENT_TAKER_FEE_ALLOWANCE', + 'INSUFFICIENT_MAKER_FEE_BALANCE', + 'INSUFFICIENT_MAKER_FEE_ALLOWANCE', + 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER', + +]); +export type ExchangeContractErrs = keyof typeof ExchangeContractErrs; + +export interface ContractResponse { + logs: ContractEvent[]; +} + +export interface ContractEvent { + event: string; + args: any; +} + +export interface Order { + maker: string; + taker: string; + makerFee: BigNumber.BigNumber; + takerFee: BigNumber.BigNumber; + makerTokenAmount: BigNumber.BigNumber; + takerTokenAmount: BigNumber.BigNumber; + makerTokenAddress: string; + takerTokenAddress: string; + salt: BigNumber.BigNumber; + feeRecipient: string; + expirationUnixTimestampSec: BigNumber.BigNumber; +} + +export interface SignedOrder extends Order { + ecSignature: ECSignature; +} + // [address, name, symbol, projectUrl, decimals, ipfsHash, swarmHash] export type TokenMetadata = [string, string, string, string, BigNumber.BigNumber, string, string]; diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 1baf572d1..aeed1c6dc 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -27,6 +27,9 @@ export const assert = { isNumber(variableName: string, value: number): void { this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value)); }, + isBoolean(variableName: string, value: boolean): void { + this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value)); + }, doesConformToSchema(variableName: string, value: object, schema: Schema): void { const schemaValidator = new SchemaValidator(); const validationResult = schemaValidator.validate(value, schema); diff --git a/src/utils/schema_validator.ts b/src/utils/schema_validator.ts index 8132f7414..932ddf62a 100644 --- a/src/utils/schema_validator.ts +++ b/src/utils/schema_validator.ts @@ -1,14 +1,26 @@ import {Validator, ValidatorResult} from 'jsonschema'; import {ecSignatureSchema, ecSignatureParameter} from '../schemas/ec_signature_schema'; +import {addressSchema, numberSchema, orderSchema, signedOrderSchema} from '../schemas/order_schemas'; import {tokenSchema} from '../schemas/token_schema'; export class SchemaValidator { private validator: Validator; + // In order to validate a complex JS object using jsonschema, we must replace any complex + // 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 { + return JSON.parse(JSON.stringify(obj)); + } constructor() { this.validator = new Validator(); - this.validator.addSchema(ecSignatureParameter, ecSignatureParameter.id); - this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id); this.validator.addSchema(tokenSchema, tokenSchema.id); + this.validator.addSchema(orderSchema, orderSchema.id); + this.validator.addSchema(numberSchema, numberSchema.id); + this.validator.addSchema(addressSchema, addressSchema.id); + this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id); + this.validator.addSchema(signedOrderSchema, signedOrderSchema.id); + this.validator.addSchema(ecSignatureParameter, ecSignatureParameter.id); } public validate(instance: object, schema: Schema): ValidatorResult { return this.validator.validate(instance, schema); diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index e65f29b56..49bd8b67d 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -2,6 +2,8 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import * as BigNumber from 'bignumber.js'; import promisify = require('es6-promisify'); +import {ZeroExError} from './types'; +import {assert} from './utils/assert'; export class Web3Wrapper { private web3: Web3; @@ -15,13 +17,16 @@ export class Web3Wrapper { public isAddress(address: string): boolean { return this.web3.isAddress(address); } - public async getSenderAddressIfExistsAsync(): Promise<string|undefined> { - const defaultAccount = this.web3.eth.defaultAccount; - if (!_.isUndefined(defaultAccount)) { - return defaultAccount; - } - const firstAccount = await this.getFirstAddressIfExistsAsync(); - return firstAccount; + public getDefaultAccount(): string { + return this.web3.eth.defaultAccount; + } + public setDefaultAccount(address: string): void { + this.web3.eth.defaultAccount = address; + } + public async getSenderAddressOrThrowAsync(): Promise<string> { + const senderAddressIfExists = await this.getSenderAddressIfExistsAsync(); + assert.assert(!_.isUndefined(senderAddressIfExists), ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES); + return senderAddressIfExists as string; } public async getFirstAddressIfExistsAsync(): Promise<string|undefined> { const addresses = await promisify(this.web3.eth.getAccounts)(); @@ -64,6 +69,14 @@ export class Web3Wrapper { const {timestamp} = await promisify(this.web3.eth.getBlock)(blockHash); return timestamp; } + public async getSenderAddressIfExistsAsync(): Promise<string|undefined> { + const defaultAccount = this.web3.eth.defaultAccount; + if (!_.isUndefined(defaultAccount)) { + return defaultAccount; + } + const firstAccount = await this.getFirstAddressIfExistsAsync(); + return firstAccount; + } private async getNetworkAsync(): Promise<number> { const networkId = await promisify(this.web3.version.getNetwork)(); return networkId; |