diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/0x.js.ts | 125 | ||||
-rw-r--r-- | src/artifacts/ProtocolToken.json | 321 | ||||
-rw-r--r-- | src/bignumber_config.ts | 11 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 194 | ||||
-rw-r--r-- | src/globals.d.ts | 1 | ||||
-rw-r--r-- | src/schemas/order_schemas.ts | 50 | ||||
-rw-r--r-- | src/types.ts | 78 | ||||
-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 |
10 files changed, 756 insertions, 70 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts index d231c579e..679d748e7 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 @@ -148,6 +116,49 @@ export class ZeroEx { this.tokenRegistry.invalidateContractInstance(); 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 given the order parameters 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 @@ -155,6 +166,8 @@ export class ZeroEx { 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 +180,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 +210,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/artifacts/ProtocolToken.json b/src/artifacts/ProtocolToken.json new file mode 100644 index 000000000..a1f8e0ff6 --- /dev/null +++ b/src/artifacts/ProtocolToken.json @@ -0,0 +1,321 @@ +{ + "contract_name": "ProtocolToken", + "abi": [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "remaining", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "inputs": [], + "payable": false, + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } + ], + "unlinked_binary": "0x606060405234610000575b6a52b7d2dcc80cd2e40000006002819055600160a060020a0333166000908152602081905260409020555b5b6105fc806100456000396000f3006060604052361561007d5763ffffffff60e060020a60003504166306fdde038114610082578063095ea7b31461010f57806318160ddd1461013f57806323b872dd1461015e578063313ce5671461019457806370a08231146101b757806395d89b41146101e2578063a9059cbb1461026f578063dd62ed3e1461029f575b610000565b346100005761008f6102d0565b6040805160208082528351818301528351919283929083019185019080838382156100d5575b8051825260208311156100d557601f1990920191602091820191016100b5565b505050905090810190601f1680156101015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b346100005761012b600160a060020a0360043516602435610307565b604080519115158252519081900360200190f35b346100005761014c610372565b60408051918252519081900360200190f35b346100005761012b600160a060020a0360043581169060243516604435610378565b604080519115158252519081900360200190f35b34610000576101a1610485565b6040805160ff9092168252519081900360200190f35b346100005761014c600160a060020a036004351661048a565b60408051918252519081900360200190f35b346100005761008f6104a9565b6040805160208082528351818301528351919283929083019185019080838382156100d5575b8051825260208311156100d557601f1990920191602091820191016100b5565b505050905090810190601f1680156101015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b346100005761012b600160a060020a03600435166024356104e0565b604080519115158252519081900360200190f35b346100005761014c600160a060020a03600435811690602435166105a3565b60408051918252519081900360200190f35b60408051808201909152601081527f3078204e6574776f726b20546f6b656e00000000000000000000000000000000602082015281565b600160a060020a03338116600081815260016020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b60025481565b600160a060020a0383166000908152602081905260408120548290108015906103c85750600160a060020a0380851660009081526001602090815260408083203390941683529290522054829010155b80156103ed5750600160a060020a038316600090815260208190526040902054828101115b1561047957600160a060020a0380841660008181526020818152604080832080548801905588851680845281842080548990039055600183528184203390961684529482529182902080548790039055815186815291519293927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a350600161047d565b5060005b5b9392505050565b601281565b600160a060020a0381166000908152602081905260409020545b919050565b60408051808201909152600381527f5a52580000000000000000000000000000000000000000000000000000000000602082015281565b600160a060020a0333166000908152602081905260408120548290108015906105225750600160a060020a038316600090815260208190526040902054828101115b1561059457600160a060020a0333811660008181526020818152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a350600161036c565b50600061036c565b5b92915050565b600160a060020a038083166000908152600160209081526040808320938516835292905220545b929150505600a165627a7a723058209912b9e0769f5bfbb24cc18ed6298aba4698371192fbf585b239f6db1b8bdc2f0029", + "networks": { + "42": { + "links": {}, + "events": { + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } + }, + "updated_at": 1495042008608 + }, + "50": { + "links": {}, + "events": { + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } + }, + "updated_at": 1495030736785 + } + }, + "schema_version": "0.0.5", + "updated_at": 1495042008608 +}
\ No newline at end of file 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..fa5c16485 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -1,15 +1,40 @@ import * as _ from 'lodash'; import {Web3Wrapper} from '../web3_wrapper'; -import {ECSignature, ZeroExError, ExchangeContract} from '../types'; +import { + ECSignature, + ExchangeContract, + ExchangeContractErrCodes, + ExchangeContractErrs, + FillOrderValidationErrs, + OrderValues, + OrderAddresses, + SignedOrder, + ContractEvent, +} 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 {ContractResponse} from '../types'; +import {constants} from '../utils/constants'; +import {TokenWrapper} from './token_wrapper'; export class ExchangeWrapper extends ContractWrapper { + private exchangeContractErrCodesToMsg = { + [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.ORDER_EXPIRED, + [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.ORDER_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_ROUNDING_ERROR, + [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.ORDER_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 +45,176 @@ 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. The caller can + * decide whether they want the call to throw if the balance/allowance checks fail by setting + * shouldCheckTransfer to false. If set to true, the call will fail without throwing, preserving gas costs. + */ + public async fillOrderAsync(signedOrder: SignedOrder, fillTakerAmountInBaseUnits: BigNumber.BigNumber, + shouldCheckTransfer: boolean): Promise<void> { + assert.doesConformToSchema('signedOrder', + SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object), + signedOrderSchema); + assert.isBigNumber('fillTakerAmountInBaseUnits', fillTakerAmountInBaseUnits); + assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); + + const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + const exchangeInstance = await this.getExchangeContractAsync(); + const zrxTokenAddress = await exchangeInstance.ZRX.call(); + await this.validateFillOrderAsync(signedOrder, fillTakerAmountInBaseUnits, senderAddress, zrxTokenAddress); + + 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, + fillTakerAmountInBaseUnits, + shouldCheckTransfer, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: senderAddress, + }, + ); + const response: ContractResponse = await exchangeInstance.fill( + orderAddresses, + orderValues, + fillTakerAmountInBaseUnits, + shouldCheckTransfer, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: senderAddress, + gas, + }, + ); + this.throwErrorLogsAsErrors(response.logs); + } + private async validateFillOrderAsync(signedOrder: SignedOrder, fillTakerAmountInBaseUnits: BigNumber.BigNumber, + senderAddress: string, zrxTokenAddress: string): Promise<void> { + if (fillTakerAmountInBaseUnits.eq(0)) { + throw new Error(FillOrderValidationErrs.FILL_AMOUNT_IS_ZERO); + } + if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== senderAddress) { + throw new Error(FillOrderValidationErrs.NOT_A_TAKER); + } + if (signedOrder.expirationUnixTimestampSec.lessThan(Date.now() / 1000)) { + throw new Error(FillOrderValidationErrs.EXPIRED); + } + + await this.validateFillOrderBalancesAndAllowancesAsync(signedOrder, fillTakerAmountInBaseUnits, + senderAddress, zrxTokenAddress); + + if (await this.isRoundingErrorAsync(signedOrder.takerTokenAmount, fillTakerAmountInBaseUnits, + signedOrder.makerTokenAmount)) { + throw new Error(FillOrderValidationErrs.ROUNDING_ERROR); + } + } + private async validateFillOrderBalancesAndAllowancesAsync(signedOrder: SignedOrder, + fillTakerAmountInBaseUnits: BigNumber.BigNumber, + senderAddress: string, + zrxTokenAddress: string): Promise<void> { + // TODO: There is a possibility that the user might have enough funds + // to fulfill the order or pay fees but not both. This will happen if + // makerToken === zrxToken || makerToken === zrxToken + // We don't check it for now. The contract checks it and throws. + + 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); + + // How many taker tokens would you get for 1 maker token; + const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount); + const fillMakerAmountInBaseUnits = fillTakerAmountInBaseUnits.div(exchangeRate); + + if (fillTakerAmountInBaseUnits.greaterThan(takerBalance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_BALANCE); + } + if (fillTakerAmountInBaseUnits.greaterThan(takerAllowance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_ALLOWANCE); + } + if (fillMakerAmountInBaseUnits.greaterThan(makerBalance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_BALANCE); + } + if (fillMakerAmountInBaseUnits.greaterThan(makerAllowance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_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(FillOrderValidationErrs.NOT_ENOUGH_TAKER_FEE_BALANCE); + } + if (signedOrder.takerFee.greaterThan(takerFeeAllowance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_FEE_ALLOWANCE); + } + if (signedOrder.makerFee.greaterThan(makerFeeBalance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_FEE_BALANCE); + } + if (signedOrder.makerFee.greaterThan(makerFeeAllowance)) { + throw new Error(FillOrderValidationErrs.NOT_ENOUGH_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, + fillTakerAmountInBaseUnits: 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, fillTakerAmountInBaseUnits, makerTokenAmount, { + from: senderAddress, + }, + ); + return isRoundingError; + } private async getExchangeContractAsync(): Promise<ExchangeContract> { if (!_.isUndefined(this.exchangeContractIfExists)) { return this.exchangeContractIfExists; 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..4819b13ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,8 +27,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, fillTakerAmountInBaseUnits: 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 +75,66 @@ 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_EXPIRED', + 'ORDER_REMAINING_FILL_AMOUNT_ZERO', + 'ORDER_ROUNDING_ERROR', + 'ORDER_BALANCE_ALLOWANCE_ERROR', +]); +export type ExchangeContractErrs = keyof typeof ExchangeContractErrs; + +export const FillOrderValidationErrs = strEnum([ + 'FILL_AMOUNT_IS_ZERO', + 'NOT_A_TAKER', + 'EXPIRED', + 'NOT_ENOUGH_TAKER_BALANCE', + 'NOT_ENOUGH_TAKER_ALLOWANCE', + 'NOT_ENOUGH_MAKER_BALANCE', + 'NOT_ENOUGH_MAKER_ALLOWANCE', + 'NOT_ENOUGH_TAKER_FEE_BALANCE', + 'NOT_ENOUGH_TAKER_FEE_ALLOWANCE', + 'NOT_ENOUGH_MAKER_FEE_BALANCE', + 'NOT_ENOUGH_MAKER_FEE_ALLOWANCE', + 'ROUNDING_ERROR', +]); +export type FillOrderValidationErrs = keyof typeof FillOrderValidationErrs; + +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..db8a960ba 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 represenation. 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; |