diff --git a/apps/etherscan/.babelrc b/apps/etherscan/.babelrc new file mode 100644 index 0000000000..b1fc975456 --- /dev/null +++ b/apps/etherscan/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", { + "runtime": "automatic" + + } + ] + ], + "plugins": [ + + ] +} diff --git a/apps/etherscan/.browserslistrc b/apps/etherscan/.browserslistrc new file mode 100644 index 0000000000..f1d12df4fa --- /dev/null +++ b/apps/etherscan/.browserslistrc @@ -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'. \ No newline at end of file diff --git a/apps/etherscan/.eslintrc.json b/apps/etherscan/.eslintrc.json new file mode 100644 index 0000000000..a92d0f887a --- /dev/null +++ b/apps/etherscan/.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": {} + } + ] +} \ No newline at end of file diff --git a/apps/etherscan/src/app/App.css b/apps/etherscan/src/app/App.css new file mode 100644 index 0000000000..27083bfb90 --- /dev/null +++ b/apps/etherscan/src/app/App.css @@ -0,0 +1,7 @@ +body { + margin: 0; +} + +#root { + padding: 5px; +} \ No newline at end of file diff --git a/apps/etherscan/src/app/AppContext.tsx b/apps/etherscan/src/app/AppContext.tsx new file mode 100644 index 0000000000..52b0c1fda8 --- /dev/null +++ b/apps/etherscan/src/app/AppContext.tsx @@ -0,0 +1,24 @@ +import React from "react" +import { PluginClient } from "@remixproject/plugin" + +import { Receipt, ThemeType } from "./types" + +export const AppContext = React.createContext({ + apiKey: "", + setAPIKey: (value: string) => { + console.log("Set API Key from Context") + }, + clientInstance: {} as PluginClient, + receipts: [] as Receipt[], + setReceipts: (receipts: Receipt[]) => { + console.log("Calling Set Receipts") + }, + contracts: [] as string[], + setContracts: (contracts: string[]) => { + console.log("Calling Set Contract Names") + }, + themeType: "dark" as ThemeType, + setThemeType: (themeType: ThemeType) => { + console.log("Calling Set Theme Type") + }, +}) diff --git a/apps/etherscan/src/app/app.tsx b/apps/etherscan/src/app/app.tsx new file mode 100644 index 0000000000..d46b66fccc --- /dev/null +++ b/apps/etherscan/src/app/app.tsx @@ -0,0 +1,157 @@ +import React, { useState, useEffect, useRef } from "react" + +import { + CompilationFileSources, + CompilationResult, +} from "@remixproject/plugin-api" + +import { PluginClient } from "@remixproject/plugin"; +import { createClient } from "@remixproject/plugin-webview"; + +import { AppContext } from "./AppContext" +import { DisplayRoutes } from "./routes" + +import { useLocalStorage } from "./hooks/useLocalStorage" + +import { getReceiptStatus, getEtherScanApi, getNetworkName } from "./utils" +import { Receipt, ThemeType } from "./types" + +import "./App.css" + +export const getNewContractNames = (compilationResult: CompilationResult) => { + const compiledContracts = compilationResult.contracts + let result: string[] = [] + + for (const file of Object.keys(compiledContracts)) { + const newContractNames = Object.keys(compiledContracts[file]) + result = [...result, ...newContractNames] + } + + return result +} + +const App = () => { + const [apiKey, setAPIKey] = useLocalStorage("apiKey", "") + const [clientInstance, setClientInstance] = useState(undefined as any) + const [receipts, setReceipts] = useLocalStorage("receipts", []) + const [contracts, setContracts] = useState([] as string[]) + const [themeType, setThemeType] = useState("dark" as ThemeType) + + const clientInstanceRef = useRef(clientInstance) + clientInstanceRef.current = clientInstance + const contractsRef = useRef(contracts) + contractsRef.current = contracts + + useEffect(() => { + console.log("Remix Etherscan loading...") + const client = new PluginClient() + createClient(client) + const loadClient = async () => { + await client.onload() + setClientInstance(client) + console.log("Remix Etherscan Plugin has been loaded") + + client.on("solidity", + "compilationFinished", + ( + fileName: string, + source: CompilationFileSources, + languageVersion: string, + data: CompilationResult + ) => { + console.log("New compilation received") + const newContractsNames = getNewContractNames(data) + + const newContractsToSave: string[] = [ + ...contractsRef.current, + ...newContractsNames, + ] + + const uniqueContracts: string[] = [...new Set(newContractsToSave)] + + setContracts(uniqueContracts) + } + ) + + //const currentTheme = await client.call("theme", "currentTheme") + //setThemeType(currentTheme.quality) + //client.on("theme", "themeChanged", (theme) => { + // setThemeType(theme.quality) + //}) + } + + loadClient() + }, []) + + useEffect(() => { + if (!clientInstance) { + return + } + + const receiptsNotVerified: Receipt[] = receipts.filter((item: Receipt) => { + return item.status !== "Verified" + }) + + if (receiptsNotVerified.length > 0) { + let timer1 = setInterval(() => { + for (const item in receiptsNotVerified) { + + } + receiptsNotVerified.forEach(async (item) => { + if (!clientInstanceRef.current) { + return {} + } + const network = await getNetworkName(clientInstanceRef.current) + if (network === "vm") { + return {} + } + const status = await getReceiptStatus( + item.guid, + apiKey, + getEtherScanApi(network) + ) + if (status === "Pass - Verified") { + const newReceipts = receipts.map((currentReceipt: Receipt) => { + if (currentReceipt.guid === item.guid) { + return { + ...currentReceipt, + status: "Verified", + } + } + return currentReceipt + }) + + clearInterval(timer1) + + setReceipts(newReceipts) + + return () => { + clearInterval(timer1) + } + } + return {} + }) + }, 5000) + } + }, [receipts, clientInstance, apiKey, setReceipts]) + + return ( + + + + ) +} + +export default App diff --git a/apps/etherscan/src/app/components/HeaderWithSettings.tsx b/apps/etherscan/src/app/components/HeaderWithSettings.tsx new file mode 100644 index 0000000000..f0ba96ae6b --- /dev/null +++ b/apps/etherscan/src/app/components/HeaderWithSettings.tsx @@ -0,0 +1,159 @@ +import React from "react" + +import { NavLink } from "react-router-dom" +import { AppContext } from "../AppContext" +import { ThemeType } from "../types" + +interface Props { + title?: string + showBackButton?: boolean + from: string +} + +interface IconProps { + from: string + themeType: ThemeType +} + +const HomeIcon: React.FC = ({ from, themeType }: IconProps) => { + return ( + { + return { + ...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" } + } + }} + > + + + + + + ) +} + +const SettingsIcon: React.FC = ({ from, themeType }: IconProps) => { + return ( + { + return { + ...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" } + } + }} + > + + + + + + + ) +} + +const getStyleFilterIcon = (themeType: ThemeType) => { + const invert = themeType === "dark" ? 1 : 0 + const brightness = themeType === "dark" ? "150" : "0" // should be >100 for icons with color + return { + filter: `invert(${invert}) grayscale(1) brightness(${brightness}%)`, + } +} + +const ReceiptsIcon: React.FC = ({ from, themeType }: IconProps) => { + return ( + { + return { + ...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" } + } + }} + > + + + + + + + ) +} +export const HeaderWithSettings: React.FC = ({ + title = "", + showBackButton = false, + from, +}) => { + return ( + + {({ themeType }) => ( + + {title} + + + + + + + + + )} + + ) +} diff --git a/apps/etherscan/src/app/components/SubmitButton.tsx b/apps/etherscan/src/app/components/SubmitButton.tsx new file mode 100644 index 0000000000..7fe0fa4f05 --- /dev/null +++ b/apps/etherscan/src/app/components/SubmitButton.tsx @@ -0,0 +1,34 @@ +import React from "react" + +interface Props { + text: string + isSubmitting?: boolean +} + +export const SubmitButton: React.FC = ({ + text, + isSubmitting = false, +}) => { + return ( + + {!isSubmitting && text} + + {isSubmitting && ( + + + Verifying...Please wait + + )} + + ) +} diff --git a/apps/etherscan/src/app/components/index.ts b/apps/etherscan/src/app/components/index.ts new file mode 100644 index 0000000000..c52e3712f0 --- /dev/null +++ b/apps/etherscan/src/app/components/index.ts @@ -0,0 +1,2 @@ +export { HeaderWithSettings } from "./HeaderWithSettings" +export { SubmitButton } from "./SubmitButton" diff --git a/apps/etherscan/src/app/hooks/useLocalStorage.tsx b/apps/etherscan/src/app/hooks/useLocalStorage.tsx new file mode 100644 index 0000000000..cca4a83a72 --- /dev/null +++ b/apps/etherscan/src/app/hooks/useLocalStorage.tsx @@ -0,0 +1,37 @@ +import { useState } from "react" + +export function useLocalStorage(key: string, initialValue: any) { + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(() => { + try { + // Get from local storage by key + const item = window.localStorage.getItem(key) + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue + } catch (error) { + // If error also return initialValue + console.log(error) + return initialValue + } + }) + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = (value: any) => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = + value instanceof Function ? value(storedValue) : value + // Save state + setStoredValue(valueToStore) + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(valueToStore)) + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error) + } + } + + return [storedValue, setValue] +} diff --git a/apps/etherscan/src/app/layouts/Default.tsx b/apps/etherscan/src/app/layouts/Default.tsx new file mode 100644 index 0000000000..93dd89b851 --- /dev/null +++ b/apps/etherscan/src/app/layouts/Default.tsx @@ -0,0 +1,19 @@ +import React, { PropsWithChildren } from "react" + +import { HeaderWithSettings } from "../components" + +interface Props { + from: string +} + +export const DefaultLayout: React.FC> = ({ + children, + from, +}) => { + return ( + + + {children} + + ) +} diff --git a/apps/etherscan/src/app/layouts/index.ts b/apps/etherscan/src/app/layouts/index.ts new file mode 100644 index 0000000000..9b8e6166d5 --- /dev/null +++ b/apps/etherscan/src/app/layouts/index.ts @@ -0,0 +1 @@ +export { DefaultLayout } from "./Default" diff --git a/apps/etherscan/src/app/logo.svg b/apps/etherscan/src/app/logo.svg new file mode 100644 index 0000000000..8fa84ab509 --- /dev/null +++ b/apps/etherscan/src/app/logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/etherscan/src/app/routes.tsx b/apps/etherscan/src/app/routes.tsx new file mode 100644 index 0000000000..68fd76318f --- /dev/null +++ b/apps/etherscan/src/app/routes.tsx @@ -0,0 +1,51 @@ +import React from "react" +import { + BrowserRouter as Router, + Route, + Routes, + RouteProps, +} from "react-router-dom" + +import { ErrorView, HomeView, ReceiptsView, CaptureKeyView } from "./views" +import { DefaultLayout } from "./layouts" + +interface Props extends RouteProps { + component: any // TODO: new (props: any) => React.Component + from: string +} + +const RouteWithHeader = ({ component: Component, ...rest }: Props) => { + return ( + + + + + + ) +} + +export const DisplayRoutes = () => ( + + + + + } /> + } /> + + + } /> + + + } /> + + +) diff --git a/apps/etherscan/src/app/star.svg b/apps/etherscan/src/app/star.svg new file mode 100644 index 0000000000..901053d385 --- /dev/null +++ b/apps/etherscan/src/app/star.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/apps/etherscan/src/app/types/Receipt.ts b/apps/etherscan/src/app/types/Receipt.ts new file mode 100644 index 0000000000..57de417094 --- /dev/null +++ b/apps/etherscan/src/app/types/Receipt.ts @@ -0,0 +1,6 @@ +export type ReceiptStatus = "Verified" | "Queue" + +export interface Receipt { + guid: string + status: ReceiptStatus +} diff --git a/apps/etherscan/src/app/types/ThemeType.ts b/apps/etherscan/src/app/types/ThemeType.ts new file mode 100644 index 0000000000..13b3710cd0 --- /dev/null +++ b/apps/etherscan/src/app/types/ThemeType.ts @@ -0,0 +1 @@ +export type ThemeType = "dark" | "light" diff --git a/apps/etherscan/src/app/types/index.ts b/apps/etherscan/src/app/types/index.ts new file mode 100644 index 0000000000..1a8733d6ff --- /dev/null +++ b/apps/etherscan/src/app/types/index.ts @@ -0,0 +1,2 @@ +export * from "./Receipt" +export * from "./ThemeType" diff --git a/apps/etherscan/src/app/utils/index.ts b/apps/etherscan/src/app/utils/index.ts new file mode 100644 index 0000000000..b23d52e6e0 --- /dev/null +++ b/apps/etherscan/src/app/utils/index.ts @@ -0,0 +1 @@ +export * from "./utilities" diff --git a/apps/etherscan/src/app/utils/utilities.ts b/apps/etherscan/src/app/utils/utilities.ts new file mode 100644 index 0000000000..17ef6fc8a0 --- /dev/null +++ b/apps/etherscan/src/app/utils/utilities.ts @@ -0,0 +1,34 @@ +import { PluginClient } from "@remixproject/plugin" +import axios from 'axios' +type RemixClient = PluginClient + +export const getEtherScanApi = (network: string) => { + return network === "main" + ? `https://api.etherscan.io/api` + : `https://api-${network}.etherscan.io/api` +} + +export const getNetworkName = async (client: RemixClient) => { + const network = await client.call("network", "detectNetwork") + if (!network) { + throw new Error("no known network to verify against") + } + const name = network.name!.toLowerCase() + // TODO : remove that when https://github.com/ethereum/remix-ide/issues/2017 is fixe + return name === "görli" ? "goerli" : name +} + +export const getReceiptStatus = async ( + receiptGuid: string, + apiKey: string, + etherscanApi: string +) => { + const params = `guid=${receiptGuid}&module=contract&action=checkverifystatus&apiKey=${apiKey}` + try { + const response = await axios.get(`${etherscanApi}?${params}`) + const { result } = response.data + return result + } catch (error) { + console.log("Error", error) + } +} diff --git a/apps/etherscan/src/app/views/CaptureKeyView.tsx b/apps/etherscan/src/app/views/CaptureKeyView.tsx new file mode 100644 index 0000000000..2bac179293 --- /dev/null +++ b/apps/etherscan/src/app/views/CaptureKeyView.tsx @@ -0,0 +1,59 @@ +import React from "react" + +import { Formik, ErrorMessage, Field } from "formik" +import { useNavigate, useLocation } from "react-router-dom" + +import { AppContext } from "../AppContext" +import { SubmitButton } from "../components" + +export const CaptureKeyView: React.FC = () => { + const location = useLocation() + const navigate = useNavigate() + return ( + + {({ apiKey, setAPIKey }) => ( + { + const errors = {} as any + if (!values.apiKey) { + errors.apiKey = "Required" + } + return errors + }} + onSubmit={(values) => { + setAPIKey(values.apiKey) + navigate((location.state as any).from) + }} + > + {({ errors, touched, handleSubmit }) => ( + + + Please Enter your API key + + + + + + + + + )} + + )} + + ) +} diff --git a/apps/etherscan/src/app/views/ErrorView.tsx b/apps/etherscan/src/app/views/ErrorView.tsx new file mode 100644 index 0000000000..5c9e623231 --- /dev/null +++ b/apps/etherscan/src/app/views/ErrorView.tsx @@ -0,0 +1,31 @@ +import React from "react" + +export const ErrorView: React.FC = () => { + return ( + + + Sorry, something unexpected happened. + + Please raise an issue:{" "} + + Here + + + + ) +} diff --git a/apps/etherscan/src/app/views/HomeView.tsx b/apps/etherscan/src/app/views/HomeView.tsx new file mode 100644 index 0000000000..0b9d2e3820 --- /dev/null +++ b/apps/etherscan/src/app/views/HomeView.tsx @@ -0,0 +1,36 @@ +import React from "react" + +import { Navigate } from "react-router-dom" + +import { AppContext } from "../AppContext" +import { Receipt } from "../types" + +import { VerifyView } from "./VerifyView" + +export const HomeView: React.FC = () => { + // const [hasError, setHasError] = useState(false) + return ( + + {({ apiKey, clientInstance, setReceipts, receipts, contracts }) => + !apiKey ? ( + + ) : ( + { + const newReceipts = [...receipts, receipt] + + setReceipts(newReceipts) + }} + /> + ) + } + + ) +} diff --git a/apps/etherscan/src/app/views/ReceiptsView.tsx b/apps/etherscan/src/app/views/ReceiptsView.tsx new file mode 100644 index 0000000000..ad13fbee33 --- /dev/null +++ b/apps/etherscan/src/app/views/ReceiptsView.tsx @@ -0,0 +1,135 @@ +import React, { useState } from "react" + +import { Formik, ErrorMessage, Field } from "formik" +import { getEtherScanApi, getNetworkName, getReceiptStatus } from "../utils" +import { Receipt } from "../types" +import { AppContext } from "../AppContext" +import { SubmitButton } from "../components" +import { Navigate } from "react-router-dom" + +interface FormValues { + receiptGuid: string +} + +export const ReceiptsView: React.FC = () => { + const [results, setResults] = useState("") + const onGetReceiptStatus = async ( + values: FormValues, + clientInstance: any, + apiKey: string + ) => { + try { + const network = await getNetworkName(clientInstance) + if (network === "vm") { + setResults("Cannot verify in the selected network") + return + } + const etherscanApi = getEtherScanApi(network) + const result = await getReceiptStatus( + values.receiptGuid, + apiKey, + etherscanApi + ) + setResults(result) + } catch (error: any) { + setResults(error.message) + } + } + + return ( + + {({ apiKey, clientInstance, receipts }) => + !apiKey ? ( + + ) : ( + + { + const errors = {} as any + if (!values.receiptGuid) { + errors.receiptGuid = "Required" + } + return errors + }} + onSubmit={(values) => + onGetReceiptStatus(values, clientInstance, apiKey) + } + > + {({ errors, touched, handleSubmit }) => ( + + + Get your Receipt GUID status + Receipt GUID + + + + + + + )} + + + + + + + ) + } + + ) +} + +const ReceiptsTable: React.FC<{ receipts: Receipt[] }> = ({ receipts }) => { + return ( + + Receipts + + + + Guid + Status + + + + {receipts && + receipts.length > 0 && + receipts.map((item: Receipt, index) => { + return ( + + {item.guid} + {item.status} + + ) + })} + + + + ) +} diff --git a/apps/etherscan/src/app/views/VerifyView.tsx b/apps/etherscan/src/app/views/VerifyView.tsx new file mode 100644 index 0000000000..a57b23a8cb --- /dev/null +++ b/apps/etherscan/src/app/views/VerifyView.tsx @@ -0,0 +1,302 @@ +import React, { useState } from "react" + +import { + PluginClient, +} from "@remixproject/plugin" +import { Formik, ErrorMessage, Field } from "formik" + +import { getNetworkName, getEtherScanApi, getReceiptStatus } from "../utils" +import { SubmitButton } from "../components" +import { Receipt } from "../types" +import { CompilationResult } from "@remixproject/plugin-api" +import axios from 'axios' + +interface Props { + client: PluginClient + apiKey: string + onVerifiedContract: (receipt: Receipt) => void + contracts: string[] +} + +interface FormValues { + contractName: string + contractArguments: string + contractAddress: string +} + +export const getContractFileName = ( + compilationResult: CompilationResult, + contractName: string +) => { + const compiledContracts = compilationResult.contracts + let fileName = "" + + for (const file of Object.keys(compiledContracts)) { + for (const contract of Object.keys(compiledContracts[file])) { + if (contract === contractName) { + fileName = file + break + } + } + } + return fileName +} + +export const getContractMetadata = ( + compilationResult: CompilationResult, + contractName: string +) => { + const compiledContracts = compilationResult.contracts + let contractMetadata = "" + + for (const file of Object.keys(compiledContracts)) { + for (const contract of Object.keys(compiledContracts[file])) { + if (contract === contractName) { + contractMetadata = compiledContracts[file][contract].metadata + if (contractMetadata) { + break + } + } + } + } + return contractMetadata +} + +export const VerifyView: React.FC = ({ + apiKey, + client, + contracts, + onVerifiedContract, +}) => { + const [results, setResults] = useState("") + + const onVerifyContract = async (values: FormValues) => { + const compilationResult = (await client.call( + "solidity", + "getCompilationResult" + )) as any + + if (!compilationResult) { + throw new Error("no compilation result available") + } + + const contractArguments = values.contractArguments.replace("0x", "") + + const verify = async ( + apiKeyParam: string, + contractAddress: string, + contractArgumentsParam: string, + contractName: string, + compilationResultParam: any + ) => { + const network = await getNetworkName(client) + if (network === "vm") { + return "Cannot verify in the selected network" + } + const etherscanApi = getEtherScanApi(network) + + try { + const contractMetadata = getContractMetadata( + compilationResultParam.data, + contractName + ) + + if (!contractMetadata) { + return "Please recompile contract" + } + + const contractMetadataParsed = JSON.parse(contractMetadata) + + const fileName = getContractFileName( + compilationResultParam.data, + contractName + ) + + const jsonInput = { + language: 'Solidity', + sources: compilationResultParam.source.sources, + settings: { + optimizer: { + enabled: contractMetadataParsed.settings.optimizer.enabled, + runs: contractMetadataParsed.settings.optimizer.runs + } + } + } + + const data: { [key: string]: string | any } = { + apikey: apiKeyParam, // A valid API-Key is required + module: "contract", // Do not change + action: "verifysourcecode", // Do not change + codeformat: "solidity-standard-json-input", + contractaddress: contractAddress, // Contract Address starts with 0x... + sourceCode: JSON.stringify(jsonInput), + contractname: fileName + ':' + contractName, + compilerversion: `v${contractMetadataParsed.compiler.version}`, // see http://etherscan.io/solcversions for list of support versions + constructorArguements: contractArgumentsParam, // if applicable + } + + const body = new FormData() + Object.keys(data).forEach((key) => body.append(key, data[key])) + + client.emit("statusChanged", { + key: "loading", + type: "info", + title: "Verifying ...", + }) + const response = await axios.post(etherscanApi, body) + const { message, result, status } = await response.data + + if (message === "OK" && status === "1") { + resetAfter10Seconds() + const receiptStatus = await getReceiptStatus( + result, + apiKey, + etherscanApi + ) + + onVerifiedContract({ + guid: result, + status: receiptStatus, + }) + return `Contract verified correctly Receipt GUID ${result}` + } + if (message === "NOTOK") { + client.emit("statusChanged", { + key: "failed", + type: "error", + title: result, + }) + resetAfter10Seconds() + } + return result + } catch (error) { + console.log("Error, something wrong happened", error) + setResults("Something wrong happened, try again") + } + } + + const resetAfter10Seconds = () => { + setTimeout(() => { + client.emit("statusChanged", { key: "none" }) + setResults("") + }, 10000) + } + + const verificationResult = await verify( + apiKey, + values.contractAddress, + contractArguments, + values.contractName, + compilationResult + ) + + setResults(verificationResult) + } + + return ( + + { + const errors = {} as any + if (!values.contractName) { + errors.contractName = "Required" + } + if (!values.contractAddress) { + errors.contractAddress = "Required" + } + if (values.contractAddress.trim() === "") { + errors.contractAddress = "Please enter a valid contract address" + } + return errors + }} + onSubmit={(values) => onVerifyContract(values)} + > + {({ errors, touched, handleSubmit, isSubmitting }) => ( + + + Verify your smart contracts + Contract + + + Select a contract + + {contracts.map((item) => ( + + {item} + + ))} + + + + + + Constructor Arguments + + + + + + Contract Address + + + + + + + )} + + + + + {/* + View Receipts + */} + + ) +} diff --git a/apps/etherscan/src/app/views/index.ts b/apps/etherscan/src/app/views/index.ts new file mode 100644 index 0000000000..c483228ece --- /dev/null +++ b/apps/etherscan/src/app/views/index.ts @@ -0,0 +1,4 @@ +export { HomeView } from "./HomeView" +export { ErrorView } from "./ErrorView" +export { ReceiptsView } from "./ReceiptsView" +export { CaptureKeyView } from "./CaptureKeyView" diff --git a/apps/etherscan/src/assets/.gitkeep b/apps/etherscan/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/etherscan/src/environments/environment.prod.ts b/apps/etherscan/src/environments/environment.prod.ts new file mode 100644 index 0000000000..3612073bc3 --- /dev/null +++ b/apps/etherscan/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/apps/etherscan/src/environments/environment.ts b/apps/etherscan/src/environments/environment.ts new file mode 100644 index 0000000000..d9370e924b --- /dev/null +++ b/apps/etherscan/src/environments/environment.ts @@ -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 +}; diff --git a/apps/etherscan/src/favicon.ico b/apps/etherscan/src/favicon.ico new file mode 100644 index 0000000000..317ebcb233 Binary files /dev/null and b/apps/etherscan/src/favicon.ico differ diff --git a/apps/etherscan/src/index.html b/apps/etherscan/src/index.html new file mode 100644 index 0000000000..b58279cb25 --- /dev/null +++ b/apps/etherscan/src/index.html @@ -0,0 +1,14 @@ + + + + + Etherscan + + + + + + + + + diff --git a/apps/etherscan/src/main.tsx b/apps/etherscan/src/main.tsx new file mode 100644 index 0000000000..353ad43f6d --- /dev/null +++ b/apps/etherscan/src/main.tsx @@ -0,0 +1,7 @@ +import { StrictMode } from 'react'; +import * as ReactDOM from 'react-dom'; + + +import App from './app/app'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/apps/etherscan/src/polyfills.ts b/apps/etherscan/src/polyfills.ts new file mode 100644 index 0000000000..2adf3d05b6 --- /dev/null +++ b/apps/etherscan/src/polyfills.ts @@ -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'; diff --git a/apps/etherscan/src/styles.css b/apps/etherscan/src/styles.css new file mode 100644 index 0000000000..90d4ee0072 --- /dev/null +++ b/apps/etherscan/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/etherscan/tsconfig.app.json b/apps/etherscan/tsconfig.app.json new file mode 100644 index 0000000000..62d6d52c3d --- /dev/null +++ b/apps/etherscan/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "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", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/apps/etherscan/tsconfig.json b/apps/etherscan/tsconfig.json new file mode 100644 index 0000000000..a3e71f89f3 --- /dev/null +++ b/apps/etherscan/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} \ No newline at end of file diff --git a/nx.json b/nx.json index 2afa03ec50..e5fa51dd58 100644 --- a/nx.json +++ b/nx.json @@ -198,6 +198,9 @@ }, "vyper": { "tags": [] + }, + "etherscan": { + "tags": [] } }, "targetDependencies": { diff --git a/package.json b/package.json index 06b9c286cf..11c4897478 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,7 @@ "file-path-filter": "^3.0.2", "file-saver": "^2.0.5", "form-data": "^4.0.0", + "formik": "^2.2.9", "fs-extra": "^3.0.1", "html-react-parser": "^1.3.0", "http-server": "^0.11.1", @@ -201,6 +202,7 @@ "react-dom": "^17.0.2", "react-draggable": "^4.4.4", "react-json-view": "^1.21.3", + "react-router-dom": "^6.3.0", "react-tabs": "^3.2.2", "regenerator-runtime": "0.13.7", "rss-parser": "^3.12.0", diff --git a/workspace.json b/workspace.json index a2eca1cee7..df7e8f9d28 100644 --- a/workspace.json +++ b/workspace.json @@ -1566,7 +1566,88 @@ "linter": "eslint", "config": "apps/vyper/.eslintrc", "files": [ - "apps/vyper/src/**/*.js", "apps/vyper/src/**/*.ts" + "apps/vyper/src/**/*.js", + "apps/vyper/src/**/*.ts" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "etherscan": { + "root": "apps/etherscan", + "sourceRoot": "apps/etherscan/src", + "projectType": "application", + "architect": { + "build": { + "builder": "@nrwl/web:build", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/apps/etherscan", + "index": "apps/etherscan/src/index.html", + "main": "apps/etherscan/src/main.tsx", + "polyfills": "apps/etherscan/src/polyfills.ts", + "tsConfig": "apps/etherscan/tsconfig.app.json", + "assets": [ + "apps/etherscan/src/favicon.ico", + "apps/etherscan/src/assets" + ], + "styles": [ + "apps/etherscan/src/styles.css" + ], + "scripts": [], + "webpackConfig": "@nrwl/react/plugins/webpack" + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "apps/etherscan/src/environments/environment.ts", + "with": "apps/etherscan/src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + } + ] + } + } + }, + "serve": { + "builder": "@nrwl/web:dev-server", + "options": { + "buildTarget": "etherscan:build", + "port": 5003 + }, + "configurations": { + "production": { + "buildTarget": "etherscan:build:production", + "hmr": false + } + } + }, + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "config": "apps/vyper/.eslintrc", + "files": [ + "apps/vyper/src/**/*.js", + "apps/vyper/src/**/*.ts" ], "exclude": [ "**/node_modules/**" diff --git a/yarn.lock b/yarn.lock index bba528ba28..cd49915370 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1622,6 +1622,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.7.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@7.0.0-beta.53": version "7.0.0-beta.53" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.53.tgz#3322290900d0b187b0a7174381e1f3bb71050d2e" @@ -9136,6 +9143,11 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -11250,6 +11262,19 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formik@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" + integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA== + dependencies: + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^1.10.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -12331,6 +12356,13 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" +history@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -15292,6 +15324,11 @@ lockfile@~1.0.3: resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79" integrity sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k= +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash._arraycopy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" @@ -19584,6 +19621,11 @@ react-draggable@^4.4.4: clsx "^1.1.1" prop-types "^15.6.0" +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -19645,6 +19687,21 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== +react-router-dom@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" + integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== + dependencies: + history "^5.2.0" + react-router "6.3.0" + +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react-tabs@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.2.2.tgz#07bdc3cdb17bdffedd02627f32a93cd4b3d6e4d0" @@ -22487,6 +22544,11 @@ tiny-invariant@^1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmp@0.0.33, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"