merge from master

pull/5323/head
bunsenstraat 4 weeks ago
parent 027181a7b2
commit 31920ef065
  1. 373
      .circleci/config.yml
  2. 2
      .github/workflows/pr-reminder.yml
  3. 4
      .gitignore
  4. 1
      .nvmrc
  5. 4
      .prettierrc.json
  6. 6
      CONTRIBUTING.md
  7. 4
      README.md
  8. 6
      apps/circuit-compiler/src/app/components/container.tsx
  9. 10
      apps/contract-verification/.babelrc
  10. 0
      apps/contract-verification/.browserslistrc
  11. 3
      apps/contract-verification/.eslintrc
  12. 0
      apps/contract-verification/.eslintrc.json
  13. 69
      apps/contract-verification/project.json
  14. 14
      apps/contract-verification/src/app/App.css
  15. 34
      apps/contract-verification/src/app/AppContext.tsx
  16. 18
      apps/contract-verification/src/app/ContractVerificationPluginClient.ts
  17. 16
      apps/contract-verification/src/app/Verifiers/AbstractVerifier.ts
  18. 50
      apps/contract-verification/src/app/Verifiers/BlockscoutVerifier.ts
  19. 289
      apps/contract-verification/src/app/Verifiers/EtherscanVerifier.ts
  20. 169
      apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts
  21. 30
      apps/contract-verification/src/app/Verifiers/index.ts
  22. 46
      apps/contract-verification/src/app/VerifyFormContext.tsx
  23. 154
      apps/contract-verification/src/app/app.tsx
  24. 105
      apps/contract-verification/src/app/components/AccordionReceipt.tsx
  25. 69
      apps/contract-verification/src/app/components/ConfigInput.tsx
  26. 135
      apps/contract-verification/src/app/components/ConstructorArguments.tsx
  27. 33
      apps/contract-verification/src/app/components/ContractAddressInput.tsx
  28. 3
      apps/contract-verification/src/app/components/ContractDropdown.css
  29. 73
      apps/contract-verification/src/app/components/ContractDropdown.tsx
  30. 33
      apps/contract-verification/src/app/components/NavMenu.tsx
  31. 113
      apps/contract-verification/src/app/components/SearchableChainDropdown.tsx
  32. 5
      apps/contract-verification/src/app/components/index.tsx
  33. 9
      apps/contract-verification/src/app/hooks/useLocalStorage.tsx
  34. 33
      apps/contract-verification/src/app/hooks/useSourcifySupported.tsx
  35. 25
      apps/contract-verification/src/app/layouts/Default.tsx
  36. 1
      apps/contract-verification/src/app/layouts/index.ts
  37. 49
      apps/contract-verification/src/app/routes.tsx
  38. 19
      apps/contract-verification/src/app/types/SettingsTypes.ts
  39. 1
      apps/contract-verification/src/app/types/ThemeType.ts
  40. 80
      apps/contract-verification/src/app/types/VerificationTypes.ts
  41. 3
      apps/contract-verification/src/app/types/index.ts
  42. 576
      apps/contract-verification/src/app/utils/default-apis.json
  43. 28
      apps/contract-verification/src/app/utils/default-settings.ts
  44. 1
      apps/contract-verification/src/app/utils/index.ts
  45. 167
      apps/contract-verification/src/app/views/LookupView.tsx
  46. 16
      apps/contract-verification/src/app/views/ReceiptsView.tsx
  47. 54
      apps/contract-verification/src/app/views/SettingsView.tsx
  48. 279
      apps/contract-verification/src/app/views/VerifyView.tsx
  49. 4
      apps/contract-verification/src/app/views/index.ts
  50. 0
      apps/contract-verification/src/assets/.gitkeep
  51. 4
      apps/contract-verification/src/environments/environment.prod.ts
  52. 4
      apps/contract-verification/src/environments/environment.ts
  53. 0
      apps/contract-verification/src/favicon.ico
  54. 5
      apps/contract-verification/src/index.html
  55. 10
      apps/contract-verification/src/main.tsx
  56. 4
      apps/contract-verification/src/polyfills.ts
  57. 16
      apps/contract-verification/src/profile.json
  58. 0
      apps/contract-verification/src/styles.css
  59. 0
      apps/contract-verification/tsconfig.app.json
  60. 18
      apps/contract-verification/tsconfig.json
  61. 26
      apps/contract-verification/webpack.config.js
  62. 69
      apps/etherscan/project.json
  63. 7
      apps/etherscan/src/app/App.css
  64. 25
      apps/etherscan/src/app/AppContext.tsx
  65. 70
      apps/etherscan/src/app/EtherscanPluginClient.ts
  66. 136
      apps/etherscan/src/app/app.tsx
  67. 81
      apps/etherscan/src/app/components/HeaderWithSettings.tsx
  68. 34
      apps/etherscan/src/app/components/SubmitButton.tsx
  69. 2
      apps/etherscan/src/app/components/index.ts
  70. 17
      apps/etherscan/src/app/layouts/Default.tsx
  71. 1
      apps/etherscan/src/app/layouts/index.ts
  72. 37
      apps/etherscan/src/app/routes.tsx
  73. 9
      apps/etherscan/src/app/types/Receipt.ts
  74. 1
      apps/etherscan/src/app/types/ThemeType.ts
  75. 2
      apps/etherscan/src/app/types/index.ts
  76. 1
      apps/etherscan/src/app/utils/index.ts
  77. 46
      apps/etherscan/src/app/utils/networks.ts
  78. 30
      apps/etherscan/src/app/utils/scripts.ts
  79. 69
      apps/etherscan/src/app/utils/utilities.ts
  80. 206
      apps/etherscan/src/app/utils/verify.ts
  81. 63
      apps/etherscan/src/app/views/CaptureKeyView.tsx
  82. 16
      apps/etherscan/src/app/views/ErrorView.tsx
  83. 31
      apps/etherscan/src/app/views/HomeView.tsx
  84. 170
      apps/etherscan/src/app/views/ReceiptsView.tsx
  85. 235
      apps/etherscan/src/app/views/VerifyView.tsx
  86. 4
      apps/etherscan/src/app/views/index.ts
  87. 14
      apps/etherscan/src/main.tsx
  88. 16
      apps/etherscan/src/profile.json
  89. 38
      apps/learneth/src/App.tsx
  90. 2
      apps/learneth/src/components/LoadingScreen/index.tsx
  91. 21
      apps/learneth/src/components/RepoImporter/index.tsx
  92. 15
      apps/learneth/src/pages/Home/index.tsx
  93. 9
      apps/learneth/src/pages/Logo/index.tsx
  94. 1
      apps/learneth/src/redux/models/remixide.ts
  95. 51
      apps/learneth/src/redux/models/workshop.ts
  96. 13
      apps/learneth/src/redux/store.ts
  97. 25
      apps/quick-dapp/src/actions/index.ts
  98. 0
      apps/remix-dapp/.eslintrc
  99. 1
      apps/remix-dapp/README.md
  100. 9
      apps/remix-dapp/package.json
  101. Some files were not shown because too many files have changed in this diff Show More

@ -105,6 +105,51 @@ jobs:
paths:
- "persist"
test-remixdesktop-linux:
machine:
image: ubuntu-2004:current
resource_class:
xlarge
working_directory: ~/remix-project
parallelism: 10
steps:
- run: ldd --version
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
nvm install 20.2
nvm use 20.2
node -v
npm install --global yarn node-gyp
python -m pip install --upgrade pip
pip install setuptools
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/
yarn add node-pty
yarn --ignore-optional
yarn add @remix-project/remix-ws-templates
./rundist.bash
- run:
name: "Run tests"
command: |
nvm use 20.2
cd apps/remixdesktop/
./run_ci_test.sh
- run:
name: "Run isogit tests"
command: |
nvm use 20.2
cd apps/remixdesktop/
./run_git_ui_isogit_tests.sh
- store_test_results:
path: ./apps/remixdesktop/reports/tests
- store_artifacts:
path: ./apps/remixdesktop/reports/screenshots
build-remixdesktop-linux:
machine:
image: ubuntu-2004:current
@ -119,22 +164,37 @@ jobs:
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
nvm install 20.2
nvm use 20.2
node -v
npm install --global yarn node-gyp
python -m pip install --upgrade pip
pip install setuptools
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/
yarn add node-pty
yarn --ignore-optional
yarn add @remix-project/remix-ws-templates
PUBLISH_FOR_PULL_REQUEST='true' yarn dist
./rundist.bash
rm -rf release/*-unpacked
- save_cache:
key: remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- run:
name: "remove unnecessary files"
command: |
rm -rf ~/remix-project/apps/remixdesktop/release/.icon*
rm -rf ~/remix-project/apps/remixdesktop/release/builder*
- store_artifacts:
path: apps/remixdesktop/release/
destination: remixdesktop-linux
- persist_to_workspace:
root: apps/remixdesktop
paths:
- "release"
build-remixdesktop-windows:
executor:
@ -147,39 +207,107 @@ jobs:
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- restore_cache:
key: node-20-windows-v3
- run:
command: |
nvm install 20.0.0
nvm use 20.0.0
nvm install 20.2
nvm use 20.2
node -v
npx -v
npm install --global yarn
npm install --global node-gyp
yarn -v
- save_cache:
key: node-20-windows-v3
paths:
- /ProgramData/nvm/v20.0.0
- restore_cache:
keys:
- remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/
python -m pip install --upgrade pip
pip install setuptools
yarn
PUBLISH_FOR_PULL_REQUEST='true' yarn dist
./rundist.bash
rm -rf release/*-unpacked
- save_cache:
key: remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- persist_to_workspace:
root: apps/remixdesktop
paths:
- "release"
test-remixdesktop-windows:
executor:
name: win/default # executor type
size: xlarge # can be medium, large, xlarge, 2xlarge
shell: bash.exe
parallelism: 10
working_directory: ~/remix-project
steps:
- run:
name: Restart local mstsc
command: psexec64.exe -accepteula -nobanner -i 0 mstsc /v:localhost /w:2560 /h:1140
background: true
shell: powershell.exe
- run:
name: Naive impl to wait until the screen stretches
command: Start-Sleep 5
shell: powershell.exe
- run:
name: Get screen info
command: |
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Screen]::AllScreens | fl *
shell: powershell.exe
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
nvm install 20.2
nvm use 20.2
node -v
npx -v
npm install --global yarn
npm install --global node-gyp
yarn -v
- run:
name: start selenium
command: |
cd "apps/remixdesktop/"
yarn -v
shell: powershell.exe
- run:
command: |
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/
nvm use 20.2
node -v
python -m pip install --upgrade pip
pip install setuptools
yarn
./rundist.bash
- run:
name: run tests
command: |
cd "apps/remixdesktop/"
yarn -v
sleep 15
./run_ci_test.sh
- run:
name: "Run isogit tests"
command: |
cd apps/remixdesktop/
yarn -v
sleep 15
./run_git_ui_isogit_tests.sh
- store_test_results:
path: ./apps/remixdesktop/reports/tests
- store_artifacts:
path: ./apps/remixdesktop/reports/screenshots
# see https://docs.digicert.com/en/software-trust-manager/ci-cd-integrations/script-integrations/github-integration-ksp.html
sign-remixdesktop-windows:
executor: win/default # executor type
@ -230,28 +358,66 @@ jobs:
command: |
Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit' -Filter signtool.exe -Recurse
- run:
name: "Signtool-Signing"
name: read env
shell: powershell.exe
command: |
& $env:Signtool sign /sha1 $env:SM_CODE_SIGNING_CERT_SHA1_HASH /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $env:RemixSetupExe
# Specify the path to your package.json file
$packageJsonPath = "C:\Users\circleci\remix-project\apps\remixdesktop\package.json"
# Check if the file exists
if (Test-Path $packageJsonPath) {
# Read the content of the package.json file
$packageJsonContent = Get-Content $packageJsonPath -Raw | ConvertFrom-Json
# Check if the 'version' field exists in the package.json
if ($packageJsonContent.'version' -ne $null) {
# Store the version value in an environment variable
$version = $packageJsonContent.version
$file = "C:\Users\circleci\remix-project\release\Remix-Desktop-Setup-$($version).exe"
Write-Host "Version $(file) stored in PACKAGE_VERSION environment variable."
"Set-Variable -Name 'PACKAGE_VERSION' -Value '$file' -Scope Global" > SetEnvVars.ps1
dir Env:
} else {
Write-Host "Error: 'version' field not found in package.json."
}
} else {
Write-Host "Error: package.json file not found at $packageJsonPath."
}
- run:
name: "Signtool-Signing"
shell: powershell.exe
command: |
. .\SetEnvVars.ps1
dir Env:
& $env:Signtool sign /sha1 $env:SM_CODE_SIGNING_CERT_SHA1_HASH /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $PACKAGE_VERSION
- run:
name: "Signtool-Verification"
shell: powershell.exe
command: |
$verify_output = $(& $env:Signtool verify /v /pa $env:RemixSetupExe)
. .\SetEnvVars.ps1
$verify_output = $(& $env:Signtool verify /v /pa $PACKAGE_VERSION)
echo ${verify_output}
if (!$verify_output.Contains("Number of files successfully Verified: 1")) {
echo 'Verification failed'
exit 1
}
- run:
name: "remove unnecessary files"
shell: bash.exe
command: |
rm -rf ~/remix-project/release/.icon*
rm -rf ~/remix-project/release/builder*
- store_artifacts:
path: ~/remix-project/release/
destination: remixdesktop-windows
- persist_to_workspace:
root: ~/remix-project/
paths:
- "release"
environment:
SM_CLIENT_CERT_FILE: 'C:\Certificate_pkcs12.p12'
Signtool: 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe'
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
RemixSetupExe: 'C:\Users\circleci\remix-project\release\Remix IDE.exe'
build-remixdesktop-mac:
macos:
@ -259,23 +425,41 @@ jobs:
resource_class:
macos.m1.large.gen1
working_directory: ~/remix-project
parameters:
arch:
type: string
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Install Apple Certificate
command: |
echo $APPLE_CERTIFICATE_BASE64 | base64 --decode > /tmp/certificate.p12
security create-keychain -p ci-password build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p ci-password build.keychain
curl -o DeveloperIDG2CA.cer "https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer"
sudo security import DeveloperIDG2CA.cer -k /Library/Keychains/System.keychain -T /usr/bin/codesign
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain DeveloperIDG2CA.cer
security import /tmp/certificate.p12 -k build.keychain -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k ci-password build.keychain
security find-identity -v -p codesigning
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
ls -la dist/apps/remix-ide
nvm install 20.0.0
nvm use 20.0.0
nvm install 20.2
nvm use 20.2
- restore_cache:
keys:
- remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
nvm use 20.0.0
cd apps/remixdesktop && yarn
nvm use 20.2
cd apps/remixdesktop
yarn || yarn
find ./node_modules
yarn add @remix-project/remix-ws-templates
- save_cache:
key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
@ -284,20 +468,120 @@ jobs:
# use USE_HARD_LINK=false https://github.com/electron-userland/electron-builder/issues/3179
- run:
command: |
nvm use 20.0.0
nvm use 20.2
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop
yarn
yarn installRipGrepMacOXarm64
PUBLISH_FOR_PULL_REQUEST='true' USE_HARD_LINKS=false yarn dist --mac --arm64
yarn installRipGrepMacOXx64
PUBLISH_FOR_PULL_REQUEST='true' USE_HARD_LINKS=false yarn dist --mac --x64
rm -rf release/mac*
- run:
command: |
nvm use 20.2
cd apps/remixdesktop
yarn installRipGrepMacOX<< parameters.arch >>
PUBLISH_FOR_PULL_REQUEST='false' USE_HARD_LINKS=false ./rundist.bash --<< parameters.arch >>
if [ -f release/latest-mac.yml ]; then
cat release/latest-mac.yml
mv release/latest-mac.yml release/latest-mac-<< parameters.arch >>.yml
fi
find build
- run:
name: Notarize the app
command: |
brew install jq
cd apps/remixdesktop
zsh notarizedmg.sh
- run:
name: "remove unnecessary files"
command: |
rm -rf ~/remix-project/apps/remixdesktop/release/.icon*
rm -rf ~/remix-project/apps/remixdesktop/release/builder*
rm -rf ~/remix-project/apps/remixdesktop/release/*.blockmap
rm -rf ~/remix-project/apps/remixdesktop/release/_.*
- store_artifacts:
path: apps/remixdesktop/release/
destination: remixdesktop-mac
- persist_to_workspace:
root: apps/remixdesktop
paths:
- "release"
test-remixdesktop-mac:
macos:
xcode: 14.2.0
resource_class:
macos.m1.large.gen1
working_directory: ~/remix-project
parallelism: 10
steps:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
ls -la dist/apps/remix-ide
nvm install 20.2
nvm use 20.2
- restore_cache:
keys:
- remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
nvm use 20.2
cd apps/remixdesktop
yarn || yarn
yarn add @remix-project/remix-ws-templates
- save_cache:
key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- run:
command: |
nvm use 20.2
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop
yarn
- run:
command: |
nvm use 20.2
cd apps/remixdesktop
yarn installRipGrepMacOXarm64
PUBLISH_FOR_PULL_REQUEST='false' DO_NOT_NOTARIZE='true' USE_HARD_LINKS=false ./rundist.bash --arm64
find build
- run:
name: "Run tests"
command: |
nvm use 20.2
cd apps/remixdesktop
./run_ci_test.sh
- run:
name: "Run isogit tests"
command: |
nvm use 20.2
cd apps/remixdesktop
./run_git_ui_isogit_tests.sh
- store_test_results:
path: ./apps/remixdesktop/reports/tests
- store_artifacts:
path: ./apps/remixdesktop/reports/screenshots
uploadartifacts:
docker:
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
steps:
- checkout
- attach_workspace:
at: .
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- run:
name: "Upload Artifacts"
command: npx ts-node apps/remix-ide/ci/update_desktop_release_assets.ts
lint:
docker:
- image: cimg/node:20.0.0-browsers
@ -504,10 +788,16 @@ workflows:
- build-desktop:
filters:
branches:
only: [/.*desktop.*/]
only: [/.*desktop.*/, 'remix_beta']
- build-remixdesktop-mac:
requires:
- build-desktop
matrix:
parameters:
arch: ["arm64", "x64"]
- test-remixdesktop-mac:
requires:
- build-desktop
- build-remixdesktop-windows:
requires:
- build-desktop
@ -517,6 +807,23 @@ workflows:
- build-remixdesktop-linux:
requires:
- build-desktop
- test-remixdesktop-linux:
requires:
- build-desktop
- test-remixdesktop-windows:
requires:
- build-desktop
- uploadartifacts:
requires:
- build-remixdesktop-mac
- build-remixdesktop-linux
- sign-remixdesktop-windows
- test-remixdesktop-windows
- test-remixdesktop-linux
- test-remixdesktop-mac
filters:
branches:
only: [/.*desktop.*/]
- build-plugin:
matrix:
parameters:

@ -14,4 +14,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
freeze-date: '2024-09-09T18:00:00Z'
freeze-date: '2024-10-21T18:00:00Z'

4
.gitignore vendored

@ -15,6 +15,7 @@ soljson.js
*_group*.*.ts
*_group*.ts
stats.json
release
# compiled output
@ -61,9 +62,10 @@ testem.log
apps/remixdesktop/.webpack
apps/remixdesktop/out
apps/remixdesktop/release/
apps/remixdesktop/build*/
apps/remix-ide/src/assets/list.json
apps/remix-ide/src/assets/esbuild.wasm
apps/remixdesktop/build*
apps/remixdesktop/reports/
apps/remixdesktop/reports
apps/remixdesktop/logs/
logs

@ -0,0 +1 @@
v20

