Merge branch 'master' into addLoc

pull/3202/head
Liana Husikyan 1 year ago committed by GitHub
commit e2bb45f0f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 220
      .circleci/config.yml
  2. 1
      .env
  3. 3
      .github/workflows/publish-action.yml
  4. 2
      .github/workflows/run-sut.yml
  5. 4
      .gitignore
  6. 2
      README.md
  7. 13
      apps/debugger/.babelrc
  8. 2
      apps/debugger/src/app/debugger.ts
  9. 1
      apps/debugger/src/index.ts
  10. 1
      apps/debugger/tsconfig.app.json
  11. 101
      apps/debugger/webpack.config.js
  12. 34
      apps/doc-gen/.eslintrc.json
  13. 13
      apps/doc-gen/.prettierrc
  14. 61
      apps/doc-gen/project.json
  15. 3
      apps/doc-gen/src/app/App.css
  16. 41
      apps/doc-gen/src/app/App.tsx
  17. 93
      apps/doc-gen/src/app/docgen-client.ts
  18. 35
      apps/doc-gen/src/app/docgen/common/helpers.ts
  19. 180
      apps/doc-gen/src/app/docgen/common/properties.ts
  20. 84
      apps/doc-gen/src/app/docgen/config.ts
  21. 27
      apps/doc-gen/src/app/docgen/doc-item.ts
  22. 87
      apps/doc-gen/src/app/docgen/render.ts
  23. 138
      apps/doc-gen/src/app/docgen/site.ts
  24. 110
      apps/doc-gen/src/app/docgen/templates.ts
  25. 34
      apps/doc-gen/src/app/docgen/themes/markdown/common.hbs
  26. 46
      apps/doc-gen/src/app/docgen/themes/markdown/contract.hbs
  27. 9
      apps/doc-gen/src/app/docgen/themes/markdown/enum.hbs
  28. 1
      apps/doc-gen/src/app/docgen/themes/markdown/error.hbs
  29. 1
      apps/doc-gen/src/app/docgen/themes/markdown/event.hbs
  30. 1
      apps/doc-gen/src/app/docgen/themes/markdown/function.hbs
  31. 62
      apps/doc-gen/src/app/docgen/themes/markdown/helpers.ts
  32. 1
      apps/doc-gen/src/app/docgen/themes/markdown/modifier.hbs
  33. 8
      apps/doc-gen/src/app/docgen/themes/markdown/page.hbs
  34. 9
      apps/doc-gen/src/app/docgen/themes/markdown/struct.hbs
  35. 1
      apps/doc-gen/src/app/docgen/themes/markdown/user-defined-value-type.hbs
  36. 1
      apps/doc-gen/src/app/docgen/themes/markdown/variable.hbs
  37. 13
      apps/doc-gen/src/app/docgen/utils/ItemError.ts
  38. 5
      apps/doc-gen/src/app/docgen/utils/arrays-equal.ts
  39. 1
      apps/doc-gen/src/app/docgen/utils/assert-equal-types.ts
  40. 6
      apps/doc-gen/src/app/docgen/utils/clone.ts
  41. 12
      apps/doc-gen/src/app/docgen/utils/ensure-array.ts
  42. 18
      apps/doc-gen/src/app/docgen/utils/execall.ts
  43. 5
      apps/doc-gen/src/app/docgen/utils/is-child.ts
  44. 7
      apps/doc-gen/src/app/docgen/utils/item-type.ts
  45. 4
      apps/doc-gen/src/app/docgen/utils/map-keys.ts
  46. 19
      apps/doc-gen/src/app/docgen/utils/map-values.ts
  47. 23
      apps/doc-gen/src/app/docgen/utils/memoized-getter.ts
  48. 145
      apps/doc-gen/src/app/docgen/utils/natspec.ts
  49. 13
      apps/doc-gen/src/app/docgen/utils/normalizeContractPath.ts
  50. 26
      apps/doc-gen/src/app/docgen/utils/read-item-docs.ts
  51. 63
      apps/doc-gen/src/app/docgen/utils/scope.ts
  52. 37
      apps/doc-gen/src/app/hooks/useLocalStorage.tsx
  53. 31
      apps/doc-gen/src/app/views/ErrorView.tsx
  54. 1
      apps/doc-gen/src/app/views/index.ts
  55. BIN
      apps/doc-gen/src/favicon.ico
  56. 13
      apps/doc-gen/src/index.html
  57. 11
      apps/doc-gen/src/main.tsx
  58. 17
      apps/doc-gen/src/profile.json
  59. 11
      apps/doc-gen/src/types.ts
  60. 23
      apps/doc-gen/tsconfig.app.json
  61. 16
      apps/doc-gen/tsconfig.json
  62. 73
      apps/doc-gen/webpack.config.js
  63. 61
      apps/doc-viewer/project.json
  64. 23
      apps/doc-viewer/src/app/App.tsx
  65. 22
      apps/doc-viewer/src/app/docviewer.ts
  66. BIN
      apps/doc-viewer/src/favicon.ico
  67. 13
      apps/doc-viewer/src/index.html
  68. 10
      apps/doc-viewer/src/main.tsx
  69. 16
      apps/doc-viewer/src/profile.json
  70. 23
      apps/doc-viewer/tsconfig.app.json
  71. 16
      apps/doc-viewer/tsconfig.json
  72. 60
      apps/doc-viewer/webpack.config.js
  73. 11
      apps/etherscan/project.json
  74. 2
      apps/etherscan/src/app/App.css
  75. 17
      apps/etherscan/src/app/RemixPlugin.tsx
  76. 75
      apps/etherscan/src/app/app.tsx
  77. 133
      apps/etherscan/src/app/components/HeaderWithSettings.tsx
  78. 16
      apps/etherscan/src/app/components/SubmitButton.tsx
  79. 4
      apps/etherscan/src/app/layouts/Default.tsx
  80. 6
      apps/etherscan/src/app/routes.tsx
  81. 5
      apps/etherscan/src/app/types/Receipt.ts
  82. 36
      apps/etherscan/src/app/utils/networks.ts
  83. 23
      apps/etherscan/src/app/utils/scripts.ts
  84. 32
      apps/etherscan/src/app/utils/utilities.ts
  85. 49
      apps/etherscan/src/app/utils/verify.ts
  86. 22
      apps/etherscan/src/app/views/CaptureKeyView.tsx
  87. 7
      apps/etherscan/src/app/views/HomeView.tsx
  88. 98
      apps/etherscan/src/app/views/ReceiptsView.tsx
  89. 226
      apps/etherscan/src/app/views/VerifyView.tsx
  90. 5
      apps/etherscan/src/index.html
  91. 16
      apps/etherscan/src/profile.json
  92. 1
      apps/etherscan/tsconfig.app.json
  93. 12
      apps/etherscan/webpack.config.js
  94. 23
      apps/remix-ide-e2e/package.json
  95. 9
      apps/remix-ide-e2e/src/commands/addFile.ts
  96. 14
      apps/remix-ide-e2e/src/commands/journalLastChildIncludes.ts
  97. 8
      apps/remix-ide-e2e/src/commands/verifyCallReturnValue.ts
  98. 4
      apps/remix-ide-e2e/src/commands/verifyContracts.ts
  99. 26
      apps/remix-ide-e2e/src/helpers/init.ts
  100. 9
      apps/remix-ide-e2e/src/local-plugin/.babelrc
  101. Some files were not shown because too many files have changed in this diff Show More

