Merge branch 'master' into remixai__chat

pull/5241/head
STetsing 1 month ago committed by GitHub
commit d2d68ce494
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .nvmrc
  2. 4
      .prettierrc.json
  3. 5
      apps/contract-verification/.babelrc
  4. 16
      apps/contract-verification/.browserslistrc
  5. 3
      apps/contract-verification/.eslintrc
  6. 34
      apps/contract-verification/.eslintrc.json
  7. 69
      apps/contract-verification/project.json
  8. 10
      apps/contract-verification/src/app/App.css
  9. 34
      apps/contract-verification/src/app/AppContext.tsx
  10. 18
      apps/contract-verification/src/app/ContractVerificationPluginClient.ts
  11. 16
      apps/contract-verification/src/app/Verifiers/AbstractVerifier.ts
  12. 50
      apps/contract-verification/src/app/Verifiers/BlockscoutVerifier.ts
  13. 289
      apps/contract-verification/src/app/Verifiers/EtherscanVerifier.ts
  14. 169
      apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts
  15. 30
      apps/contract-verification/src/app/Verifiers/index.ts
  16. 46
      apps/contract-verification/src/app/VerifyFormContext.tsx
  17. 154
      apps/contract-verification/src/app/app.tsx
  18. 105
      apps/contract-verification/src/app/components/AccordionReceipt.tsx
  19. 69
      apps/contract-verification/src/app/components/ConfigInput.tsx
  20. 135
      apps/contract-verification/src/app/components/ConstructorArguments.tsx
  21. 33
      apps/contract-verification/src/app/components/ContractAddressInput.tsx
  22. 3
      apps/contract-verification/src/app/components/ContractDropdown.css
  23. 68
      apps/contract-verification/src/app/components/ContractDropdown.tsx
  24. 31
      apps/contract-verification/src/app/components/NavMenu.tsx
  25. 113
      apps/contract-verification/src/app/components/SearchableChainDropdown.tsx
  26. 5
      apps/contract-verification/src/app/components/index.tsx
  27. 37
      apps/contract-verification/src/app/hooks/useLocalStorage.tsx
  28. 33
      apps/contract-verification/src/app/hooks/useSourcifySupported.tsx
  29. 25
      apps/contract-verification/src/app/layouts/Default.tsx
  30. 1
      apps/contract-verification/src/app/layouts/index.ts
  31. 49
      apps/contract-verification/src/app/routes.tsx
  32. 19
      apps/contract-verification/src/app/types/SettingsTypes.ts
  33. 1
      apps/contract-verification/src/app/types/ThemeType.ts
  34. 80
      apps/contract-verification/src/app/types/VerificationTypes.ts
  35. 3
      apps/contract-verification/src/app/types/index.ts
  36. 576
      apps/contract-verification/src/app/utils/default-apis.json
  37. 28
      apps/contract-verification/src/app/utils/default-settings.ts
  38. 1
      apps/contract-verification/src/app/utils/index.ts
  39. 156
      apps/contract-verification/src/app/views/LookupView.tsx
  40. 16
      apps/contract-verification/src/app/views/ReceiptsView.tsx
  41. 54
      apps/contract-verification/src/app/views/SettingsView.tsx
  42. 236
      apps/contract-verification/src/app/views/VerifyView.tsx
  43. 4
      apps/contract-verification/src/app/views/index.ts
  44. 0
      apps/contract-verification/src/assets/.gitkeep
  45. 3
      apps/contract-verification/src/environments/environment.prod.ts
  46. 6
      apps/contract-verification/src/environments/environment.ts
  47. BIN
      apps/contract-verification/src/favicon.ico
  48. 16
      apps/contract-verification/src/index.html
  49. 10
      apps/contract-verification/src/main.tsx
  50. 7
      apps/contract-verification/src/polyfills.ts
  51. 16
      apps/contract-verification/src/profile.json
  52. 1
      apps/contract-verification/src/styles.css
  53. 22
      apps/contract-verification/tsconfig.app.json
  54. 18
      apps/contract-verification/tsconfig.json
  55. 90
      apps/contract-verification/webpack.config.js
  56. 38
      apps/learneth/src/App.tsx
  57. 2
      apps/learneth/src/components/LoadingScreen/index.tsx
  58. 19
      apps/learneth/src/components/RepoImporter/index.tsx
  59. 15
      apps/learneth/src/pages/Home/index.tsx
  60. 9
      apps/learneth/src/pages/Logo/index.tsx
  61. 1
      apps/learneth/src/redux/models/remixide.ts
  62. 51
      apps/learneth/src/redux/models/workshop.ts
  63. 13
      apps/learneth/src/redux/store.ts
  64. 30
      apps/remix-ide-e2e/src/tests/runAndDeploy_injected.test.ts
  65. 19
      apps/remix-ide-e2e/src/tests/signingMessage.test.ts
  66. 20
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  67. 2
      apps/remix-ide/project.json
  68. 2
      apps/remix-ide/src/app/files/fileManager.ts
  69. 2
      apps/remix-ide/src/app/providers/abstract-provider.tsx
  70. 4
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  71. 9
      apps/remix-ide/src/app/tabs/locales/en/udapp.json
  72. 4
      apps/remix-ide/src/app/tabs/runTab/model/recorder.js
  73. 56
      apps/remix-ide/src/app/udapp/run-tab.tsx
  74. 1135
      apps/remix-ide/src/assets/list.json
  75. 6
      apps/remix-ide/src/blockchain/providers/injected.ts
  76. 21
      apps/remix-ide/src/blockchain/providers/vm.ts
  77. 31
      apps/remix-ide/src/blockchain/providers/worker-vm.ts
  78. 28
      apps/remix-ide/src/remixAppManager.js
  79. 102
      libs/remix-core-plugin/src/lib/compiler-artefacts.ts
  80. 2
      libs/remix-lib/src/execution/txExecution.ts
  81. 3
      libs/remix-lib/src/execution/txRunnerWeb3.ts
  82. 6
      libs/remix-tests/src/runTestFiles.ts
  83. 6
      libs/remix-tests/src/runTestSources.ts
  84. 2
      libs/remix-ui/home-tab/src/lib/components/homeTablangOptions.tsx
  85. 3
      libs/remix-ui/panel/src/lib/plugins/panel-header.tsx
  86. 5
      libs/remix-ui/run-tab/src/lib/actions/account.ts
  87. 3
      libs/remix-ui/run-tab/src/lib/actions/index.ts
  88. 51
      libs/remix-ui/run-tab/src/lib/components/account.tsx
  89. 1
      libs/remix-ui/run-tab/src/lib/components/settingsUI.tsx
  90. 5
      libs/remix-ui/run-tab/src/lib/css/run-tab.css
  91. 4
      libs/remix-ui/run-tab/src/lib/run-tab.tsx
  92. 8
      libs/remix-ui/run-tab/src/lib/types/blockchain.d.ts
  93. 1
      libs/remix-ui/run-tab/src/lib/types/execution-context.d.ts
  94. 4
      libs/remix-ui/run-tab/src/lib/types/index.ts
  95. 21
      libs/remix-ui/run-tab/src/lib/types/run-tab.d.ts
  96. 29
      libs/remix-ui/workspace/src/lib/actions/index.tsx
  97. 5
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  98. 14
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  99. 1
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  100. 22
      libs/remix-ui/workspace/src/lib/components/workspace-hamburger.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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
} }

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

@ -0,0 +1,16 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

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

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

@ -0,0 +1,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,10 @@
html, body, #root {
height: 100%;
}
body {
margin: 0;
}
.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,68 @@
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' : ''} ${!hasContracts ? 'text-muted' : ''}`} 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,31 @@
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 flex-column justify-content-center py-2 px-1 small ' + (isActive ? 'bg-light' : 'bg-transparent')}>
<span>
<span>{icon}</span>
<span className="ml-2">{title}</span>
</span>
</NavLink>
)
}
export const NavMenu = () => {
return (
<nav className="d-flex flex-row justify-start w-100">
<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" />
<div className="flex-grow-1"></div>
</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'

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

@ -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" 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,156 @@
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 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)
} 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 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,236 @@
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 } = 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 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 })
}
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} />
<ContractDropdown label="Contract Name" id="contract-dropdown-1" selectedContract={selectedContract} setSelectedContract={setSelectedContract} />
{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>
<button type="submit" className="btn btn-primary mt-3" disabled={submitDisabled}>
Verify
</button>
</form>
)
}

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

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

@ -0,0 +1,6 @@
// This file can be replaced during build by using the `fileReplacements` array.
// When building for production, this file is replaced with `environment.prod.ts`.
export const environment = {
production: false,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

@ -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 />)
}

@ -0,0 +1,7 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable'
import 'regenerator-runtime/runtime'

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

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

@ -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"
}
]
}

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

@ -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>

@ -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',
}) })