@ -1,9 +1,7 @@
{
"tabWidth": 2,
"printWidth": 500,
"bracketSpacing": false,
"useTabs": false,
"semi": false,
"singleQuote": true,
"bracketSpacing": false
"singleQuote": true
}

@ -27,7 +27,7 @@ Then you can replace the string with a intl component. The `id` prop will be the
+ <FormattedMessage id="home.learn" />
</label>
```
In some cases, jsx maybe not acceptable, you can use `intl.formatMessage` .
In some cases, jsx maybe not be acceptable, you can use `intl.formatMessage` .
```jsx
<input
ref={searchInputRef}
@ -77,7 +77,7 @@ const locales = [
]
```
You can find the language's `code, name, localeName` in this link
https://github.com/ethereum/ethereum-org-website/blob/dev/i18n/config.json
https://github.com/ethereum/ethereum-org-website/blob/dev/i18n.config.json
### Whether or not to use `defaultMessage`?
If you search `FormattedMessage` or `intl.formatMessage` in this project, you will notice that most of them only have a `id` prop, but a few of them have a `defaultMessage` prop.
@ -92,7 +92,7 @@ But in some cases, the `id` prop may not be static. For example,
<FormattedMessage id={plugin?.profile.name + '.displayName'} defaultMessage={plugin?.profile.displayName || plugin?.profile.name} />
</h6>
```
You can't be sure there is a match key in locale file or not. So it will be better to provide a `defaultMessage` prop.
You can't be sure whether there is a match key in locale file or not. So it will be better to provide a `defaultMessage` prop.
### Should I update the non-english locale json files?
You probably will have this question when you are updating the english locale json files.

