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: paths:
- "persist" - "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: build-remixdesktop-linux:
machine: machine:
image: ubuntu-2004:current image: ubuntu-2004:current
@ -119,22 +164,37 @@ jobs:
- run: unzip ./persist/desktopbuild.zip - run: unzip ./persist/desktopbuild.zip
- run: - run:
command: | command: |
nvm install 20.2
nvm use 20.2
node -v node -v
npm install --global yarn node-gyp
python -m pip install --upgrade pip
pip install setuptools
mkdir apps/remixdesktop/build mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/ cd apps/remixdesktop/
yarn add node-pty yarn add node-pty
yarn --ignore-optional yarn --ignore-optional
yarn add @remix-project/remix-ws-templates yarn add @remix-project/remix-ws-templates
PUBLISH_FOR_PULL_REQUEST='true' yarn dist ./rundist.bash
rm -rf release/*-unpacked rm -rf release/*-unpacked
- save_cache: - save_cache:
key: remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }} key: remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths: paths:
- apps/remixdesktop/node_modules - 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: - store_artifacts:
path: apps/remixdesktop/release/ path: apps/remixdesktop/release/
destination: remixdesktop-linux destination: remixdesktop-linux
- persist_to_workspace:
root: apps/remixdesktop
paths:
- "release"
build-remixdesktop-windows: build-remixdesktop-windows:
executor: executor:
@ -147,39 +207,107 @@ jobs:
- attach_workspace: - attach_workspace:
at: . at: .
- run: unzip ./persist/desktopbuild.zip - run: unzip ./persist/desktopbuild.zip
- restore_cache:
key: node-20-windows-v3
- run: - run:
command: | command: |
nvm install 20.0.0 nvm install 20.2
nvm use 20.0.0 nvm use 20.2
node -v node -v
npx -v npx -v
npm install --global yarn npm install --global yarn
npm install --global node-gyp
yarn -v 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: - run:
command: | command: |
mkdir apps/remixdesktop/build mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/ cd apps/remixdesktop/
python -m pip install --upgrade pip
pip install setuptools
yarn yarn
PUBLISH_FOR_PULL_REQUEST='true' yarn dist ./rundist.bash
rm -rf release/*-unpacked rm -rf release/*-unpacked
- save_cache:
key: remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- persist_to_workspace: - persist_to_workspace:
root: apps/remixdesktop root: apps/remixdesktop
paths: paths:
- "release" - "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 # see https://docs.digicert.com/en/software-trust-manager/ci-cd-integrations/script-integrations/github-integration-ksp.html
sign-remixdesktop-windows: sign-remixdesktop-windows:
executor: win/default # executor type executor: win/default # executor type
@ -230,28 +358,66 @@ jobs:
command: | command: |
Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit' -Filter signtool.exe -Recurse Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit' -Filter signtool.exe -Recurse
- run: - run:
name: "Signtool-Signing" name: read env
shell: powershell.exe shell: powershell.exe
command: | 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: - run:
name: "Signtool-Verification" name: "Signtool-Verification"
shell: powershell.exe shell: powershell.exe
command: | command: |
$verify_output = $(& $env:Signtool verify /v /pa $env:RemixSetupExe) . .\SetEnvVars.ps1
$verify_output = $(& $env:Signtool verify /v /pa $PACKAGE_VERSION)
echo ${verify_output} echo ${verify_output}
if (!$verify_output.Contains("Number of files successfully Verified: 1")) { if (!$verify_output.Contains("Number of files successfully Verified: 1")) {
echo 'Verification failed' echo 'Verification failed'
exit 1 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: - store_artifacts:
path: ~/remix-project/release/ path: ~/remix-project/release/
destination: remixdesktop-windows destination: remixdesktop-windows
- persist_to_workspace:
root: ~/remix-project/
paths:
- "release"
environment: environment:
SM_CLIENT_CERT_FILE: 'C:\Certificate_pkcs12.p12' SM_CLIENT_CERT_FILE: 'C:\Certificate_pkcs12.p12'
Signtool: 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe' Signtool: 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe'
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools' SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
RemixSetupExe: 'C:\Users\circleci\remix-project\release\Remix IDE.exe'
build-remixdesktop-mac: build-remixdesktop-mac:
macos: macos:
@ -259,23 +425,41 @@ jobs:
resource_class: resource_class:
macos.m1.large.gen1 macos.m1.large.gen1
working_directory: ~/remix-project working_directory: ~/remix-project
parameters:
arch:
type: string
steps: steps:
- checkout - checkout
- attach_workspace: - attach_workspace:
at: . 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: unzip ./persist/desktopbuild.zip
- run: - run:
command: | command: |
ls -la dist/apps/remix-ide ls -la dist/apps/remix-ide
nvm install 20.0.0 nvm install 20.2
nvm use 20.0.0 nvm use 20.2
- restore_cache: - restore_cache:
keys: keys:
- remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }} - remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run: - run:
command: | command: |
nvm use 20.0.0 nvm use 20.2
cd apps/remixdesktop && yarn cd apps/remixdesktop
yarn || yarn
find ./node_modules
yarn add @remix-project/remix-ws-templates yarn add @remix-project/remix-ws-templates
- save_cache: - save_cache:
key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }} 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 # use USE_HARD_LINK=false https://github.com/electron-userland/electron-builder/issues/3179
- run: - run:
command: | command: |
nvm use 20.0.0 nvm use 20.2
mkdir apps/remixdesktop/build mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop cd apps/remixdesktop
yarn yarn
yarn installRipGrepMacOXarm64 - run:
PUBLISH_FOR_PULL_REQUEST='true' USE_HARD_LINKS=false yarn dist --mac --arm64 command: |
yarn installRipGrepMacOXx64 nvm use 20.2
PUBLISH_FOR_PULL_REQUEST='true' USE_HARD_LINKS=false yarn dist --mac --x64 cd apps/remixdesktop
rm -rf release/mac* 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: - store_artifacts:
path: apps/remixdesktop/release/ path: apps/remixdesktop/release/
destination: remixdesktop-mac 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: lint:
docker: docker:
- image: cimg/node:20.0.0-browsers - image: cimg/node:20.0.0-browsers
@ -504,10 +788,16 @@ workflows:
- build-desktop: - build-desktop:
filters: filters:
branches: branches:
only: [/.*desktop.*/] only: [/.*desktop.*/, 'remix_beta']
- build-remixdesktop-mac: - build-remixdesktop-mac:
requires: requires:
- build-desktop - build-desktop
matrix:
parameters:
arch: ["arm64", "x64"]
- test-remixdesktop-mac:
requires:
- build-desktop
- build-remixdesktop-windows: - build-remixdesktop-windows:
requires: requires:
- build-desktop - build-desktop
@ -517,6 +807,23 @@ workflows:
- build-remixdesktop-linux: - build-remixdesktop-linux:
requires: requires:
- build-desktop - 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: - build-plugin:
matrix: matrix:
parameters: parameters:

