├── .github └── workflows │ ├── changelog.yml │ ├── node.js.yml │ └── web-ui.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── lerna.json ├── package.json ├── packages ├── abi-to-sol │ ├── .gitignore │ ├── .madgerc │ ├── README.md │ ├── bin │ │ └── abi-to-sol.ts │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── declarations │ │ │ ├── bind.test.ts │ │ │ ├── bind.ts │ │ │ ├── collect.ts │ │ │ ├── find.ts │ │ │ ├── fromParameter.test.ts │ │ │ ├── fromParameter.ts │ │ │ ├── identifier.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── kind.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── parameter.ts │ │ ├── solidity │ │ │ ├── analyze.ts │ │ │ ├── defaults.ts │ │ │ ├── features.ts │ │ │ ├── generate.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── options.ts │ │ │ └── print.ts │ │ ├── type.test.ts │ │ ├── type.ts │ │ └── visitor.ts │ ├── test │ │ ├── compile-abi.ts │ │ ├── custom-example.ts │ │ └── preflight.ts │ └── tsconfig.json └── web-ui │ ├── .gitignore │ ├── README.md │ ├── config │ ├── env.js │ ├── getHttpsConfig.js │ ├── jest │ │ ├── babelTransform.js │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── modules.js │ ├── paths.js │ ├── pnpTs.js │ ├── webpack.config.js │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── scripts │ ├── build.js │ ├── start.js │ └── test.js │ ├── src │ ├── App.tsx │ ├── CopyButton.tsx │ ├── Footer.tsx │ ├── Layout.tsx │ ├── abi │ │ ├── Editor.tsx │ │ ├── Examples.ts │ │ ├── Header.tsx │ │ ├── Input.ts │ │ ├── Status.tsx │ │ ├── examples │ │ │ ├── AirSwap.abi.json │ │ │ ├── BunchaStructs.abi.json │ │ │ ├── DepositContract.abi.json │ │ │ ├── ENS.abi.json │ │ │ ├── UDVTs.abi.json │ │ │ └── UniswapV2Router02.abi.json │ │ └── index.ts │ ├── defaultAbi.json │ ├── highlightjs-solidity.d.ts │ ├── index.tsx │ ├── logo.svg │ ├── makeSet.ts │ ├── prettier-plugin-solidity.d.ts │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── solidity │ │ ├── Code.tsx │ │ ├── Controls.tsx │ │ ├── Header.tsx │ │ ├── Options.ts │ │ ├── OptionsControls.tsx │ │ ├── Output.ts │ │ ├── Status.tsx │ │ └── index.ts │ └── tsconfig.json └── yarn.lock /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Changelog Enforcer 2 | 3 | on: 4 | pull_request: 5 | branches: [ develop ] 6 | 7 | jobs: 8 | # Enforces the update of a changelog file on every pull request 9 | changelog: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: dangoslen/changelog-enforcer@v2 14 | with: 15 | changeLogPath: 'CHANGELOG.md' 16 | skipLabels: 'skip-changelog' 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | build-package: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [14.x, 16.x, 18.x] 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - run: npm install -g yarn 26 | - run: yarn 27 | - run: yarn test 28 | 29 | build-ui: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v2.3.1 34 | 35 | - name: Setup Node.js v16.x 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: 16.x 39 | 40 | - name: Install and build 41 | run: | 42 | yarn 43 | cd packages/abi-to-sol 44 | yarn prepare 45 | cd ../web-ui 46 | PUBLIC_URL=https://gnidan.github.io/abi-to-sol/ yarn build 47 | -------------------------------------------------------------------------------- /.github/workflows/web-ui.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Web UI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2.3.1 13 | 14 | - name: Setup Node.js v16.x 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 16.x 18 | 19 | - name: Install and build 20 | run: | 21 | yarn 22 | cd packages/abi-to-sol 23 | yarn prepare 24 | cd ../web-ui 25 | PUBLIC_URL=https://gnidan.github.io/abi-to-sol/ yarn build 26 | 27 | - name: Deploy 28 | uses: JamesIves/github-pages-deploy-action@4.1.4 29 | with: 30 | branch: gh-pages 31 | folder: packages/web-ui/build 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | deps.pdf 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 g. nicholas d'andrea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/abi-to-sol/README.md -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # abi-to-sol Release Procedure 2 | 3 | 1. Cut release branch from `develop` 4 | 5 | ```console 6 | git checkout -b release 7 | git push -u origin release 8 | ``` 9 | 10 | 2. Sanity-check: see what packages changed 11 | 12 | ```console 13 | npx lerna changed 14 | ``` 15 | 16 | 3. Update package versions 17 | 18 | ```console 19 | npx lerna version 20 | ``` 21 | 22 | 4. Rebuild project 23 | 24 | ```console 25 | yarn 26 | ``` 27 | 28 | 5. Perform release 29 | 30 | ```console 31 | npx lerna publish from-package 32 | ``` 33 | 34 | 6. Update CHANGELOG.md, replacing `vNext` with corresponding version, and 35 | adding link to release notes page (although URL won't exist yet) 36 | 37 | ```console 38 | vim CHANGELOG.md 39 | git add CHANGELOG.md 40 | git commit -m "Update CHANGELOG" 41 | ``` 42 | 43 | 7. PR `release` -> `develop` and then delete branch `release` on GitHub 44 | once merged. 45 | 46 | 8. Delete local `release` branch 47 | 48 | ```console 49 | git checkout develop 50 | git pull 51 | git branch -D release 52 | ``` 53 | 54 | 9. Sync `master` with `develop` and such 55 | 56 | ```console 57 | git checkout master 58 | git pull 59 | git merge develop 60 | git push 61 | git checkout develop 62 | git merge master 63 | git push 64 | ``` 65 | 66 | 10. Write+publish release notes on GitHub (**don't forget to start 67 | discussion topic for the release**) 68 | 69 | 11. Wait for Web UI to build and visit page to make sure everything's 70 | honkidori 71 | 72 | 12. Install abi-to-sol package for good measure 73 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "lernaupdate": "lernaupdate", 6 | "prepare": "lerna bootstrap", 7 | "test": "lerna run test --stream --concurrency=1 -- --colors" 8 | }, 9 | "devDependencies": { 10 | "lerna": "^4.0.0", 11 | "lerna-update-wizard": "^1.1.0" 12 | }, 13 | "workspaces": { 14 | "packages": [ 15 | "packages/*" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/abi-to-sol/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /packages/abi-to-sol/.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeRegExp": [ 3 | "\\.test\\.ts$", 4 | "\/test\/" 5 | ], 6 | "fileExtensions": ["js","ts"], 7 | "detectiveOptions": { 8 | "ts": { 9 | "skipTypeImports": true 10 | } 11 | }, 12 | "tsConfig": "tsconfig.json" 13 | } 14 | -------------------------------------------------------------------------------- /packages/abi-to-sol/README.md: -------------------------------------------------------------------------------- 1 | # abi-to-sol 2 | 3 | [![npm version](https://badge.fury.io/js/abi-to-sol.svg)](https://www.npmjs.com/package/abi-to-sol) 4 | [![gitpoap badge](https://public-api.gitpoap.io/v1/repo/gnidan/abi-to-sol/badge)](https://www.gitpoap.io/gh/gnidan/abi-to-sol) 5 | 6 | Input an ABI JSON and get a Solidity `interface` compatible with whatever 7 | `pragma solidity ` you need. 8 | 9 | **abi-to-sol** evaluates your input ABI and finds all the external functions, 10 | events, structs, and user-defined value types in order to produce a source file 11 | that's suitable for copying and pasting into your project. Import your external 12 | contract's interface and interact with it, almost as if you had copied the whole 13 | other project's sourcecode into a "vendor" directory (but without the potential 14 | Solidity version mismatch!) 15 | 16 | It doesn't matter what version of Solidity generated the ABI in the first place 17 | (or if the contract wasn't even written in Solidity), **abi-to-sol** will give 18 | you `*.sol` output that's compatible with your existing project! (Some rare 19 | caveats may apply, see [below](#caveats).) 20 | 21 | ## Try online! 22 | 23 | Skip the terminal and just use the hosted 24 | [Web UI](https://gnidan.github.io/abi-to-sol). 25 | 26 | ## CLI instructions 27 | 28 | Install globally via: 29 | 30 | ```console 31 | $ npm install -g abi-to-sol 32 | ``` 33 | 34 | Installing locally should work fine as well, but you may have to jump through 35 | hoops to get the `abi-to-sol` script available on your PATH. 36 | 37 | ### Usage 38 | 39 | Pipe ABI JSON to stdin, get Solidity on stdout. 40 | 41 | ```console 42 | abi-to-sol [--solidity-version=] [--license=] [--validate] [] 43 | abi-to-sol -h | --help 44 | abi-to-sol --version 45 | ``` 46 | 47 | Options: 48 | 49 | ```console 50 | 51 | Name of generated interface. Default: MyInterface 52 | 53 | --validate 54 | Validate JSON before starting 55 | 56 | -V --solidity-version 57 | Version of Solidity (for pragma). Default: >=0.7.0 <0.9.0 58 | 59 | -L --license 60 | SPDX license identifier. default: UNLICENSED 61 | 62 | -h --help Show this screen. 63 | --version Show version. 64 | ``` 65 | 66 | ### Example 67 | 68 | Run the following command: 69 | 70 | ```console 71 | $ echo '[{"constant":true,"inputs":[{"name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"node","type":"bytes32"}],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"node","type":"bytes32"},{"name":"label","type":"bytes32"},{"name":"owner","type":"address"}],"name":"setSubnodeOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"node","type":"bytes32"},{"name":"ttl","type":"uint64"}],"name":"setTTL","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"node","type":"bytes32"}],"name":"ttl","outputs":[{"name":"","type":"uint64"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"node","type":"bytes32"},{"name":"resolver","type":"address"}],"name":"setResolver","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"node","type":"bytes32"},{"name":"owner","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"owner","type":"address"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":true,"name":"label","type":"bytes32"},{"indexed":false,"name":"owner","type":"address"}],"name":"NewOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"resolver","type":"address"}],"name":"NewResolver","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"ttl","type":"uint64"}],"name":"NewTTL","type":"event"}]' \ 72 | | npx abi-to-sol ENS 73 | ``` 74 | 75 | Get this output: 76 | 77 | ```solidity 78 | // SPDX-License-Identifier: UNLICENSED 79 | // !! THIS FILE WAS AUTOGENERATED BY abi-to-sol. SEE BELOW FOR SOURCE. !! 80 | pragma solidity ^0.7.0; 81 | pragma experimental ABIEncoderV2; 82 | 83 | interface ENS { 84 | function resolver(bytes32 node) external view returns (address); 85 | 86 | function owner(bytes32 node) external view returns (address); 87 | 88 | function setSubnodeOwner( 89 | bytes32 node, 90 | bytes32 label, 91 | address owner 92 | ) external; 93 | 94 | function setTTL(bytes32 node, uint64 ttl) external; 95 | 96 | function ttl(bytes32 node) external view returns (uint64); 97 | 98 | function setResolver(bytes32 node, address resolver) external; 99 | 100 | function setOwner(bytes32 node, address owner) external; 101 | 102 | event Transfer(bytes32 indexed node, address owner); 103 | event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner); 104 | event NewResolver(bytes32 indexed node, address resolver); 105 | event NewTTL(bytes32 indexed node, uint64 ttl); 106 | } 107 | 108 | // THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: 109 | /* ... */ 110 | 111 | ``` 112 | 113 | ## Caveats 114 | 115 | - This tool works best with ABIs from contracts written in Solidity, thanks to 116 | the useful `internalType` hints that Solidity provides. This is non-standard, 117 | so abi-to-sol still works without those. You should be able to use this tool 118 | to import someone else's Vyper contract interface into your Solidity project. 119 | 120 | - [User-defined value types](https://blog.soliditylang.org/2021/09/27/user-defined-value-types/) 121 | are supported, but if these UDVTs require special constructors, abi-to-sol 122 | won't give you any implementations. Take extra care to make sure you know how 123 | to interact with an external contract that has UDVTs as part of its interface. 124 | 125 | - You might run into problems if you need this tool to output interfaces that 126 | are compatible with sufficiently old versions of Solidity (`<0.5.0`), due to 127 | certain missing features (structs/arrays couldn't be external function 128 | parameters back then). 129 | 130 | ... but probably you should definitely _just don't use solc that old._ 131 | 132 | - Similarly, there might be problems if you need this tool to output interfaces 133 | that are compatible with a particularly large range of solc versions (e.g. 134 | `^0.6.0 || ^0.7.0`). This is because the data location changed across versions 135 | (from `address[] calldata` to `address[] memory`, e.g.), and there's no single 136 | syntax that abi-to-sol can output that would satisfy everything. (This only 137 | matters for input ABIs where it's relevant, so you may still be alright.) 138 | 139 | - This project does not output Vyper code... but you don't need a project like 140 | this one for Vyper because Vyper already lets you import `*.abi.json` files 141 | directly! Maybe this isn't a caveat. 142 | 143 | ## Is this project useful to you? 144 | 145 | Feel free to donate to 146 | [gnidan.eth](https://etherscan.io/address/0xefef50ebacd8da3c13932ac204361b704eb8292c) 147 | ❤️ 148 | -------------------------------------------------------------------------------- /packages/abi-to-sol/bin/abi-to-sol.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | 4 | const neodoc = require("neodoc"); 5 | import {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 6 | import * as abiSchema from "@truffle/contract-schema/spec/abi.spec.json"; 7 | import betterAjvErrors from "better-ajv-errors"; 8 | import Ajv from "ajv"; 9 | 10 | import { 11 | generateSolidity, 12 | defaults, 13 | GenerateSolidityMode 14 | } from "../src"; 15 | 16 | const usage = ` 17 | abi-to-sol 18 | 19 | Usage: 20 | abi-to-sol 21 | [--solidity-version=] 22 | [--license=] 23 | [--validate] 24 | [--embedded] 25 | [--no-attribution] 26 | [--no-source] 27 | [] 28 | abi-to-sol -h | --help 29 | abi-to-sol --version 30 | 31 | Options: 32 | 33 | Name of generated interface. Default: ${defaults.name} 34 | 35 | --validate 36 | Validate JSON before starting 37 | 38 | -V --solidity-version 39 | Version of Solidity (for pragma). Default: ${defaults.solidityVersion} 40 | 41 | -L --license 42 | SPDX license identifier. Default: ${defaults.license} 43 | 44 | -E --embedded 45 | Omit pragma and SPDX license identifier (only \`interface\`s and \`struct\`s) 46 | 47 | -A --no-attribution 48 | Skip printing "AUTOGENERATED by abi-to-sol" as a header comment in output 49 | 50 | -S --no-source 51 | Skip printing source ABI JSON as footer comment in output 52 | 53 | -h --help Show this screen. 54 | --version Show version. 55 | `; 56 | 57 | const readStdin = async () => 58 | await new Promise((accept, reject) => { 59 | const chunks: Buffer[] = []; 60 | 61 | process.stdin.setEncoding("utf8"); 62 | 63 | process.stdin.on("data", (chunk) => chunks.push(chunk)); 64 | 65 | process.stdin.on("end", () => { 66 | try { 67 | const json = chunks.join(""); 68 | const abi = JSON.parse(json); 69 | accept(abi); 70 | } catch (error) { 71 | reject(error); 72 | } 73 | }); 74 | }); 75 | 76 | const main = async () => { 77 | const args = neodoc.run(usage, { 78 | smartOptions: true, 79 | laxPlacement: true, 80 | }); 81 | 82 | const ajv = new Ajv({jsonPointers: true}); 83 | const validate = ajv.compile(abiSchema); 84 | 85 | const options = { 86 | solidityVersion: args["-V"] || args["--solidity-version"], 87 | name: args[""], 88 | license: args["-L"] || args["--license"], 89 | validate: args["--validate"] || false, 90 | outputAttribution: !(args["-A"] || args["--no-attribution"]), 91 | outputSource: !(args["-S"] || args["--no-source"]), 92 | mode: (args["-E"] || args["--embedded"]) ? GenerateSolidityMode.Embedded : GenerateSolidityMode.Normal 93 | }; 94 | 95 | const abi: SchemaAbi = (await readStdin()) as SchemaAbi; 96 | 97 | if (options.validate) { 98 | const valid = validate(abi); 99 | if (!valid) { 100 | const output = betterAjvErrors(abiSchema, abi, validate.errors, { 101 | format: "cli", 102 | }); 103 | console.log(output); 104 | process.exit(1); 105 | } 106 | } 107 | 108 | process.stdout.write( 109 | generateSolidity({ 110 | ...options, 111 | abi, 112 | }) 113 | ); 114 | }; 115 | 116 | main(); 117 | -------------------------------------------------------------------------------- /packages/abi-to-sol/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /packages/abi-to-sol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abi-to-sol", 3 | "version": "0.8.0", 4 | "description": "Compile ABI JSON to Solidity interface", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "bin": { 8 | "abi-to-sol": "dist/bin/abi-to-sol.js" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "author": "g. nicholas d'andrea ", 14 | "license": "MIT", 15 | "scripts": { 16 | "abi-to-sol": "ts-node ./bin/abi-to-sol.ts", 17 | "prepare": "tsc", 18 | "madge": "madge ./src --image deps.pdf", 19 | "test": "jest src/**", 20 | "test:test": "jest test/**", 21 | "test:dist": "yarn prepare && jest dist/src", 22 | "test:dist:test": "yarn prepare && jest dist/test" 23 | }, 24 | "homepage": "https://github.com/gnidan/abi-to-sol#readme", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/gnidan/abi-to-sol.git", 28 | "directory": "packages/abi-to-sol" 29 | }, 30 | "devDependencies": { 31 | "@types/faker": "^5.1.2", 32 | "@types/jest": "^26.0.14", 33 | "@types/jest-json-schema": "^2.1.2", 34 | "@types/prettier": "^2.1.1", 35 | "@types/semver": "^7.3.7", 36 | "change-case": "^4.1.1", 37 | "faker": "^5.1.0", 38 | "fast-check": "3.1.1", 39 | "husky": ">=4", 40 | "jest": "^26.4.2", 41 | "jest-fast-check": "^0.0.1", 42 | "jest-json-schema": "^2.1.0", 43 | "lint-staged": ">=10", 44 | "madge": "^5.0.2", 45 | "solc": "^0.8.20", 46 | "ts-jest": "^26.4.0", 47 | "ts-node": "^9.0.0", 48 | "typescript": "^4.9.5" 49 | }, 50 | "dependencies": { 51 | "@truffle/abi-utils": "^1.0.0", 52 | "@truffle/contract-schema": "^3.3.1", 53 | "ajv": "^6.12.5", 54 | "better-ajv-errors": "^0.8.2", 55 | "neodoc": "^2.0.2", 56 | "semver": "^7.3.5", 57 | "source-map-support": "^0.5.19" 58 | }, 59 | "optionalDependencies": { 60 | "prettier": "^2.7.1", 61 | "prettier-plugin-solidity": "^1.0.0-dev.23" 62 | }, 63 | "husky": { 64 | "hooks": { 65 | "pre-commit": "lint-staged" 66 | } 67 | }, 68 | "lint-staged": { 69 | "*.{ts,js,css,md}": "prettier --write" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/bind.test.ts: -------------------------------------------------------------------------------- 1 | import * as Abi from "@truffle/abi-utils"; 2 | 3 | import { Identifier } from "./identifier"; 4 | import { fromParameter } from "./fromParameter"; 5 | 6 | import { bind } from "./bind"; 7 | 8 | describe("bind", () => { 9 | it("should re-use the same identifier for the same unnamed struct", () => { 10 | const internalComponent = { 11 | name: "u", 12 | type: "uint256" 13 | }; 14 | 15 | const parameter = { 16 | name: "a", 17 | type: "tuple", 18 | internalType: "struct A", 19 | components: [{ 20 | name: "b1", 21 | type: "tuple", 22 | components: [{ ...internalComponent }] 23 | }, { 24 | name: "b2", 25 | type: "tuple", 26 | components: [{ ...internalComponent }] 27 | }] 28 | }; 29 | 30 | const { declarations, parameterKind } = fromParameter(parameter); 31 | if (!("identifier" in parameterKind) || !parameterKind.identifier) { 32 | throw new Error("Expected parameterKind to have identifier"); 33 | } 34 | 35 | const { identifier } = parameterKind; 36 | 37 | const declarationsWithBindings = bind(declarations); 38 | 39 | const outerStructKind = declarationsWithBindings.byIdentifierReference[ 40 | Identifier.toReference(identifier) 41 | ]; 42 | 43 | if (!("members" in outerStructKind)) { 44 | throw new Error("Expected outer struct to have `members`"); 45 | } 46 | 47 | const { members: [b1, b2] } = outerStructKind; 48 | 49 | if (!("identifier" in b1.kind) || !("identifier" in b2.kind)) { 50 | throw new Error("Inner struct is missing identifier"); 51 | } 52 | 53 | expect(b1.kind.identifier).toEqual(b2.kind.identifier); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/bind.ts: -------------------------------------------------------------------------------- 1 | import { Identifier } from "./identifier"; 2 | import { Kind, Bindings, HasBindings, MissingDeepBindings } from "./kind"; 3 | 4 | import { Declarations, merge } from "./types"; 5 | 6 | export const bind = ( 7 | declarations: Declarations 8 | ): Declarations => { 9 | const { 10 | newDeclarations, 11 | identifierBySignature, 12 | }: { 13 | newDeclarations: Declarations; 14 | identifierBySignature: { 15 | [signature: string]: Identifier 16 | } 17 | } = Object.entries(declarations.unnamedBySignature || {}) 18 | .map(([signature, unidentifiedKind], index) => { 19 | const identifier: Identifier = { 20 | class: "struct", 21 | name: `S_${index}` 22 | }; 23 | 24 | const kind = { 25 | ...unidentifiedKind, 26 | identifier 27 | }; 28 | 29 | return { 30 | kind, 31 | signature 32 | }; 33 | }) 34 | .reduce(( 35 | { 36 | newDeclarations: { 37 | byIdentifierReference, 38 | globalIdentifiers 39 | }, 40 | identifierBySignature 41 | }, 42 | { 43 | kind, 44 | signature 45 | } 46 | ) => ({ 47 | newDeclarations: { 48 | byIdentifierReference: { 49 | ...byIdentifierReference, 50 | [Identifier.toReference(kind.identifier)]: kind 51 | }, 52 | unnamedBySignature: {}, 53 | globalIdentifiers: new Set([ 54 | ...globalIdentifiers, 55 | Identifier.toReference(kind.identifier) 56 | ]), 57 | identifiersByContainer: {}, 58 | }, 59 | identifierBySignature: { 60 | ...identifierBySignature, 61 | [signature]: kind.identifier 62 | } 63 | }), { 64 | newDeclarations: { 65 | byIdentifierReference: {}, 66 | unnamedBySignature: {}, 67 | globalIdentifiers: new Set([]), 68 | identifiersByContainer: {} 69 | }, 70 | identifierBySignature: {} 71 | }); 72 | 73 | const declarationsMissingDeepBindings = merge( 74 | declarations as Declarations, 75 | newDeclarations 76 | ); 77 | 78 | return { 79 | byIdentifierReference: Object.entries( 80 | declarationsMissingDeepBindings.byIdentifierReference 81 | ) 82 | .map(([identifierReference, kind]) => ({ 83 | [identifierReference]: Kind.isStruct(kind) 84 | ? bindStruct(kind, identifierBySignature) 85 | : kind 86 | })) 87 | .reduce((a, b) => ({ ...a, ...b }), {}), 88 | unnamedBySignature: {}, 89 | globalIdentifiers: declarationsMissingDeepBindings.globalIdentifiers, 90 | identifiersByContainer: declarationsMissingDeepBindings.identifiersByContainer 91 | }; 92 | }; 93 | 94 | const bindKind = ( 95 | kind: Kind, 96 | identifierBySignature: { 97 | [signature: string]: Identifier; 98 | } 99 | ): Kind => { 100 | if (Kind.isElementary(kind)) { 101 | return kind; 102 | } 103 | 104 | if (Kind.isUserDefinedValueType(kind)) { 105 | return kind; 106 | } 107 | 108 | if (Kind.isStruct(kind)) { 109 | return bindStruct(kind, identifierBySignature); 110 | } 111 | 112 | if (Kind.isArray(kind)) { 113 | return bindArray(kind, identifierBySignature); 114 | } 115 | 116 | throw new Error("Could not recognize kind"); 117 | } 118 | 119 | const bindStruct = ( 120 | kind: Kind.Struct, 121 | identifierBySignature: { 122 | [signature: string]: Identifier; 123 | } 124 | ): Kind.Struct => { 125 | const { 126 | signature, 127 | identifier = identifierBySignature[signature] 128 | } = kind; 129 | 130 | const members = kind.members.map(({ name, kind }) => ({ 131 | name, 132 | kind: bindKind(kind, identifierBySignature) 133 | })); 134 | 135 | return { 136 | signature, 137 | members, 138 | identifier 139 | } 140 | }; 141 | 142 | const bindArray = ( 143 | kind: Kind.Array, 144 | identifierBySignature: { 145 | [signature: string]: Identifier; 146 | } 147 | ): Kind.Array => { 148 | const itemKind = bindKind(kind.itemKind, identifierBySignature); 149 | const length = kind.length; 150 | 151 | return { 152 | itemKind, 153 | ...("length" in kind 154 | ? { length } 155 | : {} 156 | ) 157 | }; 158 | }; 159 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/collect.ts: -------------------------------------------------------------------------------- 1 | import type {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 2 | import type * as Abi from "@truffle/abi-utils"; 3 | 4 | import {Visitor, VisitOptions, dispatch, Node} from "../visitor"; 5 | 6 | import { Declarations, empty, merge } from "./types"; 7 | import { fromParameter } from "./fromParameter"; 8 | 9 | export const collectWithoutBindings = (node: Node) => 10 | dispatch({ node, visitor: new DeclarationsCollector() }); 11 | 12 | interface Context { 13 | } 14 | 15 | type Visit = VisitOptions; 16 | 17 | 18 | class DeclarationsCollector 19 | implements Visitor 20 | { 21 | visitAbi({ 22 | node: abi, 23 | context 24 | }: Visit): Declarations { 25 | return abi 26 | .map(node => dispatch({ node, context, visitor: this })) 27 | .reduce(merge, empty()); 28 | } 29 | 30 | visitEventEntry({ 31 | node: entry, 32 | context 33 | }: Visit): Declarations { 34 | return entry.inputs 35 | .map(node => dispatch({ node, context, visitor: this })) 36 | .reduce(merge, empty()); 37 | } 38 | 39 | visitErrorEntry({ 40 | node: entry, 41 | context 42 | }: Visit): Declarations { 43 | return entry.inputs 44 | .map(node => dispatch({ node, context, visitor: this })) 45 | .reduce(merge, empty()); 46 | } 47 | 48 | visitFunctionEntry({ 49 | node: entry, 50 | context 51 | }: Visit): Declarations { 52 | return [...entry.inputs, ...(entry.outputs || [])] 53 | .map(node => dispatch({ node, context, visitor: this })) 54 | .reduce(merge, empty()); 55 | } 56 | 57 | visitConstructorEntry({ 58 | node: entry, 59 | context 60 | }: Visit): Declarations { 61 | return entry.inputs 62 | .map(node => dispatch({ node, context, visitor: this })) 63 | .reduce(merge, empty()); 64 | } 65 | 66 | visitFallbackEntry({ 67 | node: entry 68 | }: Visit): Declarations { 69 | return empty(); 70 | } 71 | 72 | visitReceiveEntry({ 73 | node: entry, 74 | }: Visit): Declarations { 75 | return empty(); 76 | } 77 | 78 | visitParameter({ 79 | node: parameter 80 | }: Visit): Declarations { 81 | const { declarations } = fromParameter(parameter); 82 | return declarations; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/find.ts: -------------------------------------------------------------------------------- 1 | import type * as Abi from "@truffle/abi-utils"; 2 | 3 | import { Parameter, isParameter } from "../parameter"; 4 | 5 | import { Identifier } from "./identifier"; 6 | import { Kind, HasBindings } from "./kind"; 7 | import { Declarations } from "./types"; 8 | 9 | export const find = ( 10 | parameter: Abi.Parameter, 11 | declarations: Declarations 12 | ): Kind => { 13 | const { type } = parameter; 14 | 15 | if (!isParameter(parameter)) { 16 | throw new Error( 17 | `Parameter type \`${parameter.type}\` is not a valid ABI type` 18 | ); 19 | } 20 | 21 | if (Parameter.isElementary(parameter)) { 22 | return findElementary(parameter, declarations); 23 | } 24 | 25 | if (Parameter.isArray(parameter)) { 26 | return findArray(parameter, declarations); 27 | } 28 | 29 | if (Parameter.isTuple(parameter)) { 30 | return findTuple(parameter, declarations); 31 | } 32 | 33 | throw new Error(`Unknown type ${type}`); 34 | } 35 | 36 | const findElementary = ( 37 | parameter: Parameter.Elementary, 38 | declarations: Declarations 39 | ): Kind => { 40 | if (!Parameter.isUserDefinedValueType(parameter)) { 41 | const { type, internalType } = parameter; 42 | return { 43 | type, 44 | ...( 45 | internalType 46 | ? { hints: { internalType } } 47 | : {} 48 | ) 49 | } 50 | } 51 | 52 | const { name, scope } = Parameter.UserDefinedValueType.recognize( 53 | parameter 54 | ); 55 | const identifier = Identifier.UserDefinedValueType.create({ name, scope }); 56 | const reference = Identifier.toReference(identifier); 57 | 58 | const kind = declarations.byIdentifierReference[reference]; 59 | 60 | if (!kind) { 61 | throw new Error( 62 | `Unknown declaration with identifier reference ${reference}` 63 | ); 64 | } 65 | 66 | return kind; 67 | }; 68 | 69 | const findArray = ( 70 | parameter: Parameter.Array, 71 | declarations: Declarations 72 | ): Kind => { 73 | const itemParameter = Parameter.Array.item(parameter); 74 | 75 | const itemKind = find(itemParameter, declarations); 76 | 77 | return { 78 | itemKind, 79 | ...( 80 | Parameter.Array.isStatic(parameter) 81 | ? { length: Parameter.Array.Static.length(parameter) } 82 | : {} 83 | ) 84 | }; 85 | } 86 | 87 | const findTuple = ( 88 | parameter: Parameter.Tuple, 89 | declarations: Declarations 90 | ): Kind => { 91 | const { 92 | signature, 93 | name, 94 | scope 95 | } = Parameter.Tuple.recognize(parameter); 96 | 97 | const identifier = name 98 | ? Identifier.Struct.create({ name, scope }) 99 | : undefined; 100 | 101 | if (identifier) { 102 | const reference = Identifier.toReference(identifier); 103 | 104 | const kind = declarations.byIdentifierReference[reference]; 105 | 106 | if (!kind) { 107 | throw new Error( 108 | `Unknown declaration with identifier reference ${reference}` 109 | ); 110 | } 111 | 112 | return kind; 113 | } 114 | 115 | // reaching here guarantees no internalType specified for `parameter` 116 | // so only match declarations that also have no internalType 117 | 118 | const kind = Object.values(declarations.byIdentifierReference) 119 | .find(kind => 120 | Kind.isStruct(kind) && 121 | kind.signature === signature && 122 | !kind.hints?.internalType 123 | ) 124 | 125 | if (!kind) { 126 | throw new Error( 127 | `Unknown declaration with tuple signature ${signature}` 128 | ); 129 | } 130 | 131 | return kind; 132 | } 133 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/fromParameter.test.ts: -------------------------------------------------------------------------------- 1 | import type * as Abi from "@truffle/abi-utils"; 2 | import { abiTupleSignature } from "@truffle/abi-utils"; 3 | 4 | import { Identifier } from "./identifier"; 5 | 6 | import { fromParameter } from "./fromParameter"; 7 | 8 | describe("fromParameter", () => { 9 | it("builds declarations from a single elementary type", () => { 10 | const parameter: Abi.Parameter = { 11 | type: "uint256", 12 | name: "u", 13 | internalType: "uint256" 14 | }; 15 | 16 | const { declarations, parameterKind } = fromParameter(parameter); 17 | 18 | expect(declarations.byIdentifierReference).toEqual({}); 19 | expect(declarations.unnamedBySignature).toEqual({}); 20 | }); 21 | 22 | it("builds a reference to an unnamed struct", () => { 23 | const parameter: Abi.Parameter = { 24 | type: "tuple", 25 | name: "s", 26 | components: [ 27 | { 28 | type: "uint256", 29 | name: "u" 30 | } 31 | ] 32 | }; 33 | 34 | const expectedSignature = abiTupleSignature( 35 | // default to satisfy type-checker (Abi.Parameter includes non-tuples) 36 | parameter.components || [] 37 | ); 38 | 39 | const { declarations, parameterKind } = fromParameter(parameter); 40 | 41 | expect(declarations.byIdentifierReference).toEqual({}); 42 | expect(declarations.unnamedBySignature).toHaveProperty("(uint256)"); 43 | 44 | const unnamedDeclaration = declarations.unnamedBySignature["(uint256)"]; 45 | if (!unnamedDeclaration) { 46 | throw new Error("Expected unnamed reference"); 47 | } 48 | 49 | expect(unnamedDeclaration.signature).toEqual(expectedSignature); 50 | expect(unnamedDeclaration.identifier).toEqual(undefined); 51 | expect(unnamedDeclaration.members).toHaveLength(1); 52 | 53 | const [member] = unnamedDeclaration.members; 54 | 55 | expect(member.name).toEqual("u"); 56 | expect(member.kind).toEqual({ 57 | type: "uint256" 58 | }); 59 | }); 60 | 61 | it("should deduplicate unnamed structs", () => { 62 | const internalComponent = { 63 | name: "u", 64 | type: "uint256" 65 | }; 66 | 67 | const parameter = { 68 | name: "a", 69 | type: "tuple", 70 | components: [{ 71 | name: "b1", 72 | type: "tuple", 73 | components: [{ ...internalComponent }] 74 | }, { 75 | name: "b2", 76 | type: "tuple", 77 | components: [{ ...internalComponent }] 78 | }] 79 | }; 80 | 81 | const { declarations, parameterKind } = fromParameter(parameter); 82 | 83 | // outer struct 84 | expect(declarations.unnamedBySignature).toHaveProperty("((uint256),(uint256))"); 85 | const outerStruct = declarations.unnamedBySignature["((uint256),(uint256))"]; 86 | 87 | // inner struct 88 | expect(declarations.unnamedBySignature).toHaveProperty("(uint256)"); 89 | }); 90 | 91 | it("should include identifiers when given internalType", () => { 92 | const parameter: Abi.Parameter = { 93 | name: "a", 94 | type: "tuple", 95 | internalType: "struct A", 96 | components: [{ 97 | name: "u", 98 | type: "uint256" 99 | }, { 100 | name: "f", 101 | type: "address", 102 | internalType: "contract Foo" 103 | }] 104 | }; 105 | 106 | const { declarations, parameterKind } = fromParameter(parameter); 107 | 108 | if (!("identifier" in parameterKind)) { 109 | throw new Error("Expected `identifier` to exist on parameterKind"); 110 | } 111 | 112 | const { identifier } = parameterKind; 113 | 114 | if (!identifier) { 115 | throw new Error("Expected identifier to be defined"); 116 | } 117 | 118 | expect(declarations.byIdentifierReference).toHaveProperty( 119 | Identifier.toReference(identifier) 120 | ); 121 | 122 | const kind = declarations.byIdentifierReference[ 123 | Identifier.toReference(identifier) 124 | ]; 125 | 126 | expect(parameterKind).toEqual(kind); 127 | }); 128 | }); 129 | 130 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/fromParameter.ts: -------------------------------------------------------------------------------- 1 | import type * as Abi from "@truffle/abi-utils"; 2 | 3 | import { Parameter, isParameter } from "../parameter"; 4 | 5 | import { Identifier } from "./identifier"; 6 | import { Kind, MissingBindings } from "./kind"; 7 | import { Declarations, empty, merge, from } from "./types"; 8 | 9 | export interface FromParameterResult { 10 | parameterKind: Kind; 11 | declarations: Declarations; 12 | } 13 | 14 | export const fromParameter = ( 15 | parameter: Abi.Parameter 16 | ): FromParameterResult => { 17 | if (!isParameter(parameter)) { 18 | throw new Error( 19 | `Parameter type \`${parameter.type}\` is not a valid ABI type` 20 | ); 21 | } 22 | 23 | if (Parameter.isElementary(parameter)) { 24 | return fromElementaryParameter(parameter); 25 | } 26 | 27 | if (Parameter.isArray(parameter)) { 28 | return fromArrayParameter(parameter); 29 | } 30 | 31 | if (Parameter.isTuple(parameter)) { 32 | return fromTupleParameter(parameter); 33 | } 34 | 35 | throw new Error(`Unexpectedly could not convert Abi.Parameter to Kind`); 36 | }; 37 | 38 | const fromElementaryParameter = ( 39 | parameter: Parameter.Elementary 40 | ): FromParameterResult => { 41 | if (Parameter.isUserDefinedValueType(parameter)) { 42 | const { name, scope } = Parameter.UserDefinedValueType.recognize( 43 | parameter 44 | ); 45 | const identifier = Identifier.UserDefinedValueType.create({ name, scope }); 46 | 47 | const { type, internalType } = parameter; 48 | 49 | const parameterKind: Kind.UserDefinedValueType = { 50 | type, 51 | hints: { internalType }, 52 | identifier 53 | }; 54 | 55 | return { 56 | parameterKind, 57 | declarations: from(parameterKind) 58 | } 59 | } 60 | 61 | const { type, internalType } = parameter; 62 | 63 | const parameterKind: Kind.Elementary = { 64 | type, 65 | ...( 66 | internalType 67 | ? { hints: { internalType } } 68 | : {} 69 | ) 70 | }; 71 | 72 | return { 73 | parameterKind, 74 | declarations: from(parameterKind) 75 | } 76 | } 77 | 78 | const fromArrayParameter = ( 79 | parameter: Parameter.Array 80 | ): FromParameterResult => { 81 | const itemParameter = Parameter.Array.item(parameter); 82 | 83 | const { 84 | parameterKind: itemKind, 85 | declarations 86 | } = fromParameter(itemParameter); 87 | 88 | const parameterKind: Kind.Array = { 89 | itemKind, 90 | ...( 91 | Parameter.Array.isStatic(parameter) 92 | ? { length: Parameter.Array.Static.length(parameter) } 93 | : {} 94 | ) 95 | }; 96 | 97 | return { 98 | declarations, 99 | parameterKind 100 | }; 101 | }; 102 | 103 | const fromTupleParameter = ( 104 | parameter: Parameter.Tuple 105 | ): FromParameterResult => { 106 | const { 107 | internalType, 108 | components 109 | } = parameter; 110 | 111 | const { 112 | signature, 113 | name, 114 | scope 115 | } = Parameter.Tuple.recognize(parameter); 116 | 117 | const identifier = name 118 | ? Identifier.Struct.create({ name, scope }) 119 | : undefined; 120 | 121 | const memberResults: { 122 | member: Kind.Struct.Member; 123 | declarations: Declarations 124 | }[] = components.map(component => { 125 | const { name } = component; 126 | 127 | const { 128 | parameterKind: kind, 129 | declarations 130 | } = fromParameter(component); 131 | 132 | return { 133 | member: { 134 | kind, 135 | ...( 136 | name 137 | ? { name } 138 | : {} 139 | ) 140 | }, 141 | declarations 142 | } 143 | }); 144 | 145 | const members = memberResults.map(({ member }) => member); 146 | const membersDeclarations = memberResults 147 | .map(({ declarations }) => declarations) 148 | .reduce(merge, empty()); 149 | 150 | const parameterKind = { 151 | signature, 152 | members, 153 | ...( 154 | internalType 155 | ? { hints: { internalType } } 156 | : {} 157 | ), 158 | ...( 159 | identifier 160 | ? { identifier } 161 | : {} 162 | ) 163 | }; 164 | 165 | const declarations = merge( 166 | membersDeclarations, 167 | from(parameterKind) 168 | ); 169 | 170 | return { 171 | declarations, 172 | parameterKind 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/identifier.ts: -------------------------------------------------------------------------------- 1 | export type Identifier = 2 | | Identifier.Interface 3 | | Identifier.Struct 4 | | Identifier.UserDefinedValueType; 5 | 6 | export namespace Identifier { 7 | export interface Properties { 8 | name: string; 9 | scope?: string; 10 | } 11 | 12 | export type Class = 13 | | Struct.Class 14 | | Interface.Class 15 | | UserDefinedValueType.Class; 16 | 17 | export interface Interface { 18 | class: Interface.Class; 19 | name: string; 20 | container?: never; 21 | } 22 | 23 | export namespace Interface { 24 | export type Class = "interface"; 25 | 26 | export const create = ({ 27 | name 28 | }: Omit): Identifier.Interface => ({ 29 | class: "interface", 30 | name 31 | }); 32 | 33 | export type Reference = `${ 34 | Identifier["class"] 35 | }--${ 36 | Identifier["name"] 37 | }`; 38 | } 39 | 40 | export interface Struct { 41 | class: Struct.Class; 42 | name: string; 43 | container?: Interface; 44 | } 45 | 46 | export namespace Struct { 47 | export type Class = "struct"; 48 | 49 | export const create = ({ 50 | name, 51 | scope 52 | }: Properties): Identifier.Struct => ({ 53 | class: "struct", 54 | name, 55 | ...( 56 | scope 57 | ? { container: Identifier.Interface.create({ name: scope }) } 58 | : {} 59 | ) 60 | }); 61 | } 62 | 63 | export interface UserDefinedValueType { 64 | class: UserDefinedValueType.Class; 65 | name: string; 66 | container?: Interface; 67 | } 68 | 69 | 70 | export namespace UserDefinedValueType { 71 | export type Class = "udvt"; 72 | 73 | export const create = ({ 74 | name, 75 | scope 76 | }: Properties): Identifier.UserDefinedValueType => ({ 77 | class: "udvt", 78 | name, 79 | ...( 80 | scope 81 | ? { container: Identifier.Interface.create({ name: scope }) } 82 | : {} 83 | ) 84 | }); 85 | } 86 | 87 | export type Reference = 88 | | `${ 89 | Identifier["class"] 90 | }--${ 91 | Identifier["name"] 92 | }` 93 | | `${ 94 | Identifier["class"] 95 | }--${ 96 | Identifier["name"] 97 | }--${ 98 | Exclude["name"] 99 | }`; 100 | 101 | export const toReference = ( 102 | identifier: Identifier 103 | ): Reference => { 104 | if (identifier.container) { 105 | return `${identifier.class}--${identifier.name}--${identifier.container.name}`; 106 | } 107 | 108 | return `${identifier.class}--${identifier.name}`; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from "fast-check"; 2 | import { testProp } from "jest-fast-check"; 3 | import { Arbitrary } from "@truffle/abi-utils"; 4 | 5 | import { Declarations } from "."; 6 | import { Kind } from "./kind"; 7 | import { Identifier } from "./identifier"; 8 | 9 | describe("Declarations.collect", () => { 10 | describe("arbitrary examples", () => { 11 | describe("for non-tuple parameters / event parameters", () => { 12 | testProp( 13 | "are empty", 14 | [fc.oneof(Arbitrary.Parameter(), Arbitrary.EventParameter())], 15 | (parameter) => { 16 | fc.pre(!parameter.type.startsWith("tuple")); 17 | 18 | expect(Declarations.collect(parameter)).toEqual({ 19 | byIdentifierReference: {}, 20 | unnamedBySignature: {}, 21 | globalIdentifiers: new Set([]), 22 | identifiersByContainer: {}, 23 | }); 24 | } 25 | ); 26 | }); 27 | 28 | describe("for tuple parameters with non-tuple components", () => { 29 | testProp( 30 | "have length 1", 31 | [fc.oneof(Arbitrary.Parameter(), Arbitrary.EventParameter())], 32 | (parameter) => { 33 | fc.pre(parameter.type.startsWith("tuple")); 34 | fc.pre( 35 | parameter.components.every( 36 | (component: any) => !component.type.startsWith("tuple") 37 | ) 38 | ); 39 | 40 | const declarations = Declarations.collect(parameter); 41 | expect(Object.keys(declarations.byIdentifierReference)).toHaveLength(1); 42 | 43 | const [kind] = Object.values(declarations.byIdentifierReference); 44 | if (!("members" in kind)) { 45 | throw new Error("Expected kind to be a struct with members"); 46 | } 47 | 48 | const { members } = kind; 49 | expect(members).toHaveLength(parameter.components.length); 50 | 51 | for (const [index, member] of members.entries()) { 52 | expect(member.name).toEqual(parameter.components[index].name); 53 | } 54 | } 55 | ); 56 | }); 57 | 58 | describe("for tuple parameters with exactly one tuple component", () => { 59 | testProp( 60 | "have length 2", 61 | [fc.oneof(Arbitrary.Parameter(), Arbitrary.EventParameter())], 62 | (parameter) => { 63 | fc.pre(parameter.type.startsWith("tuple")); 64 | 65 | // find exactly one tuple-based component 66 | const tupleComponents = parameter.components.filter( 67 | (component: any) => component.type.startsWith("tuple") 68 | ); 69 | 70 | fc.pre(tupleComponents.length === 1); 71 | 72 | const [tupleComponent] = tupleComponents; 73 | 74 | fc.pre( 75 | tupleComponent.components.every( 76 | (component: any) => !component.type.startsWith("tuple") 77 | ) 78 | ); 79 | 80 | const declarations = Declarations.collect(parameter); 81 | expect(Object.keys(declarations.byIdentifierReference)).toHaveLength(2); 82 | } 83 | ); 84 | }); 85 | 86 | testProp( 87 | "produce only valid references to each other", 88 | [fc.oneof(Arbitrary.Parameter(), Arbitrary.EventParameter())], 89 | (parameter) => { 90 | fc.pre(parameter.type.startsWith("tuple")); 91 | 92 | const components = parameter.components || []; 93 | 94 | const declarations = Declarations.collect(parameter); 95 | 96 | for (const kind of Object.values(declarations.byIdentifierReference)) { 97 | if ("members" in kind) { 98 | for (const member of kind.members) { 99 | if (Kind.isStruct(member.kind)) { 100 | if (!("identifier" in member.kind)) { 101 | throw new Error("Expected identifier"); 102 | } 103 | expect(declarations.byIdentifierReference).toHaveProperty( 104 | Identifier.toReference(member.kind.identifier) 105 | ); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | ); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/index.ts: -------------------------------------------------------------------------------- 1 | import type {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 2 | import * as Abi from "@truffle/abi-utils"; 3 | 4 | import type { Declarations as _Declarations } from "./types"; 5 | import { find as _find } from "./find"; 6 | import { bind } from "./bind"; 7 | import { HasBindings } from "./kind"; 8 | import { collectWithoutBindings } from "./collect"; 9 | 10 | export { Identifier } from "./identifier"; 11 | export { Kind } from "./kind"; 12 | 13 | export namespace Declarations { 14 | export const collect = ( 15 | abi: Abi.Abi | SchemaAbi 16 | ): _Declarations => { 17 | const unboundDeclarations = collectWithoutBindings(abi); 18 | 19 | const declarations = bind(unboundDeclarations); 20 | 21 | return declarations; 22 | }; 23 | 24 | export const find = _find; 25 | } 26 | 27 | export type Declarations = _Declarations; 28 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/kind.ts: -------------------------------------------------------------------------------- 1 | import type * as Abi from "@truffle/abi-utils"; 2 | 3 | import { Identifier } from "./identifier"; 4 | 5 | import type { Type } from "../type"; 6 | 7 | export type MissingBindings = "missing-bindings"; 8 | export type MissingDeepBindings = "missing-deep-bindings"; 9 | export type HasBindings = "has-bindings"; 10 | 11 | export type Bindings = 12 | | MissingBindings 13 | | MissingDeepBindings 14 | | HasBindings; 15 | 16 | export type Kind = 17 | | Kind.Interface 18 | | Kind.Elementary 19 | | Kind.Array 20 | | Kind.Struct; 21 | 22 | export namespace Kind { 23 | /* 24 | * Elementary 25 | */ 26 | 27 | export interface Elementary { 28 | type: Type.Elementary; 29 | hints?: { 30 | internalType?: string; 31 | } 32 | } 33 | 34 | export const isElementary = ( 35 | kind: Kind 36 | ): kind is Elementary => "type" in kind; 37 | 38 | /* 39 | * UserDefinedValueType (extends Elementary) 40 | */ 41 | 42 | export interface UserDefinedValueType extends Elementary { 43 | // UDVTs are meaningless without identifier (they'd just be elementary) 44 | identifier: Identifier 45 | 46 | // UDVTs are a Solidity feature that depend on `internalType`, so require 47 | // the corresponding hint 48 | hints: { 49 | internalType: string; 50 | } 51 | } 52 | 53 | export const isUserDefinedValueType = ( 54 | kind: Kind 55 | ): kind is UserDefinedValueType => 56 | isElementary(kind) && "identifier" in kind; 57 | 58 | /* 59 | * Array 60 | */ 61 | 62 | export interface Array { 63 | itemKind: Kind; 64 | length?: number; 65 | hints?: {}; // don't use internalType for arrays, for safety 66 | } 67 | 68 | export const isArray = ( 69 | kind: Kind 70 | ): kind is Array => 71 | "itemKind" in kind; 72 | 73 | export namespace Array { 74 | export interface Static< 75 | B extends Bindings 76 | > extends Array { 77 | length: number; 78 | } 79 | 80 | export const isStatic = ( 81 | kind: Kind 82 | ): kind is Static => 83 | isArray(kind) && typeof kind.length === "number"; 84 | 85 | export interface Dynamic< 86 | B extends Bindings 87 | > extends Array { 88 | length: never; 89 | } 90 | 91 | export const isDynamic = ( 92 | kind: Kind 93 | ): kind is Dynamic => 94 | isArray(kind) && typeof kind.length !== "number"; 95 | } 96 | 97 | /* 98 | * Struct 99 | */ 100 | 101 | export type Struct = 102 | & { 103 | signature: string; 104 | hints?: { 105 | internalType?: string; 106 | } 107 | } 108 | & ( 109 | B extends HasBindings 110 | ? { 111 | identifier: Identifier; 112 | members: Struct.Member[]; 113 | } 114 | : B extends MissingDeepBindings 115 | ? { 116 | identifier: Identifier; 117 | members: Struct.Member[]; 118 | } 119 | : B extends MissingBindings 120 | ? { 121 | identifier?: Identifier; 122 | members: Struct.Member[]; 123 | } 124 | : { 125 | identifier?: Identifier; 126 | members: Struct.Member[]; 127 | } 128 | ); 129 | 130 | export const isStruct = ( 131 | kind: Kind 132 | ): kind is Struct => 133 | "members" in kind; 134 | 135 | export namespace Struct { 136 | export interface Member { 137 | name?: string; 138 | kind: Kind; 139 | }; 140 | } 141 | 142 | export type Interface = 143 | B extends HasBindings 144 | ? { 145 | identifier: Identifier; 146 | } 147 | : B extends MissingDeepBindings 148 | ? { 149 | identifier: Identifier; 150 | } 151 | : B extends MissingBindings 152 | ? { 153 | identifier?: Identifier; 154 | } 155 | : { 156 | identifier?: Identifier; 157 | }; 158 | 159 | export const isInterface = ( 160 | kind: Kind 161 | ): kind is Interface => 162 | // only has key identifier 163 | Object.keys(kind).filter(key => key !== "identifier").length === 0 164 | } 165 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/declarations/types.ts: -------------------------------------------------------------------------------- 1 | import { Identifier } from "./identifier"; 2 | import { Bindings, HasBindings, MissingDeepBindings, MissingBindings, Kind } from "./kind"; 3 | 4 | export type Declarations = { 5 | byIdentifierReference: { 6 | [reference: Identifier.Reference]: 7 | | Kind.Interface 8 | | Kind.UserDefinedValueType 9 | | Kind.Struct 10 | }; 11 | unnamedBySignature: UnnamedBySignature; 12 | globalIdentifiers: Set; 13 | identifiersByContainer: { 14 | [reference: Identifier.Interface.Reference]: Set; 15 | }; 16 | } 17 | 18 | export type UnnamedBySignature = 19 | B extends HasBindings 20 | ? { 21 | [signature: string]: never; 22 | } 23 | : B extends MissingDeepBindings 24 | ? { 25 | [signature: string]: undefined; 26 | } 27 | : B extends MissingBindings 28 | ? { 29 | [signature: string]: Kind.Struct; // udvts are inherently named 30 | } 31 | : { 32 | [signature: string]: Kind.Struct; // udvts are inherently named 33 | }; 34 | 35 | /** 36 | * Initialize an empty set of declarations 37 | */ 38 | export const empty = (): Declarations => ({ 39 | byIdentifierReference: {}, 40 | globalIdentifiers: new Set([]), 41 | identifiersByContainer: {}, 42 | unnamedBySignature: {} as UnnamedBySignature 43 | }); 44 | 45 | /** 46 | * Merge two sets of declarations 47 | */ 48 | export const merge = ( 49 | a: Declarations, 50 | b: Declarations 51 | ): Declarations => ({ 52 | byIdentifierReference: { 53 | ...a.byIdentifierReference, 54 | ...b.byIdentifierReference 55 | }, 56 | unnamedBySignature: { 57 | ...a.unnamedBySignature, 58 | ...b.unnamedBySignature 59 | }, 60 | globalIdentifiers: new Set([ 61 | ...a.globalIdentifiers, 62 | ...b.globalIdentifiers 63 | ]), 64 | identifiersByContainer: mergeIdentifiersByContainer( 65 | a.identifiersByContainer, 66 | b.identifiersByContainer 67 | ) 68 | }); 69 | 70 | /** 71 | * Generate declarations to include a single Kind. 72 | * Note! This does not recurse; e.g. it returns empty() for arrays always 73 | */ 74 | export const from = ( 75 | kind: Kind 76 | ): Declarations => { 77 | if (Kind.isInterface(kind)) { 78 | return fromInterface(kind); 79 | } 80 | 81 | if (Kind.isStruct(kind)) { 82 | return fromStruct(kind); 83 | } 84 | 85 | if (Kind.isUserDefinedValueType(kind)) { 86 | return fromUserDefinedValueType(kind); 87 | }; 88 | 89 | return empty(); 90 | } 91 | 92 | const fromUserDefinedValueType = ( 93 | kind: Kind.UserDefinedValueType 94 | ): Declarations => { 95 | const { identifier } = kind; 96 | const reference = Identifier.toReference(identifier); 97 | const { container } = identifier; 98 | 99 | // globally-defined case 100 | if (!container) { 101 | return { 102 | byIdentifierReference: { 103 | [reference]: kind 104 | }, 105 | unnamedBySignature: {} as UnnamedBySignature, 106 | globalIdentifiers: new Set([reference]), 107 | identifiersByContainer: {} 108 | }; 109 | } 110 | 111 | // defined inside containing contract/interface 112 | const containerDeclarations = fromInterface({ identifier: container }); 113 | const containerReference = Identifier.toReference(container); 114 | 115 | return merge(containerDeclarations, { 116 | byIdentifierReference: { 117 | [reference]: kind, 118 | }, 119 | unnamedBySignature: {} as UnnamedBySignature, 120 | globalIdentifiers: new Set([]), 121 | identifiersByContainer: { 122 | [containerReference]: new Set([reference]) 123 | } 124 | }); 125 | } 126 | 127 | const fromStruct = ( 128 | kind: Kind.Struct 129 | ): Declarations => { 130 | const { identifier } = kind; 131 | 132 | // unnamed case 133 | if (!identifier) { 134 | const { signature } = kind; 135 | 136 | return { 137 | byIdentifierReference: {}, 138 | unnamedBySignature: { 139 | [signature]: kind 140 | } as UnnamedBySignature, 141 | globalIdentifiers: new Set([]), 142 | identifiersByContainer: {}, 143 | }; 144 | } 145 | 146 | const reference = Identifier.toReference(identifier); 147 | const { container } = identifier; 148 | 149 | // globally-defined case 150 | if (!container) { 151 | return { 152 | byIdentifierReference: { 153 | [reference]: kind 154 | }, 155 | unnamedBySignature: {} as UnnamedBySignature, 156 | globalIdentifiers: new Set([reference]), 157 | identifiersByContainer: {} 158 | }; 159 | } 160 | 161 | // defined inside containing contract/interface 162 | const containerDeclarations = fromInterface({ identifier: container }); 163 | 164 | const containerReference = Identifier.toReference(container); 165 | 166 | return merge(containerDeclarations, { 167 | // defined inside interface case 168 | byIdentifierReference: { 169 | [reference]: kind, 170 | }, 171 | unnamedBySignature: {} as UnnamedBySignature, 172 | globalIdentifiers: new Set([]), 173 | identifiersByContainer: { 174 | [containerReference]: new Set([reference]) 175 | } 176 | }); 177 | } 178 | 179 | const fromInterface = ( 180 | kind: Kind.Interface 181 | ): Declarations => { 182 | const { identifier } = kind; 183 | if (!identifier) { 184 | return empty(); 185 | } 186 | 187 | const reference = Identifier.toReference(identifier); 188 | 189 | return { 190 | byIdentifierReference: { 191 | [reference]: kind, 192 | }, 193 | unnamedBySignature: {} as UnnamedBySignature, 194 | globalIdentifiers: new Set([reference]), 195 | identifiersByContainer: {} 196 | }; 197 | }; 198 | 199 | const mergeIdentifiersByContainer = ( 200 | a: Declarations["identifiersByContainer"], 201 | b: Declarations["identifiersByContainer"] 202 | ) => 203 | ([...new Set([ 204 | ...Object.keys(a), 205 | ...Object.keys(b) 206 | ])] as Identifier.Interface.Reference[]) 207 | .map((containerReference: Identifier.Interface.Reference) => ({ 208 | [containerReference]: new Set([ 209 | ...(a[containerReference] || []), 210 | ...(b[containerReference] || []) 211 | ]) 212 | })) 213 | .reduce((a, b) => ({ ...a, ...b }), {}) 214 | 215 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | generateSolidity, 3 | GenerateSolidityOptions, 4 | GenerateSolidityMode, 5 | defaults 6 | } from "./solidity"; 7 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/parameter.ts: -------------------------------------------------------------------------------- 1 | import * as Abi from "@truffle/abi-utils"; 2 | import { abiTupleSignature } from "@truffle/abi-utils"; 3 | 4 | import { Type, isType } from "./type"; 5 | 6 | export type Parameter = Abi.Parameter & { 7 | type: Type 8 | }; 9 | 10 | export const isParameter = ( 11 | parameter: Abi.Parameter 12 | ): parameter is Parameter => isType(parameter.type); 13 | 14 | export namespace Parameter { 15 | export type Elementary = Abi.Parameter & { 16 | type: Type.Elementary 17 | }; 18 | 19 | export const isElementary = ( 20 | parameter: Parameter 21 | ): parameter is Elementary => Type.isElementary(parameter.type); 22 | 23 | export type UserDefinedValueType = Elementary & { 24 | internalType: string 25 | } 26 | 27 | export const isUserDefinedValueType = ( 28 | parameter: Parameter 29 | ): parameter is UserDefinedValueType => 30 | isElementary(parameter) && 31 | !!parameter.internalType && 32 | parameter.internalType !== parameter.type && 33 | UserDefinedValueType.internalTypePattern.test(parameter.internalType); 34 | 35 | export namespace UserDefinedValueType { 36 | export const internalTypePattern = new RegExp( 37 | /^(([a-zA-Z$_][a-zA-Z0-9$_]*)\.)?([a-zA-Z$_][a-zA-Z0-9$_]*)$/ 38 | ); 39 | 40 | export type RecognizeResult