@ -11,7 +11,7 @@
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat&logo=github)](https://github.com/ethereum/remix-project/blob/master/CONTRIBUTING.md)
[![GitHub contributors](https://img.shields.io/github/contributors/ethereum/remix-project?style=flat&logo=github)](https://github.com/ethereum/remix-project/blob/master/CONTRIBUTING.md)
[![Awesome Remix](https://img.shields.io/badge/Awesome--Remix-resources-green?logo=awesomelists)](https://github.com/ethereum/awesome-remix)
![GitHub](https://img.shields.io/github/license/ethereum/remix-project)
[![GitHub](https://img.shields.io/github/license/ethereum/remix-project)](https://github.com/ethereum/remix-project/blob/master/LICENSE)
[![Discord](https://img.shields.io/badge/join-discord-brightgreen.svg?style=flat&logo=discord)](https://discord.gg/mh9hFCKkEq)
[![X Follow](https://img.shields.io/twitter/follow/ethereumremix?style=flat&logo=x&color=green)](https://x.com/ethereumremix)
@ -68,7 +68,7 @@ git clone https://github.com/ethereum/remix-project.git
2. Install dependencies: `yarn install` or simply run `yarn`
3. Build Remix libraries: `yarn run build:libs`
4. Build Remix project: `yarn build`
5. Build and run project server: `yarn serve`. Optionally, run `yarn serve:hot` to enable hot module reload for frontend updates.
5. Build and run project server: `yarn serve`. Optionally, run `yarn serve:hot` to enable hot module to reload for frontend updates.
Open `http://127.0.0.1:8080` in your browser to load Remix IDE locally.

@ -74,7 +74,7 @@ export function Container () {
explain why the error occurred and how to fix it.
`
// @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message)
await circuitApp.plugin.call('remixAI', 'error_explaining', message)
} else {
const message = `
error message: ${error}
@ -82,7 +82,7 @@ export function Container () {
explain why the error occurred and how to fix it.
`
// @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message)
await circuitApp.plugin.call('remixAI', 'error_explaining', message)
}
} else {
const error = report.message
@ -92,7 +92,7 @@ export function Container () {
explain why the error occurred and how to fix it.
`
// @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message)
await circuitApp.plugin.call('remixAI', 'error_explaining', message)
}
}

@ -1,9 +1,5 @@
{
"presets": ["@babel/preset-env", ["@babel/preset-react",
{"runtime": "automatic"}
]],
"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/**"
]
}
"ignore": ["**/node_modules/**"]
}

@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.json"
}

@ -0,0 +1,69 @@
{
"name": "contract-verification",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/contract-verification/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "development",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/contract-verification",
"index": "apps/contract-verification/src/index.html",
"baseHref": "./",
"main": "apps/contract-verification/src/main.tsx",
"polyfills": "apps/contract-verification/src/polyfills.ts",
"tsConfig": "apps/contract-verification/tsconfig.app.json",
"assets": [
"apps/contract-verification/src/favicon.ico",
"apps/contract-verification/src/assets",
"apps/contract-verification/src/profile.json"
],
"styles": ["apps/contract-verification/src/styles.css"],
"scripts": [],
"webpackConfig": "apps/contract-verification/webpack.config.js"
},
"configurations": {
"development": {
},
"production": {
"fileReplacements": [
{
"replace": "apps/contract-verification/src/environments/environment.ts",
"with": "apps/contract-verification/src/environments/environment.prod.ts"
}
]
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/contract-verification/**/*.ts"],
"eslintConfig": "apps/contract-verification/.eslintrc"
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "contract-verification:build",
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {
"buildTarget": "contract-verification:build:development",
"port": 5003
},
"production": {
"buildTarget": "contract-verification:build:production"
}
}
}
},
"tags": []
}

@ -0,0 +1,14 @@
html, body, #root {
height: 100%;
}
body {
margin: 0;
}
a:focus {
background-color: var(bg-light) !important;
}
.fa-arrow-up-right-from-square::before { content: "\f08e"; }
.fa-xmark::before { content: "\f00d"; }

@ -0,0 +1,34 @@
import React from 'react'
import type { ThemeType, Chain, SubmittedContracts, ContractVerificationSettings } from './types'
import { CompilerAbstract } from '@remix-project/remix-solidity'
import { ContractVerificationPluginClient } from './ContractVerificationPluginClient'
import { ContractDropdownSelection } from './components/ContractDropdown'
// Define the type for the context
type AppContextType = {
themeType: ThemeType
setThemeType: (themeType: ThemeType) => void
clientInstance: ContractVerificationPluginClient
settings: ContractVerificationSettings
setSettings: React.Dispatch<React.SetStateAction<ContractVerificationSettings>>
chains: Chain[]
compilationOutput: { [key: string]: CompilerAbstract } | undefined
submittedContracts: SubmittedContracts
setSubmittedContracts: React.Dispatch<React.SetStateAction<SubmittedContracts>>
}
// Provide a default value with the appropriate types
const defaultContextValue: AppContextType = {
themeType: 'dark',
setThemeType: (themeType: ThemeType) => {},
clientInstance: {} as ContractVerificationPluginClient,
settings: { chains: {} },
setSettings: () => {},
chains: [],
compilationOutput: undefined,
submittedContracts: {},
setSubmittedContracts: (submittedContracts: SubmittedContracts) => {},
}
// Create the context with the type
export const AppContext = React.createContext<AppContextType>(defaultContextValue)

@ -0,0 +1,18 @@
import { PluginClient } from '@remixproject/plugin'
import { createClient } from '@remixproject/plugin-webview'
import EventManager from 'events'
export class ContractVerificationPluginClient extends PluginClient {
public internalEvents: EventManager
constructor() {
super()
this.internalEvents = new EventManager()
createClient(this)
this.onload()
}
onActivation(): void {
this.internalEvents.emit('verification_activated')
}
}

@ -0,0 +1,16 @@
import { CompilerAbstract } from '@remix-project/remix-solidity'
import type { LookupResponse, SubmittedContract, VerificationResponse } from '../types'
// Optional function definitions
export interface AbstractVerifier {
verifyProxy(submittedContract: SubmittedContract): Promise<VerificationResponse>
checkVerificationStatus?(receiptId: string): Promise<VerificationResponse>
checkProxyVerificationStatus?(receiptId: string): Promise<VerificationResponse>
}
export abstract class AbstractVerifier {
constructor(public apiUrl: string, public explorerUrl: string) {}
abstract verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse>
abstract lookup(contractAddress: string, chainId: string): Promise<LookupResponse>
}

@ -0,0 +1,50 @@
import { SourceFile } from '../types'
import { EtherscanVerifier } from './EtherscanVerifier'
// Etherscan and Blockscout return different objects from the getsourcecode method
interface BlockscoutSource {
AdditionalSources: Array<{ SourceCode: string; Filename: string }>
ConstructorArguments: string
OptimizationRuns: number
IsProxy: string
SourceCode: string
ABI: string
ContractName: string
CompilerVersion: string
OptimizationUsed: string
Runs: string
EVMVersion: string
FileName: string
Address: string
}
export class BlockscoutVerifier extends EtherscanVerifier {
LOOKUP_STORE_DIR = 'blockscout-verified'
constructor(apiUrl: string) {
// apiUrl and explorerUrl are the same for Blockscout
super(apiUrl, apiUrl, undefined)
}
getContractCodeUrl(address: string): string {
const url = new URL(this.explorerUrl + `/address/${address}`)
url.searchParams.append('tab', 'contract')
return url.href
}
processReceivedFiles(source: unknown, contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } {
const blockscoutSource = source as BlockscoutSource
const result: SourceFile[] = []
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}`
const targetFilePath = `${filePrefix}/${blockscoutSource.FileName}`
result.push({ content: blockscoutSource.SourceCode, path: targetFilePath })
for (const additional of blockscoutSource.AdditionalSources ?? []) {
result.push({ content: additional.SourceCode, path: `${filePrefix}/${additional.Filename}` })
}
return { sourceFiles: result, targetFilePath }
}
}

@ -0,0 +1,289 @@
import { CompilerAbstract } from '@remix-project/remix-solidity'
import { AbstractVerifier } from './AbstractVerifier'
import type { LookupResponse, SourceFile, SubmittedContract, VerificationResponse, VerificationStatus } from '../types'
interface EtherscanRpcResponse {
status: '0' | '1'
message: string
result: string
}
interface EtherscanCheckStatusResponse {
status: '0' | '1'
message: string
result: 'Pending in queue' | 'Pass - Verified' | 'Fail - Unable to verify' | 'Already Verified' | 'Unknown UID'
}
interface EtherscanSource {
SourceCode: string
ABI: string
ContractName: string
CompilerVersion: string
OptimizationUsed: string
Runs: string
ConstructorArguments: string
EVMVersion: string
Library: string
LicenseType: string
Proxy: string
Implementation: string
SwarmSource: string
}
interface EtherscanGetSourceCodeResponse {
status: '0' | '1'
message: string
result: EtherscanSource[]
}
export class EtherscanVerifier extends AbstractVerifier {
LOOKUP_STORE_DIR = 'etherscan-verified'
constructor(apiUrl: string, explorerUrl: string, protected apiKey?: string) {
super(apiUrl, explorerUrl)
}
async verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse> {
// TODO: Handle version Vyper contracts. This relies on Solidity metadata.
const metadata = JSON.parse(compilerAbstract.data.contracts[submittedContract.filePath][submittedContract.contractName].metadata)
const formData = new FormData()
formData.append('chainId', submittedContract.chainId)
formData.append('codeformat', 'solidity-standard-json-input')
formData.append('sourceCode', compilerAbstract.input.toString())
formData.append('contractaddress', submittedContract.address)
formData.append('contractname', submittedContract.filePath + ':' + submittedContract.contractName)
formData.append('compilerversion', `v${metadata.compiler.version}`)
formData.append('constructorArguements', submittedContract.abiEncodedConstructorArgs?.replace('0x', '') ?? '')
const url = new URL(this.apiUrl + '/api')
url.searchParams.append('module', 'contract')
url.searchParams.append('action', 'verifysourcecode')
if (this.apiKey) {
url.searchParams.append('apikey', this.apiKey)
}
const response = await fetch(url.href, {
method: 'POST',
body: formData,
})
if (!response.ok) {
const responseText = await response.text()
console.error('Error on Etherscan API verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText)
throw new Error(responseText)
}
const verificationResponse: EtherscanRpcResponse = await response.json()
if (verificationResponse.result.includes('already verified')) {
return { status: 'already verified', receiptId: null, lookupUrl: this.getContractCodeUrl(submittedContract.address) }
}
if (verificationResponse.status !== '1' || verificationResponse.message !== 'OK') {
console.error('Error on Etherscan API verification at ' + this.apiUrl + '\nStatus: ' + verificationResponse.status + '\nMessage: ' + verificationResponse.message + '\nResult: ' + verificationResponse.result)
throw new Error(verificationResponse.result)
}
const lookupUrl = this.getContractCodeUrl(submittedContract.address)
return { status: 'pending', receiptId: verificationResponse.result, lookupUrl }
}
async verifyProxy(submittedContract: SubmittedContract): Promise<VerificationResponse> {
if (!submittedContract.proxyAddress) {
throw new Error('SubmittedContract does not have a proxyAddress')
}
const formData = new FormData()
formData.append('address', submittedContract.proxyAddress)
formData.append('expectedimplementation', submittedContract.address)
const url = new URL(this.apiUrl + '/api')
url.searchParams.append('module', 'contract')
url.searchParams.append('action', 'verifyproxycontract')
if (this.apiKey) {
url.searchParams.append('apikey', this.apiKey)
}
const response = await fetch(url.href, {
method: 'POST',
body: formData,
})
if (!response.ok) {
const responseText = await response.text()
console.error('Error on Etherscan API proxy verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText)
throw new Error(responseText)
}
const verificationResponse: EtherscanRpcResponse = await response.json()
if (verificationResponse.status !== '1' || verificationResponse.message !== 'OK') {
console.error('Error on Etherscan API proxy verification at ' + this.apiUrl + '\nStatus: ' + verificationResponse.status + '\nMessage: ' + verificationResponse.message + '\nResult: ' + verificationResponse.result)
throw new Error(verificationResponse.result)
}
return { status: 'pending', receiptId: verificationResponse.result }
}
async checkVerificationStatus(receiptId: string): Promise<VerificationResponse> {
const url = new URL(this.apiUrl + '/api')
url.searchParams.append('module', 'contract')
url.searchParams.append('action', 'checkverifystatus')
url.searchParams.append('guid', receiptId)
if (this.apiKey) {
url.searchParams.append('apikey', this.apiKey)
}
const response = await fetch(url.href, { method: 'GET' })
if (!response.ok) {
const responseText = await response.text()
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText)
throw new Error(responseText)
}
const checkStatusResponse: EtherscanCheckStatusResponse = await response.json()
if (checkStatusResponse.result.startsWith('Fail - Unable to verify')) {
return { status: 'failed', receiptId, message: checkStatusResponse.result }
}
if (checkStatusResponse.result === 'Pending in queue') {
return { status: 'pending', receiptId }
}
if (checkStatusResponse.result === 'Pass - Verified') {
return { status: 'verified', receiptId }
}
if (checkStatusResponse.result === 'Already Verified') {
return { status: 'already verified', receiptId }
}
if (checkStatusResponse.result === 'Unknown UID') {
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result)
return { status: 'failed', receiptId, message: checkStatusResponse.result }
}
if (checkStatusResponse.status !== '1' || !checkStatusResponse.message.startsWith('OK')) {
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result)
throw new Error(checkStatusResponse.result)
}
return { status: 'unknown', receiptId }
}
async checkProxyVerificationStatus(receiptId: string): Promise<VerificationResponse> {
const url = new URL(this.apiUrl + '/api')
url.searchParams.append('module', 'contract')
url.searchParams.append('action', 'checkproxyverification')
url.searchParams.append('guid', receiptId)
if (this.apiKey) {
url.searchParams.append('apikey', this.apiKey)
}
const response = await fetch(url.href, { method: 'GET' })
if (!response.ok) {
const responseText = await response.text()
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText)
throw new Error(responseText)
}
const checkStatusResponse: EtherscanRpcResponse = await response.json()
if (checkStatusResponse.result === 'A corresponding implementation contract was unfortunately not detected for the proxy address.' || checkStatusResponse.result === 'The provided expected results are different than the retrieved implementation address!' || checkStatusResponse.result === 'This contract does not look like it contains any delegatecall opcode sequence.') {
return { status: 'failed', receiptId, message: checkStatusResponse.result }
}
if (checkStatusResponse.result === 'Verification in progress') {
return { status: 'pending', receiptId }
}
if (checkStatusResponse.result.startsWith("The proxy's") && checkStatusResponse.result.endsWith('and is successfully updated.')) {
return { status: 'verified', receiptId }
}
if (checkStatusResponse.result === 'Unknown UID') {
console.error('Error on Etherscan API check proxy verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result)
return { status: 'failed', receiptId, message: checkStatusResponse.result }
}
if (checkStatusResponse.status !== '1' || !checkStatusResponse.message.startsWith('OK')) {
console.error('Error on Etherscan API check proxy verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result)
throw new Error(checkStatusResponse.result)
}
return { status: 'unknown', receiptId }
}
async lookup(contractAddress: string, chainId: string): Promise<LookupResponse> {
const url = new URL(this.apiUrl + '/api')
url.searchParams.append('module', 'contract')
url.searchParams.append('action', 'getsourcecode')
url.searchParams.append('address', contractAddress)
if (this.apiKey) {
url.searchParams.append('apikey', this.apiKey)
}
const response = await fetch(url.href, { method: 'GET' })
if (!response.ok) {
const responseText = await response.text()
console.error('Error on Etherscan API lookup at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText)
throw new Error(responseText)
}
const lookupResponse: EtherscanGetSourceCodeResponse = await response.json()
if (lookupResponse.status !== '1' || !lookupResponse.message.startsWith('OK')) {
const errorResponse = lookupResponse as unknown as EtherscanRpcResponse
console.error('Error on Etherscan API lookup at ' + this.apiUrl + '\nStatus: ' + errorResponse.status + '\nMessage: ' + errorResponse.message + '\nResult: ' + errorResponse.result)
throw new Error(errorResponse.result)
}
if (lookupResponse.result[0].ABI === 'Contract source code not verified' || !lookupResponse.result[0].SourceCode) {
return { status: 'not verified' }
}
const lookupUrl = this.getContractCodeUrl(contractAddress)
const { sourceFiles, targetFilePath } = this.processReceivedFiles(lookupResponse.result[0], contractAddress, chainId)
return { status: 'verified', lookupUrl, sourceFiles, targetFilePath }
}
getContractCodeUrl(address: string): string {
const url = new URL(this.explorerUrl + `/address/${address}#code`)
return url.href
}
processReceivedFiles(source: EtherscanSource, contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } {
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}`
// Covers the cases:
// SourceFile: {[FileName]: [content]}
// SourceFile: {{sources: {[FileName]: [content]}}}
let parsedFiles: any
try {
parsedFiles = JSON.parse(source.SourceCode)
} catch (e) {
try {
// Etherscan wraps the Object in one additional bracket
parsedFiles = JSON.parse(source.SourceCode.substring(1, source.SourceCode.length - 1)).sources
} catch (e) {}
}
if (parsedFiles) {
const result: SourceFile[] = []
let targetFilePath = ''
for (const [fileName, fileObj] of Object.entries<any>(parsedFiles)) {
const path = `${filePrefix}/${fileName}`
result.push({ path, content: fileObj.content })
if (path.endsWith(`/${source.ContractName}.sol`)) {
targetFilePath = path
}
}
return { sourceFiles: result, targetFilePath }
}
// Parsing to JSON failed, SourceCode is the code itself
const targetFilePath = `${filePrefix}/${source.ContractName}.sol`
const sourceFiles: SourceFile[] = [{ content: source.SourceCode, path: targetFilePath }]
return { sourceFiles, targetFilePath }
}
}

@ -0,0 +1,169 @@
import { CompilerAbstract, SourcesCode } from '@remix-project/remix-solidity'
import { AbstractVerifier } from './AbstractVerifier'
import type { LookupResponse, SourceFile, SubmittedContract, VerificationResponse, VerificationStatus } from '../types'
import { ethers } from 'ethers'
interface SourcifyVerificationRequest {
address: string
chain: string
files: Record<string, string>
creatorTxHash?: string
chosenContract?: string
}
type SourcifyVerificationStatus = 'perfect' | 'full' | 'partial' | null
interface SourcifyVerificationResponse {
result: [
{
address: string
chainId: string
status: SourcifyVerificationStatus
libraryMap: {
[key: string]: string
}
message?: string
}
]
}
interface SourcifyErrorResponse {
error: string
}
interface SourcifyFile {
name: string
path: string
content: string
}
interface SourcifyLookupResponse {
status: Exclude<SourcifyVerificationStatus, null>
files: SourcifyFile[]
}
export class SourcifyVerifier extends AbstractVerifier {
LOOKUP_STORE_DIR = 'sourcify-verified'
async verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse> {
const metadataStr = compilerAbstract.data.contracts[submittedContract.filePath][submittedContract.contractName].metadata
const sources = compilerAbstract.source.sources
// from { "filename.sol": {content: "contract MyContract { ... }"} }
// to { "filename.sol": "contract MyContract { ... }" }
const formattedSources = Object.entries(sources).reduce((acc, [fileName, { content }]) => {
acc[fileName] = content
return acc
}, {})
const body: SourcifyVerificationRequest = {
chain: submittedContract.chainId,
address: submittedContract.address,
files: {
'metadata.json': metadataStr,
...formattedSources,
},
}
const response = await fetch(new URL(this.apiUrl + '/verify').href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorResponse: SourcifyErrorResponse = await response.json()
console.error('Error on Sourcify verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + JSON.stringify(errorResponse))
throw new Error(errorResponse.error)
}
const verificationResponse: SourcifyVerificationResponse = await response.json()
if (verificationResponse.result[0].status === null) {
console.error('Error on Sourcify verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + verificationResponse.result[0].message)
throw new Error(verificationResponse.result[0].message)
}
// Map to a user-facing status message
let status: VerificationStatus = 'unknown'
let lookupUrl: string | undefined = undefined
if (verificationResponse.result[0].status === 'perfect' || verificationResponse.result[0].status === 'full') {
status = 'fully verified'
lookupUrl = this.getContractCodeUrl(submittedContract.address, submittedContract.chainId, true)
} else if (verificationResponse.result[0].status === 'partial') {
status = 'partially verified'
lookupUrl = this.getContractCodeUrl(submittedContract.address, submittedContract.chainId, false)
}
return { status, receiptId: null, lookupUrl }
}
async lookup(contractAddress: string, chainId: string): Promise<LookupResponse> {
const url = new URL(this.apiUrl + `/files/any/${chainId}/${contractAddress}`)
const response = await fetch(url.href, { method: 'GET' })
if (!response.ok) {
const errorResponse: SourcifyErrorResponse = await response.json()
if (errorResponse.error === 'Files have not been found!') {
return { status: 'not verified' }
}
console.error('Error on Sourcify lookup at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + JSON.stringify(errorResponse))
throw new Error(errorResponse.error)
}
const lookupResponse: SourcifyLookupResponse = await response.json()
let status: VerificationStatus = 'unknown'
let lookupUrl: string | undefined = undefined
if (lookupResponse.status === 'perfect' || lookupResponse.status === 'full') {
status = 'fully verified'
lookupUrl = this.getContractCodeUrl(contractAddress, chainId, true)
} else if (lookupResponse.status === 'partial') {
status = 'partially verified'
lookupUrl = this.getContractCodeUrl(contractAddress, chainId, false)
}
const { sourceFiles, targetFilePath } = this.processReceivedFiles(lookupResponse.files, contractAddress, chainId)
return { status, lookupUrl, sourceFiles, targetFilePath }
}
getContractCodeUrl(address: string, chainId: string, fullMatch: boolean): string {
const url = new URL(this.explorerUrl + `/contracts/${fullMatch ? 'full_match' : 'partial_match'}/${chainId}/${address}/`)
return url.href
}
processReceivedFiles(files: SourcifyFile[], contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } {
const result: SourceFile[] = []
let targetFilePath: string
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}`
for (const file of files) {
let filePath: string
for (const a of [contractAddress, ethers.utils.getAddress(contractAddress)]) {
const matching = file.path.match(`/${a}/(.*)$`)
if (matching) {
filePath = matching[1]
break
}
}
if (filePath) {
result.push({ path: `${filePrefix}/${filePath}`, content: file.content })
}
if (file.name === 'metadata.json') {
const metadata = JSON.parse(file.content)
const compilationTarget = metadata.settings.compilationTarget
const contractPath = Object.keys(compilationTarget)[0]
targetFilePath = `${filePrefix}/sources/${contractPath}`
}
}
return { sourceFiles: result, targetFilePath }
}
}

@ -0,0 +1,30 @@
import type { VerifierIdentifier, VerifierSettings } from '../types'
import { AbstractVerifier } from './AbstractVerifier'
import { BlockscoutVerifier } from './BlockscoutVerifier'
import { EtherscanVerifier } from './EtherscanVerifier'
import { SourcifyVerifier } from './SourcifyVerifier'
export { AbstractVerifier } from './AbstractVerifier'
export { BlockscoutVerifier } from './BlockscoutVerifier'
export { SourcifyVerifier } from './SourcifyVerifier'
export { EtherscanVerifier } from './EtherscanVerifier'
export function getVerifier(identifier: VerifierIdentifier, settings: VerifierSettings): AbstractVerifier {
switch (identifier) {
case 'Sourcify':
if (!settings?.explorerUrl) {
throw new Error('The Sourcify verifier requires an explorer URL.')
}
return new SourcifyVerifier(settings.apiUrl, settings.explorerUrl)
case 'Etherscan':
if (!settings?.explorerUrl) {
throw new Error('The Etherscan verifier requires an explorer URL.')
}
if (!settings?.apiKey) {
throw new Error('The Etherscan verifier requires an API key.')
}
return new EtherscanVerifier(settings.apiUrl, settings.explorerUrl, settings.apiKey)
case 'Blockscout':
return new BlockscoutVerifier(settings.apiUrl)
}
}

@ -0,0 +1,46 @@
import React from 'react'
import type { Chain } from './types'
import { ContractDropdownSelection } from './components/ContractDropdown'
// Define the type for the context
type VerifyFormContextType = {
selectedChain: Chain | undefined
setSelectedChain: React.Dispatch<React.SetStateAction<Chain>>
contractAddress: string
setContractAddress: React.Dispatch<React.SetStateAction<string>>
contractAddressError: string
setContractAddressError: React.Dispatch<React.SetStateAction<string>>
selectedContract: ContractDropdownSelection | undefined
setSelectedContract: React.Dispatch<React.SetStateAction<ContractDropdownSelection>>
proxyAddress: string
setProxyAddress: React.Dispatch<React.SetStateAction<string>>
proxyAddressError: string
setProxyAddressError: React.Dispatch<React.SetStateAction<string>>
abiEncodedConstructorArgs: string
setAbiEncodedConstructorArgs: React.Dispatch<React.SetStateAction<string>>
abiEncodingError: string
setAbiEncodingError: React.Dispatch<React.SetStateAction<string>>
}
// Provide a default value with the appropriate types
const defaultContextValue: VerifyFormContextType = {
selectedChain: undefined,
setSelectedChain: (selectedChain: Chain) => {},
contractAddress: '',
setContractAddress: (contractAddress: string) => {},
contractAddressError: '',
setContractAddressError: (contractAddressError: string) => {},
selectedContract: undefined,
setSelectedContract: (selectedContract: ContractDropdownSelection) => {},
proxyAddress: '',
setProxyAddress: (proxyAddress: string) => {},
proxyAddressError: '',
setProxyAddressError: (contractAddressError: string) => {},
abiEncodedConstructorArgs: '',
setAbiEncodedConstructorArgs: (contractAddproxyAddressress: string) => {},
abiEncodingError: '',
setAbiEncodingError: (contractAddressError: string) => {},
}
// Create the context with the type
export const VerifyFormContext = React.createContext<VerifyFormContextType>(defaultContextValue)

@ -0,0 +1,154 @@
import { useState, useEffect, useRef } from 'react'
import { ContractVerificationPluginClient } from './ContractVerificationPluginClient'
import { AppContext } from './AppContext'
import { VerifyFormContext } from './VerifyFormContext'
import DisplayRoutes from './routes'
import type { ContractVerificationSettings, ThemeType, Chain, SubmittedContracts, VerificationReceipt, VerificationResponse } from './types'
import { mergeChainSettingsWithDefaults } from './utils'
import './App.css'
import { CompilerAbstract } from '@remix-project/remix-solidity'
import { useLocalStorage } from './hooks/useLocalStorage'
import { getVerifier } from './Verifiers'
import { ContractDropdownSelection } from './components/ContractDropdown'
const plugin = new ContractVerificationPluginClient()
const App = () => {
const [themeType, setThemeType] = useState<ThemeType>('dark')
const [settings, setSettings] = useLocalStorage<ContractVerificationSettings>('contract-verification:settings', { chains: {} })
const [submittedContracts, setSubmittedContracts] = useLocalStorage<SubmittedContracts>('contract-verification:submitted-contracts', {})
const [chains, setChains] = useState<Chain[]>([]) // State to hold the chains data
const [compilationOutput, setCompilationOutput] = useState<{ [key: string]: CompilerAbstract } | undefined>()
// Form values:
const [selectedChain, setSelectedChain] = useState<Chain | undefined>()
const [contractAddress, setContractAddress] = useState('')
const [contractAddressError, setContractAddressError] = useState('')
const [selectedContract, setSelectedContract] = useState<ContractDropdownSelection | undefined>()
const [proxyAddress, setProxyAddress] = useState('')
const [proxyAddressError, setProxyAddressError] = useState('')
const [abiEncodedConstructorArgs, setAbiEncodedConstructorArgs] = useState<string>('')
const [abiEncodingError, setAbiEncodingError] = useState<string>('')
const timer = useRef(null)
useEffect(() => {
plugin.internalEvents.on('verification_activated', () => {
// Fetch compiler artefacts initially
plugin.call('compilerArtefacts' as any, 'getAllCompilerAbstracts').then((obj: any) => {
setCompilationOutput(obj)
})
// Subscribe to compilations
plugin.on('compilerArtefacts' as any, 'compilationSaved', (compilerAbstracts: { [key: string]: CompilerAbstract }) => {
setCompilationOutput((prev) => ({ ...(prev || {}), ...compilerAbstracts }))
})
// Fetch chains.json and update state
fetch('https://chainid.network/chains.json')
.then((response) => response.json())
.then((data) => setChains(data))
.catch((error) => console.error('Failed to fetch chains.json:', error))
})
// Clean up on unmount
return () => {
plugin.off('compilerArtefacts' as any, 'compilationSaved')
}
}, [])
// Poll status of pending receipts frequently
useEffect(() => {
const getPendingReceipts = (submissions: SubmittedContracts) => {
const pendingReceipts: VerificationReceipt[] = []
// Check statuses of receipts
for (const submission of Object.values(submissions)) {
for (const receipt of submission.receipts) {
if (receipt.status === 'pending') {
pendingReceipts.push(receipt)
}
}
for (const proxyReceipt of submission.proxyReceipts ?? []) {
if (proxyReceipt.status === 'pending') {
pendingReceipts.push(proxyReceipt)
}
}
}
return pendingReceipts
}
let pendingReceipts = getPendingReceipts(submittedContracts)
if (pendingReceipts.length > 0) {
if (timer.current) {
clearInterval(timer.current)
timer.current = null
}
const pollStatus = async () => {
const changedSubmittedContracts = { ...submittedContracts }
for (const receipt of pendingReceipts) {
await new Promise((resolve) => setTimeout(resolve, 500)) // avoid api rate limit exceed.
const { verifierInfo, receiptId } = receipt
if (receiptId) {
const contract = changedSubmittedContracts[receipt.contractId]
const chainSettings = mergeChainSettingsWithDefaults(contract.chainId, settings)
const verifierSettings = chainSettings.verifiers[verifierInfo.name]
// In case the user overwrites the API later, prefer the one stored in localStorage
const verifier = getVerifier(verifierInfo.name, { ...verifierSettings, apiUrl: verifierInfo.apiUrl })
if (!verifier.checkVerificationStatus) {
continue
}
try {
let response: VerificationResponse
if (receipt.isProxyReceipt) {
response = await verifier.checkProxyVerificationStatus(receiptId)
} else {
response = await verifier.checkVerificationStatus(receiptId)
}
const { status, message, lookupUrl } = response
receipt.status = status
receipt.message = message
if (lookupUrl) {
receipt.lookupUrl = lookupUrl
}
} catch (e) {
receipt.failedChecks++
// Only retry 5 times
if (receipt.failedChecks >= 5) {
receipt.status = 'failed'
receipt.message = e.message
}
}
}
}
pendingReceipts = getPendingReceipts(changedSubmittedContracts)
if (timer.current && pendingReceipts.length === 0) {
clearInterval(timer.current)
timer.current = null
}
setSubmittedContracts((prev) => Object.assign({}, prev, changedSubmittedContracts))
}
timer.current = setInterval(pollStatus, 1000)
}
}, [submittedContracts])
return (
<AppContext.Provider value={{ themeType, setThemeType, clientInstance: plugin, settings, setSettings, chains, compilationOutput, submittedContracts, setSubmittedContracts }}>
<VerifyFormContext.Provider value={{ selectedChain, setSelectedChain, contractAddress, setContractAddress, contractAddressError, setContractAddressError, selectedContract, setSelectedContract, proxyAddress, setProxyAddress, proxyAddressError, setProxyAddressError, abiEncodedConstructorArgs, setAbiEncodedConstructorArgs, abiEncodingError, setAbiEncodingError }}>
<DisplayRoutes />
</VerifyFormContext.Provider>
</AppContext.Provider>
)
}
export default App

@ -0,0 +1,105 @@
import React, { useMemo } from 'react'
import { SubmittedContract, VerificationReceipt } from '../types'
import { shortenAddress, CustomTooltip } from '@remix-ui/helper'
import { AppContext } from '../AppContext'
import { CopyToClipboard } from '@remix-ui/clipboard'
interface AccordionReceiptProps {
contract: SubmittedContract
index: number
}
export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ contract, index }) => {
const { chains } = React.useContext(AppContext)
const [expanded, setExpanded] = React.useState(false)
const chain = useMemo(() => {
return chains.find((c) => c.chainId === parseInt(contract.chainId))
}, [contract, chains])
const chainName = chain?.name ?? 'Unknown Chain'
const hasProxy = contract.proxyAddress && contract.proxyReceipts
const toggleAccordion = () => {
setExpanded(!expanded)
}
return (
<div className={`${expanded ? 'bg-light' : 'border-bottom '}`}>
<div className="d-flex flex-row align-items-center">
<button className="btn" onClick={toggleAccordion} style={{ padding: '0.45rem' }}>
<i className={`fas ${expanded ? 'fa-angle-down' : 'fa-angle-right'} text-secondary`}></i>
</button>
<div className="small w-100 text-uppercase overflow-hidden text-nowrap">
<CustomTooltip placement="bottom" tooltipClasses=" text-break" tooltipText={`Contract: ${contract.contractName}, Address: ${contract.address}, Chain: ${chainName}, Proxy: ${contract.proxyAddress}`}>
<span>
{contract.contractName} at {shortenAddress(contract.address)} {contract.proxyAddress ? 'with proxy' : ''}
</span>
</CustomTooltip>
</div>
<button className="btn" style={{ padding: '0.15rem' }}>
<CopyToClipboard tip="Copy" content={contract.address} direction={'top'} />
</button>
</div>
<div className={`${expanded ? '' : 'd-none'} px-2 pt-2 pb-3 small`}>
<div>
<span className="font-weight-bold">Chain: </span>
{chainName} ({contract.chainId})
</div>
<div>
<span className="font-weight-bold">File: </span>
<span className="text-break">{contract.filePath}</span>
</div>
<div>
<span className="font-weight-bold">Submitted at: </span>
{new Date(contract.date).toLocaleString()}
</div>
<div>
<span className="font-weight-bold">Verified at: </span>
<ReceiptsBody receipts={contract.receipts} />
</div>
{hasProxy && (
<>
<div className="mt-3">
<span className="font-weight-bold">Proxy Address: </span>
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipText={contract.proxyAddress}>
<span>{shortenAddress(contract.proxyAddress)}</span>
</CustomTooltip>
<CopyToClipboard tip="Copy" content={contract.proxyAddress} direction={'top'} />
</div>
<div>
<span className="font-weight-bold">Proxy verified at: </span>
<ReceiptsBody receipts={contract.proxyReceipts} />
</div>
</>
)}
</div>
</div>
)
}
const ReceiptsBody = ({ receipts }: { receipts: VerificationReceipt[] }) => {
return (
<ul className="list-group">
{receipts.map((receipt) => (
<li className="list-group-item">
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipText={`API: ${receipt.verifierInfo.apiUrl}`}>
<span className="font-weight-bold medium">{receipt.verifierInfo.name}</span>
</CustomTooltip>
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipTextClasses="text-capitalize" tooltipText={`Status: ${receipt.status}${receipt.message ? `, Message: ${receipt.message}` : ''}`}>
<span className="ml-2">{['verified', 'partially verified', 'already verified'].includes(receipt.status) ? <i className="fas fa-check"></i> : receipt.status === 'fully verified' ? <i className="fas fa-check-double"></i> : receipt.status === 'failed' ? <i className="fas fa-xmark"></i> : ['pending', 'awaiting implementation verification'].includes(receipt.status) ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-question"></i>}</span>
</CustomTooltip>
<span className="ml-2">{!!receipt.lookupUrl && <a href={receipt.lookupUrl} target="_blank" className="fa fas fa-arrow-up-right-from-square"></a>}</span>
</li>
))}
</ul>
)
}

@ -0,0 +1,69 @@
import React, { useEffect, useState } from 'react'
import { CustomTooltip } from '@remix-ui/helper'
interface ConfigInputProps {
label: string
id: string
secret: boolean
initialValue: string
saveResult: (result: string) => void
}
// Chooses one contract from the compilation output.
export const ConfigInput: React.FC<ConfigInputProps> = ({ label, id, secret, initialValue, saveResult }) => {
const [value, setValue] = useState(initialValue)
const [enabled, setEnabled] = useState(false)
// Reset state when initialValue changes
useEffect(() => {
setValue(initialValue)
setEnabled(false)
}, [initialValue])
const handleChange = () => {
setEnabled(true)
}
const handleSave = () => {
setEnabled(false)
saveResult(value)
}
const handleCancel = () => {
setEnabled(false)
setValue(initialValue)
}
return (
<div className="form-group small mb-0">
<label className='mt-3' htmlFor={id}>{label}</label>
<div className="d-flex flex-row justify-content-start">
<input
type={secret ? 'password' : 'text'}
className={`form-control small w-100 ${!enabled ? 'bg-transparent pl-0 border-0' : ''}`}
id={id}
placeholder={`Add ${label}`}
value={value}
onChange={(e) => setValue(e.target.value)}
disabled={!enabled}
/>
{ enabled ? (
<>
<button type="button" className="btn btn-primary btn-sm ml-2" onClick={handleSave}>
Save
</button>
<button type="button" className="btn btn-secondary btn-sm ml-2" onClick={handleCancel}>
Cancel
</button>
</>
) : (
<CustomTooltip tooltipText={`Edit ${label}`}>
<button type="button" className="btn btn-sm fas fa-pen my-1" style={{ height: '100%' }} disabled={enabled} onClick={handleChange}>
</button>
</CustomTooltip>
)}
</div>
</div>
)
}

@ -0,0 +1,135 @@
import { useContext, useEffect, useRef, useState } from 'react'
import { ethers } from 'ethers'
import { AppContext } from '../AppContext'
import { ContractDropdownSelection } from './ContractDropdown'
interface ConstructorArgumentsProps {
abiEncodedConstructorArgs: string
setAbiEncodedConstructorArgs: React.Dispatch<React.SetStateAction<string>>
abiEncodingError: string
setAbiEncodingError: React.Dispatch<React.SetStateAction<string>>
selectedContract: ContractDropdownSelection
}
export const ConstructorArguments: React.FC<ConstructorArgumentsProps> = ({ abiEncodedConstructorArgs, setAbiEncodedConstructorArgs, abiEncodingError, setAbiEncodingError, selectedContract }) => {
const { compilationOutput } = useContext(AppContext)
const [toggleRawInput, setToggleRawInput] = useState<boolean>(false)
const { triggerFilePath, filePath, contractName } = selectedContract
const selectedCompilerAbstract = triggerFilePath && compilationOutput[triggerFilePath]
const compiledContract = selectedCompilerAbstract?.data?.contracts?.[filePath]?.[contractName]
const abi = compiledContract?.abi
const constructorArgs = abi && abi.find((a) => a.type === 'constructor')?.inputs
const decodeConstructorArgs = (value: string) => {
try {
const decodedObj = ethers.utils.defaultAbiCoder.decode(
constructorArgs.map((inp) => inp.type),
value
)
const decoded = decodedObj.map((val) => JSON.stringify(val))
return { decoded, errorMessage: '' }
} catch (e) {
console.error(e)
const errorMessage = 'Decoding error: ' + e.message
const decoded = Array(constructorArgs?.length ?? 0).fill('')
return { decoded, errorMessage }
}
}
const [constructorArgsValues, setConstructorArgsValues] = useState<string[]>(abiEncodedConstructorArgs ? decodeConstructorArgs(abiEncodedConstructorArgs).decoded : Array(constructorArgs?.length ?? 0).fill(''))
const constructorArgsInInitialState = useRef(true)
useEffect(() => {
if (constructorArgsInInitialState.current) {
constructorArgsInInitialState.current = false
return
}
setAbiEncodedConstructorArgs('')
setAbiEncodingError('')
setConstructorArgsValues(Array(constructorArgs?.length ?? 0).fill(''))
}, [constructorArgs])
const handleConstructorArgs = (value: string, index: number) => {
const changedConstructorArgsValues = [...constructorArgsValues.slice(0, index), value, ...constructorArgsValues.slice(index + 1)]
setConstructorArgsValues(changedConstructorArgsValues)
// if any constructorArgsValue is falsey (empty etc.), don't encode yet
if (changedConstructorArgsValues.some((value) => !value)) {
setAbiEncodedConstructorArgs('')
setAbiEncodingError('')
return
}
const types = constructorArgs.map((inp) => inp.type)
const parsedArgsValues = []
for (const arg of changedConstructorArgsValues) {
try {
parsedArgsValues.push(JSON.parse(arg))
} catch (e) {
parsedArgsValues.push(arg)
}
}
try {
const newAbiEncoding = ethers.utils.defaultAbiCoder.encode(types, parsedArgsValues)
setAbiEncodedConstructorArgs(newAbiEncoding)
setAbiEncodingError('')
} catch (e) {
console.error(e)
setAbiEncodedConstructorArgs('')
setAbiEncodingError('Encoding error: ' + e.message)
}
}
const handleRawConstructorArgs = (value: string) => {
setAbiEncodedConstructorArgs(value)
const { decoded, errorMessage } = decodeConstructorArgs(value)
setConstructorArgsValues(decoded)
setAbiEncodingError(errorMessage)
}
if (!selectedContract) return null
if (!compilationOutput && Object.keys(compilationOutput).length === 0) return null
// No render if no constructor args
if (!constructorArgs || constructorArgs.length === 0) return null
return (
<div className="mt-4">
<label>Constructor Arguments</label>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input className="form-check-input custom-control-input" type="checkbox" id="toggleRawInputSwitch" checked={toggleRawInput} onChange={() => setToggleRawInput(!toggleRawInput)} />
<label className="m-0 form-check-label custom-control-label" style={{ paddingTop: '2px' }} htmlFor="toggleRawInputSwitch">
Enter raw ABI-encoded constructor arguments
</label>
</div>
{toggleRawInput ? (
<div>
{' '}
<textarea className="form-control" rows={5} placeholder="0x00000000000000000000000000000000d41867734bbee4c6863d9255b2b06ac1..." value={abiEncodedConstructorArgs} onChange={(e) => handleRawConstructorArgs(e.target.value)} />
{abiEncodingError && <div className="text-danger small">{abiEncodingError}</div>}
</div>
) : (
<div>
{constructorArgs.map((inp, i) => (
<div key={`constructor-arg-${inp.name}`} className="d-flex flex-row align-items-center justify-content-between mb-2">
<div className="mr-2 small">{inp.name}</div>
<input className="form-control w-50" placeholder={inp.type} value={constructorArgsValues[i] ?? ''} onChange={(e) => handleConstructorArgs(e.target.value, i)} />
</div>
))}
{abiEncodedConstructorArgs && (
<div>
<label className="form-check-label" htmlFor="rawAbiEncodingResult">
ABI-encoded constructor arguments:
</label>
<textarea className="form-control" rows={5} disabled value={abiEncodedConstructorArgs} id="rawAbiEncodingResult" style={{ opacity: 0.5 }} />
</div>
)}
{abiEncodingError && <div className="text-danger small">{abiEncodingError}</div>}
</div>
)}
</div>
)
}

@ -0,0 +1,33 @@
import React, { useEffect, useState, useContext } from 'react'
import { ethers } from 'ethers/'
interface ContractAddressInputProps {
label: string
id: string
contractAddress: string
setContractAddress: (address: string) => void
contractAddressError: string
setContractAddressError: (error: string) => void
}
// Chooses one contract from the compilation output.
export const ContractAddressInput: React.FC<ContractAddressInputProps> = ({ label, id, contractAddress, setContractAddress, contractAddressError, setContractAddressError }) => {
const handleAddressChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const isValidAddress = ethers.utils.isAddress(event.target.value)
setContractAddress(event.target.value)
if (!isValidAddress) {
setContractAddressError('Invalid contract address')
console.error('Invalid contract address')
return
}
setContractAddressError('')
}
return (
<div className="form-group">
<label htmlFor={id}>{label}</label>
<div>{contractAddressError && <div className="text-danger">{contractAddressError}</div>}</div>
<input type="text" className="form-control" id={id} placeholder="0x2738d13E81e..." value={contractAddress} onChange={handleAddressChange} />
</div>
)
}

@ -0,0 +1,3 @@
.disabled-cursor {
cursor: not-allowed;
}

@ -0,0 +1,73 @@
import React, { useEffect, useState, useContext, Fragment } from 'react'
import './ContractDropdown.css'
import { AppContext } from '../AppContext'
export interface ContractDropdownSelection {
triggerFilePath: string
filePath: string
contractName: string
}
interface ContractDropdownProps {
label: string
id: string
selectedContract: ContractDropdownSelection
setSelectedContract: (selection: ContractDropdownSelection) => void
}
// Chooses one contract from the compilation output.
export const ContractDropdown: React.FC<ContractDropdownProps> = ({ label, id, selectedContract, setSelectedContract }) => {
const { compilationOutput } = useContext(AppContext)
useEffect(() => {
if (!compilationOutput || !!selectedContract) return
// Otherwise select the first by default
const triggerFilePath = Object.keys(compilationOutput)[0]
const contracts = compilationOutput[triggerFilePath]?.data?.contracts
if (contracts && Object.keys(contracts).length) {
const firstFilePath = Object.keys(contracts)[0]
const contractsInFile = contracts[firstFilePath]
if (contractsInFile && Object.keys(contractsInFile).length) {
const firstContractName = Object.keys(contractsInFile)[0]
setSelectedContract({ triggerFilePath, filePath: firstFilePath, contractName: firstContractName })
}
}
}, [compilationOutput])
const handleSelectContract = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedContract(JSON.parse(event.target.value))
}
const hasContracts = compilationOutput && Object.keys(compilationOutput).length > 0
return (
<div className="form-group">
<label htmlFor={id}>{label}</label>
<select value={selectedContract ? JSON.stringify(selectedContract) : ''}
className={`form-control custom-select pr-4 ${!hasContracts ? 'disabled-cursor text-warning' : ''}`}
id={id}
disabled={!hasContracts}
onChange={handleSelectContract}
>
{hasContracts ? (
Object.keys(compilationOutput).map((compilationTriggerFileName) => (
<optgroup key={compilationTriggerFileName} label={`Compilation trigger: ${compilationTriggerFileName}`}>
{Object.keys(compilationOutput[compilationTriggerFileName].data.contracts).map((fileName) => {
return Object.keys(compilationOutput[compilationTriggerFileName].data.contracts[fileName]).map((contractName) => {
const value = JSON.stringify({ triggerFilePath: compilationTriggerFileName, filePath: fileName, contractName: contractName })
return (
<option key={`${compilationTriggerFileName}:${fileName}:${contractName}`} value={value}>
{contractName} - {fileName}
</option>
)
})
})}
</optgroup>
))
) : (
<option>Compiled contract required</option>
)}
</select>
</div>
)
}

@ -0,0 +1,33 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
interface NavItemProps {
to: string
icon: JSX.Element
title: string
}
const NavItem: React.FC<NavItemProps> = ({ to, icon, title }) => {
return (
<NavLink
to={to}
className={({ isActive }) => 'text-decoration-none d-flex px-1 py-1 flex-column justify-content-center small ' + (isActive ? "bg-light border-top border-left border-right" : "border-0 bg-transparent")}
>
<span className=''>
<span>{icon}</span>
<span className="ml-2">{title}</span>
</span>
</NavLink>
)
}
export const NavMenu = () => {
return (
<nav className="d-flex medium flex-row w-100" style={{backgroundColor: 'var(--body-bg)!important'}}>
<NavItem to="/" icon={<i className="fas fa-home"></i>} title="Verify" />
<NavItem to="/receipts" icon={<i className="fas fa-receipt"></i>} title="Receipts" />
<NavItem to="/lookup" icon={<i className="fas fa-search"></i>} title="Lookup" />
<NavItem to="/settings" icon={<i className="fas fa-cog"></i>} title="Settings" />
</nav>
)
}

@ -0,0 +1,113 @@
import React, { useState, useEffect, useRef, useMemo } from 'react'
import Fuse from 'fuse.js'
import type { Chain } from '../types'
import { AppContext } from '../AppContext'
function getChainDescriptor(chain: Chain): string {
if (!chain) return ''
return `${chain.title || chain.name} (${chain.chainId})`
}
interface DropdownProps {
label: string
id: string
setSelectedChain: (chain: Chain) => void
selectedChain: Chain
}
export const SearchableChainDropdown: React.FC<DropdownProps> = ({ label, id, setSelectedChain, selectedChain }) => {
const { chains } = React.useContext(AppContext)
const ethereumChainIds = [1, 11155111, 17000]
// Add Ethereum chains to the head of the chains list. Sort the rest alphabetically
const dropdownChains = useMemo(
() =>
chains.sort((a, b) => {
const isAInEthereum = ethereumChainIds.includes(a.chainId)
const isBInEthereum = ethereumChainIds.includes(b.chainId)
if (isAInEthereum && !isBInEthereum) return -1
if (!isAInEthereum && isBInEthereum) return 1
if (isAInEthereum && isBInEthereum) return ethereumChainIds.indexOf(a.chainId) - ethereumChainIds.indexOf(b.chainId)
return (a.title || a.name).localeCompare(b.title || b.name)
}),
[chains]
)
const [searchTerm, setSearchTerm] = useState(selectedChain ? getChainDescriptor(selectedChain) : '')
const [isOpen, setIsOpen] = useState(false)
const [filteredOptions, setFilteredOptions] = useState<Chain[]>(dropdownChains)
const dropdownRef = useRef<HTMLDivElement>(null)
const fuse = new Fuse(dropdownChains, {
keys: ['name', 'chainId', 'title'],
threshold: 0.3,
})
useEffect(() => {
if (searchTerm === '') {
setFilteredOptions(dropdownChains)
} else {
const result = fuse.search(searchTerm)
setFilteredOptions(result.map(({ item }) => item))
}
}, [searchTerm, dropdownChains])
// Close dropdown when user clicks outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
setSearchTerm(getChainDescriptor(selectedChain))
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [selectedChain])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value)
setIsOpen(true)
}
const handleOptionClick = (option: Chain) => {
setSelectedChain(option)
setSearchTerm(getChainDescriptor(option))
setIsOpen(false)
}
const openDropdown = () => {
setIsOpen(true)
setSearchTerm('')
}
if (!dropdownChains || dropdownChains.length === 0) {
return (
<div className="dropdown">
<label htmlFor={id}>{label}</label>
<div>Loading chains...</div>
</div>
)
}
return (
<div className="dropdown mb-3" ref={dropdownRef}>
{' '}
{/* Add ref here */}
<label htmlFor={id}>{label}</label>
<input type="text" value={searchTerm} onChange={handleInputChange} onClick={openDropdown} placeholder="Select a chain" className="form-control" />
{isOpen && (
<ul className="dropdown-menu show w-100 bg-light" style={{ maxHeight: '400px', overflowY: 'auto' }}>
{filteredOptions.map((chain) => (
<li key={chain.chainId} onClick={() => handleOptionClick(chain)} className={`dropdown-item text-dark ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{ cursor: 'pointer', whiteSpace: 'normal' }}>
{getChainDescriptor(chain)}
</li>
))}
</ul>
)}
</div>
)
}

@ -0,0 +1,5 @@
export { NavMenu } from './NavMenu'
export { ContractDropdown } from './ContractDropdown'
export { SearchableChainDropdown } from './SearchableChainDropdown'
export { ContractAddressInput } from './ContractAddressInput'
export { ConfigInput } from './ConfigInput'

@ -1,9 +1,10 @@
import {useState} from 'react'
import { type Dispatch, type SetStateAction, useState } from 'react'
export function useLocalStorage(key: string, initialValue: any) {
export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>]
{
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
@ -18,7 +19,7 @@ export function useLocalStorage(key: string, initialValue: any) {
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: any) => {
const setValue = (value: SetStateAction<T>) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value

@ -0,0 +1,33 @@
import { useEffect, useState } from 'react'
import { Chain, ChainSettings } from '../types'
export function useSourcifySupported(selectedChain: Chain, chainSettings: ChainSettings): boolean {
const [sourcifySupported, setSourcifySupported] = useState(false)
useEffect(() => {
// Unsupported until fetch returns
setSourcifySupported(false)
const sourcifyApi = chainSettings?.verifiers['Sourcify']?.apiUrl
if (!sourcifyApi) {
return
}
const queriedChainId = selectedChain.chainId
const chainsUrl = new URL(sourcifyApi + '/chains')
fetch(chainsUrl.href, { method: 'GET' })
.then((response) => response.json())
.then((result: Array<{ chainId: number }>) => {
// Makes sure that the selectedChain didn't change while the request is running
if (selectedChain.chainId === queriedChainId && result.find((chain) => chain.chainId === queriedChainId)) {
setSourcifySupported(true)
}
})
.catch((error) => {
console.error('Failed to fetch chains.json:', error)
})
}, [selectedChain, chainSettings])
return sourcifySupported
}

@ -0,0 +1,25 @@
import React, { PropsWithChildren } from 'react'
import { NavMenu } from '../components/NavMenu'
interface Props {
from: string
title?: string
description?: string
}
export const DefaultLayout = ({ children, title, description }: PropsWithChildren<Props>) => {
return (
<div className="d-flex flex-column h-100">
<NavMenu />
<div className="py-4 px-3 flex-grow-1 bg-light" style={{ overflowY: 'auto' }}>
<div>
<p className="text-center" style={{ fontSize: '0.8rem' }}>
{description}
</p>
</div>
{children}
</div>
</div>
)
}

@ -0,0 +1 @@
export { DefaultLayout } from './Default'

@ -0,0 +1,49 @@
import React from 'react'
import { HashRouter as Router, Route, Routes } from 'react-router-dom'
import { VerifyView, ReceiptsView, LookupView, SettingsView } from './views'
import { DefaultLayout } from './layouts'
const DisplayRoutes = () => (
<Router>
<Routes>
<Route
path="/"
element={
<DefaultLayout from="/" title="Verify" description="Verify compiled contracts on different verification services">
<VerifyView />
</DefaultLayout>
}
/>
<Route
path="/receipts"
element={
<DefaultLayout from="/" title="Receipts" description="Check the verification statuses of contracts submitted for verification">
<ReceiptsView />
</DefaultLayout>
}
/>
<Route
path="/lookup"
element={
<DefaultLayout from="/" title="Lookup" description="Search for verified contracts and download them to Remix">
<LookupView />
</DefaultLayout>
}
/>
<Route
path="/settings"
element={
<DefaultLayout from="/" title="Settings" description="Customize settings for each verification service and chain">
<SettingsView />
</DefaultLayout>
}
/>
</Routes>
</Router>
)
export default DisplayRoutes

@ -0,0 +1,19 @@
import { VerifierIdentifier } from './VerificationTypes'
export interface VerifierSettings {
apiUrl?: string
explorerUrl?: string
apiKey?: string
}
export type SettingsForVerifier = Partial<Record<VerifierIdentifier, VerifierSettings>>
export interface ChainSettings {
verifiers: SettingsForVerifier
}
export type SettingsForChains = Record<string, ChainSettings>
export interface ContractVerificationSettings {
chains: SettingsForChains
}

@ -0,0 +1 @@
export type ThemeType = 'dark' | 'light'

@ -0,0 +1,80 @@
interface Currency {
name: string
symbol: string
decimals: number
}
// types for https://chainid.network/chains.json (i.e. https://github.com/ethereum-lists/chains)
export interface Chain {
name: string
title?: string
chainId: number
shortName?: string
network?: string
networkId?: number
nativeCurrency?: Currency
rpc: Array<string>
faucets?: string[]
infoURL?: string
}
export type VerifierIdentifier = 'Sourcify' | 'Etherscan' | 'Blockscout'
export const VERIFIERS: VerifierIdentifier[] = ['Sourcify', 'Etherscan', 'Blockscout']
export interface VerifierInfo {
name: VerifierIdentifier
apiUrl: string
}
export interface VerificationReceipt {
receiptId?: string
verifierInfo: VerifierInfo
status: VerificationStatus
message?: string
lookupUrl?: string
contractId: string
isProxyReceipt: boolean
failedChecks: number
}
export interface SubmittedContract {
id: string
filePath: string
contractName: string
chainId: string
address: string
abiEncodedConstructorArgs?: string
date: string
receipts: VerificationReceipt[]
// Only present if the contract is behind a proxy
proxyAddress?: string
proxyReceipts?: VerificationReceipt[]
}
// This and all nested subtypes should be pure interfaces, so they can be converted to JSON easily
export interface SubmittedContracts {
[id: string]: SubmittedContract
}
type SourcifyStatus = 'fully verified' | 'partially verified'
type EtherscanStatus = 'verified' | 'already verified'
export type VerificationStatus = SourcifyStatus | EtherscanStatus | 'failed' | 'pending' | 'awaiting implementation verification' | 'not verified' | 'lookup failed' | 'unknown'
export interface VerificationResponse {
status: VerificationStatus
receiptId: string | null
message?: string
lookupUrl?: string
}
export interface SourceFile {
// Should be in the correct format for creating the files in Remix
path: string
content: string
}
export interface LookupResponse {
status: VerificationStatus
lookupUrl?: string
sourceFiles?: SourceFile[]
targetFilePath?: string
}

@ -0,0 +1,3 @@
export * from './ThemeType'
export * from './SettingsTypes'
export * from './VerificationTypes'

@ -0,0 +1,576 @@
{
"Sourcify": {
"apiUrl": "https://sourcify.dev/server",
"explorerUrl": "https://repo.sourcify.dev"
},
"Etherscan": {
"1": {
"apiUrl": "https://api.etherscan.io",
"explorerUrl": "https://etherscan.io"
},
"56": {
"apiUrl": "https://api.bscscan.com",
"explorerUrl": "https://bscscan.com"
},
"137": {
"apiUrl": "https://api.polygonscan.com",
"explorerUrl": "https://polygonscan.com"
},
"250": {
"apiUrl": "https://api.ftmscan.com",
"explorerUrl": "https://ftmscan.com"
},
"42161": {
"apiUrl": "https://api.arbiscan.io",
"explorerUrl": "https://arbiscan.io"
},
"43114": {
"apiUrl": "https://api.snowtrace.io",
"explorerUrl": "https://snowtrace.io"
},
"1285": {
"apiUrl": "https://api-moonriver.moonscan.io",
"explorerUrl": "https://moonscan.io"
},
"1284": {
"apiUrl": "https://api-moonbeam.moonscan.io",
"explorerUrl": "https://moonscan.io"
},
"25": {
"apiUrl": "https://api.cronoscan.com",
"explorerUrl": "https://cronoscan.com"
},
"199": {
"apiUrl": "https://api.bttcscan.com",
"explorerUrl": "https://bttcscan.com"
},
"10": {
"apiUrl": "https://api-optimistic.etherscan.io",
"explorerUrl": "https://optimistic.etherscan.io"
},
"42220": {
"apiUrl": "https://api.celoscan.io",
"explorerUrl": "https://celoscan.io"
},
"288": {
"apiUrl": "https://api.bobascan.com",
"explorerUrl": "https://bobascan.com"
},
"100": {
"apiUrl": "https://api.gnosisscan.io",
"explorerUrl": "https://gnosisscan.io"
},
"1101": {
"apiUrl": "https://api-zkevm.polygonscan.com",
"explorerUrl": "https://zkevm.polygonscan.com"
},
"59144": {
"apiUrl": "https://api.lineascan.build",
"explorerUrl": "https://lineascan.build"
},
"8453": {
"apiUrl": "https://api.basescan.org",
"explorerUrl": "https://basescan.org"
},
"534352": {
"apiUrl": "https://api.scrollscan.com",
"explorerUrl": "https://scrollscan.com"
},
"17000": {
"apiUrl": "https://api-holesky.etherscan.io",
"explorerUrl": "https://holesky.etherscan.io"
},
"11155111": {
"apiUrl": "https://api-sepolia.etherscan.io",
"explorerUrl": "https://sepolia.etherscan.io"
},
"97": {
"apiUrl": "https://api-testnet.bscscan.com",
"explorerUrl": "https://bscscan.com"
},
"80001": {
"apiUrl": "https://api-testnet.polygonscan.com",
"explorerUrl": "https://polygonscan.com"
},
"4002": {
"apiUrl": "https://api-testnet.ftmscan.com",
"explorerUrl": "https://ftmscan.com"
},
"421611": {
"apiUrl": "https://api-testnet.arbiscan.io",
"explorerUrl": "https://arbiscan.io"
},
"42170": {
"apiUrl": "https://api-nova.arbiscan.io",
"explorerUrl": "https://nova.arbiscan.io"
},
"43113": {
"apiUrl": "https://api-testnet.snowtrace.io",
"explorerUrl": "https://snowtrace.io"
},
"1287": {
"apiUrl": "https://api-moonbase.moonscan.io",
"explorerUrl": "https://moonscan.io"
},
"338": {
"apiUrl": "https://api-testnet.cronoscan.com",
"explorerUrl": "https://cronoscan.com"
},
"1028": {
"apiUrl": "https://api-testnet.bttcscan.com",
"explorerUrl": "https://bttcscan.com"
},
"44787": {
"apiUrl": "https://api-alfajores.celoscan.io",
"explorerUrl": "https://alfajores.celoscan.io"
},
"2888": {
"apiUrl": "https://api-testnet.bobascan.com",
"explorerUrl": "https://bobascan.com"
},
"84532": {
"apiUrl": "https://api-sepolia.basescan.org",
"explorerUrl": "https://sepolia.basescan.org"
},
"1442": {
"apiUrl": "https://api-testnet-zkevm.polygonscan.com",
"explorerUrl": "https://zkevm.polygonscan.com"
},
"59140": {
"apiUrl": "https://api-testnet.lineascan.build",
"explorerUrl": "https://lineascan.build"
},
"534351": {
"apiUrl": "https://api-sepolia.scrollscan.com",
"explorerUrl": "https://sepolia.scrollscan.com"
}
},
"Blockscout": {
"1": {
"apiUrl": "https://eth.blockscout.com"
},
"5": {
"apiUrl": "https://eth-goerli.blockscout.com"
},
"10": {
"apiUrl": "https://optimism.blockscout.com"
},
"30": {
"apiUrl": "https://rootstock.blockscout.com"
},
"31": {
"apiUrl": "https://rootstock-testnet.blockscout.com"
},
"42": {
"apiUrl": "https://explorer.execution.mainnet.lukso.network"
},
"61": {
"apiUrl": "https://etc.blockscout.com"
},
"63": {
"apiUrl": "https://etc-mordor.blockscout.com"
},
"81": {
"apiUrl": "https://shibuya.blockscout.com"
},
"100": {
"apiUrl": "https://gnosis.blockscout.com"
},
"109": {
"apiUrl": "https://www.shibariumscan.io"
},
"111": {
"apiUrl": "https://testnet-explorer.gobob.xyz"
},
"122": {
"apiUrl": "https://explorer.fuse.io"
},
"123": {
"apiUrl": "https://explorer.fusespark.io"
},
"137": {
"apiUrl": "https://polygon.blockscout.com"
},
"148": {
"apiUrl": "https://explorer.evm.shimmer.network"
},
"157": {
"apiUrl": "https://puppyscan.shib.io"
},
"169": {
"apiUrl": "https://pacific-explorer.manta.network"
},
"185": {
"apiUrl": "https://explorer.mintchain.io"
},
"197": {
"apiUrl": "https://dxb.vrcscan.com"
},
"248": {
"apiUrl": "https://explorer.oasys.games"
},
"291": {
"apiUrl": "https://explorer.orderly.network"
},
"300": {
"apiUrl": "https://zksync-sepolia.blockscout.com"
},
"311": {
"apiUrl": "https://omaxscan.com"
},
"313": {
"apiUrl": "https://ncnscan.com"
},
"324": {
"apiUrl": "https://zksync.blockscout.com"
},
"336": {
"apiUrl": "https://shiden.blockscout.com"
},
"360": {
"apiUrl": "https://molten.calderaexplorer.xyz"
},
"372": {
"apiUrl": "https://explorer.fortresschain.finance"
},
"416": {
"apiUrl": "https://explorer.sx.technology"
},
"570": {
"apiUrl": "https://explorer.rollux.com"
},
"592": {
"apiUrl": "https://astar.blockscout.com"
},
"648": {
"apiUrl": "https://explorer-endurance.fusionist.io"
},
"713": {
"apiUrl": "https://vrcscan.com"
},
"721": {
"apiUrl": "https://explorer.lycanchain.com"
},
"813": {
"apiUrl": "https://qng.qitmeer.io"
},
"879": {
"apiUrl": "https://kadscan.kadsea.org"
},
"911": {
"apiUrl": "https://scan.taprootchain.io"
},
"919": {
"apiUrl": "https://sepolia.explorer.mode.network"
},
"957": {
"apiUrl": "https://explorer.lyra.finance"
},
"1073": {
"apiUrl": "https://explorer.evm.testnet.shimmer.network"
},
"1075": {
"apiUrl": "https://explorer.evm.testnet.iotaledger.net"
},
"1101": {
"apiUrl": "https://zkevm.blockscout.com"
},
"1135": {
"apiUrl": "https://blockscout.lisk.com"
},
"1291": {
"apiUrl": "https://explorer-evm.testnet.swisstronik.com"
},
"1432": {
"apiUrl": "https://explorer-sepolia.zentachain.io"
},
"1687": {
"apiUrl": "https://sepolia-testnet-explorer.mintchain.io"
},
"1729": {
"apiUrl": "https://explorer.reya.network"
},
"1750": {
"apiUrl": "https://explorer.metall2.com"
},
"1829": {
"apiUrl": "https://explorer.playblock.io"
},
"1833": {
"apiUrl": "https://verify-testnet.blockscout.com"
},
"1890": {
"apiUrl": "https://phoenix.lightlink.io"
},
"1891": {
"apiUrl": "https://pegasus.lightlink.io"
},
"1995": {
"apiUrl": "https://explorer.testnet.edexa.com"
},
"1996": {
"apiUrl": "https://explorer.sanko.xyz"
},
"2016": {
"apiUrl": "https://netzexplorer.io"
},
"2021": {
"apiUrl": "https://edgscan.live"
},
"2145": {
"apiUrl": "https://explorer.chainers.io"
},
"2410": {
"apiUrl": "https://explorer.karak.network"
},
"2999": {
"apiUrl": "https://explorer.aevo.xyz"
},
"3799": {
"apiUrl": "https://testnet-explorer.tangle.tools"
},
"3888": {
"apiUrl": "https://kalyscan.io"
},
"4058": {
"apiUrl": "https://ocean.ftnscan.com"
},
"4202": {
"apiUrl": "https://sepolia-blockscout.lisk.com"
},
"4396": {
"apiUrl": "https://explorer.vedaord.com"
},
"4460": {
"apiUrl": "https://testnet-explorer.orderly.org"
},
"4653": {
"apiUrl": "https://explorer.gold.dev"
},
"4999": {
"apiUrl": "https://blackfort.blockscout.com"
},
"5000": {
"apiUrl": "https://explorer.mantle.xyz"
},
"5003": {
"apiUrl": "https://explorer.sepolia.mantle.xyz"
},
"5112": {
"apiUrl": "https://explorer.ham.fun"
},
"6398": {
"apiUrl": "https://connext-sepolia.blockscout.com"
},
"6699": {
"apiUrl": "https://oxscan.io"
},
"6969": {
"apiUrl": "https://tombscout.com"
},
"7001": {
"apiUrl": "https://zetachain-athens-3.blockscout.com"
},
"7771": {
"apiUrl": "https://testnetscan.bit-rock.io"
},
"7887": {
"apiUrl": "https://explorer.kinto.xyz"
},
"7979": {
"apiUrl": "https://doscan.io"
},
"8131": {
"apiUrl": "https://testnet-qng.qitmeer.io"
},
"8337": {
"apiUrl": "https://explorer.ipsprotocol.xyz"
},
"8453": {
"apiUrl": "https://base.blockscout.com"
},
"8822": {
"apiUrl": "https://explorer.evm.iota.org"
},
"8853": {
"apiUrl": "https://explorer.myclique.io"
},
"8866": {
"apiUrl": "https://explorer.lumio.io"
},
"8869": {
"apiUrl": "https://lif3scout.com"
},
"8899": {
"apiUrl": "https://exp-l1-ng.jibchain.net"
},
"9996": {
"apiUrl": "https://mainnet.mindscan.info"
},
"12553": {
"apiUrl": "https://scan.rss3.io"
},
"13371": {
"apiUrl": "https://explorer.immutable.com"
},
"13473": {
"apiUrl": "https://explorer.testnet.immutable.com"
},
"17000": {
"apiUrl": "https://eth-holesky.blockscout.com"
},
"18233": {
"apiUrl": "https://unreal.blockscout.com"
},
"23452": {
"apiUrl": "https://scan.dreyerx.com"
},
"27563": {
"apiUrl": "https://scan.onchaincoin.io"
},
"34443": {
"apiUrl": "https://explorer.mode.network"
},
"42161": {
"apiUrl": "https://arbitrum.blockscout.com"
},
"42766": {
"apiUrl": "https://testnet-scan.zkfair.io"
},
"53302": {
"apiUrl": "https://sepolia-explorer.superseed.xyz"
},
"53339": {
"apiUrl": "https://blk.keeex.me"
},
"54211": {
"apiUrl": "https://explorer.testedge2.haqq.network"
},
"57000": {
"apiUrl": "https://rollux.tanenbaum.io"
},
"60808": {
"apiUrl": "https://explorer.gobob.xyz"
},
"64002": {
"apiUrl": "https://xchain-testnet-explorer.idex.io"
},
"70700": {
"apiUrl": "https://explorer.apex.proofofplay.com"
},
"78225": {
"apiUrl": "https://explorer.stack.so"
},
"84532": {
"apiUrl": "https://base-sepolia.blockscout.com"
},
"98881": {
"apiUrl": "https://explorer.ebi.xyz"
},
"101010": {
"apiUrl": "https://stability.blockscout.com"
},
"102031": {
"apiUrl": "https://creditcoin-testnet.blockscout.com"
},
"111111": {
"apiUrl": "https://explorer.main.siberium.net"
},
"111188": {
"apiUrl": "https://explorer.re.al"
},
"224433": {
"apiUrl": "https://scan.conet.network"
},
"241120": {
"apiUrl": "https://andromeda.anomalyscan.io"
},
"355113": {
"apiUrl": "https://explorer.testnet.bitfinity.network"
},
"622277": {
"apiUrl": "https://explorer.hypra.network"
},
"656476": {
"apiUrl": "https://opencampus-codex.blockscout.com"
},
"686868": {
"apiUrl": "https://scan.wonnetwork.org"
},
"782251": {
"apiUrl": "https://testnet.explorer.stack.so"
},
"984122": {
"apiUrl": "https://explorer.forma.art"
},
"5820948": {
"apiUrl": "https://onlyscan.info"
},
"7225878": {
"apiUrl": "https://explorer.saakuru.network"
},
"7777777": {
"apiUrl": "https://explorer.zora.energy"
},
"10241025": {
"apiUrl": "https://hal.explorer.caldera.xyz"
},
"11155111": {
"apiUrl": "https://eth-sepolia.blockscout.com"
},
"11155112": {
"apiUrl": "https://explorer-testnet.aevo.xyz"
},
"11155420": {
"apiUrl": "https://optimism-sepolia.blockscout.com"
},
"20180427": {
"apiUrl": "https://stability-testnet.blockscout.com"
},
"28122024": {
"apiUrl": "https://scanv2-testnet.ancient8.gg"
},
"65010002": {
"apiUrl": "https://bakerloo.autonity.org"
},
"65100002": {
"apiUrl": "https://piccadilly.autonity.org"
},
"88888888": {
"apiUrl": "https://babytuna.explorer.tunachain.io"
},
"89346162": {
"apiUrl": "https://reya-cronos.blockscout.com"
},
"245022926": {
"apiUrl": "https://neon-devnet.blockscout.com"
},
"245022934": {
"apiUrl": "https://neon.blockscout.com"
},
"666666666": {
"apiUrl": "https://explorer.degen.tips"
},
"888888888": {
"apiUrl": "https://scan.ancient8.gg"
},
"1123581321": {
"apiUrl": "https://explorer.xoracle.io"
},
"1313161554": {
"apiUrl": "https://explorer.mainnet.aurora.dev"
},
"1380012617": {
"apiUrl": "https://mainnet.explorer.rarichain.org"
},
"2046399126": {
"apiUrl": "https://elated-tan-skat.explorer.mainnet.skalenodes.com"
},
"2863311531": {
"apiUrl": "https://testnet.a8scan.io"
},
"81247166294": {
"apiUrl": "https://testnet.otoscan.io"
}
}
}

@ -0,0 +1,28 @@
import type { ChainSettings, ContractVerificationSettings, SettingsForVerifier, VerifierSettings } from '../types/SettingsTypes'
import { VerifierIdentifier, VERIFIERS } from '../types/VerificationTypes'
import DEFAULT_APIS from './default-apis.json'
export function mergeChainSettingsWithDefaults(chainId: string, userSettings: ContractVerificationSettings): ChainSettings {
const verifiers: SettingsForVerifier = {}
for (const verifierId of VERIFIERS) {
const userSetting: VerifierSettings = userSettings.chains[chainId]?.verifiers[verifierId] ?? {}
verifiers[verifierId] = { ...userSetting }
let defaultsForVerifier: VerifierSettings
if (verifierId === 'Sourcify') {
defaultsForVerifier = DEFAULT_APIS['Sourcify']
} else {
defaultsForVerifier = DEFAULT_APIS[verifierId][chainId] ?? {}
}
// Prefer user settings over defaults
verifiers[verifierId] = Object.assign({}, defaultsForVerifier, userSetting)
}
return { verifiers }
}
export function validConfiguration(chainSettings: ChainSettings | undefined, verifierId: VerifierIdentifier) {
return !!chainSettings && !!chainSettings.verifiers[verifierId]?.apiUrl && (verifierId !== 'Etherscan' || !!chainSettings.verifiers[verifierId]?.apiKey)
}

@ -0,0 +1 @@
export * from './default-settings'

@ -0,0 +1,167 @@
import { useContext, useEffect, useMemo, useState } from 'react'
import { SearchableChainDropdown, ContractAddressInput } from '../components'
import { mergeChainSettingsWithDefaults, validConfiguration } from '../utils'
import type { LookupResponse, VerifierIdentifier } from '../types'
import { VERIFIERS } from '../types'
import { AppContext } from '../AppContext'
import { CustomTooltip } from '@remix-ui/helper'
import { getVerifier } from '../Verifiers'
import { useNavigate } from 'react-router-dom'
import { VerifyFormContext } from '../VerifyFormContext'
import { useSourcifySupported } from '../hooks/useSourcifySupported'
export const LookupView = () => {
const { settings, clientInstance } = useContext(AppContext)
const { selectedChain, setSelectedChain } = useContext(VerifyFormContext)
const [contractAddress, setContractAddress] = useState('')
const [contractAddressError, setContractAddressError] = useState('')
const [loadingVerifiers, setLoadingVerifiers] = useState<Partial<Record<VerifierIdentifier, boolean>>>({})
const [lookupResults, setLookupResult] = useState<Partial<Record<VerifierIdentifier, LookupResponse>>>({})
const navigate = useNavigate()
const chainSettings = useMemo(() => (selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined), [selectedChain, settings])
const sourcifySupported = useSourcifySupported(selectedChain, chainSettings)
const noVerifierEnabled = VERIFIERS.every((verifierId) => !validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported))
const submitDisabled = !!contractAddressError || !contractAddress || !selectedChain || noVerifierEnabled
// Reset results when chain or contract changes
useEffect(() => {
setLookupResult({})
setLoadingVerifiers({})
}, [selectedChain, contractAddress])
const handleLookup = (e) => {
if (Object.values(loadingVerifiers).some((loading) => loading)) {
console.error('Lookup request already running')
return
}
e.preventDefault()
for (const verifierId of VERIFIERS) {
if (!validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported)) {
continue
}
setLoadingVerifiers((prev) => ({ ...prev, [verifierId]: true }))
const verifier = getVerifier(verifierId, chainSettings.verifiers[verifierId])
verifier
.lookup(contractAddress, selectedChain.chainId.toString())
.then((result) => setLookupResult((prev) => ({ ...prev, [verifierId]: result })))
.catch((err) =>
setLookupResult((prev) => {
console.error(err)
return { ...prev, [verifierId]: { status: 'lookup failed' } }
})
)
.finally(() => setLoadingVerifiers((prev) => ({ ...prev, [verifierId]: false })))
}
}
const sendToMatomo = async (eventAction: string, eventName: string) => {
await clientInstance.call('matomo' as any, 'track', ['trackEvent', 'ContractVerification', eventAction, eventName]);
}
const handleOpenInRemix = async (lookupResponse: LookupResponse) => {
for (const source of lookupResponse.sourceFiles ?? []) {
try {
await clientInstance.call('fileManager', 'setFile', source.path, source.content)
} catch (err) {
console.error(`Error while creating file ${source.path}: ${err.message}`)
}
}
try {
await clientInstance.call('fileManager', 'open', lookupResponse.targetFilePath)
await sendToMatomo('lookup', "openInRemix On: " + selectedChain)
} catch (err) {
console.error(`Error focusing file ${lookupResponse.targetFilePath}: ${err.message}`)
}
}
return (
<>
<form onSubmit={handleLookup}>
<SearchableChainDropdown label="Chain" id="network-dropdown" selectedChain={selectedChain} setSelectedChain={setSelectedChain} />
<ContractAddressInput
label="Contract Address"
id="contract-address"
contractAddress={contractAddress}
setContractAddress={setContractAddress}
contractAddressError={contractAddressError}
setContractAddressError={setContractAddressError}
/>
<button type="submit" className="btn w-100 btn-primary" disabled={submitDisabled}>
Lookup
</button>
</form>
<div className="pt-3">
{ chainSettings &&
VERIFIERS.map((verifierId) => {
if (!validConfiguration(chainSettings, verifierId)) {
return (
<div key={verifierId} className="pt-4">
<div>
<span className="font-weight-bold text-secondary">{verifierId}</span>{' '}
<CustomTooltip tooltipText="Configure the API in the settings">
<span className="text-secondary" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}>
Enable?
</span>
</CustomTooltip>
</div>
</div>
)
}
if (verifierId === 'Sourcify' && !sourcifySupported) {
return (
<div key={verifierId} className="pt-4">
<div>
<span className="font-weight-bold text-secondary">{verifierId}</span>{' '}
<CustomTooltip tooltipText={`The configured Sourcify server (${chainSettings.verifiers['Sourcify'].apiUrl}) does not support chain ${selectedChain?.chainId}`}>
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}>
Unsupported
</span>
</CustomTooltip>
</div>
</div>
)
}
return (
<div key={verifierId} className="pt-4">
<div>
<span className="font-weight-bold">{verifierId}</span> <span className="text-secondary">{chainSettings.verifiers[verifierId].apiUrl}</span>
</div>
{!!loadingVerifiers[verifierId] && (
<div className="pt-2 d-flex justify-content-center">
<i className="fas fa-spinner fa-spin fa-2x"></i>
</div>
)}
{!loadingVerifiers[verifierId] && !!lookupResults[verifierId] && (
<div>
<div className="pt-2">
Status:{' '}
<span className="font-weight-bold" style={{ textTransform: 'capitalize' }}>
{lookupResults[verifierId].status}
</span>{' '}
{!!lookupResults[verifierId].lookupUrl && <a href={lookupResults[verifierId].lookupUrl} target="_blank" className="fa fas fa-arrow-up-right-from-square"></a>}
</div>
{!!lookupResults[verifierId].sourceFiles && lookupResults[verifierId].sourceFiles.length > 0 && (
<div className="pt-2 d-flex flex-row justify-content-center">
<button className="btn btn-secondary bg-transparent text-body" onClick={() => handleOpenInRemix(lookupResults[verifierId])}>
<i className="fas fa-download"></i> Open in Remix
</button>
</div>
)}
</div>
)}
</div>
)
})
}
</div>
</>
)
}

@ -0,0 +1,16 @@
import { useContext } from 'react'
import { AccordionReceipt } from '../components/AccordionReceipt'
import { AppContext } from '../AppContext'
export const ReceiptsView = () => {
const { submittedContracts } = useContext(AppContext)
const contracts = Object.values(submittedContracts).reverse()
return (
<div>
{contracts.length > 0 ? contracts.map((contract, index) => (
<AccordionReceipt contract={contract} index={index} />
)) : <div className="text-center mt-5">No contracts submitted for verification</div>}
</div>
)
}

@ -0,0 +1,54 @@
import { useContext, useMemo, useState } from 'react'
import { SearchableChainDropdown, ConfigInput } from '../components'
import type { VerifierIdentifier, VerifierSettings, ContractVerificationSettings } from '../types'
import { mergeChainSettingsWithDefaults } from '../utils'
import { AppContext } from '../AppContext'
import { VerifyFormContext } from '../VerifyFormContext'
export const SettingsView = () => {
const { settings, setSettings } = useContext(AppContext)
const { selectedChain, setSelectedChain } = useContext(VerifyFormContext)
const chainSettings = useMemo(() => (selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined), [selectedChain, settings])
const handleChange = (verifier: VerifierIdentifier, key: keyof VerifierSettings, value: string) => {
const chainId = selectedChain.chainId.toString()
const changedSettings: ContractVerificationSettings = JSON.parse(JSON.stringify(settings))
if (!changedSettings.chains[chainId]) {
changedSettings.chains[chainId] = { verifiers: {} }
}
if (!changedSettings.chains[chainId].verifiers[verifier]) {
changedSettings.chains[chainId].verifiers[verifier] = {}
}
changedSettings.chains[chainId].verifiers[verifier][key] = value
setSettings(changedSettings)
}
return (
<>
<SearchableChainDropdown label="Chain" id="network-dropdown" setSelectedChain={setSelectedChain} selectedChain={selectedChain} />
{selectedChain && (
<div>
<div className="p-2 my-2 border">
<span className="font-weight-bold">Sourcify - {selectedChain.name}</span>
<ConfigInput label="API URL" id="sourcify-api-url" secret={false} initialValue={chainSettings.verifiers['Sourcify']?.apiUrl ?? ''} saveResult={(result) => handleChange('Sourcify', 'apiUrl', result)} />
<ConfigInput label="Repo URL" id="sourcify-explorer-url" secret={false} initialValue={chainSettings.verifiers['Sourcify']?.explorerUrl ?? ''} saveResult={(result) => handleChange('Sourcify', 'explorerUrl', result)} />
</div>
<div className="p-2 my-2 border">
<span className="font-weight-bold">Etherscan - {selectedChain.name}</span>
<ConfigInput label="API Key" id="etherscan-api-key" secret={true} initialValue={chainSettings.verifiers['Etherscan']?.apiKey ?? ''} saveResult={(result) => handleChange('Etherscan', 'apiKey', result)} />
<ConfigInput label="API URL" id="etherscan-api-url" secret={false} initialValue={chainSettings.verifiers['Etherscan']?.apiUrl ?? ''} saveResult={(result) => handleChange('Etherscan', 'apiUrl', result)} />
<ConfigInput label="Explorer URL" id="etherscan-explorer-url" secret={false} initialValue={chainSettings.verifiers['Etherscan']?.explorerUrl ?? ''} saveResult={(result) => handleChange('Etherscan', 'explorerUrl', result)} />
</div>
<div className="p-2 my-2 border">
<span className="font-weight-bold">Blockscout - {selectedChain.name}</span>
<ConfigInput label="Instance URL" id="blockscout-api-url" secret={false} initialValue={chainSettings.verifiers['Blockscout']?.apiUrl ?? ''} saveResult={(result) => handleChange('Blockscout', 'apiUrl', result)} />
</div>
</div>
)}
</>
)
}

@ -0,0 +1,279 @@
import { useContext, useEffect, useMemo, useState } from 'react'
import { AppContext } from '../AppContext'
import { SearchableChainDropdown, ContractDropdown, ContractAddressInput } from '../components'
import type { VerifierIdentifier, SubmittedContract, VerificationReceipt, VerifierInfo, VerificationResponse } from '../types'
import { VERIFIERS } from '../types'
import { mergeChainSettingsWithDefaults, validConfiguration } from '../utils'
import { useNavigate } from 'react-router-dom'
import { ConstructorArguments } from '../components/ConstructorArguments'
import { CustomTooltip } from '@remix-ui/helper'
import { AbstractVerifier, getVerifier } from '../Verifiers'
import { VerifyFormContext } from '../VerifyFormContext'
import { useSourcifySupported } from '../hooks/useSourcifySupported'
export const VerifyView = () => {
const { compilationOutput, setSubmittedContracts, settings, clientInstance } = useContext(AppContext)
const { selectedChain, setSelectedChain, contractAddress, setContractAddress, contractAddressError, setContractAddressError, selectedContract, setSelectedContract, proxyAddress, setProxyAddress, proxyAddressError, setProxyAddressError, abiEncodedConstructorArgs, setAbiEncodedConstructorArgs, abiEncodingError, setAbiEncodingError } = useContext(VerifyFormContext)
const [enabledVerifiers, setEnabledVerifiers] = useState<Partial<Record<VerifierIdentifier, boolean>>>({})
const [hasProxy, setHasProxy] = useState(!!proxyAddress)
const navigate = useNavigate()
const chainSettings = useMemo(() => (selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined), [selectedChain, settings])
const sourcifySupported = useSourcifySupported(selectedChain, chainSettings)
const noVerifierEnabled = VERIFIERS.every((verifierId) => !validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported)) || Object.values(enabledVerifiers).every((enabled) => !enabled)
const submitDisabled = !!contractAddressError || !contractAddress || !selectedChain || !selectedContract || (hasProxy && !!proxyAddressError) || (hasProxy && !proxyAddress) || noVerifierEnabled
// Enable all verifiers with valid configuration
useEffect(() => {
const changedEnabledVerifiers = {}
for (const verifierId of VERIFIERS) {
if (validConfiguration(chainSettings, verifierId) && (verifierId !== 'Sourcify' || sourcifySupported)) {
changedEnabledVerifiers[verifierId] = true
}
}
setEnabledVerifiers(changedEnabledVerifiers)
}, [selectedChain, sourcifySupported])
const handleVerifierCheckboxClick = (verifierId: VerifierIdentifier, checked: boolean) => {
setEnabledVerifiers({ ...enabledVerifiers, [verifierId]: checked })
}
const sendToMatomo = async (eventAction: string, eventName: string) => {
await clientInstance.call("matomo" as any, 'track', ['trackEvent', 'ContractVerification', eventAction, eventName]);
}
const handleVerify = async (e) => {
e.preventDefault()
const { triggerFilePath, filePath, contractName } = selectedContract
const compilerAbstract = compilationOutput[triggerFilePath]
if (!compilerAbstract) {
throw new Error(`Error: Compilation output not found for ${triggerFilePath}`)
}
const date = new Date()
const contractId = selectedChain?.chainId + '-' + contractAddress + '-' + date.toUTCString()
const receipts: VerificationReceipt[] = []
for (const [verifierId, enabled] of Object.entries(enabledVerifiers)) {
if (!enabled) {
continue
}
const verifierInfo: VerifierInfo = {
apiUrl: chainSettings.verifiers[verifierId].apiUrl,
name: verifierId as VerifierIdentifier,
}
receipts.push({ verifierInfo, status: 'pending', contractId, isProxyReceipt: false, failedChecks: 0 })
if (enabledVerifiers.Blockscout) await sendToMatomo('verify', "verifyWith: Blockscout On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress))
if (enabledVerifiers.Etherscan) await sendToMatomo('verify', "verifyWithEtherscan On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress))
if (enabledVerifiers.Sourcify) await sendToMatomo('verify', "verifyWithSourcify On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress))
}
const newSubmittedContract: SubmittedContract = {
id: contractId,
address: contractAddress,
chainId: selectedChain?.chainId.toString(),
filePath,
contractName,
date: date.toUTCString(),
receipts,
}
if (abiEncodedConstructorArgs) {
newSubmittedContract.abiEncodedConstructorArgs = abiEncodedConstructorArgs
}
const proxyReceipts: VerificationReceipt[] = []
if (hasProxy) {
for (const [verifierId, enabled] of Object.entries(enabledVerifiers)) {
if (!enabled) {
continue
}
const verifierSettings = chainSettings.verifiers[verifierId]
const verifierInfo: VerifierInfo = {
apiUrl: verifierSettings.apiUrl,
name: verifierId as VerifierIdentifier,
}
let verifier: AbstractVerifier
try {
verifier = getVerifier(verifierId as VerifierIdentifier, verifierSettings)
} catch (e) {
// User settings might be invalid
proxyReceipts.push({ verifierInfo, status: 'failed', contractId, isProxyReceipt: true, message: e.message, failedChecks: 0 })
continue
}
if (!verifier.verifyProxy) {
continue
}
proxyReceipts.push({ verifierInfo, status: 'awaiting implementation verification', contractId, isProxyReceipt: true, failedChecks: 0 })
}
newSubmittedContract.proxyAddress = proxyAddress
newSubmittedContract.proxyReceipts = proxyReceipts
}
setSubmittedContracts((prev) => ({ ...prev, [newSubmittedContract.id]: newSubmittedContract }))
// Reset form
setContractAddress('')
setAbiEncodedConstructorArgs('')
setSelectedContract(undefined)
setProxyAddress('')
// Take user to receipt view
navigate('/receipts')
const verify = async (receipt: VerificationReceipt) => {
if (receipt.status === 'failed') {
return // failed already when creating
}
const { verifierInfo } = receipt
if (receipt.status === 'awaiting implementation verification') {
const implementationReceipt = newSubmittedContract.receipts.find((r) => r.verifierInfo.name === verifierInfo.name)
if (implementationReceipt.status === 'pending') {
setTimeout(() => verify(receipt), 1000)
return
}
}
const verifierSettings = chainSettings.verifiers[verifierInfo.name]
try {
const verifier = getVerifier(verifierInfo.name, verifierSettings)
let response: VerificationResponse
if (receipt.isProxyReceipt) {
response = await verifier.verifyProxy(newSubmittedContract)
} else {
response = await verifier.verify(newSubmittedContract, compilerAbstract)
}
const { status, message, receiptId, lookupUrl } = response
receipt.status = status
receipt.message = message
if (lookupUrl) {
receipt.lookupUrl = lookupUrl
}
if (receiptId) {
receipt.receiptId = receiptId
}
} catch (e) {
const err = e as Error
receipt.status = 'failed'
receipt.message = err.message
}
// Update the UI
setSubmittedContracts((prev) => ({ ...prev, [newSubmittedContract.id]: newSubmittedContract }))
}
// Verify for each verifier. forEach does not wait for await and each promise will execute in parallel
receipts.forEach(verify)
proxyReceipts.forEach(verify)
}
return (
<form onSubmit={handleVerify}>
<SearchableChainDropdown label="Chain" id="network-dropdown" selectedChain={selectedChain} setSelectedChain={setSelectedChain} />
<ContractAddressInput
label="Contract Address"
id="contract-address"
contractAddress={contractAddress}
setContractAddress={setContractAddress}
contractAddressError={contractAddressError}
setContractAddressError={setContractAddressError}
/>
<CustomTooltip tooltipText="Please compile and select the solidity contract you need to verify.">
<ContractDropdown label="Contract Name" id="contract-dropdown-1" selectedContract={selectedContract} setSelectedContract={setSelectedContract} />
</CustomTooltip>
{selectedContract && <ConstructorArguments
abiEncodedConstructorArgs={abiEncodedConstructorArgs}
setAbiEncodedConstructorArgs={setAbiEncodedConstructorArgs}
selectedContract={selectedContract}
abiEncodingError={abiEncodingError}
setAbiEncodingError={setAbiEncodingError}
/>}
<div className="pt-3">
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input id="has-proxy" className="form-check-input custom-control-input" type="checkbox" checked={!!hasProxy} onChange={(e) => setHasProxy(e.target.checked)} />
<label htmlFor="has-proxy" className="m-0 form-check-label custom-control-label" style={{ paddingTop: '2px' }}>
The deployed contract is behind a proxy
</label>
</div>
{hasProxy && <ContractAddressInput
label="Proxy Address"
id="proxy-address"
contractAddress={proxyAddress}
setContractAddress={setProxyAddress}
contractAddressError={proxyAddressError}
setContractAddressError={setProxyAddressError}
/>}
</div>
<div className="pt-3">
Verify on:
{VERIFIERS.map((verifierId) => {
const disabledVerifier = !chainSettings || !validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported)
return (
<div key={verifierId} className="pt-2">
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input
className="form-check-input custom-control-input"
type="checkbox"
id={`verifier-${verifierId}`}
checked={!!enabledVerifiers[verifierId]}
onChange={(e) => handleVerifierCheckboxClick(verifierId, e.target.checked)}
disabled={disabledVerifier}
/>
<label
htmlFor={`verifier-${verifierId}`}
className={`m-0 form-check-label custom-control-label large font-weight-bold${!disabledVerifier ? '' : ' text-secondary'}`}
style={{ fontSize: '1rem', lineHeight: '1.5', color: 'var(--text)' }}
>
{verifierId}
</label>
</div>
<div className="d-flex flex-column align-items-start pl-4">
{!chainSettings ? (
''
) : !validConfiguration(chainSettings, verifierId) ? (
<CustomTooltip tooltipText="Configure the API in the settings">
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}>
Enable?
</span>
</CustomTooltip>
) : verifierId === 'Sourcify' && !sourcifySupported ? (
<CustomTooltip tooltipText={`The configured Sourcify server (${chainSettings.verifiers['Sourcify'].apiUrl}) does not support chain ${selectedChain?.chainId}`}>
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}>
Unsupported
</span>
</CustomTooltip>
) : (
<span className="text-secondary">{chainSettings.verifiers[verifierId].apiUrl}</span>
)}
</div>
</div>
)
})}
</div>
<CustomTooltip tooltipText={submitDisabled ? (
(!!contractAddressError || !contractAddress) ? "Please provide a valid contract address." :
!selectedChain ? "Please select the chain." :
!selectedContract ? "Please select the contract (compile if needed)." :
((hasProxy && !!proxyAddressError) || (hasProxy && !proxyAddress)) ? "Please provide a valid proxy contract address." :
"Please provide all necessary data to verify") // Is not expected to be a case
: "Verify with selected tools"}>
<button type="submit" className="w-100 btn btn-primary mt-3" disabled={submitDisabled}>
Verify
</button>
</CustomTooltip>
</form>
)
}

@ -0,0 +1,4 @@
export { VerifyView } from './VerifyView'
export { SettingsView } from './SettingsView'
export { LookupView } from './LookupView'
export { ReceiptsView } from './ReceiptsView'

@ -2,5 +2,5 @@
// When building for production, this file is replaced with `environment.prod.ts`.
export const environment = {
production: false
};
production: false,
}

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -2,13 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Etherscan</title>
<title>Contract Verification</title>
<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">
<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>

@ -0,0 +1,10 @@
import React from 'react'
import * as ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
import App from './app/app'
const container = document.getElementById('root')
if (container) {
createRoot(container).render(<App />)
}

@ -3,5 +3,5 @@
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import 'core-js/stable'
import 'regenerator-runtime/runtime'

File diff suppressed because one or more lines are too long

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

@ -1,11 +1,11 @@
const {composePlugins, withNx} = require('@nrwl/webpack')
const { composePlugins, withNx } = require('@nrwl/webpack')
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'
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
}
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
@ -31,28 +31,28 @@ module.exports = composePlugins(withNx(), (config) => {
readline: false,
child_process: false,
buffer: require.resolve('buffer/'),
vm: require.resolve('vm-browserify')
vm: require.resolve('vm-browserify'),
}
// add externals
config.externals = {
...config.externals,
solc: 'solc'
solc: 'solc',
}
// 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`
config.output.filename = `[name].plugin-contract-verification.${versionData.timestamp}.js`
config.output.chunkFilename = `[name].plugin-contract-verification.${versionData.timestamp}.js`
// add copy & provide plugin
config.plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'],
process: 'process/browser'
process: 'process/browser',
})
)
@ -60,7 +60,7 @@ module.exports = composePlugins(withNx(), (config) => {
config.module.rules.push({
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre'
enforce: 'pre',
})
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
@ -74,16 +74,16 @@ module.exports = composePlugins(withNx(), (config) => {
compress: false,
mangle: false,
format: {
comments: false
}
comments: false,
},
},
extractComments: false
extractComments: false,
}),
new CssMinimizerPlugin()
new CssMinimizerPlugin(),
]
config.watchOptions = {
ignored: /node_modules/
ignored: /node_modules/,
}
return config

@ -1,69 +0,0 @@
{
"name": "etherscan",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/etherscan/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "development",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/etherscan",
"index": "apps/etherscan/src/index.html",
"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/profile.json"
],
"styles": ["apps/etherscan/src/styles.css"],
"scripts": [],
"webpackConfig": "apps/etherscan/webpack.config.js"
},
"configurations": {
"development": {
},
"production": {
"fileReplacements": [
{
"replace": "apps/etherscan/src/environments/environment.ts",
"with": "apps/etherscan/src/environments/environment.prod.ts"
}
]
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/etherscan/**/*.ts"],
"eslintConfig": "apps/etherscan/.eslintrc"
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "etherscan:build",
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {
"buildTarget": "etherscan:build:development",
"port": 5003
},
"production": {
"buildTarget": "etherscan:build:production"
}
}
}
},
"tags": []
}

