diff options
-rw-r--r-- | circle.yml | 2 | ||||
-rw-r--r-- | package.json | 6 | ||||
-rw-r--r-- | src/0x.js.ts | 64 | ||||
-rw-r--r-- | src/globals.d.ts | 13 | ||||
-rw-r--r-- | src/types.ts | 1 | ||||
-rw-r--r-- | src/utils/utils.ts | 7 | ||||
-rw-r--r-- | test/0x.js_test.ts | 70 |
7 files changed, 158 insertions, 5 deletions
diff --git a/circle.yml b/circle.yml index cce012832..b6a9efeb1 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ machine: test: override: - - node node_modules/ethereumjs-testrpc/bin/testrpc: + - node node_modules/ethereumjs-testrpc/bin/testrpc -m "concert load couple harbor equip island argue ramp clarify fence smart topic": background: true - git clone git@github.com:0xProject/contracts.git ../contracts - cd ../contracts; git checkout 38c2b4c; npm install && npm run migrate diff --git a/package.json b/package.json index 3b29795a8..fc2589ee0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "run-s test:commonjs test:umd", "test:coverage": "nyc npm run test --all", "update_contracts": "for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../0x.js/src/artifacts; done;", - "testrpc": "testrpc -p 8545 --networkId 50", + "testrpc": "testrpc -p 8545 --networkId 50 -m \"concert load couple harbor equip island argue ramp clarify fence smart topic\"", "docs:json": "typedoc --json docs/index.json .", "docs:generate": "typedoc --out docs .", "docs:open": "opn docs/index.html", @@ -51,6 +51,7 @@ "@types/lodash": "^4.14.64", "@types/mocha": "^2.2.41", "@types/node": "^7.0.22", + "@types/sinon": "^2.2.2", "awesome-typescript-loader": "^3.1.3", "bignumber.js": "^4.0.2", "chai": "^3.5.0", @@ -66,6 +67,7 @@ "request": "^2.81.0", "request-promise-native": "^1.0.4", "shx": "^0.2.2", + "sinon": "^2.3.2", "source-map-support": "^0.4.15", "tslint": "^5.3.2", "tslint-config-0xproject": "^0.0.2", @@ -77,9 +79,11 @@ }, "dependencies": { "bignumber.js": "^4.0.2", + "compare-versions": "^3.0.1", "es6-promisify": "^5.0.0", "ethereumjs-abi": "^0.6.4", "ethereumjs-util": "^5.1.1", + "find-versions": "^2.0.0", "jsonschema": "^1.1.1", "lodash": "^4.17.4", "truffle-contract": "^2.0.0", diff --git a/src/0x.js.ts b/src/0x.js.ts index de082146f..eff371c3b 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -8,9 +8,11 @@ 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 {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; import {ECSignatureSchema} from './schemas/ec_signature_schema'; -import {SolidityTypes, ECSignature} from './types'; +import {SolidityTypes, ECSignature, ZeroExError} from './types'; const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; @@ -131,4 +133,64 @@ export class ZeroEx { this.web3Wrapper = new Web3Wrapper(web3); this.exchange = new ExchangeWrapper(this.web3Wrapper); } + /** + * Signs an orderHash and returns it's ECSignature + * This method currently supports TestRPC, Geth and Parity above and below V1.6.6 + */ + public async signOrderHashAsync(orderHashHex: string): Promise<ECSignature> { + assert.isHexString('orderHashHex', orderHashHex); + + let msgHashHex; + const nodeVersion = await this.web3Wrapper.getNodeVersionAsync(); + const isParityNode = utils.isParityNode(nodeVersion); + if (isParityNode) { + // Parity node adds the personalMessage prefix itself + msgHashHex = orderHashHex; + } else { + const orderHashBuff = ethUtil.toBuffer(orderHashHex); + const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff); + 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); + + let signatureData; + const [nodeVersionNumber] = findVersions(nodeVersion); + // Parity v1.6.6 and earlier returns the signatureData as vrs instead of rsv as Geth does + // Later versions return rsv but for the time being we still want to support version < 1.6.6 + // Date: May 23rd 2017 + const latestParityVersionWithVRS = '1.6.6'; + const isVersionBeforeParityFix = compareVersions(nodeVersionNumber, latestParityVersionWithVRS) <= 0; + if (isParityNode && isVersionBeforeParityFix) { + const signatureBuffer = ethUtil.toBuffer(signature); + let v = signatureBuffer[0]; + if (v < 27) { + v += 27; + } + signatureData = { + v, + r: signatureBuffer.slice(1, 33), + s: signatureBuffer.slice(33, 65), + }; + } else { + signatureData = ethUtil.fromRpcSig(signature); + } + + const {v, r, s} = signatureData; + const ecSignature: ECSignature = { + v, + r: ethUtil.bufferToHex(r), + s: ethUtil.bufferToHex(s), + }; + const isValidSignature = ZeroEx.isValidSignature(orderHashHex, ecSignature, makerAddressIfExists); + if (!isValidSignature) { + throw new Error(ZeroExError.INVALID_SIGNATURE); + } + return ecSignature; + } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 0062a05cb..0f2fe0f2f 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -36,6 +36,7 @@ declare module 'ethereumjs-util' { const pubToAddress: (pubKey: string) => Buffer; const isValidAddress: (address: string) => boolean; const bufferToInt: (buffer: Buffer) => number; + const fromRpcSig: (signature: string) => {v: number, r: Buffer, s: Buffer}; } // truffle-contract declarations @@ -53,6 +54,18 @@ declare module 'truffle-contract' { export = contract; } +// find-version declarations +declare function findVersions(version: string): string[]; +declare module 'find-versions' { + export = findVersions; +} + +// compare-version declarations +declare function compareVersions(firstVersion: string, secondVersion: string): number; +declare module 'compare-versions' { + export = compareVersions; +} + // es6-promisify declarations declare function promisify(original: any, settings?: any): ((...arg: any[]) => Promise<any>); declare module 'es6-promisify' { diff --git a/src/types.ts b/src/types.ts index 4da03a4d3..3bed01547 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,7 @@ export const ZeroExError = strEnum([ 'CONTRACT_DOES_NOT_EXIST', 'UNHANDLED_ERROR', 'USER_HAS_NO_ASSOCIATED_ADDRESSES', + 'INVALID_SIGNATURE', ]); export type ZeroExError = keyof typeof ZeroExError; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b514b702d..336eaf7bb 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import * as BN from 'bn.js'; export const utils = { @@ -11,8 +12,10 @@ export const utils = { return new BN(value.toString(), 10); }, consoleLog(message: string): void { - /* tslint:disable */ + // tslint:disable-next-line: no-console console.log(message); - /* tslint:enable */ + }, + isParityNode(nodeVersion: string): boolean { + return _.includes(nodeVersion, 'Parity'); }, }; diff --git a/test/0x.js_test.ts b/test/0x.js_test.ts index 289c823af..871e8c727 100644 --- a/test/0x.js_test.ts +++ b/test/0x.js_test.ts @@ -3,8 +3,10 @@ import * as chai from 'chai'; import 'mocha'; import * as BigNumber from 'bignumber.js'; import ChaiBigNumber = require('chai-bignumber'); +import * as Sinon from 'sinon'; import {ZeroEx} from '../src/0x.js'; import {constants} from './utils/constants'; +import {web3Factory} from './utils/web3_factory'; // Use BigNumber chai add-on chai.use(ChaiBigNumber()); @@ -158,4 +160,72 @@ describe('ZeroEx library', () => { expect(baseUnitAmount).to.be.bignumber.equal(expectedUnitAmount); }); }); + describe('#signOrderHashAsync', () => { + let stubs: Sinon.SinonStub[] = []; + afterEach(() => { + // clean up any stubs after the test has completed + _.each(stubs, s => s.restore()); + }); + it ('Should return the correct ECSignature on TestPRC nodeVersion', async () => { + const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; + const expectedECSignature = { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }; + + const web3 = web3Factory.create(); + const zeroEx = new ZeroEx(web3); + const ecSignature = await zeroEx.signOrderHashAsync(orderHash); + expect(ecSignature).to.deep.equal(expectedECSignature); + }); + it ('should return the correct ECSignature on Party > V1.6.6', async () => { + const newParityNodeVersion = 'Parity//v1.6.7-beta-e128418-20170518/x86_64-macos/rustc1.17.0'; + const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; + // tslint:disable-next-line: max-line-length + const signature = '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb021b'; + const expectedECSignature = { + v: 27, + r: '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3', + s: '0x050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb02', + }; + + const web3 = web3Factory.create(); + const zeroEx = new ZeroEx(web3); + stubs = [ + Sinon.stub(zeroEx.web3Wrapper, 'getNodeVersionAsync') + .returns(Promise.resolve(newParityNodeVersion)), + Sinon.stub(zeroEx.web3Wrapper, 'signTransactionAsync') + .returns(Promise.resolve(signature)), + Sinon.stub(ZeroEx, 'isValidSignature').returns(true), + ]; + + const ecSignature = await zeroEx.signOrderHashAsync(orderHash); + expect(ecSignature).to.deep.equal(expectedECSignature); + }); + it ('should return the correct ECSignature on Party < V1.6.6', async () => { + const newParityNodeVersion = 'Parity//v1.6.6-beta-8c6e3f3-20170411/x86_64-macos/rustc1.16.0'; + const orderHash = '0xc793e33ffded933b76f2f48d9aa3339fc090399d5e7f5dec8d3660f5480793f7'; + // tslint:disable-next-line: max-line-length + const signature = '0x1bc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee02dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960'; + const expectedECSignature = { + v: 27, + r: '0xc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee0', + s: '0x2dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960', + }; + + const web3 = web3Factory.create(); + const zeroEx = new ZeroEx(web3); + stubs = [ + Sinon.stub(zeroEx.web3Wrapper, 'getNodeVersionAsync') + .returns(Promise.resolve(newParityNodeVersion)), + Sinon.stub(zeroEx.web3Wrapper, 'signTransactionAsync') + .returns(Promise.resolve(signature)), + Sinon.stub(ZeroEx, 'isValidSignature').returns(true), + ]; + + const ecSignature = await zeroEx.signOrderHashAsync(orderHash); + expect(ecSignature).to.deep.equal(expectedECSignature); + }); + }); }); |