diff options
Diffstat (limited to 'packages')
47 files changed, 934 insertions, 370 deletions
diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index b2aebf803..c2545b7da 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -1,5 +1,18 @@ [ { + "version": "0.38.0", + "changes": [ + { + "note": "Add `zeroEx.getProvider()`", + "pr": 559 + }, + { + "note": "Move `ZeroExError.InvalidSignature` to `@0xproject/order-utils` `OrderError.InvalidSignature`", + "pr": 559 + } + ] + }, + { "version": "0.37.0", "changes": [ { diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 24fd44413..33c29a788 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -103,6 +103,7 @@ "@0xproject/types": "^0.6.1", "@0xproject/typescript-typings": "^0.2.0", "@0xproject/utils": "^0.5.2", + "@0xproject/order-utils": "^0.0.1", "@0xproject/web3-wrapper": "^0.6.1", "bintrees": "^1.0.2", "bn.js": "^4.11.8", diff --git a/packages/0x.js/src/0x.ts b/packages/0x.js/src/0x.ts index 94d97c23e..78f9f6beb 100644 --- a/packages/0x.js/src/0x.ts +++ b/packages/0x.js/src/0x.ts @@ -1,4 +1,11 @@ import { schemas, SchemaValidator } from '@0xproject/json-schemas'; +import { + generatePseudoRandomSalt, + getOrderHashHex, + isValidOrderHash, + isValidSignature, + signOrderHashAsync, +} from '@0xproject/order-utils'; import { ECSignature, Order, Provider, SignedOrder, TransactionReceiptWithDecodedLogs } from '@0xproject/types'; import { AbiDecoder, BigNumber, intervalUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; @@ -19,7 +26,6 @@ import { OrderStateWatcherConfig, ZeroExConfig, ZeroExError } from './types'; import { assert } from './utils/assert'; import { constants } from './utils/constants'; import { decorators } from './utils/decorators'; -import { signatureUtils } from './utils/signature_utils'; import { utils } from './utils/utils'; /** @@ -33,7 +39,6 @@ export class ZeroEx { * this constant for your convenience. */ public static NULL_ADDRESS = constants.NULL_ADDRESS; - /** * An instance of the ExchangeWrapper class containing methods for interacting with the 0x Exchange smart contract. */ @@ -59,6 +64,15 @@ export class ZeroEx { public proxy: TokenTransferProxyWrapper; private _web3Wrapper: Web3Wrapper; /** + * Generates a pseudo-random 256-bit salt. + * The salt can be included in a 0x order, ensuring that the order generates a unique orderHash + * and will not collide with other outstanding orders that are identical in all other parameters. + * @return A pseudo-random 256-bit number that can be used as a salt. + */ + public static generatePseudoRandomSalt(): BigNumber { + return generatePseudoRandomSalt(); + } + /** * Verifies that the elliptic curve signature `signature` was generated * by signing `data` with the private key corresponding to the `signerAddress` address. * @param data The hex encoded data signed by the supplied signature. @@ -67,27 +81,15 @@ export class ZeroEx { * @return Whether the signature is valid for the supplied signerAddress and data. */ public static isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean { - assert.isHexString('data', data); - assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema); - assert.isETHAddressHex('signerAddress', signerAddress); - const normalizedSignerAddress = signerAddress.toLowerCase(); - - const isValidSignature = signatureUtils.isValidSignature(data, signature, normalizedSignerAddress); - return isValidSignature; + return isValidSignature(data, signature, signerAddress); } /** - * Generates a pseudo-random 256-bit salt. - * The salt can be included in a 0x order, ensuring that the order generates a unique orderHash - * and will not collide with other outstanding orders that are identical in all other parameters. - * @return A pseudo-random 256-bit number that can be used as a salt. + * Computes the orderHash for a supplied order. + * @param order An object that conforms to the Order or SignedOrder interface definitions. + * @return The resulting orderHash from hashing the supplied order. */ - public static generatePseudoRandomSalt(): BigNumber { - // BigNumber.random returns a pseudo-random number between 0 & 1 with a passed in number of decimal places. - // Source: https://mikemcl.github.io/bignumber.js/#random - const randomNumber = BigNumber.random(constants.MAX_DIGITS_IN_UNSIGNED_256_INT); - const factor = new BigNumber(10).pow(constants.MAX_DIGITS_IN_UNSIGNED_256_INT - 1); - const salt = randomNumber.times(factor).round(); - return salt; + public static getOrderHashHex(order: Order | SignedOrder): string { + return getOrderHashHex(order); } /** * Checks if the supplied hex encoded order hash is valid. @@ -97,12 +99,7 @@ export class ZeroEx { * @return Whether the supplied orderHash has the expected format. */ public static isValidOrderHash(orderHash: string): boolean { - // Since this method can be called to check if any arbitrary string conforms to an orderHash's - // format, we only assert that we were indeed passed a string. - assert.isString('orderHash', orderHash); - const schemaValidator = new SchemaValidator(); - const isValidOrderHash = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid; - return isValidOrderHash; + return isValidOrderHash(orderHash); } /** * A unit amount is defined as the amount of a token above the specified decimal places (integer part). @@ -133,17 +130,6 @@ export class ZeroEx { return baseUnitAmount; } /** - * Computes the orderHash for a supplied order. - * @param order An object that conforms to the Order or SignedOrder interface definitions. - * @return The resulting orderHash from hashing the supplied order. - */ - @decorators.syncZeroExErrorHandler - public static getOrderHashHex(order: Order | SignedOrder): string { - assert.doesConformToSchema('order', order, schemas.orderSchema); - const orderHashHex = utils.getOrderHashHex(order); - return orderHashHex; - } - /** * Instantiates a new ZeroEx instance that provides the public interface to the 0x.js library. * @param provider The Provider instance you would like the 0x.js library to use for interacting with * the Ethereum network. @@ -205,6 +191,13 @@ export class ZeroEx { (this.etherToken as any)._setNetworkId(networkId); } /** + * Get the provider instance currently used by 0x.js + * @return Web3 provider instance + */ + public getProvider(): Provider { + return this._web3Wrapper.getProvider(); + } + /** * Get user Ethereum addresses available through the supplied web3 provider available for sending transactions. * @return An array of available user Ethereum addresses. */ @@ -229,41 +222,12 @@ export class ZeroEx { signerAddress: string, shouldAddPersonalMessagePrefix: boolean, ): Promise<ECSignature> { - assert.isHexString('orderHash', orderHash); - await assert.isSenderAddressAsync('signerAddress', signerAddress, this._web3Wrapper); - const normalizedSignerAddress = signerAddress.toLowerCase(); - - let msgHashHex = orderHash; - if (shouldAddPersonalMessagePrefix) { - const orderHashBuff = ethUtil.toBuffer(orderHash); - const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff); - msgHashHex = ethUtil.bufferToHex(msgHashBuff); - } - - const signature = await this._web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex); - - // HACK: There is no consensus on whether the signatureHex string should be formatted as - // v + r + s OR r + s + v, and different clients (even different versions of the same client) - // return the signature params in different orders. In order to support all client implementations, - // we parse the signature in both ways, and evaluate if either one is a valid signature. - const validVParamValues = [27, 28]; - const ecSignatureVRS = signatureUtils.parseSignatureHexAsVRS(signature); - if (_.includes(validVParamValues, ecSignatureVRS.v)) { - const isValidVRSSignature = ZeroEx.isValidSignature(orderHash, ecSignatureVRS, normalizedSignerAddress); - if (isValidVRSSignature) { - return ecSignatureVRS; - } - } - - const ecSignatureRSV = signatureUtils.parseSignatureHexAsRSV(signature); - if (_.includes(validVParamValues, ecSignatureRSV.v)) { - const isValidRSVSignature = ZeroEx.isValidSignature(orderHash, ecSignatureRSV, normalizedSignerAddress); - if (isValidRSVSignature) { - return ecSignatureRSV; - } - } - - throw new Error(ZeroExError.InvalidSignature); + return signOrderHashAsync( + this._web3Wrapper.getProvider(), + orderHash, + signerAddress, + shouldAddPersonalMessagePrefix, + ); } /** * Waits for a transaction to be mined and returns the transaction receipt. diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts index 7cda70f16..6ddaaaf4f 100644 --- a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts @@ -1,4 +1,5 @@ import { schemas } from '@0xproject/json-schemas'; +import { getOrderHashHex } from '@0xproject/order-utils'; import { BlockParamLiteral, DecodedLogArgs, @@ -570,7 +571,7 @@ export class ExchangeWrapper extends ContractWrapper { ? SHOULD_VALIDATE_BY_DEFAULT : orderTransactionOpts.shouldValidate; if (shouldValidate) { - const orderHash = utils.getOrderHashHex(order); + const orderHash = getOrderHashHex(order); const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils.validateCancelOrderThrowIfInvalid( order, @@ -629,7 +630,7 @@ export class ExchangeWrapper extends ContractWrapper { : orderTransactionOpts.shouldValidate; if (shouldValidate) { for (const orderCancellationRequest of orderCancellationRequests) { - const orderHash = utils.getOrderHashHex(orderCancellationRequest.order); + const orderHash = getOrderHashHex(orderCancellationRequest.order); const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils.validateCancelOrderThrowIfInvalid( orderCancellationRequest.order, @@ -801,7 +802,7 @@ export class ExchangeWrapper extends ContractWrapper { ): Promise<void> { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount); - const orderHash = utils.getOrderHashHex(order); + const orderHash = getOrderHashHex(order); const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils.validateCancelOrderThrowIfInvalid( order, diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index 151204928..ae9f98c5f 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -27,7 +27,6 @@ export enum ZeroExError { TokenContractDoesNotExist = 'TOKEN_CONTRACT_DOES_NOT_EXIST', UnhandledError = 'UNHANDLED_ERROR', UserHasNoAssociatedAddress = 'USER_HAS_NO_ASSOCIATED_ADDRESSES', - InvalidSignature = 'INVALID_SIGNATURE', ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', InsufficientBalanceForTransfer = 'INSUFFICIENT_BALANCE_FOR_TRANSFER', diff --git a/packages/0x.js/src/utils/assert.ts b/packages/0x.js/src/utils/assert.ts index 5e8004cd0..2588a4d09 100644 --- a/packages/0x.js/src/utils/assert.ts +++ b/packages/0x.js/src/utils/assert.ts @@ -8,13 +8,13 @@ import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; -import { signatureUtils } from '../utils/signature_utils'; +import { isValidSignature } from '@0xproject/order-utils'; export const assert = { ...sharedAssert, isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) { - const isValidSignature = signatureUtils.isValidSignature(orderHash, ecSignature, signerAddress); - this.assert(isValidSignature, `Expected order with hash '${orderHash}' to have a valid signature`); + const isValid = isValidSignature(orderHash, ecSignature, signerAddress); + this.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`); }, async isSenderAddressAsync( variableName: string, @@ -28,8 +28,4 @@ export const assert = { `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, ); }, - async isUserAddressAvailableAsync(web3Wrapper: Web3Wrapper): Promise<void> { - const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); - this.assert(!_.isEmpty(availableAddresses), 'No addresses were available on the provided web3 provider'); - }, }; diff --git a/packages/0x.js/src/utils/constants.ts b/packages/0x.js/src/utils/constants.ts index 06beec8e2..07da6745d 100644 --- a/packages/0x.js/src/utils/constants.ts +++ b/packages/0x.js/src/utils/constants.ts @@ -3,7 +3,6 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', TESTRPC_NETWORK_ID: 50, - MAX_DIGITS_IN_UNSIGNED_256_INT: 78, INVALID_JUMP_PATTERN: 'invalid JUMP at', OUT_OF_GAS_PATTERN: 'out of gas', INVALID_TAKER_FORMAT: 'instance.taker is not of a type(s) string', diff --git a/packages/0x.js/src/utils/order_validation_utils.ts b/packages/0x.js/src/utils/order_validation_utils.ts index b320a3e92..a13c3dc04 100644 --- a/packages/0x.js/src/utils/order_validation_utils.ts +++ b/packages/0x.js/src/utils/order_validation_utils.ts @@ -1,3 +1,4 @@ +import { getOrderHashHex, OrderError } from '@0xproject/order-utils'; import { Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; @@ -113,7 +114,7 @@ export class OrderValidationUtils { zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber, ): Promise<void> { - const orderHash = utils.getOrderHashHex(signedOrder); + const orderHash = getOrderHashHex(signedOrder); const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerTokenAmount, @@ -142,9 +143,9 @@ export class OrderValidationUtils { if (fillTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderFillAmountZero); } - const orderHash = utils.getOrderHashHex(signedOrder); + const orderHash = getOrderHashHex(signedOrder); if (!ZeroEx.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker)) { - throw new Error(ZeroExError.InvalidSignature); + throw new Error(OrderError.InvalidSignature); } const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( diff --git a/packages/0x.js/src/utils/signature_utils.ts b/packages/0x.js/src/utils/signature_utils.ts deleted file mode 100644 index 46f167339..000000000 --- a/packages/0x.js/src/utils/signature_utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ECSignature } from '@0xproject/types'; -import * as ethUtil from 'ethereumjs-util'; - -export const signatureUtils = { - isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean { - const dataBuff = ethUtil.toBuffer(data); - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); - try { - const pubKey = ethUtil.ecrecover( - msgHashBuff, - signature.v, - ethUtil.toBuffer(signature.r), - ethUtil.toBuffer(signature.s), - ); - const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey)); - return retrievedAddress === signerAddress; - } catch (err) { - return false; - } - }, - parseSignatureHexAsVRS(signatureHex: string): ECSignature { - const signatureBuffer = ethUtil.toBuffer(signatureHex); - let v = signatureBuffer[0]; - if (v < 27) { - v += 27; - } - const r = signatureBuffer.slice(1, 33); - const s = signatureBuffer.slice(33, 65); - const ecSignature: ECSignature = { - v, - r: ethUtil.bufferToHex(r), - s: ethUtil.bufferToHex(s), - }; - return ecSignature; - }, - parseSignatureHexAsRSV(signatureHex: string): ECSignature { - const { v, r, s } = ethUtil.fromRpcSig(signatureHex); - const ecSignature: ECSignature = { - v, - r: ethUtil.bufferToHex(r), - s: ethUtil.bufferToHex(s), - }; - return ecSignature; - }, -}; diff --git a/packages/0x.js/src/utils/utils.ts b/packages/0x.js/src/utils/utils.ts index c8bcd907e..af1125632 100644 --- a/packages/0x.js/src/utils/utils.ts +++ b/packages/0x.js/src/utils/utils.ts @@ -1,59 +1,9 @@ -import { Order, SignedOrder, SolidityTypes } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; -import BN = require('bn.js'); -import * as ethABI from 'ethereumjs-abi'; -import * as ethUtil from 'ethereumjs-util'; -import * as _ from 'lodash'; export const utils = { - /** - * Converts BigNumber instance to BN - * The only reason we convert to BN is to remain compatible with `ethABI. soliditySHA3` that - * expects values of Solidity type `uint` to be passed as type `BN`. - * We do not use BN anywhere else in the codebase. - */ - bigNumberToBN(value: BigNumber) { - return new BN(value.toString(), 10); - }, spawnSwitchErr(name: string, value: any): Error { return new Error(`Unexpected switch value: ${value} encountered for ${name}`); }, - getOrderHashHex(order: Order | SignedOrder): string { - const orderParts = [ - { value: order.exchangeContractAddress, 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; - }, getCurrentUnixTimestampSec(): BigNumber { return new BigNumber(Date.now() / 1000).round(); }, diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts index 838ee7080..6dccdaea7 100644 --- a/packages/0x.js/test/0x.js_test.ts +++ b/packages/0x.js/test/0x.js_test.ts @@ -17,8 +17,6 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); chaiSetup.configure(); const expect = chai.expect; -const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false; - describe('ZeroEx library', () => { let zeroEx: ZeroEx; before(async () => { @@ -63,14 +61,12 @@ describe('ZeroEx library', () => { }; const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; it("should return false if the data doesn't pertain to the signature & address", async () => { - expect(ZeroEx.isValidSignature('0x0', signature, address)).to.be.false(); return expect( (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync('0x0', signature, address), ).to.become(false); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; - expect(ZeroEx.isValidSignature(dataHex, signature, validUnrelatedAddress)).to.be.false(); return expect( (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync( dataHex, @@ -81,45 +77,16 @@ describe('ZeroEx library', () => { }); it("should return false if the signature doesn't pertain to the dataHex & address", async () => { const wrongSignature = _.assign({}, signature, { v: 28 }); - expect(ZeroEx.isValidSignature(dataHex, wrongSignature, address)).to.be.false(); return expect( (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, wrongSignature, address), ).to.become(false); }); it('should return true if the signature does pertain to the dataHex & address', async () => { - const isValidSignatureLocal = ZeroEx.isValidSignature(dataHex, signature, address); - expect(isValidSignatureLocal).to.be.true(); return expect( (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, signature, address), ).to.become(true); }); }); - describe('#generateSalt', () => { - it('generates different salts', () => { - const equal = ZeroEx.generatePseudoRandomSalt().eq(ZeroEx.generatePseudoRandomSalt()); - expect(equal).to.be.false(); - }); - it('generates salt in range [0..2^256)', () => { - const salt = ZeroEx.generatePseudoRandomSalt(); - expect(salt.greaterThanOrEqualTo(0)).to.be.true(); - const twoPow256 = new BigNumber(2).pow(256); - expect(salt.lessThan(twoPow256)).to.be.true(); - }); - }); - describe('#isValidOrderHash', () => { - it('returns false if the value is not a hex string', () => { - const isValid = ZeroEx.isValidOrderHash('not a hex'); - expect(isValid).to.be.false(); - }); - it('returns false if the length is wrong', () => { - const isValid = ZeroEx.isValidOrderHash('0xdeadbeef'); - expect(isValid).to.be.false(); - }); - it('returns true if order hash is correct', () => { - const isValid = ZeroEx.isValidOrderHash('0x' + Array(65).join('0')); - expect(isValid).to.be.true(); - }); - }); describe('#toUnitAmount', () => { it('should throw if invalid baseUnit amount supplied as argument', () => { const invalidBaseUnitAmount = new BigNumber(1000000000.4); @@ -152,106 +119,6 @@ describe('ZeroEx library', () => { ); }); }); - describe('#getOrderHashHex', () => { - const expectedOrderHash = '0x39da987067a3c9e5f1617694f1301326ba8c8b0498ebef5df4863bed394e3c83'; - const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b'; - const order: Order = { - maker: constants.NULL_ADDRESS, - taker: constants.NULL_ADDRESS, - feeRecipient: constants.NULL_ADDRESS, - makerTokenAddress: constants.NULL_ADDRESS, - takerTokenAddress: constants.NULL_ADDRESS, - exchangeContractAddress: fakeExchangeContractAddress, - salt: new BigNumber(0), - makerFee: new BigNumber(0), - takerFee: new BigNumber(0), - makerTokenAmount: new BigNumber(0), - takerTokenAmount: new BigNumber(0), - expirationUnixTimestampSec: new BigNumber(0), - }; - it('calculates the order hash', async () => { - const orderHash = ZeroEx.getOrderHashHex(order); - expect(orderHash).to.be.equal(expectedOrderHash); - }); - it('throws a readable error message if taker format is invalid', async () => { - const orderWithInvalidtakerFormat = { - ...order, - taker: (null as any) as string, - }; - const expectedErrorMessage = - 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; - expect(() => ZeroEx.getOrderHashHex(orderWithInvalidtakerFormat)).to.throw(expectedErrorMessage); - }); - }); - describe('#signOrderHashAsync', () => { - let stubs: Sinon.SinonStub[] = []; - let makerAddress: string; - before(async () => { - const availableAddreses = await zeroEx.getAvailableAddressesAsync(); - makerAddress = availableAddreses[0]; - }); - afterEach(() => { - // clean up any stubs after the test has completed - _.each(stubs, s => s.restore()); - stubs = []; - }); - it('Should return the correct ECSignature', async () => { - const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; - const expectedECSignature = { - v: 27, - r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', - s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', - }; - const ecSignature = await zeroEx.signOrderHashAsync( - orderHash, - makerAddress, - SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, - ); - expect(ecSignature).to.deep.equal(expectedECSignature); - }); - it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => { - const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; - const signature = - '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb021b'; - const expectedECSignature = { - v: 27, - r: '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3', - s: '0x050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb02', - }; - stubs = [ - Sinon.stub((zeroEx as any)._web3Wrapper, 'signMessageAsync').returns(Promise.resolve(signature)), - Sinon.stub(ZeroEx, 'isValidSignature').returns(true), - ]; - - const ecSignature = await zeroEx.signOrderHashAsync( - orderHash, - makerAddress, - SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, - ); - expect(ecSignature).to.deep.equal(expectedECSignature); - }); - it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => { - const orderHash = '0xc793e33ffded933b76f2f48d9aa3339fc090399d5e7f5dec8d3660f5480793f7'; - const signature = - '0x1bc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee02dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960'; - const expectedECSignature = { - v: 27, - r: '0xc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee0', - s: '0x2dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960', - }; - stubs = [ - Sinon.stub((zeroEx as any)._web3Wrapper, 'signMessageAsync').returns(Promise.resolve(signature)), - Sinon.stub(ZeroEx, 'isValidSignature').returns(true), - ]; - - const ecSignature = await zeroEx.signOrderHashAsync( - orderHash, - makerAddress, - SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, - ); - expect(ecSignature).to.deep.equal(expectedECSignature); - }); - }); describe('#awaitTransactionMinedAsync', () => { beforeEach(async () => { await blockchainLifecycle.startAsync(); diff --git a/packages/0x.js/test/assert_test.ts b/packages/0x.js/test/assert_test.ts index b08f3e23b..e69de29bb 100644 --- a/packages/0x.js/test/assert_test.ts +++ b/packages/0x.js/test/assert_test.ts @@ -1,43 +0,0 @@ -import { web3Factory } from '@0xproject/dev-utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { ZeroEx } from '../src'; -import { assert } from '../src/utils/assert'; - -import { constants } from './utils/constants'; -import { provider } from './utils/web3_wrapper'; - -const expect = chai.expect; - -describe('Assertion library', () => { - const config = { - networkId: constants.TESTRPC_NETWORK_ID, - }; - const zeroEx = new ZeroEx(provider, config); - describe('#isSenderAddressHexAsync', () => { - it('throws when address is invalid', async () => { - const address = '0xdeadbeef'; - const varName = 'address'; - return expect( - assert.isSenderAddressAsync(varName, address, (zeroEx as any)._web3Wrapper), - ).to.be.rejectedWith(`Expected ${varName} to be of type ETHAddressHex, encountered: ${address}`); - }); - it('throws when address is unavailable', async () => { - const validUnrelatedAddress = '0x8b0292b11a196601eddce54b665cafeca0347d42'; - const varName = 'address'; - return expect( - assert.isSenderAddressAsync(varName, validUnrelatedAddress, (zeroEx as any)._web3Wrapper), - ).to.be.rejectedWith( - `Specified ${varName} ${validUnrelatedAddress} isn't available through the supplied web3 provider`, - ); - }); - it("doesn't throw if address is available", async () => { - const availableAddress = (await zeroEx.getAvailableAddressesAsync())[0]; - const varName = 'address'; - return expect( - assert.isSenderAddressAsync(varName, availableAddress, (zeroEx as any)._web3Wrapper), - ).to.become(undefined); - }); - }); -}); diff --git a/packages/0x.js/test/order_validation_test.ts b/packages/0x.js/test/order_validation_test.ts index 9b843b930..0cb95c1b6 100644 --- a/packages/0x.js/test/order_validation_test.ts +++ b/packages/0x.js/test/order_validation_test.ts @@ -1,4 +1,5 @@ import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils'; +import { OrderError } from '@0xproject/order-utils'; import { BlockParamLiteral } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; @@ -169,7 +170,7 @@ describe('OrderValidation', () => { signedOrder.ecSignature.v = 28 - signedOrder.ecSignature.v + 27; return expect( zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(signedOrder, fillableAmount, takerAddress), - ).to.be.rejectedWith(ZeroExError.InvalidSignature); + ).to.be.rejectedWith(OrderError.InvalidSignature); }); it('should throw when the order is fully filled or cancelled', async () => { const signedOrder = await fillScenarios.createFillableSignedOrderAsync( diff --git a/packages/0x.js/test/utils/fill_scenarios.ts b/packages/0x.js/test/utils/fill_scenarios.ts index 7d0e8c501..5a82a56d2 100644 --- a/packages/0x.js/test/utils/fill_scenarios.ts +++ b/packages/0x.js/test/utils/fill_scenarios.ts @@ -1,10 +1,10 @@ +import { orderFactory } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { SignedOrder, Token, ZeroEx } from '../../src'; import { artifacts } from '../../src/artifacts'; import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { orderFactory } from '../utils/order_factory'; import { constants } from './constants'; @@ -164,7 +164,7 @@ export class FillScenarios { ]); const signedOrder = await orderFactory.createSignedOrderAsync( - this._zeroEx, + this._zeroEx.getProvider(), makerAddress, takerAddress, makerFee, diff --git a/packages/0x.js/test/utils/web3_wrapper.ts b/packages/0x.js/test/utils/web3_wrapper.ts index b7b3f0b7f..b0ccfa546 100644 --- a/packages/0x.js/test/utils/web3_wrapper.ts +++ b/packages/0x.js/test/utils/web3_wrapper.ts @@ -1,9 +1,6 @@ import { devConstants, web3Factory } from '@0xproject/dev-utils'; import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as Web3 from 'web3'; - -import { constants } from './constants'; const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); const provider: Provider = web3.currentProvider; diff --git a/packages/order-utils/.npmignore b/packages/order-utils/.npmignore new file mode 100644 index 000000000..89302c908 --- /dev/null +++ b/packages/order-utils/.npmignore @@ -0,0 +1,7 @@ +.* +yarn-error.log +/scripts/ +/generated_docs/ +/src/ +tsconfig.json +/lib/monorepo_scripts/ diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/packages/order-utils/CHANGELOG.json @@ -0,0 +1 @@ +[] diff --git a/packages/order-utils/README.md b/packages/order-utils/README.md new file mode 100644 index 000000000..4b571509a --- /dev/null +++ b/packages/order-utils/README.md @@ -0,0 +1,77 @@ +## @0xproject/order-utils + +0x order-related utilities for those developing on top of 0x protocol. + +### Read the [Documentation](https://0xproject.com/docs/order-utils). + +## Installation + +```bash +yarn add @0xproject/order-utils +``` + +If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: + +```json +"compilerOptions": { + "typeRoots": ["node_modules/@0xproject/typescript-typings/types", "node_modules/@types"], +} +``` + +## Contributing + +We welcome improvements and fixes from the wider community! To report bugs within this package, please create an issue in this repository. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started. + +### Install dependencies + +If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them: + +```bash +yarn config set workspaces-experimental true +``` + +Then install dependencies + +```bash +yarn install +``` + +### Build + +If this is your **first** time building this package, you must first build **all** packages within the monorepo. This is because packages that depend on other packages located inside this monorepo are symlinked when run from **within** the monorepo. This allows you to make changes across multiple packages without first publishing dependent packages to NPM. To build all packages, run the following from the monorepo root directory: + +```bash +yarn lerna:rebuild +``` + +Or continuously rebuild on change: + +```bash +yarn dev +``` + +You can also build this specific package by running the following from within its directory: + +```bash +yarn build +``` + +or continuously rebuild on change: + +```bash +yarn build:watch +``` + +### Clean + +```bash +yarn clean +``` + +### Lint + +```bash +yarn lint +``` diff --git a/packages/order-utils/coverage/.gitkeep b/packages/order-utils/coverage/.gitkeep new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/order-utils/coverage/.gitkeep diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json new file mode 100644 index 000000000..cb21139a2 --- /dev/null +++ b/packages/order-utils/package.json @@ -0,0 +1,76 @@ +{ + "name": "@0xproject/order-utils", + "version": "0.0.1", + "description": "0x order utils", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", + "scripts": { + "build:watch": "tsc -w", + "build": "tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", + "test": "run-s clean build run_mocha", + "test:circleci": "yarn test:coverage", + "run_mocha": "mocha lib/test/**/*_test.js --bail --exit", + "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", + "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", + "clean": "shx rm -rf lib scripts", + "lint": "tslint --project .", + "manual:postpublish": "yarn build; node ./scripts/postpublish.js", + "docs:stage": "yarn build && node ./scripts/stage_docs.js", + "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES", + "upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" + }, + "config": { + "postpublish": { + "docPublishConfigs": { + "extraFileIncludes": [ + "../types/src/index.ts" + ], + "s3BucketPath": "s3://doc-jsons/order-utils/", + "s3StagingBucketPath": "s3://staging-doc-jsons/order-utils/" + } + } + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x-monorepo.git" + }, + "bugs": { + "url": "https://github.com/0xProject/0x-monorepo/issues" + }, + "homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md", + "devDependencies": { + "@0xproject/monorepo-scripts": "^0.1.18", + "@0xproject/dev-utils": "^0.3.6", + "@0xproject/tslint-config": "^0.4.16", + "@types/lodash": "4.14.104", + "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-bignumber": "^2.0.1", + "dirty-chai": "^2.0.1", + "sinon": "^4.0.0", + "mocha": "^4.0.1", + "copyfiles": "^1.2.0", + "npm-run-all": "^4.1.2", + "typedoc": "0xProject/typedoc", + "shx": "^0.2.2", + "tslint": "5.8.0", + "typescript": "2.7.1" + }, + "dependencies": { + "@0xproject/assert": "^0.2.7", + "@0xproject/types": "^0.6.1", + "@0xproject/json-schemas": "^0.7.21", + "@0xproject/typescript-typings": "^0.2.0", + "@0xproject/web3-wrapper": "^0.6.1", + "@0xproject/utils": "^0.5.2", + "@types/node": "^8.0.53", + "bn.js": "^4.11.8", + "lodash": "^4.17.4", + "ethereumjs-abi": "^0.6.4", + "ethereumjs-util": "^5.1.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/order-utils/src/assert.ts b/packages/order-utils/src/assert.ts new file mode 100644 index 000000000..5ac402e7e --- /dev/null +++ b/packages/order-utils/src/assert.ts @@ -0,0 +1,27 @@ +import { assert as sharedAssert } from '@0xproject/assert'; +// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// tslint:disable-next-line:no-unused-variable +import { Schema } from '@0xproject/json-schemas'; +// tslint:disable-next-line:no-unused-variable +import { ECSignature } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; + +import { isValidSignature } from './signature_utils'; + +export const assert = { + ...sharedAssert, + async isSenderAddressAsync( + variableName: string, + senderAddressHex: string, + web3Wrapper: Web3Wrapper, + ): Promise<void> { + sharedAssert.isETHAddressHex(variableName, senderAddressHex); + const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex); + sharedAssert.assert( + isSenderAddressAvailable, + `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, + ); + }, +}; diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts new file mode 100644 index 000000000..ec2fe744a --- /dev/null +++ b/packages/order-utils/src/constants.ts @@ -0,0 +1,3 @@ +export const constants = { + NULL_ADDRESS: '0x0000000000000000000000000000000000000000', +}; diff --git a/packages/order-utils/src/globals.d.ts b/packages/order-utils/src/globals.d.ts new file mode 100644 index 000000000..94e63a32d --- /dev/null +++ b/packages/order-utils/src/globals.d.ts @@ -0,0 +1,6 @@ +declare module '*.json' { + const json: any; + /* tslint:disable */ + export default json; + /* tslint:enable */ +} diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts new file mode 100644 index 000000000..2b565f713 --- /dev/null +++ b/packages/order-utils/src/index.ts @@ -0,0 +1,6 @@ +export { getOrderHashHex, isValidOrderHash } from './order_hash'; +export { isValidSignature, signOrderHashAsync } from './signature_utils'; +export { orderFactory } from './order_factory'; +export { generatePseudoRandomSalt } from './salt'; +export { constants } from './constants'; +export { OrderError } from './types'; diff --git a/packages/order-utils/src/monorepo_scripts/postpublish.ts b/packages/order-utils/src/monorepo_scripts/postpublish.ts new file mode 100644 index 000000000..dcb99d0f7 --- /dev/null +++ b/packages/order-utils/src/monorepo_scripts/postpublish.ts @@ -0,0 +1,8 @@ +import { postpublishUtils } from '@0xproject/monorepo-scripts'; + +import * as packageJSON from '../package.json'; +import * as tsConfigJSON from '../tsconfig.json'; + +const cwd = `${__dirname}/..`; +// tslint:disable-next-line:no-floating-promises +postpublishUtils.runAsync(packageJSON, tsConfigJSON, cwd); diff --git a/packages/order-utils/src/monorepo_scripts/stage_docs.ts b/packages/order-utils/src/monorepo_scripts/stage_docs.ts new file mode 100644 index 000000000..e732ac8eb --- /dev/null +++ b/packages/order-utils/src/monorepo_scripts/stage_docs.ts @@ -0,0 +1,8 @@ +import { postpublishUtils } from '@0xproject/monorepo-scripts'; + +import * as packageJSON from '../package.json'; +import * as tsConfigJSON from '../tsconfig.json'; + +const cwd = `${__dirname}/..`; +// tslint:disable-next-line:no-floating-promises +postpublishUtils.publishDocsToStagingAsync(packageJSON, tsConfigJSON, cwd); diff --git a/packages/0x.js/test/utils/order_factory.ts b/packages/order-utils/src/order_factory.ts index 08f2081a4..2759aac81 100644 --- a/packages/0x.js/test/utils/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -1,13 +1,16 @@ +import { Provider, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; -import { SignedOrder, ZeroEx } from '../../src'; +import { getOrderHashHex } from './order_hash'; +import { generatePseudoRandomSalt } from './salt'; +import { signOrderHashAsync } from './signature_utils'; const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false; export const orderFactory = { async createSignedOrderAsync( - zeroEx: ZeroEx, + provider: Provider, maker: string, taker: string, makerFee: BigNumber, @@ -33,13 +36,13 @@ export const orderFactory = { takerTokenAmount, makerTokenAddress, takerTokenAddress, - salt: ZeroEx.generatePseudoRandomSalt(), + salt: generatePseudoRandomSalt(), exchangeContractAddress, feeRecipient, expirationUnixTimestampSec, }; - const orderHash = ZeroEx.getOrderHashHex(order); - const ecSignature = await zeroEx.signOrderHashAsync(orderHash, maker, SHOULD_ADD_PERSONAL_MESSAGE_PREFIX); + const orderHash = getOrderHashHex(order); + const ecSignature = await signOrderHashAsync(provider, orderHash, maker, SHOULD_ADD_PERSONAL_MESSAGE_PREFIX); const signedOrder: SignedOrder = _.assign(order, { ecSignature }); return signedOrder; }, diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts new file mode 100644 index 000000000..8da11c596 --- /dev/null +++ b/packages/order-utils/src/order_hash.ts @@ -0,0 +1,89 @@ +import { schemas, SchemaValidator } from '@0xproject/json-schemas'; +import { Order, SignedOrder, SolidityTypes } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import BN = require('bn.js'); +import * as ethABI from 'ethereumjs-abi'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { assert } from './assert'; + +const INVALID_TAKER_FORMAT = 'instance.taker is not of a type(s) string'; + +/** + * Converts BigNumber instance to BN + * The only reason we convert to BN is to remain compatible with `ethABI.soliditySHA3` that + * expects values of Solidity type `uint` to be passed as type `BN`. + * We do not use BN anywhere else in the codebase. + */ +function bigNumberToBN(value: BigNumber) { + return new BN(value.toString(), 10); +} + +/** + * Computes the orderHash for a supplied order. + * @param order An object that conforms to the Order or SignedOrder interface definitions. + * @return The resulting orderHash from hashing the supplied order. + */ +export function getOrderHashHex(order: Order | SignedOrder): string { + try { + assert.doesConformToSchema('order', order, schemas.orderSchema); + } catch (error) { + if (_.includes(error.message, INVALID_TAKER_FORMAT)) { + const errMsg = + 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; + throw new Error(errMsg); + } + throw error; + } + const orderParts = [ + { value: order.exchangeContractAddress, 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: bigNumberToBN(order.makerTokenAmount), + type: SolidityTypes.Uint256, + }, + { + value: bigNumberToBN(order.takerTokenAmount), + type: SolidityTypes.Uint256, + }, + { + value: bigNumberToBN(order.makerFee), + type: SolidityTypes.Uint256, + }, + { + value: bigNumberToBN(order.takerFee), + type: SolidityTypes.Uint256, + }, + { + value: bigNumberToBN(order.expirationUnixTimestampSec), + type: SolidityTypes.Uint256, + }, + { value: 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; +} + +/** + * Checks if the supplied hex encoded order hash is valid. + * Note: Valid means it has the expected format, not that an order with the orderHash exists. + * Use this method when processing orderHashes submitted as user input. + * @param orderHash Hex encoded orderHash. + * @return Whether the supplied orderHash has the expected format. + */ +export function isValidOrderHash(orderHash: string): boolean { + // Since this method can be called to check if any arbitrary string conforms to an orderHash's + // format, we only assert that we were indeed passed a string. + assert.isString('orderHash', orderHash); + const schemaValidator = new SchemaValidator(); + const isValid = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid; + return isValid; +} diff --git a/packages/order-utils/src/salt.ts b/packages/order-utils/src/salt.ts new file mode 100644 index 000000000..90a4197c0 --- /dev/null +++ b/packages/order-utils/src/salt.ts @@ -0,0 +1,18 @@ +import { BigNumber } from '@0xproject/utils'; + +const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; + +/** + * Generates a pseudo-random 256-bit salt. + * The salt can be included in a 0x order, ensuring that the order generates a unique orderHash + * and will not collide with other outstanding orders that are identical in all other parameters. + * @return A pseudo-random 256-bit number that can be used as a salt. + */ +export function generatePseudoRandomSalt(): BigNumber { + // BigNumber.random returns a pseudo-random number between 0 & 1 with a passed in number of decimal places. + // Source: https://mikemcl.github.io/bignumber.js/#random + const randomNumber = BigNumber.random(MAX_DIGITS_IN_UNSIGNED_256_INT); + const factor = new BigNumber(10).pow(MAX_DIGITS_IN_UNSIGNED_256_INT - 1); + const salt = randomNumber.times(factor).round(); + return salt; +} diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts new file mode 100644 index 000000000..b511573a8 --- /dev/null +++ b/packages/order-utils/src/signature_utils.ts @@ -0,0 +1,119 @@ +import { schemas } from '@0xproject/json-schemas'; +import { ECSignature, Provider } from '@0xproject/types'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { assert } from './assert'; +import { OrderError } from './types'; + +/** + * Verifies that the elliptic curve signature `signature` was generated + * by signing `data` with the private key corresponding to the `signerAddress` address. + * @param data The hex encoded data signed by the supplied signature. + * @param signature An object containing the elliptic curve signature parameters. + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the signature is valid for the supplied signerAddress and data. + */ +export function isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean { + assert.isHexString('data', data); + assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema); + assert.isETHAddressHex('signerAddress', signerAddress); + const normalizedSignerAddress = signerAddress.toLowerCase(); + + const dataBuff = ethUtil.toBuffer(data); + const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); + try { + const pubKey = ethUtil.ecrecover( + msgHashBuff, + signature.v, + ethUtil.toBuffer(signature.r), + ethUtil.toBuffer(signature.s), + ); + const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey)); + return retrievedAddress === signerAddress; + } catch (err) { + return false; + } +} +/** + * Signs an orderHash and returns it's elliptic curve signature. + * This method currently supports TestRPC, Geth and Parity above and below V1.6.6 + * @param orderHash Hex encoded orderHash to sign. + * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address + * must be available via the Provider supplied to 0x.js. + * @param shouldAddPersonalMessagePrefix Some signers add the personal message prefix `\x19Ethereum Signed Message` + * themselves (e.g Parity Signer, Ledger, TestRPC) and others expect it to already be done by the client + * (e.g Metamask). Depending on which signer this request is going to, decide on whether to add the prefix + * before sending the request. + * @return An object containing the Elliptic curve signature parameters generated by signing the orderHash. + */ +export async function signOrderHashAsync( + provider: Provider, + orderHash: string, + signerAddress: string, + shouldAddPersonalMessagePrefix: boolean, +): Promise<ECSignature> { + assert.isHexString('orderHash', orderHash); + const web3Wrapper = new Web3Wrapper(provider); + await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); + const normalizedSignerAddress = signerAddress.toLowerCase(); + + let msgHashHex = orderHash; + if (shouldAddPersonalMessagePrefix) { + const orderHashBuff = ethUtil.toBuffer(orderHash); + const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff); + msgHashHex = ethUtil.bufferToHex(msgHashBuff); + } + + const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex); + + // HACK: There is no consensus on whether the signatureHex string should be formatted as + // v + r + s OR r + s + v, and different clients (even different versions of the same client) + // return the signature params in different orders. In order to support all client implementations, + // we parse the signature in both ways, and evaluate if either one is a valid signature. + const validVParamValues = [27, 28]; + const ecSignatureVRS = parseSignatureHexAsVRS(signature); + if (_.includes(validVParamValues, ecSignatureVRS.v)) { + const isValidVRSSignature = isValidSignature(orderHash, ecSignatureVRS, normalizedSignerAddress); + if (isValidVRSSignature) { + return ecSignatureVRS; + } + } + + const ecSignatureRSV = parseSignatureHexAsRSV(signature); + if (_.includes(validVParamValues, ecSignatureRSV.v)) { + const isValidRSVSignature = isValidSignature(orderHash, ecSignatureRSV, normalizedSignerAddress); + if (isValidRSVSignature) { + return ecSignatureRSV; + } + } + + throw new Error(OrderError.InvalidSignature); +} + +function parseSignatureHexAsVRS(signatureHex: string): ECSignature { + const signatureBuffer = ethUtil.toBuffer(signatureHex); + let v = signatureBuffer[0]; + if (v < 27) { + v += 27; + } + const r = signatureBuffer.slice(1, 33); + const s = signatureBuffer.slice(33, 65); + const ecSignature: ECSignature = { + v, + r: ethUtil.bufferToHex(r), + s: ethUtil.bufferToHex(s), + }; + return ecSignature; +} + +function parseSignatureHexAsRSV(signatureHex: string): ECSignature { + const { v, r, s } = ethUtil.fromRpcSig(signatureHex); + const ecSignature: ECSignature = { + v, + r: ethUtil.bufferToHex(r), + s: ethUtil.bufferToHex(s), + }; + return ecSignature; +} diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts new file mode 100644 index 000000000..f79d52359 --- /dev/null +++ b/packages/order-utils/src/types.ts @@ -0,0 +1,3 @@ +export enum OrderError { + InvalidSignature = 'INVALID_SIGNATURE', +} diff --git a/packages/order-utils/test/assert_test.ts b/packages/order-utils/test/assert_test.ts new file mode 100644 index 000000000..dfd19bf86 --- /dev/null +++ b/packages/order-utils/test/assert_test.ts @@ -0,0 +1,35 @@ +import { web3Factory } from '@0xproject/dev-utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { assert } from '../src/assert'; + +import { chaiSetup } from './utils/chai_setup'; +import { web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('Assertion library', () => { + describe('#isSenderAddressHexAsync', () => { + it('throws when address is invalid', async () => { + const address = '0xdeadbeef'; + const varName = 'address'; + return expect(assert.isSenderAddressAsync(varName, address, web3Wrapper)).to.be.rejectedWith( + `Expected ${varName} to be of type ETHAddressHex, encountered: ${address}`, + ); + }); + it('throws when address is unavailable', async () => { + const validUnrelatedAddress = '0x8b0292b11a196601eddce54b665cafeca0347d42'; + const varName = 'address'; + return expect(assert.isSenderAddressAsync(varName, validUnrelatedAddress, web3Wrapper)).to.be.rejectedWith( + `Specified ${varName} ${validUnrelatedAddress} isn't available through the supplied web3 provider`, + ); + }); + it("doesn't throw if address is available", async () => { + const availableAddress = (await web3Wrapper.getAvailableAddressesAsync())[0]; + const varName = 'address'; + return expect(assert.isSenderAddressAsync(varName, availableAddress, web3Wrapper)).to.become(undefined); + }); + }); +}); diff --git a/packages/order-utils/test/order_hash_test.ts b/packages/order-utils/test/order_hash_test.ts new file mode 100644 index 000000000..b6dda1a43 --- /dev/null +++ b/packages/order-utils/test/order_hash_test.ts @@ -0,0 +1,46 @@ +import { web3Factory } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { constants, getOrderHashHex } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('Order hashing', () => { + describe('#getOrderHashHex', () => { + const expectedOrderHash = '0x39da987067a3c9e5f1617694f1301326ba8c8b0498ebef5df4863bed394e3c83'; + const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b'; + const order = { + maker: constants.NULL_ADDRESS, + taker: constants.NULL_ADDRESS, + feeRecipient: constants.NULL_ADDRESS, + makerTokenAddress: constants.NULL_ADDRESS, + takerTokenAddress: constants.NULL_ADDRESS, + exchangeContractAddress: fakeExchangeContractAddress, + salt: new BigNumber(0), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerTokenAmount: new BigNumber(0), + takerTokenAmount: new BigNumber(0), + expirationUnixTimestampSec: new BigNumber(0), + }; + it('calculates the order hash', async () => { + const orderHash = getOrderHashHex(order); + expect(orderHash).to.be.equal(expectedOrderHash); + }); + it('throws a readable error message if taker format is invalid', async () => { + const orderWithInvalidtakerFormat = { + ...order, + taker: (null as any) as string, + }; + const expectedErrorMessage = + 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; + expect(() => getOrderHashHex(orderWithInvalidtakerFormat)).to.throw(expectedErrorMessage); + }); + }); +}); diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts new file mode 100644 index 000000000..ede681619 --- /dev/null +++ b/packages/order-utils/test/signature_utils_test.ts @@ -0,0 +1,164 @@ +import { web3Factory } from '@0xproject/dev-utils'; +import { JSONRPCErrorCallback, JSONRPCRequestPayload } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; +import * as Sinon from 'sinon'; + +import { generatePseudoRandomSalt, isValidOrderHash, isValidSignature, signOrderHashAsync } from '../src'; +import * as signatureUtils from '../src/signature_utils'; + +import { chaiSetup } from './utils/chai_setup'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false; + +describe('Signature utils', () => { + describe('#isValidSignature', () => { + // The Exchange smart contract `isValidSignature` method only validates orderHashes and assumes + // the length of the data is exactly 32 bytes. Thus for these tests, we use data of this size. + const dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; + const signature = { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }; + const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; + it("should return false if the data doesn't pertain to the signature & address", async () => { + expect(isValidSignature('0x0', signature, address)).to.be.false(); + }); + it("should return false if the address doesn't pertain to the signature & data", async () => { + const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; + expect(isValidSignature(dataHex, signature, validUnrelatedAddress)).to.be.false(); + }); + it("should return false if the signature doesn't pertain to the dataHex & address", async () => { + const wrongSignature = _.assign({}, signature, { v: 28 }); + expect(isValidSignature(dataHex, wrongSignature, address)).to.be.false(); + }); + it('should return true if the signature does pertain to the dataHex & address', async () => { + const isValidSignatureLocal = isValidSignature(dataHex, signature, address); + expect(isValidSignatureLocal).to.be.true(); + }); + }); + describe('#generateSalt', () => { + it('generates different salts', () => { + const equal = generatePseudoRandomSalt().eq(generatePseudoRandomSalt()); + expect(equal).to.be.false(); + }); + it('generates salt in range [0..2^256)', () => { + const salt = generatePseudoRandomSalt(); + expect(salt.greaterThanOrEqualTo(0)).to.be.true(); + const twoPow256 = new BigNumber(2).pow(256); + expect(salt.lessThan(twoPow256)).to.be.true(); + }); + }); + describe('#isValidOrderHash', () => { + it('returns false if the value is not a hex string', () => { + const isValid = isValidOrderHash('not a hex'); + expect(isValid).to.be.false(); + }); + it('returns false if the length is wrong', () => { + const isValid = isValidOrderHash('0xdeadbeef'); + expect(isValid).to.be.false(); + }); + it('returns true if order hash is correct', () => { + const isValid = isValidOrderHash('0x' + Array(65).join('0')); + expect(isValid).to.be.true(); + }); + }); + describe('#signOrderHashAsync', () => { + let stubs: Sinon.SinonStub[] = []; + let makerAddress: string; + before(async () => { + const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); + makerAddress = availableAddreses[0]; + }); + afterEach(() => { + // clean up any stubs after the test has completed + _.each(stubs, s => s.restore()); + stubs = []; + }); + it('Should return the correct ECSignature', async () => { + const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; + const expectedECSignature = { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }; + const ecSignature = await signOrderHashAsync( + provider, + orderHash, + makerAddress, + SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, + ); + expect(ecSignature).to.deep.equal(expectedECSignature); + }); + it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => { + const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; + const expectedECSignature = { + v: 27, + r: '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113', + s: '0x7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b', + }; + + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback) { + if (payload.method === 'eth_sign') { + const [address, message] = payload.params; + const signature = await web3Wrapper.signMessageAsync(address, message); + callback(null, { + id: 42, + jsonrpc: '2.0', + result: `0x${signature.substr(130)}${signature.substr(2, 128)}`, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, + }; + + const ecSignature = await signOrderHashAsync( + fakeProvider, + orderHash, + makerAddress, + SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, + ); + expect(ecSignature).to.deep.equal(expectedECSignature); + }); + it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => { + const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; + const expectedECSignature = { + v: 27, + r: '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113', + s: '0x7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b', + }; + const fakeProvider = { + async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback) { + if (payload.method === 'eth_sign') { + const [address, message] = payload.params; + const signature = await web3Wrapper.signMessageAsync(address, message); + callback(null, { + id: 42, + jsonrpc: '2.0', + result: signature, + }); + } else { + callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] }); + } + }, + }; + + const ecSignature = await signOrderHashAsync( + fakeProvider, + orderHash, + makerAddress, + SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, + ); + expect(ecSignature).to.deep.equal(expectedECSignature); + }); + }); +}); diff --git a/packages/order-utils/test/utils/chai_setup.ts b/packages/order-utils/test/utils/chai_setup.ts new file mode 100644 index 000000000..078edd309 --- /dev/null +++ b/packages/order-utils/test/utils/chai_setup.ts @@ -0,0 +1,13 @@ +import * as chai from 'chai'; +import chaiAsPromised = require('chai-as-promised'); +import ChaiBigNumber = require('chai-bignumber'); +import * as dirtyChai from 'dirty-chai'; + +export const chaiSetup = { + configure() { + chai.config.includeStack = true; + chai.use(ChaiBigNumber()); + chai.use(dirtyChai); + chai.use(chaiAsPromised); + }, +}; diff --git a/packages/order-utils/test/utils/web3_wrapper.ts b/packages/order-utils/test/utils/web3_wrapper.ts new file mode 100644 index 000000000..b0ccfa546 --- /dev/null +++ b/packages/order-utils/test/utils/web3_wrapper.ts @@ -0,0 +1,9 @@ +import { devConstants, web3Factory } from '@0xproject/dev-utils'; +import { Provider } from '@0xproject/types'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; + +const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); +const provider: Provider = web3.currentProvider; +const web3Wrapper = new Web3Wrapper(web3.currentProvider); + +export { provider, web3Wrapper }; diff --git a/packages/order-utils/tsconfig.json b/packages/order-utils/tsconfig.json new file mode 100644 index 000000000..8b4cd47a2 --- /dev/null +++ b/packages/order-utils/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["src/**/*", "test/**/*"] +} diff --git a/packages/order-utils/tslint.json b/packages/order-utils/tslint.json new file mode 100644 index 000000000..ffaefe83a --- /dev/null +++ b/packages/order-utils/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["@0xproject/tslint-config"] +} diff --git a/packages/react-docs/src/types.ts b/packages/react-docs/src/types.ts index 3b4a57ad5..f4e61edc9 100644 --- a/packages/react-docs/src/types.ts +++ b/packages/react-docs/src/types.ts @@ -94,6 +94,7 @@ export enum KindString { Method = 'Method', Interface = 'Interface', TypeAlias = 'Type alias', + ObjectLiteral = 'Object literal', Variable = 'Variable', Function = 'Function', Enumeration = 'Enumeration', diff --git a/packages/react-docs/src/utils/typedoc_utils.ts b/packages/react-docs/src/utils/typedoc_utils.ts index 9c89b135a..0c7803a04 100644 --- a/packages/react-docs/src/utils/typedoc_utils.ts +++ b/packages/react-docs/src/utils/typedoc_utils.ts @@ -93,10 +93,16 @@ export const typeDocUtils = { throw new Error('`react-docs` only supports projects with 1 exported class per file'); } const isClassExport = packageDefinitionWithMergedChildren.children[0].kindString === KindString.Class; + const isObjectLiteralExport = + packageDefinitionWithMergedChildren.children[0].kindString === KindString.ObjectLiteral; if (isClassExport) { entities = packageDefinitionWithMergedChildren.children[0].children; const commentObj = packageDefinitionWithMergedChildren.children[0].comment; packageComment = !_.isUndefined(commentObj) ? commentObj.shortText : packageComment; + } else if (isObjectLiteralExport) { + entities = packageDefinitionWithMergedChildren.children[0].children; + const commentObj = packageDefinitionWithMergedChildren.children[0].comment; + packageComment = !_.isUndefined(commentObj) ? commentObj.shortText : packageComment; } else { entities = packageDefinitionWithMergedChildren.children; } diff --git a/packages/website/md/docs/order_utils/installation.md b/packages/website/md/docs/order_utils/installation.md new file mode 100644 index 000000000..68a7cf960 --- /dev/null +++ b/packages/website/md/docs/order_utils/installation.md @@ -0,0 +1,17 @@ +**Install** + +```bash +yarn add @0xproject/order-utils +``` + +**Import** + +```javascript +import { createSignedOrderAsync } from '@0xproject/order-utils'; +``` + +or + +```javascript +var createSignedOrderAsync = require('@0xproject/order-utils').createSignedOrderAsync; +``` diff --git a/packages/website/md/docs/order_utils/introduction.md b/packages/website/md/docs/order_utils/introduction.md new file mode 100644 index 000000000..d5f3f2925 --- /dev/null +++ b/packages/website/md/docs/order_utils/introduction.md @@ -0,0 +1 @@ +Welcome to the [@0xproject/order-utils](https://github.com/0xProject/0x-monorepo/tree/development/packages/order-utils) documentation! Order utils is a set of utils around creating, signing, validating 0x orders. diff --git a/packages/website/ts/containers/order_utils_documentation.ts b/packages/website/ts/containers/order_utils_documentation.ts new file mode 100644 index 000000000..64aa7300f --- /dev/null +++ b/packages/website/ts/containers/order_utils_documentation.ts @@ -0,0 +1,99 @@ +import { constants as docConstants, DocsInfo, DocsInfoConfig, SupportedDocJson } from '@0xproject/react-docs'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { DocPage as DocPageComponent, DocPageProps } from 'ts/pages/documentation/doc_page'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { State } from 'ts/redux/reducer'; +import { DocPackages, Environments, WebsitePaths } from 'ts/types'; +import { configs } from 'ts/utils/configs'; +import { constants } from 'ts/utils/constants'; +import { Translate } from 'ts/utils/translate'; + +/* tslint:disable:no-var-requires */ +const IntroMarkdown = require('md/docs/order_utils/introduction'); +const InstallationMarkdown = require('md/docs/order_utils/installation'); +/* tslint:enable:no-var-requires */ + +const docSections = { + introduction: 'introduction', + installation: 'installation', + usage: 'usage', + types: 'types', +}; + +const docsInfoConfig: DocsInfoConfig = { + id: DocPackages.OrderUtils, + type: SupportedDocJson.TypeDoc, + displayName: 'Order utils', + packageUrl: 'https://github.com/0xProject/0x-monorepo', + menu: { + introduction: [docSections.introduction], + install: [docSections.installation], + usage: [docSections.usage], + types: [docSections.types], + }, + sectionNameToMarkdown: { + [docSections.introduction]: IntroMarkdown, + [docSections.installation]: InstallationMarkdown, + }, + sectionNameToModulePath: { + [docSections.usage]: [ + '"order-utils/src/order_hash"', + '"order-utils/src/signature_utils"', + '"order-utils/src/order_factory"', + '"order-utils/src/salt"', + '"order-utils/src/assert"', + '"order-utils/src/constants"', + ], + [docSections.types]: ['"order-utils/src/types"', '"types/src/index"'], + }, + menuSubsectionToVersionWhenIntroduced: {}, + sections: docSections, + visibleConstructors: [], + typeConfigs: { + // Note: This needs to be kept in sync with the types exported in index.ts. Unfortunately there is + // currently no way to extract the re-exported types from index.ts via TypeDoc :( + publicTypes: [ + 'OrderError', + 'Order', + 'SignedOrder', + 'ECSignature', + 'Provider', + 'JSONRPCRequestPayload', + 'JSONRPCResponsePayload', + 'JSONRPCErrorCallback', + ], + typeNameToExternalLink: { + BigNumber: constants.URL_BIGNUMBERJS_GITHUB, + }, + }, +}; +const docsInfo = new DocsInfo(docsInfoConfig); + +interface ConnectedState { + docsVersion: string; + availableDocVersions: string[]; + docsInfo: DocsInfo; + translate: Translate; +} + +interface ConnectedDispatch { + dispatcher: Dispatcher; +} + +const mapStateToProps = (state: State, ownProps: DocPageProps): ConnectedState => ({ + docsVersion: state.docsVersion, + availableDocVersions: state.availableDocVersions, + translate: state.translate, + docsInfo, +}); + +const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ + dispatcher: new Dispatcher(dispatch), +}); + +export const Documentation: React.ComponentClass<DocPageProps> = connect(mapStateToProps, mapDispatchToProps)( + DocPageComponent, +); diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index d99187151..9535dd222 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -61,6 +61,9 @@ const LazySolCovDocumentation = createLazyComponent('Documentation', async () => const LazySubprovidersDocumentation = createLazyComponent('Documentation', async () => System.import<any>(/* webpackChunkName: "subproviderDocs" */ 'ts/containers/subproviders_documentation'), ); +const LazyOrderUtilsDocumentation = createLazyComponent('Documentation', async () => + System.import<any>(/* webpackChunkName: "orderUtilsDocs" */ 'ts/containers/order_utils_documentation'), +); analytics.init(); // tslint:disable-next-line:no-floating-promises @@ -94,6 +97,10 @@ render( component={LazySubprovidersDocumentation} /> <Route + path={`${WebsitePaths.OrderUtils}/:version?`} + component={LazyOrderUtilsDocumentation} + /> + <Route path={`${WebsitePaths.Web3Wrapper}/:version?`} component={LazyWeb3WrapperDocumentation} /> diff --git a/packages/website/ts/pages/documentation/doc_page.tsx b/packages/website/ts/pages/documentation/doc_page.tsx index aca20447b..9feb27dac 100644 --- a/packages/website/ts/pages/documentation/doc_page.tsx +++ b/packages/website/ts/pages/documentation/doc_page.tsx @@ -34,6 +34,7 @@ const docIdToSubpackageName: { [id: string]: string } = { [DocPackages.JSONSchemas]: 'json-schemas', [DocPackages.SolCov]: 'sol-cov', [DocPackages.Subproviders]: 'subproviders', + [DocPackages.OrderUtils]: 'order-utils', }; export interface DocPageProps { diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index f4fddb556..fb4f22873 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -361,6 +361,7 @@ export enum WebsitePaths { JSONSchemas = '/docs/json-schemas', SolCov = '/docs/sol-cov', Subproviders = '/docs/subproviders', + OrderUtils = '/docs/order-utils', Jobs = '/jobs', } @@ -373,6 +374,7 @@ export enum DocPackages { JSONSchemas = 'JSON_SCHEMAS', SolCov = 'SOL_COV', Subproviders = 'SUBPROVIDERS', + OrderUtils = 'ORDER_UTILS', } export enum Key { diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 53d60b3ce..0d77b9e72 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -1,4 +1,5 @@ import { ECSignature, ExchangeContractErrs, ZeroEx, ZeroExError } from '0x.js'; +import { OrderError } from '@0xproject/order-utils'; import { constants as sharedConstants, EtherscanLinkSuffixes, Networks } from '@0xproject/react-shared'; import { Provider } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; @@ -202,7 +203,7 @@ export const utils = { [ZeroExError.ZRXContractDoesNotExist]: 'ZRX contract does not exist', [ZeroExError.UnhandledError]: 'Unhandled error occured', [ZeroExError.UserHasNoAssociatedAddress]: 'User has no addresses available', - [ZeroExError.InvalidSignature]: 'Order signature is not valid', + [OrderError.InvalidSignature]: 'Order signature is not valid', [ZeroExError.ContractNotDeployedOnNetwork]: 'Contract is not deployed on the detected network', [ZeroExError.InvalidJump]: 'Invalid jump occured while executing the transaction', [ZeroExError.OutOfGas]: 'Transaction ran out of gas', |