From e07d0f0cb7abf5395569a55da6596ba10d98850b Mon Sep 17 00:00:00 2001 From: PG Herveou Date: Wed, 30 Apr 2025 17:24:52 +0200 Subject: [PATCH] Move @parity/resolc from js-revive (#296) - Move npm package from paritytech/js-revive - Rename package to `@parity/resolc` --- .cargo/config.toml | 4 +- .github/workflows/release.yml | 27 +++ .github/workflows/test-wasm.yml | 19 +- .gitignore | 1 + .prettierrc.json | 6 + Makefile | 5 +- eslint.config.mjs | 11 + js/{ => emscripten}/build.js | 2 +- js/{ => emscripten}/e2e/web.test.js | 0 js/{ => emscripten}/embed/pre.js | 0 .../embed/soljson_interface.js | 0 js/emscripten/examples/node/resolc.js | 1 + js/emscripten/examples/node/resolc.wasm | 1 + js/{ => emscripten}/examples/node/revive.js | 0 .../examples/node/run_revive.js | 0 js/{ => emscripten}/examples/web/index.html | 0 js/emscripten/examples/web/resolc.wasm | 1 + js/emscripten/examples/web/resolc_web.js | 1 + js/{ => emscripten}/examples/web/worker.js | 0 js/emscripten/fixtures/instantiate.json | 19 ++ .../fixtures/instantiate_tokens.json | 4 +- .../fixtures/invalid_contract_content.json | 0 .../fixtures/missing_import.json | 0 js/{ => emscripten}/fixtures/storage.json | 0 js/{ => emscripten}/fixtures/token.json | 0 js/{ => emscripten}/package.json | 0 js/{ => emscripten}/playwright.config.js | 0 js/{ => emscripten}/tests/node.test.mjs | 43 ++-- js/examples/node/resolc.js | 1 - js/examples/node/resolc.wasm | 1 - js/examples/web/resolc.wasm | 1 - js/examples/web/resolc_web.js | 1 - js/fixtures/instantiate.json | 20 -- js/resolc/README.md | 17 ++ js/resolc/fixtures/bad_pragma.sol | 10 + js/resolc/fixtures/storage.sol | 28 +++ js/resolc/fixtures/storage_bad.sol | 26 +++ js/resolc/fixtures/token.sol | 22 ++ js/resolc/package.json | 37 ++++ js/resolc/src/bin.ts | 203 ++++++++++++++++++ js/resolc/src/index.test.ts | 115 ++++++++++ js/resolc/src/index.ts | 202 +++++++++++++++++ js/resolc/src/resolc.ts | 30 +++ js/resolc/src/resolc/.gitkeep | 0 js/resolc/src/resolc/resolc.js | 1 + js/resolc/src/resolc/resolc.wasm | 1 + js/resolc/src/solc.d.ts | 95 ++++++++ js/resolc/tsconfig.json | 27 +++ package.json | 32 +-- 49 files changed, 940 insertions(+), 75 deletions(-) create mode 100644 .prettierrc.json create mode 100644 eslint.config.mjs rename js/{ => emscripten}/build.js (96%) rename js/{ => emscripten}/e2e/web.test.js (100%) rename js/{ => emscripten}/embed/pre.js (100%) rename js/{ => emscripten}/embed/soljson_interface.js (100%) create mode 120000 js/emscripten/examples/node/resolc.js create mode 120000 js/emscripten/examples/node/resolc.wasm rename js/{ => emscripten}/examples/node/revive.js (100%) rename js/{ => emscripten}/examples/node/run_revive.js (100%) rename js/{ => emscripten}/examples/web/index.html (100%) create mode 120000 js/emscripten/examples/web/resolc.wasm create mode 120000 js/emscripten/examples/web/resolc_web.js rename js/{ => emscripten}/examples/web/worker.js (100%) create mode 100644 js/emscripten/fixtures/instantiate.json rename js/{ => emscripten}/fixtures/instantiate_tokens.json (99%) rename js/{ => emscripten}/fixtures/invalid_contract_content.json (100%) rename js/{ => emscripten}/fixtures/missing_import.json (100%) rename js/{ => emscripten}/fixtures/storage.json (100%) rename js/{ => emscripten}/fixtures/token.json (100%) rename js/{ => emscripten}/package.json (100%) rename js/{ => emscripten}/playwright.config.js (100%) rename js/{ => emscripten}/tests/node.test.mjs (85%) delete mode 120000 js/examples/node/resolc.js delete mode 120000 js/examples/node/resolc.wasm delete mode 120000 js/examples/web/resolc.wasm delete mode 120000 js/examples/web/resolc_web.js delete mode 100644 js/fixtures/instantiate.json create mode 100644 js/resolc/README.md create mode 100644 js/resolc/fixtures/bad_pragma.sol create mode 100644 js/resolc/fixtures/storage.sol create mode 100644 js/resolc/fixtures/storage_bad.sol create mode 100644 js/resolc/fixtures/token.sol create mode 100644 js/resolc/package.json create mode 100755 js/resolc/src/bin.ts create mode 100644 js/resolc/src/index.test.ts create mode 100644 js/resolc/src/index.ts create mode 100644 js/resolc/src/resolc.ts create mode 100644 js/resolc/src/resolc/.gitkeep create mode 120000 js/resolc/src/resolc/resolc.js create mode 120000 js/resolc/src/resolc/resolc.wasm create mode 100644 js/resolc/src/solc.d.ts create mode 100644 js/resolc/tsconfig.json diff --git a/.cargo/config.toml b/.cargo/config.toml index 8dec717..2ed5838 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,8 +10,8 @@ rustflags = [ "-Clink-arg=-sWASM_ASYNC_COMPILATION=0", "-Clink-arg=-sDYNAMIC_EXECUTION=0", "-Clink-arg=-sALLOW_TABLE_GROWTH=1", - "-Clink-arg=--js-library=js/embed/soljson_interface.js", - "-Clink-arg=--pre-js=js/embed/pre.js", + "-Clink-arg=--js-library=js/emscripten/embed/soljson_interface.js", + "-Clink-arg=--pre-js=js/emscripten/embed/pre.js", "-Clink-arg=-sSTACK_SIZE=128kb", "-Clink-arg=-sNODEJS_CATCH_EXIT=0" ] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1110ac..a91d4bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -286,3 +286,30 @@ jobs: resolc.wasm resolc_web.js checksums.txt + + npm-release: + needs: [create-release] + runs-on: macos-14 + environment: tags + steps: + - name: Build + run: npm -w js/resolc run build + + - name: Set version + run: npm -w js/resolc version --no-git-tag-version ${{github.event.release.tag_name}} + + - name: npm pack + run: npm -w js/resolc pack + + - uses: actions/upload-artifact@v4 + with: + name: package + path: "parity-resolc-*.tgz" + + - uses: octokit/request-action@bbedc70b1981e610d89f1f8de88311a1fc02fb83 + with: + route: POST /repos/paritytech/npm_publish_automation/actions/workflows/publish.yml/dispatches + ref: main + inputs: '${{ format(''{{ "repo": "{0}", "run_id": "{1}" }}'', github.repository, github.run_id) }}' + env: + GITHUB_TOKEN: ${{ secrets.NPM_PUBLISH_AUTOMATION_TOKEN }} diff --git a/.github/workflows/test-wasm.yml b/.github/workflows/test-wasm.yml index 28ffc5f..7beb396 100644 --- a/.github/workflows/test-wasm.yml +++ b/.github/workflows/test-wasm.yml @@ -86,13 +86,18 @@ jobs: - name: Install Node Packages run: npm install - - name: Run Playwright tests - run: | - cd js - npx playwright install --with-deps - npx playwright test - - - name: Test revive + - name: Test emscripten run: | echo "Running tests for ${{ matrix.os }}" npm run test:wasm + + - name: Test @parity/resolc + run: | + echo "Running tests for ${{ matrix.os }}" + npm run -w js/resolc test + + - name: Run Playwright tests + run: | + cd js/emscripten + npx playwright install --with-deps + npx playwright test diff --git a/.gitignore b/.gitignore index dc2bedc..c1773ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/js/resolc/dist target-llvm *.dot .vscode/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f6ca428 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": false +} diff --git a/Makefile b/Makefile index 6f7fb8c..4e92847 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,4 @@ clean: rm -rf node_modules ; \ rm -rf crates/solidity/src/tests/cli-tests/artifacts ; \ cargo uninstall revive-solidity ; \ - cargo uninstall revive-llvm-builder ; \ - rm -f package-lock.json ; \ - rm -rf js/dist ; \ - rm -f js/src/resolc.{wasm,js} + cargo uninstall revive-llvm-builder ; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..2db5ff9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,11 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { files: ['**/*.{mjs,ts}'] }, + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +] diff --git a/js/build.js b/js/emscripten/build.js similarity index 96% rename from js/build.js rename to js/emscripten/build.js index e2f5e5f..7eebbc2 100644 --- a/js/build.js +++ b/js/emscripten/build.js @@ -8,7 +8,7 @@ const RESOLC_WASM_URI = process.env.RELEASE_RESOLC_WASM_URI || "http://127.0.0.1:8080/resolc.wasm"; const RESOLC_WASM_TARGET_DIR = path.join( __dirname, - "../target/wasm32-unknown-emscripten/release", + "../../target/wasm32-unknown-emscripten/release", ); const RESOLC_JS = path.join(RESOLC_WASM_TARGET_DIR, "resolc.js"); const RESOLC_WEB_JS = path.join(RESOLC_WASM_TARGET_DIR, "resolc_web.js"); diff --git a/js/e2e/web.test.js b/js/emscripten/e2e/web.test.js similarity index 100% rename from js/e2e/web.test.js rename to js/emscripten/e2e/web.test.js diff --git a/js/embed/pre.js b/js/emscripten/embed/pre.js similarity index 100% rename from js/embed/pre.js rename to js/emscripten/embed/pre.js diff --git a/js/embed/soljson_interface.js b/js/emscripten/embed/soljson_interface.js similarity index 100% rename from js/embed/soljson_interface.js rename to js/emscripten/embed/soljson_interface.js diff --git a/js/emscripten/examples/node/resolc.js b/js/emscripten/examples/node/resolc.js new file mode 120000 index 0000000..8bbe709 --- /dev/null +++ b/js/emscripten/examples/node/resolc.js @@ -0,0 +1 @@ +../../../../target/wasm32-unknown-emscripten/release/resolc.js \ No newline at end of file diff --git a/js/emscripten/examples/node/resolc.wasm b/js/emscripten/examples/node/resolc.wasm new file mode 120000 index 0000000..bec2fc2 --- /dev/null +++ b/js/emscripten/examples/node/resolc.wasm @@ -0,0 +1 @@ +../../../../target/wasm32-unknown-emscripten/release/resolc.wasm \ No newline at end of file diff --git a/js/examples/node/revive.js b/js/emscripten/examples/node/revive.js similarity index 100% rename from js/examples/node/revive.js rename to js/emscripten/examples/node/revive.js diff --git a/js/examples/node/run_revive.js b/js/emscripten/examples/node/run_revive.js similarity index 100% rename from js/examples/node/run_revive.js rename to js/emscripten/examples/node/run_revive.js diff --git a/js/examples/web/index.html b/js/emscripten/examples/web/index.html similarity index 100% rename from js/examples/web/index.html rename to js/emscripten/examples/web/index.html diff --git a/js/emscripten/examples/web/resolc.wasm b/js/emscripten/examples/web/resolc.wasm new file mode 120000 index 0000000..bec2fc2 --- /dev/null +++ b/js/emscripten/examples/web/resolc.wasm @@ -0,0 +1 @@ +../../../../target/wasm32-unknown-emscripten/release/resolc.wasm \ No newline at end of file diff --git a/js/emscripten/examples/web/resolc_web.js b/js/emscripten/examples/web/resolc_web.js new file mode 120000 index 0000000..ddd9b6b --- /dev/null +++ b/js/emscripten/examples/web/resolc_web.js @@ -0,0 +1 @@ +../../../../target/wasm32-unknown-emscripten/release/resolc_web.js \ No newline at end of file diff --git a/js/examples/web/worker.js b/js/emscripten/examples/web/worker.js similarity index 100% rename from js/examples/web/worker.js rename to js/emscripten/examples/web/worker.js diff --git a/js/emscripten/fixtures/instantiate.json b/js/emscripten/fixtures/instantiate.json new file mode 100644 index 0000000..0e59398 --- /dev/null +++ b/js/emscripten/fixtures/instantiate.json @@ -0,0 +1,19 @@ +{ + "language": "Solidity", + "sources": { + "fixtures/instantiate.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.8.2 <0.9.0;\ncontract ChildContract {\n constructor() {\n }\n}\ncontract MainContract {\n constructor() {\n ChildContract newContract = new ChildContract();\n }\n}" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "outputSelection": { + "*": { + "*": ["abi"] + } + } + } +} diff --git a/js/fixtures/instantiate_tokens.json b/js/emscripten/fixtures/instantiate_tokens.json similarity index 99% rename from js/fixtures/instantiate_tokens.json rename to js/emscripten/fixtures/instantiate_tokens.json index 9e8dce6..773f654 100644 --- a/js/fixtures/instantiate_tokens.json +++ b/js/emscripten/fixtures/instantiate_tokens.json @@ -72,9 +72,7 @@ }, "outputSelection": { "*": { - "*": [ - "abi" - ] + "*": ["abi"] } } } diff --git a/js/fixtures/invalid_contract_content.json b/js/emscripten/fixtures/invalid_contract_content.json similarity index 100% rename from js/fixtures/invalid_contract_content.json rename to js/emscripten/fixtures/invalid_contract_content.json diff --git a/js/fixtures/missing_import.json b/js/emscripten/fixtures/missing_import.json similarity index 100% rename from js/fixtures/missing_import.json rename to js/emscripten/fixtures/missing_import.json diff --git a/js/fixtures/storage.json b/js/emscripten/fixtures/storage.json similarity index 100% rename from js/fixtures/storage.json rename to js/emscripten/fixtures/storage.json diff --git a/js/fixtures/token.json b/js/emscripten/fixtures/token.json similarity index 100% rename from js/fixtures/token.json rename to js/emscripten/fixtures/token.json diff --git a/js/package.json b/js/emscripten/package.json similarity index 100% rename from js/package.json rename to js/emscripten/package.json diff --git a/js/playwright.config.js b/js/emscripten/playwright.config.js similarity index 100% rename from js/playwright.config.js rename to js/emscripten/playwright.config.js diff --git a/js/tests/node.test.mjs b/js/emscripten/tests/node.test.mjs similarity index 85% rename from js/tests/node.test.mjs rename to js/emscripten/tests/node.test.mjs index 2ea3a73..ad3f9e5 100644 --- a/js/tests/node.test.mjs +++ b/js/emscripten/tests/node.test.mjs @@ -65,15 +65,15 @@ describe("Compile Function Tests", function () { expect(result).to.be.a("string"); const output = JSON.parse(result); expect(output).to.have.property("contracts"); - expect(output.contracts["fixtures/instantiate_tokens.sol"]).to.have.property( - "TokensFactory", - ); - expect(output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory).to.have.property( - "abi", - ); - expect(output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory).to.have.property( - "evm", - ); + expect( + output.contracts["fixtures/instantiate_tokens.sol"], + ).to.have.property("TokensFactory"); + expect( + output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory, + ).to.have.property("abi"); + expect( + output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory, + ).to.have.property("evm"); expect( output.contracts["fixtures/instantiate_tokens.sol"].TokensFactory.evm, ).to.have.property("bytecode"); @@ -117,27 +117,26 @@ describe("Compile Function Tests", function () { expect(output.contracts["fixtures/instantiate.sol"]).to.have.property( "ChildContract", ); - expect(output.contracts["fixtures/instantiate.sol"].ChildContract).to.have.property( - "abi", - ); - expect(output.contracts["fixtures/instantiate.sol"].ChildContract).to.have.property( - "evm", - ); + expect( + output.contracts["fixtures/instantiate.sol"].ChildContract, + ).to.have.property("abi"); + expect( + output.contracts["fixtures/instantiate.sol"].ChildContract, + ).to.have.property("evm"); expect( output.contracts["fixtures/instantiate.sol"].ChildContract.evm, ).to.have.property("bytecode"); expect(output.contracts["fixtures/instantiate.sol"]).to.have.property( "MainContract", ); - expect(output.contracts["fixtures/instantiate.sol"].MainContract).to.have.property( - "abi", - ); - expect(output.contracts["fixtures/instantiate.sol"].MainContract).to.have.property( - "evm", - ); + expect( + output.contracts["fixtures/instantiate.sol"].MainContract, + ).to.have.property("abi"); + expect( + output.contracts["fixtures/instantiate.sol"].MainContract, + ).to.have.property("evm"); expect( output.contracts["fixtures/instantiate.sol"].MainContract.evm, ).to.have.property("bytecode"); }); - }); diff --git a/js/examples/node/resolc.js b/js/examples/node/resolc.js deleted file mode 120000 index ed95f38..0000000 --- a/js/examples/node/resolc.js +++ /dev/null @@ -1 +0,0 @@ -../../../target/wasm32-unknown-emscripten/release/resolc.js \ No newline at end of file diff --git a/js/examples/node/resolc.wasm b/js/examples/node/resolc.wasm deleted file mode 120000 index 79f581c..0000000 --- a/js/examples/node/resolc.wasm +++ /dev/null @@ -1 +0,0 @@ -../../../target/wasm32-unknown-emscripten/release/resolc.wasm \ No newline at end of file diff --git a/js/examples/web/resolc.wasm b/js/examples/web/resolc.wasm deleted file mode 120000 index 79f581c..0000000 --- a/js/examples/web/resolc.wasm +++ /dev/null @@ -1 +0,0 @@ -../../../target/wasm32-unknown-emscripten/release/resolc.wasm \ No newline at end of file diff --git a/js/examples/web/resolc_web.js b/js/examples/web/resolc_web.js deleted file mode 120000 index 85640e9..0000000 --- a/js/examples/web/resolc_web.js +++ /dev/null @@ -1 +0,0 @@ -../../../target/wasm32-unknown-emscripten/release/resolc_web.js \ No newline at end of file diff --git a/js/fixtures/instantiate.json b/js/fixtures/instantiate.json deleted file mode 100644 index af790c9..0000000 --- a/js/fixtures/instantiate.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "language": "Solidity", - "sources": { - "fixtures/instantiate.sol": { - "content": "// SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.8.2 <0.9.0;\ncontract ChildContract {\n constructor() {\n }\n}\ncontract MainContract {\n constructor() {\n ChildContract newContract = new ChildContract();\n }\n}" - } - }, - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": ["abi"] - } - } - } - } - \ No newline at end of file diff --git a/js/resolc/README.md b/js/resolc/README.md new file mode 100644 index 0000000..80e0166 --- /dev/null +++ b/js/resolc/README.md @@ -0,0 +1,17 @@ +# Usage from Node.js + +```typescript +import { compile } from "@parity/resolc"; +const sources = { +["contracts/1_Storage.sol"]: { + content: readFileSync("fixtures/storage.sol", "utf8"), +} + +const out = await compile(sources); +``` + +# Usage from shell + +```bash + npx @parity/resolc@latest --bin contracts/1_Storage.sol +``` diff --git a/js/resolc/fixtures/bad_pragma.sol b/js/resolc/fixtures/bad_pragma.sol new file mode 100644 index 0000000..6fc8798 --- /dev/null +++ b/js/resolc/fixtures/bad_pragma.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^9999; + +/** + * @title Storage + * @dev Store & retrieve value in a variable + * @custom:dev-run-script ./scripts/deploy_with_ethers.ts + */ +contract BadPragma { +} diff --git a/js/resolc/fixtures/storage.sol b/js/resolc/fixtures/storage.sol new file mode 100644 index 0000000..e4797ea --- /dev/null +++ b/js/resolc/fixtures/storage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.2 <0.9.0; + +/** + * @title Storage + * @dev Store & retrieve value in a variable + * @custom:dev-run-script ./scripts/deploy_with_ethers.ts + */ +contract Storage { + + uint256 number; + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} diff --git a/js/resolc/fixtures/storage_bad.sol b/js/resolc/fixtures/storage_bad.sol new file mode 100644 index 0000000..d1b7697 --- /dev/null +++ b/js/resolc/fixtures/storage_bad.sol @@ -0,0 +1,26 @@ +// missing license and pragama +/** + * @title Storage + * @dev Store & retrieve value in a variable + * @custom:dev-run-script ./scripts/deploy_with_ethers.ts + */ +contract Storage { + + uint256 number; + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} diff --git a/js/resolc/fixtures/token.sol b/js/resolc/fixtures/token.sol new file mode 100644 index 0000000..5e70483 --- /dev/null +++ b/js/resolc/fixtures/token.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.22; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +contract MyToken is ERC20, Ownable, ERC20Permit { + constructor(address initialOwner) + ERC20("MyToken", "MTK") + Ownable(initialOwner) + ERC20Permit("MyToken") + { + _mint(msg.sender, 100 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} + diff --git a/js/resolc/package.json b/js/resolc/package.json new file mode 100644 index 0000000..d39fa65 --- /dev/null +++ b/js/resolc/package.json @@ -0,0 +1,37 @@ +{ + "name": "@parity/resolc", + "license": "Apache-2.0", + "version": "0.0.0-updated-via-gh-releases", + "author": "Parity (https://parity.io)", + "module": "index.ts", + "types": "./dist/index.d.ts", + "main": "./dist/index.js", + "bin": { + "resolc": "./dist/bin.js" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && cp src/resolc/** dist/resolc", + "test": "npm run build && node ./dist/index.test.js" + }, + "devDependencies": { + "@openzeppelin/contracts": "5.1.0", + "globals": "^15.12.0", + "typescript": "^5.6.3" + }, + "dependencies": { + "@types/node": "^22.9.0", + "commander": "^13.1.0", + "package-json": "^10.0.1", + "solc": "^0.8.29" + } +} diff --git a/js/resolc/src/bin.ts b/js/resolc/src/bin.ts new file mode 100755 index 0000000..21f432d --- /dev/null +++ b/js/resolc/src/bin.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env node + +import * as commander from 'commander' +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import * as resolc from '.' +import { SolcInput } from '.' + +async function main() { + // hold on to any exception handlers that existed prior to this script running, we'll be adding them back at the end + const originalUncaughtExceptionListeners = + process.listeners('uncaughtException') + // FIXME: remove annoying exception catcher of Emscripten + // see https://github.com/chriseth/browser-solidity/issues/167 + process.removeAllListeners('uncaughtException') + + const program = new commander.Command() + + program.name('solcjs') + program.version(resolc.version()) + program + .option('--bin', 'Binary of the contracts in hex.') + .option('--abi', 'ABI of the contracts.') + .option( + '--base-path ', + 'Root of the project source tree. ' + + 'The import callback will attempt to interpret all import paths as relative to this directory.' + ) + .option( + '--include-path ', + 'Extra source directories available to the import callback. ' + + 'When using a package manager to install libraries, use this option to specify directories where packages are installed. ' + + 'Can be used multiple times to provide multiple locations.' + ) + .option( + '-o, --output-dir ', + 'Output directory for the contracts.' + ) + .option('-p, --pretty-json', 'Pretty-print all JSON output.', false) + .option('-v, --verbose', 'More detailed console output.', false) + .argument('') + + program.parse(process.argv) + const options = program.opts<{ + verbose: boolean + abi: boolean + bin: boolean + outputDir?: string + prettyJson: boolean + basePath?: string + includePath?: string[] + }>() + const files: string[] = program.args + const destination = options.outputDir ?? '.' + + function abort(msg: string) { + console.error(msg || 'Error occurred') + process.exit(1) + } + + function withUnixPathSeparators(filePath: string) { + // On UNIX-like systems forward slashes in paths are just a part of the file name. + if (os.platform() !== 'win32') { + return filePath + } + + return filePath.replace(/\\/g, '/') + } + + function makeSourcePathRelativeIfPossible(sourcePath: string) { + const absoluteBasePath = options.basePath + ? path.resolve(options.basePath) + : path.resolve('.') + const absoluteIncludePaths = options.includePath + ? options.includePath.map((prefix: string) => { + return path.resolve(prefix) + }) + : [] + + // Compared to base path stripping logic in solc this is much simpler because path.resolve() + // handles symlinks correctly (does not resolve them except in work dir) and strips .. segments + // from paths going beyond root (e.g. `/../../a/b/c` -> `/a/b/c/`). It's simpler also because it + // ignores less important corner cases: drive letters are not stripped from absolute paths on + // Windows and UNC paths are not handled in a special way (at least on Linux). Finally, it has + // very little test coverage so there might be more differences that we are just not aware of. + const absoluteSourcePath = path.resolve(sourcePath) + + for (const absolutePrefix of [absoluteBasePath].concat( + absoluteIncludePaths + )) { + const relativeSourcePath = path.relative( + absolutePrefix, + absoluteSourcePath + ) + + if (!relativeSourcePath.startsWith('../')) { + return withUnixPathSeparators(relativeSourcePath) + } + } + + // File is not located inside base path or include paths so use its absolute path. + return withUnixPathSeparators(absoluteSourcePath) + } + + function toFormattedJson(input: T) { + return JSON.stringify(input, null, options.prettyJson ? 4 : 0) + } + + if (files.length === 0) { + console.error('Must provide a file') + process.exit(1) + } + + if (!(options.bin || options.abi)) { + abort('Invalid option selected, must specify either --bin or --abi') + } + + const sources: SolcInput = {} + + for (let i = 0; i < files.length; i++) { + try { + sources[makeSourcePathRelativeIfPossible(files[i])] = { + content: fs.readFileSync(files[i]).toString(), + } + } catch (e) { + abort('Error reading ' + files[i] + ': ' + e) + } + } + + if (options.verbose) { + console.log('>>> Compiling:\n' + toFormattedJson(sources) + '\n') + } + + const output = await resolc.compile(sources) + let hasError = false + + if (!output) { + abort('No output from compiler') + } else if (output.errors) { + for (const error in output.errors) { + const message = output.errors[error] + if (message.severity === 'warning') { + console.log(message.formattedMessage) + } else { + console.error(message.formattedMessage) + hasError = true + } + } + } + + fs.mkdirSync(destination, { recursive: true }) + + function writeFile(file: string, content: Buffer | string) { + file = path.join(destination, file) + fs.writeFile(file, content, function (err) { + if (err) { + console.error('Failed to write ' + file + ': ' + err) + } + }) + } + + for (const fileName in output.contracts) { + for (const contractName in output.contracts[fileName]) { + let contractFileName = fileName + ':' + contractName + contractFileName = contractFileName.replace(/[:./\\]/g, '_') + + if (options.bin) { + writeFile( + contractFileName + '.polkavm', + Buffer.from( + output.contracts[fileName][contractName].evm.bytecode + .object, + 'hex' + ) + ) + } + + if (options.abi) { + writeFile( + contractFileName + '.abi', + toFormattedJson( + output.contracts[fileName][contractName].abi + ) + ) + } + } + } + + // Put back original exception handlers. + originalUncaughtExceptionListeners.forEach(function (listener) { + process.addListener('uncaughtException', listener) + }) + + if (hasError) { + process.exit(1) + } +} + +main().catch((err) => { + console.error('Error:', err) + process.exit(1) +}) diff --git a/js/resolc/src/index.test.ts b/js/resolc/src/index.test.ts new file mode 100644 index 0000000..8ea3910 --- /dev/null +++ b/js/resolc/src/index.test.ts @@ -0,0 +1,115 @@ +import { test } from 'node:test' +import { readFileSync, existsSync } from 'node:fs' +import assert from 'node:assert' +import { compile, tryResolveImport } from '.' +import { resolve } from 'node:path' + + +const compileOptions = [{}] +if (existsSync('../../target/release/resolc')) { + compileOptions.push({ bin: '../../target/release/resolc' }) +} + +for (const options of compileOptions) { + test(`check Ok output with option ${JSON.stringify(options)}`, async () => { + const contract = 'fixtures/token.sol' + const sources = { + [contract]: { + content: readFileSync('fixtures/storage.sol', 'utf8'), + }, + } + + const out = await compile(sources, options) + assert(out.contracts[contract].Storage.abi != null) + assert(out.contracts[contract].Storage.evm.bytecode != null) + }) +} + +test('check Err output', async () => { + const sources = { + bad: { + content: readFileSync('fixtures/storage_bad.sol', 'utf8'), + }, + } + + const out = await compile(sources) + assert( + out?.errors?.[0].message.includes( + 'SPDX license identifier not provided in source file' + ) + ) + assert( + out?.errors?.[1].message.includes( + 'Source file does not specify required compiler version' + ) + ) +}) + +test('check Err from stderr', async () => { + const sources = { + bad: { + content: readFileSync('fixtures/bad_pragma.sol', 'utf8'), + }, + } + + try { + await compile(sources) + assert(false, 'Expected error') + } catch (error) { + assert( + String(error).includes( + 'Source file requires different compiler version' + ) + ) + } +}) + +test('resolve import', () => { + const cases = [ + // local + { + file: './fixtures/storage.sol', + expected: resolve('fixtures/storage.sol'), + }, + // scopped module with version + { + file: '@openzeppelin/contracts@5.1.0/token/ERC20/ERC20.sol', + expected: require.resolve('@openzeppelin/contracts/token/ERC20/ERC20.sol'), + }, + // scopped module without version + { + file: '@openzeppelin/contracts/token/ERC20/ERC20.sol', + expected: require.resolve('@openzeppelin/contracts/token/ERC20/ERC20.sol'), + }, + // scopped module with wrong version + { + file: '@openzeppelin/contracts@4.8.3/token/ERC20/ERC20.sol', + expected: `Error: Version mismatch: Specified @openzeppelin/contracts@4.8.3, but installed version is 5.1.0`, + }, + // module without version + { + file: '@openzeppelin/contracts/package.json', + expected: require.resolve('@openzeppelin/contracts/package.json'), + }, + // scopped module with version + { + file: '@openzeppelin/contracts@5.1.0/package.json', + expected: require.resolve('@openzeppelin/contracts/package.json'), + }, + ] + + for (const { file, expected } of cases) { + try { + const resolved = tryResolveImport(file) + assert( + resolved === expected, + `\nGot:\n${resolved}\nExpected:\n${expected}` + ) + } catch (error) { + assert( + String(error) == expected, + `\nGot:\n${String(error)}\nExpected:\n${expected}` + ) + } + } +}) diff --git a/js/resolc/src/index.ts b/js/resolc/src/index.ts new file mode 100644 index 0000000..7d17684 --- /dev/null +++ b/js/resolc/src/index.ts @@ -0,0 +1,202 @@ +import solc from 'solc' +import { spawn } from 'child_process' +import { resolc, version as resolcVersion } from './resolc' +import path from 'path' +import { existsSync, readFileSync } from 'fs' + +export type SolcInput = { + [contractName: string]: { + content: string + } +} + +export type SolcError = { + component: string + errorCode: string + formattedMessage: string + message: string + severity: string + sourceLocation?: { + file: string + start: number + end: number + } + type: string +} + +export type SolcOutput = { + contracts: { + [contractPath: string]: { + [contractName: string]: { + abi: Array<{ + name: string + inputs: Array<{ name: string; type: string }> + outputs: Array<{ name: string; type: string }> + stateMutability: string + type: string + }> + evm: { + bytecode: { object: string } + } + } + } + } + errors?: Array +} + +export function resolveInputs(sources: SolcInput): SolcInput { + const input = { + language: 'Solidity', + sources, + settings: { + outputSelection: { + '*': { + '*': ['evm.bytecode.object'], + }, + }, + }, + } + + const out = solc.compile(JSON.stringify(input), { + import: (path: string) => { + return { + contents: readFileSync(tryResolveImport(path), 'utf8'), + } + }, + }) + + const output = JSON.parse(out) as { + sources: { [fileName: string]: { id: number } } + errors: Array + } + + if (output.errors && Object.keys(output.sources).length === 0) { + throw new Error(output.errors[0].formattedMessage) + } + + return Object.fromEntries( + Object.keys(output.sources).map((fileName) => { + return [ + fileName, + sources[fileName] ?? { + content: readFileSync(tryResolveImport(fileName), 'utf8'), + }, + ] + }) + ) +} + +export function version(): string { + const v = resolcVersion() + return v.split(' ').pop() ?? v +} + +export async function compile( + sources: SolcInput, + option: { bin?: string } = {} +): Promise { + const input = JSON.stringify({ + language: 'Solidity', + sources: resolveInputs(sources), + settings: { + optimizer: { enabled: true, runs: 200 }, + outputSelection: { + '*': { + '*': ['abi'], + }, + }, + }, + }) + + if (option.bin) { + return compileWithBin(input, option.bin) + } + + return resolc(input) +} + +/** + * Resolve an import path to a file path. + * @param importPath - The import path to resolve. + */ +export function tryResolveImport(importPath: string) { + // resolve local path + if (existsSync(importPath)) { + return path.resolve(importPath) + } + + const importRegex = /^(@?[^@/]+(?:\/[^@/]+)?)(?:@([^/]+))?(\/.+)$/ + const match = importPath.match(importRegex) + + if (!match) { + throw new Error('Invalid import path format.') + } + + const basePackage = match[1] // "foo", "@scope/foo" + const specifiedVersion = match[2] // "1.2.3" (optional) + const relativePath = match[3] // "/path/to/file.sol" + + let packageJsonPath + try { + packageJsonPath = require.resolve( + path.join(basePackage, 'package.json') + ) + } catch { + throw new Error(`Could not resolve package ${basePackage}`) + } + + // Check if a version was specified and compare with the installed version + if (specifiedVersion) { + const installedVersion = JSON.parse( + readFileSync(packageJsonPath, 'utf-8') + ).version + + if (installedVersion !== specifiedVersion) { + throw new Error( + `Version mismatch: Specified ${basePackage}@${specifiedVersion}, but installed version is ${installedVersion}` + ) + } + } + + const packageRoot = path.dirname(packageJsonPath) + + // Construct full path to the requested file + const resolvedPath = path.join(packageRoot, relativePath) + if (existsSync(resolvedPath)) { + return resolvedPath + } else { + throw new Error(`Resolved path ${resolvedPath} does not exist.`) + } +} +function compileWithBin(input: string, bin: string): PromiseLike { + return new Promise((resolve, reject) => { + const process = spawn(bin, ['--standard-json']) + + let output = '' + let error = '' + + process.stdin.write(input) + process.stdin.end() + + process.stdout.on('data', (data) => { + output += data.toString() + }) + + process.stderr.on('data', (data) => { + error += data.toString() + }) + + process.on('close', (code) => { + if (code === 0) { + try { + const result: SolcOutput = JSON.parse(output) + resolve(result) + } catch { + reject(new Error(`Failed to parse output`)) + } + } else { + reject(new Error(`Process exited with code ${code}: ${error}`)) + } + }) + }) +} diff --git a/js/resolc/src/resolc.ts b/js/resolc/src/resolc.ts new file mode 100644 index 0000000..737cc15 --- /dev/null +++ b/js/resolc/src/resolc.ts @@ -0,0 +1,30 @@ +import soljson from 'solc/soljson' +import Resolc from './resolc/resolc' +import type { SolcOutput } from '.' + +export function resolc(input: string): SolcOutput { + const m = Resolc() as any // eslint-disable-line @typescript-eslint/no-explicit-any + m.soljson = soljson + m.writeToStdin(input) + m.callMain(['--standard-json']) + const err = m.readFromStderr() + + if (err) { + throw new Error(err) + } + + return JSON.parse(m.readFromStdout()) as SolcOutput +} + +export function version(): string { + const m = Resolc() as any // eslint-disable-line @typescript-eslint/no-explicit-any + m.soljson = soljson + m.callMain(['--version']) + const err = m.readFromStderr() + + if (err) { + throw new Error(err) + } + + return m.readFromStdout() +} diff --git a/js/resolc/src/resolc/.gitkeep b/js/resolc/src/resolc/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/js/resolc/src/resolc/resolc.js b/js/resolc/src/resolc/resolc.js new file mode 120000 index 0000000..8bbe709 --- /dev/null +++ b/js/resolc/src/resolc/resolc.js @@ -0,0 +1 @@ +../../../../target/wasm32-unknown-emscripten/release/resolc.js \ No newline at end of file diff --git a/js/resolc/src/resolc/resolc.wasm b/js/resolc/src/resolc/resolc.wasm new file mode 120000 index 0000000..bec2fc2 --- /dev/null +++ b/js/resolc/src/resolc/resolc.wasm @@ -0,0 +1 @@ +../../../../target/wasm32-unknown-emscripten/release/resolc.wasm \ No newline at end of file diff --git a/js/resolc/src/solc.d.ts b/js/resolc/src/solc.d.ts new file mode 100644 index 0000000..04b47c8 --- /dev/null +++ b/js/resolc/src/solc.d.ts @@ -0,0 +1,95 @@ +declare module 'solc/soljson' {} + +declare module 'solc' { + // Basic types for input/output handling + export interface CompileInput { + language: string + sources: { + [fileName: string]: { + content: string + } + } + settings?: { + optimizer?: { + enabled: boolean + runs: number + } + outputSelection: { + [fileName: string]: { + [contractName: string]: string[] + } + } + } + } + + export interface CompileOutput { + errors?: Array<{ + component: string + errorCode: string + formattedMessage: string + message: string + severity: string + sourceLocation?: { + file: string + start: number + end: number + } + type: string + }> + sources?: { + [fileName: string]: { + id: number + ast: object + } + } + contracts?: { + [fileName: string]: { + [contractName: string]: { + abi: object[] + evm: { + bytecode: { + object: string + sourceMap: string + linkReferences: { + [fileName: string]: { + [libraryName: string]: Array<{ + start: number + length: number + }> + } + } + } + deployedBytecode: { + object: string + sourceMap: string + linkReferences: { + [fileName: string]: { + [libraryName: string]: Array<{ + start: number + length: number + }> + } + } + } + } + } + } + } + } + + // Main exported functions + export function compile( + input: string | CompileInput, + options?: { + import: (path: string) => + | { + contents: string + error?: undefined + } + | { + error: string + contents?: undefined + } + } + ): string +} diff --git a/js/resolc/tsconfig.json b/js/resolc/tsconfig.json new file mode 100644 index 0000000..8c85d6c --- /dev/null +++ b/js/resolc/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true, + "moduleResolution": "node", + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "strict": true, + "noImplicitAny": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "useUnknownInCatchVariables": true, + "types": ["node"], + "typeRoots": ["../../node_modules/@types", "./src"], + "paths": { + "#src/*": ["./src/*"] + }, + "plugins": [] + }, + "include": ["src/**/*"] +} diff --git a/package.json b/package.json index c20e62c..1a86896 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,21 @@ { - "name": "root", - "private": true, - "scripts": { - "test:cli": "npm run test -w crates/solidity/src/tests/cli-tests", - "test:wasm": "npm run test:node -w js", - "build:package": "npm run build:package -w js" - }, - "workspaces": [ - "crates/solidity/src/tests/cli-tests", - "js" - ] -} \ No newline at end of file + "name": "root", + "private": true, + "scripts": { + "test:cli": "npm run test -w crates/solidity/src/tests/cli-tests", + "test:wasm": "npm run test:node -w js/emscripten", + "build:package": "npm run build:package -w js/emscripten", + "lint": "npx eslint 'js/**/*.{cjs,mjs,ts}' && npx prettier --check '**/*.{mjs,cjs,ts}'", + "lint:fix": "npx prettier --write '**/*.{mjs,cjs,ts}'" + }, + "workspaces": [ + "crates/solidity/src/tests/cli-tests", + "js/emscripten", + "js/resolc" + ], + "dependencies": { + "@eslint/js": "^9.14.0", + "eslint": "^9.14.0", + "typescript-eslint": "^8.13.0" + } +}