@ -1,7 +0,0 @@
body {
margin: 0;
}
#root {
padding: 8px 14px;
}

@ -1,25 +0,0 @@
import React from 'react'
import {PluginClient} from '@remixproject/plugin'
import {Receipt, ThemeType} from './types'
export const AppContext = React.createContext({
apiKey: '',
setAPIKey: (value: string) => {
console.log('Set API Key from Context')
},
clientInstance: {} as PluginClient,
receipts: [] as Receipt[],
setReceipts: (receipts: Receipt[]) => {
console.log('Calling Set Receipts')
},
contracts: [] as string[],
setContracts: (contracts: string[]) => {
console.log('Calling Set Contract Names')
},
themeType: 'dark' as ThemeType,
setThemeType: (themeType: ThemeType) => {
console.log('Calling Set Theme Type')
},
networkName: ''
})

@ -1,70 +0,0 @@
import { PluginClient } from '@remixproject/plugin'
import { createClient } from '@remixproject/plugin-webview'
import { verify, EtherScanReturn } from './utils/verify'
import { getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus } from './utils'
import EventManager from 'events'
export class EtherscanPluginClient extends PluginClient {
public internalEvents: EventManager
constructor() {
super()
this.internalEvents = new EventManager()
createClient(this)
this.onload()
}
onActivation(): void {
this.internalEvents.emit('etherscan_activated')
}
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, isProxyContract: boolean) {
try {
const { network, networkId } = await getNetworkName(this)
if (network === 'vm') {
throw new Error('Cannot check the receipt status in the selected network')
}
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
}
} catch (e: any) {
return {
status: 'error',
message: e.message,
succeed: false
}
}
}
}