@ -238,6 +238,36 @@ const tests = {
browser browser
.executeScriptInTerminal('web3.eth.getAccounts()') .executeScriptInTerminal('web3.eth.getAccounts()')
.journalLastChildIncludes('["0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f"]') .journalLastChildIncludes('["0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f"]')
},
'Test EIP 712 Signature with Injected Provider (Metamask) #group1 #flaky': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('i[id="remixRunSignMsg"]')
.click('i[id="remixRunSignMsg"]')
.waitForElementVisible('*[data-id="signMessageTextarea"]', 120000)
.click('*[data-id="sign-eip-712"]')
.waitForElementVisible('*[data-id="udappNotify-modal-footer-ok-react"]')
.modalFooterOKClick('udappNotify')
.pause(1000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('"primaryType": "AuthRequest",') !== -1, 'EIP 712 data file must be opened')
})
.clickLaunchIcon('filePanel')
.rightClick('li[data-id="treeViewLitreeViewItemEIP-712-data.json"]')
.click('*[data-id="contextMenuItemsignTypedData"]')
.perform((done) => { // call delegate
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => {
browser
.hideMetaMaskPopup()
.saveScreenshot('./reports/screenshots/metamask_6.png')
.waitForElementPresent('button[aria-label="Scroll down"]', 60000)
.click('button[aria-label="Scroll down"]') // scroll down
.click('button[data-testid="confirm-footer-button"]') // confirm
.switchBrowserTab(0) // back to remix
.perform(() => done())
})
})
.pause(1000)
.journalChildIncludes('0x8be3a81e17b7e4a40006864a4ff6bfa3fb1e18b292b6f47edec95cd8feaa53275b90f56ca02669d461a297e6bf94ab0ee4b7c89aede3228ed5aedb59c7e007501c')
} }
} }

@ -51,7 +51,24 @@ module.exports = {
}) })
}) })
}) })
.end() },
'Test EIP 712 Signature': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('i[id="remixRunSignMsg"]')
.click('i[id="remixRunSignMsg"]')
.waitForElementVisible('*[data-id="signMessageTextarea"]', 120000)
.click('*[data-id="sign-eip-712"]')
.waitForElementVisible('*[data-id="udappNotify-modal-footer-ok-react"]')
.modalFooterOKClick('udappNotify')
.pause(1000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('"primaryType": "AuthRequest",') !== -1, 'EIP 712 data file must be opened')
})
.clickLaunchIcon('filePanel')
.rightClick('li[data-id="treeViewLitreeViewItemEIP-712-data.json"]')
.click('*[data-id="contextMenuItemsignTypedData"]')
.pause(1000)
.journalChildIncludes('0x8be3a81e17b7e4a40006864a4ff6bfa3fb1e18b292b6f47edec95cd8feaa53275b90f56ca02669d461a297e6bf94ab0ee4b7c89aede3228ed5aedb59c7e007501c')
} }
} }

