commit
cf4f397dca
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"presets": [ |
||||||
|
[ |
||||||
|
"@nrwl/react/babel", { |
||||||
|
"runtime": "automatic" |
||||||
|
|
||||||
|
} |
||||||
|
] |
||||||
|
], |
||||||
|
"plugins": [ |
||||||
|
|
||||||
|
] |
||||||
|
} |
@ -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,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,7 @@ |
|||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
#root { |
||||||
|
padding: 5px; |
||||||
|
} |
@ -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") |
||||||
|
}, |
||||||
|
}) |
@ -0,0 +1,153 @@ |
|||||||
|
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(() => { |
||||||
|
const client = new PluginClient() |
||||||
|
createClient(client) |
||||||
|
const loadClient = async () => { |
||||||
|
await client.onload() |
||||||
|
setClientInstance(client) |
||||||
|
client.on("solidity", |
||||||
|
"compilationFinished", |
||||||
|
( |
||||||
|
fileName: string, |
||||||
|
source: CompilationFileSources, |
||||||
|
languageVersion: string, |
||||||
|
data: CompilationResult |
||||||
|
) => { |
||||||
|
const newContractsNames = getNewContractNames(data) |
||||||
|
|
||||||
|
const newContractsToSave: string[] = [ |
||||||
|
...contractsRef.current, |
||||||
|
...newContractsNames, |
||||||
|
] |
||||||
|
|
||||||
|
const uniqueContracts: string[] = [...new Set(newContractsToSave)] |
||||||
|
|
||||||
|
setContracts(uniqueContracts) |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
//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 ( |
||||||
|
<AppContext.Provider |
||||||
|
value={{ |
||||||
|
apiKey, |
||||||
|
setAPIKey, |
||||||
|
clientInstance, |
||||||
|
receipts, |
||||||
|
setReceipts, |
||||||
|
contracts, |
||||||
|
setContracts, |
||||||
|
themeType, |
||||||
|
setThemeType, |
||||||
|
}} |
||||||
|
> |
||||||
|
<DisplayRoutes /> |
||||||
|
</AppContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default App |
@ -0,0 +1,160 @@ |
|||||||
|
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<IconProps> = ({ from, themeType }: IconProps) => { |
||||||
|
return ( |
||||||
|
<NavLink |
||||||
|
data-id="home" |
||||||
|
data-toggle="tooltip" |
||||||
|
data-placement="top" |
||||||
|
title="Home" |
||||||
|
to={{ |
||||||
|
pathname: "/" |
||||||
|
}} |
||||||
|
state={ from } |
||||||
|
style={isActive => { |
||||||
|
return { |
||||||
|
...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" } |
||||||
|
} |
||||||
|
}} |
||||||
|
> |
||||||
|
<svg |
||||||
|
style={{ filter: "invert(0.5)" }} |
||||||
|
width="1em" |
||||||
|
height="1em" |
||||||
|
viewBox="0 0 16 16" |
||||||
|
className="bi bi-house-door-fill" |
||||||
|
fill="currentColor" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path d="M6.5 10.995V14.5a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .146-.354l6-6a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 .146.354v7a.5.5 0 0 1-.5.5h-4a.5.5 0 0 1-.5-.5V11c0-.25-.25-.5-.5-.5H7c-.25 0-.5.25-.5.495z" /> |
||||||
|
<path |
||||||
|
fillRule="evenodd" |
||||||
|
d="M13 2.5V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</NavLink> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const SettingsIcon: React.FC<IconProps> = ({ from, themeType }: IconProps) => { |
||||||
|
return ( |
||||||
|
<NavLink |
||||||
|
data-toggle="tooltip" |
||||||
|
data-placement="top" |
||||||
|
title="Settings" |
||||||
|
to={{ |
||||||
|
pathname: "/settings", |
||||||
|
}} |
||||||
|
state= {from} |
||||||
|
style={isActive => { |
||||||
|
return { |
||||||
|
...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" } |
||||||
|
} |
||||||
|
}} |
||||||
|
> |
||||||
|
<svg |
||||||
|
style={{ filter: "invert(0.5)" }} |
||||||
|
width="1em" |
||||||
|
height="1em" |
||||||
|
viewBox="0 0 16 16" |
||||||
|
className="bi bi-gear-fill" |
||||||
|
fill="currentColor" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path |
||||||
|
fillRule="evenodd" |
||||||
|
d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 0 0-5.86 2.929 2.929 0 0 0 0 5.858z" |
||||||
|
/> |
||||||
|
|
||||||
|
<path |
||||||
|
fillRule="evenodd" |
||||||
|
d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 0 0-5.86 2.929 2.929 0 0 0 0 5.858z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</NavLink> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const getStyleFilterIcon = (themeType: ThemeType) => { |
||||||
|
const invert = themeType === "dark" ? 1 : 0 |
||||||
|
const brightness = themeType === "dark" ? "150" : "0" // should be >100 for icons with color
|
||||||
|
return { |
||||||
|
filter: `invert(${invert}) grayscale(1) brightness(${brightness}%)`, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const ReceiptsIcon: React.FC<IconProps> = ({ from, themeType }: IconProps) => { |
||||||
|
return ( |
||||||
|
<NavLink
|
||||||
|
data-toggle="tooltip" |
||||||
|
data-placement="top" |
||||||
|
title="Receipts" |
||||||
|
to={{ |
||||||
|
pathname: "/receipts", |
||||||
|
}} |
||||||
|
state= { from } |
||||||
|
style={isActive => { |
||||||
|
return { |
||||||
|
...(isActive ? getStyleFilterIcon(themeType) : {}), ...{ marginRight: "0.4em" } |
||||||
|
} |
||||||
|
}} |
||||||
|
> |
||||||
|
<svg |
||||||
|
style={{ filter: "invert(0.5)" }} |
||||||
|
width="1em" |
||||||
|
height="1em" |
||||||
|
viewBox="0 0 16 16" |
||||||
|
className="bi bi-receipt" |
||||||
|
fill="currentColor" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path |
||||||
|
fillRule="evenodd" |
||||||
|
d="M1.92.506a.5.5 0 0 1 .434.14L3 1.293l.646-.647a.5.5 0 0 1 .708 0L5 1.293l.646-.647a.5.5 0 0 1 .708 0L7 1.293l.646-.647a.5.5 0 0 1 .708 0L9 1.293l.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .801.13l.5 1A.5.5 0 0 1 15 2v12a.5.5 0 0 1-.053.224l-.5 1a.5.5 0 0 1-.8.13L13 14.707l-.646.647a.5.5 0 0 1-.708 0L11 14.707l-.646.647a.5.5 0 0 1-.708 0L9 14.707l-.646.647a.5.5 0 0 1-.708 0L7 14.707l-.646.647a.5.5 0 0 1-.708 0L5 14.707l-.646.647a.5.5 0 0 1-.708 0L3 14.707l-.646.647a.5.5 0 0 1-.801-.13l-.5-1A.5.5 0 0 1 1 14V2a.5.5 0 0 1 .053-.224l.5-1a.5.5 0 0 1 .367-.27zm.217 1.338L2 2.118v11.764l.137.274.51-.51a.5.5 0 0 1 .707 0l.646.647.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.646.646.646-.646a.5.5 0 0 1 .708 0l.509.509.137-.274V2.118l-.137-.274-.51.51a.5.5 0 0 1-.707 0L12 1.707l-.646.647a.5.5 0 0 1-.708 0L10 1.707l-.646.647a.5.5 0 0 1-.708 0L8 1.707l-.646.647a.5.5 0 0 1-.708 0L6 1.707l-.646.647a.5.5 0 0 1-.708 0L4 1.707l-.646.647a.5.5 0 0 1-.708 0l-.509-.51z" |
||||||
|
/> |
||||||
|
|
||||||
|
<path |
||||||
|
fillRule="evenodd" |
||||||
|
d="M3 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm8-6a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</NavLink> |
||||||
|
) |
||||||
|
} |
||||||
|
export const HeaderWithSettings: React.FC<Props> = ({ |
||||||
|
title = "", |
||||||
|
showBackButton = false, |
||||||
|
from, |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<AppContext.Consumer> |
||||||
|
{({ themeType }) => ( |
||||||
|
<div> |
||||||
|
<h6>{title}</h6> |
||||||
|
<div style={{ float: "right" }}> |
||||||
|
<HomeIcon from={from} themeType={themeType} /> |
||||||
|
|
||||||
|
<ReceiptsIcon from={from} themeType={themeType} /> |
||||||
|
|
||||||
|
<SettingsIcon from={from} themeType={themeType} /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</AppContext.Consumer> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import React from "react" |
||||||
|
|
||||||
|
interface Props { |
||||||
|
text: string |
||||||
|
isSubmitting?: boolean |
||||||
|
dataId?: string |
||||||
|
} |
||||||
|
|
||||||
|
export const SubmitButton: React.FC<Props> = ({ |
||||||
|
text, |
||||||
|
dataId, |
||||||
|
isSubmitting = false, |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<button |
||||||
|
data-id={dataId} |
||||||
|
style={{ padding: "0.25rem 0.4rem", marginRight: "0.5em" }} |
||||||
|
type="submit" |
||||||
|
className="btn btn-primary" |
||||||
|
disabled={isSubmitting} |
||||||
|
> |
||||||
|
{!isSubmitting && text} |
||||||
|
|
||||||
|
{isSubmitting && ( |
||||||
|
<div> |
||||||
|
<span |
||||||
|
className="spinner-border spinner-border-sm" |
||||||
|
role="status" |
||||||
|
aria-hidden="true" |
||||||
|
style={{ marginRight: "0.3em" }} |
||||||
|
/> |
||||||
|
Verifying... Please wait |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</button> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
export { HeaderWithSettings } from "./HeaderWithSettings" |
||||||
|
export { SubmitButton } from "./SubmitButton" |
@ -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.error(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.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return [storedValue, setValue] |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import React, { PropsWithChildren } from "react" |
||||||
|
|
||||||
|
import { HeaderWithSettings } from "../components" |
||||||
|
|
||||||
|
interface Props { |
||||||
|
from: string |
||||||
|
} |
||||||
|
|
||||||
|
export const DefaultLayout: React.FC<PropsWithChildren<Props>> = ({ |
||||||
|
children, |
||||||
|
from, |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<HeaderWithSettings from={from} /> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { DefaultLayout } from "./Default" |
After Width: | Height: | Size: 2.9 KiB |
@ -0,0 +1,51 @@ |
|||||||
|
import React from "react" |
||||||
|
import { |
||||||
|
HashRouter as Router, |
||||||
|
Route, |
||||||
|
Routes, |
||||||
|
RouteProps, |
||||||
|
} from "react-router-dom" |
||||||
|
|
||||||
|
import { ErrorView, HomeView, ReceiptsView, CaptureKeyView } from "./views" |
||||||
|
import { DefaultLayout } from "./layouts" |
||||||
|
|
||||||
|
interface Props extends RouteProps { |
||||||
|
component: any // TODO: new (props: any) => React.Component
|
||||||
|
from: string |
||||||
|
} |
||||||
|
|
||||||
|
const RouteWithHeader = ({ component: Component, ...rest }: Props) => { |
||||||
|
return ( |
||||||
|
<Route |
||||||
|
{...rest}
|
||||||
|
> |
||||||
|
<DefaultLayout {...rest}> |
||||||
|
<Component /> |
||||||
|
</DefaultLayout> |
||||||
|
</Route> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const DisplayRoutes = () => ( |
||||||
|
<Router> |
||||||
|
<Routes>
|
||||||
|
<Route |
||||||
|
path="/" |
||||||
|
element={<DefaultLayout from="/"> |
||||||
|
<HomeView /> |
||||||
|
</DefaultLayout>} /> |
||||||
|
<Route path="/error" |
||||||
|
element={<ErrorView />} /> |
||||||
|
<Route |
||||||
|
path="/receipts" |
||||||
|
element={<DefaultLayout from="/receipts"> |
||||||
|
<ReceiptsView /> |
||||||
|
</DefaultLayout>} /> |
||||||
|
<Route |
||||||
|
path="/settings" |
||||||
|
element={<DefaultLayout from="/settings"> |
||||||
|
<CaptureKeyView /> |
||||||
|
</DefaultLayout>} /> |
||||||
|
</Routes> |
||||||
|
</Router> |
||||||
|
) |
After Width: | Height: | Size: 347 B |
@ -0,0 +1,6 @@ |
|||||||
|
export type ReceiptStatus = "Verified" | "Queue" |
||||||
|
|
||||||
|
export interface Receipt { |
||||||
|
guid: string |
||||||
|
status: ReceiptStatus |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export type ThemeType = "dark" | "light" |
@ -0,0 +1,2 @@ |
|||||||
|
export * from "./Receipt" |
||||||
|
export * from "./ThemeType" |
@ -0,0 +1 @@ |
|||||||
|
export * from "./utilities" |
@ -0,0 +1,32 @@ |
|||||||
|
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") |
||||||
|
} |
||||||
|
return network.name!.toLowerCase() |
||||||
|
} |
||||||
|
|
||||||
|
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.error(error) |
||||||
|
} |
||||||
|
} |
@ -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 ( |
||||||
|
<AppContext.Consumer> |
||||||
|
{({ apiKey, setAPIKey }) => ( |
||||||
|
<Formik |
||||||
|
initialValues={{ apiKey }} |
||||||
|
validate={(values) => { |
||||||
|
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 }) => ( |
||||||
|
<form onSubmit={handleSubmit}> |
||||||
|
<div className="form-group" style={{ marginBottom: "0.5rem" }}> |
||||||
|
<label htmlFor="apikey">Please Enter your API key</label> |
||||||
|
<Field |
||||||
|
className={ |
||||||
|
errors.apiKey && touched.apiKey |
||||||
|
? "form-control form-control-sm is-invalid" |
||||||
|
: "form-control form-control-sm" |
||||||
|
} |
||||||
|
type="text" |
||||||
|
name="apiKey" |
||||||
|
placeholder="Example: GM1T20XY6JGSAPWKDCYZ7B2FJXKTJRFVGZ" |
||||||
|
/> |
||||||
|
<ErrorMessage |
||||||
|
className="invalid-feedback" |
||||||
|
name="apiKey" |
||||||
|
component="div" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
<SubmitButton text="Save API key" dataId="save-api-key" /> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
)} |
||||||
|
</Formik> |
||||||
|
)} |
||||||
|
</AppContext.Consumer> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
import React from "react" |
||||||
|
|
||||||
|
export const ErrorView: React.FC = () => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
width: "100%", |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<img |
||||||
|
style={{ paddingBottom: "2em" }} |
||||||
|
width="250" |
||||||
|
src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png" |
||||||
|
alt="Error page" |
||||||
|
/> |
||||||
|
<h5>Sorry, something unexpected happened. </h5> |
||||||
|
<h5> |
||||||
|
Please raise an issue:{" "} |
||||||
|
<a |
||||||
|
style={{ color: "red" }} |
||||||
|
href="https://github.com/ethereum/remix-project/issues" |
||||||
|
> |
||||||
|
Here |
||||||
|
</a> |
||||||
|
</h5> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -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 ( |
||||||
|
<AppContext.Consumer> |
||||||
|
{({ apiKey, clientInstance, setReceipts, receipts, contracts }) => |
||||||
|
!apiKey ? ( |
||||||
|
<Navigate |
||||||
|
to={{ |
||||||
|
pathname: "/settings" |
||||||
|
}} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<VerifyView |
||||||
|
contracts={contracts} |
||||||
|
client={clientInstance} |
||||||
|
apiKey={apiKey} |
||||||
|
onVerifiedContract={(receipt: Receipt) => { |
||||||
|
const newReceipts = [...receipts, receipt] |
||||||
|
|
||||||
|
setReceipts(newReceipts) |
||||||
|
}} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
</AppContext.Consumer> |
||||||
|
) |
||||||
|
} |
@ -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 ( |
||||||
|
<AppContext.Consumer> |
||||||
|
{({ apiKey, clientInstance, receipts }) => |
||||||
|
!apiKey ? ( |
||||||
|
<Navigate |
||||||
|
to={{ |
||||||
|
pathname: "/settings" |
||||||
|
}} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<div> |
||||||
|
<Formik |
||||||
|
initialValues={{ receiptGuid: "" }} |
||||||
|
validate={(values) => { |
||||||
|
const errors = {} as any |
||||||
|
if (!values.receiptGuid) { |
||||||
|
errors.receiptGuid = "Required" |
||||||
|
} |
||||||
|
return errors |
||||||
|
}} |
||||||
|
onSubmit={(values) => |
||||||
|
onGetReceiptStatus(values, clientInstance, apiKey) |
||||||
|
} |
||||||
|
> |
||||||
|
{({ errors, touched, handleSubmit }) => ( |
||||||
|
<form onSubmit={handleSubmit}> |
||||||
|
<div |
||||||
|
className="form-group" |
||||||
|
style={{ marginBottom: "0.5rem" }} |
||||||
|
> |
||||||
|
<h6>Get your Receipt GUID status</h6> |
||||||
|
<label htmlFor="receiptGuid">Receipt GUID</label> |
||||||
|
<Field |
||||||
|
className={ |
||||||
|
errors.receiptGuid && touched.receiptGuid |
||||||
|
? "form-control form-control-sm is-invalid" |
||||||
|
: "form-control form-control-sm" |
||||||
|
} |
||||||
|
type="text" |
||||||
|
name="receiptGuid" |
||||||
|
/> |
||||||
|
<ErrorMessage |
||||||
|
className="invalid-feedback" |
||||||
|
name="receiptGuid" |
||||||
|
component="div" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<SubmitButton text="Check" /> |
||||||
|
</form> |
||||||
|
)} |
||||||
|
</Formik> |
||||||
|
|
||||||
|
<div |
||||||
|
style={{ |
||||||
|
marginTop: "2em", |
||||||
|
fontSize: "0.8em", |
||||||
|
textAlign: "center", |
||||||
|
}} |
||||||
|
dangerouslySetInnerHTML={{ __html: results }} |
||||||
|
/> |
||||||
|
|
||||||
|
<ReceiptsTable receipts={receipts} /> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
</AppContext.Consumer> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const ReceiptsTable: React.FC<{ receipts: Receipt[] }> = ({ receipts }) => { |
||||||
|
return ( |
||||||
|
<div className="table-responsive" style={{ fontSize: "0.7em" }}> |
||||||
|
<h6>Receipts</h6> |
||||||
|
<table className="table table-sm"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Guid</th> |
||||||
|
<th scope="col">Status</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{receipts && |
||||||
|
receipts.length > 0 && |
||||||
|
receipts.map((item: Receipt, index) => { |
||||||
|
return ( |
||||||
|
<tr key={item.guid}> |
||||||
|
<td>{item.guid}</td> |
||||||
|
<td>{item.status}</td> |
||||||
|
</tr> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -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<Props> = ({ |
||||||
|
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 <br> Receipt GUID ${result}` |
||||||
|
} |
||||||
|
if (message === "NOTOK") { |
||||||
|
client.emit("statusChanged", { |
||||||
|
key: "failed", |
||||||
|
type: "error", |
||||||
|
title: result, |
||||||
|
}) |
||||||
|
resetAfter10Seconds() |
||||||
|
} |
||||||
|
return result |
||||||
|
} catch (error) { |
||||||
|
console.error(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 ( |
||||||
|
<div> |
||||||
|
<Formik |
||||||
|
initialValues={{ |
||||||
|
contractName: "", |
||||||
|
contractArguments: "", |
||||||
|
contractAddress: "", |
||||||
|
}} |
||||||
|
validate={(values) => { |
||||||
|
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 }) => ( |
||||||
|
<form onSubmit={handleSubmit}> |
||||||
|
<div className="form-group"> |
||||||
|
<h6>Verify your smart contracts</h6> |
||||||
|
<label htmlFor="contractName">Contract</label> |
||||||
|
<Field |
||||||
|
as="select" |
||||||
|
className={ |
||||||
|
errors.contractName && touched.contractName |
||||||
|
? "form-control form-control-sm is-invalid" |
||||||
|
: "form-control form-control-sm" |
||||||
|
} |
||||||
|
name="contractName" |
||||||
|
> |
||||||
|
<option disabled={true} value=""> |
||||||
|
Select a contract |
||||||
|
</option> |
||||||
|
{contracts.map((item) => ( |
||||||
|
<option key={item} value={item}> |
||||||
|
{item} |
||||||
|
</option> |
||||||
|
))} |
||||||
|
</Field> |
||||||
|
<ErrorMessage |
||||||
|
className="invalid-feedback" |
||||||
|
name="contractName" |
||||||
|
component="div" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="form-group"> |
||||||
|
<label htmlFor="contractArguments">Constructor Arguments</label> |
||||||
|
<Field |
||||||
|
className={ |
||||||
|
errors.contractArguments && touched.contractArguments |
||||||
|
? "form-control form-control-sm is-invalid" |
||||||
|
: "form-control form-control-sm" |
||||||
|
} |
||||||
|
type="text" |
||||||
|
name="contractArguments" |
||||||
|
placeholder="hex encoded" |
||||||
|
/> |
||||||
|
<ErrorMessage |
||||||
|
className="invalid-feedback" |
||||||
|
name="contractArguments" |
||||||
|
component="div" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="form-group"> |
||||||
|
<label htmlFor="contractAddress">Contract Address</label> |
||||||
|
<Field |
||||||
|
className={ |
||||||
|
errors.contractAddress && touched.contractAddress |
||||||
|
? "form-control form-control-sm is-invalid" |
||||||
|
: "form-control form-control-sm" |
||||||
|
} |
||||||
|
type="text" |
||||||
|
name="contractAddress" |
||||||
|
placeholder="i.e. 0x11b79afc03baf25c631dd70169bb6a3160b2706e" |
||||||
|
/> |
||||||
|
<ErrorMessage |
||||||
|
className="invalid-feedback" |
||||||
|
name="contractAddress" |
||||||
|
component="div" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<SubmitButton dataId="verify-contract" text="Verify Contract" isSubmitting={isSubmitting} /> |
||||||
|
</form> |
||||||
|
)} |
||||||
|
</Formik> |
||||||
|
|
||||||
|
<div data-id="verify-result" |
||||||
|
style={{ marginTop: "2em", fontSize: "0.8em", textAlign: "center" }} |
||||||
|
dangerouslySetInnerHTML={{ __html: results }} |
||||||
|
/> |
||||||
|
|
||||||
|
{/* <div style={{ display: "block", textAlign: "center", marginTop: "1em" }}> |
||||||
|
<Link to="/receipts">View Receipts</Link> |
||||||
|
</div> */} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
export { HomeView } from "./HomeView" |
||||||
|
export { ErrorView } from "./ErrorView" |
||||||
|
export { ReceiptsView } from "./ReceiptsView" |
||||||
|
export { CaptureKeyView } from "./CaptureKeyView" |
@ -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 |
||||||
|
}; |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,14 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<title>Etherscan</title> |
||||||
|
<base href="/" /> |
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" /> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="root"></div> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,7 @@ |
|||||||
|
import { StrictMode } from 'react'; |
||||||
|
import * as ReactDOM from 'react-dom'; |
||||||
|
|
||||||
|
|
||||||
|
import App from './app/app'; |
||||||
|
|
||||||
|
ReactDOM.render(<StrictMode><App /></StrictMode>, document.getElementById('root')); |
@ -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'; |
@ -0,0 +1 @@ |
|||||||
|
/* You can add global styles to this file, and also import other style files */ |
@ -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"] |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
'use strict' |
||||||
|
|
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
|
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done, 'http://127.0.0.1:8080', true) |
||||||
|
}, |
||||||
|
'Should add line texts': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.openFile('contracts') |
||||||
|
.openFile('contracts/1_Storage.sol') |
||||||
|
.addFile('scripts/addlinetext.ts', {content: addLineText}) |
||||||
|
.pause(4000) |
||||||
|
.executeScriptInTerminal('remix.exeCurrent()') |
||||||
|
.pause(4000) |
||||||
|
.openFile('contracts/1_Storage.sol') |
||||||
|
.useXpath() |
||||||
|
.waitForElementVisible("//*[@class='view-line' and contains(.,'contract')]//span//span[contains(.,'mylinetext1')]") |
||||||
|
.waitForElementVisible("//*[@class='view-line' and contains(.,'function')]//span//span[contains(.,'mylinetext2')]") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const addLineText = ` |
||||||
|
(async () => { |
||||||
|
|
||||||
|
await remix.call('editor', 'discardLineTexts' as any) |
||||||
|
let linetext = { |
||||||
|
content: 'mylinetext1', |
||||||
|
position: { |
||||||
|
start: { |
||||||
|
line: 9, |
||||||
|
column: 1, |
||||||
|
} |
||||||
|
}, |
||||||
|
hide: false, |
||||||
|
className: 'text-muted small', |
||||||
|
afterContentClassName: 'text-muted small fas fa-gas-pump pl-4', |
||||||
|
hoverMessage: [{ |
||||||
|
value: 'hovering1', |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
|
||||||
|
await remix.call('editor', 'addLineText' as any, linetext, 'contracts/1_Storage.sol') |
||||||
|
|
||||||
|
|
||||||
|
linetext = { |
||||||
|
content: 'mylinetext2', |
||||||
|
position: { |
||||||
|
start: { |
||||||
|
line: 17, |
||||||
|
column: 1, |
||||||
|
} |
||||||
|
}, |
||||||
|
hide: false, |
||||||
|
className: 'text-muted small', |
||||||
|
afterContentClassName: 'text-muted small fas fa-gas-pump pl-4', |
||||||
|
hoverMessage: [{ |
||||||
|
value: 'hovering2', |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
|
||||||
|
await remix.call('editor', 'addLineText' as any, linetext, 'contracts/1_Storage.sol') |
||||||
|
|
||||||
|
})()` |
@ -0,0 +1,107 @@ |
|||||||
|
'use strict' |
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
|
||||||
|
declare global { |
||||||
|
interface Window { testplugin: { name: string, url: string }; } |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
'@disabled': true, |
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done, null, true, { name: 'etherscan', url: 'http://127.0.0.1:5003'}) |
||||||
|
}, |
||||||
|
|
||||||
|
'Should load etherscan plugin #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clickLaunchIcon('pluginManager') |
||||||
|
.scrollAndClick('[data-id="pluginManagerComponentActivateButtonetherscan"]') |
||||||
|
.clickLaunchIcon('etherscan') |
||||||
|
.pause(5000) |
||||||
|
// @ts-ignore
|
||||||
|
.frame(0) |
||||||
|
.waitForElementVisible('input[name="apiKey"]') |
||||||
|
.setValue('input[name="apiKey"]', '2HKUX5ZVASZIKWJM8MIQVCRUVZ6JAWT531') |
||||||
|
.click('[data-id="save-api-key"]') |
||||||
|
}, |
||||||
|
|
||||||
|
'Should verify a contract (contract is already verified) #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.frameParent() |
||||||
|
.clickLaunchIcon('udapp') // switch to Goerli
|
||||||
|
.switchEnvironment('External Http Provider') |
||||||
|
.waitForElementPresent('[data-id="basic-http-provider-modal-footer-ok-react"]') |
||||||
|
.execute(() => { |
||||||
|
(document.querySelector('*[data-id="basic-http-providerModalDialogContainer-react"] input[data-id="modalDialogCustomPromp"]') as any).focus() |
||||||
|
}, [], () => {}) |
||||||
|
.setValue('[data-id="modalDialogCustomPromp"]', 'https://remix-goerli.ethdevops.io') |
||||||
|
.modalFooterOKClick('basic-http-provider') |
||||||
|
.clickLaunchIcon('solidity') // compile
|
||||||
|
.testContracts('Owner_1.sol', { content: verifiedContract }, ['Owner']) |
||||||
|
.clickLaunchIcon('etherscan') // start etherscan verification
|
||||||
|
// @ts-ignore
|
||||||
|
.frame(0) |
||||||
|
.click('[data-id="home"]') |
||||||
|
.setValue('select[name="contractName"]', 'Owner') |
||||||
|
.setValue('*[name="contractAddress"]', '0x9981c9d00103da481c3c65b22a79582a3e3ff50b') |
||||||
|
.click('[data-id="verify-contract"]') |
||||||
|
.waitForElementVisible('[data-id="verify-result"]') |
||||||
|
.waitForElementContainsText('[data-id="verify-result"]', 'Contract source code already verified') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const verifiedContract = ` |
||||||
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
|
|
||||||
|
pragma solidity >=0.7.0 <0.9.0; |
||||||
|
|
||||||
|
/** |
||||||
|
* @title Owner |
||||||
|
* @dev Set & change owner |
||||||
|
*/ |
||||||
|
contract Owner { |
||||||
|
|
||||||
|
address private owner; |
||||||
|
|
||||||
|
// event for EVM logging
|
||||||
|
event OwnerSet(address indexed oldOwner, address indexed newOwner); |
||||||
|
|
||||||
|
// modifier to check if caller is owner
|
||||||
|
modifier isOwner() { |
||||||
|
// If the first argument of 'require' evaluates to 'false', execution terminates and all
|
||||||
|
// changes to the state and to Ether balances are reverted.
|
||||||
|
// This used to consume all gas in old EVM versions, but not anymore.
|
||||||
|
// It is often a good idea to use 'require' to check if functions are called correctly.
|
||||||
|
// As a second argument, you can also provide an explanation about what went wrong.
|
||||||
|
require(msg.sender == owner, "Caller is not owner"); |
||||||
|
_; |
||||||
|
} |
||||||
|
|
||||||
|
function getInt() public returns (uint) { |
||||||
|
return 123498; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Set contract deployer as owner |
||||||
|
*/ |
||||||
|
constructor() { |
||||||
|
owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
|
||||||
|
emit OwnerSet(address(0), owner); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Change owner |
||||||
|
* @param newOwner address of new owner |
||||||
|
*/ |
||||||
|
function changeOwner(address newOwner) public isOwner { |
||||||
|
emit OwnerSet(owner, newOwner); |
||||||
|
owner = newOwner; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Return owner address
|
||||||
|
* @return address of owner |
||||||
|
*/ |
||||||
|
function getOwner() external view returns (address) { |
||||||
|
return owner; |
||||||
|
} |
||||||
|
}` |
@ -0,0 +1,125 @@ |
|||||||
|
|
||||||
|
'use strict' |
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
|
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done) |
||||||
|
}, |
||||||
|
|
||||||
|
'Test decorators with script': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.openFile('contracts') |
||||||
|
.openFile('contracts/2_Owner.sol') |
||||||
|
.openFile('contracts/1_Storage.sol') |
||||||
|
.openFile('contracts/3_Ballot.sol') |
||||||
|
.addFile('scripts/decorators.ts', { content: testScriptSet }) |
||||||
|
.pause(2000) |
||||||
|
.executeScriptInTerminal('remix.exeCurrent()') |
||||||
|
.pause(4000) |
||||||
|
.useXpath() |
||||||
|
.waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', '2') |
||||||
|
.waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', '2') |
||||||
|
.waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-custom-contracts/2_Owner.sol"]', 'U') |
||||||
|
.waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-custom-contracts/2_Owner.sol"]', 'U') |
||||||
|
.waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-warning-contracts/1_Storage.sol"]', '2') |
||||||
|
.waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-warning-contracts/1_Storage.sol"]', '2') |
||||||
|
.waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 'customtext') |
||||||
|
.waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 'customtext') |
||||||
|
.moveToElement('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', 0, 0) |
||||||
|
.waitForElementVisible('//*[@id="error-tooltip-contracts/2_Owner.sol"]') |
||||||
|
.waitForElementContainsText('//*[@id="error-tooltip-contracts/2_Owner.sol"]', 'error on owner') |
||||||
|
}, |
||||||
|
|
||||||
|
'clear ballot decorator': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.useCss() |
||||||
|
.addFile('scripts/clearballot.ts', { content: testScriptClearBallot }) |
||||||
|
.pause(2000) |
||||||
|
.executeScriptInTerminal('remix.exeCurrent()') |
||||||
|
.pause(4000) |
||||||
|
.waitForElementNotPresent('[data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 10000) |
||||||
|
}, |
||||||
|
'clear all decorators': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.addFile('scripts/clearall.ts', { content: testScriptClear }) |
||||||
|
.pause(2000) |
||||||
|
.executeScriptInTerminal('remix.exeCurrent()') |
||||||
|
.pause(4000) |
||||||
|
.waitForElementNotPresent('[data-id="file-decoration-error-contracts/2_Owner.sol"]', 10000) |
||||||
|
.waitForElementNotPresent('[data-id="file-decoration-warning-contracts/1_Storage.sol"]', 10000) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
const testScriptSet = ` |
||||||
|
(async () => { |
||||||
|
remix.call('fileDecorator' as any, 'clearFileDecorators') |
||||||
|
let decorator: any = { |
||||||
|
path: 'contracts/2_Owner.sol', |
||||||
|
isDirectory: false, |
||||||
|
fileStateType: 'ERROR', |
||||||
|
fileStateLabelClass: 'text-danger', |
||||||
|
fileStateIconClass: '', |
||||||
|
fileStateIcon: '', |
||||||
|
text: '2', |
||||||
|
bubble: true, |
||||||
|
comment: 'error on owner', |
||||||
|
} |
||||||
|
let decorator2: any = { |
||||||
|
path: 'contracts/2_Owner.sol', |
||||||
|
isDirectory: false, |
||||||
|
fileStateType: 'CUSTOM', |
||||||
|
fileStateLabelClass: 'text-success', |
||||||
|
fileStateIconClass: 'text-success', |
||||||
|
fileStateIcon: 'U', |
||||||
|
text: '', |
||||||
|
bubble: true, |
||||||
|
comment: 'modified', |
||||||
|
} |
||||||
|
await remix.call('fileDecorator' as any, 'setFileDecorators', [decorator, decorator2]) |
||||||
|
|
||||||
|
decorator = { |
||||||
|
path: 'contracts/1_Storage.sol', |
||||||
|
isDirectory: false, |
||||||
|
fileStateType: 'WARNING', |
||||||
|
fileStateLabelClass: 'text-warning', |
||||||
|
fileStateIconClass: '', |
||||||
|
fileStateIcon: '', |
||||||
|
text: '2', |
||||||
|
bubble: true, |
||||||
|
comment: 'warning on storage', |
||||||
|
} |
||||||
|
await remix.call('fileDecorator' as any, 'setFileDecorators', decorator) |
||||||
|
|
||||||
|
decorator = { |
||||||
|
path: 'contracts/3_Ballot.sol', |
||||||
|
isDirectory: false, |
||||||
|
fileStateType: 'CUSTOM', |
||||||
|
fileStateLabelClass: '', |
||||||
|
fileStateIconClass: '', |
||||||
|
fileStateIcon: 'customtext', |
||||||
|
text: 'with text', |
||||||
|
bubble: true, |
||||||
|
comment: 'custom comment', |
||||||
|
} |
||||||
|
await remix.call('fileDecorator' as any, 'setFileDecorators', decorator) |
||||||
|
|
||||||
|
})()` |
||||||
|
|
||||||
|
|
||||||
|
const testScriptClearBallot = ` |
||||||
|
(async () => { |
||||||
|
|
||||||
|
await remix.call('fileDecorator' as any, 'clearFileDecorators', 'contracts/3_Ballot.sol') |
||||||
|
|
||||||
|
})()` |
||||||
|
|
||||||
|
const testScriptClear = ` |
||||||
|
(async () => { |
||||||
|
await remix.call('fileDecorator' as any, 'clearAllFileDecorators') |
||||||
|
|
||||||
|
|
||||||
|
})()` |
@ -0,0 +1,96 @@ |
|||||||
|
'use strict' |
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
|
||||||
|
declare global { |
||||||
|
interface Window { testplugin: { name: string, url: string }; } |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
'@disabled': true, |
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done, null, true, { name: 'vyper', url: 'http://127.0.0.1:5002'}) |
||||||
|
}, |
||||||
|
|
||||||
|
'Should connect to vyper plugin #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clickLaunchIcon('pluginManager') |
||||||
|
.scrollAndClick('[data-id="pluginManagerComponentActivateButtonvyper"]') |
||||||
|
.clickLaunchIcon('vyper') |
||||||
|
.pause(5000) |
||||||
|
// @ts-ignore
|
||||||
|
.frame(0) |
||||||
|
}, |
||||||
|
|
||||||
|
'Should add the Ballot.vy #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.click('button[data-id="add-ballot"]') |
||||||
|
.frameParent() |
||||||
|
.openFile('ballot.vy') |
||||||
|
}, |
||||||
|
|
||||||
|
'Compile ballot.vy should error #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clickLaunchIcon('vyper') |
||||||
|
// @ts-ignore
|
||||||
|
.frame(0) |
||||||
|
.click('[data-id="remote-compiler"]') |
||||||
|
.click('[data-id="compile"]') |
||||||
|
.assert.containsText('[data-id="error-message"]', 'unexpected indent') |
||||||
|
}, |
||||||
|
|
||||||
|
'Compile test contract should success #group1': function (browser: NightwatchBrowser) { |
||||||
|
let contractAddress |
||||||
|
browser |
||||||
|
.frameParent() |
||||||
|
.addFile('test.vy', { content: testContract }) |
||||||
|
.clickLaunchIcon('vyper') |
||||||
|
// @ts-ignore
|
||||||
|
.frame(0) |
||||||
|
.click('[data-id="compile"]') |
||||||
|
.frameParent() |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.createContract('') |
||||||
|
.clickInstance(0) |
||||||
|
.clickFunction('totalPokemonCount - call') |
||||||
|
.getAddressAtPosition(0, (address) => { |
||||||
|
console.log('Vyper contract ' + address) |
||||||
|
contractAddress = address |
||||||
|
}) |
||||||
|
.perform((done) => { |
||||||
|
browser.verifyCallReturnValue(contractAddress, ['0:uint256: 0']) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const testContract = ` |
||||||
|
# @version >=0.2.4 <0.3.0 |
||||||
|
|
||||||
|
DNA_DIGITS: constant(uint256) = 16 |
||||||
|
DNA_MODULUS: constant(uint256) = 10 ** DNA_DIGITS |
||||||
|
# add HP_LIMIT |
||||||
|
|
||||||
|
struct Pokemon: |
||||||
|
name: String[32] |
||||||
|
dna: uint256 |
||||||
|
HP: uint256 |
||||||
|
matches: uint256 |
||||||
|
wins: uint256 |
||||||
|
|
||||||
|
totalPokemonCount: public(uint256) |
||||||
|
pokemonList: HashMap[uint256, Pokemon] |
||||||
|
|
||||||
|
@pure |
||||||
|
@internal |
||||||
|
def _generateRandomDNA(_name: String[32]) -> uint256: |
||||||
|
random: uint256 = convert(keccak256(_name), uint256) |
||||||
|
return random % DNA_MODULUS |
||||||
|
# modify _createPokemon |
||||||
|
@internal |
||||||
|
def _createPokemon(_name: String[32], _dna: uint256, _HP: uint256): |
||||||
|
self.pokemonList[self.totalPokemonCount] = Pokemon({ |
||||||
|
name: _name, |
||||||
|
dna: _dna, |
||||||
|
HP: _HP, |
||||||
|
matches: 0, |
||||||
|
wins: 0 |
||||||
|
}) |
||||||
|
self.totalPokemonCount += 1` |
@ -0,0 +1,25 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
BUILD_ID=${CIRCLE_BUILD_NUM:-${TRAVIS_JOB_NUMBER}} |
||||||
|
echo "$BUILD_ID" |
||||||
|
TEST_EXITCODE=0 |
||||||
|
|
||||||
|
yarn run serve:production & |
||||||
|
npx nx serve etherscan & |
||||||
|
|
||||||
|
sleep 5 |
||||||
|
|
||||||
|
yarn run build:e2e |
||||||
|
|
||||||
|
TESTFILES=$(grep -IRiL "\'@disabled\': \?true" "dist/apps/remix-ide-e2e/src/tests" | grep "etherscan_api" | sort | circleci tests split ) |
||||||
|
for TESTFILE in $TESTFILES; do |
||||||
|
npx nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js $TESTFILE --env=chrome || TEST_EXITCODE=1 |
||||||
|
done |
||||||
|
|
||||||
|
echo "$TEST_EXITCODE" |
||||||
|
if [ "$TEST_EXITCODE" -eq 1 ] |
||||||
|
then |
||||||
|
exit 1 |
||||||
|
fi |
@ -0,0 +1,25 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
BUILD_ID=${CIRCLE_BUILD_NUM:-${TRAVIS_JOB_NUMBER}} |
||||||
|
echo "$BUILD_ID" |
||||||
|
TEST_EXITCODE=0 |
||||||
|
|
||||||
|
yarn run serve:production & |
||||||
|
npx nx serve vyper & |
||||||
|
|
||||||
|
sleep 5 |
||||||
|
|
||||||
|
yarn run build:e2e |
||||||
|
|
||||||
|
TESTFILES=$(grep -IRiL "\'@disabled\': \?true" "dist/apps/remix-ide-e2e/src/tests" | grep "vyper_api" | sort | circleci tests split ) |
||||||
|
for TESTFILE in $TESTFILES; do |
||||||
|
npx nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js $TESTFILE --env=chrome || TEST_EXITCODE=1 |
||||||
|
done |
||||||
|
|
||||||
|
echo "$TEST_EXITCODE" |
||||||
|
if [ "$TEST_EXITCODE" -eq 1 ] |
||||||
|
then |
||||||
|
exit 1 |
||||||
|
fi |
@ -0,0 +1,86 @@ |
|||||||
|
'use strict' |
||||||
|
|
||||||
|
import { default as deepequal } from 'deep-equal' // eslint-disable-line
|
||||||
|
|
||||||
|
import { Plugin } from '@remixproject/engine' |
||||||
|
import { fileDecoration } from '@remix-ui/file-decorators' |
||||||
|
|
||||||
|
const profile = { |
||||||
|
name: 'fileDecorator', |
||||||
|
desciption: 'Keeps decorators of the files', |
||||||
|
methods: ['setFileDecorators', 'clearFileDecorators', 'clearAllFileDecorators'], |
||||||
|
events: ['fileDecoratorsChanged'], |
||||||
|
version: '0.0.1' |
||||||
|
} |
||||||
|
|
||||||
|
export class FileDecorator extends Plugin { |
||||||
|
private _fileStates: fileDecoration[] = [] |
||||||
|
constructor() { |
||||||
|
super(profile) |
||||||
|
} |
||||||
|
|
||||||
|
onActivation(): void { |
||||||
|
this.on('filePanel', 'setWorkspace', async () => { |
||||||
|
await this.clearAllFileDecorators() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
*
|
||||||
|
* @param fileStates Array of file states |
||||||
|
*/ |
||||||
|
async setFileDecorators(fileStates: fileDecoration[] | fileDecoration) { |
||||||
|
const { from } = this.currentRequest |
||||||
|
const workspace = await this.call('filePanel', 'getCurrentWorkspace') |
||||||
|
const fileStatesPayload = Array.isArray(fileStates) ? fileStates : [fileStates] |
||||||
|
// clear all file states in the previous state of this owner on the files called
|
||||||
|
fileStatesPayload.forEach((state) => { |
||||||
|
state.workspace = workspace |
||||||
|
state.owner = from |
||||||
|
}) |
||||||
|
const filteredState = this._fileStates.filter((state) => { |
||||||
|
const index = fileStatesPayload.findIndex((payloadFileState: fileDecoration) => { |
||||||
|
return from == state.owner && payloadFileState.path == state.path |
||||||
|
}) |
||||||
|
return index == -1 |
||||||
|
}) |
||||||
|
const newState = [...filteredState, ...fileStatesPayload].sort(sortByPath) |
||||||
|
|
||||||
|
if (!deepequal(newState, this._fileStates)) { |
||||||
|
this._fileStates = newState |
||||||
|
this.emit('fileDecoratorsChanged', this._fileStates) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async clearFileDecorators(path?: string) { |
||||||
|
const { from } = this.currentRequest |
||||||
|
if (!from) return |
||||||
|
|
||||||
|
const filteredState = this._fileStates.filter((state) => { |
||||||
|
if(state.owner != from) return true |
||||||
|
if(path && state.path != path) return true |
||||||
|
}) |
||||||
|
const newState = [...filteredState].sort(sortByPath) |
||||||
|
|
||||||
|
if (!deepequal(newState, this._fileStates)) { |
||||||
|
this._fileStates = newState |
||||||
|
this.emit('fileDecoratorsChanged', this._fileStates) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async clearAllFileDecorators() { |
||||||
|
this._fileStates = [] |
||||||
|
this.emit('fileDecoratorsChanged', []) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const sortByPath = (a: fileDecoration, b: fileDecoration) => { |
||||||
|
if (a.path < b.path) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
if (a.path > b.path) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"presets": [ |
||||||
|
[ |
||||||
|
"@nrwl/react/babel", { |
||||||
|
"runtime": "automatic" |
||||||
|
|
||||||
|
} |
||||||
|
] |
||||||
|
], |
||||||
|
"plugins": [ |
||||||
|
|
||||||
|
] |
||||||
|
} |
@ -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,18 @@ |
|||||||
|
{ |
||||||
|
"env": { |
||||||
|
"browser": true, |
||||||
|
"es6": true |
||||||
|
}, |
||||||
|
"extends": "../../.eslintrc.json", |
||||||
|
"globals": { |
||||||
|
"Atomics": "readonly", |
||||||
|
"SharedArrayBuffer": "readonly" |
||||||
|
}, |
||||||
|
"parserOptions": { |
||||||
|
"ecmaVersion": 11, |
||||||
|
"sourceType": "module" |
||||||
|
}, |
||||||
|
"rules": { |
||||||
|
"standard/no-callback-literal": "off" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,124 @@ |
|||||||
|
* { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
html, body, #root, main { |
||||||
|
height: 100%; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
#vyper-plugin header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
height: 50px; |
||||||
|
padding: 5px 15px; |
||||||
|
} |
||||||
|
|
||||||
|
#vyper-plugin header a, |
||||||
|
#vyper-plugin header .title { |
||||||
|
display: flex; |
||||||
|
height: 60%; |
||||||
|
} |
||||||
|
|
||||||
|
#vyper-plugin header svg, |
||||||
|
#vyper-plugin header img { |
||||||
|
width: auto; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
#vyper-plugin section { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
height: calc(100% - 50px); |
||||||
|
} |
||||||
|
|
||||||
|
.btn-group-toggle { |
||||||
|
width: 90%; |
||||||
|
text-transform: uppercase; |
||||||
|
margin: 10px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-group-toggle .btn { |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.8rem; |
||||||
|
} |
||||||
|
|
||||||
|
#local-url { |
||||||
|
width: 90%; |
||||||
|
} |
||||||
|
|
||||||
|
#compile-btn { |
||||||
|
width: 90%; |
||||||
|
} |
||||||
|
|
||||||
|
#compile-btn * { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
#result { |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
padding-top: 10px; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
#result.error { |
||||||
|
justify-content: flex-start; |
||||||
|
} |
||||||
|
#result.error svg { |
||||||
|
width: 40px; |
||||||
|
height: 40px; |
||||||
|
margin: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
#result nav { |
||||||
|
width: 100%; |
||||||
|
flex-wrap: nowrap; |
||||||
|
justify-content: space-evenly; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#result nav a { |
||||||
|
padding: 0.5rem 1rem; |
||||||
|
font-size: 0.8rem; |
||||||
|
} |
||||||
|
|
||||||
|
#result .tab-content { |
||||||
|
flex: 1; |
||||||
|
width: 100%; |
||||||
|
padding: 15px; |
||||||
|
overflow: auto; |
||||||
|
} |
||||||
|
|
||||||
|
#result .tab-pane.active { |
||||||
|
height: 100%; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: stretch; |
||||||
|
} |
||||||
|
|
||||||
|
#result .copy { |
||||||
|
padding: 5x; |
||||||
|
font-size: 0.8rem; |
||||||
|
margin: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
#result p { |
||||||
|
text-align: center; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
#result .react-json-view { |
||||||
|
overflow-y: auto; |
||||||
|
} |
||||||
|
|
||||||
|
#result textarea { |
||||||
|
border: none; |
||||||
|
height: 100%; |
||||||
|
width: 100%; |
||||||
|
} |
@ -0,0 +1,118 @@ |
|||||||
|
import React, { useState, useEffect } from 'react' |
||||||
|
|
||||||
|
import { VyperCompilationOutput, remixClient } from './utils' |
||||||
|
import { CompilationResult } from '@remixproject/plugin-api' |
||||||
|
|
||||||
|
// Components
|
||||||
|
import CompilerButton from './components/CompilerButton' |
||||||
|
import WarnRemote from './components/WarnRemote' |
||||||
|
import VyperResult from './components/VyperResult' |
||||||
|
import LocalUrlInput from './components/LocalUrl' |
||||||
|
import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup' |
||||||
|
import ToggleButton from 'react-bootstrap/ToggleButton' |
||||||
|
|
||||||
|
import vyperLogo from './logo.svg' |
||||||
|
import './app.css' |
||||||
|
|
||||||
|
interface AppState { |
||||||
|
status: 'idle' | 'inProgress' |
||||||
|
environment: 'remote' | 'local' |
||||||
|
compilationResult?: CompilationResult |
||||||
|
localUrl: string |
||||||
|
} |
||||||
|
|
||||||
|
interface OutputMap { |
||||||
|
[fileName: string]: VyperCompilationOutput |
||||||
|
} |
||||||
|
|
||||||
|
const App: React.FC = () => { |
||||||
|
const [contract, setContract] = useState<string>() |
||||||
|
const [output, setOutput] = useState<OutputMap>({}) |
||||||
|
const [state, setState] = useState<AppState>({ |
||||||
|
status: 'idle', |
||||||
|
environment: 'local', |
||||||
|
localUrl: 'http://localhost:8000/compile' |
||||||
|
}) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
async function start() { |
||||||
|
try { |
||||||
|
await remixClient.loaded() |
||||||
|
remixClient.onFileChange(name => setContract(name)) |
||||||
|
const name = await remixClient.getContractName() |
||||||
|
setContract(name) |
||||||
|
} catch (err) { |
||||||
|
console.log(err) |
||||||
|
} |
||||||
|
} |
||||||
|
start() |
||||||
|
}, []) |
||||||
|
|
||||||
|
/** Update the environment state value */ |
||||||
|
function setEnvironment(environment: 'local' | 'remote') { |
||||||
|
setState({ ...state, environment }) |
||||||
|
} |
||||||
|
|
||||||
|
function setLocalUrl(url: string) { |
||||||
|
setState({ ...state, localUrl: url }) |
||||||
|
} |
||||||
|
|
||||||
|
function compilerUrl() { |
||||||
|
return state.environment === 'remote' |
||||||
|
? 'https://vyper.remixproject.org/compile' |
||||||
|
: state.localUrl |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<main id="vyper-plugin"> |
||||||
|
<header className="bg-light"> |
||||||
|
<div className="title"> |
||||||
|
<img src={vyperLogo} alt="Vyper logo" /> |
||||||
|
<h4>yper Compiler</h4> |
||||||
|
</div> |
||||||
|
<a |
||||||
|
rel="noopener noreferrer" |
||||||
|
href="https://github.com/GrandSchtroumpf/vyper-remix" |
||||||
|
target="_blank" |
||||||
|
> |
||||||
|
<i className="fab fa-github"></i> |
||||||
|
</a> |
||||||
|
</header> |
||||||
|
<section> |
||||||
|
<ToggleButtonGroup |
||||||
|
name="remote" |
||||||
|
onChange={setEnvironment} |
||||||
|
type="radio" |
||||||
|
value={state.environment} |
||||||
|
> |
||||||
|
<ToggleButton data-id="remote-compiler" variant="secondary" name="remote" value="remote"> |
||||||
|
Remote Compiler |
||||||
|
</ToggleButton> |
||||||
|
<ToggleButton data-id="local-compiler" variant="secondary" name="local" value="local"> |
||||||
|
Local Compiler |
||||||
|
</ToggleButton> |
||||||
|
</ToggleButtonGroup> |
||||||
|
<LocalUrlInput |
||||||
|
url={state.localUrl} |
||||||
|
setUrl={setLocalUrl} |
||||||
|
environment={state.environment} |
||||||
|
/> |
||||||
|
<WarnRemote environment={state.environment} /> |
||||||
|
<div id="compile-btn"> |
||||||
|
<CompilerButton |
||||||
|
compilerUrl={compilerUrl()} |
||||||
|
contract={contract} |
||||||
|
setOutput={(name, update) => |
||||||
|
setOutput({ ...output, [name]: update }) |
||||||
|
} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<article id="result"> |
||||||
|
<VyperResult output={contract ? output[contract] : undefined} /> |
||||||
|
</article> |
||||||
|
</section> |
||||||
|
</main> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default App |
@ -0,0 +1,75 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { |
||||||
|
isVyper, |
||||||
|
compile, |
||||||
|
toStandardOutput, |
||||||
|
VyperCompilationOutput, |
||||||
|
isCompilationError, |
||||||
|
remixClient |
||||||
|
} from '../utils' |
||||||
|
import Button from 'react-bootstrap/Button' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
compilerUrl: string |
||||||
|
contract?: string, |
||||||
|
setOutput: (name: string, output: VyperCompilationOutput) => void |
||||||
|
} |
||||||
|
|
||||||
|
function CompilerButton({ contract, setOutput, compilerUrl }: Props) { |
||||||
|
|
||||||
|
if (!contract || !contract) { |
||||||
|
return <Button disabled>No contract selected</Button> |
||||||
|
} |
||||||
|
|
||||||
|
if (!isVyper(contract)) { |
||||||
|
return <Button disabled>Not a vyper contract</Button> |
||||||
|
} |
||||||
|
|
||||||
|
/** Compile a Contract */ |
||||||
|
async function compileContract() { |
||||||
|
try { |
||||||
|
const _contract = await remixClient.getContract() |
||||||
|
remixClient.changeStatus({ |
||||||
|
key: 'loading', |
||||||
|
type: 'info', |
||||||
|
title: 'Compiling' |
||||||
|
}) |
||||||
|
const output = await compile(compilerUrl, _contract) |
||||||
|
setOutput(_contract.name, output) |
||||||
|
// ERROR
|
||||||
|
if (isCompilationError(output)) { |
||||||
|
const line = output.line |
||||||
|
const lineColumnPos = { |
||||||
|
start: { line: line - 1 }, |
||||||
|
end: { line: line - 1 } |
||||||
|
} |
||||||
|
remixClient.highlight(lineColumnPos as any, _contract.name, '#e0b4b4') |
||||||
|
throw new Error(output.message) |
||||||
|
} |
||||||
|
// SUCCESS
|
||||||
|
remixClient.discardHighlight() |
||||||
|
remixClient.changeStatus({ |
||||||
|
key: 'succeed', |
||||||
|
type: 'success', |
||||||
|
title: 'succeed' |
||||||
|
}) |
||||||
|
const data = toStandardOutput(_contract.name, output) |
||||||
|
remixClient.compilationFinish(_contract.name, _contract.content, data) |
||||||
|
} catch (err: any) { |
||||||
|
remixClient.changeStatus({ |
||||||
|
key: 'failed', |
||||||
|
type: 'error', |
||||||
|
title: err.message |
||||||
|
}) |
||||||
|
console.error(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Button data-id="compile" onClick={compileContract} variant="primary"> |
||||||
|
Compile {contract} |
||||||
|
</Button> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default CompilerButton |
@ -0,0 +1,36 @@ |
|||||||
|
import React from 'react' |
||||||
|
import Form from 'react-bootstrap/Form' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
url: string |
||||||
|
setUrl: (url: string) => void, |
||||||
|
environment: 'remote' | 'local' |
||||||
|
} |
||||||
|
|
||||||
|
function LocalUrlInput({ url, setUrl, environment }: Props) { |
||||||
|
|
||||||
|
if (environment === 'remote') { |
||||||
|
return <></> |
||||||
|
} |
||||||
|
|
||||||
|
function updateUrl(event: React.FocusEvent<HTMLInputElement>) { |
||||||
|
setUrl(event.target.value) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Form id="local-url"> |
||||||
|
<Form.Group controlId="localUrl"> |
||||||
|
<Form.Label>Local Compiler Url</Form.Label> |
||||||
|
<Form.Control onBlur={updateUrl} |
||||||
|
defaultValue={url} |
||||||
|
type="email" |
||||||
|
placeholder="eg http://localhost:8000/compile" /> |
||||||
|
<Form.Text className="text-muted"> |
||||||
|
The url to your local compiler |
||||||
|
</Form.Text> |
||||||
|
</Form.Group> |
||||||
|
</Form> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default LocalUrlInput; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue