diff --git a/README.md b/README.md index 0383631..373a532 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,28 @@ npm install --save @weiroll/weiroll.js ## Usage ### Wrapping contracts - -Weiroll programs consist of a sequence of delegatecalls to library functions in external contracts. Before you can start creating a weiroll program, you will need to create interfaces for at least one library contract you intend to use. +Weiroll programs consist of a sequence of calls to functions in external contracts. These calls can either be delegate calls to dedicated library contracts, or standard/static calls to external contracts. Before you can start creating a weiroll program, you will need to create interfaces for at least one contract you intend to use. The easiest way to do this is by wrapping ethers.js contract instances: ```javascript const ethersContract = new ethers.Contract(address, abi); -const contract = weiroll.Contract.fromEthersContract(ethersContract); +const contract = weiroll.Contract.newLibrary(ethersContract); ``` -You can repeat this for each library contract you wish to use. A weiroll `Contract` object can be reused across as many planner instances as you wish; there is no need to construct them again for each new program. +This will produce a contract object that generates delegate calls to the contract in `ethersContract`. + +To create regular or static calls to an external contract, use `newContract`: + +```javascript +const ethersContract = new ethers.Contract(address, abi); +// Makes calls using CALL +const contract = weiroll.Contract.newContract(ethersContract); +// Makes calls using STATICCALL +const contract = weiroll.Contract.newContract(ethersContract, CommandFlags.STATICCALL); +``` + +You can repeat this for each contract you wish to use. A weiroll `Contract` object can be reused across as many planner instances as you wish; there is no need to construct them again for each new program. ### Planning programs @@ -49,6 +60,20 @@ planner.add(contract.func2(ret)); Remember to wrap each call to a contract in `planner.add`. Attempting to pass the result of one contract function directly to another will not work - each one needs to be added to the planner! +For calls to external contracts, you can also pass a value in ether to send: + +```javascript +planner.add(contract.func(a, b).withValue(c)); +``` + +`withValue` takes the same argument types as contract functions, so you can pass the return value of another function, or a literal value. You cannot combine `withValue` with delegate calls (eg, calls to a library created with `Contract.newLibrary`) or static calls. + +Weiroll only supports functions that return a single value by default. If your function returns multiple values, though, you can instruct weiroll to wrap it in a `bytes`, which subsequent commands can decode and work with: + +```javascript +const ret = planner.add(contract.func(a, b).rawValue()); +``` + Once you are done planning operations, generate the program: ```javascript @@ -73,6 +98,24 @@ planner.add(Events.logUint(sum)); const {commands, state} = planner.plan(); ``` +### Subplans +In some cases it may be useful to be able to instantiate nested instances of the weiroll VM - for example, when using flash loans, or other systems that function by making a callback to your code. The weiroll planner supports this via 'subplans'. + +To make a subplan, construct the operations that should take place inside the nested instance normally, then pass the planner object to a contract function that executes the subplan, and pass that to the outer planner's `.addSubplan()` function instead of `.add()`. + +For example, suppose you want to call a nested instance to do some math: + +```javascript +const subplanner = new Planner(); +const sum = subplanner.add(Math.add(1, 2)); + +const planner = new Planner(); +planner.addSubplan(Weiroll.execute(subplanner, subplanner.state)); +planner.add(Events.logUint(sum)); + +const {commands, state} = planner.plan(); +``` + Subplan functions must specify which argument receives the current state using the special variable `Planner.state`, and must take exactly one subplanner and one state argument. Subplan functions must either return an updated state or nothing. If a subplan returns updated state, return values created in a subplanner, such as `sum` above, can be referenced in the outer scope, and even in other subplans, as long as they are referenced after the command that produces them. Subplans that do not return updated state are read-only, and return values defined inside them cannot be referenced outside them. diff --git a/package-lock.json b/package-lock.json index a23248c..281fb33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ } }, "@babel/compat-data": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz", - "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", + "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==", "dev": true }, "@babel/core": { @@ -201,9 +201,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { "@babel/types": "^7.14.5" @@ -404,9 +404,9 @@ } }, "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { @@ -421,9 +421,9 @@ } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.5.tgz", - "integrity": "sha512-tbD/CG3l43FIXxmu4a7RBe4zH7MLJ+S/lFowPFO7HetS2hyOZ/0nnnznegDuzFzfkyQYTxqdTH/hKmuBngaDAA==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz", + "integrity": "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5", @@ -513,12 +513,12 @@ } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.5.tgz", - "integrity": "sha512-VzMyY6PWNPPT3pxc5hi9LloKNr4SSrVCg7Yr6aZpW4Ym07r7KqSU/QXYwjXLVxqwSv0t/XSXkFoKBPUkZ8vb2A==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz", + "integrity": "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==", "dev": true, "requires": { - "@babel/compat-data": "^7.14.5", + "@babel/compat-data": "^7.14.7", "@babel/helper-compilation-targets": "^7.14.5", "@babel/helper-plugin-utils": "^7.14.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", @@ -785,9 +785,9 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.5.tgz", - "integrity": "sha512-wU9tYisEbRMxqDezKUqC9GleLycCRoUsai9ddlsq54r8QRLaeEhc+d+9DqCG+kV9W2GgQjTZESPTpn5bAFMDww==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5" @@ -906,9 +906,9 @@ } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.5.tgz", - "integrity": "sha512-+Xe5+6MWFo311U8SchgeX5c1+lJM+eZDBZgD+tvXu9VVQPXwwVzeManMMjYX6xw2HczngfOSZjoFYKwdeB/Jvw==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz", + "integrity": "sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg==", "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.14.5" @@ -1035,17 +1035,17 @@ } }, "@babel/preset-env": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.5.tgz", - "integrity": "sha512-ci6TsS0bjrdPpWGnQ+m4f+JSSzDKlckqKIJJt9UZ/+g7Zz9k0N8lYU8IeLg/01o2h8LyNZDMLGgRLDTxpudLsA==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.7.tgz", + "integrity": "sha512-itOGqCKLsSUl0Y+1nSfhbuuOlTs0MJk2Iv7iSH+XT/mR8U1zRLO7NjWlYXB47yhK4J/7j+HYty/EhFZDYKa/VA==", "dev": true, "requires": { - "@babel/compat-data": "^7.14.5", + "@babel/compat-data": "^7.14.7", "@babel/helper-compilation-targets": "^7.14.5", "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", - "@babel/plugin-proposal-async-generator-functions": "^7.14.5", + "@babel/plugin-proposal-async-generator-functions": "^7.14.7", "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/plugin-proposal-class-static-block": "^7.14.5", "@babel/plugin-proposal-dynamic-import": "^7.14.5", @@ -1054,7 +1054,7 @@ "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-object-rest-spread": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", "@babel/plugin-proposal-optional-chaining": "^7.14.5", "@babel/plugin-proposal-private-methods": "^7.14.5", @@ -1080,7 +1080,7 @@ "@babel/plugin-transform-block-scoping": "^7.14.5", "@babel/plugin-transform-classes": "^7.14.5", "@babel/plugin-transform-computed-properties": "^7.14.5", - "@babel/plugin-transform-destructuring": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", "@babel/plugin-transform-dotall-regex": "^7.14.5", "@babel/plugin-transform-duplicate-keys": "^7.14.5", "@babel/plugin-transform-exponentiation-operator": "^7.14.5", @@ -1092,7 +1092,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.14.5", "@babel/plugin-transform-modules-systemjs": "^7.14.5", "@babel/plugin-transform-modules-umd": "^7.14.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.7", "@babel/plugin-transform-new-target": "^7.14.5", "@babel/plugin-transform-object-super": "^7.14.5", "@babel/plugin-transform-parameters": "^7.14.5", @@ -1100,7 +1100,7 @@ "@babel/plugin-transform-regenerator": "^7.14.5", "@babel/plugin-transform-reserved-words": "^7.14.5", "@babel/plugin-transform-shorthand-properties": "^7.14.5", - "@babel/plugin-transform-spread": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.6", "@babel/plugin-transform-sticky-regex": "^7.14.5", "@babel/plugin-transform-template-literals": "^7.14.5", "@babel/plugin-transform-typeof-symbol": "^7.14.5", @@ -1111,7 +1111,7 @@ "babel-plugin-polyfill-corejs2": "^0.2.2", "babel-plugin-polyfill-corejs3": "^0.2.2", "babel-plugin-polyfill-regenerator": "^0.2.2", - "core-js-compat": "^3.14.0", + "core-js-compat": "^3.15.0", "semver": "^6.3.0" }, "dependencies": { @@ -1155,12 +1155,12 @@ } }, "@babel/runtime-corejs3": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz", - "integrity": "sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.7.tgz", + "integrity": "sha512-Wvzcw4mBYbTagyBVZpAJWI06auSIj033T/yNE0Zn1xcup83MieCddZA7ls3kme17L4NOGBrQ09Q+nKB41RLWBA==", "dev": true, "requires": { - "core-js-pure": "^3.14.0", + "core-js-pure": "^3.15.0", "regenerator-runtime": "^0.13.4" } }, @@ -1176,9 +1176,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -1186,7 +1186,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -2886,9 +2886,9 @@ "dev": true }, "axe-core": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.2.2.tgz", - "integrity": "sha512-OKRkKM4ojMEZRJ5UNJHmq9tht7cEnRnqKG6KyB/trYws00Xtkv12mHtlJ0SK7cmuNbrU8dPUova3ELTuilfBbw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.2.3.tgz", + "integrity": "sha512-pXnVMfJKSIWU2Ml4JHP7pZEPIrgBO1Fd3WGx+fPBsS+KRGhE4vxooD8XBGWbQOIVSZsVK7pUDBBkCicNu80yzQ==", "dev": true }, "axobject-query": { @@ -3963,9 +3963,9 @@ "dev": true }, "core-js-compat": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.14.0.tgz", - "integrity": "sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.15.1.tgz", + "integrity": "sha512-xGhzYMX6y7oEGQGAJmP2TmtBLvR4nZmRGEcFa3ubHOq5YEp51gGN9AovVa0AoujGZIq+Wm6dISiYyGNfdflYww==", "dev": true, "requires": { "browserslist": "^4.16.6", @@ -3981,9 +3981,9 @@ } }, "core-js-pure": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.14.0.tgz", - "integrity": "sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.1.tgz", + "integrity": "sha512-OZuWHDlYcIda8sJLY4Ec6nWq2hRjlyCqCZ+jCflyleMkVt3tPedDVErvHslyS2nbO+SlBFMSBJYvtLMwxnrzjA==", "dev": true }, "core-util-is": { @@ -4536,9 +4536,9 @@ } }, "electron-to-chromium": { - "version": "1.3.752", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz", - "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==", + "version": "1.3.757", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.757.tgz", + "integrity": "sha512-kP0ooyrvavDC+Y9UG6G/pUVxfRNM2VTJwtLQLvgsJeyf1V+7shMCb68Wj0/TETmfx8dWv9pToGkVT39udE87wQ==", "dev": true }, "elliptic": { @@ -4596,9 +4596,9 @@ } }, "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", "dev": true }, "errno": { @@ -6156,11 +6156,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "heap-js": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.1.5.tgz", - "integrity": "sha512-zJeFH1ZxUHuYG6x9+a8Dy+5kjqbKk1K8yPI1KZEdsi18VCd0UjTyJ2u9PfuIoj7t3+saMmI132rJGV33tP52VQ==" - }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -8799,9 +8794,9 @@ "dev": true }, "normalize-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.0.1.tgz", - "integrity": "sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true }, "npm-run-path": { @@ -8980,9 +8975,9 @@ "dev": true }, "optimize-css-assets-webpack-plugin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-6.0.0.tgz", - "integrity": "sha512-XKVxJuCBSslP1Eyuf1uVtZT3Pkp6jEIkmg7BMcNU/pq6XAnDXTINkYFWmiQWt8+j//FO4dIDd4v+gn0m5VWJIw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-6.0.1.tgz", + "integrity": "sha512-BshV2UZPfggZLdUfN3zFBbG4sl/DynUI+YCB6fRRDWaqO2OiWN8GPcp4Y0/fEV6B3k9Hzyk3czve3V/8B/SzKQ==", "dev": true, "requires": { "cssnano": "^5.0.2", @@ -12993,9 +12988,9 @@ }, "dependencies": { "acorn": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.0.tgz", - "integrity": "sha512-ULr0LDaEqQrMFGyQ3bhJkLsbtrQ8QibAseGZeaSUiT/6zb9IvIkomWHJIvgvwad+hinRAgsI51JcWk2yvwyL+w==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", "dev": true }, "commander": { diff --git a/src/planner.ts b/src/planner.ts index eb9b0b3..da35fb3 100644 --- a/src/planner.ts +++ b/src/planner.ts @@ -3,7 +3,7 @@ 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 { hexConcat, hexDataSlice } from '@ethersproject/bytes'; export interface Value { readonly param: ParamType; @@ -51,10 +51,75 @@ class SubplanValue implements Value { } } -export interface FunctionCall { +export enum CommandFlags { + DELEGATECALL = 0x00, + CALL = 0x01, + STATICCALL = 0x02, + CALL_WITH_VALUE = 0x03, + CALLTYPE_MASK = 0x03, + EXTENDED_COMMAND = 0x40, + TUPLE_RETURN = 0x80, +} + +export class FunctionCall { readonly contract: Contract; + readonly flags: CommandFlags; readonly fragment: FunctionFragment; readonly args: Value[]; + readonly callvalue?: Value; + + constructor( + contract: Contract, + flags: CommandFlags, + fragment: FunctionFragment, + args: Value[], + callvalue?: Value + ) { + this.contract = contract; + this.flags = flags; + this.fragment = fragment; + this.args = args; + this.callvalue = callvalue; + } + + withValue(value: Value): FunctionCall { + if ( + (this.flags & CommandFlags.CALLTYPE_MASK) !== CommandFlags.CALL && + (this.flags & CommandFlags.CALLTYPE_MASK) !== CommandFlags.CALL_WITH_VALUE + ) { + throw new Error('Only CALL operations can send value'); + } + return new FunctionCall( + this.contract, + (this.flags & ~CommandFlags.CALLTYPE_MASK) | CommandFlags.CALL_WITH_VALUE, + this.fragment, + this.args, + encodeArg(value, ParamType.from('uint')) + ); + } + + rawValue(): FunctionCall { + return new FunctionCall( + this.contract, + this.flags | CommandFlags.TUPLE_RETURN, + this.fragment, + this.args, + this.callvalue + ); + } + + staticcall(): FunctionCall { + if ((this.flags & CommandFlags.CALLTYPE_MASK) !== CommandFlags.CALL) { + throw new Error('Only CALL operations can be made static'); + } + return new FunctionCall( + this.contract, + (this.flags & ~CommandFlags.CALLTYPE_MASK) | CommandFlags.STATICCALL, + this.fragment, + this.args, + this.callvalue + ); + } } export type ContractFunction = (...args: Array) => FunctionCall; @@ -75,6 +140,22 @@ function abiEncodeSingle(param: ParamType, value: any): LiteralValue { return new LiteralValue(param, defaultAbiCoder.encode([param], [value])); } +function encodeArg(arg: any, param: ParamType): Value { + 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 if (arg instanceof Planner) { + return new SubplanValue(arg); + } else { + return abiEncodeSingle(param, arg); + } +} + function buildCall( contract: Contract, fragment: FunctionFragment @@ -86,40 +167,44 @@ function buildCall( ); } - 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 if (arg instanceof Planner) { - return new SubplanValue(arg); - } else { - return abiEncodeSingle(param, arg); - } - }); + const encodedArgs = args.map((arg, idx) => + encodeArg(arg, fragment.inputs[idx]) + ); - return { contract, fragment, args: encodedArgs }; + return new FunctionCall( + contract, + contract.commandflags, + fragment, + encodedArgs + ); }; } class BaseContract { readonly address: string; + readonly commandflags: CommandFlags; readonly interface: Interface; readonly functions: { [name: string]: ContractFunction }; - constructor(address: string, contractInterface: ContractInterface) { + constructor( + address: string, + contractInterface: ContractInterface, + commandflags: CommandFlags + ) { this.interface = getStatic< (contractInterface: ContractInterface) => Interface >( new.target, 'getInterface' )(contractInterface); + if ((commandflags & ~CommandFlags.CALLTYPE_MASK) !== 0) { + throw new Error( + 'Only calltype flags may be supplied to BaseContract constructor' + ); + } + this.address = address; + this.commandflags = commandflags; this.functions = {}; const uniqueNames: { [name: string]: Array } = {}; @@ -178,8 +263,19 @@ class BaseContract { }); } - static fromEthersContract(contract: EthersContract): Contract { - return new Contract(contract.address, contract.interface); + static createContract( + contract: EthersContract, + commandflags = CommandFlags.CALL + ): Contract { + return new Contract(contract.address, contract.interface, commandflags); + } + + static createLibrary(contract: EthersContract): Contract { + return new Contract( + contract.address, + contract.interface, + CommandFlags.DELEGATECALL + ); } static getInterface(contractInterface: ContractInterface): Interface { @@ -226,6 +322,10 @@ interface PlannerState { state: Array; } +function padArray(a: Array, len: number, value: number): Array { + return a.concat(new Array(len - a.length).fill(value)); +} + export class Planner { readonly state: StateValue; commands: Command[]; @@ -247,6 +347,9 @@ export class Planner { } } + if (call.flags & CommandFlags.TUPLE_RETURN) { + return new ReturnValue(ParamType.fromString('bytes'), command); + } if (call.fragment.outputs?.length !== 1) { return null; } @@ -273,6 +376,9 @@ export class Planner { if (!hasSubplan || !hasState) { throw new Error('Subplans must take planner and state arguments'); } + if (!hasSubplan || !hasState) { + throw new Error('Subplans must take planner and state arguments'); + } if ( call.fragment.outputs?.length === 1 && @@ -317,7 +423,18 @@ export class Planner { // Build visibility maps for (let command of this.commands) { - for (let arg of command.call.args) { + let inargs = command.call.args; + if ( + (command.call.flags & CommandFlags.CALLTYPE_MASK) === + CommandFlags.CALL_WITH_VALUE + ) { + if (!command.call.callvalue) { + throw new Error('Call with value must have a value parameter'); + } + inargs = [command.call.callvalue].concat(inargs); + } + + for (let arg of inargs) { if (arg instanceof ReturnValue) { if (!seen.has(arg.command)) { throw new Error( @@ -357,10 +474,21 @@ export class Planner { returnSlotMap: Map, literalSlotMap: Map, state: Array - ): string { + ): Array { // Build a list of argument value indexes - const args = new Uint8Array(7).fill(0xff); - command.call.args.forEach((arg, j) => { + let inargs = command.call.args; + if ( + (command.call.flags & CommandFlags.CALLTYPE_MASK) === + CommandFlags.CALL_WITH_VALUE + ) { + if (!command.call.callvalue) { + throw new Error('Call with value must have a value parameter'); + } + inargs = [command.call.callvalue].concat(inargs); + } + + const args = new Array(); + inargs.forEach((arg) => { let slot: number; if (arg instanceof ReturnValue) { slot = returnSlotMap.get(arg.command) as number; @@ -377,10 +505,10 @@ export class Planner { if (isDynamicType(arg.param)) { slot |= 0x80; } - args[j] = slot; + args.push(slot); }); - return hexlify(args); + return args; } private buildCommands(ps: PlannerState): Array { @@ -404,6 +532,8 @@ export class Planner { ps.freeSlots.push(ps.state.length - 1); } + let flags = command.call.flags; + const args = this.buildCommandArgs( command, ps.returnSlotMap, @@ -411,6 +541,10 @@ export class Planner { ps.state ); + if (args.length > 6) { + flags |= CommandFlags.EXTENDED_COMMAND; + } + // Add any newly unused state slots to the list ps.freeSlots = ps.freeSlots.concat( ps.stateExpirations.get(command) || [] @@ -447,7 +581,10 @@ export class Planner { ps.state.push('0x'); } - if (isDynamicType(command.call.fragment.outputs?.[0])) { + if ( + isDynamicType(command.call.fragment.outputs?.[0]) || + (command.call.flags & CommandFlags.TUPLE_RETURN) !== 0 + ) { ret |= 0x80; } } else if ( @@ -462,14 +599,31 @@ export class Planner { } } - encodedCommands.push( - hexConcat([ - command.call.contract.interface.getSighash(command.call.fragment), - args, - hexlify([ret]), - command.call.contract.address, - ]) - ); + if ( + (flags & CommandFlags.EXTENDED_COMMAND) === + CommandFlags.EXTENDED_COMMAND + ) { + // Extended command + encodedCommands.push( + hexConcat([ + command.call.contract.interface.getSighash(command.call.fragment), + [flags, 0, 0, 0, 0, 0, 0, ret], + command.call.contract.address, + ]) + ); + encodedCommands.push(hexConcat([padArray(args, 32, 0xff)])); + } else { + // Standard command + encodedCommands.push( + hexConcat([ + command.call.contract.interface.getSighash(command.call.fragment), + [flags], + padArray(args, 6, 0xff), + [ret], + command.call.contract.address, + ]) + ); + } } return encodedCommands; } diff --git a/tests/test_planner.ts b/tests/test_planner.ts index 852f7a3..577fa9a 100644 --- a/tests/test_planner.ts +++ b/tests/test_planner.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; import { hexConcat, hexDataSlice } from '@ethersproject/bytes'; import { defaultAbiCoder } from '@ethersproject/abi'; -import { Contract, Planner } from '../src/planner'; +import { CommandFlags, Contract, Planner } from '../src/planner'; import * as mathABI from '../abis/Math.json'; import * as stringsABI from '../abis/Strings.json'; @@ -12,7 +12,7 @@ describe('Contract', () => { let Math: Contract; before(() => { - Math = Contract.fromEthersContract( + Math = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi) ); }); @@ -41,10 +41,10 @@ describe('Planner', () => { let Strings: Contract; before(() => { - Math = Contract.fromEthersContract( + Math = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi) ); - Strings = Contract.fromEthersContract( + Strings = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, stringsABI.abi) ); }); @@ -65,7 +65,7 @@ describe('Planner', () => { expect(commands.length).to.equal(1); expect(commands[0]).to.equal( - '0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x771602f7000001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(2); @@ -89,10 +89,10 @@ describe('Planner', () => { expect(commands.length).to.equal(2); expect(commands[0]).to.equal( - '0x771602f70001ffffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x771602f7000001ffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(commands[1]).to.equal( - '0x771602f70102ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x771602f7000102ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(3); @@ -109,10 +109,10 @@ describe('Planner', () => { expect(commands.length).to.equal(2); expect(commands[0]).to.equal( - '0x771602f70000ffffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x771602f7000000ffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(commands[1]).to.equal( - '0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x771602f7000001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(2); @@ -127,7 +127,7 @@ describe('Planner', () => { expect(commands.length).to.equal(1); expect(commands[0]).to.equal( - '0x367bbd7880ffffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x367bbd780080ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(1); @@ -143,7 +143,7 @@ describe('Planner', () => { expect(commands.length).to.equal(1); expect(commands[0]).to.equal( - '0xd824ccf38081ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0xd824ccf3008081ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(2); @@ -163,10 +163,10 @@ describe('Planner', () => { expect(commands.length).to.equal(2); expect(commands[0]).to.equal( - '0xd824ccf38081ffffffffff81eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0xd824ccf3008081ffffffff81eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(commands[1]).to.equal( - '0x367bbd7881ffffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x367bbd780081ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(2); @@ -184,7 +184,7 @@ describe('Planner', () => { }); it('plans a call to a function that takes and replaces the current state', () => { - const TestContract = Contract.fromEthersContract( + const TestContract = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, [ 'function useState(bytes[] state) returns(bytes[])', ]) @@ -196,20 +196,20 @@ describe('Planner', () => { expect(commands.length).to.equal(1); expect(commands[0]).to.equal( - '0x08f389c8fefffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + '0x08f389c800fefffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ); expect(state.length).to.equal(0); }); describe('addSubplan()', () => { - const SubplanContract = Contract.fromEthersContract( + const SubplanContract = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, [ 'function execute(bytes32[] commands, bytes[] state) returns(bytes[])', ]) ); - const ReadonlySubplanContract = Contract.fromEthersContract( + const ReadonlySubplanContract = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, [ 'function execute(bytes32[] commands, bytes[] state)', ]) @@ -224,7 +224,7 @@ describe('Planner', () => { const { commands, state } = planner.plan(); expect(commands).to.deep.equal([ - '0xde792d5f82fefffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xde792d5f0082fefffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', ]); expect(state.length).to.equal(3); @@ -238,7 +238,7 @@ describe('Planner', () => { ]) )[0]; expect(subcommands).to.deep.equal([ - '0x771602f70001ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0x771602f7000001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', ]); }); @@ -253,9 +253,9 @@ describe('Planner', () => { const { commands } = planner.plan(); expect(commands).to.deep.equal([ // Invoke subplanner - '0xde792d5f83fefffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xde792d5f0083fefffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // sum + 3 - '0x771602f70102ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0x771602f7000102ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', ]); }); @@ -277,9 +277,9 @@ describe('Planner', () => { const { commands, state } = planner.plan(); expect(commands).to.deep.equal([ // Invoke subplanner1 - '0xde792d5f83fefffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xde792d5f0083fefffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // Invoke subplanner2 - '0xde792d5f84fefffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xde792d5f0084fefffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', ]); expect(state.length).to.equal(5); @@ -292,7 +292,7 @@ describe('Planner', () => { )[0]; expect(subcommands2).to.deep.equal([ // sum + 3 - '0x771602f70102ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0x771602f7000102ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', ]); }); @@ -322,7 +322,7 @@ describe('Planner', () => { }); it("doesn't allow more than one subplan per call", () => { - const MultiSubplanContract = Contract.fromEthersContract( + const MultiSubplanContract = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, [ 'function execute(bytes32[] commands, bytes32[] commands2, bytes[] state) returns(bytes[])', ]) @@ -340,7 +340,7 @@ describe('Planner', () => { }); it("doesn't allow more than one state array per call", () => { - const MultiStateContract = Contract.fromEthersContract( + const MultiStateContract = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, [ 'function execute(bytes32[] commands, bytes[] state, bytes[] state2) returns(bytes[])', ]) @@ -362,7 +362,7 @@ describe('Planner', () => { }); it('requires subplan functions return bytes32[] or nothing', () => { - const BadSubplanContract = Contract.fromEthersContract( + const BadSubplanContract = Contract.createLibrary( new ethers.Contract(SAMPLE_ADDRESS, [ 'function execute(bytes32[] commands, bytes[] state) returns(uint)', ]) @@ -396,7 +396,7 @@ describe('Planner', () => { const { commands } = planner.plan(); expect(commands).to.deep.equal([ - '0xde792d5f82feffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xde792d5f0082feffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', ]); }); @@ -415,4 +415,139 @@ describe('Planner', () => { ); }); }); + + it('plans CALLs', () => { + let Math = Contract.createContract( + new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi) + ); + + const planner = new Planner(); + planner.add(Math.add(1, 2)); + const { commands } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0x771602f7010001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + }); + + it('plans STATICCALLs', () => { + let Math = Contract.createContract( + new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi), + CommandFlags.STATICCALL + ); + + const planner = new Planner(); + planner.add(Math.add(1, 2)); + const { commands } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0x771602f7020001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + }); + + it('plans STATICCALLs via .staticcall()', () => { + let Math = Contract.createContract( + new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi) + ); + + const planner = new Planner(); + planner.add(Math.add(1, 2).staticcall()); + const { commands } = planner.plan(); + + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0x771602f7020001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + }); + + it('plans CALLs with value', () => { + const Test = Contract.createContract( + new ethers.Contract(SAMPLE_ADDRESS, ['function deposit(uint x) payable']) + ); + + const planner = new Planner(); + planner.add(Test.deposit(123).withValue(456)); + + const { commands } = planner.plan(); + expect(commands.length).to.equal(1); + expect(commands[0]).to.equal( + '0xb6b55f25030001ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + }); + + it('allows returns from other calls to be used for the value parameter', () => { + const Test = Contract.createContract( + new ethers.Contract(SAMPLE_ADDRESS, ['function deposit(uint x) payable']) + ); + + const planner = new Planner(); + const sum = planner.add(Math.add(1, 2)); + planner.add(Test.deposit(123).withValue(sum)); + + const { commands } = planner.plan(); + expect(commands.length).to.equal(2); + expect(commands).to.deep.equal([ + '0x771602f7000001ffffffff01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xb6b55f25030102ffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + ]); + }); + + it('does not allow value-calls for DELEGATECALL or STATICCALL', () => { + expect(() => Math.add(1, 2).withValue(3)).to.throw( + 'Only CALL operations can send value' + ); + + const StaticMath = Contract.createContract( + new ethers.Contract(SAMPLE_ADDRESS, mathABI.abi), + CommandFlags.STATICCALL + ); + expect(() => StaticMath.add(1, 2).withValue(3)).to.throw( + 'Only CALL operations can send value' + ); + }); + + it('does not allow making DELEGATECALL static', () => { + expect(() => Math.add(1, 2).staticcall()).to.throw( + 'Only CALL operations can be made static' + ); + }); + + it('uses extended commands where necessary', () => { + const Test = Contract.createLibrary( + new ethers.Contract(SAMPLE_ADDRESS, [ + 'function test(uint a, uint b, uint c, uint d, uint e, uint f, uint g) returns(uint)', + ]) + ); + + const planner = new Planner(); + planner.add(Test.test(1, 2, 3, 4, 5, 6, 7)); + const { commands } = planner.plan(); + expect(commands.length).to.equal(2); + expect(commands[0]).to.equal( + '0xe473580d40000000000000ffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ); + expect(commands[1]).to.equal( + '0x00010203040506ffffffffffffffffffffffffffffffffffffffffffffffffff' + ); + }); + + it('supports capturing the whole return value as a bytes', () => { + const Test = Contract.createLibrary( + new ethers.Contract(SAMPLE_ADDRESS, [ + 'function returnsTuple() returns(uint a, bytes32[] b)', + 'function acceptsBytes(bytes raw)', + ]) + ); + + const planner = new Planner(); + const ret = planner.add(Test.returnsTuple().rawValue()); + planner.add(Test.acceptsBytes(ret)); + const { commands } = planner.plan(); + expect(commands).to.deep.equal([ + '0x61a7e05e80ffffffffffff80eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0x3e9ef66a0080ffffffffffffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + ]); + }); });