@ -1,136 +0,0 @@
import React, {useState, useEffect, useRef} from 'react'
import {CompilationFileSources, CompilationResult} from '@remixproject/plugin-api'
import { EtherscanPluginClient } from './EtherscanPluginClient'
import {AppContext} from './AppContext'
import {DisplayRoutes} from './routes'
import {useLocalStorage} from './hooks/useLocalStorage'
import {getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus} from './utils'
import {Receipt, ThemeType} from './types'
import './App.css'
export const getNewContractNames = (compilationResult: CompilationResult) => {
const compiledContracts = compilationResult.contracts
let result: string[] = []
for (const file of Object.keys(compiledContracts)) {
const newContractNames = Object.keys(compiledContracts[file])
result = [...result, ...newContractNames]
}
return result
}
const plugin = new EtherscanPluginClient()
const App = () => {
const [apiKey, setAPIKey] = useLocalStorage('apiKey', '')
const [receipts, setReceipts] = useLocalStorage('receipts', [])
const [contracts, setContracts] = useState<string[]>([])
const [themeType, setThemeType] = useState<ThemeType>('dark')
const [networkName, setNetworkName] = useState('Loading...')
const timer = useRef(null)
const contractsRef = useRef(contracts)
contractsRef.current = contracts
const setListeners = () => {
plugin.on('solidity', 'compilationFinished', (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => {
const newContractsNames = getNewContractNames(data)
const newContractsToSave: string[] = [...contractsRef.current, ...newContractsNames]
const uniqueContracts: string[] = [...new Set(newContractsToSave)]
setContracts(uniqueContracts)
})
plugin.on('blockchain' as any, 'networkStatus', (result) => {
setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : '(Not supported)'}`)
})
// @ts-ignore
plugin.call('blockchain', 'getCurrentNetworkStatus').then((result: any) => setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : '(Not supported)'}`))
}
useEffect(() => {
plugin.onload(() => {
setListeners()
})
}, [])
useEffect(() => {
let receiptsNotVerified: Receipt[] = receipts.filter((item: Receipt) => item.status === 'Pending in queue' || item.status === 'Max rate limit reached')
if (receiptsNotVerified.length > 0) {
if (timer.current) {
clearInterval(timer.current)
timer.current = null
}
timer.current = setInterval(async () => {
const {network, networkId} = await getNetworkName(plugin)
if (!plugin) return
if (network === 'vm') return
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(networkId))
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) {
const res = {
...currentReceipt,
status: status.result
}
if (currentReceipt.isProxyContract) res.message = status.message
return res
}
return currentReceipt
})
}
}
receiptsNotVerified = newReceipts.filter((item: Receipt) => item.status === 'Pending in queue' || item.status === 'Max rate limit reached')
if (timer.current && receiptsNotVerified.length === 0) {
clearInterval(timer.current)
timer.current = null
}
setReceipts(newReceipts)
}, 10000)
}
}, [receipts])
return (
<AppContext.Provider
value={{
apiKey,
setAPIKey,
clientInstance: plugin,
receipts,
setReceipts,
contracts,
setContracts,
themeType,
setThemeType,
networkName
}}
>
{ plugin && <DisplayRoutes /> }
</AppContext.Provider>
)
}
export default App