@ -110,17 +110,19 @@ module.exports = {
'Should create blank workspace with no files #group1': function (browser: NightwatchBrowser) { 'Should create blank workspace with no files #group1': function (browser: NightwatchBrowser) {
browser browser
.click('*[data-id="workspacesMenuDropdown"]') .click('*[data-id="workspaceMenuDropdown"]')
.click('*[data-id="workspacecreate"]') .click('*[data-id="workspacecreateBlank"]')
.waitForElementPresent('*[data-id="create-blank"]') .waitForElementPresent('*[data-id="fileSystemModalDialogModalTitle-react"]')
.scrollAndClick('*[data-id="create-blank"]') .assert.containsText('*[data-id="fileSystemModalDialogModalTitle-react"]', 'Create Blank Workspace')
// .scrollAndClick('*[data-id="create-blank"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.scrollAndClick('*[data-id="modalDialogCustomPromptTextCreate"]') .scrollAndClick('*[data-id="modalDialogCustomPromptTextCreate"]')
.setValue('*[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_blank') .setValue('*[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_blank')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_blank' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_blank' })
.modalFooterOKClick('TemplatesSelection') .click('*[data-id="fileSystem-modal-footer-ok-react"]')
.pause(100) .pause(100)
.currentWorkspaceIs('workspace_blank')
.waitForElementPresent('*[data-id="treeViewUltreeViewMenu"]') .waitForElementPresent('*[data-id="treeViewUltreeViewMenu"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.prettierrc.json"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItem.prettierrc.json"]')
.execute(function () { .execute(function () {
@ -418,11 +420,12 @@ module.exports = {
.click('*[data-id="workspacesMenuDropdown"]') .click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacecreate"]') .click('*[data-id="workspacecreate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]') .waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]') .click('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.click('input[data-id="modalDialogCustomPromptTextCreate"]') .click('input[data-id="modalDialogCustomPromptTextCreate"]')
.setValue('input[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_name') .setValue('input[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_name')
.modalFooterOKClick('TemplatesSelection') // .modalFooterOKClick('TemplatesSelection')
.click('*[data-id="TemplatesSelection-modal-footer-ok-react"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.addFile('test.sol', { content: 'test' }) .addFile('test.sol', { content: 'test' })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
@ -437,7 +440,8 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.click('input[data-id="modalDialogCustomPromptTextCreate"]') .click('input[data-id="modalDialogCustomPromptTextCreate"]')
.setValue('input[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_name_1') .setValue('input[data-id="modalDialogCustomPromptTextCreate"]', 'workspace_name_1')
.modalFooterOKClick('TemplatesSelection') // .modalFooterOKClick('TemplatesSelection')
.click('*[data-id="TemplatesSelection-modal-footer-ok-react"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.switchWorkspace('workspace_name') .switchWorkspace('workspace_name')

@ -3,7 +3,7 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/remix-ide/src", "sourceRoot": "apps/remix-ide/src",
"projectType": "application", "projectType": "application",
"implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth", "quick-dapp", "remix-dapp"], "implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "contract-verification", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth", "quick-dapp", "remix-dapp"],
"targets": { "targets": {
"build": { "build": {
"executor": "@nrwl/webpack:webpack", "executor": "@nrwl/webpack:webpack",

@ -271,7 +271,7 @@ class FileManager extends Plugin {
} else { } else {
const ret = await this.setFileContent(path, data) const ret = await this.setFileContent(path, data)
this.emit('fileAdded', path) this.emit('fileAdded', path)
return { newContent: ret, newpath: path } return { newContent: ret, newPath: path }
} }
} catch (e) { } catch (e) {
throw new Error(e) throw new Error(e)

@ -129,7 +129,7 @@ export abstract class AbstractProvider extends Plugin implements IProvider {
const result = await this.provider.send(data.method, data.params) const result = await this.provider.send(data.method, data.params)
resolve({ jsonrpc: '2.0', result, id: data.id }) resolve({ jsonrpc: '2.0', result, id: data.id })
} catch (error) { } catch (error) {
if (error && error.message && error.message.includes('net_version') && error.message.includes('SERVER_ERROR')) { if (error && error.message && error.message.includes('SERVER_ERROR')) {
this.switchAway(true) this.switchAway(true)
} }
error.code = -32603 error.code = -32603

@ -150,5 +150,7 @@
"filePanel.saveCodeSample": "This code-sample workspace will not be persisted. Click here to save it.", "filePanel.saveCodeSample": "This code-sample workspace will not be persisted. Click here to save it.",
"filePanel.logInGithub": "Sign in to GitHub.", "filePanel.logInGithub": "Sign in to GitHub.",
"filePanel.gitHubLoggedAs": "Signed in as {githubuser}", "filePanel.gitHubLoggedAs": "Signed in as {githubuser}",
"filePanel.updateSubmodules": "Update all submodules of repository. Click to pull dependencies." "filePanel.updateSubmodules": "Update all submodules of repository. Click to pull dependencies.",
"filePanel.signTypedData": "Sign Typed Data",
"filePanel.signTypedDataError": "Error while signing this typed data."
} }

@ -46,7 +46,7 @@
"udapp._comment_account.tsx": "libs/remix-ui/run-tab/src/lib/components/account.tsx", "udapp._comment_account.tsx": "libs/remix-ui/run-tab/src/lib/components/account.tsx",
"udapp.account": "Account", "udapp.account": "Account",
"udapp.signAMessage": "Sign a message", "udapp.signAMessage": "Sign a message",
"udapp.enterAMessageToSign": "Enter a message to sign", "udapp.enterAMessageToSign": "Enter a message to sign and click `Sign`",
"udapp.hash": "hash", "udapp.hash": "hash",
"udapp.signature": "signature", "udapp.signature": "signature",
"udapp.injectedTitle": "Unfortunately it's not possible to create an account using injected provider. Please create the account directly from your provider (i.e metamask or other of the same type).", "udapp.injectedTitle": "Unfortunately it's not possible to create an account using injected provider. Please create the account directly from your provider (i.e metamask or other of the same type).",
@ -161,5 +161,10 @@
"udapp.ganacheProviderText1": "Note: To run Ganache on your system, run:", "udapp.ganacheProviderText1": "Note: To run Ganache on your system, run:",
"udapp.ganacheProviderText2": "For more info, visit: <a>Ganache Documentation</a>", "udapp.ganacheProviderText2": "For more info, visit: <a>Ganache Documentation</a>",
"udapp.hardhatProviderText1": "Note: To run Hardhat network node on your system, go to hardhat project folder and run command:", "udapp.hardhatProviderText1": "Note: To run Hardhat network node on your system, go to hardhat project folder and run command:",
"udapp.hardhatProviderText2": "For more info, visit: <a>Hardhat Documentation</a>" "udapp.hardhatProviderText2": "For more info, visit: <a>Hardhat Documentation</a>",
"udapp.EIP712-2": "Please follow <a>this link</a> to get more information.",
"udapp.EIP712-3": "In Remix, signing typed data is possible by right clicking (right click / Sign Typed Data) on a JSON file whose content is EIP-712 compatible.",
"udapp.EIP712-create-template": "Create a JSON compliant with EIP-712",
"udapp.EIP712-close": "Close",
"udapp.sign": "Sign"
} }

@ -21,7 +21,7 @@ const profile = {
/** /**
* Record transaction as long as the user create them. * Record transaction as long as the user create them.
*/ */
class Recorder extends Plugin { export class Recorder extends Plugin {
constructor (blockchain) { constructor (blockchain) {
super(profile) super(profile)
this.event = new EventManager() this.event = new EventManager()
@ -328,5 +328,3 @@ class Recorder extends Plugin {
}) })
} }
} }
module.exports = Recorder

@ -1,14 +1,17 @@
import React from 'react' // eslint-disable-line import React from 'react' // eslint-disable-line
import {RunTabUI} from '@remix-ui/run-tab' import { RunTabUI } from '@remix-ui/run-tab'
import {ViewPlugin} from '@remixproject/engine-web' import { ViewPlugin } from '@remixproject/engine-web'
import isElectron from 'is-electron' import isElectron from 'is-electron'
import {addressToString} from '@remix-ui/helper' import { addressToString } from '@remix-ui/helper'
import {InjectedProviderDefault} from '../providers/injected-provider-default' import { InjectedProviderDefault } from '../providers/injected-provider-default'
import {InjectedCustomProvider} from '../providers/injected-custom-provider' import { InjectedCustomProvider } from '../providers/injected-custom-provider'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import { EventManager } from '@remix-project/remix-lib'
const EventManager = require('../../lib/events') import type { Blockchain } from '../../blockchain/blockchain'
const Recorder = require('../tabs/runTab/model/recorder.js') import type { CompilerArtefacts } from '@remix-project/core-plugin'
// import type { NetworkModule } from '../tabs/network-module'
// import type FileProvider from '../files/fileProvider'
import { Recorder } from '../tabs/runTab/model/recorder'
const _paq = (window._paq = window._paq || []) const _paq = (window._paq = window._paq || [])
const profile = { const profile = {
@ -37,7 +40,20 @@ const profile = {
} }
export class RunTab extends ViewPlugin { export class RunTab extends ViewPlugin {
constructor(blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, fileProvider, engine) { event: EventManager
engine: any
config: any
blockchain: Blockchain
fileManager: any
editor: any
filePanel: any
compilersArtefacts: CompilerArtefacts
networkModule: any
fileProvider: any
recorder: any
REACT_API: any
el: any
constructor(blockchain: Blockchain, config: any, fileManager: any, editor: any, filePanel: any, compilersArtefacts: CompilerArtefacts, networkModule: any, fileProvider: any, engine: any) {
super(profile) super(profile)
this.event = new EventManager() this.event = new EventManager()
this.engine = engine this.engine = engine
@ -74,7 +90,7 @@ export class RunTab extends ViewPlugin {
async setEnvironmentMode(env) { async setEnvironmentMode(env) {
const canCall = await this.askUserPermission('setEnvironmentMode', 'change the environment used') const canCall = await this.askUserPermission('setEnvironmentMode', 'change the environment used')
if (canCall) { if (canCall) {
env = typeof env === 'string' ? {context: env} : env env = typeof env === 'string' ? { context: env } : env
this.emit('setEnvironmentModeReducer', env, this.currentRequest.from) this.emit('setEnvironmentModeReducer', env, this.currentRequest.from)
} }
} }
@ -83,7 +99,7 @@ export class RunTab extends ViewPlugin {
this.emit('clearAllInstancesReducer') this.emit('clearAllInstancesReducer')
} }
addInstance(address, abi, name, contractData) { addInstance(address, abi, name, contractData?) {
this.emit('addInstanceReducer', address, abi, name, contractData) this.emit('addInstanceReducer', address, abi, name, contractData)
} }
@ -181,21 +197,32 @@ export class RunTab extends ViewPlugin {
provider: { provider: {
sendAsync (payload) { sendAsync (payload) {
return udapp.call(name, 'sendAsync', payload) return udapp.call(name, 'sendAsync', payload)
},
async request (payload) {
try {
const requestResult = await udapp.call(name, 'sendAsync', payload)
if (requestResult.error) {
throw new Error(requestResult.error.message)
}
return requestResult.result
} catch (err) {
throw new Error(err.message)
}
} }
} }
}) })
} }
const addCustomInjectedProvider = async (position, event, name, displayName, networkId, urls, nativeCurrency) => { const addCustomInjectedProvider = async (position, event, name, displayName, networkId, urls, nativeCurrency?) => {
// name = `${name} through ${event.detail.info.name}` // name = `${name} through ${event.detail.info.name}`
await this.engine.register([new InjectedCustomProvider(event.detail.provider, name, networkId, urls, nativeCurrency)]) await this.engine.register([new InjectedCustomProvider(event.detail.provider, name, networkId, urls, nativeCurrency)])
await addProvider(position, name, displayName, true, false, false) await addProvider(position, name, displayName, true, false)
} }
const registerInjectedProvider = async (event) => { const registerInjectedProvider = async (event) => {
const name = 'injected-' + event.detail.info.name const name = 'injected-' + event.detail.info.name
const displayName = 'Injected Provider - ' + event.detail.info.name const displayName = 'Injected Provider - ' + event.detail.info.name
await this.engine.register([new InjectedProviderDefault(event.detail.provider, name)]) await this.engine.register([new InjectedProviderDefault(event.detail.provider, name)])
await addProvider(0, name, displayName, true, false, false) await addProvider(0, name, displayName, true, false)
if (event.detail.info.name === 'MetaMask') { if (event.detail.info.name === 'MetaMask') {
await addCustomInjectedProvider(7, event, 'injected-metamask-optimism', 'L2 - Optimism - ' + event.detail.info.name, '0xa', ['https://mainnet.optimism.io']) await addCustomInjectedProvider(7, event, 'injected-metamask-optimism', 'L2 - Optimism - ' + event.detail.info.name, '0xa', ['https://mainnet.optimism.io'])
@ -218,7 +245,6 @@ export class RunTab extends ViewPlugin {
"symbol": "XDAI", "symbol": "XDAI",
"decimals": 18 "decimals": 18
}) })
/* /*
await addCustomInjectedProvider(9, event, 'SKALE Chaos Testnet', '0x50877ed6', ['https://staging-v3.skalenodes.com/v1/staging-fast-active-bellatrix'], await addCustomInjectedProvider(9, event, 'SKALE Chaos Testnet', '0x50877ed6', ['https://staging-v3.skalenodes.com/v1/staging-fast-active-bellatrix'],
{ {

File diff suppressed because it is too large Load Diff

@ -38,11 +38,11 @@ export class InjectedProvider {
} }
signMessage (message, account, _passphrase, cb) { signMessage (message, account, _passphrase, cb) {
message = isHexString(message) ? message : Web3.utils.utf8ToHex(message)
const messageHash = hashPersonalMessage(Buffer.from(message)) const messageHash = hashPersonalMessage(Buffer.from(message))
try { try {
message = isHexString(message) ? message : Web3.utils.utf8ToHex(message) this.executionContext.web3().eth.sign(messageHash, account).then((signedData) => {
this.executionContext.web3().eth.personal.sign(message, account).then((error, signedData) => { cb(null, bytesToHex(messageHash), signedData)
cb(error, bytesToHex(messageHash), signedData)
}).catch((error => cb(error))) }).catch((error => cb(error)))
} catch (e) { } catch (e) {
cb(e.message) cb(e.message)

@ -1,4 +1,4 @@
import { Web3, FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider } from 'web3' import { Web3, FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider, LegacyRequestProvider } from 'web3'
import { fromWei, toBigInt } from 'web3-utils' import { fromWei, toBigInt } from 'web3-utils'
import { privateToAddress, hashPersonalMessage, isHexString, bytesToHex } from '@ethereumjs/util' import { privateToAddress, hashPersonalMessage, isHexString, bytesToHex } from '@ethereumjs/util'
import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-project/remix-simulator' import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-project/remix-simulator'
@ -10,6 +10,7 @@ export class VMProvider {
worker: Worker worker: Worker
provider: { provider: {
sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void
request: (query: JSONRPCRequestPayload) => Promise<any>
} }
newAccountCallback: {[stamp: number]: (error: Error, address: string) => void} newAccountCallback: {[stamp: number]: (error: Error, address: string) => void}
constructor (executionContext: ExecutionContext) { constructor (executionContext: ExecutionContext) {
@ -37,7 +38,13 @@ export class VMProvider {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.worker.addEventListener('message', (msg) => { this.worker.addEventListener('message', (msg) => {
if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) { if (msg.data.cmd === 'requestResult' && stamps[msg.data.stamp]) {
if (msg.data.error) {
stamps[msg.data.stamp].reject(msg.data.error)
} else {
stamps[msg.data.stamp].resolve(msg.data.result)
}
} else if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) {
if (stamps[msg.data.stamp].callback) { if (stamps[msg.data.stamp].callback) {
stamps[msg.data.stamp].callback(msg.data.error, msg.data.result) stamps[msg.data.stamp].callback(msg.data.error, msg.data.result)
return return
@ -57,9 +64,17 @@ export class VMProvider {
stamps[stamp] = { callback, resolve, reject } stamps[stamp] = { callback, resolve, reject }
this.worker.postMessage({ cmd: 'sendAsync', query, stamp }) this.worker.postMessage({ cmd: 'sendAsync', query, stamp })
}) })
},
request: (query) => {
return new Promise((resolve, reject) => {
const stamp = Date.now() + incr
incr++
stamps[stamp] = { resolve, reject }
this.worker.postMessage({ cmd: 'request', query, stamp })
})
} }
} }
this.web3 = new Web3(this.provider as LegacySendAsyncProvider) this.web3 = new Web3(this.provider as (LegacySendAsyncProvider | LegacyRequestProvider))
this.web3.setConfig({ defaultTransactionType: '0x0' }) this.web3.setConfig({ defaultTransactionType: '0x0' })
extend(this.web3) extend(this.web3)
this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3) this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3)

@ -43,6 +43,37 @@ self.onmessage = (e: MessageEvent) => {
break break
} }
case 'request':
{
(function (data) {
const stamp = data.stamp
if (provider) {
provider.request(data.query).then((result) => {
self.postMessage({
cmd: 'requestResult',
error: null,
result: result,
stamp: stamp
})
}).catch((error) => {
self.postMessage({
cmd: 'requestResult',
error: error,
result: null,
stamp: stamp
})
})
} else {
self.postMessage({
cmd: 'requestResult',
error: 'Provider not instantiated',
result: null,
stamp: stamp
})
}
})(data)
break
}
case 'addAccount': case 'addAccount':
{ {
if (provider) { if (provider) {

@ -7,7 +7,8 @@ import {Registry} from '@remix-project/remix-lib'
const _paq = (window._paq = window._paq || []) const _paq = (window._paq = window._paq || [])
// requiredModule removes the plugin from the plugin manager list on UI // requiredModule removes the plugin from the plugin manager list on UI
let requiredModules = [ // services + layout views + system views let requiredModules = [
// services + layout views + system views
'manager', 'manager',
'config', 'config',
'compilerArtefacts', 'compilerArtefacts',
@ -91,14 +92,14 @@ let requiredModules = [ // services + layout views + system views
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither'] const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth', 'quick-dapp'] const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'contract-verification', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth', 'quick-dapp']
const partnerPlugins = ['cookbookdev'] const partnerPlugins = ['cookbookdev']
const sensitiveCalls = { const sensitiveCalls = {
fileManager: ['writeFile', 'copyFile', 'rename', 'copyDir'], fileManager: ['writeFile', 'copyFile', 'rename', 'copyDir'],
contentImport: ['resolveAndSave'], contentImport: ['resolveAndSave'],
web3Provider: ['sendAsync'] web3Provider: ['sendAsync'],
} }
const isInjectedProvider = (name) => { const isInjectedProvider = (name) => {
@ -139,7 +140,8 @@ export function isNative(name) {
//'remixGuide', //'remixGuide',
'environmentExplorer', 'environmentExplorer',
'templateSelection', 'templateSelection',
'walletconnect' 'walletconnect',
'contract-verification'
] ]
return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name)
} }
@ -315,7 +317,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 5 group: 5,
}) })
await this.call('filePanel', 'registerContextMenuItem', { await this.call('filePanel', 'registerContextMenuItem', {
id: 'nahmii-compiler', id: 'nahmii-compiler',
@ -326,7 +328,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 6 group: 6,
}) })
await this.call('filePanel', 'registerContextMenuItem', { await this.call('filePanel', 'registerContextMenuItem', {
id: 'solidityumlgen', id: 'solidityumlgen',
@ -337,7 +339,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 7 group: 7,
}) })
await this.call('filePanel', 'registerContextMenuItem', { await this.call('filePanel', 'registerContextMenuItem', {
id: 'doc-gen', id: 'doc-gen',
@ -348,7 +350,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 7 group: 7,
}) })
await this.call('filePanel', 'registerContextMenuItem', { await this.call('filePanel', 'registerContextMenuItem', {
id: 'vyper', id: 'vyper',
@ -359,7 +361,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 7 group: 7,
}) })
if (Registry.getInstance().get('platform').api.isDesktop()) { if (Registry.getInstance().get('platform').api.isDesktop()) {
await this.call('filePanel', 'registerContextMenuItem', { await this.call('filePanel', 'registerContextMenuItem', {
@ -371,7 +373,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 8 group: 8,
}) })
await this.call('filePanel', 'registerContextMenuItem', { await this.call('filePanel', 'registerContextMenuItem', {
id: 'fs', id: 'fs',
@ -382,7 +384,7 @@ export class RemixAppManager extends PluginManager {
path: [], path: [],
pattern: [], pattern: [],
sticky: true, sticky: true,
group: 8 group: 8,
}) })
} }
} }
@ -417,7 +419,7 @@ class PluginLoader {
}, },
get: () => { get: () => {
return JSON.parse(localStorage.getItem('workspace')) return JSON.parse(localStorage.getItem('workspace'))
} },
} }
this.loaders.queryParams = { this.loaders.queryParams = {
@ -428,7 +430,7 @@ class PluginLoader {
const {activate} = queryParams.get() const {activate} = queryParams.get()
if (!activate) return [] if (!activate) return []
return activate.split(',') return activate.split(',')
} },
} }
this.current = queryParams.get().activate ? 'queryParams' : 'localStorage' this.current = queryParams.get().activate ? 'queryParams' : 'localStorage'

@ -5,85 +5,64 @@ import { CompilerAbstract } from '@remix-project/remix-solidity'
const profile = { const profile = {
name: 'compilerArtefacts', name: 'compilerArtefacts',
methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult', 'getArtefactsByContractName', 'getContractDataFromAddress', 'getContractDataFromByteCode', 'saveCompilerAbstract'], methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult', 'getArtefactsByContractName', 'getContractDataFromAddress', 'getContractDataFromByteCode', 'saveCompilerAbstract', 'getAllCompilerAbstracts'],
events: [], events: ['compilationSaved'],
version: '0.0.1' version: '0.0.1',
} }
export class CompilerArtefacts extends Plugin { export class CompilerArtefacts extends Plugin {
compilersArtefactsPerFile: any compilersArtefactsPerFile: any
compilersArtefacts: any compilersArtefacts: any
constructor () { constructor() {
super(profile) super(profile)
this.compilersArtefacts = {} this.compilersArtefacts = {}
this.compilersArtefactsPerFile = {} this.compilersArtefactsPerFile = {}
} }
clear () { clear() {
this.compilersArtefacts = {} this.compilersArtefacts = {}
this.compilersArtefactsPerFile = {} this.compilersArtefactsPerFile = {}
} }
saveCompilerAbstract (file: string, compilerAbstract: CompilerAbstract) { saveCompilerAbstract(file: string, compilerAbstract: CompilerAbstract) {
this.compilersArtefactsPerFile[file] = compilerAbstract this.compilersArtefactsPerFile[file] = compilerAbstract
} }
onActivation () { getAllCompilerAbstracts() {
const saveCompilationPerFileResult = (file, source, languageVersion, data, input?) => { return this.compilersArtefactsPerFile
}
onActivation() {
const saveCompilationResult = (file, source, languageVersion, data, input?) => {
this.compilersArtefactsPerFile[file] = new CompilerAbstract(languageVersion, data, source, input) this.compilersArtefactsPerFile[file] = new CompilerAbstract(languageVersion, data, source, input)
this.compilersArtefacts.__last = this.compilersArtefactsPerFile[file]
this.emit('compilationSaved', { [file]: this.compilersArtefactsPerFile[file] })
} }
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => { this.on('solidity', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source, input)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('vyper', 'compilationFinished', (file, source, languageVersion, data) => { this.on('vyper', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('lexon', 'compilationFinished', (file, source, languageVersion, data) => { this.on('lexon', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('yulp', 'compilationFinished', (file, source, languageVersion, data) => { this.on('yulp', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('solidityUnitTesting', 'compilationFinished', (file, source, languageVersion, data, input, version) => { this.on('solidityUnitTesting', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source, input)
saveCompilationPerFileResult(file, source, languageVersion, data, input)
})
this.on('nahmii-compiler', 'compilationFinished', (file, source, languageVersion, data) => { this.on('nahmii-compiler', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('hardhat', 'compilationFinished', (file, source, languageVersion, data) => { this.on('hardhat', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('truffle', 'compilationFinished', (file, source, languageVersion, data) => { this.on('truffle', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('foundry', 'compilationFinished', (file, source, languageVersion, data) => { this.on('foundry', 'compilationFinished', saveCompilationResult)
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
} }
/** /**
* Get artefacts for last compiled contract * Get artefacts for last compiled contract
* * @returns last compiled contract compiler abstract * * @returns last compiled contract compiler abstract
*/ */
getLastCompilationResult () { getLastCompilationResult() {
return this.compilersArtefacts.__last return this.compilersArtefacts.__last
} }
@ -91,7 +70,7 @@ export class CompilerArtefacts extends Plugin {
* Get compilation output for contracts compiled during a session of Remix IDE * Get compilation output for contracts compiled during a session of Remix IDE
* @returns compilatin output * @returns compilatin output
*/ */
getAllContractDatas () { getAllContractDatas() {
return this.filterAllContractDatas(() => true) return this.filterAllContractDatas(() => true)
} }
@ -99,7 +78,7 @@ export class CompilerArtefacts extends Plugin {
* filter compilation output for contracts compiled during a session of Remix IDE * filter compilation output for contracts compiled during a session of Remix IDE
* @returns compilatin output * @returns compilatin output
*/ */
filterAllContractDatas (filter) { filterAllContractDatas(filter) {
const contractsData = {} const contractsData = {}
Object.keys(this.compilersArtefactsPerFile).map((targetFile) => { Object.keys(this.compilersArtefactsPerFile).map((targetFile) => {
const artefact = this.compilersArtefactsPerFile[targetFile] const artefact = this.compilersArtefactsPerFile[targetFile]
@ -124,7 +103,7 @@ export class CompilerArtefacts extends Plugin {
* @param contractName contract name * @param contractName contract name
* @returns arefacts object, with fully qualified name (e.g; contracts/1_Storage.sol:Storage) as key * @returns arefacts object, with fully qualified name (e.g; contracts/1_Storage.sol:Storage) as key
*/ */
_getAllContractArtefactsfromOutput (compilerOutput, contractName) { _getAllContractArtefactsfromOutput(compilerOutput, contractName) {
const contractArtefacts = {} const contractArtefacts = {}
for (const filename in compilerOutput) { for (const filename in compilerOutput) {
if (Object.keys(compilerOutput[filename]).includes(contractName)) contractArtefacts[filename + ':' + contractName] = compilerOutput[filename][contractName] if (Object.keys(compilerOutput[filename]).includes(contractName)) contractArtefacts[filename + ':' + contractName] = compilerOutput[filename][contractName]
@ -139,12 +118,12 @@ export class CompilerArtefacts extends Plugin {
* @param contractArtefacts populated resultant artefacts object, with fully qualified name (e.g: contracts/1_Storage.sol:Storage) as key * @param contractArtefacts populated resultant artefacts object, with fully qualified name (e.g: contracts/1_Storage.sol:Storage) as key
* Once method execution completes, contractArtefacts object will hold all possible artefacts for contract * Once method execution completes, contractArtefacts object will hold all possible artefacts for contract
*/ */
async _populateAllContractArtefactsFromFE (path, contractName, contractArtefacts) { async _populateAllContractArtefactsFromFE(path, contractName, contractArtefacts) {
const dirList = await this.call('fileManager', 'dirList', path) const dirList = await this.call('fileManager', 'dirList', path)
if (dirList && dirList.length) { if (dirList && dirList.length) {
for (const dirPath of dirList) { for (const dirPath of dirList) {
// check if directory contains an 'artifacts' folder and a 'build-info' folder inside 'artifacts' // check if directory contains an 'artifacts' folder and a 'build-info' folder inside 'artifacts'
if (dirPath === path + '/artifacts' && await this.call('fileManager', 'exists', dirPath + '/build-info')) { if (dirPath === path + '/artifacts' && (await this.call('fileManager', 'exists', dirPath + '/build-info'))) {
const buildFileList = await this.call('fileManager', 'fileList', dirPath + '/build-info') const buildFileList = await this.call('fileManager', 'fileList', dirPath + '/build-info')
// process each build-info file to populate the artefacts for contractName // process each build-info file to populate the artefacts for contractName
for (const buildFile of buildFileList) { for (const buildFile of buildFileList) {
@ -155,7 +134,7 @@ export class CompilerArtefacts extends Plugin {
// populate the resultant object with artefacts // populate the resultant object with artefacts
Object.assign(contractArtefacts, artefacts) Object.assign(contractArtefacts, artefacts)
} }
} else await this._populateAllContractArtefactsFromFE (dirPath, contractName, contractArtefacts) } else await this._populateAllContractArtefactsFromFE(dirPath, contractName, contractArtefacts)
} }
} else return } else return
} }
@ -165,7 +144,7 @@ export class CompilerArtefacts extends Plugin {
* @param name contract name or fully qualified name i.e. <filename>:<contractname> e.g: contracts/1_Storage.sol:Storage * @param name contract name or fully qualified name i.e. <filename>:<contractname> e.g: contracts/1_Storage.sol:Storage
* @returns artefacts for the contract * @returns artefacts for the contract
*/ */
async getArtefactsByContractName (name) { async getArtefactsByContractName(name) {
const contractsDataByFilename = this.getAllContractDatas() const contractsDataByFilename = this.getAllContractDatas()
// check if name is a fully qualified name // check if name is a fully qualified name
if (name.includes(':')) { if (name.includes(':')) {
@ -173,11 +152,10 @@ export class CompilerArtefacts extends Plugin {
const nameArr = fullyQualifiedName.split(':') const nameArr = fullyQualifiedName.split(':')
const filename = nameArr[0] const filename = nameArr[0]
const contract = nameArr[1] const contract = nameArr[1]
if (Object.keys(contractsDataByFilename).includes(filename) && contractsDataByFilename[filename][contract]) if (Object.keys(contractsDataByFilename).includes(filename) && contractsDataByFilename[filename][contract]) return contractsDataByFilename[filename][contract]
return contractsDataByFilename[filename][contract]
else { else {
const allContractsData = {} const allContractsData = {}
await this._populateAllContractArtefactsFromFE ('contracts', contract, allContractsData) await this._populateAllContractArtefactsFromFE('contracts', contract, allContractsData)
if (allContractsData[fullyQualifiedName]) return { fullyQualifiedName, artefact: allContractsData[fullyQualifiedName] } if (allContractsData[fullyQualifiedName]) return { fullyQualifiedName, artefact: allContractsData[fullyQualifiedName] }
else throw new Error(`Could not find artifacts for ${fullyQualifiedName}. Compile contract to generate artifacts.`) else throw new Error(`Could not find artifacts for ${fullyQualifiedName}. Compile contract to generate artifacts.`)
} }
@ -186,7 +164,7 @@ export class CompilerArtefacts extends Plugin {
const contractArtefacts = this._getAllContractArtefactsfromOutput(contractsDataByFilename, contractName) const contractArtefacts = this._getAllContractArtefactsfromOutput(contractsDataByFilename, contractName)
let keys = Object.keys(contractArtefacts) let keys = Object.keys(contractArtefacts)
if (!keys.length) { if (!keys.length) {
await this._populateAllContractArtefactsFromFE ('contracts', contractName, contractArtefacts) await this._populateAllContractArtefactsFromFE('contracts', contractName, contractArtefacts)
keys = Object.keys(contractArtefacts) keys = Object.keys(contractArtefacts)
} }
if (keys.length === 1) return { fullyQualifiedName: keys[0], artefact: contractArtefacts[keys[0]] } if (keys.length === 1) return { fullyQualifiedName: keys[0], artefact: contractArtefacts[keys[0]] }
@ -199,7 +177,7 @@ export class CompilerArtefacts extends Plugin {
} }
} }
async getCompilerAbstract (file) { async getCompilerAbstract(file) {
if (!file) return null if (!file) return null
if (this.compilersArtefactsPerFile[file]) return this.compilersArtefactsPerFile[file] if (this.compilersArtefactsPerFile[file]) return this.compilersArtefactsPerFile[file]
const path = await this.call('fileManager', 'getPathFromUrl', file) const path = await this.call('fileManager', 'getPathFromUrl', file)
@ -215,24 +193,24 @@ export class CompilerArtefacts extends Plugin {
return artefact return artefact
} }
addResolvedContract (address: string, compilerData: CompilerAbstract) { addResolvedContract(address: string, compilerData: CompilerAbstract) {
this.compilersArtefacts[address] = compilerData this.compilersArtefacts[address] = compilerData
} }
isResolved (address) { isResolved(address) {
return this.compilersArtefacts[address] !== undefined return this.compilersArtefacts[address] !== undefined
} }
get (key) { get(key) {
return this.compilersArtefacts[key] return this.compilersArtefacts[key]
} }
async getContractDataFromAddress (address) { async getContractDataFromAddress(address) {
const code = await this.call('blockchain', 'getCode', address) const code = await this.call('blockchain', 'getCode', address)
return this.getContractDataFromByteCode(code) return this.getContractDataFromByteCode(code)
} }
async getContractDataFromByteCode (code) { async getContractDataFromByteCode(code) {
let found let found
this.filterAllContractDatas((file, contractsData) => { this.filterAllContractDatas((file, contractsData) => {
for (const name of Object.keys(contractsData)) { for (const name of Object.keys(contractsData)) {

@ -172,6 +172,6 @@ export function checkError (execResult, compiledContracts) {
msg = '\tState changes is not allowed in Static Call context\n' msg = '\tState changes is not allowed in Static Call context\n'
ret.error = true ret.error = true
} }
ret.message = `${error}\n${exceptionError}\n${msg}\nYou may want to cautiously increase the gas limit if the transaction went out of gas.` ret.message = `${error}\n${exceptionError}\n${msg}\nIf the transaction failed for not having enough gas, try increasing the gas limit gently.`
return ret return ret
} }

@ -160,9 +160,10 @@ export class TxRunnerWeb3 {
}, callback) }, callback)
}) })
.catch(err => { .catch(err => {
if (err && err.message.indexOf('Invalid JSON RPC response') !== -1) { if (err && err.error && err.error.indexOf('Invalid JSON RPC response') !== -1) {
// // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed // // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed
callback(new Error('Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.')) callback(new Error('Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.'))
return
} }
err = network.name === 'VM' ? null : err // just send the tx if "VM" err = network.name === 'VM' ? null : err // just send the tx if "VM"
gasEstimationForceSend(err, () => { gasEstimationForceSend(err, () => {

@ -82,12 +82,12 @@ export function runTestFiles (filepath: string, isDirectory: boolean, web3: Web3
// If contract deployment fails because of 'Out of Gas' error, try again with double gas // If contract deployment fails because of 'Out of Gas' error, try again with double gas
// This is temporary, should be removed when remix-tests will have a dedicated UI to // This is temporary, should be removed when remix-tests will have a dedicated UI to
// accept deployment params from UI // accept deployment params from UI
if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { if (err.error.includes('The contract code couldn\'t be stored, please check your gas limit')) {
deployAll(compilationResult, web3, accounts, true, null, (error, contracts) => { deployAll(compilationResult, web3, accounts, true, null, (error, contracts) => {
if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.innerError || error.message, severity: 'error' }]) // IDE expects errors in array if (error) next([{ message: 'contract deployment failed after trying twice: ' + (error.innerError || error.error), severity: 'error' }]) // IDE expects errors in array
else next(null, compilationResult, contracts) else next(null, compilationResult, contracts)
}) })
} else { next([{ message: 'contract deployment failed: ' + err.innerError || err.message, severity: 'error' }]) } // IDE expects errors in array } else { next([{ message: 'contract deployment failed: ' + (err.innerError || err.error), severity: 'error' }]) } // IDE expects errors in array
} else { next(null, compilationResult, contracts) } } else { next(null, compilationResult, contracts) }
}) })
}, },

@ -62,12 +62,12 @@ export class UnitTestRunner {
// If contract deployment fails because of 'Out of Gas' error, try again with double gas // If contract deployment fails because of 'Out of Gas' error, try again with double gas
// This is temporary, should be removed when remix-tests will have a dedicated UI to // This is temporary, should be removed when remix-tests will have a dedicated UI to
// accept deployment params from UI // accept deployment params from UI
if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { if (err.error.includes('The contract code couldn\'t be stored, please check your gas limit')) {
deployAll(compilationResult, this.web3, this.testsAccounts, true, deployCb, (error, contracts) => { deployAll(compilationResult, this.web3, this.testsAccounts, true, deployCb, (error, contracts) => {
if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.innerError || error.message, severity: 'error' }]) // IDE expects errors in array if (error) next([{ message: 'contract deployment failed after trying twice: ' + (error.innerError || error.error), severity: 'error' }]) // IDE expects errors in array
else next(null, compilationResult, contracts) else next(null, compilationResult, contracts)
}) })
} else { next([{ message: 'contract deployment failed: ' + err.innerError || err.message, severity: 'error' }]) } // IDE expects errors in array } else { next([{ message: 'contract deployment failed: ' + (err.innerError || err.error), severity: 'error' }]) } // IDE expects errors in array
} else { next(null, compilationResult, contracts) } } else { next(null, compilationResult, contracts) }
}) })
}, },

@ -3,6 +3,7 @@ import { Dropdown, DropdownButton } from 'react-bootstrap'
import DropdownItem from 'react-bootstrap/DropdownItem' import DropdownItem from 'react-bootstrap/DropdownItem'
import { localeLang } from './types/carouselTypes' import { localeLang } from './types/carouselTypes'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
const _paq = (window._paq = window._paq || [])
export function LanguageOptions({ plugin }: { plugin: any }) { export function LanguageOptions({ plugin }: { plugin: any }) {
const [langOptions, setLangOptions] = useState<string>() const [langOptions, setLangOptions] = useState<string>()
@ -39,6 +40,7 @@ export function LanguageOptions({ plugin }: { plugin: any }) {
{ {
changeLanguage(lang.toLowerCase()) changeLanguage(lang.toLowerCase())
setLangOptions(lang) setLangOptions(lang)
_paq.push(['trackEvent', 'hometab', 'switchTo', lang])
}} }}
style={{ color: 'var(--text)', cursor: 'pointer' }} style={{ color: 'var(--text)', cursor: 'pointer' }}
key={index} key={index}

@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl'
import { PluginRecord } from '../types' import { PluginRecord } from '../types'
import './panel.css' import './panel.css'
import { CustomTooltip, RenderIf, RenderIfNot } from '@remix-ui/helper' import { CustomTooltip, RenderIf, RenderIfNot } from '@remix-ui/helper'
const _paq = (window._paq = window._paq || [])
export interface RemixPanelProps { export interface RemixPanelProps {
plugins: Record<string, PluginRecord>, plugins: Record<string, PluginRecord>,
@ -29,10 +30,12 @@ const RemixUIPanelHeader = (props: RemixPanelProps) => {
const pinPlugin = () => { const pinPlugin = () => {
props.pinView && props.pinView(plugin.profile, plugin.view) props.pinView && props.pinView(plugin.profile, plugin.view)
_paq.push(['trackEvent', 'PluginPanel', 'pinToRight', plugin.profile.name])
} }
const unPinPlugin = () => { const unPinPlugin = () => {
props.unPinView && props.unPinView(plugin.profile) props.unPinView && props.unPinView(plugin.profile)
_paq.push(['trackEvent', 'PluginPanel', 'pinToLeft', plugin.profile.name])
} }
const tooltipChild = <i className={`px-1 ml-2 pt-1 pb-2 ${!toggleExpander ? 'fas fa-angle-right' : 'fas fa-angle-down bg-light'}`} aria-hidden="true"></i> const tooltipChild = <i className={`px-1 ml-2 pt-1 pb-2 ${!toggleExpander ? 'fas fa-angle-right' : 'fas fa-angle-down bg-light'}`} aria-hidden="true"></i>

@ -95,3 +95,8 @@ export const signMessageWithAddress = (plugin: RunTab, dispatch: React.Dispatch<
dispatch(displayNotification('Signed Message', modalContent(msgHash, signedData), 'OK', null, () => {}, null)) dispatch(displayNotification('Signed Message', modalContent(msgHash, signedData), 'OK', null, () => {}, null))
}) })
} }
export const addFileInternal = async (plugin: RunTab, path: string, content: string) => {
const file = await plugin.call('fileManager', 'writeFileNoRewrite', path, content)
await plugin.call('fileManager', 'open', file.newPath)
}

@ -2,7 +2,7 @@
import React from 'react' import React from 'react'
import { RunTab } from '../types/run-tab' import { RunTab } from '../types/run-tab'
import { resetAndInit, setupEvents, setEventsDispatch } from './events' import { resetAndInit, setupEvents, setEventsDispatch } from './events'
import { createNewBlockchainAccount, setExecutionContext, signMessageWithAddress } from './account' import { createNewBlockchainAccount, setExecutionContext, signMessageWithAddress, addFileInternal } from './account'
import { clearInstances, clearPopUp, removeInstance, pinInstance, unpinInstance, setAccount, setGasFee, setMatchPassphrasePrompt, import { clearInstances, clearPopUp, removeInstance, pinInstance, unpinInstance, setAccount, setGasFee, setMatchPassphrasePrompt,
setNetworkNameFromProvider, setPassphrasePrompt, setSelectedContract, setSendTransactionValue, setUnit, setNetworkNameFromProvider, setPassphrasePrompt, setSelectedContract, setSendTransactionValue, setUnit,
updateBaseFeePerGas, updateConfirmSettings, updateGasPrice, updateGasPriceStatus, updateMaxFee, updateMaxPriorityFee, updateScenarioPath } from './actions' updateBaseFeePerGas, updateConfirmSettings, updateGasPrice, updateGasPriceStatus, updateMaxFee, updateMaxPriorityFee, updateScenarioPath } from './actions'
@ -32,6 +32,7 @@ export const initRunTab = (udapp: RunTab, resetEventsAndAccounts: boolean) => as
} }
} }
export const addFile = (path: string, content: string) => addFileInternal(plugin, path, content)
export const setAccountAddress = (account: string) => setAccount(dispatch, account) export const setAccountAddress = (account: string) => setAccount(dispatch, account)
export const setUnitValue = (unit: 'ether' | 'finney' | 'gwei' | 'wei') => setUnit(dispatch, unit) export const setUnitValue = (unit: 'ether' | 'finney' | 'gwei' | 'wei') => setUnit(dispatch, unit)
export const setGasFeeAmount = (value: number) => setGasFee(dispatch, value) export const setGasFeeAmount = (value: number) => setGasFee(dispatch, value)

@ -111,7 +111,7 @@ export function AccountUI(props: AccountProps) {
props.modal( props.modal(
intl.formatMessage({ id: 'udapp.signAMessage' }), intl.formatMessage({ id: 'udapp.signAMessage' }),
signMessagePrompt(), signMessagePrompt(),
intl.formatMessage({ id: 'udapp.ok' }), intl.formatMessage({ id: 'udapp.sign' }),
() => { () => {
props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt, props.passphrase) props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt, props.passphrase)
props.setPassphrase('') props.setPassphrase('')
@ -130,7 +130,7 @@ export function AccountUI(props: AccountProps) {
props.modal( props.modal(
intl.formatMessage({ id: 'udapp.signAMessage' }), intl.formatMessage({ id: 'udapp.signAMessage' }),
signMessagePrompt(), signMessagePrompt(),
intl.formatMessage({ id: 'udapp.ok' }), intl.formatMessage({ id: 'udapp.sign' }),
() => { () => {
props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt) props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt)
}, },
@ -167,7 +167,7 @@ export function AccountUI(props: AccountProps) {
<FormattedMessage id="udapp.enterAMessageToSign" /> <FormattedMessage id="udapp.enterAMessageToSign" />
<textarea <textarea
id="prompt_text" id="prompt_text"
className="bg-light text-light" className="bg-light text-light form-control"
data-id="signMessageTextarea" data-id="signMessageTextarea"
style={{ width: '100%' }} style={{ width: '100%' }}
rows={4} rows={4}
@ -175,6 +175,25 @@ export function AccountUI(props: AccountProps) {
onInput={handleMessageInput} onInput={handleMessageInput}
defaultValue={messageRef.current} defaultValue={messageRef.current}
></textarea> ></textarea>
<div className='mt-2'>
<span>otherwise</span><button className='ml-2 modal-ok btn btn-sm border-primary' data-id="sign-eip-712" onClick={() => {
props.modal(
'Message signing with EIP-712',
<div>
<div>{intl.formatMessage({ id: 'udapp.EIP712-2' }, {
a: (chunks) => (
<a href='https://eips.ethereum.org/EIPS/eip-712' target="_blank" rel="noreferrer">
{chunks}
</a>
)
})}</div>
<div>{intl.formatMessage({ id: 'udapp.EIP712-3' })}</div></div>,
intl.formatMessage({ id: 'udapp.EIP712-create-template' }),
() => { props.addFile('EIP-712-data.json', JSON.stringify(EIP712_Example, null, '\t')) },
intl.formatMessage({ id: 'udapp.EIP712-close' }),
() => {})
}}>Sign with EIP 712</button>
</div>
</div> </div>
) )
} }
@ -236,3 +255,29 @@ export function AccountUI(props: AccountProps) {
</div> </div>
) )
} }
const EIP712_Example = {
domain: {
chainId: 1,
name: "Example App",
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
version: "1",
},
message: {
prompt: "Welcome! In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way.",
createdAt: 1718570375196,
},
primaryType: 'AuthRequest',
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
AuthRequest: [
{ name: 'prompt', type: 'string' },
{ name: 'createdAt', type: 'uint256' },
],
},
}

@ -15,6 +15,7 @@ export function SettingsUI(props: SettingsProps) {
<EnvironmentUI selectedEnv={props.selectExEnv} providers={props.providers} setExecutionContext={props.setExecutionContext} /> <EnvironmentUI selectedEnv={props.selectExEnv} providers={props.providers} setExecutionContext={props.setExecutionContext} />
<NetworkUI networkName={props.networkName} /> <NetworkUI networkName={props.networkName} />
<AccountUI <AccountUI
addFile={props.addFile}
personalMode={props.personalMode} personalMode={props.personalMode}
selectExEnv={props.selectExEnv} selectExEnv={props.selectExEnv}
accounts={props.accounts} accounts={props.accounts}

@ -141,11 +141,6 @@
.udapp_input { .udapp_input {
font-size: 10px !important; font-size: 10px !important;
} }
.udapp_noInstancesText {
font-style: italic;
text-align: left;
padding-left: 15px;
}
.udapp_pendingTxsText { .udapp_pendingTxsText {
font-style: italic; font-style: italic;
display: flex; display: flex;

@ -47,7 +47,8 @@ import {
updateSelectedContract, updateSelectedContract,
syncContracts, syncContracts,
isValidProxyAddress, isValidProxyAddress,
isValidProxyUpgrade isValidProxyUpgrade,
addFile
} from './actions' } from './actions'
import './css/run-tab.css' import './css/run-tab.css'
import { PublishToStorage } from '@remix-ui/publish-to-storage' import { PublishToStorage } from '@remix-ui/publish-to-storage'
@ -280,6 +281,7 @@ export function RunTabUI(props: RunTabProps) {
<div className="udapp_runTabView run-tab" id="runTabView" data-id="runTabView"> <div className="udapp_runTabView run-tab" id="runTabView" data-id="runTabView">
<div className="list-group pb-4 list-group-flush"> <div className="list-group pb-4 list-group-flush">
<SettingsUI <SettingsUI
addFile={addFile}
networkName={runTab.networkName} networkName={runTab.networkName}
personalMode={runTab.personalMode} personalMode={runTab.personalMode}
selectExEnv={runTab.selectExEnv} selectExEnv={runTab.selectExEnv}

@ -17,12 +17,11 @@ export class Blockchain extends Plugin<any, any> {
}; };
setupEvents(): void; setupEvents(): void;
getCurrentNetworkStatus(): { getCurrentNetworkStatus(): {
name: string;
id: string;
network?: { network?: {
name: string; name: string;
id: string; id: string;
}; };
error?: string;
}; };
setupProviders(): void; setupProviders(): void;
providers: any; providers: any;
@ -35,8 +34,8 @@ export class Blockchain extends Plugin<any, any> {
determineGasPrice(cb: any): void; determineGasPrice(cb: any): void;
getInputs(funABI: any): any; getInputs(funABI: any): any;
fromWei(value: any, doTypeConversion: any, unit: any): string; fromWei(value: any, doTypeConversion: any, unit: any): string;
toWei(value: any, unit: any): import("bn.js"); toWei(value: any, unit: any): string;
calculateFee(gas: any, gasPrice: any, unit: any): import("bn.js"); calculateFee(gas: any, gasPrice: any, unit: any): bigint;
determineGasFees(tx: any): (gasPrice: any, cb: any) => void; determineGasFees(tx: any): (gasPrice: any, cb: any) => void;
changeExecutionContext(context: any, confirmCb: any, infoCb: any, cb: any): Promise<any>; changeExecutionContext(context: any, confirmCb: any, infoCb: any, cb: any): Promise<any>;
detectNetwork(cb: any): void; detectNetwork(cb: any): void;
@ -58,7 +57,6 @@ export class Blockchain extends Plugin<any, any> {
removeProvider(name: any): void; removeProvider(name: any): void;
/** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */ /** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */
startListening(txlistener: any): void; startListening(txlistener: any): void;
resetEnvironment(): Promise<void>;
/** /**
* Create a VM Account * Create a VM Account
* @param {{privateKey: string, balance: string}} newAccount The new account to create * @param {{privateKey: string, balance: string}} newAccount The new account to create

@ -14,7 +14,6 @@ export class ExecutionContext {
txs: any; txs: any;
customWeb3: any; customWeb3: any;
init(config: any): void; init(config: any): void;
askPermission(): void;
getProvider(): any; getProvider(): any;
getCurrentFork(): string; getCurrentFork(): string;
isVM(): boolean; isVM(): boolean;

@ -6,7 +6,7 @@ import { SolcInput, SolcOutput } from '@openzeppelin/upgrades-core'
import { LayoutCompatibilityReport } from '@openzeppelin/upgrades-core/dist/storage/report' import { LayoutCompatibilityReport } from '@openzeppelin/upgrades-core/dist/storage/report'
export interface RunTabProps { export interface RunTabProps {
plugin: RunTab, plugin: RunTab,
initialState: RunTabState initialState?: RunTabState
} }
export interface Contract { export interface Contract {
@ -145,6 +145,7 @@ export interface SettingsProps {
isSuccessful: boolean, isSuccessful: boolean,
error: string error: string
}, },
addFile: (path: string, content: string) => void,
setExecutionContext: (executionContext: { context: string, fork: string }) => void, setExecutionContext: (executionContext: { context: string, fork: string }) => void,
createNewBlockchainAccount: (cbMessage: JSX.Element) => void, createNewBlockchainAccount: (cbMessage: JSX.Element) => void,
setPassphrase: (passphrase: string) => void, setPassphrase: (passphrase: string) => void,
@ -180,6 +181,7 @@ export interface AccountProps {
isSuccessful: boolean, isSuccessful: boolean,
error: string error: string
}, },
addFile: (path: string, content: string) => void,
setAccount: (account: string) => void, setAccount: (account: string) => void,
personalMode: boolean, personalMode: boolean,
createNewBlockchainAccount: (cbMessage: JSX.Element) => void, createNewBlockchainAccount: (cbMessage: JSX.Element) => void,

@ -1,11 +1,11 @@
export class RunTab extends ViewPlugin { import type { CompilerArtefacts } from '@remix-project/core-plugin'
constructor(blockchain: any, config: any, fileManager: any, editor: any, filePanel: any, compilersArtefacts: any, networkModule: any, mainView: any, fileProvider: any); export interface RunTab extends ViewPlugin {
// constructor(blockchain: Blockchain, config: any, fileManager: any, editor: any, filePanel: any, compilersArtefacts: CompilerArtefacts, networkModule: any, fileProvider: any, engine: any);
event: any; event: any;
config: any; config: any;
blockchain: Blockchain; blockchain: Blockchain;
fileManager: any; fileManager: any;
editor: any; editor: any;
logCallback: (msg: any) => void;
filePanel: any; filePanel: any;
compilersArtefacts: any; compilersArtefacts: any;
networkModule: any; networkModule: any;
@ -19,24 +19,9 @@ export class RunTab extends ViewPlugin {
sendTransaction(tx: any): any; sendTransaction(tx: any): any;
getAccounts(cb: any): any; getAccounts(cb: any): any;
pendingTransactionsCount(): any; pendingTransactionsCount(): any;
renderInstanceContainer(): void;
instanceContainer: any;
noInstancesText: any;
renderSettings(): void;
settingsUI: any;
renderDropdown(udappUI: any, fileManager: any, compilersArtefacts: any, config: any, editor: any, logCallback: any): void;
contractDropdownUI: any;
renderRecorder(udappUI: any, fileManager: any, config: any, logCallback: any): void;
recorderCount: any;
recorderInterface: any;
renderRecorderCard(): void;
recorderCard: any;
udappUI: any;
renderComponent(): void;
onReady(api: any): void; onReady(api: any): void;
onInitDone(): void; onInitDone(): void;
recorder: Recorder; recorder: Recorder;
// syncContracts(): void
} }
import { ViewPlugin } from "@remixproject/engine-web"; import { ViewPlugin } from "@remixproject/engine-web";
import { Blockchain } from "./blockchain"; import { Blockchain } from "./blockchain";

@ -10,8 +10,7 @@ import { fetchContractFromEtherscan, fetchContractFromBlockscout } from '@remix-
import JSZip from 'jszip' import JSZip from 'jszip'
import { Actions, FileTree } from '../types' import { Actions, FileTree } from '../types'
import IpfsHttpClient from 'ipfs-http-client' import IpfsHttpClient from 'ipfs-http-client'
import { AppModal } from '@remix-ui/app' import { AppModal, ModalTypes } from '@remix-ui/app'
import { MessageWrapper } from '../components/file-explorer'
export * from './events' export * from './events'
export * from './workspace' export * from './workspace'
@ -510,6 +509,32 @@ export const runScript = async (path: string) => {
}) })
} }
export const signTypedData = async (path: string) => {
const typedData = await plugin.call('fileManager', 'readFile', path)
const web3 = await plugin.call('blockchain', 'web3')
const settings = await plugin.call('udapp', 'getSettings')
let parsed
try {
parsed = JSON.parse(typedData)
} catch (err) {
dispatch(displayPopUp(`${path} isn't a valid JSON.`))
return
}
try {
const result = await web3.currentProvider.request({
method: 'eth_signTypedData_v4',
params: [settings.selectedAccount, parsed]
})
plugin.call('terminal', 'log', { type: 'log', value: `${path} signature using ${settings.selectedAccount} : ${result}` })
} catch (e) {
console.error(e)
plugin.call('terminal', 'log', { type: 'error', value: `error while signing ${path}: ${e.message}` })
dispatch(displayPopUp(e.message))
}
}
export const emitContextMenuEvent = async (cmd: customAction) => { export const emitContextMenuEvent = async (cmd: customAction) => {
await plugin.call(cmd.id, cmd.name, cmd) await plugin.call(cmd.id, cmd.name, cmd)
} }

@ -41,6 +41,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
downloadPath, downloadPath,
uploadFile, uploadFile,
publishManyFilesToGist, publishManyFilesToGist,
signTypedData,
...otherProps ...otherProps
} = props } = props
const contextMenuRef = useRef(null) const contextMenuRef = useRef(null)
@ -234,6 +235,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace']) _paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace'])
publishFolderToGist(path) publishFolderToGist(path)
break break
case 'Sign Typed Data':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'signTypedData'])
signTypedData(path)
break
default: default:
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', `${item.id}/${item.name}`]) _paq.push(['trackEvent', 'fileExplorer', 'contextMenu', `${item.id}/${item.name}`])
emit && emit({ ...item, path: [path]} as customAction) emit && emit({ ...item, path: [path]} as customAction)

@ -54,13 +54,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
placement: 'top', placement: 'top',
platforms: [appPlatformTypes.web, appPlatformTypes.desktop] platforms: [appPlatformTypes.web, appPlatformTypes.desktop]
}, },
{
action: 'connectToLocalFileSystem',
title: 'Connect to local filesystem using remixd',
icon: 'fa-solid fa-desktop',
placement: 'top',
platforms: [appPlatformTypes.web]
},
{ {
action: 'initializeWorkspaceAsGitRepo', action: 'initializeWorkspaceAsGitRepo',
title: 'Initialize workspace as a git repository', title: 'Initialize workspace as a git repository',
@ -154,7 +147,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
return ( return (
<CustomTooltip <CustomTooltip
placement={placement as Placement} placement={placement as Placement}
tooltipId="uploadFolderTooltip" tooltipId="initializeWorkspaceAsGitRepoTooltip"
tooltipClasses="text-nowrap" tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id={`filePanel.${action}`} defaultMessage={title} />} tooltipText={<FormattedMessage id={`filePanel.${action}`} defaultMessage={title} />}
key={`index-${action}-${placement}-${icon}`} key={`index-${action}-${placement}-${icon}`}
@ -162,7 +155,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
<label <label
id={action} id={action}
style={{ fontSize: '1.1rem', cursor: 'pointer' }} style={{ fontSize: '1.1rem', cursor: 'pointer' }}
data-id={'fileExplorerUploadFolder' + action} data-id={'fileExplorerInitializeWorkspaceAsGitRepo' + action}
className={icon + ' mx-1 remixui_menuItem'} className={icon + ' mx-1 remixui_menuItem'}
key={`index-${action}-${placement}-${icon}`} key={`index-${action}-${placement}-${icon}`}
onClick={() => { onClick={() => {
@ -195,9 +188,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
props.createNewFolder() props.createNewFolder()
} else if (action === 'publishToGist' || action == 'updateGist') { } else if (action === 'publishToGist' || action == 'updateGist') {
props.publishToGist() props.publishToGist()
} else if (action === 'connectToLocalFileSystem') {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.connectToLocalFileSystem()
} else if (action === 'importFromIpfs') { } else if (action === 'importFromIpfs') {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action]) _paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.importFromIpfs('Ipfs', 'ipfs hash', ['ipfs://QmQQfBMkpDgmxKzYaoAtqfaybzfgGm9b2LWYyT56Chv6xH'], 'ipfs://') props.importFromIpfs('Ipfs', 'ipfs hash', ['ipfs://QmQQfBMkpDgmxKzYaoAtqfaybzfgGm9b2LWYyT56Chv6xH'], 'ipfs://')

@ -615,7 +615,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
uploadFolder={uploadFolder} uploadFolder={uploadFolder}
importFromIpfs={props.importFromIpfs} importFromIpfs={props.importFromIpfs}
importFromHttps={props.importFromHttps} importFromHttps={props.importFromHttps}
connectToLocalFileSystem={() => props.connectToLocalFileSystem()}
handleGitInit={handleGitInit} handleGitInit={handleGitInit}
/> />
</div> </div>

@ -99,6 +99,17 @@ export function HamburgerMenu(props: HamburgerMenuProps) {
platforms={[appPlatformTypes.web]} platforms={[appPlatformTypes.web]}
></HamburgerMenuItem> ></HamburgerMenuItem>
<Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{ pointerEvents: 'none' }} /> <Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{ pointerEvents: 'none' }} />
<HamburgerMenuItem
kind="localFileSystem"
fa="far fa-desktop"
hideOption={hideWorkspaceOptions}
actionOnClick={() => {
props.handleRemixdWorkspace()
props.hideIconsMenu(!showIconsMenu)
}}
platforms={[appPlatformTypes.web]}
></HamburgerMenuItem>
<Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{ pointerEvents: 'none' }} />
<HamburgerMenuItem <HamburgerMenuItem
kind={selectedWorkspace.isGist ? "updateGist" : "publishToGist"} kind={selectedWorkspace.isGist ? "updateGist" : "publishToGist"}
fa="fab fa-github" fa="fab fa-github"
@ -140,17 +151,6 @@ export function HamburgerMenu(props: HamburgerMenuProps) {
}} }}
platforms={[appPlatformTypes.web]} platforms={[appPlatformTypes.web]}
></HamburgerMenuItem> ></HamburgerMenuItem>
<Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{ pointerEvents: 'none' }} />
<HamburgerMenuItem
kind="localFileSystem"
fa="far fa-desktop"
hideOption={hideWorkspaceOptions}
actionOnClick={() => {
props.handleRemixdWorkspace()
props.hideIconsMenu(!showIconsMenu)
}}
platforms={[appPlatformTypes.web]}
></HamburgerMenuItem>
</> </>
) )
} }

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

Loading…
Cancel
Save