@ -9,7 +9,7 @@ orbs:
jobs:
build:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
@ -24,23 +24,66 @@ jobs:
key: v1-deps-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run: NX_BIN_URL=http://127.0.0.1:8080/assets/js NX_WASM_URL=http://127.0.0.1:8080/assets/js NPM_URL=http://localhost:9090/ yarn build:production
- run: yarn nx build remix-ide-e2e-src-local-plugin & yarn run build:libs
- run: yarn nx run debugger:build:production
- run: yarn nx run solidity-compiler:build:production
- run: yarn nx run remixd:build
- run:
name: Build
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
NX_BIN_URL=http://127.0.0.1:8080/assets/js/soljson NX_WASM_URL=http://127.0.0.1:8080/assets/js/soljson NPM_URL=http://localhost:9090/ yarn build:production
else
NX_BIN_URL=http://127.0.0.1:8080/assets/js/soljson NX_WASM_URL=http://127.0.0.1:8080/assets/js/soljson NPM_URL=http://localhost:9090/ yarn build
fi
- run: yarn run build:e2e
- run: grep -ir "[0-9]+commit" apps/* libs/* --include \*.ts --include \*.tsx --include \*.json > soljson-versions.txt
- restore_cache:
keys:
- soljson-v7-{{ checksum "soljson-versions.txt" }}
- run: yarn run downloadsolc_assets_e2e
- save_cache:
key: soljson-v7-{{ checksum "soljson-versions.txt" }}
paths:
- dist/apps/remix-ide/assets/js/soljson
- run: mkdir persist && zip -0 -r persist/dist.zip dist
- persist_to_workspace:
root: .
paths:
- "persist"
build-plugin:
docker:
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
parameters:
plugin:
type: string
steps:
- checkout
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- save_cache:
key: v1-deps-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run: yarn nx build << parameters.plugin >> --configuration=production
- run: mkdir persist && zip -0 -r persist/plugin-<< parameters.plugin >>.zip dist
- persist_to_workspace:
root: .
paths:
- "persist"
lint:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
parallelism: 1
steps:
- checkout
- restore_cache:
@ -53,7 +96,7 @@ jobs:
command: node ./apps/remix-ide/ci/lint-targets.js
remix-libs:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -62,11 +105,12 @@ jobs:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/dist.zip
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn --version
- run: yarn
- run: yarn build:libs
- run: cd dist/libs/remix-tests && yarn
- run: cd dist/libs/remix-tests && yarn add @remix-project/remix-url-resolver ../../libs/remix-url-resolver
- run: cd dist/libs/remix-tests && yarn add @remix-project/remix-lib ../../libs/remix-lib
@ -78,7 +122,7 @@ jobs:
remix-ide-browser:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
@ -120,12 +164,8 @@ jobs:
- attach_workspace:
at: .
- run: unzip ./persist/dist.zip
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- run: yarn run downloadsolc_assets_e2e && yarn run downloadsolc_assets_dist
- run: ls -la ./dist/apps/remix-ide/assets/js
- run: yarn run selenium-install || yarn run selenium-install
- run:
@ -146,14 +186,17 @@ jobs:
remix-test-plugins:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
parameters:
script:
plugin:
type: string
parallelism: 4
parallelism:
type: integer
default: 1
parallelism: << parameters.parallelism >>
steps:
- browser-tools/install-browser-tools:
install-firefox: false
@ -166,98 +209,67 @@ jobs:
- checkout
- attach_workspace:
at: .
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- run: unzip ./persist/dist.zip
- run: yarn run downloadsolc_assets_e2e && yarn run downloadsolc_assets_dist
- run: unzip ./persist/plugin-<< parameters.plugin >>.zip
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- run: yarn run selenium-install || yarn run selenium-install
- run:
name: Start Selenium
command: yarn run selenium
background: true
- run: ./apps/remix-ide/ci/<< parameters.script >>
- run: ./apps/remix-ide/ci/browser_test_plugin.sh << parameters.plugin >>
- store_test_results:
path: ./reports/tests
- store_artifacts:
path: ./reports/screenshots
deploy-remix-live:
docker:
- image: cimg/node:14.17.6-browsers
resource_class:
xlarge
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/404.html dist/apps/remix-ide/*.js dist/apps/remix-ide/*.css dist/apps/remix-ide/assets dist/apps/remix-ide/favicon.ico"
working_directory: ~/remix-project
steps:
- checkout
- run: yarn
- run: yarn run downloadsolc_assets
- run: yarn run build:production
- run:
name: Deploy
command: |
if [ "${CIRCLE_BRANCH}" == "remix_live" ]; then
./apps/remix-ide/ci/deploy_from_travis_remix-live.sh;
fi
deploy-remix-alpha:
predeploy:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/404.html dist/apps/remix-ide/*.js dist/apps/remix-ide/*.css dist/apps/remix-ide/assets dist/apps/remix-ide/favicon.ico"
working_directory: ~/remix-project
steps:
- checkout
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- run: yarn run downloadsolc_assets
- run: yarn run build:production
- run:
name: Deploy
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
./apps/remix-ide/ci/deploy_from_travis_remix-alpha.sh;
fi
- save_cache:
key: v1-deps-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run: yarn build:production
- run: mkdir persist && zip -0 -r persist/predeploy.zip dist
- persist_to_workspace:
root: .
paths:
- "persist"
deploy-remix-beta:
deploy-build:
docker:
- image: cimg/node:14.17.6-browsers
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/index.html dist/apps/remix-ide/404.html dist/apps/remix-ide/*.js dist/apps/remix-ide/*.css dist/apps/remix-ide/assets dist/apps/remix-ide/favicon.ico"
COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
COMMIT_AUTHOR: "Circle CI"
working_directory: ~/remix-project
parameters:
script:
type: string
steps:
- checkout
- run: yarn
- run: yarn run build:libs
- run: yarn run downloadsolc_assets
- run: yarn run build:production
- run:
name: Deploy
command: |
if [ "${CIRCLE_BRANCH}" == "remix_beta" ]; then
./apps/remix-ide/ci/deploy_from_travis_remix-beta.sh;
fi
- attach_workspace:
at: .
- run: unzip ./persist/predeploy.zip
- run: ./apps/remix-ide/ci/deploy_from_travis_remix-<< parameters.script >>.sh
workflows:
version: 2
run_flaky_tests:
when: << pipeline.parameters.run_flaky_tests >>
jobs:
@ -275,18 +287,28 @@ workflows:
unless: << pipeline.parameters.run_flaky_tests >>
jobs:
- build
- build-plugin:
matrix:
parameters:
plugin: ["plugin_api"]
- lint:
requires:
- build
- remix-libs:
requires:
- build
- remix-libs
- remix-test-plugins:
name: test-plugin-<< matrix.plugin >>
requires:
- build
- build-plugin
matrix:
alias: plugins
parameters:
script: ["browser_tests_plugin_api.sh", "browser_tests_etherscan_plugin.sh", "browser_tests_vyper_plugin.sh"]
plugin: ["plugin_api"]
parallelism: [1, 9]
exclude:
- plugin: plugin_api
parallelism: 1
- remix-ide-browser:
requires:
- build
@ -301,31 +323,45 @@ workflows:
- lint
- remix-libs
- remix-ide-browser
- remix-test-plugins
- deploy-remix-live:
- plugins
- predeploy:
filters:
branches:
only: ['master', 'remix_live', 'remix_beta']
- deploy-build:
script: "live"
name: "deploy-live"
requires:
- lint
- remix-libs
- remix-ide-browser
- remix-test-plugins
- plugins
- predeploy
filters:
branches:
only: remix_live
- deploy-remix-alpha:
- deploy-build:
script: "alpha"
name: "deploy-alpha"
requires:
- lint
- remix-libs
- remix-ide-browser
- remix-test-plugins
- plugins
- predeploy
filters:
branches:
only: master
- deploy-remix-beta:
- deploy-build:
script: "beta"
name: "deploy-beta"
requires:
- lint
- remix-libs
- remix-ide-browser
- remix-test-plugins
- plugins
- predeploy
filters:
branches:
only: remix_beta

@ -2,3 +2,4 @@ gist_token=<token>
account_passphrase=<passphrase>
account_password=<password>
NODE_OPTIONS=--max-old-space-size=2048
# WALLET_CONNECT_PROJECT_ID=<walletconnect cloud PROJECT_ID>

@ -11,11 +11,10 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 14.17.6
node-version: 20.0.0
- run: yarn install
- run: ls
- run: pwd
- run: yarn run downloadsolc_assets
- run: yarn run build:production
- run: echo "action_state=$('./apps/remix-ide/ci/publishIpfs' ${{ secrets.IPFS_PROJET_ID }} ${{ secrets.IPFS_PROJECT_SECRET }})" >> $GITHUB_ENV
- uses: mshick/add-pr-comment@v1

@ -12,7 +12,7 @@ jobs:
- name: Environment Setup
uses: actions/setup-node@v3
with:
node-version: 14.17.6
node-version: 20.0.0
- name: Run SUT Action
uses: EthereumRemix/sol-test@v1
with:

4
.gitignore vendored

@ -57,7 +57,3 @@ testem.log
.DS_Store
.vscode/settings.json
.vscode/launch.json
libs/remix-node/
libs/remix-niks/
apps/remix-react

@ -50,7 +50,7 @@ Note: It contains the latest supported version of Solidity available at the time
*Supported versions:*
```bash
"engines": {
"node": "^14.17.6",
"node": "^20.0.0",
"npm": "^6.14.15"
}
```

@ -0,0 +1,13 @@
{
"presets": [
[
"@nrwl/react/babel", {
"runtime": "automatic"
}
]
],
"plugins": [
]
}

@ -2,7 +2,7 @@ import { PluginClient } from "@remixproject/plugin";
import { createClient } from "@remixproject/plugin-webview";
import { IDebuggerApi, LineColumnLocation,
onBreakpointClearedListener, onBreakpointAddedListener, onEditorContentChanged, onEnvChangedListener, TransactionReceipt } from '@remix-ui/debugger-ui'
import { DebuggerApiMixin } from './debugger-api'
import { DebuggerApiMixin } from '@remix-ui/debugger-ui'
import { CompilerAbstract } from '@remix-project/remix-solidity'
export class DebuggerClientApi extends DebuggerApiMixin(PluginClient) {

@ -1 +0,0 @@
export * from './app/debugger-api';

@ -9,7 +9,6 @@
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",

@ -1,32 +1,87 @@
const nxWebpack = require('@nrwl/react/plugins/webpack')
const { composePlugins, withNx } = require('@nrwl/webpack')
const webpack = require('webpack')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
module.exports = config => {
const nxWebpackConfig = nxWebpack(config)
const webpackConfig = {
...nxWebpackConfig,
resolve : {
...nxWebpackConfig.resolve,
fallback: {
...nxWebpackConfig.resolve.fallback,
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
// add fallback for node modules
config.resolve.fallback = {
...config.resolve.fallback,
"crypto": require.resolve("crypto-browserify"),
"stream": require.resolve("stream-browserify"),
"http" : require.resolve("stream-http"),
"https" : require.resolve("https-browserify"),
"path" : require.resolve("path-browserify"),
"path": require.resolve("path-browserify"),
"http": require.resolve("stream-http"),
"https": require.resolve("https-browserify"),
"constants": require.resolve("constants-browserify"),
"os": false, //require.resolve("os-browserify/browser"),
"timers": false, // require.resolve("timers-browserify"),
"zlib": require.resolve("browserify-zlib"),
"fs": false,
"module": false,
"fs" : false
},
}
"tls": false,
"net": false,
"readline": false,
"child_process": false,
"buffer": require.resolve("buffer/"),
"vm": require.resolve('vm-browserify'),
}
if (process.env.NODE_ENV === 'production') {
return {
...webpackConfig,
mode: 'production',
devtool: 'source-map',
// add externals
config.externals = {
...config.externals,
solc: 'solc',
}
} else {
return webpackConfig
// add public path
config.output.publicPath = '/'
// add copy & provide plugin
config.plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'],
process: 'process/browser',
})
)
// souce-map loader
config.module.rules.push({
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
})
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
// set minimizer
config.optimization.minimizer = [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2015,
compress: false,
mangle: false,
format: {
comments: false,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
];
config.watchOptions = {
ignored: /node_modules/
}
}
return config;
});

@ -0,0 +1,34 @@
{
"extends": [
"plugin:@nrwl/nx/react",
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

@ -0,0 +1,13 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"endOfLine": "lf"
}

@ -0,0 +1,61 @@
{
"name": "doc-gen",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/doc-gen/src",
"projectType": "application",
"implicitDependencies": [
],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "development",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/doc-gen",
"index": "apps/doc-gen/src/index.html",
"baseHref": "./",
"main": "apps/doc-gen/src/main.tsx",
"tsConfig": "apps/doc-gen/tsconfig.app.json",
"assets": [
"apps/doc-gen/src/favicon.ico",
"apps/doc-gen/src/profile.json"
],
"styles": [],
"scripts": [],
"webpackConfig": "apps/doc-gen/webpack.config.js"
},
"configurations": {
"development": {
},
"production": {
"fileReplacements": [
{
"replace": "apps/doc-gen/src/environments/environment.ts",
"with": "apps/doc-gen/src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "doc-gen:build",
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {
"buildTarget": "doc-gen:build:development",
"port": 6003
},
"production": {
"buildTarget": "doc-gen:build:production"
}
}
}
},
"tags": []
}

@ -0,0 +1,3 @@
body {
margin: 0;
}

@ -0,0 +1,41 @@
import React, { useState, useEffect } from 'react'
import './App.css'
import { DocGenClient } from './docgen-client'
import { Build } from './docgen/site'
export const client = new DocGenClient()
const App = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [themeType, setThemeType] = useState<string>('dark');
const [hasBuild, setHasBuild] = useState<boolean>(false);
const [fileName, setFileName] = useState<string>('');
useEffect(() => {
const watchThemeSwitch = async () => {
client.eventEmitter.on('themeChanged', (theme: string) => {
setThemeType(theme)
})
client.eventEmitter.on('compilationFinished', (build: Build, fileName: string) => {
setHasBuild(true)
setFileName(fileName)
})
client.eventEmitter.on('docsGenerated', (docs: string[]) => {
})
}
watchThemeSwitch()
}, [])
return (
<div className="p-3">
<h5 className="h-5 mb-3">Compile a Solidity contract and generate its documentation as Markdown. (Right-click on a contract in the File Explorer and select "Generate Docs" from the context menu.).</h5>
{fileName && <div className="border-bottom border-top px-2 py-3 justify-center align-items-center d-flex">
<h6>File: {fileName}</h6>
</div>}
{hasBuild && <button className="btn btn-primary btn-block mt-4" onClick={() => client.generateDocs()}>Generate Docs</button>}
</div>
)
}
export default App

@ -0,0 +1,93 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { PluginClient } from '@remixproject/plugin'
import { CompilationResult, SourceWithTarget, customAction } from '@remixproject/plugin-api'
import { createClient } from '@remixproject/plugin-webview'
import EventEmitter from 'events'
import { Config, defaults } from './docgen/config'
import { Build, buildSite } from './docgen/site'
import { loadTemplates } from './docgen/templates'
import { SolcInput, SolcOutput } from 'solidity-ast/solc'
import { render } from './docgen/render'
import { normalizeContractPath } from './docgen/utils/normalizeContractPath'
export class DocGenClient extends PluginClient {
private currentTheme
public eventEmitter: EventEmitter
private build: Build
public docs: string[] = []
private fileName: string = ''
private contractPath: string = ''
constructor() {
super()
this.eventEmitter = new EventEmitter()
this.methods = ['generateDocs', 'openDocs', 'generateDocsCustomAction']
createClient(this)
this.onload().then(async () => {
await this.setListeners()
})
}
async setListeners() {
this.currentTheme = await this.call('theme', 'currentTheme')
this.on('theme', 'themeChanged', (theme: any) => {
this.currentTheme = theme
this.eventEmitter.emit('themeChanged', this.currentTheme)
});
this.eventEmitter.emit('themeChanged', this.currentTheme)
this.on('solidity', 'compilationFinished', (fileName: string, source: SourceWithTarget, languageVersion: string, data: CompilationResult) => {
const input: SolcInput = {
sources: source.sources
}
const output: SolcOutput = {
sources: data.sources as any
}
this.build = {
input: input,
output: output
}
const segmentedPathList = normalizeContractPath(fileName)
this.fileName = segmentedPathList[segmentedPathList.length - 1]
this.contractPath = segmentedPathList[0]
this.eventEmitter.emit('compilationFinished', this.build, this.fileName)
})
}
async generateDocsCustomAction(action: customAction) {
await this.call('solidity', 'compile', action.path[0])
await this.generateDocs()
}
async docgen(builds: Build[], userConfig?: Config): Promise<void> {
const config = { ...defaults, ...userConfig }
config.sourcesDir = this.contractPath !== config.sourcesDir ? this.contractPath : config.sourcesDir
const templates = await loadTemplates(config.theme, config.root, config.templates)
const site = buildSite(builds, config, templates.properties ?? {})
const renderedSite = render(site, templates, config.collapseNewlines)
const docs: string[] = []
for (const { id, contents } of renderedSite) {
const temp = `${this.fileName.split('.')[0]}.${id.split('.')[1]}`
const newFileName = `docs/${temp}`
await this.call('fileManager', 'setFile', newFileName , contents)
docs.push(newFileName)
}
this.eventEmitter.emit('docsGenerated', docs)
this.emit('docgen' as any, 'docsGenerated', docs)
this.docs = docs
await this.openDocs(docs)
}
async openDocs(docs: string[]) {
await this.call('manager', 'activatePlugin', 'doc-viewer')
await this.call('tabs' as any, 'focus', 'doc-viewer')
await this.call('doc-viewer' as any, 'viewDocs', docs)
}
async generateDocs() {
this.eventEmitter.on('compilationFinished', async (build: Build, fileName: string) => {
await this.docgen([build])
})
}
}

@ -0,0 +1,35 @@
import { VariableDeclaration } from "solidity-ast";
export function trim(text: string) {
if (typeof text === 'string') {
return text.trim();
}
}
export function joinLines(text?: string) {
if (typeof text === 'string') {
return text.replace(/\n+/g, ' ');
}
}
/**
* Format a variable as its type followed by its name, if available.
*/
export function formatVariable(v: VariableDeclaration): string {
return [v.typeName?.typeDescriptions.typeString].concat(v.name || []).join(' ');
}
export const eq = (a: unknown, b: unknown) => a === b;
export const slug = (str) => {
if (str === undefined) {
throw new Error('Missing argument');
}
return str.replace(/\W/g, '-');
}
export const names = params => params.map(p => p.name).join(', ');
export const typedParams = params => {
return params?.map(p => `${p.type}${p.indexed ? ' indexed' : ''}${p.name ? ' ' + p.name : ''}`).join(', ');
};