= ( 41 | P extends Parameter.UserDefinedValueType 42 | ? { 43 | name: string; 44 | scope?: string 45 | } 46 | : { 47 | name: string; 48 | scope?: string 49 | } | undefined 50 | ); 51 | 52 | export const recognize =

( 53 | parameter: P 54 | ): RecognizeResult

=> { 55 | const { type, internalType } = parameter; 56 | 57 | if (!internalType || internalType === type) { 58 | return undefined as RecognizeResult

; 59 | } 60 | 61 | const match = internalType.match(internalTypePattern); 62 | if (!match) { 63 | return undefined as RecognizeResult

; 64 | } 65 | 66 | const scope = match[2]; 67 | const name = match[3]; 68 | 69 | return { 70 | name, 71 | ...( 72 | scope 73 | ? { scope } 74 | : {} 75 | ) 76 | } as RecognizeResult

; 77 | }; 78 | } 79 | 80 | export type Array = Parameter & { 81 | type: Type.Array 82 | }; 83 | 84 | export const isArray = ( 85 | parameter: Parameter 86 | ): parameter is Parameter.Array => Type.isArray(parameter.type); 87 | 88 | export namespace Array { 89 | export const item = ( 90 | parameter: Parameter.Array 91 | ): Parameter => { 92 | const type = Type.Array.underlying(parameter.type); 93 | 94 | let internalType; 95 | { 96 | const match = (parameter.internalType || "").match(/^(.+)\[[^\]]*\]$/); 97 | if (match) { 98 | const [_, underlying] = match; 99 | internalType = underlying; 100 | } 101 | } 102 | 103 | return { 104 | ...parameter, 105 | type, 106 | ...( 107 | internalType && internalType !== "tuple" 108 | ? { internalType } 109 | : {} 110 | ) 111 | }; 112 | }; 113 | 114 | export type Static = Parameter.Array & { 115 | type: Type.Array.Static 116 | }; 117 | 118 | export const isStatic = ( 119 | parameter: Parameter.Array 120 | ): parameter is Parameter.Array.Static => 121 | Type.Array.isStatic(parameter.type); 122 | 123 | export namespace Static { 124 | export const length = ( 125 | parameter: Parameter.Array.Static 126 | ): number => Type.Array.length(parameter.type); 127 | } 128 | } 129 | 130 | export type Tuple = Parameter & { 131 | type: Type.Tuple; 132 | components: Exclude; 133 | } 134 | 135 | export const isTuple = ( 136 | parameter: Parameter 137 | ): parameter is Parameter.Tuple => Type.isTuple(parameter.type); 138 | 139 | 140 | export namespace Tuple { 141 | export const internalTypePattern = new RegExp( 142 | /^struct (([a-zA-Z$_][a-zA-Z0-9$_]*)\.)?([a-zA-Z$_][a-zA-Z0-9$_]*)$/ 143 | ); 144 | 145 | export type TupleRecognizeResult = { 146 | signature: string; 147 | name?: string; 148 | scope?: string 149 | } 150 | 151 | export type RecognizeResult

= 152 | P extends Parameter.Tuple 153 | ? TupleRecognizeResult 154 | : TupleRecognizeResult | undefined; 155 | 156 | export const recognize =

( 157 | parameter: P 158 | ): RecognizeResult

=> { 159 | if (!Parameter.isTuple(parameter)) { 160 | return undefined as RecognizeResult

; 161 | } 162 | 163 | const signature = abiTupleSignature(parameter.components); 164 | 165 | if (!parameter.internalType) { 166 | return { signature }; 167 | } 168 | 169 | const match = parameter.internalType.match(internalTypePattern); 170 | if (!match) { 171 | return { signature }; 172 | } 173 | 174 | const scope = match[2]; 175 | const name = match[3]; 176 | 177 | return { 178 | signature, 179 | name, 180 | ...( 181 | scope 182 | ? { scope } 183 | : {} 184 | ) 185 | }; 186 | }; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/analyze.ts: -------------------------------------------------------------------------------- 1 | import type { Abi as SchemaAbi } from "@truffle/contract-schema/spec"; 2 | import type * as Abi from "@truffle/abi-utils"; 3 | 4 | import { Visitor, VisitOptions, dispatch, Node } from "../visitor"; 5 | 6 | export const observableProperties = [ 7 | "defines-receive", 8 | "defines-fallback", 9 | "needs-abiencoder-v2", 10 | "defines-error", 11 | ] as const; 12 | 13 | export type AbiProperty = typeof observableProperties[number]; 14 | export type AbiProperties = Partial<{ 15 | [F in AbiProperty]: true 16 | }>; 17 | 18 | export const analyze = (node: SchemaAbi | Node) => 19 | dispatch({ 20 | node, 21 | visitor: new AbiPropertiesCollector(), 22 | }); 23 | 24 | export class AbiPropertiesCollector implements Visitor { 25 | visitAbi({ node: nodes }: VisitOptions): AbiProperties { 26 | return nodes 27 | .map((node) => dispatch({ node, visitor: this })) 28 | .reduce((a, b) => ({ ...a, ...b }), {}); 29 | } 30 | 31 | visitEventEntry({ node: entry }: VisitOptions): AbiProperties { 32 | return entry.inputs 33 | .map((node) => dispatch({ node, visitor: this })) 34 | .reduce((a, b) => ({ ...a, ...b }), {}); 35 | } 36 | 37 | visitErrorEntry({ node: entry }: VisitOptions): AbiProperties { 38 | return entry.inputs 39 | .map((node) => dispatch({ node, visitor: this })) 40 | .reduce((a, b) => ({ ...a, ...b }), {}); 41 | } 42 | 43 | visitFunctionEntry({ 44 | node: entry, 45 | }: VisitOptions): AbiProperties { 46 | return [...entry.inputs, ...(entry.outputs || [])] 47 | .map((node) => dispatch({ node, visitor: this })) 48 | .reduce((a, b) => ({ ...a, ...b }), {}); 49 | } 50 | 51 | visitConstructorEntry({ 52 | node: entry, 53 | }: VisitOptions): AbiProperties { 54 | return entry.inputs 55 | .map((node) => dispatch({ node, visitor: this })) 56 | .reduce((a, b) => ({ ...a, ...b }), {}); 57 | } 58 | 59 | visitFallbackEntry({ 60 | node: entry, 61 | }: VisitOptions): AbiProperties { 62 | return { "defines-fallback": true }; 63 | } 64 | 65 | visitReceiveEntry({ 66 | node: entry, 67 | }: VisitOptions): AbiProperties { 68 | return { "defines-receive": true }; 69 | } 70 | 71 | visitParameter({ 72 | node: parameter, 73 | }: VisitOptions): AbiProperties { 74 | if ( 75 | parameter.type.startsWith("tuple") || // anything with tuples 76 | parameter.type.includes("string[") || // arrays of strings 77 | parameter.type.includes("bytes[") || // arrays of bytes 78 | parameter.type.includes("][") // anything with nested arrays 79 | ) { 80 | return { "needs-abiencoder-v2": true }; 81 | } 82 | 83 | return {}; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/defaults.ts: -------------------------------------------------------------------------------- 1 | import { GenerateSolidityMode } from "./options"; 2 | 3 | export const name = "MyInterface"; 4 | export const license = "UNLICENSED"; 5 | export const solidityVersion = ">=0.7.0 <0.9.0"; 6 | export const prettifyOutput = true; 7 | export const outputAttribution = true; 8 | export const outputSource = true; 9 | export const mode = GenerateSolidityMode.Normal; 10 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/features.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver"; 2 | 3 | export const allFeatures = { 4 | "receive-keyword": { 5 | ">=0.6.0": true, 6 | "<0.6.0": false 7 | }, 8 | "fallback-keyword": { 9 | ">=0.6.0": true, 10 | "<0.6.0": false 11 | }, 12 | "array-parameter-location": { 13 | ">=0.7.0": "memory", 14 | "^0.5.0 || ^0.6.0": "calldata", 15 | "<0.5.0": undefined 16 | }, 17 | "abiencoder-v2": { 18 | ">=0.8.0": "default", 19 | "<0.8.0": "experimental", 20 | }, 21 | "global-structs": { 22 | ">=0.6.0": true, 23 | "<0.6.0": false 24 | }, 25 | "structs-in-interfaces": { 26 | ">=0.5.0": true, 27 | "<0.5.0": false 28 | }, 29 | "custom-errors": { 30 | ">=0.8.4": true, 31 | "<0.8.4": false 32 | }, 33 | "user-defined-value-types": { 34 | ">=0.8.8": true, 35 | "<0.8.8": false 36 | } 37 | } as const; 38 | 39 | export type AllFeatures = typeof allFeatures; 40 | 41 | export type Category = keyof AllFeatures; 42 | 43 | export type CategorySpecification = AllFeatures[C]; 44 | 45 | export type CategoryOptionRange = string & { 46 | [K in C]: keyof CategorySpecification 47 | }[C]; 48 | 49 | export type CategoryOption = { 50 | [K in C]: CategorySpecification[CategoryOptionRange] 51 | }[C]; 52 | 53 | export type BooleanCategory = { 54 | [C in Category]: CategoryOption extends boolean 55 | ? C 56 | : never 57 | }[Category]; 58 | 59 | export const isBooleanCategory = ( 60 | category: Category 61 | ): category is BooleanCategory => { 62 | return Object.values(allFeatures[category]) 63 | .every(option => option === true || option === false); 64 | }; 65 | 66 | export type VersionsFeatures = { 67 | [C in Category]: VersionsFeature; 68 | }; 69 | 70 | export type VersionsFeature = 71 | C extends BooleanCategory 72 | ? { 73 | supported(): boolean; 74 | missing(): boolean; 75 | varies(): boolean; 76 | } 77 | : { 78 | consistently(option: CategoryOption): boolean; 79 | }; 80 | 81 | export const forRange = (range: string | semver.Range): VersionsFeatures => { 82 | return (Object.keys(allFeatures) as Category[]) 83 | .map((category: C) => { 84 | const specification = allFeatures[category]; 85 | const matchingRanges: CategoryOptionRange[] = 86 | (Object.keys(specification) as CategoryOptionRange[]) 87 | .filter(optionRange => semver.intersects(range, optionRange)); 88 | 89 | const matchingOptions: CategoryOption[] = [...new Set( 90 | matchingRanges.map(range => specification[range]) 91 | )]; 92 | 93 | if (isBooleanCategory(category)) { 94 | return { 95 | [category]: { 96 | supported() { 97 | return ( 98 | matchingOptions.length === 1 && 99 | matchingOptions[0] === true as unknown as CategoryOption 100 | ); 101 | }, 102 | 103 | missing() { 104 | return ( 105 | matchingOptions.indexOf( 106 | true as unknown as CategoryOption 107 | ) === -1 108 | ) 109 | }, 110 | 111 | varies() { 112 | return matchingOptions.length > 1; 113 | } 114 | } 115 | }; 116 | } 117 | 118 | return { 119 | [category]: { 120 | consistently(option: CategoryOption) { 121 | if (matchingOptions.length !== 1) { 122 | return false; 123 | } 124 | 125 | const [onlyOption] = matchingOptions; 126 | return option === onlyOption; 127 | } 128 | } as VersionsFeature 129 | }; 130 | 131 | }) 132 | .reduce((a, b) => ({ ...a, ...b }), {}) as VersionsFeatures; 133 | } 134 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from "fast-check"; 2 | import { testProp } from "jest-fast-check"; 3 | import * as Abi from "@truffle/abi-utils"; 4 | import * as Example from "../../test/custom-example"; 5 | import { compileAbi } from "../../test/compile-abi"; 6 | import { excludesFunctionParameters } from "../../test/preflight"; 7 | 8 | import { generateSolidity } from "."; 9 | import { GenerateSolidityMode } from "./options"; 10 | 11 | const removeProps = (obj: any, keys: Set) => { 12 | if (obj instanceof Array) { 13 | for (const item of obj) { 14 | removeProps(item, keys); 15 | } 16 | } else if (typeof obj === "object") { 17 | for (const [key, value] of Object.entries(obj)) { 18 | if (keys.has(key)) { 19 | delete obj[key]; 20 | } else { 21 | removeProps(obj[key], keys); 22 | } 23 | } 24 | } 25 | 26 | return obj; 27 | }; 28 | 29 | describe("generateSolidity", () => { 30 | testProp("respects output settings", [ 31 | Abi.Arbitrary.Abi(), 32 | fc.constantFrom(GenerateSolidityMode.Normal, GenerateSolidityMode.Embedded), 33 | fc.boolean(), // outputAttribution 34 | fc.boolean(), // outputSource 35 | ], (abi, mode, outputAttribution, outputSource) => { 36 | fc.pre(abi.length > 0); 37 | 38 | const output = generateSolidity({ 39 | name: "MyInterface", 40 | abi, 41 | solidityVersion: "^0.8.20", 42 | mode, 43 | outputAttribution, 44 | outputSource 45 | }); 46 | 47 | const attributionOutput = output.indexOf("abi-to-sol") !== -1; 48 | const sourceOutput = output.indexOf("FROM THE FOLLOWING ABI JSON:") !== -1; 49 | const modeOutput = output.indexOf("SPDX-License-Identifier") === -1 50 | ? GenerateSolidityMode.Embedded 51 | : GenerateSolidityMode.Normal; 52 | 53 | expect(mode).toEqual(modeOutput); 54 | expect(outputAttribution).toEqual(attributionOutput); 55 | expect(outputSource).toEqual(sourceOutput); 56 | }); 57 | 58 | testProp("compiles to input ABI", [Abi.Arbitrary.Abi()], (abi) => { 59 | fc.pre( 60 | abi.every((entry) => "type" in entry && entry.type !== "constructor") 61 | ); 62 | fc.pre(excludesFunctionParameters(abi)); 63 | 64 | fc.pre(abi.length > 0); 65 | 66 | const output = generateSolidity({ 67 | name: "MyInterface", 68 | abi, 69 | solidityVersion: "^0.8.20", 70 | }); 71 | 72 | let resultAbi; 73 | try { 74 | resultAbi = compileAbi(output); 75 | } catch (error) { 76 | console.log("Failed to compile. Solidity:\n%s", output); 77 | throw error; 78 | } 79 | 80 | const compiledAbi = new Set( 81 | removeProps(resultAbi, new Set(["internalType"])) 82 | ); 83 | 84 | const expectedAbi = new Set(Abi.normalize(abi)); 85 | 86 | expect(compiledAbi).toEqual(expectedAbi); 87 | }, 88 | ); 89 | 90 | describe("custom example", () => { 91 | const abiWithoutConstructor = Abi.normalize( 92 | Example.abi.filter(({type}) => type !== "constructor") 93 | ); 94 | 95 | const output = generateSolidity({ 96 | name: "Example", 97 | abi: abiWithoutConstructor, 98 | solidityVersion: "^0.8.4", 99 | }); 100 | 101 | it("generates output", () => { 102 | const compiledAbi = compileAbi(output); 103 | 104 | const expectedAbi = abiWithoutConstructor.map((entry) => ({ 105 | ...entry, 106 | type: entry.type || "function", 107 | })); 108 | 109 | expect(compiledAbi).toEqual(expectedAbi); 110 | }); 111 | }); 112 | 113 | describe("function pointers", () => { 114 | const abi: Abi.Abi = [ 115 | { 116 | "inputs": [ 117 | { 118 | "internalType": "function (uint256) external returns (uint256)", 119 | "name": "f", 120 | "type": "function" 121 | }, 122 | { 123 | "internalType": "uint256[]", 124 | "name": "l", 125 | "type": "uint256[]" 126 | } 127 | ], 128 | "name": "map", 129 | "outputs": [], 130 | "stateMutability": "pure", 131 | "type": "function" 132 | } 133 | ]; 134 | 135 | const output = generateSolidity({ 136 | name: "Example", 137 | abi, 138 | solidityVersion: "^0.8.13", 139 | }); 140 | 141 | it("generates output", () => { 142 | const compiledAbi = compileAbi(output); 143 | expect(compiledAbi).toEqual(abi); 144 | }); 145 | }); 146 | 147 | describe("UDVT support", () => { 148 | const abi: Abi.Abi = [ 149 | { 150 | "inputs": [ 151 | { 152 | "internalType": "Int", 153 | "name": "i", 154 | "type": "int256" 155 | }, 156 | { 157 | "internalType": "Example.Uint", 158 | "name": "u", 159 | "type": "uint256" 160 | }, 161 | { 162 | "internalType": "Other.Bool", 163 | "name": "b", 164 | "type": "bool" 165 | } 166 | ], 167 | "name": "fnoo", 168 | "outputs": [], 169 | "stateMutability": "pure", 170 | "type": "function" 171 | } 172 | ]; 173 | 174 | const output = generateSolidity({ 175 | name: "Example", 176 | abi, 177 | solidityVersion: "^0.8.13", 178 | }); 179 | 180 | it("generates output", () => { 181 | const compiledAbi = compileAbi(output); 182 | expect(compiledAbi).toEqual(abi); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/index.ts: -------------------------------------------------------------------------------- 1 | import type Prettier from "prettier"; 2 | 3 | import { Declarations } from "../declarations"; 4 | 5 | import * as defaults from "./defaults"; 6 | import * as Features from "./features"; 7 | import { GenerateSolidityOptions } from "./options"; 8 | import { generateRawSolidity } from "./generate"; 9 | import { analyze } from "./analyze"; 10 | 11 | export { defaults }; 12 | export { GenerateSolidityOptions, GenerateSolidityMode } from "./options"; 13 | 14 | let prettier: typeof Prettier 15 | try { 16 | prettier = require("prettier"); 17 | } catch { 18 | // no-op 19 | } 20 | 21 | export const generateSolidity = ({ 22 | abi, 23 | name = defaults.name, 24 | solidityVersion = defaults.solidityVersion, 25 | license = defaults.license, 26 | mode = defaults.mode, 27 | outputAttribution = defaults.outputAttribution, 28 | outputSource = defaults.outputSource, 29 | prettifyOutput = prettier && defaults.prettifyOutput, 30 | }: GenerateSolidityOptions) => { 31 | if (!prettier && prettifyOutput) { 32 | throw new Error("Could not require() prettier"); 33 | } 34 | 35 | const versionsFeatures = Features.forRange(solidityVersion); 36 | const abiProperties = analyze(abi); 37 | const declarations = Declarations.collect(abi); 38 | 39 | const raw = generateRawSolidity(abi, { 40 | name, 41 | solidityVersion, 42 | license, 43 | mode, 44 | outputAttribution, 45 | outputSource, 46 | versionsFeatures, 47 | abiProperties, 48 | declarations 49 | }); 50 | 51 | if (!prettifyOutput) { 52 | return raw; 53 | } 54 | 55 | try { 56 | return prettier.format(raw, { 57 | plugins: ["prettier-plugin-solidity"], 58 | // @ts-ignore 59 | parser: "solidity-parse", 60 | }); 61 | } catch (error) { 62 | return raw; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/options.ts: -------------------------------------------------------------------------------- 1 | import type * as Abi from "@truffle/abi-utils"; 2 | import type {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 3 | 4 | export enum GenerateSolidityMode { 5 | Normal = "normal", 6 | Embedded = "embedded" 7 | } 8 | 9 | export interface GenerateSolidityOptions { 10 | abi: Abi.Abi | SchemaAbi; 11 | name?: string; 12 | solidityVersion?: string; 13 | license?: string; 14 | mode?: GenerateSolidityMode; 15 | outputAttribution?: boolean; 16 | outputSource?: boolean; 17 | prettifyOutput?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/solidity/print.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from "../declarations"; 2 | 3 | export const printType = ( 4 | kind: Kind, 5 | options: { 6 | currentInterfaceName?: string; 7 | enableUserDefinedValueTypes?: boolean; 8 | enableGlobalStructs?: boolean; 9 | shimGlobalInterfaceName?: string; 10 | } = {} 11 | ): string => { 12 | const { 13 | currentInterfaceName, 14 | enableUserDefinedValueTypes = false, 15 | enableGlobalStructs = false, 16 | shimGlobalInterfaceName 17 | } = options; 18 | 19 | if (Kind.isUserDefinedValueType(kind)) { 20 | return printUserDefinedValueTypeType(kind, { 21 | currentInterfaceName, 22 | enableUserDefinedValueTypes 23 | }); 24 | } 25 | 26 | if (Kind.isElementary(kind)) { 27 | return printElementaryType(kind); 28 | } 29 | 30 | if (Kind.isStruct(kind)) { 31 | return printStructType(kind, { 32 | currentInterfaceName, 33 | enableGlobalStructs, 34 | shimGlobalInterfaceName 35 | }); 36 | } 37 | 38 | if (Kind.isArray(kind)) { 39 | return printArrayType(kind, options); 40 | } 41 | 42 | throw new Error(`Unexpectedly unsupported kind: ${JSON.stringify(kind)}`); 43 | } 44 | 45 | const printUserDefinedValueTypeType = ( 46 | kind: Kind.UserDefinedValueType, 47 | options: { 48 | currentInterfaceName?: string; 49 | enableUserDefinedValueTypes?: boolean; 50 | } = {} 51 | ): string => { 52 | const { 53 | currentInterfaceName, 54 | enableUserDefinedValueTypes = false 55 | } = options; 56 | 57 | const result = ( 58 | kind.identifier.container && 59 | kind.identifier.container.name !== currentInterfaceName 60 | ) 61 | ? `${kind.identifier.container.name}.${kind.identifier.name}` 62 | : kind.identifier.name; 63 | 64 | if (!enableUserDefinedValueTypes) { 65 | return [ 66 | `/* warning: missing UDVT support in source Solidity version; `, 67 | `parameter is \`${result}\`. */ `, 68 | kind.type 69 | ].join(""); 70 | } 71 | 72 | return result; 73 | }; 74 | 75 | const printElementaryType = ( 76 | kind: Kind.Elementary, 77 | options: {} = {} 78 | ): string => { 79 | if (kind.type !== "function") { 80 | return kind.type; 81 | } 82 | 83 | // use just the `internalType` field if it exists 84 | if (kind.hints?.internalType) { 85 | return kind.hints.internalType; 86 | } 87 | 88 | // otherwise output minimally syntactically-valid syntax with a warning 89 | return [ 90 | "/* warning: the following type may be incomplete. ", 91 | "the receiving contract may expect additional input or output parameters. */ ", 92 | "function() external" 93 | ].join(""); 94 | } 95 | 96 | const printStructType = ( 97 | kind: Kind.Struct, 98 | options: { 99 | currentInterfaceName?: string, 100 | enableGlobalStructs?: boolean; 101 | shimGlobalInterfaceName?: string; 102 | } = {} 103 | ): string => { 104 | const { 105 | currentInterfaceName, 106 | enableGlobalStructs = false, 107 | shimGlobalInterfaceName 108 | } = options; 109 | 110 | if (!enableGlobalStructs && !shimGlobalInterfaceName) { 111 | throw new Error( 112 | "Option `shimGlobalInterfaceName` is required without global structs" 113 | ) 114 | } 115 | 116 | if ( 117 | kind.identifier.container && 118 | kind.identifier.container.name !== currentInterfaceName 119 | ) { 120 | return `${kind.identifier.container.name}.${kind.identifier.name}`; 121 | } 122 | 123 | if ( 124 | !kind.identifier.container && 125 | currentInterfaceName && 126 | !enableGlobalStructs 127 | ) { 128 | return `${shimGlobalInterfaceName}.${kind.identifier.name}`; 129 | } 130 | 131 | return kind.identifier.name; 132 | } 133 | 134 | const printArrayType = ( 135 | kind: Kind.Array, 136 | options: { 137 | currentInterfaceName?: string; 138 | enableUserDefinedValueTypes?: boolean; 139 | enableGlobalStructs?: boolean; 140 | shimGlobalInterfaceName?: string; 141 | } = {} 142 | ): string => { 143 | if (Kind.Array.isDynamic(kind)) { 144 | return `${printType(kind.itemKind, options)}[]`; 145 | } 146 | 147 | // static case 148 | return `${printType(kind.itemKind, options)}[${kind.length}]`; 149 | } 150 | 151 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/type.test.ts: -------------------------------------------------------------------------------- 1 | import { isType } from "./type"; 2 | 3 | describe("isType", () => { 4 | it("recognizes dynamic arrays", () => { 5 | expect(isType("uint256[]")).toEqual(true); 6 | }); 7 | 8 | it("recognizes static arrays", () => { 9 | expect(isType("uint256[1]")).toEqual(true); 10 | }); 11 | 12 | it("recognizes arrays of arrays", () => { 13 | expect(isType("ufixed256x18[][]")).toEqual(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/type.ts: -------------------------------------------------------------------------------- 1 | export type Octal = 2 | | "8" 3 | | "16" 4 | | "24" 5 | | "32" 6 | | "48" 7 | | "56" 8 | | "64" 9 | | "72" 10 | | "80" 11 | | "88" 12 | | "96" 13 | | "104" 14 | | "112" 15 | | "120" 16 | | "128" 17 | | "136" 18 | | "144" 19 | | "152" 20 | | "160" 21 | | "168" 22 | | "176" 23 | | "184" 24 | | "192" 25 | | "200" 26 | | "208" 27 | | "216" 28 | | "224" 29 | | "232" 30 | | "240" 31 | | "248" 32 | | "256"; 33 | 34 | export const isOctal = (expression: string): expression is Octal => { 35 | const integer = parseInt(expression, 10); 36 | if (expression !== `${integer}`) { 37 | return false; 38 | } 39 | 40 | return integer % 8 === 0 && integer >= 8 && integer <= 256; 41 | } 42 | 43 | export type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; 44 | 45 | // gotta do some janky stuff to represent the natural / whole numbers 46 | // even still, `Whole` will disagree with `isWhole` at times (shhhh it's ok) 47 | export type Digits = `${bigint}` & `${Digit}${string}`; 48 | export type Natural = 49 | & Digits 50 | & `${Exclude}${string}`; 51 | export type Whole = Natural | "0"; 52 | export const isWhole = ( 53 | expression: string 54 | ): expression is Whole => { 55 | const integer = parseInt(expression, 10); 56 | if (expression !== `${integer}`) { 57 | return false; 58 | } 59 | 60 | return integer >= 0; 61 | }; 62 | 63 | export type NaturalLessThanEqualToEighty = 64 | | Exclude // 1-9 65 | | `${Exclude}${Digit}` // 10-79 66 | | "80"; 67 | 68 | export const isNaturalLessThanEqualToEighty = ( 69 | expression: string 70 | ): expression is NaturalLessThanEqualToEighty => { 71 | const integer = parseInt(expression, 10); 72 | if (expression !== `${integer}`) { 73 | return false; 74 | } 75 | 76 | return integer > 0 && integer <= 80; 77 | } 78 | 79 | export type NaturalLessThanEqualToThirtyTwo = 80 | | Exclude // 1-9 81 | | `${"1" | "2"}${Digit}` // 10-29 82 | | "30" 83 | | "31" 84 | | "32"; 85 | 86 | export const isNaturalLessThanEqualToThirtyTwo = ( 87 | expression: string 88 | ): expression is NaturalLessThanEqualToThirtyTwo => { 89 | const integer = parseInt(expression, 10); 90 | if (expression !== `${integer}`) { 91 | return false; 92 | } 93 | 94 | return integer > 0 && integer <= 32; 95 | } 96 | 97 | export namespace Type { 98 | export namespace Elementary { 99 | export type Uint = "uint" | `uint${Octal}`; 100 | export const isUint = ( 101 | expression: string 102 | ): expression is Uint => 103 | expression === "uint" || ( 104 | expression.startsWith("uint") && 105 | isOctal(expression.slice(4)) 106 | ); 107 | 108 | export type Int = "int" | `int${Octal}`; 109 | export const isInt = ( 110 | expression: string 111 | ): expression is Int => 112 | expression === "int" || ( 113 | expression.startsWith("int") && 114 | isOctal(expression.slice(3)) 115 | ); 116 | 117 | export type Address = "address"; 118 | export const isAddress = ( 119 | expression: string 120 | ): expression is Address => 121 | expression === "address"; 122 | 123 | export type Bool = "bool"; 124 | export const isBool = ( 125 | expression: string 126 | ): expression is Bool => 127 | expression === "bool"; 128 | 129 | export type Ufixed = "ufixed" | `ufixed${Octal}x${NaturalLessThanEqualToEighty}`; 130 | export const isUfixed = ( 131 | expression: string 132 | ): expression is Ufixed => { 133 | if (expression === "ufixed") { 134 | return true; 135 | } 136 | 137 | const match = expression.match(/^ufixed([^x]+)x([^x]+)$/); 138 | if (!match) { 139 | return false; 140 | } 141 | 142 | const [_, m, n] = match; 143 | return isOctal(m) && isNaturalLessThanEqualToEighty(n); 144 | } 145 | 146 | export type Fixed = "fixed" | `fixed${Octal}x${NaturalLessThanEqualToEighty}`; 147 | export const isFixed = ( 148 | expression: string 149 | ): expression is Fixed => { 150 | if (expression === "fixed") { 151 | return true; 152 | } 153 | 154 | const match = expression.match(/^fixed([^x]+)x([^x]+)$/); 155 | if (!match) { 156 | return false; 157 | } 158 | 159 | const [_, m, n] = match; 160 | return isOctal(m) && isNaturalLessThanEqualToEighty(n); 161 | } 162 | 163 | export type StaticBytes = `bytes${NaturalLessThanEqualToThirtyTwo}`; 164 | export const isStaticBytes = ( 165 | expression: string 166 | ): expression is StaticBytes => 167 | expression.startsWith("bytes") && 168 | isNaturalLessThanEqualToThirtyTwo(expression.slice(5)); 169 | 170 | export type Bytes = "bytes"; 171 | export const isBytes = ( 172 | expression: string 173 | ): expression is Bytes => 174 | expression === "bytes"; 175 | 176 | export type String = "string"; 177 | export const isString = ( 178 | expression: string 179 | ): expression is String => 180 | expression === "string"; 181 | 182 | export type Function = "function"; 183 | export const isFunction = ( 184 | expression: string 185 | ): expression is Function => 186 | expression === "function"; 187 | } 188 | 189 | export type Elementary = 190 | | Elementary.Uint 191 | | Elementary.Int 192 | | Elementary.Address 193 | | Elementary.Bool 194 | | Elementary.Ufixed 195 | | Elementary.Fixed 196 | | Elementary.StaticBytes 197 | | Elementary.Bytes 198 | | Elementary.String 199 | | Elementary.Function; 200 | 201 | export const isElementary = ( 202 | expression: string 203 | ): expression is Elementary => 204 | Elementary.isUint(expression) || 205 | Elementary.isInt(expression) || 206 | Elementary.isAddress(expression) || 207 | Elementary.isBool(expression) || 208 | Elementary.isUfixed(expression) || 209 | Elementary.isFixed(expression) || 210 | Elementary.isStaticBytes(expression) || 211 | Elementary.isBytes(expression) || 212 | Elementary.isString(expression) || 213 | Elementary.isFunction(expression); 214 | 215 | export namespace Array { 216 | export type Static = `${string}[${Whole}]`; 217 | export const isStatic = ( 218 | expression: string 219 | ): expression is Static => { 220 | const match = expression.match(/^(.+)\[([0-9]+)\]$/); 221 | if (!match) { 222 | return false; 223 | } 224 | 225 | const [_, underlying, length] = match; 226 | return isType(underlying) && isWhole(length); 227 | }; 228 | 229 | export const length = ( 230 | type: Static 231 | ): number => { 232 | const match = type.match(/\[([0-9]+)\]$/); 233 | if (!match) { 234 | throw new Error( 235 | `Unexpected mismatch, type \`${type}\` is not a valid static array` 236 | ); 237 | } 238 | 239 | const [_, length] = match; 240 | return parseInt(length, 10); 241 | } 242 | 243 | export type Dynamic = `${string}[]`; 244 | export const isDynamic = ( 245 | expression: string 246 | ): expression is Dynamic => { 247 | const match = expression.match(/^(.+)\[\]$/); 248 | if (!match) { 249 | return false; 250 | } 251 | 252 | const [_, underlying] = match; 253 | return isType(underlying); 254 | } 255 | 256 | export const underlying = ( 257 | type: Array 258 | ): Type => { 259 | const match = type.match(/^(.+)\[[^\]]*\]$/); 260 | if (!match) { 261 | throw new Error( 262 | `Unexpected mismatch, \`${type}\` is not a valid array type` 263 | ); 264 | } 265 | 266 | const [_, underlying] = match; 267 | if (!isType(underlying)) { 268 | throw new Error( 269 | `Underlying type \`${underlying}\` is not a valid type` 270 | ); 271 | 272 | } 273 | 274 | return underlying; 275 | } 276 | } 277 | 278 | export type Array = 279 | | Array.Static 280 | | Array.Dynamic; 281 | 282 | export const isArray = ( 283 | expression: string 284 | ): expression is Array => 285 | Array.isStatic(expression) || 286 | Array.isDynamic(expression); 287 | 288 | export type Tuple = "tuple"; 289 | export const isTuple = ( 290 | expression: string 291 | ): expression is Tuple => 292 | expression === "tuple"; 293 | } 294 | 295 | export type Type = 296 | | Type.Elementary 297 | | Type.Array 298 | | Type.Tuple; 299 | 300 | export const isType = ( 301 | expression: string 302 | ): expression is Type => 303 | Type.isElementary(expression) || 304 | Type.isArray(expression) || 305 | Type.isTuple(expression); 306 | -------------------------------------------------------------------------------- /packages/abi-to-sol/src/visitor.ts: -------------------------------------------------------------------------------- 1 | import type {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 2 | import * as Abi from "@truffle/abi-utils"; 3 | 4 | export interface VisitOptions { 5 | node: N; 6 | context?: C; 7 | } 8 | 9 | export interface Visitor { 10 | visitAbi(options: VisitOptions): T; 11 | visitFunctionEntry(options: VisitOptions): T; 12 | visitConstructorEntry(options: VisitOptions): T; 13 | visitFallbackEntry(options: VisitOptions): T; 14 | visitReceiveEntry(options: VisitOptions): T; 15 | visitEventEntry(options: VisitOptions): T; 16 | visitErrorEntry(options: VisitOptions): T; 17 | visitParameter(options: VisitOptions): T; 18 | } 19 | 20 | export interface DispatchOptions { 21 | node: Node; 22 | visitor: Visitor; 23 | context?: C; 24 | } 25 | 26 | export type Node = 27 | | Abi.Abi 28 | | SchemaAbi 29 | | Abi.Entry 30 | | Abi.FunctionEntry 31 | | Abi.ConstructorEntry 32 | | Abi.FallbackEntry 33 | | Abi.ReceiveEntry 34 | | Abi.EventEntry 35 | | Abi.Parameter 36 | | Abi.EventParameter; 37 | 38 | export const dispatch = (options: DispatchOptions): T => { 39 | const {node, visitor, context} = options; 40 | 41 | if (isAbi(node)) { 42 | return visitor.visitAbi({ 43 | node: Abi.normalize(node), 44 | context, 45 | }); 46 | } 47 | 48 | if (isEntry(node)) { 49 | switch (node.type) { 50 | case "function": 51 | return visitor.visitFunctionEntry({node, context}); 52 | case "constructor": 53 | return visitor.visitConstructorEntry({node, context}); 54 | case "fallback": 55 | return visitor.visitFallbackEntry({node, context}); 56 | case "receive": 57 | return visitor.visitReceiveEntry({node, context}); 58 | case "event": 59 | return visitor.visitEventEntry({node, context}); 60 | case "error": 61 | return visitor.visitErrorEntry({node, context}); 62 | } 63 | } 64 | 65 | return visitor.visitParameter({node, context}); 66 | }; 67 | 68 | const isAbi = (node: Node | SchemaAbi): node is Abi.Abi | SchemaAbi => 69 | node instanceof Array; 70 | 71 | const isEntry = (node: Node): node is Abi.Entry => 72 | typeof node === "object" && 73 | "type" in node && 74 | typeof node.type === "string" && 75 | ["function", "constructor", "fallback", "receive", "event", "error"].includes( 76 | node.type 77 | ) && 78 | (node.type !== "function" || "stateMutability" in node || "constant" in node); 79 | -------------------------------------------------------------------------------- /packages/abi-to-sol/test/compile-abi.ts: -------------------------------------------------------------------------------- 1 | import {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 2 | 3 | const solc = require("solc"); 4 | 5 | export const compileAbi = (content: string): SchemaAbi => { 6 | const source = "interface.sol"; 7 | 8 | const input = { 9 | language: "Solidity", 10 | sources: { 11 | [source]: { 12 | content, 13 | }, 14 | }, 15 | settings: { 16 | outputSelection: { 17 | "*": { 18 | "*": ["abi"], 19 | }, 20 | }, 21 | }, 22 | }; 23 | 24 | const output: any = JSON.parse(solc.compile(JSON.stringify(input))); 25 | const errors = (output.errors || []).filter( 26 | ({type}: any) => type !== "Warning" 27 | ); 28 | if (errors.length > 0) { 29 | console.error(errors); 30 | } 31 | const {contracts} = output; 32 | const sourceOutput: any = contracts[source]; 33 | 34 | return (Object.values(sourceOutput)[0] as any).abi; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/abi-to-sol/test/custom-example.ts: -------------------------------------------------------------------------------- 1 | import {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 2 | 3 | /** 4 | * Solidity used to generate this ABI: 5 | * 6 | * ```solidity 7 | * // SPDX-License-Identifier: UNLICENSED 8 | * pragma solidity ^0.7.0; 9 | * pragma experimental ABIEncoderV2; 10 | * 11 | * struct Bar { 12 | * uint256 a; 13 | * uint256 b; 14 | * } 15 | * 16 | * struct Foo { 17 | * Bar[] bars; 18 | * uint256 c; 19 | * } 20 | * 21 | * contract TestCase { 22 | * event Event (Bar[] indexed); 23 | * 24 | * constructor (Foo memory foo1, Foo memory foo2, Bar memory bar) { 25 | * } 26 | * } 27 | * ``` 28 | */ 29 | export const abi: SchemaAbi = [ 30 | { 31 | inputs: [ 32 | { 33 | components: [ 34 | { 35 | components: [ 36 | {internalType: "uint256", name: "a", type: "uint256"}, 37 | {internalType: "uint256", name: "b", type: "uint256"}, 38 | ], 39 | internalType: "struct Bar[]", 40 | name: "bars", 41 | type: "tuple[]", 42 | }, 43 | {internalType: "uint256", name: "c", type: "uint256"}, 44 | ], 45 | internalType: "struct Foo", 46 | name: "foo1", 47 | type: "tuple", 48 | }, 49 | { 50 | components: [ 51 | { 52 | components: [ 53 | {internalType: "uint256", name: "a", type: "uint256"}, 54 | {internalType: "uint256", name: "b", type: "uint256"}, 55 | ], 56 | internalType: "struct Bar[]", 57 | name: "bars", 58 | type: "tuple[]", 59 | }, 60 | {internalType: "uint256", name: "c", type: "uint256"}, 61 | ], 62 | internalType: "struct Foo", 63 | name: "foo2", 64 | type: "tuple", 65 | }, 66 | { 67 | components: [ 68 | {internalType: "uint256", name: "a", type: "uint256"}, 69 | {internalType: "uint256", name: "b", type: "uint256"}, 70 | ], 71 | internalType: "struct Bar", 72 | name: "bar", 73 | type: "tuple", 74 | }, 75 | ], 76 | stateMutability: "nonpayable", 77 | type: "constructor", 78 | }, 79 | { 80 | anonymous: false, 81 | inputs: [ 82 | { 83 | components: [ 84 | {internalType: "uint256", name: "a", type: "uint256"}, 85 | {internalType: "uint256", name: "b", type: "uint256"}, 86 | ], 87 | indexed: true, 88 | internalType: "struct Bar[]", 89 | name: "", 90 | type: "tuple[]", 91 | }, 92 | ], 93 | name: "Event", 94 | type: "event", 95 | }, 96 | ]; 97 | 98 | export const expectedSignatures: {[name: string]: string} = { 99 | Foo: "((uint256,uint256)[],uint256)", 100 | Bar: "(uint256,uint256)", 101 | }; 102 | -------------------------------------------------------------------------------- /packages/abi-to-sol/test/preflight.ts: -------------------------------------------------------------------------------- 1 | import {Abi as SchemaAbi} from "@truffle/contract-schema/spec"; 2 | import * as Abi from "@truffle/abi-utils"; 3 | 4 | import {Visitor, VisitOptions, dispatch, Node} from "../src/visitor"; 5 | 6 | export const excludesFunctionParameters = (node: SchemaAbi | Abi.Abi) => 7 | dispatch({ 8 | node, 9 | // @ts-ignore 10 | visitor: new FunctionParameterExcluder(), 11 | }); 12 | 13 | class FunctionParameterExcluder implements Visitor { 14 | visitAbi({node: entries}: VisitOptions): boolean { 15 | return entries 16 | .map((node) => dispatch({node, visitor: this})) 17 | .reduce((a, b) => a && b, true); 18 | } 19 | 20 | visitFunctionEntry({node: entry}: VisitOptions): boolean { 21 | const {inputs, outputs} = entry; 22 | 23 | const inputsExcludeFunctions = inputs 24 | .map((node) => dispatch({node, visitor: this})) 25 | .reduce((a, b) => a && b, true); 26 | 27 | const outputsExcludeFunctions = outputs 28 | .map((node) => dispatch({node, visitor: this})) 29 | .reduce((a, b) => a && b, true); 30 | 31 | return inputsExcludeFunctions && outputsExcludeFunctions; 32 | } 33 | 34 | visitConstructorEntry({ 35 | node: entry, 36 | }: VisitOptions): boolean { 37 | const {inputs} = entry; 38 | 39 | return inputs 40 | .map((node) => dispatch({node, visitor: this})) 41 | .reduce((a, b) => a && b, true); 42 | } 43 | 44 | visitFallbackEntry({node: entry}: VisitOptions): boolean { 45 | return true; 46 | } 47 | 48 | visitReceiveEntry({node: entry}: VisitOptions): boolean { 49 | return true; 50 | } 51 | 52 | visitEventEntry({node: entry}: VisitOptions): boolean { 53 | const {inputs} = entry; 54 | 55 | return inputs 56 | .map((node) => dispatch({node, visitor: this})) 57 | .reduce((a, b) => a && b, true); 58 | } 59 | 60 | visitErrorEntry({node: entry}: VisitOptions): boolean { 61 | const {inputs} = entry; 62 | 63 | return inputs 64 | .map((node) => dispatch({node, visitor: this})) 65 | .reduce((a, b) => a && b, true); 66 | } 67 | 68 | visitParameter({node: parameter}: VisitOptions): boolean { 69 | if (parameter.type.startsWith("function")) { 70 | return false; 71 | } 72 | 73 | const {components} = parameter; 74 | 75 | if (components) { 76 | return components 77 | .map((node) => dispatch({node, visitor: this})) 78 | .reduce((a, b) => a && b, true); 79 | } 80 | 81 | return true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/abi-to-sol/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["es2019"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "dist", /* Redirect output structure to the directory. */ 18 | "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "resolveJsonModule": true, 30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 31 | "strictNullChecks": true, /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "include": [ 71 | "./src/**/*.ts", 72 | "./test/**/*.ts", 73 | "./bin/**/*.ts" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /packages/web-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/web-ui/README.md: -------------------------------------------------------------------------------- 1 | # Web UI 2 | 3 | Try out `abi-to-sol` via a web UI. 4 | 5 | ## Getting started 6 | 7 | ### Prerequisites 8 | 9 | - Node 16 10 | 11 | ### Instructions 12 | 13 | 1. If dependencies aren't already installed, run the following in the **root** of the repo: 14 | 15 | ```bash 16 | $ yarn 17 | ``` 18 | 19 | 2. If `abi-to-sol` hasn't been built, i.e. `packages/abi-to-sol/dist` doesn't exist, run the following in `packages/abi-to-sol`: 20 | 21 | ```bash 22 | $ yarn prepare 23 | ``` 24 | 25 | ## Troubleshooting 26 | 27 | ### `Error: error:0308010C:digital envelope routines::unsupported` 28 | 29 | Switch to Node v16. Explanation of the issue can be found [here](https://stackoverflow.com/a/73027407/6475944). 30 | 31 | ### `Cannot find module or its corresponding type declarations.` for `abi-to-sol` 32 | 33 | `packages/abi-to-sol` hasn't been built. Go to `packages/abi-to-sol` and run `yarn prepare`. 34 | -------------------------------------------------------------------------------- /packages/web-ui/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | const dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | // Don't include `.env.local` for `test` environment 21 | // since normally you expect tests to produce the same 22 | // results for everyone 23 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 24 | `${paths.dotenv}.${NODE_ENV}`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | // We support configuring the sockjs pathname during development. 81 | // These settings let a developer run multiple simultaneous projects. 82 | // They are used as the connection `hostname`, `pathname` and `port` 83 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` 84 | // and `sockPort` options in webpack-dev-server. 85 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 86 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 87 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 88 | // Whether or not react-refresh is enabled. 89 | // react-refresh is not 100% stable at this time, 90 | // which is why it's disabled by default. 91 | // It is defined here so it is available in the webpackHotDevClient. 92 | FAST_REFRESH: process.env.FAST_REFRESH !== 'false', 93 | } 94 | ); 95 | // Stringify all values so we can feed into webpack DefinePlugin 96 | const stringified = { 97 | 'process.env': Object.keys(raw).reduce((env, key) => { 98 | env[key] = JSON.stringify(raw[key]); 99 | return env; 100 | }, {}), 101 | }; 102 | 103 | return { raw, stringified }; 104 | } 105 | 106 | module.exports = getClientEnvironment; 107 | -------------------------------------------------------------------------------- /packages/web-ui/config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /packages/web-ui/config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babelJest = require('babel-jest'); 4 | 5 | const hasJsxRuntime = (() => { 6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 7 | return false; 8 | } 9 | 10 | try { 11 | require.resolve('react/jsx-runtime'); 12 | return true; 13 | } catch (e) { 14 | return false; 15 | } 16 | })(); 17 | 18 | module.exports = babelJest.createTransformer({ 19 | presets: [ 20 | [ 21 | require.resolve('babel-preset-react-app'), 22 | { 23 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 24 | }, 25 | ], 26 | ], 27 | babelrc: false, 28 | configFile: false, 29 | }); 30 | -------------------------------------------------------------------------------- /packages/web-ui/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/web-ui/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/web-ui/config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | if (!baseUrl) { 18 | return ''; 19 | } 20 | 21 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 22 | 23 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 24 | // the default behavior. 25 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 26 | return null; 27 | } 28 | 29 | // Allow the user set the `baseUrl` to `appSrc`. 30 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 31 | return [paths.appSrc]; 32 | } 33 | 34 | // If the path is equal to the root directory we ignore it here. 35 | // We don't want to allow importing from the root directly as source files are 36 | // not transpiled outside of `src`. We do allow importing them with the 37 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 38 | // an alias. 39 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 40 | return null; 41 | } 42 | 43 | // Otherwise, throw an error. 44 | throw new Error( 45 | chalk.red.bold( 46 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 47 | ' Create React App does not support other values at this time.' 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 54 | * 55 | * @param {*} options 56 | */ 57 | function getWebpackAliases(options = {}) { 58 | const baseUrl = options.baseUrl; 59 | 60 | if (!baseUrl) { 61 | return {}; 62 | } 63 | 64 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 65 | 66 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 67 | return { 68 | src: paths.appSrc, 69 | }; 70 | } 71 | } 72 | 73 | /** 74 | * Get jest aliases based on the baseUrl of a compilerOptions object. 75 | * 76 | * @param {*} options 77 | */ 78 | function getJestAliases(options = {}) { 79 | const baseUrl = options.baseUrl; 80 | 81 | if (!baseUrl) { 82 | return {}; 83 | } 84 | 85 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 86 | 87 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 88 | return { 89 | '^src/(.*)$': '/src/$1', 90 | }; 91 | } 92 | } 93 | 94 | function getModules() { 95 | // Check if TypeScript is setup 96 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 97 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 98 | 99 | if (hasTsConfig && hasJsConfig) { 100 | throw new Error( 101 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 102 | ); 103 | } 104 | 105 | let config; 106 | 107 | // If there's a tsconfig.json we assume it's a 108 | // TypeScript project and set up the config 109 | // based on tsconfig.json 110 | if (hasTsConfig) { 111 | const ts = require(resolve.sync('typescript', { 112 | basedir: paths.appNodeModules, 113 | })); 114 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 115 | // Otherwise we'll check if there is jsconfig.json 116 | // for non TS projects. 117 | } else if (hasJsConfig) { 118 | config = require(paths.appJsConfig); 119 | } 120 | 121 | config = config || {}; 122 | const options = config.compilerOptions || {}; 123 | 124 | const additionalModulePaths = getAdditionalModulePaths(options); 125 | 126 | return { 127 | additionalModulePaths: additionalModulePaths, 128 | webpackAliases: getWebpackAliases(options), 129 | jestAliases: getJestAliases(options), 130 | hasTsConfig, 131 | }; 132 | } 133 | 134 | module.exports = getModules(); 135 | -------------------------------------------------------------------------------- /packages/web-ui/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right 28 | 31 | 32 | 33 | 46 | 47 | 48 | 49 | 50 |

51 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /packages/web-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnidan/abi-to-sol/055a4a6818309695e9061918f0645373327eb38e/packages/web-ui/public/logo192.png -------------------------------------------------------------------------------- /packages/web-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnidan/abi-to-sol/055a4a6818309695e9061918f0645373327eb38e/packages/web-ui/public/logo512.png -------------------------------------------------------------------------------- /packages/web-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/web-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/web-ui/scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const path = require('path'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const fs = require('fs-extra'); 21 | const bfj = require('bfj'); 22 | const webpack = require('webpack'); 23 | const configFactory = require('../config/webpack.config'); 24 | const paths = require('../config/paths'); 25 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 26 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 27 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 28 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 29 | const printBuildError = require('react-dev-utils/printBuildError'); 30 | 31 | const measureFileSizesBeforeBuild = 32 | FileSizeReporter.measureFileSizesBeforeBuild; 33 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | 36 | // These sizes are pretty large. We'll warn for bundles exceeding them. 37 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 38 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 39 | 40 | const isInteractive = process.stdout.isTTY; 41 | 42 | // Warn and crash if required files are missing 43 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 44 | process.exit(1); 45 | } 46 | 47 | const argv = process.argv.slice(2); 48 | const writeStatsJson = argv.indexOf('--stats') !== -1; 49 | 50 | // Generate configuration 51 | const config = configFactory('production'); 52 | 53 | // We require that you explicitly set browsers and do not fall back to 54 | // browserslist defaults. 55 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 56 | checkBrowsers(paths.appPath, isInteractive) 57 | .then(() => { 58 | // First, read the current file sizes in build directory. 59 | // This lets us display how much they changed later. 60 | return measureFileSizesBeforeBuild(paths.appBuild); 61 | }) 62 | .then(previousFileSizes => { 63 | // Remove all content but keep the directory so that 64 | // if you're in it, you don't end up in Trash 65 | fs.emptyDirSync(paths.appBuild); 66 | // Merge with the public folder 67 | copyPublicFolder(); 68 | // Start the webpack build 69 | return build(previousFileSizes); 70 | }) 71 | .then( 72 | ({ stats, previousFileSizes, warnings }) => { 73 | if (warnings.length) { 74 | console.log(chalk.yellow('Compiled with warnings.\n')); 75 | console.log(warnings.join('\n\n')); 76 | console.log( 77 | '\nSearch for the ' + 78 | chalk.underline(chalk.yellow('keywords')) + 79 | ' to learn more about each warning.' 80 | ); 81 | console.log( 82 | 'To ignore, add ' + 83 | chalk.cyan('// eslint-disable-next-line') + 84 | ' to the line before.\n' 85 | ); 86 | } else { 87 | console.log(chalk.green('Compiled successfully.\n')); 88 | } 89 | 90 | console.log('File sizes after gzip:\n'); 91 | printFileSizesAfterBuild( 92 | stats, 93 | previousFileSizes, 94 | paths.appBuild, 95 | WARN_AFTER_BUNDLE_GZIP_SIZE, 96 | WARN_AFTER_CHUNK_GZIP_SIZE 97 | ); 98 | console.log(); 99 | 100 | const appPackage = require(paths.appPackageJson); 101 | const publicUrl = paths.publicUrlOrPath; 102 | const publicPath = config.output.publicPath; 103 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 104 | printHostingInstructions( 105 | appPackage, 106 | publicUrl, 107 | publicPath, 108 | buildFolder, 109 | useYarn 110 | ); 111 | }, 112 | err => { 113 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; 114 | if (tscCompileOnError) { 115 | console.log( 116 | chalk.yellow( 117 | 'Compiled with the following type errors (you may want to check these before deploying your app):\n' 118 | ) 119 | ); 120 | printBuildError(err); 121 | } else { 122 | console.log(chalk.red('Failed to compile.\n')); 123 | printBuildError(err); 124 | process.exit(1); 125 | } 126 | } 127 | ) 128 | .catch(err => { 129 | if (err && err.message) { 130 | console.log(err.message); 131 | } 132 | process.exit(1); 133 | }); 134 | 135 | // Create the production build and print the deployment instructions. 136 | function build(previousFileSizes) { 137 | console.log('Creating an optimized production build...'); 138 | 139 | const compiler = webpack(config); 140 | return new Promise((resolve, reject) => { 141 | compiler.run((err, stats) => { 142 | let messages; 143 | if (err) { 144 | if (!err.message) { 145 | return reject(err); 146 | } 147 | 148 | let errMessage = err.message; 149 | 150 | // Add additional information for postcss errors 151 | if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) { 152 | errMessage += 153 | '\nCompileError: Begins at CSS selector ' + 154 | err['postcssNode'].selector; 155 | } 156 | 157 | messages = formatWebpackMessages({ 158 | errors: [errMessage], 159 | warnings: [], 160 | }); 161 | } else { 162 | messages = formatWebpackMessages( 163 | stats.toJson({ all: false, warnings: true, errors: true }) 164 | ); 165 | } 166 | if (messages.errors.length) { 167 | // Only keep the first error. Others are often indicative 168 | // of the same problem, but confuse the reader with noise. 169 | if (messages.errors.length > 1) { 170 | messages.errors.length = 1; 171 | } 172 | return reject(new Error(messages.errors.join('\n\n'))); 173 | } 174 | if ( 175 | process.env.CI && 176 | (typeof process.env.CI !== 'string' || 177 | process.env.CI.toLowerCase() !== 'false') && 178 | messages.warnings.length 179 | ) { 180 | console.log( 181 | chalk.yellow( 182 | '\nTreating warnings as errors because process.env.CI = true.\n' + 183 | 'Most CI servers set it automatically.\n' 184 | ) 185 | ); 186 | return reject(new Error(messages.warnings.join('\n\n'))); 187 | } 188 | 189 | const resolveArgs = { 190 | stats, 191 | previousFileSizes, 192 | warnings: messages.warnings, 193 | }; 194 | 195 | if (writeStatsJson) { 196 | return bfj 197 | .write(paths.appBuild + '/bundle-stats.json', stats.toJson()) 198 | .then(() => resolve(resolveArgs)) 199 | .catch(error => reject(new Error(error))); 200 | } 201 | 202 | return resolve(resolveArgs); 203 | }); 204 | }); 205 | } 206 | 207 | function copyPublicFolder() { 208 | fs.copySync(paths.appPublic, paths.appBuild, { 209 | dereference: true, 210 | filter: file => file !== paths.appHtml, 211 | }); 212 | } 213 | -------------------------------------------------------------------------------- /packages/web-ui/scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const semver = require('semver'); 32 | const paths = require('../config/paths'); 33 | const configFactory = require('../config/webpack.config'); 34 | const createDevServerConfig = require('../config/webpackDevServer.config'); 35 | const getClientEnvironment = require('../config/env'); 36 | const react = require(require.resolve('react', { paths: [paths.appPath] })); 37 | 38 | const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); 39 | const useYarn = fs.existsSync(paths.yarnLockFile); 40 | const isInteractive = process.stdout.isTTY; 41 | 42 | // Warn and crash if required files are missing 43 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 44 | process.exit(1); 45 | } 46 | 47 | // Tools like Cloud9 rely on this. 48 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 49 | const HOST = process.env.HOST || '0.0.0.0'; 50 | 51 | if (process.env.HOST) { 52 | console.log( 53 | chalk.cyan( 54 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 55 | chalk.bold(process.env.HOST) 56 | )}` 57 | ) 58 | ); 59 | console.log( 60 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 61 | ); 62 | console.log( 63 | `Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}` 64 | ); 65 | console.log(); 66 | } 67 | 68 | // We require that you explicitly set browsers and do not fall back to 69 | // browserslist defaults. 70 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 71 | checkBrowsers(paths.appPath, isInteractive) 72 | .then(() => { 73 | // We attempt to use the default port but if it is busy, we offer the user to 74 | // run on a different port. `choosePort()` Promise resolves to the next free port. 75 | return choosePort(HOST, DEFAULT_PORT); 76 | }) 77 | .then(port => { 78 | if (port == null) { 79 | // We have not found a port. 80 | return; 81 | } 82 | 83 | const config = configFactory('development'); 84 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 85 | const appName = require(paths.appPackageJson).name; 86 | 87 | const useTypeScript = fs.existsSync(paths.appTsConfig); 88 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; 89 | const urls = prepareUrls( 90 | protocol, 91 | HOST, 92 | port, 93 | paths.publicUrlOrPath.slice(0, -1) 94 | ); 95 | const devSocket = { 96 | warnings: warnings => 97 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 98 | errors: errors => 99 | devServer.sockWrite(devServer.sockets, 'errors', errors), 100 | }; 101 | // Create a webpack compiler that is configured with custom messages. 102 | const compiler = createCompiler({ 103 | appName, 104 | config, 105 | devSocket, 106 | urls, 107 | useYarn, 108 | useTypeScript, 109 | tscCompileOnError, 110 | webpack, 111 | }); 112 | // Load proxy config 113 | const proxySetting = require(paths.appPackageJson).proxy; 114 | const proxyConfig = prepareProxy( 115 | proxySetting, 116 | paths.appPublic, 117 | paths.publicUrlOrPath 118 | ); 119 | // Serve webpack assets generated by the compiler over a web server. 120 | const serverConfig = createDevServerConfig( 121 | proxyConfig, 122 | urls.lanUrlForConfig 123 | ); 124 | const devServer = new WebpackDevServer(compiler, serverConfig); 125 | // Launch WebpackDevServer. 126 | devServer.listen(port, HOST, err => { 127 | if (err) { 128 | return console.log(err); 129 | } 130 | if (isInteractive) { 131 | clearConsole(); 132 | } 133 | 134 | if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { 135 | console.log( 136 | chalk.yellow( 137 | `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.` 138 | ) 139 | ); 140 | } 141 | 142 | console.log(chalk.cyan('Starting the development server...\n')); 143 | openBrowser(urls.localUrlForBrowser); 144 | }); 145 | 146 | ['SIGINT', 'SIGTERM'].forEach(function (sig) { 147 | process.on(sig, function () { 148 | devServer.close(); 149 | process.exit(); 150 | }); 151 | }); 152 | 153 | if (process.env.CI !== 'true') { 154 | // Gracefully exit when stdin ends 155 | process.stdin.on('end', function () { 156 | devServer.close(); 157 | process.exit(); 158 | }); 159 | } 160 | }) 161 | .catch(err => { 162 | if (err && err.message) { 163 | console.log(err.message); 164 | } 165 | process.exit(1); 166 | }); 167 | -------------------------------------------------------------------------------- /packages/web-ui/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /packages/web-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ChakraProvider, 4 | Box, 5 | Flex, 6 | Divider, 7 | } from "@chakra-ui/react"; 8 | import { Page, Container } from "./Layout"; 9 | import { Footer } from "./Footer"; 10 | import * as Abi from "./abi"; 11 | import * as Solidity from "./solidity"; 12 | 13 | function App() { 14 | return ( 15 |
19 | 20 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {/**/} 90 | 91 |
92 | ); 93 | } 94 | 95 | export default App; 96 | -------------------------------------------------------------------------------- /packages/web-ui/src/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, Button, useClipboard } from "@chakra-ui/react"; 3 | import { CopyIcon } from "@chakra-ui/icons"; 4 | 5 | export interface CopyButtonOptions { 6 | text?: string; 7 | } 8 | 9 | export const CopyButton = ({ text }: CopyButtonOptions) => { 10 | let buttonText = "Copy to clipboard"; 11 | 12 | const { hasCopied, onCopy } = useClipboard(text || ""); 13 | 14 | if (!text) { 15 | buttonText = "No result"; 16 | } else if (hasCopied) { 17 | buttonText = 'Copied' 18 | } 19 | 20 | return ( 21 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/web-ui/src/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Box, 4 | Link, 5 | Heading, 6 | Text, 7 | } from "@chakra-ui/react"; 8 | import { FaGithub } from "react-icons/fa"; 9 | import { version } from "abi-to-sol/package.json"; 10 | 11 | export const Footer = () => { 12 | return ( 13 | 14 | 15 | abi-to-sol v{version} 19 | 20 | 21 | 22 | 23 | Tool & web UI © 2020-2023{" "} 24 | 28 | @gnidan 29 | and distributed under the MIT license. 30 | 31 | 32 | 36 | Having issues with this tool? 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /packages/web-ui/src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import{ 3 | Box, 4 | Flex, 5 | PropsOf, 6 | } from "@chakra-ui/react"; 7 | 8 | export const Page: React.FC> = ({ 9 | children, 10 | ...props 11 | }) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | 19 | 20 | export const Container: React.FC> = ({ 21 | children, 22 | ...props 23 | }) => { 24 | return ( 25 | 31 | {children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@chakra-ui/react"; 3 | 4 | import SimpleEditor from "react-simple-code-editor"; 5 | 6 | // Highlight.js setup 7 | import "highlight.js/styles/default.css"; 8 | import hljs from "highlight.js"; 9 | 10 | import * as Input from "./Input"; 11 | 12 | export const Editor = () => { 13 | const { contents, setContents } = Input.Container.useContainer(); 14 | 15 | return ( 16 | 20 | hljs.highlight(contents, { language: "json" }).value} 24 | style={{ 25 | fontFamily: "SFMono-Regular,Menlo,Monaco,Consolas,monospace" 26 | 27 | }} 28 | /> 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/Examples.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createContainer } from "unstated-next"; 3 | 4 | import * as Input from "./Input"; 5 | import { Options } from "../solidity"; 6 | import ENS from "./examples/ENS.abi.json"; 7 | import DepositContract from "./examples/DepositContract.abi.json"; 8 | import UniswapV2RouterO2 from "./examples/UniswapV2Router02.abi.json"; 9 | import AirSwap from "./examples/AirSwap.abi.json"; 10 | import BunchaStructs from "./examples/BunchaStructs.abi.json"; 11 | import UDVTs from "./examples/UDVTs.abi.json"; 12 | 13 | export interface Example { 14 | name: string; 15 | license: string; 16 | contents: string; 17 | } 18 | 19 | export const examples: { [exampleName: string]: Example } = { 20 | ens: { 21 | name: "ENS", 22 | license: "BSD-2-Clause", 23 | contents: JSON.stringify(ENS, undefined, 2), 24 | }, 25 | eth2: { 26 | name: "Eth2Deposit", 27 | license: "CC0-1.0", 28 | contents: JSON.stringify(DepositContract, undefined, 2) 29 | }, 30 | uniswap: { 31 | name: "UniswapV2Router02", 32 | license: "GPL-3.0", 33 | contents: JSON.stringify(UniswapV2RouterO2, undefined, 2), 34 | }, 35 | airswap: { 36 | name: "AirSwap", 37 | license: "Apache-2.0", 38 | contents: JSON.stringify(AirSwap, undefined, 2) 39 | }, 40 | bunchastructs: { 41 | name: "BunchaStructs", 42 | license: "", 43 | contents: JSON.stringify(BunchaStructs, undefined, 2) 44 | }, 45 | udvts: { 46 | name: "UDVTs", 47 | license: "", 48 | contents: JSON.stringify(UDVTs, undefined, 2) 49 | } 50 | }; 51 | 52 | export type State = string | undefined; 53 | 54 | export const useExample = () => { 55 | const [state, setState] = React.useState(); 56 | const [locked, setLocked] = React.useState(false); 57 | const input = Input.Container.useContainer(); 58 | const options = Options.Container.useContainer(); 59 | 60 | const selectExample = (exampleName: string) => { 61 | if (exampleName === "") { 62 | setState(undefined); 63 | return; 64 | } 65 | 66 | const example = examples[exampleName]; 67 | 68 | if (!example) { 69 | throw new Error(`Unknown example: "${exampleName}"`); 70 | } 71 | 72 | setLocked(true); 73 | 74 | const { name, license, contents } = example; 75 | const { prettifyOutput, solidityVersion, setState: setOptions } = options; 76 | const { setContents } = input; 77 | 78 | console.debug("setting example %o", example); 79 | setState(exampleName); 80 | setOptions({ 81 | name: name, 82 | license: license, 83 | solidityVersion, 84 | prettifyOutput, 85 | }); 86 | setContents(contents); 87 | 88 | setLocked(false); 89 | }; 90 | 91 | React.useEffect(() => { 92 | if (locked) { 93 | console.debug("locked"); 94 | return; 95 | } 96 | 97 | if (!state) { 98 | return; 99 | } 100 | 101 | console.debug("state %o", state); 102 | const { name, license, contents } = examples[state]; 103 | const nameDiffers = name !== options.name; 104 | const licenseDiffers = license !== options.license; 105 | const contentsDiffer = contents !== input.contents; 106 | 107 | if (nameDiffers) { 108 | console.debug("name differs"); 109 | } 110 | if (licenseDiffers) { 111 | console.debug("license differs"); 112 | } 113 | if (contentsDiffer) { 114 | console.debug("contentsDiffer"); 115 | } 116 | 117 | if (nameDiffers || licenseDiffers || contentsDiffer) { 118 | setState(undefined); 119 | } 120 | }, [locked, state, input.contents, options.name, options.license]); 121 | 122 | return { selectedExample: state, selectExample }; 123 | }; 124 | 125 | export const Container = createContainer(useExample); 126 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | Box, 5 | HStack, 6 | Spacer, 7 | Heading, 8 | } from "@chakra-ui/react"; 9 | 10 | import { Status } from "./Status"; 11 | 12 | export const Header = () => { 13 | return ( 14 | 15 | ABI 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/Input.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createContainer } from "unstated-next"; 3 | import TruffleContractSchema from "@truffle/contract-schema"; 4 | 5 | import type { GenerateSolidityOptions } from "abi-to-sol"; 6 | 7 | import { forState } from "../makeSet"; 8 | 9 | export interface CommonState { 10 | contents: string; 11 | } 12 | 13 | export interface ReadingState { 14 | isReading: true; 15 | } 16 | 17 | export interface SuccessState { 18 | isReading: false; 19 | abi: GenerateSolidityOptions["abi"]; 20 | } 21 | 22 | export interface ErrorState { 23 | isReading: false; 24 | error: Error; 25 | } 26 | 27 | export type State = CommonState & (ReadingState | SuccessState | ErrorState); 28 | 29 | type ReadResult = Pick | Pick; 30 | 31 | export const useInput = (defaultContents: string = "") => { 32 | const [state, setState] = React.useState({ 33 | contents: defaultContents, 34 | isReading: true, 35 | }); 36 | 37 | const makeSet = forState({ state, setState }); 38 | 39 | const setContents = makeSet("contents"); 40 | 41 | React.useEffect(() => { 42 | // mark isReading 43 | setState({ 44 | contents: state.contents, 45 | isReading: true, 46 | }); 47 | 48 | // read 49 | const result = readContents(state.contents); 50 | 51 | // mark result 52 | setState( 53 | "abi" in result 54 | ? ({ 55 | contents: state.contents, 56 | isReading: false, 57 | abi: result.abi, 58 | } as const) 59 | : ({ 60 | contents: state.contents, 61 | isReading: false, 62 | error: result.error, 63 | } as const) 64 | ); 65 | }, [state.contents]); 66 | 67 | return { 68 | ...state, 69 | setContents, 70 | }; 71 | }; 72 | 73 | function readContents(contents: string): ReadResult { 74 | try { 75 | const abi = JSON.parse(contents); 76 | 77 | try { 78 | TruffleContractSchema.validate({ abi }); 79 | } catch (error) { 80 | console.error(error); 81 | throw error; 82 | } 83 | 84 | return { abi }; 85 | } catch (error) { 86 | return { error: error as Error }; 87 | } 88 | } 89 | 90 | export const Container = createContainer(useInput); 91 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/Status.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Center, 5 | Spinner, 6 | Text 7 | } from "@chakra-ui/react"; 8 | import { 9 | CheckCircleIcon, 10 | WarningIcon, 11 | } from "@chakra-ui/icons"; 12 | 13 | import * as Input from "./Input"; 14 | 15 | export const Status = () => { 16 | const { 17 | isReading, 18 | contents: _contents, 19 | ...result 20 | } = Input.Container.useContainer(); 21 | 22 | let component; 23 | if (isReading) { 24 | component = ( 25 | 26 | Reading ABI{" "} 27 | 28 | 29 | ) 30 | } else if ("abi" in result) { 31 | component = ( 32 | 33 | Valid{" "} 34 | 35 | 36 | ); 37 | } else { 38 | component = ( 39 | 40 | Invalid (maybe copy/paste again?){" "} 41 | 42 | 43 | ); 44 | } 45 | 46 | return ( 47 |
48 | {component} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/examples/AirSwap.abi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"wrapperSwapContract","type":"address"},{"internalType":"address","name":"wrapperWethContract","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"constant":false,"inputs":[{"components":[{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"components":[{"internalType":"bytes4","name":"kind","type":"bytes4"},{"internalType":"address","name":"wallet","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"internalType":"struct Types.Party","name":"signer","type":"tuple"},{"components":[{"internalType":"bytes4","name":"kind","type":"bytes4"},{"internalType":"address","name":"wallet","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"internalType":"struct Types.Party","name":"sender","type":"tuple"},{"components":[{"internalType":"bytes4","name":"kind","type":"bytes4"},{"internalType":"address","name":"wallet","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"internalType":"struct Types.Party","name":"affiliate","type":"tuple"},{"components":[{"internalType":"address","name":"signatory","type":"address"},{"internalType":"address","name":"validator","type":"address"},{"internalType":"bytes1","name":"version","type":"bytes1"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct Types.Signature","name":"signature","type":"tuple"}],"internalType":"struct Types.Order","name":"order","type":"tuple"},{"internalType":"contract IDelegate","name":"delegate","type":"address"}],"name":"provideDelegateOrder","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"components":[{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"components":[{"internalType":"bytes4","name":"kind","type":"bytes4"},{"internalType":"address","name":"wallet","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"internalType":"struct Types.Party","name":"signer","type":"tuple"},{"components":[{"internalType":"bytes4","name":"kind","type":"bytes4"},{"internalType":"address","name":"wallet","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"internalType":"struct Types.Party","name":"sender","type":"tuple"},{"components":[{"internalType":"bytes4","name":"kind","type":"bytes4"},{"internalType":"address","name":"wallet","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"}],"internalType":"struct Types.Party","name":"affiliate","type":"tuple"},{"components":[{"internalType":"address","name":"signatory","type":"address"},{"internalType":"address","name":"validator","type":"address"},{"internalType":"bytes1","name":"version","type":"bytes1"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct Types.Signature","name":"signature","type":"tuple"}],"internalType":"struct Types.Order","name":"order","type":"tuple"}],"name":"swap","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"swapContract","outputs":[{"internalType":"contract ISwap","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"wethContract","outputs":[{"internalType":"contract IWETH","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}] 2 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/examples/BunchaStructs.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "components": [ 7 | { 8 | "internalType": "uint256", 9 | "name": "a", 10 | "type": "uint256" 11 | } 12 | ], 13 | "indexed": false, 14 | "internalType": "struct BunchaStructs.A[]", 15 | "name": "a", 16 | "type": "tuple[]" 17 | }, 18 | { 19 | "components": [ 20 | { 21 | "components": [ 22 | { 23 | "components": [ 24 | { 25 | "components": [ 26 | { 27 | "internalType": "address", 28 | "name": "e", 29 | "type": "address" 30 | } 31 | ], 32 | "internalType": "struct Other.E", 33 | "name": "e", 34 | "type": "tuple" 35 | }, 36 | { 37 | "internalType": "string", 38 | "name": "d", 39 | "type": "string" 40 | } 41 | ], 42 | "internalType": "struct C[]", 43 | "name": "c", 44 | "type": "tuple[]" 45 | } 46 | ], 47 | "internalType": "struct B", 48 | "name": "b", 49 | "type": "tuple" 50 | }, 51 | { 52 | "components": [ 53 | { 54 | "internalType": "address", 55 | "name": "e", 56 | "type": "address" 57 | } 58 | ], 59 | "internalType": "struct Other.E", 60 | "name": "e", 61 | "type": "tuple" 62 | } 63 | ], 64 | "indexed": false, 65 | "internalType": "struct Other.D", 66 | "name": "d", 67 | "type": "tuple" 68 | } 69 | ], 70 | "name": "Event", 71 | "type": "event" 72 | } 73 | ] 74 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/examples/DepositContract.abi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"pubkey","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"withdrawal_credentials","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"amount","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"signature","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"index","type":"bytes"}],"name":"DepositEvent","type":"event"},{"inputs":[{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"bytes","name":"withdrawal_credentials","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"bytes32","name":"deposit_data_root","type":"bytes32"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"get_deposit_count","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"get_deposit_root","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}] 2 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/examples/ENS.abi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"contract ENS","name":"_old","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"NewOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"resolver","type":"address"}],"name":"NewResolver","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"NewTTL","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"Transfer","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"old","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"recordExists","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"resolver","type":"address"}],"name":"setResolver","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setSubnodeOwner","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setSubnodeRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setTTL","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"ttl","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"}] 2 | 3 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/examples/UDVTs.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "Int", 6 | "name": "i", 7 | "type": "int256" 8 | }, 9 | { 10 | "internalType": "UDVTs.Uint", 11 | "name": "u", 12 | "type": "uint256" 13 | }, 14 | { 15 | "internalType": "Other.Bool", 16 | "name": "b", 17 | "type": "bool" 18 | } 19 | ], 20 | "name": "fnoo", 21 | "outputs": [], 22 | "stateMutability": "pure", 23 | "type": "function" 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /packages/web-ui/src/abi/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from "./Header"; 2 | export * as Input from "./Input"; 3 | export { Editor } from "./Editor"; 4 | export * as Examples from "./Examples"; 5 | -------------------------------------------------------------------------------- /packages/web-ui/src/defaultAbi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"amountADesired","type":"uint256"},{"internalType":"uint256","name":"amountBDesired","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountTokenDesired","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsIn","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"reserveA","type":"uint256"},{"internalType":"uint256","name":"reserveB","type":"uint256"}],"name":"quote","outputs":[{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETHSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermit","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermitSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityWithPermit","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapETHForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 2 | 3 | -------------------------------------------------------------------------------- /packages/web-ui/src/highlightjs-solidity.d.ts: -------------------------------------------------------------------------------- 1 | declare module "highlightjs-solidity" { 2 | export = {} as any; 3 | } 4 | -------------------------------------------------------------------------------- /packages/web-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /packages/web-ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/web-ui/src/makeSet.ts: -------------------------------------------------------------------------------- 1 | export interface ForStateOptions { 2 | state: State; 3 | setState(newState: State): void; 4 | } 5 | 6 | /** 7 | * Predicate that says whether a value should be interpreted as undefined 8 | */ 9 | export type IsEmpty = ( 10 | value: State[N] 11 | ) => boolean; 12 | 13 | export type MakeSet = ( 14 | propertyName: N, 15 | isEmpty?: IsEmpty 16 | ) => (newValue: State[N]) => void; 17 | 18 | export const forState = 19 | ({ state, setState }: ForStateOptions): MakeSet => 20 | (propertyName, isEmpty) => 21 | (value) => { 22 | const newState = { 23 | ...state, 24 | }; 25 | 26 | if (isEmpty && isEmpty(value)) { 27 | delete newState[propertyName]; 28 | } else { 29 | newState[propertyName] = value; 30 | } 31 | 32 | setState(newState); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/web-ui/src/prettier-plugin-solidity.d.ts: -------------------------------------------------------------------------------- 1 | declare module "prettier-plugin-solidity" { 2 | export = {} as any; 3 | } 4 | -------------------------------------------------------------------------------- /packages/web-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/web-ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /packages/web-ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/Code.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, AlertIcon, AlertTitle, AlertDescription, Box } from "@chakra-ui/react"; 3 | 4 | import * as Output from "./Output"; 5 | 6 | // Highlight.js setup 7 | import "highlight.js/styles/default.css"; 8 | import hljs from "highlight.js"; 9 | import hljsDefineSolidity from "highlightjs-solidity"; 10 | hljsDefineSolidity(hljs); 11 | hljs.initHighlightingOnLoad(); 12 | 13 | 14 | export const Code = () => { 15 | const { isGenerating, ...result } = Output.Container.useContainer(); 16 | 17 | const [html, setHtml] = React.useState(""); 18 | const [showHtml, setShowHtml] = React.useState(false); 19 | 20 | const error = "error" in result && result.error; 21 | 22 | React.useEffect(() => { 23 | if (isGenerating) { 24 | setShowHtml(false); 25 | return; 26 | } 27 | 28 | if ("contents" in result) { 29 | const { contents } = result; 30 | 31 | try { 32 | setHtml(hljs.highlight(contents, { language: "solidity" }).value); 33 | setShowHtml(true); 34 | } catch { 35 | setHtml(contents); 36 | setShowHtml(true); 37 | } 38 | 39 | return; 40 | } 41 | 42 | setShowHtml(false); 43 | 44 | }, [isGenerating, result]); 45 | 46 | return ( 47 | 48 | {error && ( 49 | 50 | 51 | Could not generate Solidity! 52 | {error.message} 53 | 54 | )} 55 | {showHtml && ( 56 |
59 |       )}
60 |     
61 |   )
62 | }
63 | 
64 | 


--------------------------------------------------------------------------------
/packages/web-ui/src/solidity/Controls.tsx:
--------------------------------------------------------------------------------
 1 | import React from "react";
 2 | import {
 3 |   Box,
 4 |   HStack,
 5 |   Stack,
 6 |   FormLabel,
 7 |   Center,
 8 |   Spacer,
 9 |   Input,
10 |   Switch,
11 |   InputGroup,
12 |   InputLeftElement,
13 | } from "@chakra-ui/react";
14 | import { LockIcon } from "@chakra-ui/icons";
15 | 
16 | import * as Output from "./Output";
17 | import * as Options from "./Options";
18 | import { CopyButton } from "../CopyButton";
19 | 
20 | export const Controls = () => {
21 |   const { isGenerating, ...result } = Output.Container.useContainer();
22 |   const {
23 |     license,
24 |     prettifyOutput,
25 |     setLicense,
26 |     setPrettifyOutput
27 |   } = Options.Container.useContainer();
28 | 
29 |   return (
30 |     
35 |       
36 |         License
37 |         
38 |           }
41 |           />
42 |           ) => {
48 |               setLicense(event.target.value)
49 |             }}
50 |           />
51 |         
52 |       
53 |       
54 |         Prettify output?
55 |         
56 | { 60 | setPrettifyOutput(!prettifyOutput); 61 | }} 62 | /> 63 |
64 |
65 | 66 | 67 | 68 | 75 | 76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | Box, 5 | HStack, 6 | Spacer, 7 | Heading, 8 | } from "@chakra-ui/react"; 9 | 10 | import { Status } from "./Status"; 11 | 12 | export const Header = () => { 13 | return ( 14 | 15 | Solidity 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/Options.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createContainer } from "unstated-next"; 3 | 4 | import type { GenerateSolidityOptions } from "abi-to-sol"; 5 | import { forState } from "../makeSet"; 6 | 7 | export type State = Pick< 8 | GenerateSolidityOptions, 9 | "name" | "solidityVersion" | "license" 10 | > & { 11 | prettifyOutput: boolean; 12 | }; 13 | 14 | export function useOptions() { 15 | const [state, setState] = React.useState({ 16 | prettifyOutput: true, 17 | }); 18 | 19 | const makeSet = forState({ state, setState }); 20 | 21 | return { 22 | ...state, 23 | setName: makeSet("name", (value) => value === ""), 24 | setSolidityVersion: makeSet("solidityVersion", (value) => value === ""), 25 | setLicense: makeSet("license", (value) => value === ""), 26 | setPrettifyOutput: makeSet("prettifyOutput"), 27 | setState, 28 | }; 29 | } 30 | 31 | export const Container = createContainer(useOptions); 32 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/OptionsControls.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Stack, 4 | HStack, 5 | FormLabel, 6 | Input, 7 | Select, 8 | InputGroup, 9 | InputLeftElement, 10 | } from "@chakra-ui/react"; 11 | import { InfoIcon } from "@chakra-ui/icons"; 12 | 13 | import * as Options from "./Options"; 14 | import * as Examples from "../abi/Examples"; 15 | import { examples } from "../abi/Examples"; 16 | import { defaults } from "abi-to-sol"; 17 | 18 | 19 | export const OptionsControls = () => { 20 | const { 21 | name, 22 | setName, 23 | setSolidityVersion, 24 | ...options 25 | } = Options.Container.useContainer(); 26 | 27 | const { 28 | selectedExample, 29 | selectExample 30 | } = Examples.Container.useContainer(); 31 | 32 | console.debug("name %s", name); 33 | 34 | return ( 35 | 36 | 37 | Interface name 38 | 39 | } 42 | /> 43 | ) => { 49 | console.debug("changing name via form"); 50 | setName(event.target.value); 51 | }} 52 | /> 53 | 54 | 55 | 56 | Solidity version 57 | 72 | 73 | 74 | See example 75 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/Output.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createContainer } from "unstated-next"; 3 | 4 | import { useToast } from "@chakra-ui/react"; 5 | 6 | import prettierSolidity from "prettier-plugin-solidity"; 7 | import { generateSolidity } from "abi-to-sol"; 8 | import * as Options from "./Options"; 9 | import * as Abi from "../abi"; 10 | 11 | // @ts-ignore 12 | const prettier = window.prettier; 13 | 14 | export interface GeneratingState { 15 | isGenerating: true; 16 | } 17 | 18 | export interface NoInputState { 19 | isGenerating: false; 20 | } 21 | 22 | export interface SuccessState { 23 | isGenerating: false; 24 | contents: string; 25 | } 26 | 27 | export interface ErrorState { 28 | isGenerating: false; 29 | error: Error; 30 | } 31 | 32 | export type State = GeneratingState | NoInputState | SuccessState | ErrorState; 33 | 34 | export const useOutput = (defaultContents: string = "") => { 35 | const toast = useToast(); 36 | 37 | const [state, setState] = React.useState({ 38 | isGenerating: true, 39 | }); 40 | 41 | const { 42 | name, 43 | license, 44 | solidityVersion, 45 | prettifyOutput, 46 | setPrettifyOutput, 47 | } = Options.Container.useContainer(); 48 | 49 | const abiInput = Abi.Input.Container.useContainer() as Abi.Input.State; 50 | 51 | React.useEffect(() => { 52 | setState({ isGenerating: true }); 53 | 54 | if (abiInput.isReading || !("abi" in abiInput)) { 55 | console.debug("waiting for input"); 56 | setState({ 57 | isGenerating: false, 58 | }); 59 | return; 60 | } 61 | 62 | const { abi } = abiInput; 63 | 64 | try { 65 | console.info("generating solidity"); 66 | const unformatted = generateSolidity({ 67 | abi, 68 | name, 69 | license, 70 | solidityVersion, 71 | prettifyOutput: false, 72 | }); 73 | 74 | if (prettifyOutput) { 75 | try { 76 | const formatted = prettier.format(unformatted, { 77 | plugins: [prettierSolidity], 78 | parser: "solidity-parse", 79 | }); 80 | 81 | setState({ 82 | isGenerating: false, 83 | contents: formatted, 84 | }); 85 | } catch { 86 | setPrettifyOutput(false); 87 | 88 | setState({ 89 | isGenerating: false, 90 | contents: unformatted, 91 | }); 92 | } 93 | } else { 94 | setState({ 95 | isGenerating: false, 96 | contents: unformatted, 97 | }); 98 | } 99 | } catch (error) { 100 | setState({ 101 | isGenerating: false, 102 | error: error as Error, 103 | }); 104 | } 105 | }, [ 106 | abiInput, 107 | license, 108 | name, 109 | prettifyOutput, 110 | solidityVersion, 111 | setPrettifyOutput, 112 | ]); 113 | 114 | // const abi = "abi" in abiInput && abiInput.abi; 115 | const contents = useDebounce( 116 | "contents" in state ? state.contents : undefined, 117 | 500 118 | ); 119 | const [previousContents, setPreviousContents] = React.useState< 120 | string | undefined 121 | >(undefined); 122 | React.useEffect(() => { 123 | if (contents && contents !== previousContents) { 124 | const toastOptions = { 125 | title: "Generated Solidity", 126 | status: "success", 127 | position: "bottom-right", 128 | duration: 1000, 129 | } as const; 130 | 131 | toast(toastOptions); 132 | } 133 | setPreviousContents(contents); 134 | }, [contents, previousContents, toast]); 135 | 136 | return state; 137 | }; 138 | 139 | export const Container = createContainer(useOutput); 140 | 141 | function useDebounce(value: T, delay: number): T { 142 | // State and setters for debounced value 143 | const [debouncedValue, setDebouncedValue] = React.useState(value); 144 | React.useEffect( 145 | () => { 146 | // Update debounced value after delay 147 | const handler = setTimeout(() => { 148 | setDebouncedValue(value); 149 | }, delay); 150 | // Cancel the timeout if value changes (also on delay change or unmount) 151 | // This is how we prevent debounced value from updating if value is changed ... 152 | // .. within the delay period. Timeout gets cleared and restarted. 153 | return () => { 154 | clearTimeout(handler); 155 | }; 156 | }, 157 | [value, delay] // Only re-call effect if value or delay changes 158 | ); 159 | return debouncedValue; 160 | } 161 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/Status.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Center, 5 | Spinner, 6 | Text 7 | } from "@chakra-ui/react"; 8 | import { 9 | CheckCircleIcon, 10 | QuestionIcon, 11 | WarningIcon, 12 | } from "@chakra-ui/icons"; 13 | 14 | import * as Output from "./Output"; 15 | 16 | export const Status = () => { 17 | const { 18 | isGenerating, 19 | ...result 20 | } = Output.Container.useContainer(); 21 | 22 | let component; 23 | if (isGenerating) { 24 | component = ( 25 | 26 | Generating{" "} 27 | 28 | 29 | ) 30 | } else if ("contents" in result) { 31 | component = ( 32 | 33 | Success{" "} 34 | 35 | 36 | ); 37 | } else if ("error" in result) { 38 | component = ( 39 | 40 | Error{" "} 41 | 42 | 43 | ); 44 | } else { 45 | component = ( 46 | 47 | Waiting for input{" "} 48 | 49 | 50 | ); 51 | } 52 | 53 | return ( 54 |
55 | {component} 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /packages/web-ui/src/solidity/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from "./Header"; 2 | export { Controls } from "./Controls"; 3 | export { OptionsControls } from "./OptionsControls"; 4 | export { Code } from "./Code"; 5 | export * as Options from "./Options"; 6 | export * as Output from "./Output"; 7 | -------------------------------------------------------------------------------- /packages/web-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------