@ -1,81 +0,0 @@
import React from 'react'
import {NavLink} from 'react-router-dom'
import {CustomTooltip} from '@remix-ui/helper'
import {AppContext} from '../AppContext'
interface Props {
title?: string
from: string
}
interface IconProps {
from: string
}
const HomeIcon = ({from}: IconProps) => {
return (
<NavLink
data-id="home"
to={{
pathname: '/'
}}
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')}
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})}
state={from}
>
<CustomTooltip tooltipText="Home" tooltipId="etherscan-nav-home" placement="bottom">
<i className="fas fa-home"></i>
</CustomTooltip>
</NavLink>
)
}
const ReceiptsIcon = ({from}: IconProps) => {
return (
<NavLink
data-id="receipts"
to={{
pathname: '/receipts'
}}
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')}
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})}
state={from}
>
<CustomTooltip tooltipText="Receipts" tooltipId="etherscan-nav-receipts" placement="bottom">
<i className="fas fa-receipt"></i>
</CustomTooltip>
</NavLink>
)
}
const SettingsIcon = ({from}: IconProps) => {
return (
<NavLink
data-id="settings"
to={{
pathname: '/settings'
}}
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')}
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})}
state={from}
>
<CustomTooltip tooltipText="Settings" tooltipId="etherscan-nav-settings" placement="bottom">
<i className="fas fa-cog"></i>
</CustomTooltip>
</NavLink>
)
}
export const HeaderWithSettings = ({title = '', from}) => {
return (
<div className="d-flex justify-content-between">
<h6 className="d-inline">{title}</h6>
<div className="nav">
<HomeIcon from={from} />
<ReceiptsIcon from={from} />
<SettingsIcon from={from} />
</div>
</div>
)
}

