From 6a5965d73bb542634631d7af76c150795d899744 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Thu, 26 Jul 2018 14:11:03 -0700 Subject: Add strictArgumentEncodingCheck to BaseContract and use it in contract templates --- packages/0x.js/test/0x.js_test.ts | 3 +- packages/base-contract/src/index.ts | 17 +++ packages/base-contract/test/base_contract_test.ts | 110 +++++++++++++++ .../partials/callAsync.handlebars | 1 + packages/contract_templates/partials/tx.handlebars | 1 + .../contracts/test/exchange/signature_validator.ts | 6 +- packages/order-utils/test/signature_utils_test.ts | 3 +- packages/utils/package.json | 10 +- packages/utils/src/abi_utils.ts | 153 +++++++++++++++++++++ packages/utils/test/abi_utils_test.ts | 19 +++ packages/utils/tsconfig.json | 5 +- 11 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 packages/base-contract/test/base_contract_test.ts create mode 100644 packages/utils/test/abi_utils_test.ts diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts index b4baecb1e..2c1cb2c74 100644 --- a/packages/0x.js/test/0x.js_test.ts +++ b/packages/0x.js/test/0x.js_test.ts @@ -48,8 +48,9 @@ describe('ZeroEx library', () => { const ethSignSignature = '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403'; const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; + const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; it("should return false if the data doesn't pertain to the signature & address", async () => { - return expect((zeroEx.exchange as any).isValidSignatureAsync('0x0', address, ethSignSignature)).to.become( + return expect((zeroEx.exchange as any).isValidSignatureAsync(bytes32Zeros, address, ethSignSignature)).to.become( false, ); }); diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index a240fb8b6..e354f109e 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -82,6 +82,23 @@ export class BaseContract { } return txDataWithDefaults; } + // Throws if the given arguments cannot be safely/correctly encoded based on + // the given inputAbi. An argument may not be considered safely encodeable + // if it overflows the corresponding Solidity type, there is a bug in the + // encoder, or the encoder performs unsafe type coercion. + public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void { + const coder = (ethers as any).utils.AbiCoder.defaultCoder; + const params = abiUtils.parseEthersParams(inputAbi); + const rawEncoded = coder.encode(params.names, params.types, args); + const rawDecoded = coder.decode(params.names, params.types, rawEncoded); + for (let i = 0; i < rawDecoded.length; i++) { + const original = args[i]; + const decoded = rawDecoded[i]; + if (!abiUtils.isAbiDataEqual(params.names[i], params.types[i], original, decoded)) { + throw new Error(`Cannot safely encode argument: ${params.names[i]} (${original}) of type ${params.types[i]}. (Possible type overflow or other encoding error)`); + } + } + } protected _lookupEthersInterface(functionSignature: string): ethers.Interface { const ethersInterface = this._ethersInterfacesByFunctionSignature[functionSignature]; if (_.isUndefined(ethersInterface)) { diff --git a/packages/base-contract/test/base_contract_test.ts b/packages/base-contract/test/base_contract_test.ts new file mode 100644 index 000000000..57aacdc8a --- /dev/null +++ b/packages/base-contract/test/base_contract_test.ts @@ -0,0 +1,110 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { BaseContract } from '../src'; + +const { expect } = chai; + +describe('BaseContract', () => { + describe('strictArgumentEncodingCheck', () => { + it('works for simple types', () => { + BaseContract.strictArgumentEncodingCheck([{ name: 'to', type: 'address' }], ['0xe834ec434daba538cd1b9fe1582052b880bd7e63']); + }); + it('works for array types', () => { + const inputAbi = [{ + name: 'takerAssetFillAmounts', + type: 'uint256[]', + }]; + const args = [ + [ + '9000000000000000000', + '79000000000000000000', + '979000000000000000000', + '7979000000000000000000', + ], + ]; + BaseContract.strictArgumentEncodingCheck(inputAbi, args); + }); + it('works for tuple/struct types', () => { + const inputAbi = [ + { + components: [ + { + name: 'makerAddress', + type: 'address', + }, + { + name: 'takerAddress', + type: 'address', + }, + { + name: 'feeRecipientAddress', + type: 'address', + }, + { + name: 'senderAddress', + type: 'address', + }, + { + name: 'makerAssetAmount', + type: 'uint256', + }, + { + name: 'takerAssetAmount', + type: 'uint256', + }, + { + name: 'makerFee', + type: 'uint256', + }, + { + name: 'takerFee', + type: 'uint256', + }, + { + name: 'expirationTimeSeconds', + type: 'uint256', + }, + { + name: 'salt', + type: 'uint256', + }, + { + name: 'makerAssetData', + type: 'bytes', + }, + { + name: 'takerAssetData', + type: 'bytes', + }, + ], + name: 'order', + type: 'tuple', + }, + ]; + const args = [ + { + makerAddress: '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb', + takerAddress: '0x0000000000000000000000000000000000000000', + feeRecipientAddress: '0xe834ec434daba538cd1b9fe1582052b880bd7e63', + senderAddress: '0x0000000000000000000000000000000000000000', + makerAssetAmount: '0', + takerAssetAmount: '200000000000000000000', + makerFee: '1000000000000000000', + takerFee: '1000000000000000000', + expirationTimeSeconds: '1532563026', + salt: '59342956082154660870994022243365949771115859664887449740907298019908621891376', + makerAssetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', + takerAssetData: '0xf47261b00000000000000000000000001d7022f5b17d2f8b695918fb48fa1089c9f85401', + }, + ]; + BaseContract.strictArgumentEncodingCheck(inputAbi, args); + }); + it('throws for integer overflows', () => { + expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'amount', type: 'uint8' }], ['256'])).to.throw(); + }); + it('throws for fixed byte array overflows', () => { + expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'hash', type: 'bytes8' }], ['0x001122334455667788'])).to.throw(); + }); + }); +}); diff --git a/packages/contract_templates/partials/callAsync.handlebars b/packages/contract_templates/partials/callAsync.handlebars index fcaae57c6..94752691d 100644 --- a/packages/contract_templates/partials/callAsync.handlebars +++ b/packages/contract_templates/partials/callAsync.handlebars @@ -7,6 +7,7 @@ async callAsync( const functionSignature = '{{this.functionSignature}}'; const inputAbi = self._lookupAbi(functionSignature).inputs; [{{> params inputs=inputs}}] = BaseContract._formatABIDataItemList(inputAbi, [{{> params inputs=inputs}}], BaseContract._bigNumberToString.bind(self)); + BaseContract.strictArgumentEncodingCheck(inputAbi, [{{> params inputs=inputs}}]); const ethersFunction = self._lookupEthersInterface(functionSignature).functions.{{this.name}}( {{> params inputs=inputs}} ) as ethers.CallDescription; diff --git a/packages/contract_templates/partials/tx.handlebars b/packages/contract_templates/partials/tx.handlebars index e297d05e6..4340d662e 100644 --- a/packages/contract_templates/partials/tx.handlebars +++ b/packages/contract_templates/partials/tx.handlebars @@ -11,6 +11,7 @@ public {{this.tsName}} = { const self = this as any as {{contractName}}Contract; const inputAbi = self._lookupAbi('{{this.functionSignature}}').inputs; [{{> params inputs=inputs}}] = BaseContract._formatABIDataItemList(inputAbi, [{{> params inputs=inputs}}], BaseContract._bigNumberToString.bind(self)); + BaseContract.strictArgumentEncodingCheck(inputAbi, [{{> params inputs=inputs}}]); const encodedData = self._lookupEthersInterface('{{this.functionSignature}}').functions.{{this.name}}( {{> params inputs=inputs}} ).data; diff --git a/packages/contracts/test/exchange/signature_validator.ts b/packages/contracts/test/exchange/signature_validator.ts index f2bb42c75..bef0547bd 100644 --- a/packages/contracts/test/exchange/signature_validator.ts +++ b/packages/contracts/test/exchange/signature_validator.ts @@ -113,7 +113,7 @@ describe('MixinSignatureValidator', () => { it('should revert when signature type is unsupported', async () => { const unsupportedSignatureType = SignatureType.NSignatureTypes; - const unsupportedSignatureHex = `0x${unsupportedSignatureType}`; + const unsupportedSignatureHex = '0x' + Buffer.from([unsupportedSignatureType]).toString('hex'); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); return expectContractCallFailed( signatureValidator.publicIsValidSignature.callAsync( @@ -126,7 +126,7 @@ describe('MixinSignatureValidator', () => { }); it('should revert when SignatureType=Illegal', async () => { - const unsupportedSignatureHex = `0x${SignatureType.Illegal}`; + const unsupportedSignatureHex = '0x' + Buffer.from([SignatureType.Illegal]).toString('hex'); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); return expectContractCallFailed( signatureValidator.publicIsValidSignature.callAsync( @@ -139,7 +139,7 @@ describe('MixinSignatureValidator', () => { }); it('should return false when SignatureType=Invalid and signature has a length of zero', async () => { - const signatureHex = `0x${SignatureType.Invalid}`; + const signatureHex = '0x' + Buffer.from([SignatureType.Invalid]).toString('hex'); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( orderHashHex, diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index 5714f9671..baae2b414 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -22,7 +22,8 @@ describe('Signature utils', () => { let address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; it("should return false if the data doesn't pertain to the signature & address", async () => { - expect(await isValidSignatureAsync(provider, '0x0', ethSignSignature, address)).to.be.false(); + const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; + expect(await isValidSignatureAsync(provider, bytes32Zeros, ethSignSignature, address)).to.be.false(); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; diff --git a/packages/utils/package.json b/packages/utils/package.json index ee150cb0e..46c1d05d0 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -5,12 +5,13 @@ "node": ">=6.12" }, "description": "0x TS utils", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", "scripts": { "watch_without_deps": "tsc -w", "build": "tsc && copyfiles -u 2 './lib/monorepo_scripts/**/*' ./scripts", "clean": "shx rm -rf lib scripts", + "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", "lint": "tslint --project .", "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, @@ -27,12 +28,15 @@ "@0xproject/monorepo-scripts": "^1.0.4", "@0xproject/tslint-config": "^1.0.4", "@types/lodash": "4.14.104", + "@types/mocha": "^2.2.42", "copyfiles": "^1.2.0", "make-promises-safe": "^1.1.0", "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.11.0", - "typescript": "2.9.2" + "typescript": "2.9.2", + "chai": "^4.0.1", + "mocha": "^4.0.1" }, "dependencies": { "@0xproject/types": "^1.0.1-rc.3", diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 421dd405c..16aa72afd 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -1,7 +1,160 @@ import { AbiDefinition, AbiType, ContractAbi, DataItem, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; +import { BigNumber } from './configured_bignumber'; + +export type EthersParamName = null | string | EthersNestedParamName; + +export interface EthersNestedParamName { + name: string | null; + names: EthersParamName[]; +} + +// Note(albrow): This function is unexported in ethers.js. Copying it here for +// now. +// Source: https://github.com/ethers-io/ethers.js/blob/884593ab76004a808bf8097e9753fb5f8dcc3067/contracts/interface.js#L30 +function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; types: string[] } { + const names: EthersParamName[] = []; + const types: string[] = []; + + params.forEach((param: DataItem) => { + if (param.components != null) { + let suffix = ''; + const arrayBracket = param.type.indexOf('['); + if (arrayBracket >= 0) { suffix = param.type.substring(arrayBracket); } + + const result = parseEthersParams(param.components); + names.push({ name: (param.name || null), names: result.names }); + types.push('tuple(' + result.types.join(',') + ')' + suffix); + } else { + names.push(param.name || null); + types.push(param.type); + } + }); + + return { + names, + types, + }; +} + +// returns true if x is equal to y and false otherwise. Performs some minimal +// type conversion and data massaging for x and y, depending on type. name and +// type should typically be derived from parseEthersParams. +function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): boolean { + if (_.isUndefined(x) && _.isUndefined(y)) { + return true; + } else if (_.isUndefined(x) && !_.isUndefined(y)) { + return false; + } else if (!_.isUndefined(x) && _.isUndefined(y)) { + return false; + } + if (_.endsWith(type, '[]')) { + // For array types, we iterate through the elements and check each one + // individually. Strangely, name does not need to be changed in this + // case. + if (x.length !== y.length) { + return false; + } + const newType = _.trimEnd(type, '[]'); + for (let i = 0; i < x.length; i++) { + if (!isAbiDataEqual(name, newType, x[i], y[i])) { + return false; + } + } + return true; + } + if (_.startsWith(type, 'tuple(')) { + if (_.isString(name)) { + throw new Error('Internal error: type was tuple but names was a string'); + } else if (_.isNull(name)) { + throw new Error('Internal error: type was tuple but names was a null'); + } + // For tuples, we iterate through the underlying values and check each + // one individually. + const types = splitTupleTypes(type); + if (types.length !== name.names.length) { + throw new Error(`Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`); + } + for (let i = 0; i < types.length; i++) { + // For tuples, name is an object with a names property that is an + // array. As an example, for orders, name looks like: + // + // { + // name: 'orders', + // names: [ + // 'makerAddress', + // // ... + // 'takerAssetData' + // ] + // } + // + const nestedName = _.isString(name.names[i]) ? name.names[i] as string : (name.names[i] as EthersNestedParamName).name as string; + if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { + return false; + } + } + return true; + } else if (type === 'address' || type === 'bytes') { + // HACK(albrow): ethers.js sometimes changes the case of addresses/bytes + // when decoding/encoding. To account for that, we convert to lowercase + // before comparing. + return _.isEqual(_.toLower(x), _.toLower(y)); + } else if (_.startsWith(type, 'uint') || _.startsWith(type, 'int')) { + return new BigNumber(x).eq(new BigNumber(y)); + } + return _.isEqual(x, y); +} + +// splitTupleTypes splits a tuple type string (of the form `tuple(X)` where X is +// any other type or list of types) into its component types. It works with +// nested tuples, so, e.g., `tuple(tuple(uint256,address),bytes32)` will yield: +// `['tuple(uint256,address)', 'bytes32']`. It expects exactly one tuple type as +// an argument (not an array). +function splitTupleTypes(type: string): string[] { + if (_.endsWith(type, '[]')) { + throw new Error('Internal error: array types are not supported'); + } else if (!_.startsWith(type, 'tuple(')) { + throw new Error('Internal error: expected tuple type but got non-tuple type: ' + type); + } + // Trim the outtermost tuple(). + const trimmedType = type.substring('tuple('.length, type.length - 1); + const types: string[] = []; + let currToken = ''; + let parenCount = 0; + // Tokenize the type string while keeping track of parentheses. + for (const char of trimmedType) { + switch (char) { + case '(': + parenCount += 1; + currToken += char; + break; + case ')': + parenCount -= 1; + currToken += char; + break; + case ',': + if (parenCount === 0) { + types.push(currToken); + currToken = ''; + break; + } else { + currToken += char; + break; + } + default: + currToken += char; + break; + } + } + types.push(currToken); + return types; +} + export const abiUtils = { + parseEthersParams, + isAbiDataEqual, + splitTupleTypes, parseFunctionParam(param: DataItem): string { if (param.type === 'tuple') { // Parse out tuple types into {type_1, type_2, ..., type_N} diff --git a/packages/utils/test/abi_utils_test.ts b/packages/utils/test/abi_utils_test.ts new file mode 100644 index 000000000..0ebee64c4 --- /dev/null +++ b/packages/utils/test/abi_utils_test.ts @@ -0,0 +1,19 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { abiUtils } from '../src'; + +const expect = chai.expect; + +describe('abiUtils', () => { + describe('splitTupleTypes', () => { + it('handles basic types', () => { + const got = abiUtils.splitTupleTypes('tuple(bytes,uint256,address)'); + expect(got).to.deep.equal(['bytes', 'uint256', 'address']); + }); + it('handles nested tuple types', () => { + const got = abiUtils.splitTupleTypes('tuple(tuple(bytes,uint256),address)'); + expect(got).to.deep.equal(['tuple(bytes,uint256)', 'address']); + }); + }); +}); diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index c56d255d5..852708eba 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "outDir": "lib" }, - "include": ["./src/**/*"] + "include": [ + "src/**/*", + "test/**/*" + ] } -- cgit v1.2.3 From 6a6739ebbec291b61226c047fde7b3d0bb4a7250 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Thu, 26 Jul 2018 14:32:52 -0700 Subject: Apply prettier --- packages/0x.js/test/0x.js_test.ts | 6 ++--- packages/base-contract/src/index.ts | 6 ++++- packages/base-contract/test/base_contract_test.ts | 30 +++++++++++++---------- packages/utils/src/abi_utils.ts | 14 ++++++++--- packages/utils/tsconfig.json | 5 +--- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts index 2c1cb2c74..be2a94482 100644 --- a/packages/0x.js/test/0x.js_test.ts +++ b/packages/0x.js/test/0x.js_test.ts @@ -50,9 +50,9 @@ describe('ZeroEx library', () => { const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; it("should return false if the data doesn't pertain to the signature & address", async () => { - return expect((zeroEx.exchange as any).isValidSignatureAsync(bytes32Zeros, address, ethSignSignature)).to.become( - false, - ); + return expect( + (zeroEx.exchange as any).isValidSignatureAsync(bytes32Zeros, address, ethSignSignature), + ).to.become(false); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index e354f109e..31d2e4019 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -95,7 +95,11 @@ export class BaseContract { const original = args[i]; const decoded = rawDecoded[i]; if (!abiUtils.isAbiDataEqual(params.names[i], params.types[i], original, decoded)) { - throw new Error(`Cannot safely encode argument: ${params.names[i]} (${original}) of type ${params.types[i]}. (Possible type overflow or other encoding error)`); + throw new Error( + `Cannot safely encode argument: ${params.names[i]} (${original}) of type ${ + params.types[i] + }. (Possible type overflow or other encoding error)`, + ); } } } diff --git a/packages/base-contract/test/base_contract_test.ts b/packages/base-contract/test/base_contract_test.ts index 57aacdc8a..2c31d1f11 100644 --- a/packages/base-contract/test/base_contract_test.ts +++ b/packages/base-contract/test/base_contract_test.ts @@ -8,20 +8,20 @@ const { expect } = chai; describe('BaseContract', () => { describe('strictArgumentEncodingCheck', () => { it('works for simple types', () => { - BaseContract.strictArgumentEncodingCheck([{ name: 'to', type: 'address' }], ['0xe834ec434daba538cd1b9fe1582052b880bd7e63']); + BaseContract.strictArgumentEncodingCheck( + [{ name: 'to', type: 'address' }], + ['0xe834ec434daba538cd1b9fe1582052b880bd7e63'], + ); }); it('works for array types', () => { - const inputAbi = [{ - name: 'takerAssetFillAmounts', - type: 'uint256[]', - }]; + const inputAbi = [ + { + name: 'takerAssetFillAmounts', + type: 'uint256[]', + }, + ]; const args = [ - [ - '9000000000000000000', - '79000000000000000000', - '979000000000000000000', - '7979000000000000000000', - ], + ['9000000000000000000', '79000000000000000000', '979000000000000000000', '7979000000000000000000'], ]; BaseContract.strictArgumentEncodingCheck(inputAbi, args); }); @@ -101,10 +101,14 @@ describe('BaseContract', () => { BaseContract.strictArgumentEncodingCheck(inputAbi, args); }); it('throws for integer overflows', () => { - expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'amount', type: 'uint8' }], ['256'])).to.throw(); + expect(() => + BaseContract.strictArgumentEncodingCheck([{ name: 'amount', type: 'uint8' }], ['256']), + ).to.throw(); }); it('throws for fixed byte array overflows', () => { - expect(() => BaseContract.strictArgumentEncodingCheck([{ name: 'hash', type: 'bytes8' }], ['0x001122334455667788'])).to.throw(); + expect(() => + BaseContract.strictArgumentEncodingCheck([{ name: 'hash', type: 'bytes8' }], ['0x001122334455667788']), + ).to.throw(); }); }); }); diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 16aa72afd..874e0b2da 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -21,10 +21,12 @@ function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; type if (param.components != null) { let suffix = ''; const arrayBracket = param.type.indexOf('['); - if (arrayBracket >= 0) { suffix = param.type.substring(arrayBracket); } + if (arrayBracket >= 0) { + suffix = param.type.substring(arrayBracket); + } const result = parseEthersParams(param.components); - names.push({ name: (param.name || null), names: result.names }); + names.push({ name: param.name || null, names: result.names }); types.push('tuple(' + result.types.join(',') + ')' + suffix); } else { names.push(param.name || null); @@ -74,7 +76,9 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo // one individually. const types = splitTupleTypes(type); if (types.length !== name.names.length) { - throw new Error(`Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`); + throw new Error( + `Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`, + ); } for (let i = 0; i < types.length; i++) { // For tuples, name is an object with a names property that is an @@ -89,7 +93,9 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo // ] // } // - const nestedName = _.isString(name.names[i]) ? name.names[i] as string : (name.names[i] as EthersNestedParamName).name as string; + const nestedName = _.isString(name.names[i]) + ? (name.names[i] as string) + : ((name.names[i] as EthersNestedParamName).name as string); if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { return false; } diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 852708eba..8b4cd47a2 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -3,8 +3,5 @@ "compilerOptions": { "outDir": "lib" }, - "include": [ - "src/**/*", - "test/**/*" - ] + "include": ["src/**/*", "test/**/*"] } -- cgit v1.2.3 From 52e094addcecc6136c9582a51dd52b8f44f769f7 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Mon, 6 Aug 2018 16:58:46 -0700 Subject: Move some ethers-related types to typescript-typings/ethers --- packages/base-contract/src/index.ts | 2 +- packages/typescript-typings/types/ethers/index.d.ts | 18 ++++++++++++++++++ packages/utils/src/abi_utils.ts | 18 ++++++------------ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/base-contract/src/index.ts b/packages/base-contract/src/index.ts index 31d2e4019..12f974445 100644 --- a/packages/base-contract/src/index.ts +++ b/packages/base-contract/src/index.ts @@ -87,7 +87,7 @@ export class BaseContract { // if it overflows the corresponding Solidity type, there is a bug in the // encoder, or the encoder performs unsafe type coercion. public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void { - const coder = (ethers as any).utils.AbiCoder.defaultCoder; + const coder = ethers.utils.AbiCoder.defaultCoder; const params = abiUtils.parseEthersParams(inputAbi); const rawEncoded = coder.encode(params.names, params.types, args); const rawDecoded = coder.decode(params.names, params.types, rawEncoded); diff --git a/packages/typescript-typings/types/ethers/index.d.ts b/packages/typescript-typings/types/ethers/index.d.ts index f869196e0..58bc1e8a9 100644 --- a/packages/typescript-typings/types/ethers/index.d.ts +++ b/packages/typescript-typings/types/ethers/index.d.ts @@ -34,4 +34,22 @@ declare module 'ethers' { const enum errors { INVALID_ARGUMENT = 'INVALID_ARGUMENT', } + + export type ParamName = null | string | NestedParamName; + + export interface NestedParamName { + name: string | null; + names: ParamName[]; + } + + export const utils: { + AbiCoder: { + defaultCoder: AbiCoder; + }; + }; + + export interface AbiCoder { + encode: (names?: ParamName[], types: string[], args: any[]) => string; + decode: (names?: ParamName[], types: string[], data: string) => any; + } } diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 874e0b2da..fc64a2a89 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -1,20 +1,14 @@ import { AbiDefinition, AbiType, ContractAbi, DataItem, MethodAbi } from 'ethereum-types'; +import * as ethers from 'ethers'; import * as _ from 'lodash'; import { BigNumber } from './configured_bignumber'; -export type EthersParamName = null | string | EthersNestedParamName; - -export interface EthersNestedParamName { - name: string | null; - names: EthersParamName[]; -} - // Note(albrow): This function is unexported in ethers.js. Copying it here for // now. // Source: https://github.com/ethers-io/ethers.js/blob/884593ab76004a808bf8097e9753fb5f8dcc3067/contracts/interface.js#L30 -function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; types: string[] } { - const names: EthersParamName[] = []; +function parseEthersParams(params: DataItem[]): { names: ethers.ParamName[]; types: string[] } { + const names: ethers.ParamName[] = []; const types: string[] = []; params.forEach((param: DataItem) => { @@ -43,7 +37,7 @@ function parseEthersParams(params: DataItem[]): { names: EthersParamName[]; type // returns true if x is equal to y and false otherwise. Performs some minimal // type conversion and data massaging for x and y, depending on type. name and // type should typically be derived from parseEthersParams. -function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): boolean { +function isAbiDataEqual(name: ethers.ParamName, type: string, x: any, y: any): boolean { if (_.isUndefined(x) && _.isUndefined(y)) { return true; } else if (_.isUndefined(x) && !_.isUndefined(y)) { @@ -70,7 +64,7 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo if (_.isString(name)) { throw new Error('Internal error: type was tuple but names was a string'); } else if (_.isNull(name)) { - throw new Error('Internal error: type was tuple but names was a null'); + throw new Error('Internal error: type was tuple but names was null'); } // For tuples, we iterate through the underlying values and check each // one individually. @@ -95,7 +89,7 @@ function isAbiDataEqual(name: EthersParamName, type: string, x: any, y: any): bo // const nestedName = _.isString(name.names[i]) ? (name.names[i] as string) - : ((name.names[i] as EthersNestedParamName).name as string); + : ((name.names[i] as ethers.NestedParamName).name as string); if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { return false; } -- cgit v1.2.3 From 09af23f950b6143cc3aec3c5843963e70d2a5b5c Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Tue, 7 Aug 2018 11:01:41 -0700 Subject: Update CHANGELOG.json for base-contract --- packages/base-contract/CHANGELOG.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/base-contract/CHANGELOG.json b/packages/base-contract/CHANGELOG.json index 9dddde18a..b041e13da 100644 --- a/packages/base-contract/CHANGELOG.json +++ b/packages/base-contract/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.0.0-rc.1", + "changes": [ + { + "pr": 915, + "note": "Added strict encoding/decoding checks for sendTransaction and call" + } + ] + }, { "timestamp": 1532619515, "version": "1.0.4", -- cgit v1.2.3 From 44b01f2069c10e84fc7fd55dc24767dfe877321c Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 14:52:05 -0700 Subject: Update ethers typings for TypeScript 2.9.2 --- packages/typescript-typings/types/ethers/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-typings/types/ethers/index.d.ts b/packages/typescript-typings/types/ethers/index.d.ts index 58bc1e8a9..875563ba2 100644 --- a/packages/typescript-typings/types/ethers/index.d.ts +++ b/packages/typescript-typings/types/ethers/index.d.ts @@ -49,7 +49,7 @@ declare module 'ethers' { }; export interface AbiCoder { - encode: (names?: ParamName[], types: string[], args: any[]) => string; - decode: (names?: ParamName[], types: string[], data: string) => any; + encode: (names: ParamName[] | string[], types: string[] | any[], args: any[] | undefined) => string; + decode: (names: ParamName[] | string[], types: string[] | string, data: string | undefined) => any; } } -- cgit v1.2.3 From 44d909c0c789a5b3fba2393746593d39c0ed1df7 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 14:54:35 -0700 Subject: Update CHANGELOG.json for contract-wrappers --- packages/contract-wrappers/CHANGELOG.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 02e1acf91..fb077ce73 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -2,6 +2,10 @@ { "version": "1.0.1-rc.3", "changes": [ + { + "pr": 915, + "note": "Added strict encoding/decoding checks for sendTransaction and call" + }, { "note": "Add ForwarderWrapper", "pr": 934 -- cgit v1.2.3 From 762bbe9bcd79ca5b832859cc20ffa4c83603cbcb Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 16:44:52 -0700 Subject: Update remaining CHANGELOG.json files --- packages/0x.js/CHANGELOG.json | 8 ++++++++ packages/fill-scenarios/CHANGELOG.json | 3 +++ packages/order-utils/CHANGELOG.json | 3 +++ packages/order-watcher/CHANGELOG.json | 8 ++++++++ 4 files changed, 22 insertions(+) diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index 4f39a9b8c..913b7a76e 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "1.0.1-rc.3", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ diff --git a/packages/fill-scenarios/CHANGELOG.json b/packages/fill-scenarios/CHANGELOG.json index 04e076203..1c3864da2 100644 --- a/packages/fill-scenarios/CHANGELOG.json +++ b/packages/fill-scenarios/CHANGELOG.json @@ -6,6 +6,9 @@ "note": "Updated to use latest orderFactory interface, fixed `feeRecipient` spelling error in public interface", "pr": 936 + }, + { + "note": "Dependencies updated" } ] }, diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 776bd67ec..fa82976ad 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -10,6 +10,9 @@ { "note": "Added marketUtils", "pr": 937 + }, + { + "note": "Dependencies updated" } ] }, diff --git a/packages/order-watcher/CHANGELOG.json b/packages/order-watcher/CHANGELOG.json index 08c1f7f58..0c9ef7a8a 100644 --- a/packages/order-watcher/CHANGELOG.json +++ b/packages/order-watcher/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "1.0.1-rc.3", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, { "version": "1.0.1-rc.2", "changes": [ -- cgit v1.2.3 From ca7d8a8940df23e56fb034878939112bc359cb2f Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 16:47:36 -0700 Subject: Update CI config and package.json to run @0xproject/utils tests on CI --- .circleci/config.yml | 1 + packages/utils/package.json | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b93f62fa7..43e542a86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,6 +80,7 @@ jobs: - run: yarn wsrun test:circleci @0xproject/sra-report - run: yarn wsrun test:circleci @0xproject/subproviders - run: yarn wsrun test:circleci @0xproject/web3-wrapper + - run: yarn wsrun test:circleci @0xproject/utils - save_cache: key: coverage-0xjs-{{ .Environment.CIRCLE_SHA1 }} paths: diff --git a/packages/utils/package.json b/packages/utils/package.json index 46c1d05d0..ee5fc264f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,7 +11,11 @@ "watch_without_deps": "tsc -w", "build": "tsc && copyfiles -u 2 './lib/monorepo_scripts/**/*' ./scripts", "clean": "shx rm -rf lib scripts", - "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", + "test": "yarn run_mocha", + "test:circleci": "yarn test:coverage", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 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", "lint": "tslint --project .", "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, -- cgit v1.2.3 From 5b7774f9d00a0f80601e6ff4ed0920c6a150a350 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 17:33:20 -0700 Subject: Add packages/coverage/.gitkeep file --- packages/utils/coverage/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/utils/coverage/.gitkeep diff --git a/packages/utils/coverage/.gitkeep b/packages/utils/coverage/.gitkeep new file mode 100644 index 000000000..e69de29bb -- cgit v1.2.3 From c4c37cafa0d8a77bfdc01b1cc111ba0101e86c8b Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Wed, 8 Aug 2018 17:58:04 -0700 Subject: Update comment about ethers checksummed address behavior --- packages/utils/src/abi_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index fc64a2a89..c9b70966c 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -96,9 +96,9 @@ function isAbiDataEqual(name: ethers.ParamName, type: string, x: any, y: any): b } return true; } else if (type === 'address' || type === 'bytes') { - // HACK(albrow): ethers.js sometimes changes the case of addresses/bytes - // when decoding/encoding. To account for that, we convert to lowercase - // before comparing. + // HACK(albrow): ethers.js returns the checksummed address even when + // initially passed in a non-checksummed address. To account for that, + // we convert to lowercase before comparing. return _.isEqual(_.toLower(x), _.toLower(y)); } else if (_.startsWith(type, 'uint') || _.startsWith(type, 'int')) { return new BigNumber(x).eq(new BigNumber(y)); -- cgit v1.2.3