@ -14,4 +14,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} 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
*_group*.ts *_group*.ts
stats.json stats.json
release
# compiled output # compiled output
@ -61,9 +62,10 @@ testem.log
apps/remixdesktop/.webpack apps/remixdesktop/.webpack
apps/remixdesktop/out apps/remixdesktop/out
apps/remixdesktop/release/ apps/remixdesktop/release/
apps/remixdesktop/build*/
apps/remix-ide/src/assets/list.json apps/remix-ide/src/assets/list.json
apps/remix-ide/src/assets/esbuild.wasm apps/remix-ide/src/assets/esbuild.wasm
apps/remixdesktop/build* apps/remixdesktop/build*
apps/remixdesktop/reports/ apps/remixdesktop/reports
apps/remixdesktop/logs/ apps/remixdesktop/logs/
logs logs

@ -0,0 +1 @@
v20

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

@ -27,7 +27,7 @@ Then you can replace the string with a intl component. The `id` prop will be the
+ <FormattedMessage id="home.learn" /> + <FormattedMessage id="home.learn" />
</label> </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 ```jsx
<input <input
ref={searchInputRef} ref={searchInputRef}
@ -77,7 +77,7 @@ const locales = [
] ]
``` ```
You can find the language's `code, name, localeName` in this link 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`? ### 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. 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} /> <FormattedMessage id={plugin?.profile.name + '.displayName'} defaultMessage={plugin?.profile.displayName || plugin?.profile.name} />
</h6> </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? ### Should I update the non-english locale json files?
You probably will have this question when you are updating the 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) [![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) [![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) [![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) [![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) [![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` 2. Install dependencies: `yarn install` or simply run `yarn`
3. Build Remix libraries: `yarn run build:libs` 3. Build Remix libraries: `yarn run build:libs`
4. Build Remix project: `yarn build` 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. 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. explain why the error occurred and how to fix it.
` `
// @ts-ignore // @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message) await circuitApp.plugin.call('remixAI', 'error_explaining', message)
} else { } else {
const message = ` const message = `
error message: ${error} error message: ${error}
@ -82,7 +82,7 @@ export function Container () {
explain why the error occurred and how to fix it. explain why the error occurred and how to fix it.
` `
// @ts-ignore // @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message) await circuitApp.plugin.call('remixAI', 'error_explaining', message)
} }
} else { } else {
const error = report.message const error = report.message
@ -92,7 +92,7 @@ export function Container () {
explain why the error occurred and how to fix it. explain why the error occurred and how to fix it.
` `
// @ts-ignore // @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", "presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }]],
{"runtime": "automatic"}
]],
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"], "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"],
"ignore": [ "ignore": ["**/node_modules/**"]
"**/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 // State to store our value
// Pass initial state function to useState so logic is only executed once // Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => { const [storedValue, setStoredValue] = useState<T>(() => {
try { try {
// Get from local storage by key // Get from local storage by key
const item = window.localStorage.getItem(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 ... // Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage. // ... persists the new value to localStorage.
const setValue = (value: any) => { const setValue = (value: SetStateAction<T>) => {
try { try {
// Allow value to be a function so we have same API as useState // Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value 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'

@ -1,3 +1,3 @@
export const environment = { export const environment = {
production: true production: true,
}; }

@ -2,5 +2,5 @@
// When building for production, this file is replaced with `environment.prod.ts`. // When building for production, this file is replaced with `environment.prod.ts`.
export const environment = { 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"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Etherscan</title> <title>Contract Verification</title>
<base href="./" /> <base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" <link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" />
crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
</head> </head>
<body> <body>
<div id="root"></div> <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 * See: https://github.com/zloirock/core-js#babel
*/ */
import 'core-js/stable'; import 'core-js/stable'
import 'regenerator-runtime/runtime'; 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 webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const versionData = { const versionData = {
timestamp: Date.now(), timestamp: Date.now(),
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
} }
// Nx plugins for webpack. // Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => { module.exports = composePlugins(withNx(), (config) => {
@ -31,28 +31,28 @@ module.exports = composePlugins(withNx(), (config) => {
readline: false, readline: false,
child_process: false, child_process: false,
buffer: require.resolve('buffer/'), buffer: require.resolve('buffer/'),
vm: require.resolve('vm-browserify') vm: require.resolve('vm-browserify'),
} }
// add externals // add externals
config.externals = { config.externals = {
...config.externals, ...config.externals,
solc: 'solc' solc: 'solc',
} }
// add public path // add public path
config.output.publicPath = '/' config.output.publicPath = '/'
// set filename // set filename
config.output.filename = `[name].plugin-etherscan.${versionData.timestamp}.js` config.output.filename = `[name].plugin-contract-verification.${versionData.timestamp}.js`
config.output.chunkFilename = `[name].plugin-etherscan.${versionData.timestamp}.js` config.output.chunkFilename = `[name].plugin-contract-verification.${versionData.timestamp}.js`
// add copy & provide plugin // add copy & provide plugin
config.plugins.push( config.plugins.push(
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'], Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'], url: ['url', 'URL'],
process: 'process/browser' process: 'process/browser',
}) })
) )
@ -60,7 +60,7 @@ module.exports = composePlugins(withNx(), (config) => {
config.module.rules.push({ config.module.rules.push({
test: /\.js$/, test: /\.js$/,
use: ['source-map-loader'], use: ['source-map-loader'],
enforce: 'pre' enforce: 'pre',
}) })
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
@ -74,16 +74,16 @@ module.exports = composePlugins(withNx(), (config) => {
compress: false, compress: false,
mangle: false, mangle: false,
format: { format: {
comments: false comments: false,
} },
}, },
extractComments: false extractComments: false,
}), }),
new CssMinimizerPlugin() new CssMinimizerPlugin(),
] ]
config.watchOptions = { config.watchOptions = {
ignored: /node_modules/ ignored: /node_modules/,
} }
return config 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 React, { useEffect } from 'react'
import {createHashRouter, RouterProvider} from 'react-router-dom' import { createHashRouter, RouterProvider } from 'react-router-dom'
import {ToastContainer} from 'react-toastify' import { ToastContainer } from 'react-toastify'
import LoadingScreen from './components/LoadingScreen' import LoadingScreen from './components/LoadingScreen'
import LogoPage from './pages/Logo' import LogoPage from './pages/Logo'
import HomePage from './pages/Home' import HomePage from './pages/Home'
import StepListPage from './pages/StepList' import StepListPage from './pages/StepList'
import StepDetailPage from './pages/StepDetail' 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 'react-toastify/dist/ReactToastify.css'
import './App.css' import './App.css'
@ -29,6 +32,35 @@ export const router = createHashRouter([
]) ])
function App(): JSX.Element { 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 ( return (
<> <>
<RouterProvider router={router} /> <RouterProvider router={router} />

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

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

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

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

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

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

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

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