diff --git a/packages/testcases/generation/package.json b/packages/testcases/generation/package.json new file mode 100644 index 00000000..f53fc453 --- /dev/null +++ b/packages/testcases/generation/package.json @@ -0,0 +1,18 @@ +{ + "name": "private-generation-scripts", + "private": true, + "version": "0.0.1", + "description": "Scripts to geenrate test cases.", + "main": "index.js", + "scripts": { + "auto-build": "npm run build -- -w", + "build": "tsc --build ./tsconfig.json" + }, + "devDependencies": { + "@types/node": "^14.11.8", + "eth-sig-util": "^2.5.3", + "typescript": "3.8.3" + }, + "author": "Richard Moore ", + "license": "MIT" +} diff --git a/packages/testcases/generation/src.ts/abi-test.ts b/packages/testcases/generation/src.ts/abi-test.ts new file mode 100644 index 00000000..beca7071 --- /dev/null +++ b/packages/testcases/generation/src.ts/abi-test.ts @@ -0,0 +1,144 @@ +import { AbstractTest } from "./test"; + +export interface AbiType { + name: string; + type: string; + + struct?: string; + components?: Array; + + create(): any; +} + +export abstract class AbstractAbiTest extends AbstractTest { + _nextNames: Record; + + constructor(name: string) { + super(name); + this._nextNames = { }; + } + + nextName(prefix?: string): string { + if (prefix == null) { prefix = "p"; } + if (!this._nextNames[prefix]) { this._nextNames[prefix] = 1; } + return prefix + (this._nextNames[prefix]++); + } + + randomType(dynamicOrType?: boolean | string): AbiType { + if (dynamicOrType == null) { dynamicOrType = true; } + + let type: number | string = null; + let dynamic = true; + if (typeof(dynamicOrType) === "boolean") { + dynamic = dynamicOrType; + type = this.randomInteger(0, dynamic ? 8: 6); + } else { + type = dynamicOrType; + } + + const name = this.nextName(); + + switch (type) { + + // Static + + // address + case 0: case "address": + return { name, type: "address", create: () => { + return this.randomAddress(); + } }; + + // bool + case 1: case "bool": + return { name, type: "bool", create: () => { + return this.randomChoice([ false, true ]); + } }; + + // intXX and uintXX + case 2: case "number": { + const signed = this.randomChoice([ false, true ]); + const width = this.randomInteger(1, 33); + return { name, type: `${ signed ? "": "u" }int${ width * 8 }`, create: () => { + const bytes = this.randomBytes(width); + let value = BigInt("0x" + bytes.toString("hex")); + if (signed && (bytes[0] & 0x80)) { + bytes[0] &= ~0x80; + value = -BigInt("0x" + bytes.toString("hex")); + } + return value.toString(); + } }; + } + + // bytesXX + case 3: case "bytesX": { + const width = this.randomInteger(1, 33); + return { name, type: `bytes${ width }`, create: () => { + return this.randomHexString(width); + } }; + } + + // Static or dynamic nested types + + // Array + case 4: case "array": { + const baseType = this.randomType(dynamic); + + let length = this.randomInteger(0, 4); + if (length == 0) { length = null; } + const lengthString = ((length == null) ? "": String(length)) + + let struct = undefined; + let components = undefined; + if (baseType.struct) { + struct = `${ baseType.struct }[${ lengthString }]`; + components = baseType.components; + } + + return { name, components, struct, type: `${ baseType.type }[${ lengthString }]`, create: () => { + let l = length; + if (l == null) { l = this.randomInteger(0, 4); } + + const result = [ ]; + for (let i = 0; i < l; i++) { + result.push(baseType.create()); + } + return result; + } }; + } + + // Tuple + case 5: case "tuple": { + const components: Array = [ ]; + const length = this.randomInteger(1, 5); + for (let i = 0; i < length; i++) { + components.push(this.randomType(dynamic)); + } + const struct = this.nextName("Struct"); + const type = `tuple(${ components.map(c => c.type).join(",") })` + return { name, struct, type, components, create: () => { + const result: Record = { }; + components.forEach((type) => { + result[type.name] = type.create(); + }); + return result; + } }; + } + + // Dynamic + + // string + case 6: case "string": + return { name, type: "string", create: () => { + return this.randomString(0, 64); + } }; + + // bytes + case 7: case "bytes": + return { name, type: "bytes", create: () => { + return this.randomHexString(0, 64); + } }; + } + + throw new Error("should not be reached"); + } +} diff --git a/packages/testcases/generation/src.ts/eip712-test.ts b/packages/testcases/generation/src.ts/eip712-test.ts new file mode 100644 index 00000000..88789cf8 --- /dev/null +++ b/packages/testcases/generation/src.ts/eip712-test.ts @@ -0,0 +1,139 @@ +import { TypedDataUtils } from "eth-sig-util"; + +import { AbstractAbiTest, AbiType } from "./abi-test"; +import { saveTests, TestCase } from "./test"; + +function fill(testcase: Partial): TestCase.Eip712 { + const domainType: Array<{ name: string, type:string }> = []; + + if (testcase.domain.name != null) { + domainType.push({ name: "name", type: "string" }); + } + if (testcase.domain.version != null) { + domainType.push({ name: "version", type: "string" }); + } + if (testcase.domain.chainId != null) { + domainType.push({ name: "chainId", type: "uint256" }); + } + if (testcase.domain.verifyingContract != null) { + domainType.push({ name: "verifyingContract", type: "address" }); + } + if (testcase.domain.salt != null) { + domainType.push({ name: "salt", type: "bytes32" }); + } + + const typesWithDomain: Record> = { + EIP712Domain: domainType + }; + for (const key in testcase.types) { typesWithDomain[key] = testcase.types[key]; } + + + testcase.encoded = "0x" + TypedDataUtils.encodeData(testcase.primaryType, testcase.data, testcase.types).toString("hex"); + testcase.digest = "0x" + TypedDataUtils.sign({ + types: typesWithDomain, + domain: testcase.domain, + primaryType: testcase.primaryType, + message: testcase.data + }, true).toString("hex"); + + return testcase; +} + +export class Eip712Test extends AbstractAbiTest { + generateTest(): TestCase.Eip712 { + const type = this.randomType("tuple"); + + const types: Record> = { }; + function spelunk(type: AbiType): void { + if (type.struct) { + types[type.struct.split("[")[0]] = type.components.map((t) => { + spelunk(t); + return { name: t.name, type: (t.struct || t.type) }; + });; + } + } + spelunk(type); + + const primaryType = type.struct; + const data = type.create(); + + const domain: any = { }; + if (this.randomChoice([ false, true])) { + domain.name = this.randomString(1, 64); + } + if (this.randomChoice([ false, true])) { + domain.version = [ + this.randomInteger(0, 50), + this.randomInteger(0, 50), + this.randomInteger(0, 50), + ].join("."); + } + if (this.randomChoice([ false, true])) { + domain.chainId = this.randomInteger(0, 1337); + } + if (this.randomChoice([ false, true])) { + domain.verifyingContract = this.randomAddress(); + } + if (this.randomChoice([ false, true])) { + domain.salt = this.randomHexString(32); + } + + return fill({ + domain, + type: type.type, + seed: this.seed, + primaryType, types, data + }); + } +} + +if (require.main === module) { + const tests: Array = [ ]; + + for (let i = 0; i < 1024; i++) { + const test = new Eip712Test(String(i)); + tests.push(test.generateTest()); + } + + tests.sort((a, b) => (a.type.length - b.type.length)); + tests.forEach((t, i) => { t.name = `random-${ i }`; }); + + tests.push({ + name: "EIP712 example", + domain: { + name: 'Ether Mail', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' + }, + primaryType: "Mail", + types: { + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' } + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' } + ] + }, + data: { + from: { + name: 'Cow', + wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' + }, + to: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' + }, + contents: 'Hello, Bob!' + }, + encoded: "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8", + digest: "0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2", + privateKey: "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", + signature: "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" + }); + + saveTests("eip712", tests); +} diff --git a/packages/testcases/generation/src.ts/test.ts b/packages/testcases/generation/src.ts/test.ts new file mode 100644 index 00000000..9039de5d --- /dev/null +++ b/packages/testcases/generation/src.ts/test.ts @@ -0,0 +1,91 @@ +import { createHash } from "crypto"; +import { saveTests as _saveTests } from "../../lib/index"; +import { ethers } from "../../../ethers"; + +import * as TestCase from "../../lib/testcases"; +export { TestCase }; + +function sha256(value: Buffer): Buffer { + return createHash("sha256").update(value).digest(); +} + +const words = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum".split(" "); + +export abstract class AbstractTest { + readonly seed: string; + _seed: Buffer; + + constructor(seed: string) { + this.seed = seed; + this._seed = sha256(Buffer.from(seed)); + } + + _nextWord(): Buffer { + const result = this._seed; + this._seed = sha256(this._seed); + return result; + } + + randomBytes(lower: number, upper?: number): Buffer { + if (!upper) { upper = lower; } + + if (upper === 0 && upper === lower) { return Buffer.alloc(0); } + + let result = this._nextWord(); + while (result.length < upper) { + result = Buffer.concat([ result, this._nextWord() ]); + } + + const top = this._nextWord(); + const percent = ((top[0] << 16) | (top[1] << 8) | top[2]) / 0x01000000; + + return result.slice(0, lower + Math.floor((upper - lower) * percent)); + } + + randomFloat(): number { + const top = this._nextWord(); + return ((top[0] << 16) | (top[1] << 8) | top[2]) / 0x01000000; + } + + randomInteger(lower: number, upper: number): number { + return lower + Math.floor((upper - lower) * this.randomFloat()); + } + + randomChoice(choice: Array): T { + return choice[this.randomInteger(0, choice.length)]; + } + + randomAddress(): string { + while (true) { + const address = this.randomHexString(20); + if (address.match(/[a-f]/i)) { + return ethers.utils.getAddress(address); + } + } + } + + randomHexString(lower: number, upper?: number): string { + return "0x" + this.randomBytes(lower, upper).toString("hex"); + } + + randomString(lower: number, upper?: number): string { + if (!upper) { upper = lower; } + if (upper === 0 && upper === lower) { return ""; } + + const length = this.randomInteger(lower, upper); + + let result = ""; + while (result.length < length + 1) { + result += this.randomChoice(words) + " "; + } + + return result.substring(0, length); + } + + abstract generateTest(): T; +} + +export function saveTests(tag: string, tests: Array): void { + // @TODO : copy defn files over for testcase.ts and testcase.d.ts + _saveTests(tag, tests); +} diff --git a/packages/testcases/generation/thirdparty.d.ts b/packages/testcases/generation/thirdparty.d.ts new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/testcases/generation/thirdparty.d.ts @@ -0,0 +1 @@ + diff --git a/packages/testcases/generation/tsconfig.json b/packages/testcases/generation/tsconfig.json new file mode 100644 index 00000000..88551f9c --- /dev/null +++ b/packages/testcases/generation/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "lib": ["es2015", "es5", "dom"], + "module": "commonjs", + "target": "es2015", + "moduleResolution": "node", + + "rootDir": "./src.ts", + "outDir": "./lib/", + + "declaration": true, + + "preserveSymlinks": true, + + "preserveWatchOutput": true, + "pretty": false, + + "forceConsistentCasingInFileNames": true, + + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true + }, + "include": [ + "./thirdparty.d.ts", + "./src.ts/*.ts" + ], + "exclude": [ ] +}