@ -1,34 +0,0 @@
import React from 'react'
import {CustomTooltip} from '@remix-ui/helper'
interface Props {
text: string
isSubmitting?: boolean
dataId?: string
disable?: boolean
}
export const SubmitButton = ({text, dataId, isSubmitting = false, disable = true}) => {
return (
<div>
<button data-id={dataId} type="submit" className="btn btn-primary btn-block p-1 text-decoration-none" disabled={disable}>
<CustomTooltip
tooltipText={disable ? 'Fill in the valid value(s) and select a supported network' : 'Click to proceed'}
tooltipId={'etherscan-submit-button-' + dataId}
tooltipTextClasses="border bg-light text-dark p-1 pr-3"
placement="bottom"
>
<div>
{!isSubmitting && text}
{isSubmitting && (
<div>
<span className="spinner-border spinner-border-sm mr-1" role="status" aria-hidden="true" />
Verifying... Please wait
</div>
)}
</div>
</CustomTooltip>
</button>
</div>
)
}

@ -1,2 +0,0 @@
export { HeaderWithSettings } from "./HeaderWithSettings"
export { SubmitButton } from "./SubmitButton"

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

@ -1 +0,0 @@
export { DefaultLayout } from "./Default"

@ -1,37 +0,0 @@
import React from 'react'
import {HashRouter as Router, Route, Routes, RouteProps} from 'react-router-dom'
import {ErrorView, HomeView, ReceiptsView, CaptureKeyView} from './views'
import {DefaultLayout} from './layouts'
export const DisplayRoutes = () => (
<Router>
<Routes>
<Route
path="/"
element={
<DefaultLayout from="/" title="Verify Smart Contracts">
<HomeView />
</DefaultLayout>
}
/>
<Route path="/error" element={<ErrorView />} />
<Route
path="/receipts"
element={
<DefaultLayout from="/receipts" title="Check Receipt GUID Status">
<ReceiptsView />
</DefaultLayout>
}
/>
<Route
path="/settings"
element={
<DefaultLayout from="/settings" title="Set Explorer API Key">
<CaptureKeyView />
</DefaultLayout>
}
/>
</Routes>
</Router>
)

@ -1,9 +0,0 @@
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
}

@ -1 +0,0 @@
export type ThemeType = "dark" | "light"

@ -1,2 +0,0 @@
export * from "./Receipt"
export * from "./ThemeType"

@ -1 +0,0 @@
export * from "./utilities"

@ -1,46 +0,0 @@
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',
59144: 'https://api.lineascan.build/api',
8453: 'https://api.basescan.org/api',
534352: 'https://api.scrollscan.com/api',
1116: 'https://openapi.coredao.org/api',
// all testnet
17000: 'https://api-holesky.etherscan.io/api',
11155111: 'https://api-sepolia.etherscan.io/api',
97: 'https://api-testnet.bscscan.com/api',
80001: 'https://api-testnet.polygonscan.com/api',
80002: 'https://api-amoy.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',
84532: "https://api-sepolia.basescan.org/api",
1442: 'https://api-testnet-zkevm.polygonscan.com/api',
2442: 'https://api-cardona-zkevm.polygonscan.com/api',
59140: 'https://api-testnet.lineascan.build/api',
534351: 'https://api-sepolia.scrollscan.com/api',
1115: 'https://api.test.btcs.network/api',
}

@ -1,30 +0,0 @@
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} 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, chainRef?: number | string, isProxyContract?: boolean, expectedImplAddress?: string) => {
const compilationResultParam = await remix.call('compilerArtefacts' as any, 'getCompilerAbstract', contractFile)
console.log('verifying.. ' + contractName)
// 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 {boolean} isProxyContract - true, if contract is a proxy contract (optional)
* @returns {{ status, message, succeed }} receiptStatus
*/
export const receiptStatus = async (apikey: string, guid: string, isProxyContract?: boolean) => {
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey, isProxyContract)
}
`

@ -1,69 +0,0 @@
import { PluginClient } from "@remixproject/plugin"
import axios from 'axios'
import { scanAPIurls } from "./networks"
type RemixClient = PluginClient
/*
status: 0=Error, 1=Pass
message: OK, NOTOK
result: explanation
*/
export type receiptStatus = {
result: string
message: string
status: string
}
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) => {
const network = await client.call("network", "detectNetwork")
if (!network) {
throw new Error("no known network to verify against")
}
return { network: network.name!.toLowerCase(), networkId: network.id }
}
export const getReceiptStatus = async (
receiptGuid: string,
apiKey: string,
etherscanApi: string
): Promise<receiptStatus> => {
const params = `guid=${receiptGuid}&module=contract&action=checkverifystatus&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)
}
}
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,206 +0,0 @@
import { getNetworkName, getEtherScanApi, getReceiptStatus, getProxyContractReceiptStatus } from "../utils"
import { CompilationResult } from "@remixproject/plugin-api"
import { CompilerAbstract } from '@remix-project/remix-solidity'
import axios from 'axios'
import { PluginClient } from "@remixproject/plugin"
const resetAfter10Seconds = (client: PluginClient, setResults: (value: string) => void) => {
setTimeout(() => {
client.emit("statusChanged", { key: "none" })
setResults("")
}, 10000)
}
export type EtherScanReturn = {
guid: any,
status: any,
}
export const verify = async (
apiKeyParam: string,
contractAddress: string,
contractArgumentsParam: string,
contractName: string,
compilationResultParam: CompilerAbstract,
chainRef: number | string,
isProxyContract: boolean,
expectedImplAddress: string,
client: PluginClient,
onVerifiedContract: (value: EtherScanReturn) => void,
setResults: (value: string) => void
) => {
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)
}
}
try {
const contractMetadata = getContractMetadata(
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
compilationResultParam.data as unknown as CompilationResult,
contractName
)
if (!contractMetadata) {
return {
succeed: false,
message: "Please recompile contract"
}
}
const contractMetadataParsed = JSON.parse(contractMetadata)
const fileName = getContractFileName(
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
compilationResultParam.data as unknown as CompilationResult,
contractName
)
const jsonInput = {
language: 'Solidity',
sources: compilationResultParam.source.sources,
settings: {
optimizer: {
enabled: contractMetadataParsed.settings.optimizer.enabled,
runs: contractMetadataParsed.settings.optimizer.runs
}
}
}
const data: { [key: string]: string | any } = {
apikey: apiKeyParam, // A valid API-Key is required
module: "contract", // Do not change
action: "verifysourcecode", // Do not change
codeformat: "solidity-standard-json-input",
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]))
client.emit("statusChanged", {
key: "loading",
type: "info",
title: "Verifying ...",
})
const response = await axios.post(etherscanApi, body)
const { message, result, status } = await response.data
if (message === "OK" && status === "1") {
resetAfter10Seconds(client, setResults)
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
)
const returnValue = {
guid: result,
status: receiptStatus.result,
message: `Verification request submitted successfully. Use this receipt GUID ${result} to track the status of your submission`,
succeed: true,
isProxyContract
}
onVerifiedContract(returnValue)
return returnValue
} else if (message === "NOTOK") {
client.emit("statusChanged", {
key: "failed",
type: "error",
title: result,
})
const returnValue = {
message: result,
succeed: false,
isProxyContract
}
resetAfter10Seconds(client, setResults)
return returnValue
}
return {
message: 'unknown reason ' + result,
succeed: false
}
} catch (error: any) {
console.error(error)
setResults("Something wrong happened, try again")
return {
message: error.message,
succeed: false
}
}
}
export const getContractFileName = (
compilationResult: CompilationResult,
contractName: string
) => {
const compiledContracts = compilationResult.contracts
let fileName = ""
for (const file of Object.keys(compiledContracts)) {
for (const contract of Object.keys(compiledContracts[file])) {
if (contract === contractName) {
fileName = file
break
}
}
}
return fileName
}
export const getContractMetadata = (
compilationResult: CompilationResult,
contractName: string
) => {
const compiledContracts = compilationResult.contracts
let contractMetadata = ""
for (const file of Object.keys(compiledContracts)) {
for (const contract of Object.keys(compiledContracts[file])) {
if (contract === contractName) {
contractMetadata = compiledContracts[file][contract].metadata
if (contractMetadata) {
break
}
}
}
}
return contractMetadata
}

@ -1,63 +0,0 @@
import React, {useState, useEffect} from 'react'
import {Formik, ErrorMessage, Field} from 'formik'
import {useNavigate, useLocation} from 'react-router-dom'
import {AppContext} from '../AppContext'
import {SubmitButton} from '../components'
export const CaptureKeyView = () => {
const location = useLocation()
const navigate = useNavigate()
const [msg, setMsg] = useState('')
const context = React.useContext(AppContext)
useEffect(() => {
if (!context.apiKey) setMsg('Please provide a 34 or 32 character API key to continue')
}, [context.apiKey])
return (
<div>
<Formik
initialValues={{apiKey: context.apiKey}}
validate={(values) => {
const errors = {} as any
if (!values.apiKey) {
errors.apiKey = 'Required'
} else if (values.apiKey.length !== 34 && values.apiKey.length !== 32) {
errors.apiKey = 'API key should be 34 or 32 characters long'
}
return errors
}}
onSubmit={(values) => {
const apiKey = values.apiKey
if (apiKey.length === 34 || apiKey.length === 32) {
context.setAPIKey(values.apiKey)
navigate(location && location.state ? location.state : '/')
}
}}
>
{({errors, touched, handleSubmit}) => (
<form onSubmit={handleSubmit}>
<div className="form-group mb-2">
<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="password"
name="apiKey"
placeholder="e.g. GM1T20XY6JGSAPWKDCYZ7B2FJXKTJRFVGZ"
/>
<ErrorMessage className="invalid-feedback" name="apiKey" component="div" />
</div>
<div>
<SubmitButton text="Save" dataId="save-api-key" disable={errors && errors.apiKey ? true : false} />
</div>
</form>
)}
</Formik>
<div data-id="api-key-result" className="text-primary mt-4 text-center" style={{fontSize: '0.8em'}} dangerouslySetInnerHTML={{__html: msg}} />
</div>
)
}

@ -1,16 +0,0 @@
import React from 'react'
export const ErrorView = () => {
return (
<div className="d-flex w-100 flex-column align-items-center">
<img className="pb-4" 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 className="text-danger" href="https://github.com/ethereum/remix-project/issues">
Here
</a>
</h5>
</div>
)
}

@ -1,31 +0,0 @@
import React from 'react'
import {Navigate} from 'react-router-dom'
import {AppContext} from '../AppContext'
import {Receipt} from '../types'
import {VerifyView} from './VerifyView'
export const HomeView = () => {
const context = React.useContext(AppContext)
return !context.apiKey ? (
<Navigate
to={{
pathname: '/settings'
}}
/>
) : (
<VerifyView
contracts={context.contracts}
client={context.clientInstance}
apiKey={context.apiKey}
onVerifiedContract={(receipt: Receipt) => {
const newReceipts = [...context.receipts, receipt]
context.setReceipts(newReceipts)
}}
networkName={context.networkName}
/>
)
}

