diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 535e4b7..22afb98 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node: ['10.x', '12.x', '14.x'] + node: ['12.x', '14.x'] os: [ubuntu-latest, windows-latest, macOS-latest] steps: diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..297345c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn format && yarn lint diff --git a/package.json b/package.json index 6bebaaa..399bbb1 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,10 @@ "build": "tsdx build", "tsdx-test": "tsdx test", "lint": "tsdx lint", - "prepare": "tsdx build", + "prepare": "husky install && tsdx build", "size": "size-limit", - "analyze": "size-limit --why" + "analyze": "size-limit --why", + "format": "prettier --write \"./src/*.{js,ts}\" \"tests/*.{js,ts}\"" }, "repository": { "type": "git", @@ -31,16 +32,12 @@ "url": "https://github.com/weiroll/weiroll.js/issues" }, "peerDependencies": {}, - "husky": { - "hooks": { - "pre-commit": "tsdx lint" - } - }, "prettier": { "printWidth": 80, "semi": true, "singleQuote": true, - "trailingComma": "es5" + "trailingComma": "es5", + "arrowParens": "always" }, "size-limit": [ { @@ -59,8 +56,10 @@ "@types/mocha": "^8.2.2", "@types/node": "^15.12.2", "chai": "^4.3.4", + "eslint-plugin-prettier": "^3.4.0", "husky": "^6.0.0", "mocha": "^9.0.0", + "prettier": "^2.3.1", "size-limit": "^4.12.0", "ts-loader": "^9.2.3", "ts-node": "^10.0.0", diff --git a/src/index.ts b/src/index.ts index 911f18e..9a77521 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,9 @@ -"use strict"; - export { - Contract, - ContractFunction, - FunctionCall, - Value, - LiteralValue, - ReturnValue, - Planner -} from './planner'; \ No newline at end of file + Contract, + ContractFunction, + FunctionCall, + Value, + LiteralValue, + ReturnValue, + Planner, +} from './planner'; diff --git a/src/planner.ts b/src/planner.ts index c1ea09e..f068280 100644 --- a/src/planner.ts +++ b/src/planner.ts @@ -1,321 +1,348 @@ -"use strict"; - -import { Contract as EthersContract, ContractInterface } from '@ethersproject/contracts'; -import { Interface, FunctionFragment, ParamType, defaultAbiCoder } from '@ethersproject/abi'; +import { ContractInterface } from '@ethersproject/contracts'; +import type { Contract as EthersContract } from '@ethersproject/contracts'; +import { Interface, ParamType, defaultAbiCoder } from '@ethersproject/abi'; +import type { FunctionFragment } from '@ethersproject/abi'; import { defineReadOnly, getStatic } from '@ethersproject/properties'; import { hexConcat, hexDataSlice, hexlify } from '@ethersproject/bytes'; import { Heap } from 'heap-js'; export interface Value { - readonly param: ParamType; + readonly param: ParamType; } -function isValue(arg:any): arg is Value { - return (arg as Value).param !== undefined; +function isValue(arg: any): arg is Value { + return (arg as Value).param !== undefined; } export class LiteralValue implements Value { - readonly param: ParamType; - readonly value: string; + readonly param: ParamType; + readonly value: string; - constructor(param: ParamType, value: string) { - this.param = param; - this.value = value; - } + constructor(param: ParamType, value: string) { + this.param = param; + this.value = value; + } } export class ReturnValue implements Value { - readonly param: ParamType; - readonly planner: Planner; - readonly commandIndex: number; // Index of the command in the array of planned commands - - constructor(param: ParamType, planner: Planner, commandIndex: number) { - this.param = param; - this.planner = planner; - this.commandIndex = commandIndex; - } + readonly param: ParamType; + readonly planner: Planner; + readonly commandIndex: number; // Index of the command in the array of planned commands + + constructor(param: ParamType, planner: Planner, commandIndex: number) { + this.param = param; + this.planner = planner; + this.commandIndex = commandIndex; + } } export class StateValue implements Value { - readonly param: ParamType; + readonly param: ParamType; - constructor() { - this.param = ParamType.from('bytes[]'); - } + constructor() { + this.param = ParamType.from('bytes[]'); + } } export interface FunctionCall { - readonly contract: Contract; - readonly fragment: FunctionFragment; - readonly args: Value[]; + readonly contract: Contract; + readonly fragment: FunctionFragment; + readonly args: Value[]; } export type ContractFunction = (...args: Array) => FunctionCall; export function isDynamicType(param?: ParamType): boolean { - if (typeof param === "undefined") return false + if (typeof param === 'undefined') return false; - return ["string", "bytes", "array", "tuple"].includes(param.baseType); + return ['string', 'bytes', 'array', 'tuple'].includes(param.baseType); } function abiEncodeSingle(param: ParamType, value: any): LiteralValue { - if(isDynamicType(param)) { - return new LiteralValue(param, hexDataSlice(defaultAbiCoder.encode([param], [value]), 32)); - } - return new LiteralValue(param, defaultAbiCoder.encode([param], [value])); + if (isDynamicType(param)) { + return new LiteralValue( + param, + hexDataSlice(defaultAbiCoder.encode([param], [value]), 32) + ); + } + return new LiteralValue(param, defaultAbiCoder.encode([param], [value])); } -function buildCall(contract: Contract, fragment: FunctionFragment): ContractFunction { - return function(...args: Array): FunctionCall { - if(args.length != fragment.inputs.length) { - throw new Error(`Function ${fragment.name} has ${fragment.inputs.length} arguments but ${args.length} provided`); - } - const encodedArgs = args.map((arg, idx) => { - const param = fragment.inputs[idx]; - if(isValue(arg)) { - if(arg.param.type != param.type) { - // Todo: type casting rules - throw new Error(`Cannot pass value of type ${arg.param.type} to input of type ${param.type}`); - } - return arg; - } else { - return abiEncodeSingle(param, arg); - } - }); - return {contract, fragment, args: encodedArgs}; +function buildCall( + contract: Contract, + fragment: FunctionFragment +): ContractFunction { + return function call(...args: Array): FunctionCall { + if (args.length !== fragment.inputs.length) { + throw new Error( + `Function ${fragment.name} has ${fragment.inputs.length} arguments but ${args.length} provided` + ); } + + const encodedArgs = args.map((arg, idx) => { + const param = fragment.inputs[idx]; + if (isValue(arg)) { + if (arg.param.type !== param.type) { + // Todo: type casting rules + throw new Error( + `Cannot pass value of type ${arg.param.type} to input of type ${param.type}` + ); + } + return arg; + } else { + return abiEncodeSingle(param, arg); + } + }); + + return { contract, fragment, args: encodedArgs }; + }; } class BaseContract { - readonly address: string; - readonly interface: Interface; - readonly functions: { [ name: string ]: ContractFunction }; - - constructor(address: string, contractInterface: ContractInterface) { - this.interface = getStatic<(contractInterface: ContractInterface) => Interface>(new.target, "getInterface")(contractInterface); - this.address = address; - this.functions = {}; - - const uniqueNames: { [ name: string ]: Array } = { }; - const uniqueSignatures: { [ signature: string ]: boolean } = { }; - Object.keys(this.interface.functions).forEach((signature) => { - const fragment = this.interface.functions[signature]; - - // Check that the signature is unique; if not the ABI generation has - // not been cleaned or may be incorrectly generated - if (uniqueSignatures[signature]) { - throw new Error(`Duplicate ABI entry for ${ JSON.stringify(signature) }`); - } - uniqueSignatures[signature] = true; - - // Track unique names; we only expose bare named functions if they - // are ambiguous - { - const name = fragment.name; - if (!uniqueNames[name]) { uniqueNames[name] = [ ]; } - uniqueNames[name].push(signature); - } - - if ((this)[signature] == null) { - defineReadOnly(this, signature, buildCall(this, fragment)); - } - - // We do not collapse simple calls on this bucket, which allows - // frameworks to safely use this without introspection as well as - // allows decoding error recovery. - if (this.functions[signature] == null) { - defineReadOnly(this.functions, signature, buildCall(this, fragment)); - } - }); - - Object.keys(uniqueNames).forEach((name) => { - - // Ambiguous names to not get attached as bare names - const signatures = uniqueNames[name]; - if (signatures.length > 1) { return; } - - const signature = signatures[0]; - - // If overwriting a member property that is null, swallow the error - try { - if ((this)[name] == null) { - defineReadOnly(this, name, (this)[signature]); - } - } catch (e) { } - - if (this.functions[name] == null) { - defineReadOnly(this.functions, name, this.functions[signature]); - } - }); - } + readonly address: string; + readonly interface: Interface; + readonly functions: { [name: string]: ContractFunction }; + + constructor(address: string, contractInterface: ContractInterface) { + this.interface = getStatic< + (contractInterface: ContractInterface) => Interface + >( + new.target, + 'getInterface' + )(contractInterface); + this.address = address; + this.functions = {}; + + const uniqueNames: { [name: string]: Array } = {}; + const uniqueSignatures: { [signature: string]: boolean } = {}; + Object.keys(this.interface.functions).forEach((signature) => { + const fragment = this.interface.functions[signature]; + + // Check that the signature is unique; if not the ABI generation has + // not been cleaned or may be incorrectly generated + if (uniqueSignatures[signature]) { + throw new Error(`Duplicate ABI entry for ${JSON.stringify(signature)}`); + } + uniqueSignatures[signature] = true; + + // Track unique names; we only expose bare named functions if they + // are ambiguous + { + const name = fragment.name; + if (!uniqueNames[name]) { + uniqueNames[name] = []; + } + uniqueNames[name].push(signature); + } + + if ((this as Contract)[signature] == null) { + defineReadOnly(this, signature, buildCall(this, fragment)); + } + + // We do not collapse simple calls on this bucket, which allows + // frameworks to safely use this without introspection as well as + // allows decoding error recovery. + if (this.functions[signature] == null) { + defineReadOnly(this.functions, signature, buildCall(this, fragment)); + } + }); + + Object.keys(uniqueNames).forEach((name) => { + // Ambiguous names to not get attached as bare names + const signatures = uniqueNames[name]; + if (signatures.length > 1) { + return; + } + + const signature = signatures[0]; + + // If overwriting a member property that is null, swallow the error + try { + if ((this as Contract)[name] == null) { + defineReadOnly(this as Contract, name, (this as Contract)[signature]); + } + } catch (e) {} - static fromEthersContract(contract: EthersContract): Contract { - return new Contract(contract.address, contract.interface); - } + if (this.functions[name] == null) { + defineReadOnly(this.functions, name, this.functions[signature]); + } + }); + } - static getInterface(contractInterface: ContractInterface): Interface { - if (Interface.isInterface(contractInterface)) { - return contractInterface; - } - return new Interface(contractInterface); + static fromEthersContract(contract: EthersContract): Contract { + return new Contract(contract.address, contract.interface); + } + + static getInterface(contractInterface: ContractInterface): Interface { + if (Interface.isInterface(contractInterface)) { + return contractInterface; } + return new Interface(contractInterface); + } } export class Contract extends BaseContract { - // The meta-class properties - readonly [ key: string ]: ContractFunction | any; + // The meta-class properties + readonly [key: string]: ContractFunction | any; } export class Planner { - readonly state: StateValue; - calls: {call: FunctionCall, replacesState: boolean}[]; - - constructor() { - this.state = new StateValue(); - this.calls = []; + readonly state: StateValue; + calls: { call: FunctionCall; replacesState: boolean }[]; + + constructor() { + this.state = new StateValue(); + this.calls = []; + } + + add(call: FunctionCall): ReturnValue | null { + for (let arg of call.args) { + if (arg instanceof ReturnValue) { + if (arg.planner !== this) { + throw new Error('Cannot reuse return values across planners'); + } + } } - add(call: FunctionCall): ReturnValue | null { - for(let arg of call.args) { - if(arg instanceof ReturnValue) { - if(arg.planner != this) { - throw new Error("Cannot reuse return values across planners"); - } - } - } + const commandIndex = this.calls.length; + this.calls.push({ call, replacesState: false }); - const commandIndex = this.calls.length; - this.calls.push({call, replacesState: false}); - - if(call.fragment.outputs?.length != 1) { - return null; + if (call.fragment.outputs?.length !== 1) { + return null; + } + return new ReturnValue(call.fragment.outputs[0], this, commandIndex); + } + + replaceState(call: FunctionCall) { + for (let arg of call.args) { + if (arg instanceof ReturnValue) { + if (arg.planner !== this) { + throw new Error('Cannot reuse return values across planners'); } - return new ReturnValue(call.fragment.outputs[0], this, commandIndex); + } + } + + if ( + call.fragment.outputs?.length !== 1 || + call.fragment.outputs[0].type !== 'bytes[]' + ) { + throw new Error('Function replacing state must return a bytes[]'); } - replaceState(call: FunctionCall) { - for(let arg of call.args) { - if(arg instanceof ReturnValue) { - if(arg.planner != this) { - throw new Error("Cannot reuse return values across planners"); - } - } + this.calls.push({ call, replacesState: true }); + } + + plan(): { commands: string[]; state: string[] } { + // Tracks the last time a literal is used in the program + let literalVisibility = new Map(); + // Tracks the last time a command's output is used in the program + let commandVisibility: number[] = Array(this.calls.length).fill(-1); + + // Build visibility maps + for (let i = 0; i < this.calls.length; i++) { + const { call } = this.calls[i]; + for (let arg of call.args) { + if (arg instanceof ReturnValue) { + commandVisibility[arg.commandIndex] = i; + } else if (arg instanceof LiteralValue) { + literalVisibility.set(arg.value, i); + } else if (!(arg instanceof StateValue)) { + throw new Error(`Unknown function argument type '${typeof arg}'`); } + } + } - if(call.fragment.outputs?.length != 1 || call.fragment.outputs[0].type != 'bytes[]') { - throw new Error("Function replacing state must return a bytes[]"); + // Tracks when state slots go out of scope + type HeapEntry = { slot: number; dies: number }; + let nextDeadSlot = new Heap((a, b) => a.dies - b.dies); + + // Tracks the state slot each literal is stored in + let literalSlotMap = new Map(); + // Tracks the state slot each return value is stored in + let returnSlotMap = Array(this.calls.length); + + let commands: string[] = []; + let state: string[] = []; + + // Prepopulate the state with literals + literalVisibility.forEach((dies, literal) => { + const slot = state.length; + literalSlotMap.set(literal, slot); + nextDeadSlot.push({ slot, dies }); + state.push(literal); + }); + + // Build commands, and add state entries as needed + for (let i = 0; i < this.calls.length; i++) { + const { call, replacesState } = this.calls[i]; + + // Build a list of argument value indexes + const args = new Uint8Array(7).fill(0xff); + call.args.forEach((arg, j) => { + let slot; + if (arg instanceof ReturnValue) { + slot = returnSlotMap[arg.commandIndex]; + } else if (arg instanceof LiteralValue) { + slot = literalSlotMap.get(arg.value); + } else if (arg instanceof StateValue) { + slot = 0xfe; + } else { + throw new Error(`Unknown function argument type '${typeof arg}'`); + } + if (isDynamicType(arg.param)) { + slot |= 0x80; + } + args[j] = slot; + }); + + // Figure out where to put the return value + let ret = 0xff; + if (commandVisibility[i] !== -1) { + if (replacesState) { + throw new Error( + `Return value of ${call.fragment.name} cannot be used to replace state and in another function` + ); } + ret = state.length; - this.calls.push({call, replacesState: true}); - } + const topNode = nextDeadSlot.peek(); + + // Is there a spare state slot? + if (typeof topNode !== 'undefined' && topNode.dies <= i) { + const extractedTopNode = nextDeadSlot.pop(); - plan(): {commands: string[], state: string[]} { - // Tracks the last time a literal is used in the program - let literalVisibility = new Map(); - // Tracks the last time a command's output is used in the program - let commandVisibility:number[] = Array(this.calls.length).fill(-1); - - // Build visibility maps - for(let i = 0; i < this.calls.length; i++) { - const {call} = this.calls[i]; - for(let arg of call.args) { - if(arg instanceof ReturnValue) { - commandVisibility[arg.commandIndex] = i; - } else if(arg instanceof LiteralValue) { - literalVisibility.set(arg.value, i); - } else if(!(arg instanceof StateValue)) { - throw new Error(`Unknown function argument type '${typeof arg}'`); - } - } + if (extractedTopNode) { + ret = extractedTopNode?.slot; + } } - // Tracks when state slots go out of scope - type HeapEntry = {slot: number, dies: number}; - let nextDeadSlot = new Heap((a, b) => a.dies - b.dies); - - // Tracks the state slot each literal is stored in - let literalSlotMap = new Map(); - // Tracks the state slot each return value is stored in - let returnSlotMap = Array(this.calls.length); - - let commands: string[] = []; - let state: string[] = []; - - // Prepopulate the state with literals - literalVisibility.forEach((dies, literal) => { - const slot = state.length; - literalSlotMap.set(literal, slot); - nextDeadSlot.push({slot, dies}); - state.push(literal); - }); - - // Build commands, and add state entries as needed - for(let i = 0; i < this.calls.length; i++) { - const {call, replacesState} = this.calls[i]; - - // Build a list of argument value indexes - const args = new Uint8Array(7).fill(0xff); - call.args.forEach((arg, j) => { - let slot; - if(arg instanceof ReturnValue) { - slot = returnSlotMap[arg.commandIndex]; - } else if(arg instanceof LiteralValue) { - slot = literalSlotMap.get(arg.value); - } else if(arg instanceof StateValue) { - slot = 0xfe; - } else { - throw new Error(`Unknown function argument type '${typeof arg}'`); - } - if(isDynamicType(arg.param)) { - slot |= 0x80; - } - args[j] = slot; - }); - - // Figure out where to put the return value - let ret = 0xff; - if(commandVisibility[i] != -1) { - if(replacesState) { - throw new Error(`Return value of ${call.fragment.name} cannot be used to replace state and in another function`); - } - ret = state.length; - - const topNode = nextDeadSlot.peek(); - - // Is there a spare state slot? - if(typeof topNode !== "undefined" && topNode.dies <= i) { - const extractedTopNode = nextDeadSlot.pop(); - - if (extractedTopNode) { - ret = extractedTopNode?.slot; - } - } - - // Store the slot mapping - returnSlotMap[i] = ret; - - // Make the slot available when it's not needed - nextDeadSlot.push({slot: ret, dies: commandVisibility[i]}); - - if(ret == state.length) { - state.push('0x'); - } - - if(isDynamicType(call.fragment.outputs?.[0])) { - ret |= 0x80; - } - } else if(replacesState) { - ret = 0xfe; - } - - commands.push(hexConcat([ - call.contract.interface.getSighash(call.fragment), - hexlify(args), - hexlify([ret]), - call.contract.address - ])); + // Store the slot mapping + returnSlotMap[i] = ret; + + // Make the slot available when it's not needed + nextDeadSlot.push({ slot: ret, dies: commandVisibility[i] }); + + if (ret === state.length) { + state.push('0x'); } - return {commands, state}; + if (isDynamicType(call.fragment.outputs?.[0])) { + ret |= 0x80; + } + } else if (replacesState) { + ret = 0xfe; + } + + commands.push( + hexConcat([ + call.contract.interface.getSighash(call.fragment), + hexlify(args), + hexlify([ret]), + call.contract.address, + ]) + ); } -} \ No newline at end of file + + return { commands, state }; + } +} diff --git a/tests/test_planner.ts b/tests/test_planner.ts index 4d856b9..9b03579 100644 --- a/tests/test_planner.ts +++ b/tests/test_planner.ts @@ -11,161 +11,199 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const SAMPLE_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; describe('Contract', () => { - let Math: Contract; - - before(() => { - Math = Contract.fromEthersContract(new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi)); - }); - - it('wraps contract objects and exposes their functions', () => { - expect(Math.add).to.not.be.undefined; - }); - - it('returns a FunctionCall when contract functions are called', () => { - const result = Math.add(1, 2); - - expect(result.contract).to.equal(Math); - expect(result.fragment).to.equal(Math.interface.getFunction('add')); - - const args = result.args; - expect(args.length).to.equal(2); - expect(args[0].param).to.equal(Math.interface.getFunction('add').inputs[0]); - expect(args[0].value).to.equal(defaultAbiCoder.encode(['uint'], [1])); - expect(args[1].param).to.equal(Math.interface.getFunction('add').inputs[1]); - expect(args[1].value).to.equal(defaultAbiCoder.encode(['uint'], [2])); - }); + let Math: Contract; + + before(() => { + Math = Contract.fromEthersContract( + new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi) + ); + }); + + it('wraps contract objects and exposes their functions', () => { + expect(Math.add).to.not.be.undefined; + }); + + it('returns a FunctionCall when contract functions are called', () => { + const result = Math.add(1, 2); + + expect(result.contract).to.equal(Math); + expect(result.fragment).to.equal(Math.interface.getFunction('add')); + + const args = result.args; + expect(args.length).to.equal(2); + expect(args[0].param).to.equal(Math.interface.getFunction('add').inputs[0]); + expect(args[0].value).to.equal(defaultAbiCoder.encode(['uint'], [1])); + expect(args[1].param).to.equal(Math.interface.getFunction('add').inputs[1]); + expect(args[1].value).to.equal(defaultAbiCoder.encode(['uint'], [2])); + }); }); describe('Planner', () => { - let Math: Contract; - let Strings: Contract; - - before(() => { - Math = Contract.fromEthersContract(new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi)); - Strings = Contract.fromEthersContract(new ethers.Contract(SAMPLE_ADDRESS, stringsABI.abi)); - }); - - it('adds function calls to a list of commands', () => { - const planner = new Planner(); - const sum1 = planner.add(Math.add(1, 2)); - const sum2 = planner.add(Math.add(3, 4)); - const sum3 = planner.add(Math.add(sum1, sum2)); - - expect(planner.calls.length).to.equal(3); - expect(sum1.commandIndex).to.equal(0); - expect(sum2.commandIndex).to.equal(1); - expect(sum3.commandIndex).to.equal(2); - }); - - it('plans a simple program', () => { - const planner = new Planner(); - planner.add(Math.add(1, 2)); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(1); - expect(commands[0]).to.equal("0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(2); - expect(state[0]).to.equal(defaultAbiCoder.encode(['uint'], [1])); - expect(state[1]).to.equal(defaultAbiCoder.encode(['uint'], [2])); - }); - - it('deduplicates identical literals', () => { - const planner = new Planner(); - const sum1 = planner.add(Math.add(1, 1)); - const {commands, state} = planner.plan(); - - expect(state.length).to.equal(1); - }) - - it('plans a program that uses return values', () => { - const planner = new Planner(); - const sum1 = planner.add(Math.add(1, 2)); - planner.add(Math.add(sum1, 3)); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(2); - expect(commands[0]).to.equal("0x771602f70001ffffffffff00eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - expect(commands[1]).to.equal("0x771602f70002ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(3); - expect(state[0]).to.equal(defaultAbiCoder.encode(['uint'], [1])); - expect(state[1]).to.equal(defaultAbiCoder.encode(['uint'], [2])); - expect(state[2]).to.equal(defaultAbiCoder.encode(['uint'], [3])); - }); - - it('plans a program that needs extra state slots for intermediate values', () => { - const planner = new Planner(); - const sum1 = planner.add(Math.add(1, 1)); - planner.add(Math.add(1, sum1)); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(2); - expect(commands[0]).to.equal("0x771602f70000ffffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - expect(commands[1]).to.equal("0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(2); - expect(state[0]).to.equal(defaultAbiCoder.encode(['uint'], [1])); - expect(state[1]).to.equal('0x'); - }); - - it('plans a program that takes dynamic arguments', () => { - const planner = new Planner(); - planner.add(Strings.strlen("Hello, world!")); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(1); - expect(commands[0]).to.equal("0x367bbd7880ffffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(1); - expect(state[0]).to.equal(hexDataSlice(defaultAbiCoder.encode(['string'], ["Hello, world!"]), 32)); - }); - - it('plans a program that returns dynamic arguments', () => { - const planner = new Planner(); - planner.add(Strings.strcat("Hello, ", "world!")); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(1); - expect(commands[0]).to.equal("0xd824ccf38081ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(2); - expect(state[0]).to.equal(hexDataSlice(defaultAbiCoder.encode(['string'], ["Hello, "]), 32)); - expect(state[1]).to.equal(hexDataSlice(defaultAbiCoder.encode(['string'], ["world!"]), 32)); - }); - - it('plans a program that takes a dynamic argument from a return value', () => { - const planner = new Planner(); - const str = planner.add(Strings.strcat("Hello, ", "world!")); - planner.add(Strings.strlen(str)); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(2); - expect(commands[0]).to.equal("0xd824ccf38081ffffffffff80eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - expect(commands[1]).to.equal("0x367bbd7880ffffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(2); - expect(state[0]).to.equal(hexDataSlice(defaultAbiCoder.encode(['string'], ["Hello, "]), 32)); - expect(state[1]).to.equal(hexDataSlice(defaultAbiCoder.encode(['string'], ["world!"]), 32)); - }); - - it('requires argument counts to match the function definition', () => { - const planner = new Planner(); - expect(() => planner.add(Math.add(1))).to.throw(); - }); - - it('plans a call to a function that takes and replaces the current state', () => { - const TestContract = Contract.fromEthersContract(new ethers.Contract(SAMPLE_ADDRESS, [ - "function useState(bytes[] state) returns(bytes[])" - ])); - - const planner = new Planner(); - planner.replaceState(TestContract.useState(planner.state)); - const {commands, state} = planner.plan(); - - expect(commands.length).to.equal(1); - expect(commands[0]).to.equal("0x08f389c8fefffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - - expect(state.length).to.equal(0); - }); + let Math: Contract; + let Strings: Contract; + + before(() => { + Math = Contract.fromEthersContract( + new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi) + ); + Strings = Contract.fromEthersContract( + new ethers.Contract(SAMPLE_ADDRESS, stringsABI.abi) + ); + }); + + it('adds function calls to a list of commands', () => { + const planner = new Planner(); + const sum1 = planner.add(Math.add(1, 2)); + const sum2 = planner.add(Math.add(3, 4)); + const sum3 = planner.add(Math.add(sum1, sum2)); + + expect(planner.calls.length).to.equal(3); + expect(sum1.commandIndex).to.equal(0); + expect(sum2.commandIndex).to.equal(1); + expect(sum3.commandIndex).to.equal(2); + }); + + it('plans a simple program', () => { + const planner = new Planner(); + planner.add(Math.add(1, 2)); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(2); + expect(state[0]).to.equal(defaultAbiCoder.encode(['uint'], [1])); + expect(state[1]).to.equal(defaultAbiCoder.encode(['uint'], [2])); + }); + + it('deduplicates identical literals', () => { + const planner = new Planner(); + const sum1 = planner.add(Math.add(1, 1)); + const { commands, state } = planner.plan(); + + expect(state.length).to.equal(1); + }); + + it('plans a program that uses return values', () => { + const planner = new Planner(); + const sum1 = planner.add(Math.add(1, 2)); + planner.add(Math.add(sum1, 3)); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(2); + expect(commands[0]).to.equal( + '0x771602f70001ffffffffff00eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + expect(commands[1]).to.equal( + '0x771602f70002ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(3); + expect(state[0]).to.equal(defaultAbiCoder.encode(['uint'], [1])); + expect(state[1]).to.equal(defaultAbiCoder.encode(['uint'], [2])); + expect(state[2]).to.equal(defaultAbiCoder.encode(['uint'], [3])); + }); + + it('plans a program that needs extra state slots for intermediate values', () => { + const planner = new Planner(); + const sum1 = planner.add(Math.add(1, 1)); + planner.add(Math.add(1, sum1)); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(2); + expect(commands[0]).to.equal( + '0x771602f70000ffffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + expect(commands[1]).to.equal( + '0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(2); + expect(state[0]).to.equal(defaultAbiCoder.encode(['uint'], [1])); + expect(state[1]).to.equal('0x'); + }); + + it('plans a program that takes dynamic arguments', () => { + const planner = new Planner(); + planner.add(Strings.strlen('Hello, world!')); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0x367bbd7880ffffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(1); + expect(state[0]).to.equal( + hexDataSlice(defaultAbiCoder.encode(['string'], ['Hello, world!']), 32) + ); + }); + + it('plans a program that returns dynamic arguments', () => { + const planner = new Planner(); + planner.add(Strings.strcat('Hello, ', 'world!')); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0xd824ccf38081ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(2); + expect(state[0]).to.equal( + hexDataSlice(defaultAbiCoder.encode(['string'], ['Hello, ']), 32) + ); + expect(state[1]).to.equal( + hexDataSlice(defaultAbiCoder.encode(['string'], ['world!']), 32) + ); + }); + + it('plans a program that takes a dynamic argument from a return value', () => { + const planner = new Planner(); + const str = planner.add(Strings.strcat('Hello, ', 'world!')); + planner.add(Strings.strlen(str)); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(2); + expect(commands[0]).to.equal( + '0xd824ccf38081ffffffffff80eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + expect(commands[1]).to.equal( + '0x367bbd7880ffffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(2); + expect(state[0]).to.equal( + hexDataSlice(defaultAbiCoder.encode(['string'], ['Hello, ']), 32) + ); + expect(state[1]).to.equal( + hexDataSlice(defaultAbiCoder.encode(['string'], ['world!']), 32) + ); + }); + + it('requires argument counts to match the function definition', () => { + const planner = new Planner(); + expect(() => planner.add(Math.add(1))).to.throw(); + }); + + it('plans a call to a function that takes and replaces the current state', () => { + const TestContract = Contract.fromEthersContract( + new ethers.Contract(SAMPLE_ADDRESS, [ + 'function useState(bytes[] state) returns(bytes[])', + ]) + ); + + const planner = new Planner(); + planner.replaceState(TestContract.useState(planner.state)); + const { commands, state } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0x08f389c8fefffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + + expect(state.length).to.equal(0); + }); }); diff --git a/yarn.lock b/yarn.lock index a9bce8c..b49bd74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3784,7 +3784,7 @@ eslint-plugin-jsx-a11y@^6.2.3: jsx-ast-utils "^3.1.0" language-tags "^1.0.5" -eslint-plugin-prettier@^3.1.0: +eslint-plugin-prettier@^3.1.0, eslint-plugin-prettier@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7" integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw== @@ -7092,6 +7092,11 @@ prettier@^1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"