@ -0,0 +1,180 @@
import { EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, ModifierDefinition, ParameterList, StructDefinition, UserDefinedValueTypeDefinition, VariableDeclaration } from 'solidity-ast';
import { findAll, isNodeType } from 'solidity-ast/utils';
import { NatSpec, parseNatspec } from '../utils/natspec';
import { DocItemContext, DOC_ITEM_CONTEXT } from '../site';
import { mapValues } from '../utils/map-values';
import { DocItem, docItemTypes } from '../doc-item';
import { formatVariable, slug } from './helpers';
import { PropertyGetter } from '../templates';
import { itemType } from '../utils/item-type';
type TypeDefinition = StructDefinition | EnumDefinition | UserDefinedValueTypeDefinition;
export function type ({ item }: DocItemContext): string {
return itemType(item);
}
export function natspec ({ item }: DocItemContext): NatSpec {
return parseNatspec(item);
}
export function name({ item }: DocItemContext, original?: unknown): string {
if (item.nodeType === 'FunctionDefinition') {
return item.kind === 'function' ? original as string : item.kind;
} else {
return original as string;
}
}
export function fullName ({ item, contract }: DocItemContext): string {
if (contract) {
return `${contract.name}.${item.name}`;
} else {
return `${item.name}`;
}
}
export function signature ({ item }: DocItemContext): string | undefined {
switch (item.nodeType) {
case 'ContractDefinition':
return undefined;
case 'FunctionDefinition': {
const { kind, name } = item;
const params = item.parameters.parameters;
const returns = item.returnParameters.parameters;
const head = (kind === 'function' || kind === 'freeFunction') ? [kind, name].join(' ') : kind;
const res = [
`${head}(${params.map(formatVariable).join(', ')})`,
item.visibility,
];
if (item.stateMutability !== 'nonpayable') {
res.push(item.stateMutability);
}
if (item.virtual) {
res.push('virtual');
}
if (returns.length > 0) {
res.push(`returns (${returns.map(formatVariable).join(', ')})`);
}
return res.join(' ');
}
case 'EventDefinition': {
const params = item.parameters.parameters;
return `event ${item.name}(${params.map(formatVariable).join(', ')})`;
}
case 'ErrorDefinition': {
const params = item.parameters.parameters;
return `error ${item.name}(${params.map(formatVariable).join(', ')})`;
}
case 'ModifierDefinition': {
const params = item.parameters.parameters;
return `modifier ${item.name}(${params.map(formatVariable).join(', ')})`;
}
case 'VariableDeclaration':
return formatVariable(item);
}
}
interface Param extends VariableDeclaration {
type: string;
natspec?: string;
};
function getParams (params: ParameterList, natspec: NatSpec['params'] | NatSpec['returns']): Param[] {
return params.parameters.map((p, i) => ({
...p,
type: p.typeDescriptions.typeString!,
natspec: natspec?.find((q, j) => q.name === undefined ? i === j : p.name === q.name)?.description,
}));
}
export function params ({ item }: DocItemContext): Param[] | undefined {
if ('parameters' in item) {
return getParams(item.parameters, natspec(item[DOC_ITEM_CONTEXT]).params);
}
}
export function returns ({ item }: DocItemContext): Param[] | undefined {
if ('returnParameters' in item) {
return getParams(item.returnParameters, natspec(item[DOC_ITEM_CONTEXT]).returns);
}
}
export function items ({ item }: DocItemContext): DocItem[] | undefined {
return (item.nodeType === 'ContractDefinition')
? item.nodes.filter(isNodeType(docItemTypes)).filter(n => !('visibility' in n) || n.visibility !== 'private')
: undefined;
}
export function functions ({ item }: DocItemContext): FunctionDefinition[] | undefined {
return [...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private');
}
export function events ({ item }: DocItemContext): EventDefinition[] | undefined {
return [...findAll('EventDefinition', item)];
}
export function modifiers ({ item }: DocItemContext): ModifierDefinition[] | undefined {
return [...findAll('ModifierDefinition', item)];
}
export function errors ({ item }: DocItemContext): ErrorDefinition[] | undefined {
return [...findAll('ErrorDefinition', item)];
}
export function variables ({ item }: DocItemContext): VariableDeclaration[] | undefined {
return (item.nodeType === 'ContractDefinition')
? item.nodes.filter(isNodeType('VariableDeclaration')).filter(v => v.stateVariable && v.visibility !== 'private')
: undefined;
}
export function types ({ item }: DocItemContext): TypeDefinition[] | undefined {
return [...findAll(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition'], item)];
}
export function anchor({ item, contract }: DocItemContext) {
let res = '';
if (contract) {
res += contract.name + '-';
}
res += item.name;
if ('parameters' in item) {
const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(',');
res += slug('(' + signature + ')');
}
if (isNodeType('VariableDeclaration', item)) {
res += '-' + slug(item.typeName.typeDescriptions.typeString);
}
return res;
}
export function inheritance ({ item, build }: DocItemContext) {
if (!isNodeType('ContractDefinition', item)) {
throw new Error('used inherited-items on non-contract');
}
return item.linearizedBaseContracts
.map(id => build.deref('ContractDefinition', id))
.filter((c, i) => c.name !== 'Context' || i === 0);
}
export function hasfunctions ({ item }: DocItemContext) {
return (item as any).inheritance.some(c => c.functions.length > 0);
}
export function hasevents ({ item }: DocItemContext) {
return (item as any).inheritance.some(c => c.events.length > 0);
}
export function inheritedfunctions ({ item }: DocItemContext) {
const { inheritance } = (item as any)
const baseFunctions = new Set(inheritance.flatMap(c => c.functions.flatMap(f => f.baseFunctions ?? [])));
return inheritance.map((contract, i) => ({
contract,
functions: contract.functions.filter(f => !baseFunctions.has(f.id) && (f.name !== 'constructor' || i === 0)),
}))
}

@ -0,0 +1,84 @@
import type { SourceUnit } from 'solidity-ast';
import type { DocItem } from './doc-item';
import type { PageAssigner, PageStructure } from './site';
export interface UserConfig {
/**
* The directory where rendered pages will be written.
* Defaults to 'docs'.
*/
outputDir?: string;
/**
* A directory of custom templates that should take precedence over the
* theme's templates.
*/
templates?: string;
/**
* The name of the built-in templates that will be used by default.
* Defaults to 'markdown'.
*/
theme?: string;
/**
* The way documentable items (contracts, functions, custom errors, etc.)
* will be organized in pages. Built in options are:
* - 'single': all items in one page
* - 'items': one page per item
* - 'files': one page per input Solidity file
* More customization is possible by defining a function that returns a page
* path given the AST node for the item and the source unit where it is
* defined.
* Defaults to 'single'.
*/
pages?: 'single' | 'items' | 'files' | PageAssigner;
/**
* An array of sources subdirectories that should be excluded from
* documentation, relative to the contract sources directory.
*/
exclude?: string[];
/**
* Clean up the output by collapsing 3 or more contiguous newlines into only 2.
* Enabled by default.
*/
collapseNewlines?: boolean;
/**
* The extension for generated pages.
* Defaults to '.md'.
*/
pageExtension?: string;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Other config parameters that will be provided by the environment (e.g. Hardhat)
// rather than by the user manually, unless using the library directly.
export interface Config extends UserConfig {
/**
* The root directory relative to which 'outputDir', 'sourcesDir', and
* 'templates' are specified. Defaults to the working directory.
*/
root?: string;
/**
* The Solidity sources directory.
*/
sourcesDir?: string;
}
export type FullConfig = Required<Config>;
export const defaults: Omit<FullConfig, 'templates'> = {
root: process.cwd(),
sourcesDir: 'contracts',
outputDir: 'docs',
pages: 'single',
exclude: [],
theme: 'markdown',
collapseNewlines: true,
pageExtension: '.md',
};

@ -0,0 +1,27 @@
import { ContractDefinition, ImportDirective, PragmaDirective, SourceUnit, UsingForDirective } from "solidity-ast";
import { Node, NodeType, NodeTypeMap } from "solidity-ast/node";
import { AssertEqual } from "./utils/assert-equal-types";
export type DocItem = Exclude<
SourceUnit['nodes'][number] | ContractDefinition['nodes'][number],
ImportDirective | PragmaDirective | UsingForDirective
>;
export const docItemTypes = [
'ContractDefinition',
'EnumDefinition',
'ErrorDefinition',
'EventDefinition',
'FunctionDefinition',
'ModifierDefinition',
'StructDefinition',
'UserDefinedValueTypeDefinition',
'VariableDeclaration',
] as const;
// Make sure at compile time that docItemTypes contains exactly the node types of DocItem.
const _: AssertEqual<typeof docItemTypes[number], DocItem['nodeType']> = true;
export function isDocItem(node: Node): node is DocItem {
return (docItemTypes as readonly string[]).includes(node.nodeType);
}

@ -0,0 +1,87 @@
import Handlebars, { RuntimeOptions } from 'handlebars';
import { Site, Page, DocItemWithContext, DOC_ITEM_CONTEXT } from './site';
import { Templates } from './templates';
import { itemType } from './utils/item-type';
import fs from 'fs';
export interface RenderedPage {
id: string;
contents: string;
}
interface TemplateOptions {
data: {
site: Site;
};
}
export function render(site: Site, templates: Templates, collapseNewlines?: boolean): RenderedPage[] {
const renderPage = buildRenderer(templates);
const renderedPages: RenderedPage[] = [];
for (const page of site.pages) {
let contents = renderPage(page, { data: { site } });
if (collapseNewlines) {
contents = contents.replace(/\n{3,}/g, '\n\n');
}
renderedPages.push({
id: page.id,
contents,
});
}
return renderedPages;
}
export const itemPartialName = (item: DocItemWithContext) => itemType(item).replace(/ /g, '-').toLowerCase();
function itemPartial(item: DocItemWithContext, options?: RuntimeOptions) {
if (!item.__item_context) {
throw new Error(`Partial 'item' used in unsupported context (not a doc item)`);
}
const partial = options?.partials?.[itemPartialName(item)];
if (!partial) {
throw new Error(`Missing partial '${itemPartialName(item)}'`);
}
return partial(item, options);
}
function readmeHelper(H: typeof Handlebars, path: string, opts: RuntimeOptions) {
const items: DocItemWithContext[] = opts.data.root.items;
const renderedItems = Object.fromEntries(
items.map(item => [
item.name,
new H.SafeString(
H.compile('{{>item}}')(item, opts),
),
]),
);
return new H.SafeString(
H.compile(fs.readFileSync(path, 'utf8'))(renderedItems, opts),
);
}
function buildRenderer(templates: Templates): (page: Page, options: TemplateOptions) => string {
const pageTemplate = templates.partials?.page;
if (pageTemplate === undefined) {
throw new Error(`Missing 'page' template`);
}
const H = Handlebars.create();
for (const [name, getBody] of Object.entries(templates.partials ?? {})) {
let partial: HandlebarsTemplateDelegate | undefined;
H.registerPartial(name, (...args) => {
partial ??= H.compile(getBody());
return partial(...args);
});
}
H.registerHelper('readme', (path: string, opts: RuntimeOptions) => readmeHelper(H, path, opts));
for (const [name, fn] of Object.entries(templates.helpers ?? {})) {
H.registerHelper(name, fn);
}
H.registerPartial('item', itemPartial);
return H.compile('{{>page}}');
}

@ -0,0 +1,138 @@
import path from 'path';
import { ContractDefinition, SourceUnit } from 'solidity-ast';
import { SolcOutput, SolcInput } from 'solidity-ast/solc';
import { astDereferencer, ASTDereferencer, findAll, isNodeType, srcDecoder, SrcDecoder } from 'solidity-ast/utils';
import { FullConfig } from './config';
import { DocItem, docItemTypes, isDocItem } from './doc-item';
import { Properties } from './templates';
import { clone } from './utils/clone';
import { isChild } from './utils/is-child';
import { mapValues } from './utils/map-values';
import { defineGetterMemoized } from './utils/memoized-getter';
export interface Build {
input: SolcInput;
output: SolcOutput;
}
export interface BuildContext extends Build {
deref: ASTDereferencer;
decodeSrc: SrcDecoder;
}
export type SiteConfig = Pick<FullConfig, 'pages' | 'exclude' | 'sourcesDir' | 'pageExtension'>;
export type PageStructure = SiteConfig['pages'];
export type PageAssigner = ((item: DocItem, file: SourceUnit, config: SiteConfig) => string | undefined);
export const pageAssigner: Record<PageStructure & string, PageAssigner> = {
single: (_1, _2, { pageExtension: ext }) => 'index' + ext,
items: (item, _, { pageExtension: ext }) => item.name + ext,
files: (_, file, { pageExtension: ext, sourcesDir }) =>
path.relative(sourcesDir, file.absolutePath).replace('.sol', ext),
};
export interface Site {
items: DocItemWithContext[];
pages: Page[];
}
export interface Page {
id: string;
items: DocItemWithContext[];
}
export const DOC_ITEM_CONTEXT = '__item_context' as const;
export type DocItemWithContext = DocItem & { [DOC_ITEM_CONTEXT]: DocItemContext };
export interface DocItemContext {
page?: string;
item: DocItemWithContext;
contract?: ContractDefinition;
file: SourceUnit;
build: BuildContext;
}
export function buildSite (builds: Build[], siteConfig: SiteConfig, properties: Properties = {}): Site {
const assign = typeof siteConfig.pages === 'string' ? pageAssigner[siteConfig.pages] : siteConfig.pages;
const seen = new Set<string>();
const items: DocItemWithContext[] = [];
const pages: Record<string, DocItemWithContext[]> = {};
// eslint-disable-next-line prefer-const
for (let { input, output } of builds) {
// Clone because we will mutate in order to add item context.
output = { ...output, sources: clone(output.sources) };
const deref = astDereferencer(output);
const decodeSrc = srcDecoder(input, output);
const build = { input, output, deref, decodeSrc };
for (const { ast: file } of Object.values(output.sources)) {
const isNewFile = !seen.has(file.absolutePath);
seen.add(file.absolutePath);
for (const topLevelItem of file.nodes) {
if (!isDocItem(topLevelItem)) continue;
const page = assignIfIncludedSource(assign, topLevelItem, file, siteConfig);
const withContext = defineContext(topLevelItem, build, file, page);
defineProperties(withContext, properties);
if (isNewFile && page !== undefined) {
(pages[page] ??= []).push(withContext);
items.push(withContext);
}
if (!isNodeType('ContractDefinition', topLevelItem)) {
continue;
}
for (const item of topLevelItem.nodes) {
if (!isDocItem(item)) continue;
if (isNewFile && page !== undefined) items.push(item as DocItemWithContext);
const contract = topLevelItem.nodeType === 'ContractDefinition' ? topLevelItem : undefined;
const withContext = defineContext(item, build, file, page, contract);
defineProperties(withContext, properties);
}
}
}
}
return {
items,
pages: Object.entries(pages).map(([id, pageItems]) => ({ id, items: pageItems })),
};
}
function defineContext (item: DocItem, build: BuildContext, file: SourceUnit, page?: string, contract?: ContractDefinition): DocItemWithContext {
return Object.assign(item, {
[DOC_ITEM_CONTEXT]: { build, file, contract, page, item: item as DocItemWithContext },
});
}
function defineProperties (item: DocItemWithContext, properties: Properties) {
for (const [prop, fn] of Object.entries(properties)) {
const original: unknown = (item as any)[prop];
defineGetterMemoized(item as any, prop, () => fn(item.__item_context, original));
}
}
function assignIfIncludedSource (
assign: PageAssigner,
item: DocItem,
file: SourceUnit,
config: SiteConfig,
) {
return isFileIncluded(file.absolutePath, config)
? assign(item, file, config)
: undefined;
}
function isFileIncluded (file: string, config: SiteConfig) {
return (
isChild(file, config.sourcesDir) &&
config.exclude.every(e => !isChild(file, path.join(config.sourcesDir, e)))
);
}

@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { mapKeys } from './utils/map-keys';
import { DocItemContext } from './site';
import * as defaultProperties from './common/properties';
import * as themeHelpers from './themes/markdown/helpers'
const common = require('./themes/markdown/common.hbs');
const contract = require('./themes/markdown/contract.hbs');
const enum_ = require('./themes/markdown/enum.hbs');
const error = require('./themes/markdown/error.hbs');
const event = require('./themes/markdown/event.hbs');
const function_ = require('./themes/markdown/function.hbs');
const modifier = require('./themes/markdown/modifier.hbs');
const page = require('./themes/markdown/page.hbs');
const struct = require('./themes/markdown/struct.hbs');
const variable = require('./themes/markdown/variable.hbs');
const userDefinedValueType = require('./themes/markdown/user-defined-value-type.hbs');
export type PropertyGetter = (ctx: DocItemContext, original?: unknown) => unknown;
export type Properties = Record<string, PropertyGetter>;
export interface Templates {
partials?: Record<string, () => string>;
helpers?: Record<string, (...args: unknown[]) => string>;
properties?: Record<string, PropertyGetter>;
}
/**
* Loads the templates that will be used for rendering a site based on a
* default theme and user templates.
*
* The result contains all partials, helpers, and property getters defined in
* the user templates and the default theme, where the user's take precedence
* if there is a clash. Additionally, all theme partials and helpers are
* included with the theme prefix, e.g. `markdown/contract` will be a partial.
*/
export async function loadTemplates(defaultTheme: string, root: string, userTemplatesPath?: string): Promise<Templates> {
const themes = await readThemes();
// Initialize templates with the default theme.
const templates: Required<Templates> = {
partials: { ...themes[defaultTheme]?.partials },
helpers: { ...themes[defaultTheme]?.helpers },
properties: { ...defaultProperties },
};
// Add partials and helpers from all themes, prefixed with the theme name.
for (const [themeName, theme] of Object.entries(themes)) {
const addPrefix = (k: string) => `${themeName}/${k}`;
Object.assign(templates.partials, mapKeys(theme.partials, addPrefix));
Object.assign(templates.helpers, mapKeys(theme.helpers, addPrefix));
}
return templates;
}
/**
* Read templates and helpers from a directory.
*/
export async function readTemplates(): Promise<Required<Templates>> {
return {
partials: await readPartials(),
helpers: await readHelpers('helpers'),
properties: await readHelpers('properties'),
};
}
async function readPartials() {
const partials: NonNullable<Templates['partials']> = {};
partials["common"] = () => common
partials["contract"] = () => contract
partials["enum"] = () => enum_
partials["error"] = () => error
partials["event"] = () => event
partials["function"] = () => function_
partials["modifier"] = () => modifier
partials["page"] = () => page
partials["struct"] = () => struct
partials["variable"] = () => variable
partials["user-defined-value-type"] = () => userDefinedValueType
return partials;
}
async function readHelpers(name: string) {
const helpers: Record<string, (...args: any[]) => any> = {};
for (const name in themeHelpers) {
if (typeof themeHelpers[name] === 'function') {
helpers[name] = themeHelpers[name];
}
}
return helpers;
}
/**
* Reads all built-in themes into an object. Partials will always be found in
* src/themes, whereas helpers may instead be found in dist/themes if TypeScript
* can't be imported directly.
*/
async function readThemes(): Promise<Record<string, Required<Templates>>> {
const themes: Record<string, Required<Templates>> = {}
themes['markdown'] = await readTemplates()
return themes
}

@ -0,0 +1,34 @@
{{h}} {{name}}
{{#if signature}}
```solidity
{{{signature}}}
```
{{/if}}
{{{natspec.notice}}}
{{#if natspec.dev}}
_{{{natspec.dev}}}_
{{/if}}
{{#if natspec.params}}
{{h 2}} Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
{{#each params}}
| {{name}} | {{type}} | {{{joinLines natspec}}} |
{{/each}}
{{/if}}
{{#if natspec.returns}}
{{h 2}} Return Values
| Name | Type | Description |
| ---- | ---- | ----------- |
{{#each returns}}
| {{#if name}}{{name}}{{else}}[{{@index}}]{{/if}} | {{type}} | {{{joinLines natspec}}} |
{{/each}}
{{/if}}

@ -0,0 +1,46 @@
{{>common}}
{{h 2}} Contract
{{name}} : {{__item_context.file.absolutePath}}
{{{natspec.dev}}}
{{#if modifiers}}
{{s}}
{{h 2}} Modifiers:
{{#each modifiers}}
{{#hsection}}
{{>item}}
{{/hsection}}
{{/each}}
{{/if}}
{{#if hasfunctions}}
{{s}}
{{h 2}} Functions:
{{#each inheritedfunctions}}
{{#unless @first}}
inherits {{contract.name}}:
{{/unless}}
{{#each functions}}
{{#hsection}}
{{>item}}
{{/hsection}}
{{/each}}
{{/each}}
{{/if}}
{{#if hasevents}}
{{s}}
{{h 2}} Events:
{{#each inheritance}}
{{#unless @first}}
inherits {{name}}:
{{/unless}}
{{#each events}}
{{#hsection}}
{{>item}}
{{/hsection}}
{{/each}}
{{/each}}
{{/if}}

@ -0,0 +1,9 @@
{{>common}}
```solidity
enum {{name}} {
{{#each members}}
{{name}}{{#unless @last}},{{/unless}}
{{/each}}
}
```

@ -0,0 +1,62 @@
import { HelperOptions, Utils } from 'handlebars';
export * from '../../common/helpers';
/**
* Returns a Markdown heading marker. An optional number increases the heading level.
*
* Input Output
* {{h}} {{name}} # Name
* {{h 2}} {{name}} ## Name
*/
export function h(opts: HelperOptions): string;
export function h(hsublevel: number, opts: HelperOptions): string;
export function h(hsublevel: number | HelperOptions, opts?: HelperOptions) {
const { hlevel } = getHLevel(hsublevel, opts);
return new Array(hlevel).fill('#').join('');
};
/**
* Delineates a section where headings should be increased by 1 or a custom number.
*
* {{#hsection}}
* {{>partial-with-headings}}
* {{/hsection}}
*/
export function hsection(opts: HelperOptions): string;
export function hsection(hsublevel: number, opts: HelperOptions): string;
export function hsection(this: unknown, hsublevel: number | HelperOptions, opts?: HelperOptions) {
let hlevel;
({ hlevel, opts } = getHLevel(hsublevel, opts));
opts.data = Utils.createFrame(opts.data);
opts.data.hlevel = hlevel;
return opts.fn(this as unknown, opts);
}
/**
* Returns a Markdown heading marker. An optional number increases the heading level.
*
* Input Output
* {{h}} {{name}} # Name
* {{h 2}} {{name}} ## Name
*/
export function s(opts: HelperOptions): string;
export function s(hsublevel: number, opts: HelperOptions): string;
export function s(hsublevel: number | HelperOptions, opts?: HelperOptions) {
return ' --- '
};
/**
* Helper for dealing with the optional hsublevel argument.
*/
function getHLevel(hsublevel: number | HelperOptions, opts?: HelperOptions) {
if (typeof hsublevel === 'number') {
opts = opts!;
hsublevel = Math.max(1, hsublevel);
} else {
opts = hsublevel;
hsublevel = 1;
}
const contextHLevel: number = opts.data?.hlevel ?? 0;
return { opts, hlevel: contextHLevel + hsublevel };
}

@ -0,0 +1,8 @@
# Solidity API
{{#each items}}
{{#hsection}}
{{>item}}
{{/hsection}}
{{/each}}

@ -0,0 +1,9 @@
{{>common}}
```solidity
struct {{name}} {
{{#each members}}
{{{typeName.typeDescriptions.typeString}}} {{name}};
{{/each}}
}
```

@ -0,0 +1,13 @@
import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site';
export class ItemError extends Error {
constructor(msg: string, item: DocItemWithContext) {
const ctx = item[DOC_ITEM_CONTEXT];
const src = ctx && ctx.build.decodeSrc(item);
if (src) {
super(msg + ` (${src})`);
} else {
super(msg);
}
}
}

@ -0,0 +1,5 @@
export function arraysEqual<T>(a: T[], b: T[]): boolean;
export function arraysEqual<T, U>(a: T[], b: T[], mapFn: (x: T) => U): boolean;
export function arraysEqual<T>(a: T[], b: T[], mapFn = (x: T) => x): boolean {
return a.length === b.length && a.every((x, i) => mapFn(x) === mapFn(b[i]!));
}

@ -0,0 +1 @@
export type AssertEqual<T, U> = [T, U] extends [U, T] ? true : never;

@ -0,0 +1,6 @@
/**
* Deep cloning good enough for simple objects like solc output. Types are not
* sound because the function may lose information: non-enumerable properties,
* symbols, undefined values, prototypes, etc.
*/
export const clone = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));

@ -0,0 +1,12 @@
// The function below would not be correctly typed if the return type was T[]
// because T may itself be an array type and Array.isArray would not know the
// difference. Adding IfArray<T> makes sure the return type is always correct.
type IfArray<T> = T extends any[] ? T : never;
export function ensureArray<T>(x: T | T[]): T[] | IfArray<T> {
if (Array.isArray(x)) {
return x;
} else {
return [x];
}
}

@ -0,0 +1,18 @@
/**
* Iterates over all contiguous matches of the regular expression over the
* text. Stops as soon as the regular expression no longer matches at the
* current position.
*/
export function* execAll(re: RegExp, text: string) {
re = new RegExp(re, re.flags + (re.sticky ? '' : 'y'));
while (true) {
const match = re.exec(text);
// We break out of the loop if there is no match or if the empty string is
// matched because no progress will be made and it will loop indefinitely.
if (!match?.[0]) break;
yield match;
}
}

@ -0,0 +1,5 @@
import path from 'path';
export function isChild(file: string, parent: string) {
return path.normalize(file + path.sep).startsWith(path.normalize(parent + path.sep));
}

@ -0,0 +1,7 @@
import { DocItem } from '../doc-item';
export function itemType(item: DocItem): string {
return item.nodeType
.replace(/(Definition|Declaration)$/, '')
.replace(/(\w)([A-Z])/g, '$1 $2');
}

@ -0,0 +1,4 @@
export function mapKeys<T>(obj: Record<string, T>, fn: (key: string) => string): Record<string, T> {
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [fn(k), v]));
}

@ -0,0 +1,19 @@
export function mapValues<T, U>(obj: Record<string, T>, fn: (value: T) => U): Record<string, U> {
const res: Record<string, U> = {};
for (const [k, v] of Object.entries(obj)) {
res[k] = fn(v);
}
return res;
}
export function filterValues<T, U extends T>(obj: Record<string, T>, fn: (value: T) => value is U): Record<string, U>;
export function filterValues<T>(obj: Record<string, T>, fn: (value: T) => boolean): Record<string, T>;
export function filterValues<T>(obj: Record<string, T>, fn: (value: T) => boolean): Record<string, T> {
const res: Record<string, T> = {};
for (const [k, v] of Object.entries(obj)) {
if (fn(v)) {
res[k] = v;
}
}
return res;
}

@ -0,0 +1,23 @@
export function defineGetterMemoized<K extends keyof any, T, O extends { [k in K]?: T }>(obj: O, key: K, getter: () => T) {
let state: 'todo' | 'doing' | 'done' = 'todo';
let value: T;
Object.defineProperty(obj, key, {
enumerable: true,
get() {
switch (state) {
case 'done':
return value;
case 'doing':
throw new Error("Detected recursion");
case 'todo':
state = 'doing';
value = getter();
state = 'done';
return value;
}
}
});
}

@ -0,0 +1,145 @@
import { FunctionDefinition } from 'solidity-ast';
import { findAll } from 'solidity-ast/utils';
import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site';
import { arraysEqual } from './arrays-equal';
import { execAll } from './execall';
import { itemType } from './item-type';
import { ItemError } from './ItemError';
import { readItemDocs } from './read-item-docs';
import { getContractsInScope } from './scope';
export interface NatSpec {
title?: string;
notice?: string;
dev?: string;
params?: {
name: string;
description: string;
}[];
returns?: {
name?: string;
description: string;
}[];
custom?: {
[tag: string]: string;
};
}
export function parseNatspec(item: DocItemWithContext): NatSpec {
if (!item[DOC_ITEM_CONTEXT]) throw new Error(`Not an item or item is missing context`);
let res: NatSpec = {};
const docSource = readItemDocs(item);
const docString = docSource !== undefined
? cleanUpDocstringFromSource(docSource)
: 'documentation' in item && item.documentation
? typeof item.documentation === 'string'
? item.documentation
: cleanUpDocstringFromSolc(item.documentation.text)
: '';
const tagMatches = execAll(
/^(?:@(\w+|custom:[a-z][a-z-]*) )?((?:(?!^@(?:\w+|custom:[a-z][a-z-]*) )[^])*)/m,
docString,
);
let inheritFrom: FunctionDefinition | undefined;
for (const [, tag = 'notice', content] of tagMatches) {
if (content === undefined) throw new ItemError('Unexpected error', item);
if (tag === 'dev' || tag === 'notice') {
res[tag] ??= '';
res[tag] += content;
}
if (tag === 'title') {
res.title = content.trim();
}
if (tag === 'param') {
const paramMatches = content.match(/(\w+) ([^]*)/);
if (paramMatches) {
const [, name, description] = paramMatches as [string, string, string];
res.params ??= [];
res.params.push({ name, description: description.trim() });
}
}
if (tag === 'return') {
if (!('returnParameters' in item)) {
throw new ItemError(`Item does not contain return parameters`, item);
}
res.returns ??= [];
const i = res.returns.length;
const p = item.returnParameters.parameters[i];
if (p === undefined) {
throw new ItemError('Got more @return tags than expected', item);
}
if (!p.name) {
res.returns.push({ description: content.trim() });
} else {
const paramMatches = content.match(/(\w+)( ([^]*))?/);
if (!paramMatches || paramMatches[1] !== p.name) {
throw new ItemError(`Expected @return tag to start with name '${p.name}'`, item);
}
const [, name, description] = paramMatches as [string, string, string?];
res.returns.push({ name, description: description?.trim() ?? '' });
}
}
if (tag?.startsWith('custom:')) {
const key = tag.replace(/^custom:/, '');
res.custom ??= {};
res.custom[key] ??= '';
res.custom[key] += content;
}
if (tag === 'inheritdoc') {
if (!(item.nodeType === 'FunctionDefinition' || item.nodeType === 'VariableDeclaration')) {
throw new ItemError(`Expected function or variable but saw ${itemType(item)}`, item);
}
const parentContractName = content.trim();
const parentContract = getContractsInScope(item)[parentContractName];
if (!parentContract) {
throw new ItemError(`Parent contract '${parentContractName}' not found`, item);
}
inheritFrom = [...findAll('FunctionDefinition', parentContract)].find(f => item.baseFunctions?.includes(f.id));
}
}
if (docString.length === 0) {
if ('baseFunctions' in item && item.baseFunctions?.length === 1) {
const baseFn = item[DOC_ITEM_CONTEXT].build.deref('FunctionDefinition', item.baseFunctions[0]!);
const shouldInherit = item.nodeType === 'VariableDeclaration' || arraysEqual(item.parameters.parameters, baseFn.parameters.parameters, p => p.name);
if (shouldInherit) {
inheritFrom = baseFn;
}
}
}
if (res.dev) res.dev = res.dev.trim();
if (res.notice) res.notice = res.notice.trim();
if (inheritFrom) {
res = { ...parseNatspec(inheritFrom as DocItemWithContext), ...res };
}
return res;
}
// Fix solc buggy parsing of doc comments.
// Reverse engineered from solc behavior.
function cleanUpDocstringFromSolc(text: string) {
return text
.replace(/\n\n?^[ \t]*(?:\*|\/\/\/)/mg, '\n\n')
.replace(/^[ \t]?/mg, '');
}
function cleanUpDocstringFromSource(text: string) {
return text
.replace(/^\/\*\*(.*)\*\/$/s, '$1')
.trim()
.replace(/^[ \t]*(\*|\/\/\/)[ \t]?/mg, '');
}

@ -0,0 +1,13 @@
export function normalizeContractPath(contractPath: string): string[]{
const paths = contractPath.split('/')
const filename = paths[paths.length - 1]
let folders = ''
for (let i = 0; i < paths.length - 1; i++) {
if(i !== paths.length -1) {
folders += `${paths[i]}/`
}
}
const resultingPath = `${folders}${filename}`
return [folders,resultingPath, filename]
}

@ -0,0 +1,26 @@
import { DocItemWithContext, DOC_ITEM_CONTEXT, Build } from '../site';
export function readItemDocs(item: DocItemWithContext): string | undefined {
const { build } = item[DOC_ITEM_CONTEXT];
// Note that Solidity 0.5 has item.documentation: string even though the
// types do not reflect that. This is why we check typeof === object.
if ('documentation' in item && item.documentation && typeof item.documentation === 'object') {
const { source, start, length } = decodeSrc(item.documentation.src, build);
const content = build.input.sources[source]?.content;
if (content !== undefined) {
return Buffer.from(content, 'utf8').slice(start, start + length).toString('utf8');
}
}
}
function decodeSrc(src: string, build: Build): { source: string; start: number; length: number } {
const [start, length, sourceId] = src.split(':').map(s => parseInt(s));
if (start === undefined || length === undefined || sourceId === undefined) {
throw new Error(`Bad source string ${src}`);
}
const source = Object.keys(build.output.sources).find(s => build.output.sources[s]?.id === sourceId);
if (source === undefined) {
throw new Error(`No source with id ${sourceId}`);
}
return { source, start, length };
}

@ -0,0 +1,63 @@
import { ContractDefinition, SourceUnit } from "solidity-ast";
import { findAll, isNodeType } from "solidity-ast/utils";
import { DocItemWithContext } from "../site";
import { filterValues, mapValues } from './map-values';
import { mapKeys } from './map-keys';
type Definition = SourceUnit['nodes'][number] & { name: string };
type Scope = { [name in string]: () => { namespace: Scope } | { definition: Definition } };
export function getContractsInScope(item: DocItemWithContext) {
const cache = new WeakMap<SourceUnit, Scope>();
return filterValues(
flattenScope(run(item.__item_context.file)),
isNodeType('ContractDefinition'),
);
function run(file: SourceUnit): Scope {
if (cache.has(file)) {
return cache.get(file)!;
}
const scope: Scope = {};
cache.set(file, scope);
for (const c of file.nodes) {
if ('name' in c) {
scope[c.name] = () => ({ definition: c });
}
}
for (const i of findAll('ImportDirective', file)) {
const importedFile = item.__item_context.build.deref('SourceUnit', i.sourceUnit);
const importedScope = run(importedFile);
if (i.unitAlias) {
scope[i.unitAlias] = () => ({ namespace: importedScope });
} else if (i.symbolAliases.length === 0) {
Object.assign(scope, importedScope);
} else {
for (const a of i.symbolAliases) {
// Delayed function call supports circular dependencies
scope[a.local ?? a.foreign.name] = importedScope[a.foreign.name] ?? (() => importedScope[a.foreign.name]!());
}
}
};
return scope;
}
}
function flattenScope(scope: Scope): Record<string, Definition> {
return Object.fromEntries(
Object.entries(scope).flatMap(([k, fn]) => {
const v = fn();
if ('definition' in v) {
return [[k, v.definition] as const];
} else {
return Object.entries(mapKeys(flattenScope(v.namespace), k2 => k + '.' + k2));
}
}),
);
}

@ -0,0 +1,37 @@
import { useState } from "react";
export function useLocalStorage(key: string, initialValue: any) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: any) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}

@ -0,0 +1,31 @@
import React from "react";
export const ErrorView: React.FC = () => {
return (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<img
style={{ paddingBottom: "2em" }}
width="250"
src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png"
alt="Error page"
/>
<h5>Sorry, something unexpected happened. </h5>
<h5>
Please raise an issue:{" "}
<a
style={{ color: "red" }}
href="https://github.com/Machinalabs/remix-ethdoc-plugin/issues"
>
Here
</a>
</h5>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Documentation generator</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<div id="root"></div>
</body>
</html>

@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./app/App";
// import { Routes } from "./routes";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);

@ -0,0 +1,17 @@
{
"name": "doc-gen",
"displayName": "Docgen - Documentation Generator",
"description": "Generate Solidity documentation (as md)",
"version": "0.1.0",
"events": [],
"methods": ["generateDocs", "openDocs"],
"kind": "none",
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxMDI0IiB3aWR0aD0iMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOTUwLjE1NCAxOTJINzMuODQ2QzMzLjEyNyAxOTIgMCAyMjUuMTI2OTk5OTk5OTk5OTUgMCAyNjUuODQ2djQ5Mi4zMDhDMCA3OTguODc1IDMzLjEyNyA4MzIgNzMuODQ2IDgzMmg4NzYuMzA4YzQwLjcyMSAwIDczLjg0Ni0zMy4xMjUgNzMuODQ2LTczLjg0NlYyNjUuODQ2QzEwMjQgMjI1LjEyNjk5OTk5OTk5OTk1IDk5MC44NzUgMTkyIDk1MC4xNTQgMTkyek01NzYgNzAzLjg3NUw0NDggNzA0VjUxMmwtOTYgMTIzLjA3N0wyNTYgNTEydjE5MkgxMjhWMzIwaDEyOGw5NiAxMjggOTYtMTI4IDEyOC0wLjEyNVY3MDMuODc1ek03NjcuMDkxIDczNS44NzVMNjA4IDUxMmg5NlYzMjBoMTI4djE5Mmg5Nkw3NjcuMDkxIDczNS44NzV6Ii8+PC9zdmc+",
"location": "sidePanel",
"documentation": "",
"repo": "https://github.com/ethereum/remix-project/",
"maintainedBy": "",
"authorContact": "",
"url": "",
"targets":["remix"]
}

@ -0,0 +1,11 @@
export type Documentation = string
export interface EthDocumentation {
[contractName: string]: Documentation
}
export type ContractName = string
export type FileName = string
export type PublishedSite = string

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
]
}

@ -0,0 +1,73 @@
const { composePlugins, withNx } = require('@nrwl/webpack')
const { withReact } = require('@nrwl/react')
const webpack = require('webpack')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), withReact(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
// add fallback for node modules
config.resolve.fallback = {
...config.resolve.fallback,
"path": require.resolve("path-browserify"),
"fs": false,
}
// add externals
config.externals = {
...config.externals,
solc: 'solc',
}
config.module.rules.push({
test: /\.hbs$/,
type: 'asset/source'
})
// add public path
config.output.publicPath = '/'
// add copy & provide plugin
config.plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'],
process: 'process/browser',
}),
new webpack.DefinePlugin({
}),
)
// souce-map loader
config.module.rules.push({
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
})
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
// set minimizer
config.optimization.minimizer = [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2015,
compress: false,
mangle: false,
format: {
comments: false,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
];
return config;
})

@ -0,0 +1,61 @@
{
"name": "doc-viewer",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/doc-viewer/src",
"projectType": "application",
"implicitDependencies": [
],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "development",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/doc-viewer",
"index": "apps/doc-viewer/src/index.html",
"baseHref": "./",
"main": "apps/doc-viewer/src/main.tsx",
"tsConfig": "apps/doc-viewer/tsconfig.app.json",
"assets": [
"apps/doc-viewer/src/favicon.ico",
"apps/doc-viewer/src/profile.json"
],
"styles": [],
"scripts": [],
"webpackConfig": "apps/doc-viewer/webpack.config.js"
},
"configurations": {
"development": {
},
"production": {
"fileReplacements": [
{
"replace": "apps/doc-viewer/src/environments/environment.ts",
"with": "apps/doc-viewer/src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "doc-viewer:build",
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {
"buildTarget": "doc-viewer:build:development",
"port": 7003
},
"production": {
"buildTarget": "doc-viewer:build:production"
}
}
}
},
"tags": []
}

@ -0,0 +1,23 @@
import React, { useEffect, useState } from "react"
import { DocViewer } from "./docviewer"
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const client = new DocViewer()
export default function App() {
const [contents, setContents] = useState('')
useEffect(() => {
client.eventEmitter.on('contentsReady', (fileContents: string) => {
setContents(fileContents)
})
}, [])
return (
<>
<div className="m-5 p-2">
<ReactMarkdown children={contents} remarkPlugins={[remarkGfm]}/>
</div>
</>
)
}

@ -0,0 +1,22 @@
import { PluginClient } from '@remixproject/plugin'
import { createClient } from '@remixproject/plugin-webview'
import EventEmitter from 'events'
export class DocViewer extends PluginClient {
mdFile: string
eventEmitter: EventEmitter
constructor() {
super()
this.eventEmitter = new EventEmitter()
this.methods = ['viewDocs']
createClient(this)
this.mdFile = ''
this.onload()
}
async viewDocs(docs: string[]) {
this.mdFile = docs[0]
const contents = await this.call('fileManager', 'readFile', this.mdFile)
this.eventEmitter.emit('contentsReady', contents)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Doc Viewer</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<div id="root"></div>
</body>
</html>

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app/App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);

@ -0,0 +1,16 @@
{
"name": "doc-viewer",
"displayName": "Docgen Viewer",
"description": "Visualize Solidity documentation from Docgen Plugin",
"version": "0.1.0",
"events": [],
"methods": ["viewDoc"],
"kind": "none",
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxMDI0IiB3aWR0aD0iMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOTUwLjE1NCAxOTJINzMuODQ2QzMzLjEyNyAxOTIgMCAyMjUuMTI2OTk5OTk5OTk5OTUgMCAyNjUuODQ2djQ5Mi4zMDhDMCA3OTguODc1IDMzLjEyNyA4MzIgNzMuODQ2IDgzMmg4NzYuMzA4YzQwLjcyMSAwIDczLjg0Ni0zMy4xMjUgNzMuODQ2LTczLjg0NlYyNjUuODQ2QzEwMjQgMjI1LjEyNjk5OTk5OTk5OTk1IDk5MC44NzUgMTkyIDk1MC4xNTQgMTkyek01NzYgNzAzLjg3NUw0NDggNzA0VjUxMmwtOTYgMTIzLjA3N0wyNTYgNTEydjE5MkgxMjhWMzIwaDEyOGw5NiAxMjggOTYtMTI4IDEyOC0wLjEyNVY3MDMuODc1ek03NjcuMDkxIDczNS44NzVMNjA4IDUxMmg5NlYzMjBoMTI4djE5Mmg5Nkw3NjcuMDkxIDczNS44NzV6Ii8+PC9zdmc+",
"location": "mainPanel",
"url": "",
"documentation": "https://remix-plugins.readthedocs.io/en/latest/",
"repo": "https://github.com/Machinalabs/remix-ethdoc-plugin/",
"maintainedBy": "Remix",
"authorContact": "remix@ethereum.org"
}

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
]
}

@ -0,0 +1,60 @@
const { composePlugins, withNx } = require('@nrwl/webpack')
const { withReact } = require('@nrwl/react')
const webpack = require('webpack')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), withReact(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
// add externals
config.externals = {
...config.externals,
solc: 'solc',
}
// add public path
config.output.publicPath = '/'
// add copy & provide plugin
config.plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'],
process: 'process/browser',
}),
new webpack.DefinePlugin({
}),
)
// souce-map loader
config.module.rules.push({
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
})
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
// set minimizer
config.optimization.minimizer = [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2015,
compress: false,
mangle: false,
format: {
comments: false,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
];
return config;
})

@ -3,9 +3,6 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/etherscan/src",
"projectType": "application",
"implicitDependencies": [
"remix-debug"
],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
@ -15,13 +12,14 @@
"compiler": "babel",
"outputPath": "dist/apps/etherscan",
"index": "apps/etherscan/src/index.html",
"baseHref": "/",
"baseHref": "./",
"main": "apps/etherscan/src/main.tsx",
"polyfills": "apps/etherscan/src/polyfills.ts",
"tsConfig": "apps/etherscan/tsconfig.app.json",
"assets": [
"apps/etherscan/src/favicon.ico",
"apps/etherscan/src/assets"
"apps/etherscan/src/assets",
"apps/etherscan/src/profile.json"
],
"styles": ["apps/etherscan/src/styles.css"],
"scripts": [],
@ -45,7 +43,8 @@
"defaultConfiguration": "development",
"options": {
"buildTarget": "etherscan:build",
"hmr": true
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {

@ -3,5 +3,5 @@ body {
}
#root {
padding: 5px;
padding: 8px 14px;
}

@ -1,6 +1,6 @@
import { PluginClient } from '@remixproject/plugin';
import { verify, EtherScanReturn } from './utils/verify';
import { getReceiptStatus, getEtherScanApi, getNetworkName } from './utils';
import { getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus } from './utils';
export class RemixClient extends PluginClient {
@ -8,20 +8,23 @@ export class RemixClient extends PluginClient {
return this.onload()
}
async verify (apiKey: string, contractAddress: string, contractArguments: string, contractName: string, compilationResultParam: any) {
const result = await verify(apiKey, contractAddress, contractArguments, contractName, compilationResultParam, this,
async verify (apiKey: string, contractAddress: string, contractArguments: string, contractName: string, compilationResultParam: any, chainRef?: number | string, isProxyContract?: boolean, expectedImplAddress?: string) {
const result = await verify(apiKey, contractAddress, contractArguments, contractName, compilationResultParam, chainRef, isProxyContract, expectedImplAddress, this,
(value: EtherScanReturn) => {}, (value: string) => {})
return result
}
async receiptStatus (receiptGuid: string, apiKey: string) {
async receiptStatus (receiptGuid: string, apiKey: string, isProxyContract: boolean) {
try {
const network = await getNetworkName(this)
const { network, networkId } = await getNetworkName(this)
if (network === "vm") {
throw new Error("Cannot check the receipt status in the selected network")
}
const etherscanApi = getEtherScanApi(network)
const receiptStatus = await getReceiptStatus(receiptGuid, apiKey, etherscanApi)
const etherscanApi = getEtherScanApi(networkId)
let receiptStatus
if (isProxyContract) receiptStatus = await getProxyContractReceiptStatus(receiptGuid, apiKey, etherscanApi)
else receiptStatus = await getReceiptStatus(receiptGuid, apiKey, etherscanApi)
return {
message: receiptStatus.result,
succeed: receiptStatus.status === '0' ? false : true

@ -13,7 +13,7 @@ import { DisplayRoutes } from "./routes"
import { useLocalStorage } from "./hooks/useLocalStorage"
import { getReceiptStatus, getEtherScanApi, getNetworkName } from "./utils"
import { getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus } from "./utils"
import { Receipt, ThemeType } from "./types"
import "./App.css"
@ -36,6 +36,7 @@ const App = () => {
const [receipts, setReceipts] = useLocalStorage("receipts", [])
const [contracts, setContracts] = useState([] as string[])
const [themeType, setThemeType] = useState("dark" as ThemeType)
const timer = useRef(null)
const clientInstanceRef = useRef(clientInstance)
clientInstanceRef.current = clientInstance
@ -80,56 +81,70 @@ const App = () => {
}, [])
useEffect(() => {
if (!clientInstance) {
return
}
const receiptsNotVerified: Receipt[] = receipts.filter((item: Receipt) => {
return item.status !== "Verified"
let receiptsNotVerified: Receipt[] = receipts.filter((item: Receipt) => {
return item.status === "Pending in queue" || item.status === "Max rate limit reached"
})
if (receiptsNotVerified.length > 0) {
const timer1 = setInterval(() => {
for (const item in receiptsNotVerified) {
if (timer.current) {
clearInterval(timer.current)
timer.current = null
}
receiptsNotVerified.forEach(async (item) => {
timer.current = setInterval(async () => {
const { network, networkId } = await getNetworkName(clientInstanceRef.current)
if (!clientInstanceRef.current) {
return {}
return
}
const network = await getNetworkName(clientInstanceRef.current)
if (network === "vm") {
return {}
return
}
const status = await getReceiptStatus(
let newReceipts = receipts
for (const item of receiptsNotVerified) {
await new Promise(r => setTimeout(r, 500)) // avoid api rate limit exceed.
let status
if (item.isProxyContract) {
status = await getProxyContractReceiptStatus(
item.guid,
apiKey,
getEtherScanApi(network)
getEtherScanApi(networkId)
)
if (status.result === "Pass - Verified") {
const newReceipts = receipts.map((currentReceipt: Receipt) => {
if (status.status === '1') {
status.message = status.result
status.result = 'Successfully Updated'
}
} else
status = await getReceiptStatus(
item.guid,
apiKey,
getEtherScanApi(networkId)
)
if (status.result === "Pass - Verified" || status.result === "Already Verified" ||
status.result === "Successfully Updated") {
newReceipts = newReceipts.map((currentReceipt: Receipt) => {
if (currentReceipt.guid === item.guid) {
return {
let res = {
...currentReceipt,
status: "Verified",
status: status.result,
}
if (currentReceipt.isProxyContract) res.message = status.message
return res
}
return currentReceipt
})
clearInterval(timer1)
setReceipts(newReceipts)
return () => {
clearInterval(timer1)
}
}
return {}
receiptsNotVerified = newReceipts.filter((item: Receipt) => {
return item.status === "Pending in queue" || item.status === "Max rate limit reached"
})
}, 5000)
if (timer.current && receiptsNotVerified.length === 0) {
clearInterval(timer.current)
timer.current = null
}
setReceipts(newReceipts)
}, 10000)
}
}, [receipts, clientInstance, apiKey, setReceipts])
}, [receipts])
return (
<AppContext.Provider

@ -1,8 +1,8 @@
import React from "react"
import { NavLink } from "react-router-dom"
import { CustomTooltip } from '@remix-ui/helper'
import { AppContext } from "../AppContext"
import { ThemeType } from "../types"
interface Props {
title?: string
@ -12,130 +12,69 @@ interface Props {
interface IconProps {
from: string
themeType: ThemeType
}
const HomeIcon: React.FC<IconProps> = ({ from, themeType }: IconProps) => {
const HomeIcon: React.FC<IconProps> = ({ from }: IconProps) => {
return (
<NavLink
data-id="home"
data-toggle="tooltip"
data-placement="top"
title="Home"
to={{
pathname: "/"
}}
state={ from }
style={isActive => {
return {
...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" }
}
}}
>
<svg
style={{ filter: "invert(0.5)" }}
width="1em"
height="1em"
viewBox="0 0 16 16"
className="bi bi-house-door-fill"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
<CustomTooltip
tooltipText='Home'
tooltipId='etherscan-nav-home'
placement='bottom'
>
<path d="M6.5 10.995V14.5a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .146-.354l6-6a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 .146.354v7a.5.5 0 0 1-.5.5h-4a.5.5 0 0 1-.5-.5V11c0-.25-.25-.5-.5-.5H7c-.25 0-.5.25-.5.495z" />
<path
fillRule="evenodd"
d="M13 2.5V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"
/>
</svg>
<i className="fas fa-home"></i>
</CustomTooltip>
</NavLink>
)
}
const SettingsIcon: React.FC<IconProps> = ({ from, themeType }: IconProps) => {
const ReceiptsIcon: React.FC<IconProps> = ({ from }: IconProps) => {
return (
<NavLink
data-toggle="tooltip"
data-placement="top"
title="Settings"
data-id="receipts"
to={{
pathname: "/settings",
}}
state= {from}
style={isActive => {
return {
...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" }
}
pathname: "/receipts"
}}
state={ from }
className="mx-2"
>
<svg
style={{ filter: "invert(0.5)" }}
width="1em"
height="1em"
viewBox="0 0 16 16"
className="bi bi-gear-fill"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
<CustomTooltip
tooltipText='Receipts'
tooltipId='etherscan-nav-receipts'
placement='bottom'
>
<path
fillRule="evenodd"
d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 0 0-5.86 2.929 2.929 0 0 0 0 5.858z"
/>
<path
fillRule="evenodd"
d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 0 0-5.86 2.929 2.929 0 0 0 0 5.858z"
/>
</svg>
<i className="fas fa-receipt"></i>
</CustomTooltip>
</NavLink>
)
}
const getStyleFilterIcon = (themeType: ThemeType) => {
const invert = themeType === "dark" ? 1 : 0
const brightness = themeType === "dark" ? "150" : "0" // should be >100 for icons with color
return {
filter: `invert(${invert}) grayscale(1) brightness(${brightness}%)`,
}
}
const ReceiptsIcon: React.FC<IconProps> = ({ from, themeType }: IconProps) => {
const SettingsIcon: React.FC<IconProps> = ({ from }: IconProps) => {
return (
<NavLink
data-toggle="tooltip"
data-placement="top"
title="Receipts"
data-id="settings"
to={{
pathname: "/receipts",
}}
state= { from }
style={isActive => {
return {
...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" }
}
pathname: "/settings"
}}
state= {from}
>
<svg
style={{ filter: "invert(0.5)" }}
width="1em"
height="1em"
viewBox="0 0 16 16"
className="bi bi-receipt"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
<CustomTooltip
tooltipText='Settings'
tooltipId='etherscan-nav-settings'
placement='bottom'
>
<path
fillRule="evenodd"
d="M1.92.506a.5.5 0 0 1 .434.14L3 1.293l.646-.647a.5.5 0 0 1 .708 0L5 1.293l.646-.647a.5.5 0 0 1 .708 0L7 1.293l.646-.647a.5.5 0 0 1 .708 0L9 1.293l.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .801.13l.5 1A.5.5 0 0 1 15 2v12a.5.5 0 0 1-.053.224l-.5 1a.5.5 0 0 1-.8.13L13 14.707l-.646.647a.5.5 0 0 1-.708 0L11 14.707l-.646.647a.5.5 0 0 1-.708 0L9 14.707l-.646.647a.5.5 0 0 1-.708 0L7 14.707l-.646.647a.5.5 0 0 1-.708 0L5 14.707l-.646.647a.5.5 0 0 1-.708 0L3 14.707l-.646.647a.5.5 0 0 1-.801-.13l-.5-1A.5.5 0 0 1 1 14V2a.5.5 0 0 1 .053-.224l.5-1a.5.5 0 0 1 .367-.27zm.217 1.338L2 2.118v11.764l.137.274.51-.51a.5.5 0 0 1 .707 0l.646.647.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.509.509.137-.274V2.118l-.137-.274-.51.51a.5.5 0 0 1-.707 0L12 1.707l-.646.647a.5.5 0 0 1-.708 0L10 1.707l-.646.647a.5.5 0 0 1-.708 0L8 1.707l-.646.647a.5.5 0 0 1-.708 0L6 1.707l-.646.647a.5.5 0 0 1-.708 0L4 1.707l-.646.647a.5.5 0 0 1-.708 0l-.509-.51z"
/>
<path
fillRule="evenodd"
d="M3 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm8-6a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5z"
/>
</svg>
<i className="fas fa-cog"></i>
</CustomTooltip>
</NavLink>
)
}
export const HeaderWithSettings: React.FC<Props> = ({
title = "",
showBackButton = false,
@ -143,15 +82,13 @@ export const HeaderWithSettings: React.FC<Props> = ({
}) => {
return (
<AppContext.Consumer>
{({ themeType }) => (
{() => (
<div>
<h6>{title}</h6>
<h6 className="d-inline">{title}</h6>
<div style={{ float: "right" }}>
<HomeIcon from={from} themeType={themeType} />
<ReceiptsIcon from={from} themeType={themeType} />
<SettingsIcon from={from} themeType={themeType} />
<HomeIcon from={from} />
<ReceiptsIcon from={from} />
<SettingsIcon from={from} />
</div>
</div>
)}

@ -1,26 +1,34 @@
import React from "react"
import { CustomTooltip } from '@remix-ui/helper'
interface Props {
text: string
isSubmitting?: boolean
dataId?: string
disable?: boolean
}
export const SubmitButton: React.FC<Props> = ({
text,
dataId,
isSubmitting = false,
disable = true
}) => {
return (
<CustomTooltip
tooltipText={disable ? "Fill the fields with valid values" : "Click to proceed"}
tooltipId='etherscan-submit-button'
placement='bottom'
>
<div>
<button
data-id={dataId}
style={{ padding: "0.25rem 0.4rem", marginRight: "0.5em" }}
type="submit"
className="btn btn-primary"
disabled={isSubmitting}
className="btn btn-primary btn-block text-decoration-none"
disabled={disable}
>
{!isSubmitting && text}
{isSubmitting && (
<div>
<span
@ -33,5 +41,7 @@ export const SubmitButton: React.FC<Props> = ({
</div>
)}
</button>
</div>
</CustomTooltip>
)
}

@ -4,15 +4,17 @@ import { HeaderWithSettings } from "../components"
interface Props {
from: string
title?: string
}
export const DefaultLayout: React.FC<PropsWithChildren<Props>> = ({
children,
from,
title
}) => {
return (
<div>
<HeaderWithSettings from={from} />
<HeaderWithSettings from={from} title={title} />
{children}
</div>
)

@ -31,19 +31,19 @@ export const DisplayRoutes = () => (
<Routes>
<Route
path="/"
element={<DefaultLayout from="/">
element={<DefaultLayout from="/" title="Verify Smart Contracts">
<HomeView />
</DefaultLayout>} />
<Route path="/error"
element={<ErrorView />} />
<Route
path="/receipts"
element={<DefaultLayout from="/receipts">
element={<DefaultLayout from="/receipts" title="Check Receipt GUID Status">
<ReceiptsView />
</DefaultLayout>} />
<Route
path="/settings"
element={<DefaultLayout from="/settings">
element={<DefaultLayout from="/settings" title="Set Explorer API Key">
<CaptureKeyView />
</DefaultLayout>} />
</Routes>

@ -1,6 +1,9 @@
export type ReceiptStatus = "Verified" | "Queue"
export type ReceiptStatus = "Pending in queue" | "Pass - Verified" | "Already Verified" | "Max rate limit reached" | "Successfully Updated"
export interface Receipt {
guid: string
status: ReceiptStatus
isProxyContract: boolean
message?: string
succeed?: boolean
}

@ -0,0 +1,36 @@
export const scanAPIurls = {
// all mainnet
1: "https://api.etherscan.io/api",
56: "https://api.bscscan.com/api",
137: "https://api.polygonscan.com/api",
250: "https://api.ftmscan.com/api",
42161: "https://api.arbiscan.io/api",
43114: "https://api.snowtrace.io/api",
1285: "https://api-moonriver.moonscan.io/api",
1284: "https://api-moonbeam.moonscan.io/api",
25: "https://api.cronoscan.com/api",
199: "https://api.bttcscan.com/api",
10: "https://api-optimistic.etherscan.io/api",
42220: "https://api.celoscan.io/api",
288: "https://api.bobascan.com/api",
100: "https://api.gnosisscan.io/api",
1101: "https://api-zkevm.polygonscan.com/api",
// all testnet
5: "https://api-goerli.etherscan.io/api",
11155111: "https://api-sepolia.etherscan.io/api",
97: "https://api-testnet.bscscan.com/api",
80001: "https://api-testnet.polygonscan.com/api",
4002: "https://api-testnet.ftmscan.com/api",
421611: "https://api-testnet.arbiscan.io/api",
42170: "https://api-nova.arbiscan.io/api",
43113: "https://api-testnet.snowtrace.io/api",
1287: "https://api-moonbase.moonscan.io/api",
338: "https://api-testnet.cronoscan.com/api",
1028: "https://api-testnet.bttcscan.com/api",
420: "https://api-goerli-optimistic.etherscan.io/api",
44787: "https://api-alfajores.celoscan.io/api",
2888: "https://api-testnet.bobascan.com/api",
84531: "https://api-goerli.basescan.org/api",
1442: "https://api-testnet-zkevm.polygonscan.com/api"
}

@ -1,25 +1,30 @@
export const verifyScript = `
/**
* @param {string} apikey - etherscan api key.
* @param {string} contractAddress - Address of the contract to verify.
* @param {string} contractArguments - Parameters used in the contract constructor during the initial deployment. It should be the hex encoded value.
* @param {string} apikey - etherscan api key
* @param {string} contractAddress - Address of the contract to verify
* @param {string} contractArguments - Parameters used in the contract constructor during the initial deployment. It should be the hex encoded value
* @param {string} contractName - Name of the contract
* @param {string} contractFile - File where the contract is located
* @param {number | string} chainRef - Network chain id or API URL (optional)
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional)
* @param {string} expectedImplAddress - Implementation contract address, in case of proxy contract verification (optional)
* @returns {{ guid, status, message, succeed }} verification result
*/
export const verify = async (apikey: string, contractAddress: string, contractArguments: string, contractName: string, contractFile: string) => {
export const verify = async (apikey: string, contractAddress: string, contractArguments: string, contractName: string, contractFile: string, chainRef?: number | string, isProxyContract?: boolean, expectedImplAddress?: string) => {
const compilationResultParam = await remix.call('compilerArtefacts' as any, 'getCompilerAbstract', contractFile)
console.log('verifying.. ' + contractName)
return await remix.call('etherscan' as any, 'verify', apikey, contractAddress, contractArguments, contractName, compilationResultParam)
// update apiKey and chainRef to verify contract on multiple networks
return await remix.call('etherscan' as any, 'verify', apikey, contractAddress, contractArguments, contractName, compilationResultParam, chainRef, isProxyContract, expectedImplAddress)
}`
export const receiptGuidScript = `
/**
* @param {string} apikey - etherscan api key.
* @param {string} guid - receipt id.
* @param {string} apikey - etherscan api key
* @param {string} guid - receipt id
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional)
* @returns {{ status, message, succeed }} receiptStatus
*/
export const receiptStatus = async (apikey: string, guid: string) => {
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey)
export const receiptStatus = async (apikey: string, guid: string, isProxyContract?: boolean) => {
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey, isProxyContract)
}
`

@ -1,5 +1,6 @@
import { PluginClient } from "@remixproject/plugin"
import axios from 'axios'
import { scanAPIurls } from "./networks"
type RemixClient = PluginClient
/*
@ -13,10 +14,12 @@ export type receiptStatus = {
status: string
}
export const getEtherScanApi = (network: string) => {
return network === "main"
? `https://api.etherscan.io/api`
: `https://api-${network}.etherscan.io/api`
export const getEtherScanApi = (networkId: any) => {
if (!(networkId in scanAPIurls)) {
throw new Error("no known network to verify against")
}
const apiUrl = (scanAPIurls as any)[networkId]
return apiUrl
}
export const getNetworkName = async (client: RemixClient) => {
@ -24,7 +27,7 @@ export const getNetworkName = async (client: RemixClient) => {
if (!network) {
throw new Error("no known network to verify against")
}
return network.name!.toLowerCase()
return { network: network.name!.toLowerCase(), networkId: network.id }
}
export const getReceiptStatus = async (
@ -45,3 +48,22 @@ export const getReceiptStatus = async (
console.error(error)
}
}
export const getProxyContractReceiptStatus = async (
receiptGuid: string,
apiKey: string,
etherscanApi: string
): Promise<receiptStatus> => {
const params = `guid=${receiptGuid}&module=contract&action=checkproxyverification&apiKey=${apiKey}`
try {
const response = await axios.get(`${etherscanApi}?${params}`)
const { result, message, status } = response.data
return {
result,
message,
status,
}
} catch (error) {
console.error(error)
}
}

@ -1,4 +1,4 @@
import { getNetworkName, getEtherScanApi, getReceiptStatus } from "../utils"
import { getNetworkName, getEtherScanApi, getReceiptStatus, getProxyContractReceiptStatus } from "../utils"
import { CompilationResult } from "@remixproject/plugin-api"
import { CompilerAbstract } from '@remix-project/remix-solidity'
import axios from 'axios'
@ -21,18 +21,33 @@ export const verify = async (
contractArgumentsParam: string,
contractName: string,
compilationResultParam: CompilerAbstract,
chainRef: number | string,
isProxyContract: boolean,
expectedImplAddress: string,
client: PluginClient,
onVerifiedContract: (value: EtherScanReturn) => void,
setResults: (value: string) => void
) => {
const network = await getNetworkName(client)
let networkChainId
let etherscanApi
if (chainRef) {
if (typeof chainRef === 'number') {
networkChainId = chainRef
etherscanApi = getEtherScanApi(networkChainId)
} else if (typeof chainRef === 'string') etherscanApi = chainRef
} else {
const { network, networkId } = await getNetworkName(client)
if (network === "vm") {
return {
succeed: false,
message: "Cannot verify in the selected network"
}
} else {
networkChainId = networkId
etherscanApi = getEtherScanApi(networkChainId)
}
const etherscanApi = getEtherScanApi(network)
}
try {
const contractMetadata = getContractMetadata(
@ -72,13 +87,20 @@ export const verify = async (
module: "contract", // Do not change
action: "verifysourcecode", // Do not change
codeformat: "solidity-standard-json-input",
contractaddress: contractAddress, // Contract Address starts with 0x...
sourceCode: JSON.stringify(jsonInput),
contractname: fileName + ':' + contractName,
compilerversion: `v${contractMetadataParsed.compiler.version}`, // see http://etherscan.io/solcversions for list of support versions
constructorArguements: contractArgumentsParam ? contractArgumentsParam.replace('0x', '') : '', // if applicable
}
if (isProxyContract) {
data.action = "verifyproxycontract"
data.expectedimplementation = expectedImplAddress
data.address = contractAddress
} else {
data.contractaddress = contractAddress
}
const body = new FormData()
Object.keys(data).forEach((key) => body.append(key, data[key]))
@ -92,7 +114,18 @@ export const verify = async (
if (message === "OK" && status === "1") {
resetAfter10Seconds(client, setResults)
const receiptStatus = await getReceiptStatus(
let receiptStatus
if (isProxyContract) {
receiptStatus = await getProxyContractReceiptStatus(
result,
apiKeyParam,
etherscanApi
)
if (receiptStatus.status === '1') {
receiptStatus.message = receiptStatus.result
receiptStatus.result = 'Successfully Updated'
}
} else receiptStatus = await getReceiptStatus(
result,
apiKeyParam,
etherscanApi
@ -102,7 +135,8 @@ export const verify = async (
guid: result,
status: receiptStatus.result,
message: `Verification process started correctly. Receipt GUID ${result}`,
succeed: true
succeed: true,
isProxyContract
}
onVerifiedContract(returnValue)
return returnValue
@ -114,7 +148,8 @@ export const verify = async (
})
const returnValue = {
message: result,
succeed: false
succeed: false,
isProxyContract
}
resetAfter10Seconds(client, setResults)
return returnValue

@ -11,8 +11,9 @@ export const CaptureKeyView: React.FC = () => {
const navigate = useNavigate()
return (
<AppContext.Consumer>
{({ apiKey, setAPIKey }) => (
<Formik
{({ apiKey, clientInstance, setAPIKey }) => {
if (!apiKey && clientInstance && clientInstance.call) clientInstance.call('notification' as any, 'toast', 'Please add API key to continue')
return <Formik
initialValues={{ apiKey }}
validate={(values) => {
const errors = {} as any
@ -22,23 +23,27 @@ export const CaptureKeyView: React.FC = () => {
return errors
}}
onSubmit={(values) => {
const apiKey = values.apiKey
if (apiKey.length === 34) {
setAPIKey(values.apiKey)
navigate((location.state as any).from)
clientInstance.call('notification' as any, 'toast', 'API key saved successfully!!!')
navigate((location && location.state ? location.state : '/'))
} else clientInstance.call('notification' as any, 'toast', 'API key should be 34 characters long')
}}
>
{({ errors, touched, handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div className="form-group" style={{ marginBottom: "0.5rem" }}>
<label htmlFor="apikey">Please Enter your API key</label>
<label htmlFor="apikey">API Key</label>
<Field
className={
errors.apiKey && touched.apiKey
? "form-control form-control-sm is-invalid"
: "form-control form-control-sm"
}
type="text"
type="password"
name="apiKey"
placeholder="Example: GM1T20XY6JGSAPWKDCYZ7B2FJXKTJRFVGZ"
placeholder="e.g. GM1T20XY6JGSAPWKDCYZ7B2FJXKTJRFVGZ"
/>
<ErrorMessage
className="invalid-feedback"
@ -48,12 +53,13 @@ export const CaptureKeyView: React.FC = () => {
</div>
<div>
<SubmitButton text="Save API key" dataId="save-api-key" />
<SubmitButton text="Save" dataId="save-api-key" disable={false} />
</div>
</form>
)}
</Formik>
)}
}
}
</AppContext.Consumer>
)
}

@ -11,8 +11,9 @@ export const HomeView: React.FC = () => {
// const [hasError, setHasError] = useState(false)
return (
<AppContext.Consumer>
{({ apiKey, clientInstance, setReceipts, receipts, contracts }) =>
!apiKey ? (
{({ apiKey, clientInstance, setReceipts, receipts, contracts }) => {
if (!apiKey && clientInstance && clientInstance.call) clientInstance.call('notification' as any, 'toast', 'Please add API key to continue')
return !apiKey ? (
<Navigate
to={{
pathname: "/settings"
@ -25,12 +26,12 @@ export const HomeView: React.FC = () => {
apiKey={apiKey}
onVerifiedContract={(receipt: Receipt) => {
const newReceipts = [...receipts, receipt]
setReceipts(newReceipts)
}}
/>
)
}
}
</AppContext.Consumer>
)
}

@ -1,45 +1,71 @@
import React, { useState } from "react"
import { Formik, ErrorMessage, Field } from "formik"
import { getEtherScanApi, getNetworkName, getReceiptStatus } from "../utils"
import { getEtherScanApi, getNetworkName, getReceiptStatus, getProxyContractReceiptStatus } from "../utils"
import { Receipt } from "../types"
import { AppContext } from "../AppContext"
import { SubmitButton } from "../components"
import { Navigate } from "react-router-dom"
import { Button } from "react-bootstrap"
import { CustomTooltip } from '@remix-ui/helper'
interface FormValues {
receiptGuid: string
}
export const ReceiptsView: React.FC = () => {
const [results, setResults] = useState("")
const [results, setResults] = useState({succeed: false, message: ''})
const [isProxyContractReceipt, setIsProxyContractReceipt] = useState(false)
const onGetReceiptStatus = async (
values: FormValues,
clientInstance: any,
apiKey: string
) => {
try {
const network = await getNetworkName(clientInstance)
const { network, networkId } = await getNetworkName(clientInstance)
if (network === "vm") {
setResults("Cannot verify in the selected network")
setResults({
succeed: false,
message: "Cannot verify in the selected network"
})
return
}
const etherscanApi = getEtherScanApi(network)
const result = await getReceiptStatus(
const etherscanApi = getEtherScanApi(networkId)
let result
if (isProxyContractReceipt) {
result = await getProxyContractReceiptStatus(
values.receiptGuid,
apiKey,
etherscanApi
)
setResults(result.result)
if (result.status === '1') {
result.message = result.result
result.result = 'Successfully Updated'
}
} else
result = await getReceiptStatus(
values.receiptGuid,
apiKey,
etherscanApi
)
setResults({
succeed: result.status === '1' ? true : false,
message: result.result || (result.status === '0' ? 'Verification failed' : result.message)
})
} catch (error: any) {
setResults(error.message)
setResults({
succeed: false,
message: error.message
})
}
}
return (
<AppContext.Consumer>
{({ apiKey, clientInstance, receipts }) =>
!apiKey ? (
{({ apiKey, clientInstance, receipts, setReceipts }) => {
if (!apiKey && clientInstance && clientInstance.call) clientInstance.call('notification' as any, 'toast', 'Please add API key to continue')
return !apiKey ? (
<Navigate
to={{
pathname: "/settings"
@ -60,13 +86,12 @@ export const ReceiptsView: React.FC = () => {
onGetReceiptStatus(values, clientInstance, apiKey)
}
>
{({ errors, touched, handleSubmit }) => (
{({ errors, touched, handleSubmit, handleChange }) => (
<form onSubmit={handleSubmit}>
<div
className="form-group"
style={{ marginBottom: "0.5rem" }}
>
<h6>Get your Receipt GUID status</h6>
<label htmlFor="receiptGuid">Receipt GUID</label>
<Field
className={
@ -84,7 +109,21 @@ export const ReceiptsView: React.FC = () => {
/>
</div>
<SubmitButton text="Check" />
<div className="d-flex mb-2 custom-control custom-checkbox">
<Field
className="custom-control-input"
type="checkbox"
name="isProxyReceipt"
id="isProxyReceipt"
onChange={async (e) => {
handleChange(e)
if (e.target.checked) setIsProxyContractReceipt(true)
else setIsProxyContractReceipt(false)
}}
/>
<label className="form-check-label custom-control-label" htmlFor="isProxyReceipt">It's a proxy contract GUID</label>
</div>
<SubmitButton text="Check" disable = {!touched.receiptGuid || (touched.receiptGuid && errors.receiptGuid) ? true : false} />
</form>
)}
</Formik>
@ -94,27 +133,36 @@ export const ReceiptsView: React.FC = () => {
marginTop: "2em",
fontSize: "0.8em",
textAlign: "center",
color: results['succeed'] ? "green" : "red"
}}
dangerouslySetInnerHTML={{ __html: results }}
dangerouslySetInnerHTML={{ __html: results.message ? results.message : '' }}
/>
<ReceiptsTable receipts={receipts} />
<ReceiptsTable receipts={receipts} /><br/>
<CustomTooltip
tooltipText="Clear the list of receipts"
tooltipId='etherscan-clear-receipts'
placement='bottom'
>
<Button onClick={() => { setReceipts([]) }} >Clear</Button>
</CustomTooltip>
</div>
)
}
}
</AppContext.Consumer>
)
}
const ReceiptsTable: React.FC<{ receipts: Receipt[] }> = ({ receipts }) => {
return (
<div className="table-responsive" style={{ fontSize: "0.7em" }}>
<div className="table-responsive" style={{ fontSize: "0.8em" }}>
<h6>Receipts</h6>
<table className="table table-sm">
<thead>
<tr>
<th scope="col">Guid</th>
<th scope="col">Status</th>
<th scope="col">GUID</th>
</tr>
</thead>
<tbody>
@ -123,8 +171,22 @@ const ReceiptsTable: React.FC<{ receipts: Receipt[] }> = ({ receipts }) => {
receipts.map((item: Receipt, index) => {
return (
<tr key={item.guid}>
<td className={(item.status === 'Pass - Verified' || item.status === 'Successfully Updated')
? 'text-success' : (item.status === 'Pending in queue'
? 'text-warning' : (item.status === 'Already Verified'
? 'text-info': 'text-secondary'))}>
{item.status}
{item.status === 'Successfully Updated' && <CustomTooltip
placement={'bottom'}
tooltipClasses="text-wrap"
tooltipId="etherscan-receipt-proxy-status"
tooltipText={item.message}
>
<i style={{ fontSize: 'small' }} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i>
</CustomTooltip>
}
</td>
<td>{item.guid}</td>
<td>{item.status}</td>
</tr>
)
})}

@ -1,14 +1,16 @@
import React, { useState } from "react"
import React, { useEffect, useRef, useState } from "react"
import Web3 from 'web3'
import {
PluginClient,
} from "@remixproject/plugin"
import { CustomTooltip } from '@remix-ui/helper'
import { Formik, ErrorMessage, Field } from "formik"
import { SubmitButton } from "../components"
import { Receipt } from "../types"
import { verify } from "../utils/verify"
import { receiptGuidScript, verifyScript } from "../utils/scripts"
import { etherscanScripts } from "@remix-project/remix-ws-templates"
interface Props {
client: PluginClient
@ -19,12 +21,10 @@ interface Props {
interface FormValues {
contractName: string
contractArguments: string
contractAddress: string
expectedImplAddress?: string
}
export const VerifyView: React.FC<Props> = ({
apiKey,
client,
@ -32,6 +32,23 @@ export const VerifyView: React.FC<Props> = ({
onVerifiedContract,
}) => {
const [results, setResults] = useState("")
const [networkName, setNetworkName] = useState("Loading...")
const [showConstructorArgs, setShowConstructorArgs] = useState(false)
const [isProxyContract, setIsProxyContract] = useState(false)
const [constructorInputs, setConstructorInputs] = useState([])
const verificationResult = useRef({})
useEffect(() => {
if (client && client.on) {
client.on("blockchain" as any, 'networkStatus', (result) => {
setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : ''}`)
})
}
return () => {
// To fix memory leak
if (client && client.off) client.off("blockchain" as any, 'networkStatus')
}
}, [client])
const onVerifyContract = async (values: FormValues) => {
const compilationResult = (await client.call(
@ -43,20 +60,29 @@ export const VerifyView: React.FC<Props> = ({
throw new Error("no compilation result available")
}
const contractArguments = values.contractArguments.replace("0x", "")
const constructorValues = []
for (const key in values) {
if (key.startsWith('contractArgValue')) constructorValues.push(values[key])
}
const web3 = new Web3()
const constructorTypes = constructorInputs.map(e => e.type)
let contractArguments = web3.eth.abi.encodeParameters(constructorTypes, constructorValues)
contractArguments = contractArguments.replace("0x", "")
const verificationResult = await verify(
verificationResult.current = await verify(
apiKey,
values.contractAddress,
contractArguments,
values.contractName,
compilationResult,
null,
isProxyContract,
values.expectedImplAddress,
client,
onVerifiedContract,
setResults,
)
setResults(verificationResult.message)
setResults(verificationResult.current['message'])
}
return (
@ -64,8 +90,7 @@ export const VerifyView: React.FC<Props> = ({
<Formik
initialValues={{
contractName: "",
contractArguments: "",
contractAddress: "",
contractAddress: ""
}}
validate={(values) => {
const errors = {} as any
@ -75,52 +100,51 @@ export const VerifyView: React.FC<Props> = ({
if (!values.contractAddress) {
errors.contractAddress = "Required"
}
if (values.contractAddress.trim() === "") {
if (values.contractAddress.trim() === "" || !values.contractAddress.startsWith('0x')
|| values.contractAddress.length !== 42) {
errors.contractAddress = "Please enter a valid contract address"
}
return errors
}}
onSubmit={(values) => onVerifyContract(values)}
>
{({ errors, touched, handleSubmit, isSubmitting }) => (
<form onSubmit={handleSubmit}>
<h6>Verify your smart contracts</h6>
<button
type="button"
style={{ padding: "0.25rem 0.4rem", marginRight: "0.5em", marginBottom: "0.5em"}}
className="btn btn-primary"
title="Generate the necessary helpers to start the verification from a TypeScript script"
onClick={async () => {
if (!await client.call('fileManager', 'exists' as any, 'scripts/etherscan/receiptStatus.ts')) {
await client.call('fileManager', 'writeFile', 'scripts/etherscan/receiptStatus.ts', receiptGuidScript)
await client.call('fileManager', 'open', 'scripts/etherscan/receiptStatus.ts')
} else {
client.call('notification' as any, 'toast', 'file receiptStatus.ts already present..')
}
{({ errors, touched, handleSubmit, handleChange, isSubmitting }) => {
return (<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="network">Selected Network</label>
<Field
className="form-control"
type="text"
name="network"
value={networkName}
disabled={true}
/>
</div>
if (!await client.call('fileManager', 'exists' as any, 'scripts/etherscan/verify.ts')) {
await client.call('fileManager', 'writeFile', 'scripts/etherscan/verify.ts', verifyScript)
await client.call('fileManager', 'open', 'scripts/etherscan/verify.ts')
} else {
client.call('notification' as any, 'toast', 'file verify.ts already present..')
}
}}
>
Generate Etherscan helper scripts
</button>
<div className="form-group">
<label htmlFor="contractName">Contract</label>
<label htmlFor="contractName">Contract Name</label>
<Field
as="select"
className={
errors.contractName && touched.contractName
? "form-control form-control-sm is-invalid"
: "form-control form-control-sm"
errors.contractName && touched.contractName && contracts.length
? "form-control is-invalid"
: "form-control"
}
name="contractName"
onChange={async (e) => {
handleChange(e)
const {artefact} = await client.call("compilerArtefacts" as any, "getArtefactsByContractName", e.target.value)
if (artefact && artefact.abi && artefact.abi[0] && artefact.abi[0].type && artefact.abi[0].type === 'constructor' && artefact.abi[0].inputs.length > 0) {
setConstructorInputs(artefact.abi[0].inputs)
setShowConstructorArgs(true)
} else {
setConstructorInputs([])
setShowConstructorArgs(false)
}
}}
>
<option disabled={true} value="">
Select a contract
{ contracts.length ? 'Select a contract' : `--- No compiled contracts ---` }
</option>
{contracts.map((item) => (
<option key={item} value={item}>
@ -135,23 +159,36 @@ export const VerifyView: React.FC<Props> = ({
/>
</div>
<div className="form-group">
<label htmlFor="contractArguments">Constructor Arguments</label>
<div className={ showConstructorArgs ? 'form-group d-block': 'form-group d-none' } >
<label>Constructor Arguments</label>
{constructorInputs.map((item, index) => {
return (
<div className="d-flex">
<Field
className={
errors.contractArguments && touched.contractArguments
? "form-control form-control-sm is-invalid"
: "form-control form-control-sm"
}
className="form-control m-1"
type="text"
name="contractArguments"
placeholder="hex encoded"
key={`contractArgName${index}`}
name={`contractArgName${index}`}
value={item.name}
disabled={true}
/>
<ErrorMessage
className="invalid-feedback"
name="contractArguments"
component="div"
<CustomTooltip
tooltipText={`value of ${item.name}`}
tooltipId={`etherscan-constructor-value${index}`}
placement='top'
>
<Field
className="form-control m-1"
type="text"
key={`contractArgValue${index}`}
name={`contractArgValue${index}`}
placeholder={item.type}
/>
</CustomTooltip>
</div>
)}
)}
</div>
<div className="form-group">
@ -159,12 +196,12 @@ export const VerifyView: React.FC<Props> = ({
<Field
className={
errors.contractAddress && touched.contractAddress
? "form-control form-control-sm is-invalid"
: "form-control form-control-sm"
? "form-control is-invalid"
: "form-control"
}
type="text"
name="contractAddress"
placeholder="i.e. 0x11b79afc03baf25c631dd70169bb6a3160b2706e"
placeholder="e.g. 0x11b79afc03baf25c631dd70169bb6a3160b2706e"
/>
<ErrorMessage
className="invalid-feedback"
@ -173,13 +210,80 @@ export const VerifyView: React.FC<Props> = ({
/>
</div>
<SubmitButton dataId="verify-contract" text="Verify Contract" isSubmitting={isSubmitting} />
<div className="d-flex mb-2 custom-control custom-checkbox">
<Field
className="custom-control-input"
type="checkbox"
name="isProxy"
id="isProxy"
onChange={async (e) => {
handleChange(e)
if (e.target.checked) setIsProxyContract(true)
else setIsProxyContract(false)
}}
/>
<label className="form-check-label custom-control-label" htmlFor="isProxy">It's a proxy contract</label>
</div>
<div className={ isProxyContract ? 'form-group d-block': 'form-group d-none' }>
<label htmlFor="expectedImplAddress">Expected Implementation Address</label>
<CustomTooltip
placement={'top'}
tooltipClasses="text-wrap"
tooltipId="etherscan-impl-address-info"
tooltipText="Make sure implementation contract is already verified before proxy contract"
>
<i style={{ fontSize: 'small' }} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i>
</CustomTooltip>
<CustomTooltip
tooltipText='Providing expected implementation address enforces a check to ensure the returned implementation contract address is same as address picked up by the verifier'
tooltipId='etherscan-impl-address'
placement='bottom'
>
<Field
className="form-control"
type="text"
name="expectedImplAddress"
placeholder="e.g. 0x11b79afc03baf25c631dd70169bb6a3160b2706e"
/>
</CustomTooltip>
</div>
<SubmitButton dataId="verify-contract" text="Verify"
isSubmitting={isSubmitting}
disable={ !contracts.length ||
!touched.contractName ||
!touched.contractAddress ||
(touched.contractName && errors.contractName) ||
(touched.contractAddress && errors.contractAddress)
? true
: false}
/>
<br/>
<CustomTooltip
tooltipText='Generate the required TS scripts to verify a contract on Etherscan'
tooltipId='etherscan-generate-scripts'
placement='bottom'
>
<button
type="button"
style={{ padding: "0.25rem 0.4rem", marginRight: "0.5em", marginBottom: "0.5em"}}
className="btn btn-secondary btn-block"
onClick={async () => {
etherscanScripts(client)
}}
>
Generate Verification Scripts
</button>
</CustomTooltip>
</form>
)}
)
}
}
</Formik>
<div data-id="verify-result"
style={{ marginTop: "2em", fontSize: "0.8em", textAlign: "center" }}
style={{ marginTop: "2em", fontSize: "0.8em", textAlign: "center", color: verificationResult.current['succeed'] ? "green" : "red" }}
dangerouslySetInnerHTML={{ __html: results }}
/>

@ -3,12 +3,15 @@
<head>
<meta charset="utf-8" />
<title>Etherscan</title>
<base href="/" />
<base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf"
crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
</head>
<body>
<div id="root"></div>
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script>
</body>
</html>

@ -0,0 +1,16 @@
{
"name": "etherscan",
"displayName": "Contract verification - Etherscan",
"description": "Verify Solidity contract code using Etherscan, BscScan, PolygonScan etc. APIs",
"version": "0.1.0",
"events": [],
"methods": ["verify", "receiptStatus"],
"kind": "none",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHsAAAB7CAYAAABUx/9/AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHBklEQVR42u2dy3WbTBTH/+ikgFEFwZ8K8HQAm6yjDkIHdiogHWBXgDqADgQVaLLJToZ1NtAB38JC0QNJIPGYx73nsIgcHVvz033OnblWVVXQQcqyhBACQgiUZYk0TfHnz5/q79+/rd7PGPsA8I1z/gEAjuOAMQbOOVzX1WKNLFVhCyGQJAnSNEUcx0N/CMu2bXDO4TgOXNcF51y9RauqSomnKApEUQTG2BZANeXDGNsyxuB5HqIoUmYNpf8DoyjC1HBbPEqAl/KPyrJMCg2+V+N930eWZQT72rNer6Ea4FvavtlsCLbGkM+gu66L9XptNuwsy3SG3KjpU5r3ySJrgyCfQfd93wzYmpvs1sBt2x7dtJM2S2DatYJN2nxby8eI2of/BQSzNfQgCNSETWb7PuDL5VIt2IalVL0D55wPwmXW98bKfD7fPj096bFvOtHeVJ7n2/l8DiGEvLteKtazZa+z9xm4EWiDgBNog4ATaIOAE2iDgFMebVBadnfDoWVZlF5N1CTKOcdms+n8xhmBVi8PF0Lg58+fw+fZZErlMeldGxwpIFM8YOvS+UJarUF/W++waWGbNSuKoof6ynpom269NUrme0SfOYQydTHnpNUPwO6zU3QMc06gH1hgyTp6blqam0UVyqkvL02fJ2B7WGfLtu26caR7UYVAK9f0gLe3t+7fzvl8vi3L8j9aQ2U0u75QYLHbt2hfQSOfrJzPvnni5OK3k0y4epp9y3fPCLSevnu1WrX7dhJspTX74jbojEDruw162oo8o3XRV97f3y+bIkq3tDHjzWmYhukWPM976Oxzy50oFQ5AIgzD5to4DG/I6whACdiHBwVnhyZcB9uq5M2DAwljbJskyXmApouv/vr1K1E+YFqWJeI4pmjcFEnTlGCbInmeAwC+UCFFa5/9AaC+//UTNom2PntxWDIlM65x0ScMw6PshGBrCtr3fXied/QiwdYQtOd5+PXr19kPZhSc6QXadV2EYdj4wykCNItzDs555wLI+/s7bdTcWNfdbZIXRam7Om9sUPR64y/UqY232hMY3Wf/+PHjofcXRbEgJT7X6DaH80eHrcuMLNVAUzRuEGiCbRBogm0Q6Le3N6qNmwB6V0uxSLMVK5jcCZrMuEqg6ybKLnLaakawFQDt+/7FEug10KfVRvLZkkrd9x0EwdnuVRtpKisTbIlBr9fru7plL21uEWwJzXY9+umuN1/ZxSSfLRno19fXQUCTZkskVVVZcRxjuVwOApo0WzIZEjTB1sHud+g0mlVVZdGSaQ/a4pyTZpug0cDncFeCbQBoAHh+fibYJoAGPjuECLYikuf5Q2fyOOefeXZVVRb1j8srD951Y3HOwRgjzdYc9N6EA1RB09I/n4rjOJ95dv1CfZaXRB/QjLEPxti+MreHTc33+ml0WZaLwz598tl3sPA8z7p2Y/9UEXeTfP/+/d8/Rp7yM/S5qCHPesHzvF6HwAy59vVUoKIo9r/jKEArimJBKdixzwOweHl5aTzvLJvZPjXhnueBMbZ/jaLxKzJGHDOkcp0eojzz2bQLNo4kSTIkaItzfnaIkjRb4Wj7mry8vJy91hiNk3YPI0NE203fJdu2G9uPSbNHkjHvcr904cGXa5Eo3V+ijtk+yB7w+vraDbZqaVj9QW3bNlKb63QrCIKjdOvoSzfyLM5eRi3UR1Bt2wbnHI7jwHVdae4az/McT09PYyvKzVmcSvrsLMss2bR4Km0+lCAIrv+HsUcHDlFqlOHZHaedbFxGL/Oze67bagd5ZzYnm4tS18Db1OxbmfGiKBZxHO/nRDHG8Pz8fC1YOvKftm1LaXJVibJvBWW+77da315nU1HOPP73zXXd1jcy0H52B1mtVrAsq5IBdN2F0ulGBl0Dpj6f3YJKN8PscEBbq0CbYCoHuaqrZF0/D/lsuX1yo5++5y408tknVa/5fL6VxSdfq5J1vSKLfLb8proxn95sNvf31JkIuOUUXa1AGwd7VztWanJwX6CNgK2KmR4atJawsyxTUoOHBq0N7CiK6u6MSodnCNDKwtYNLhom+Qxx8kT65oUkSSCEwO/fv7FarXSvAFnL5RJhGF5sLXpEpICd5znyPIcQAmVZIk1TJEliWmnPCoLgYrOgUrCTJEGSJAD+TWo3EOjFqlgYhsOPwRrT12rqYx+eYHh40nLQ9TesV0sayHWNe1RlmyqiNhm07/ujabM0qdfUzXpTmOyhDvMrk2fv9ma1hVz3iU29ztSWO7Am910F066ClmUZfN9XdofK9/1JzbWy5dIoilSAjuVyiSiKpF5L5WriMjQd1BrseR6iKJoksr7nUbbhUAiBJEmQpiniOB7lNgMZT4x2+hA6dZfWmyZ1fV0I0bpLtL4Gq4boOM7+GFN9q6/q8j8QmqQtM04gOgAAAABJRU5ErkJggg==",
"location": "sidePanel",
"url": "https://ipfs-cluster.ethdevops.io/ipfs/QmQsZbBSYCVBVpz2mVRbPRVTrcz59oJEpuuoxiT9otu3mh",
"repo": "https://github.com/ethereum/remix-project/tree/master/apps/etherscan",
"documentation": "https://remix-etherscan-plugin.readthedocs.io/en/latest",
"maintainedBy": "Remix",
"authorContact": "remix@ethereum.org"
}

@ -9,7 +9,6 @@
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",

@ -3,6 +3,10 @@ const webpack = require('webpack')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
const versionData = {
timestamp: Date.now(),
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
}
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
@ -30,7 +34,6 @@ module.exports = composePlugins(withNx(), (config) => {
"vm": require.resolve('vm-browserify'),
}
// add externals
config.externals = {
...config.externals,
@ -40,6 +43,9 @@ module.exports = composePlugins(withNx(), (config) => {
// add public path
config.output.publicPath = '/'
// set filename
config.output.filename = `[name].plugin-etherscan.${versionData.timestamp}.js`
config.output.chunkFilename = `[name].plugin-etherscan.${versionData.timestamp}.js`
// add copy & provide plugin
@ -78,5 +84,9 @@ module.exports = composePlugins(withNx(), (config) => {
new CssMinimizerPlugin(),
];
config.watchOptions = {
ignored: /node_modules/
}
return config;
});

@ -0,0 +1,23 @@
{
"name": "remix-ide-e2e",
"license": "MIT",
"engines": {
"node": "^20.0.0",
"npm": "^6.14.15"
},
"dependencies": {
"@openzeppelin/contracts": "^4.8.3",
"@openzeppelin/contracts-upgradeable": "^4.8.3",
"@openzeppelin/upgrades-core": "^1.22.0",
"@openzeppelin/wizard": "^0.1.1",
"@remix-project/remixd": "../../dist/libs/remixd",
"deep-equal": "^1.0.1",
"ganache-cli": "^6.8.1",
"selenium-standalone": "^8.2.3",
"tree-kill": "^1.2.2"
},
"devDependencies": {
"http-server": "^14.1.1",
"nightwatch": "2.3"
}
}

@ -54,7 +54,16 @@ function addFile(browser: NightwatchBrowser, name: string, content: NightwatchCo
suppressNotFoundErrors: true,
timeout: 60000
})
.waitForElementVisible({
selector: `//*[@data-id='tab-active' and contains(@data-path, "${name}")]`,
locateStrategy: 'xpath'
})
.setEditorValue(content.content)
.getEditorValue((result) => {
if(result != content.content) {
browser.setEditorValue(content.content)
}
})
.perform(function () {
done()
})

@ -5,14 +5,14 @@ import EventEmitter from 'events'
Check if the last log in the console contains a specific text
*/
class JournalLastChildIncludes extends EventEmitter {
command (this: NightwatchBrowser, val: string): NightwatchBrowser {
command(this: NightwatchBrowser, val: string): NightwatchBrowser {
this.api
.waitForElementVisible('*[data-id="terminalJournal"]', 10000)
.pause(1000)
.getText('*[data-id="terminalJournal"]', (result) => {
console.log('JournalLastChildIncludes', result.value)
if (typeof result.value === 'string' && result.value.indexOf(val) === -1) return this.api.assert.fail(`wait for ${val} in ${result.value}`)
else this.api.assert.ok(true, `<*[data-id="terminalJournal"]> contains ${val}.`)
.waitForElementPresent({
selector: `//*[@data-id='terminalJournal' and contains(.,'${val}')]`,
timeout: 10000,
locateStrategy: 'xpath'
}).perform((done) => {
done()
this.emit('complete')
})
return this

@ -1,5 +1,6 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
import { callbackCheckVerifyCallReturnValue } from '../types/index'
class VerifyCallReturnValue extends EventEmitter {
command (this: NightwatchBrowser, address: string, checks: string[]): NightwatchBrowser {
@ -13,7 +14,7 @@ class VerifyCallReturnValue extends EventEmitter {
}
}
function verifyCallReturnValue (browser: NightwatchBrowser, address: string, checks: string[], done: VoidFunction) {
function verifyCallReturnValue (browser: NightwatchBrowser, address: string, checks: string[] | callbackCheckVerifyCallReturnValue, done: VoidFunction) {
browser.execute(function (address: string) {
const nodes = document.querySelectorAll('#instance' + address + ' [data-id="udapp_value"]') as NodeListOf<HTMLElement>
const ret = []
@ -23,9 +24,14 @@ function verifyCallReturnValue (browser: NightwatchBrowser, address: string, che
}
return ret
}, [address], function (result) {
if (typeof checks === 'function') {
const ret = checks(result.value as string[])
if (!ret.pass) browser.assert.fail(ret.message)
} else {
for (const k in checks) {
browser.assert.equal(result.value[k].trim(), checks[k].trim())
}
}
done()
})
}

@ -31,7 +31,7 @@ function verifyContracts (browser: NightwatchBrowser, compiledContractNames: str
.click('*[data-id="treeViewDivtreeViewItemcompiler"]')
.waitForElementVisible('*[data-id="treeViewLiversion"]')
.assert.containsText('*[data-id="treeViewLiversion"]', `${opts.version}`)
.click('[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.click('[data-id="workspacesModalDialog-modal-footer-cancel-react"]')
.perform(() => {
done()
callback()
@ -49,7 +49,7 @@ function verifyContracts (browser: NightwatchBrowser, compiledContractNames: str
.click('*[data-id="treeViewDivtreeViewItemoptimizer"]')
.waitForElementVisible('*[data-id="treeViewDivruns"]')
.assert.containsText('*[data-id="treeViewDivruns"]', `${opts.runs}`)
.click('[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.click('[data-id="workspacesModalDialog-modal-footer-cancel-react"]')
.perform(() => {
done()
callback()

@ -7,7 +7,7 @@ type LoadPlugin = {
url: string
}
export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, loadPlugin?: LoadPlugin): void {
export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, loadPlugin?: LoadPlugin, hideToolTips: boolean = true): void {
browser
.url(url || 'http://127.0.0.1:8080')
//.switchBrowserTab(0)
@ -27,6 +27,30 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url
})
.verifyLoad()
.perform(() => {
if (hideToolTips) {
browser.execute(function () { // hide tooltips
function addStyle(styleString) {
const style = document.createElement('style');
style.textContent = styleString;
document.head.append(style);
}
addStyle(`
.bs-popover-right {
display:none !important;
}
.bs-popover-top {
display:none !important;
}
.bs-popover-left {
display:none !important;
}
.bs-popover-bottom {
display:none !important;
}
`);
})
}
if (preloadPlugins) {
initModules(browser, () => {
browser

@ -1,4 +1,9 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
"presets": ["@babel/preset-env", ["@babel/preset-react",
{"runtime": "automatic"}
]],
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"],
"ignore": [
"**/node_modules/**"
]
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save