@ -1,170 +0,0 @@
import React, {useState} from 'react'
import {Formik, ErrorMessage, Field} from 'formik'
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 = () => {
const [results, setResults] = useState({succeed: false, message: ''})
const [isProxyContractReceipt, setIsProxyContractReceipt] = useState(false)
const context = React.useContext(AppContext)
const onGetReceiptStatus = async (values: FormValues, clientInstance: any, apiKey: string) => {
try {
const {network, networkId} = await getNetworkName(clientInstance)
if (network === 'vm') {
setResults({
succeed: false,
message: 'Cannot verify in the selected network'
})
return
}
const etherscanApi = getEtherScanApi(networkId)
let result
if (isProxyContractReceipt) {
result = await getProxyContractReceiptStatus(values.receiptGuid, apiKey, etherscanApi)
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({
succeed: false,
message: error.message
})
}
}
return !context.apiKey ? (
<Navigate
to={{
pathname: '/settings'
}}
/>
) : (
<div>
<Formik
initialValues={{receiptGuid: ''}}
validate={(values) => {
const errors = {} as any
if (!values.receiptGuid) {
errors.receiptGuid = 'Required'
}
return errors
}}
onSubmit={(values) => onGetReceiptStatus(values, context.clientInstance, context.apiKey)}
>
{({errors, touched, handleSubmit, handleChange}) => (
<form onSubmit={handleSubmit}>
<div className="form-group mb-2">
<label htmlFor="receiptGuid">Receipt GUID</label>
<Field
className={errors.receiptGuid && touched.receiptGuid ? 'form-control form-control-sm is-invalid' : 'form-control form-control-sm'}
type="text"
name="receiptGuid"
/>
<ErrorMessage className="invalid-feedback" name="receiptGuid" component="div" />
</div>
<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 dataId={null} text="Check" disable={!touched.receiptGuid || (touched.receiptGuid && errors.receiptGuid) ? true : false} />
</form>
)}
</Formik>
<div
className={results['succeed'] ? 'text-success mt-3 text-center' : 'text-danger mt-3 text-center'}
dangerouslySetInnerHTML={{
__html: results.message ? results.message : ''
}}
/>
<ReceiptsTable receipts={context.receipts} />
<br />
<CustomTooltip tooltipText="Clear the list of receipts" tooltipId="etherscan-clear-receipts" placement="bottom">
<Button
className="btn-sm"
onClick={() => {
context.setReceipts([])
}}
>
Clear
</Button>
</CustomTooltip>
</div>
)
}
const ReceiptsTable = ({receipts}) => {
return (
<div className="table-responsive">
<h6>Receipts</h6>
<table className="table h6 table-sm">
<thead>
<tr>
<th scope="col">Status</th>
<th scope="col">GUID</th>
</tr>
</thead>
<tbody>
{receipts &&
receipts.length > 0 &&
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>
</tr>
)
})}
</tbody>
</table>
</div>
)
}

@ -1,235 +0,0 @@
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 {etherscanScripts} from '@remix-project/remix-ws-templates'
interface Props {
client: PluginClient
apiKey: string
onVerifiedContract: (receipt: Receipt) => void
contracts: string[],
networkName: string
}
interface FormValues {
contractName: string
contractAddress: string
expectedImplAddress?: string
}
export const VerifyView = ({apiKey, client, contracts, onVerifiedContract, networkName}) => {
const [results, setResults] = useState('')
const [selectedContract, setSelectedContract] = useState('')
const [showConstructorArgs, setShowConstructorArgs] = useState(false)
const [isProxyContract, setIsProxyContract] = useState(false)
const [constructorInputs, setConstructorInputs] = useState([])
const verificationResult = useRef({})
useEffect(() => {
if (contracts.includes(selectedContract)) updateConsFields(selectedContract)
}, [contracts])
const updateConsFields = (contractName) => {
client.call('compilerArtefacts' as any, 'getArtefactsByContractName', contractName).then((result) => {
const {artefact} = result
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)
}
})
}
const onVerifyContract = async (values: FormValues) => {
const compilationResult = (await client.call('solidity', 'getCompilationResult')) as any
if (!compilationResult) {
throw new Error('no compilation result available')
}
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', '')
verificationResult.current = await verify(
apiKey,
values.contractAddress,
contractArguments,
values.contractName,
compilationResult,
null,
isProxyContract,
values.expectedImplAddress,
client,
onVerifiedContract,
setResults
)
setResults(verificationResult.current['message'])
}
return (
<div>
<Formik
initialValues={{
contractName: '',
contractAddress: ''
}}
validate={(values) => {
const errors = {} as any
if (!values.contractName) {
errors.contractName = 'Required'
}
if (!values.contractAddress) {
errors.contractAddress = 'Required'
}
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, handleChange, isSubmitting}) => {
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="network">Selected Network</label>
<CustomTooltip
tooltipText="Network is fetched from 'Deploy and Run Transactions' plugin's ENVIRONMENT field"
tooltipId="etherscan-impl-address2"
placement="bottom"
>
<Field className="form-control" type="text" name="network" value={networkName} disabled={true} />
</CustomTooltip>
</div>
<div className="form-group">
<label htmlFor="contractName">Contract Name</label>
<Field
as="select"
className={errors.contractName && touched.contractName && contracts.length ? 'form-control is-invalid' : 'form-control'}
name="contractName"
onChange={async (e) => {
handleChange(e)
setSelectedContract(e.target.value)
updateConsFields(e.target.value)
}}
>
<option disabled={true} value="">
{contracts.length ? 'Select a contract' : `--- No compiled contracts ---`}
</option>
{contracts.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Field>
<ErrorMessage className="invalid-feedback" name="contractName" component="div" />
</div>
<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="form-control m-1" type="text" key={`contractArgName${index}`} name={`contractArgName${index}`} value={item.name} disabled={true} />
<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">
<label htmlFor="contractAddress">Contract Address</label>
<Field
className={errors.contractAddress && touched.contractAddress ? 'form-control is-invalid' : 'form-control'}
type="text"
name="contractAddress"
placeholder="e.g. 0x11b79afc03baf25c631dd70169bb6a3160b2706e"
/>
<ErrorMessage className="invalid-feedback" name="contractAddress" component="div" />
<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 address
</label>
</div>
</div>
<div className={isProxyContract ? 'form-group d-block' : 'form-group d-none'}>
<label htmlFor="expectedImplAddress">Expected Implementation Address</label>
<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="verified implementation contract address" />
</CustomTooltip>
<i style={{fontSize: 'x-small'}} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i>
<label> &nbsp;Make sure contract is already verified on Etherscan</label>
</div>
<SubmitButton
dataId="verify-contract"
text="Verify"
isSubmitting={isSubmitting}
disable={
!contracts.length ||
!touched.contractName ||
!touched.contractAddress ||
(touched.contractName && errors.contractName) ||
(touched.contractAddress && errors.contractAddress) ||
networkName === 'VM (Not supported)'
? 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"
className="mr-2 mb-2 py-1 px-2 btn btn-secondary btn-block"
onClick={async () => {
etherscanScripts({}, client)
}}
>
Generate Verification Scripts
</button>
</CustomTooltip>
</form>
)
}}
</Formik>
<div
data-id="verify-result"
className={verificationResult.current['succeed'] ? 'text-success mt-4 text-center' : 'text-danger mt-4 text-center'}
style={{fontSize: '0.8em'}}
dangerouslySetInnerHTML={{__html: results}}
/>
{/* <div style={{ display: "block", textAlign: "center", marginTop: "1em" }}>
<Link to="/receipts">View Receipts</Link>
</div> */}
</div>
)
}

@ -1,4 +0,0 @@
export { HomeView } from "./HomeView"
export { ErrorView } from "./ErrorView"
export { ReceiptsView } from "./ReceiptsView"
export { CaptureKeyView } from "./CaptureKeyView"

@ -1,14 +0,0 @@
import React from 'react'
import * as ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client';
import App from './app/app'
const container = document.getElementById('root');
if (container) {
createRoot(container).render(
<App />
);
}

@ -1,16 +0,0 @@
{
"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-ide.readthedocs.io/en/latest/contract_verification.html#etherscan",
"maintainedBy": "Remix",
"authorContact": "remix@ethereum.org"
}

@ -1,11 +1,14 @@
import React from 'react'
import {createHashRouter, RouterProvider} from 'react-router-dom'
import {ToastContainer} from 'react-toastify'
import React, { useEffect } from 'react'
import { createHashRouter, RouterProvider } from 'react-router-dom'
import { ToastContainer } from 'react-toastify'
import LoadingScreen from './components/LoadingScreen'
import LogoPage from './pages/Logo'
import HomePage from './pages/Home'
import StepListPage from './pages/StepList'
import StepDetailPage from './pages/StepDetail'
import remixClient from './remix-client'
import { repoMap } from './redux/models/workshop'
import { useAppDispatch } from './redux/hooks'
import 'react-toastify/dist/ReactToastify.css'
import './App.css'
@ -29,6 +32,35 @@ export const router = createHashRouter([
])
function App(): JSX.Element {
const dispatch = useAppDispatch()
const loadRepo = (locale: any) => {
dispatch({
type: 'remixide/save',
payload: { localeCode: locale.code },
})
dispatch({
type: 'workshop/loadRepo',
payload: repoMap[locale.code] || repoMap.en,
})
}
useEffect(() => {
dispatch({
type: 'remixide/connect',
callback: () => {
// @ts-ignore
remixClient.call('locale', 'currentLocale').then((locale: any) => {
loadRepo(locale)
})
// @ts-ignore
remixClient.on('locale', 'localeChanged', (locale: any) => {
loadRepo(locale)
})
}
})
}, [])
return (
<>
<RouterProvider router={router} />

@ -1,7 +1,7 @@
import React from 'react'
import BounceLoader from 'react-spinners/BounceLoader'
import './index.css'
import {useAppSelector} from '../../redux/hooks'
import { useAppSelector } from '../../redux/hooks'
const LoadingScreen: React.FC = () => {
const loading = useAppSelector((state) => state.loading.screen)

@ -1,13 +1,14 @@
import React, {useState, useEffect} from 'react'
import {Button, Dropdown, Form, Tooltip, OverlayTrigger} from 'react-bootstrap'
import {useAppDispatch} from '../../redux/hooks'
import React, { useState, useEffect } from 'react'
import { Button, Dropdown, Form, Tooltip, OverlayTrigger } from 'react-bootstrap'
import { useAppDispatch, useAppSelector } from '../../redux/hooks'
import './index.css'
function RepoImporter({list, selectedRepo}: any): JSX.Element {
function RepoImporter({ list, selectedRepo }: any): JSX.Element {
const [open, setOpen] = useState(false)
const [name, setName] = useState('')
const [branch, setBranch] = useState('')
const dispatch = useAppDispatch()
const localeCode = useAppSelector((state) => state.remixide.localeCode)
useEffect(() => {
setName(selectedRepo.name)
@ -19,18 +20,18 @@ function RepoImporter({list, selectedRepo}: any): JSX.Element {
}
const selectRepo = (repo: {name: string; branch: string}) => {
dispatch({type: 'workshop/loadRepo', payload: repo});
dispatch({ type: 'workshop/loadRepo', payload: repo });
(window as any)._paq.push(['trackEvent', 'learneth', 'select_repo', `${name}/${branch}`])
}
const importRepo = (event: {preventDefault: () => void}) => {
event.preventDefault()
dispatch({type: 'workshop/loadRepo', payload: {name, branch}});
dispatch({ type: 'workshop/loadRepo', payload: { name, branch } });
(window as any)._paq.push(['trackEvent', 'learneth', 'import_repo', `${name}/${branch}`])
}
const resetAll = () => {
dispatch({type: 'workshop/resetAll'})
dispatch({ type: 'workshop/resetAll', payload: { code: localeCode } })
setName('')
setBranch('')
}
@ -45,7 +46,7 @@ function RepoImporter({list, selectedRepo}: any): JSX.Element {
</div>
)}
<div onClick={panelChange} style={{cursor: 'pointer'}} className="container-fluid d-flex mb-3 small">
<div onClick={panelChange} style={{ cursor: 'pointer' }} className="container-fluid d-flex mb-3 small">
<div className="d-flex pr-2 pl-2">
<i className={`arrow-icon pt-1 fas fa-xs ${open ? 'fa-chevron-down' : 'fa-chevron-right'}`}></i>
</div>
@ -71,7 +72,7 @@ function RepoImporter({list, selectedRepo}: any): JSX.Element {
))}
</Dropdown.Menu>
</Dropdown>
<div onClick={resetAll} className="small mb-3" style={{cursor: 'pointer'}}>
<div onClick={resetAll} className="small mb-3" style={{ cursor: 'pointer' }}>
reset list
</div>
</div>
@ -109,7 +110,7 @@ function RepoImporter({list, selectedRepo}: any): JSX.Element {
Import {name}
</Button>
<a href="https://github.com/bunsenstraat/remix-learneth-plugin/blob/master/README.md" className="d-none" target="_blank" rel="noreferrer">
<i className="fas fa-info-circle" /> how to setup your repo
<i className="fas fa-info" /> How to setup your repo
</a>
</Form>
)}

@ -1,9 +1,9 @@
import React, {useEffect} from 'react'
import {Link} from 'react-router-dom'
import React from 'react'
import { Link } from 'react-router-dom'
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import {useAppDispatch, useAppSelector} from '../../redux/hooks'
import { useAppSelector } from '../../redux/hooks'
import RepoImporter from '../../components/RepoImporter'
import './index.css'
@ -15,8 +15,7 @@ function HomePage(): JSX.Element {
setOpenKeys(isOpen(key) ? openKeys.filter((item) => item !== key) : [...openKeys, key])
}
const dispatch = useAppDispatch()
const {list, detail, selectedId} = useAppSelector((state) => state.workshop)
const { list, detail, selectedId } = useAppSelector((state) => state.workshop)
const selectedRepo = detail[selectedId]
@ -26,12 +25,6 @@ function HomePage(): JSX.Element {
3: 'Advanced',
}
useEffect(() => {
dispatch({
type: 'workshop/init',
})
}, [])
return (
<div className="App">
<RepoImporter list={list} selectedRepo={selectedRepo || {}} />

@ -1,13 +1,6 @@
import React, {useEffect} from 'react'
import {useAppDispatch} from '../../redux/hooks'
import React from 'react'
const LogoPage: React.FC = () => {
const dispatch = useAppDispatch()
useEffect(() => {
dispatch({type: 'remixide/connect'})
}, [])
return (
<div>
<div>

@ -15,6 +15,7 @@ const Model: ModelType = {
success: false,
errorLoadingFile: false,
// theme: '',
localeCode: 'en'
},
reducers: {
save(state, { payload }) {

@ -3,16 +3,30 @@ import { toast } from 'react-toastify'
import groupBy from 'lodash/groupBy'
import pick from 'lodash/pick'
import { type ModelType } from '../store'
import remixClient from '../../remix-client'
import { router } from '../../App'
// const apiUrl = 'http://localhost:3001';
const apiUrl = 'https://static.220.14.12.49.clients.your-server.de:3000'
export const repoMap = {
en: {
name: 'ethereum/remix-workshops',
branch: 'master',
},
zh: {
name: 'ethereum/remix-workshops',
branch: 'zh',
},
es: {
name: 'ethereum/remix-workshops',
branch: 'es',
},
}
const Model: ModelType = {
namespace: 'workshop',
state: {
list: [],
list: Object.keys(repoMap).map(item => repoMap[item]),
detail: {},
selectedId: '',
},
@ -22,26 +36,9 @@ const Model: ModelType = {
},
},
effects: {
*init(_, { put }) {
const cache = null // don't use cache because remote might change
if (cache) {
const workshopState = JSON.parse(cache)
yield put({
type: 'workshop/save',
payload: workshopState,
})
} else {
yield put({
type: 'workshop/loadRepo',
payload: {
name: 'ethereum/remix-workshops',
branch: 'master',
},
})
}
},
*loadRepo({ payload }, { put, select }) {
yield router.navigate('/home')
toast.info(`loading ${payload.name}/${payload.branch}`)
yield put({
@ -111,14 +108,13 @@ const Model: ModelType = {
...payload,
},
},
list: detail[repoId] ? list : [...list, payload],
list: list.map(item => `${item.name}/${item.branch}`).includes(`${payload.name}/${payload.branch}`) ? list : [...list, payload],
selectedId: repoId,
}
yield put({
type: 'workshop/save',
payload: workshopState,
})
localStorage.setItem('workshop.state', JSON.stringify(workshopState))
toast.dismiss()
yield put({
@ -141,20 +137,19 @@ const Model: ModelType = {
}
(<any>window)._paq.push(['trackEvent', 'learneth', 'load_repo', payload.name])
},
*resetAll(_, { put }) {
*resetAll({ payload }, { put }) {
yield put({
type: 'workshop/save',
payload: {
list: [],
list: Object.keys(repoMap).map(item => repoMap[item]),
detail: {},
selectedId: '',
},
})
localStorage.removeItem('workshop.state')
yield put({
type: 'workshop/init',
type: 'workshop/loadRepo',
payload: repoMap[payload.code]
});
(<any>window)._paq.push(['trackEvent', 'learneth', 'reset_all'])
},

@ -46,19 +46,20 @@ function watchEffects(model: ModelType): ForkEffect {
return fork(function* () {
for (const key in model.effects) {
const effect = model.effects[key]
yield takeEvery(`${model.namespace}/${key}`, function* (action: PayloadAction) {
yield takeEvery(`${model.namespace}/${key}`, function* ({ callback, ...action }: {type: string; payload: any; callback?: any}) {
yield put({
type: 'loading/save',
payload: {
[`${model.namespace}/${key}`]: true,
},
})
yield effect(action, {
const result = yield effect(action, {
call,
put,
delay,
select,
})
callback && callback(result)
yield put({
type: 'loading/save',
payload: {
@ -82,7 +83,13 @@ const configureAppStore = (initialState = {}) => {
const store = configureStore({
reducer: rootReducer,
middleware: (gDM) => gDM().concat([...middleware]),
middleware: (gDM) =>
gDM({
serializableCheck: {
// Ignore these field paths in all actions
ignoredActionPaths: ['callback'],
},
}).concat([...middleware]),
preloadedState: initialState,
devTools: process.env.NODE_ENV !== 'production',
})

@ -10,8 +10,8 @@ const { encodeFunctionId } = execution.txHelper;
const surgeClient = new SurgeClient({
// surge backend doesn't support cross-domain, that's why the proxy goes
// here is the codebase of proxy: https://github.com/drafish/vercel-proxy
proxy: 'https://vercel-proxy-bice-six.vercel.app',
// here is the codebase of proxy: https://github.com/remix-project-org/remix-wildcard/blob/master/src/hosts/common-corsproxy.ts
proxy: 'https://common-corsproxy.remixproject.org/',
onError: (err: Error) => {
console.log(err);
},
@ -160,19 +160,10 @@ export const deploy = async (payload: any, callback: any) => {
const { data } = await axios.get(
// It's the json file contains all the static files paths of dapp-template.
// It's generated through the build process automatically.
'https://dev.remix-dapp.pages.dev/manifest.json'
`${window.origin}/plugins/remix-dapp/manifest.json`
);
let paths: any = [];
Object.keys(data).forEach((key) => {
if (data[key].src === 'index.html') {
const { src, file, css, assets } = data[key];
paths = paths.concat([src, file, ...css, ...assets]);
} else {
paths.push(data[key].file);
}
});
const paths = Object.keys(data);
const { logo, ...instance } = state.instance;
@ -183,7 +174,7 @@ export const deploy = async (payload: any, callback: any) => {
})
const files: Record<string, string> = {
'dir/instance.json': instanceJson,
'dir/assets/instance.json': instanceJson,
};
// console.log(
@ -197,11 +188,11 @@ export const deploy = async (payload: any, callback: any) => {
const path = paths[index];
// download all the static files from the dapp-template domain.
// here is the codebase of dapp-template: https://github.com/drafish/remix-dapp
const resp = await axios.get(`https://dev.remix-dapp.pages.dev/${path}`);
const resp = await axios.get(`${window.origin}/plugins/remix-dapp/${path}`);
files[`dir/${path}`] = resp.data;
}
files['dir/logo.png'] = logo
files['dir/assets/logo.png'] = logo
files['dir/CORS'] = '*'
files['dir/index.html'] = files['dir/index.html'].replace(
'assets/css/themes/remix-dark_tvx1s2.css',
@ -236,7 +227,7 @@ export const deploy = async (payload: any, callback: any) => {
try {
// some times deployment might fail even if it says successfully, that's why we need to do the double check.
const instanceResp = await axios.get(`https://${payload.subdomain}.surge.sh/instance.json`);
const instanceResp = await axios.get(`https://${payload.subdomain}.surge.sh/assets/instance.json`);
if (instanceResp.status === 200 && JSON.stringify(instanceResp.data) === instanceJson) {
callback({ code: 'SUCCESS', error: '' });
return;

@ -0,0 +1,9 @@
{
"name": "remix-dapp",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"webpack-manifest-plugin": "^5.0.0"
}
}

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

Loading…
Cancel
Save