├── .babelrc ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.toml ├── README.md ├── doc └── example.jpeg ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── public ├── discord_logo.svg ├── favicon.ico ├── icon.png └── inverted_flag.png ├── src ├── components │ ├── ActiveLink.js │ ├── AddressInput.js │ ├── AutoExpandSelector.js │ ├── ChainLoader.js │ ├── ChainSelector.js │ ├── ExplorerApiEndpoints.json │ ├── FileDiff.js │ ├── FileList.js │ ├── Filter.js │ ├── HighlightSyntaxSelector.js │ ├── Layout.js │ ├── Navigation.js │ └── ThemeSelector.js ├── hooks.js ├── pages │ ├── _app.js │ ├── _document.js │ ├── diff.js │ └── index.js ├── store │ ├── chains.js │ ├── index.js │ ├── options.js │ ├── reducers.js │ └── swap.js ├── styles │ └── globals.css ├── utils │ ├── api.js │ └── string.js └── vendor │ ├── prism.css │ └── prism.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "babel-plugin-styled-components", 6 | { 7 | "ssr": true 8 | } 9 | ], 10 | [ 11 | "babel-plugin-import", 12 | { 13 | "libraryName": "@mui/material", 14 | "libraryDirectory": "", 15 | "camel2DashComponentName": false 16 | }, 17 | "core" 18 | ], 19 | [ 20 | "babel-plugin-import", 21 | { 22 | "libraryName": "@mui/icons-material", 23 | "libraryDirectory": "", 24 | "camel2DashComponentName": false 25 | }, 26 | "icons" 27 | ] 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | "next/core-web-vitals", 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | ], 12 | ignorePatterns: ["src/vendor"], 13 | overrides: [ 14 | { 15 | env: { 16 | node: true, 17 | }, 18 | files: [".eslintrc.{js,cjs}"], 19 | parserOptions: { 20 | sourceType: "script", 21 | }, 22 | }, 23 | ], 24 | parser: "@typescript-eslint/parser", 25 | parserOptions: { 26 | ecmaVersion: "latest", 27 | sourceType: "module", 28 | }, 29 | plugins: ["@typescript-eslint", "react"], 30 | rules: { 31 | "react/react-in-jsx-scope": "off", 32 | "react/display-name": "off", 33 | "react/prop-types": "off", 34 | "react-hooks/exhaustive-deps": "off", 35 | "import/no-anonymous-default-export": "off", 36 | "@typescript-eslint/no-unused-vars": [ 37 | "error", 38 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v2 17 | with: 18 | version: latest 19 | 20 | - name: Use Node.js 18.x 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 18.x 24 | cache: "pnpm" 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | # This runs `next lint` before building. 30 | - name: Build 31 | run: pnpm build 32 | 33 | fmt: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: pnpm/action-setup@v2 39 | with: 40 | version: latest 41 | 42 | - name: Use Node.js 18.x 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: 18.x 46 | cache: "pnpm" 47 | 48 | - name: Install dependencies 49 | run: pnpm install 50 | 51 | - name: Check formatting 52 | run: pnpm fmt:check 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | node_modules/ 3 | src/vendor/ 4 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | printWidth = 80 2 | trailingComma = "es5" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contract Diffs 2 | 3 | A tool for comparing contracts across many chains.[^1] 4 | 5 | ## Features 6 | 7 | - Massively multichain (supports ~300 chains). 8 | - Automatically fetch source code. 9 | - Multifile support. 10 | - Auto-formats contracts using prettier to remove formatting diffs. 11 | - Optional dense mode to remove comments and whitespace from diffs. 12 | - Split mode and unified mode. 13 | - Solidity syntax highlighting. 14 | - Diff summaries. 15 | - File explorer. 16 | 17 | Below is an example diff, which can be viewed at [here](https://contracts.evmdiff.com/diff?address1=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&chain1=1&address2=0x4200000000000000000000000000000000000006&chain2=10). 18 | 19 | Screenshot_2023-05-13 21 39 08_TZDRO2 20 | 21 | [^1]: Forked from https://github.com/x48115/contract-diff-tool which is no longer maintained 22 | -------------------------------------------------------------------------------- /doc/example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mds1/contract-diff-tool/4a6575ec2cef3e99cbf3b73a33e739c10274218c/doc/example.jpeg -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: [], 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next", 5 | "build": "next build", 6 | "start": "next start", 7 | "lint": "next lint --fix", 8 | "fmt": "prettier --write .", 9 | "fmt:check": "prettier --check ." 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.0", 13 | "@emotion/styled": "^11.11.0", 14 | "@giantmachines/redux-websocket": "^1.5.1", 15 | "@mui/icons-material": "^5.11.16", 16 | "@mui/material": "^5.12.3", 17 | "@mui/styled-engine-sc": "npm:@mui/styled-engine-sc@latest", 18 | "@reduxjs/toolkit": "^1.3.6", 19 | "ethereum-checksum-address": "^0.0.8", 20 | "next": "^14.1.0", 21 | "prettier": "^2.8.8", 22 | "prettier-plugin-solidity": "^1.1.3", 23 | "react": "^18.2.0", 24 | "react-diff-viewer-continued": "^3.2.6", 25 | "react-dom": "^18.2.0", 26 | "react-redux": "^7.2.0", 27 | "redux-persist": "^6.0.0", 28 | "string-similarity": "^4.0.4", 29 | "styled-components": "^6.0.0-rc.1", 30 | "uuid": "^9.0.0", 31 | "uuidv4": "^6.2.13" 32 | }, 33 | "resolutions": { 34 | "@mui/styled-engine-sc": "npm:@mui/styled-engine-sc@latest" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "20.11.17", 38 | "@typescript-eslint/eslint-plugin": "^7.0.1", 39 | "@typescript-eslint/parser": "^7.0.1", 40 | "babel-plugin-import": "^1.13.6", 41 | "babel-plugin-styled-components": "^2.0.7", 42 | "eslint": "^8.56.0", 43 | "eslint-config-next": "^14.1.0", 44 | "eslint-config-react": "^1.1.7", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/discord_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mds1/contract-diff-tool/4a6575ec2cef3e99cbf3b73a33e739c10274218c/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mds1/contract-diff-tool/4a6575ec2cef3e99cbf3b73a33e739c10274218c/public/icon.png -------------------------------------------------------------------------------- /public/inverted_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mds1/contract-diff-tool/4a6575ec2cef3e99cbf3b73a33e739c10274218c/public/inverted_flag.png -------------------------------------------------------------------------------- /src/components/ActiveLink.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import Link from "next/link"; 3 | import React, { useState, useEffect } from "react"; 4 | 5 | const ActiveLink = ({ children, activeClassName, className, ...props }) => { 6 | const { asPath, isReady } = useRouter(); 7 | const [computedClassName, setComputedClassName] = useState(className); 8 | 9 | useEffect(() => { 10 | if (isReady) { 11 | const linkPathname = new URL(props.as || props.href, location.href) 12 | .pathname; 13 | const activePathname = new URL(asPath, location.href).pathname; 14 | const newClassName = 15 | linkPathname === activePathname 16 | ? `${className || ""} ${activeClassName}`.trim() 17 | : className; 18 | 19 | if (newClassName !== computedClassName) { 20 | setComputedClassName(newClassName); 21 | } 22 | } 23 | }, [ 24 | asPath, 25 | isReady, 26 | props.as, 27 | props.href, 28 | activeClassName, 29 | className, 30 | computedClassName, 31 | ]); 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | export default ActiveLink; 41 | -------------------------------------------------------------------------------- /src/components/AddressInput.js: -------------------------------------------------------------------------------- 1 | import { TextField, IconButton, InputAdornment, Tooltip } from "@mui/material"; 2 | import { ContentCopy, OpenInNew } from "@mui/icons-material"; 3 | import styled from "styled-components"; 4 | import { toChecksumAddress } from "ethereum-checksum-address"; 5 | import { useSelectExplorer1, useSelectExplorer2 } from "../hooks"; 6 | import { useEffect } from "react"; 7 | 8 | const ShiftRight = styled.div` 9 | position: relative; 10 | left: 10px; 11 | `; 12 | 13 | const OnlyDesktop = styled.div` 14 | @media (max-width: 990px) { 15 | display: none; 16 | } 17 | `; 18 | 19 | export default ({ 20 | label, 21 | addressState, 22 | setAddressState, 23 | field, 24 | helperTextOverride, 25 | errorOverride, 26 | clearAddressHelper, 27 | }) => { 28 | const explorer1 = useSelectExplorer1(); 29 | const explorer2 = useSelectExplorer2(); 30 | 31 | const explorer = field === 1 ? explorer1 : explorer2; 32 | 33 | const explorerAddress = explorer 34 | ? `${explorer}/address/${addressState.value}#code` 35 | : ""; 36 | 37 | const copy = () => { 38 | navigator.clipboard.writeText(addressState.value); 39 | }; 40 | 41 | const openAddress = () => { 42 | window.open(explorerAddress, "_blank").focus(); 43 | }; 44 | 45 | const params = new Proxy(new URLSearchParams(window.location.search), { 46 | get: (searchParams, prop) => searchParams.get(prop), 47 | }); 48 | 49 | useEffect(() => { 50 | if (params.address1) { 51 | setAddress(params[`address${field}`]); 52 | } 53 | }, []); 54 | 55 | const setAddress = (newValue) => { 56 | let isValid = false; 57 | let checksum; 58 | try { 59 | checksum = toChecksumAddress(newValue); 60 | isValid = true; 61 | } catch (e) { 62 | checksum = newValue; 63 | } 64 | if (!newValue || newValue === "") { 65 | clearAddressHelper = { clearAddressHelper }; 66 | } 67 | const error = !isValid && newValue !== ""; 68 | setAddressState({ valid: isValid, value: checksum, error }); 69 | }; 70 | 71 | return ( 72 | setAddress(evt.target.value), 96 | endAdornment: ( 97 | 98 | 99 | 100 | 101 |
102 | 109 | 110 | 111 |
112 |
113 | 114 |
115 | 122 | 123 | 124 |
125 |
126 |
127 |
128 |
129 | ), 130 | }} 131 | /> 132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /src/components/AutoExpandSelector.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { setAutoExpand } from "../store/options.js"; 3 | import { useAutoExpand } from "../hooks"; 4 | 5 | export default () => { 6 | const dispatch = useDispatch(); 7 | const autoExpand = useAutoExpand(); 8 | 9 | const onChange = () => { 10 | if (autoExpand) { 11 | dispatch(setAutoExpand(false)); 12 | } else { 13 | dispatch(setAutoExpand(true)); 14 | } 15 | }; 16 | return ( 17 | <> 18 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/ChainLoader.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { setChains } from "../store/chains"; 4 | import explorerUrls from "./ExplorerApiEndpoints.json"; 5 | 6 | export default ({ children }) => { 7 | const dispatch = useDispatch(); 8 | 9 | const apiKeys = { 10 | 1: "GEQXZDY67RZ4QHNU1A57QVPNDV3RP1RYH4", // etherscan 11 | 250: "3FV52UFPDWJ8JC22TJ1Y45Y443EAW4NVSH", // fantom 12 | 25: "RQMT5RSZRATKAJ5YP1MT4QH825MEKAHNIF", // cronos 13 | 42161: "NPVW1QSPANK5TCWGD6IHX9B4NM4CIIACVN", // arbitrum 14 | 43113: "XD73JI82I8U1FKHHCZ6R5ES291J7SBCXF9", // avalanche 15 | 10: "85YRD6N3IRUHZEWHZF3V6W5N8VIQCP64CA", // optimism 16 | 56: "7YDHPB7CSP9F9P3SNWDWP53ERRR2NIUT9B", // bsc 17 | 137: "UIXVPUFBEUSPK2KUXAEMK5YFTF4FTUZCEQ", // polygon 18 | 42220: "IF2FG9SQNJJWBQEBQJVHE1DYUZPSBQVGRS", // celo 19 | }; 20 | 21 | const delay = (time) => { 22 | return new Promise((resolve) => setTimeout(resolve, time)); 23 | }; 24 | 25 | useEffect(() => { 26 | const fetchData = async () => { 27 | const chainsUrl = "https://chainid.network/chains.json"; 28 | const fetchIt = async () => { 29 | try { 30 | return await fetch(chainsUrl).then((response) => response.json()); 31 | } catch (e) { 32 | await delay(1000); 33 | console.log("retry"); 34 | fetchIt(); 35 | } 36 | }; 37 | 38 | let chains = await fetchIt(); 39 | 40 | // Inject explorer APIs 41 | chains.map((chain) => { 42 | const match = explorerUrls.find( 43 | (explorer) => explorer.chainId === chain.chainId 44 | ); 45 | if (match) { 46 | chain.explorerApiUrl = match.url; 47 | } 48 | return chain; 49 | }); 50 | 51 | // Inject API keys 52 | chains.map((chain) => { 53 | chain.apiKey = apiKeys[chain.chainId]; 54 | return chain; 55 | }); 56 | 57 | // Only show chains with explorer APIs 58 | let filteredChains = chains.filter((chain) => chain.explorerApiUrl); 59 | 60 | // Blacklist 61 | const blacklistedChains = [ 62 | 8738, 2203, 2888, 113, 2213, 3501, 224168, 751230, 63 | ]; 64 | filteredChains = filteredChains.filter( 65 | (chain) => !blacklistedChains.includes(chain.networkId) 66 | ); 67 | 68 | // Alphabetical 69 | let sortedChains = filteredChains.sort((a, b) => 70 | a.name.localeCompare(b.name) 71 | ); 72 | 73 | // Preferred 74 | const preferredChainIds = [ 75 | 1, 42161, 56, 137, 10, 43114, 25, 250, 2222, 32659, 42220, 76 | ].reverse(); 77 | sortedChains = filteredChains.sort( 78 | (a, b) => 79 | preferredChainIds.indexOf(b.chainId) - 80 | preferredChainIds.indexOf(a.chainId) 81 | ); 82 | 83 | dispatch(setChains(sortedChains)); 84 | }; 85 | 86 | fetchData(); 87 | }, []); 88 | return children; 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/ChainSelector.js: -------------------------------------------------------------------------------- 1 | import { FormControl, Select, MenuItem, InputLabel } from "@mui/material"; 2 | import { 3 | useSelectChains, 4 | useSelectNetwork1, 5 | useSelectNetwork2, 6 | } from "../hooks"; 7 | import { setNetwork1, setNetwork2 } from "../store/options"; 8 | import { useDispatch } from "react-redux"; 9 | import { useEffect } from "react"; 10 | 11 | export default ({ field }) => { 12 | const network1 = useSelectNetwork1(); 13 | const network2 = useSelectNetwork2(); 14 | const chains = useSelectChains(); 15 | const network = field === 1 ? network1 : network2; 16 | const setNetwork = field === 1 ? setNetwork1 : setNetwork2; 17 | const dispatch = useDispatch(); 18 | const params = new Proxy(new URLSearchParams(window.location.search), { 19 | get: (searchParams, prop) => searchParams.get(prop), 20 | }); 21 | 22 | useEffect(() => { 23 | let networkParam = params[`chain${field}`]; 24 | if (!networkParam) { 25 | networkParam = 1; 26 | } 27 | dispatch(setNetwork(networkParam)); 28 | }, []); 29 | 30 | return ( 31 | 32 | {`Network ${field}`} 33 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/ExplorerApiEndpoints.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "chainId": 1, 4 | "url": "https://api.etherscan.io" 5 | }, 6 | { 7 | "chainId": 7, 8 | "url": "https://exp.thaichain.org" 9 | }, 10 | { 11 | "chainId": 10, 12 | "url": "https://api-optimistic.etherscan.io" 13 | }, 14 | { 15 | "chainId": 14, 16 | "url": "https://flare-explorer.flare.network" 17 | }, 18 | { 19 | "chainId": 16, 20 | "url": "https://coston-explorer.flare.network" 21 | }, 22 | { 23 | "chainId": 18, 24 | "url": "https://explorer-testnet.thundercore.com" 25 | }, 26 | { 27 | "chainId": 19, 28 | "url": "https://songbird-explorer.flare.network" 29 | }, 30 | { 31 | "chainId": 20, 32 | "url": "https://esc.elastos.io" 33 | }, 34 | { 35 | "chainId": 21, 36 | "url": "https://esc-testnet.elastos.io" 37 | }, 38 | { 39 | "chainId": 25, 40 | "url": "https://api.cronoscan.com" 41 | }, 42 | { 43 | "chainId": 39, 44 | "url": "https://testnet.uniultra.xyz" 45 | }, 46 | { 47 | "chainId": 47, 48 | "url": "https://explorer.acria.ai" 49 | }, 50 | { 51 | "chainId": 48, 52 | "url": "https://etmscan.network" 53 | }, 54 | { 55 | "chainId": 49, 56 | "url": "https://pioneer.etmscan.network" 57 | }, 58 | { 59 | "chainId": 55, 60 | "url": "https://zyxscan.com" 61 | }, 62 | { 63 | "chainId": 56, 64 | "url": "https://api.bscscan.com" 65 | }, 66 | { 67 | "chainId": 57, 68 | "url": "https://explorer.syscoin.org" 69 | }, 70 | { 71 | "chainId": 61, 72 | "url": "https://blockscout.com/etc/mainnet" 73 | }, 74 | { 75 | "chainId": 74, 76 | "url": "https://explorer.idchain.one" 77 | }, 78 | { 79 | "chainId": 77, 80 | "url": "https://blockscout.com/poa/sokol" 81 | }, 82 | { 83 | "chainId": 79, 84 | "url": "https://scan.zenithchain.co" 85 | }, 86 | { 87 | "chainId": 84, 88 | "url": "https://explorer.linqto-dev.com" 89 | }, 90 | { 91 | "chainId": 99, 92 | "url": "https://blockscout.com/poa/core" 93 | }, 94 | { 95 | "chainId": 100, 96 | "url": "https://blockscout.com/xdai/mainnet" 97 | }, 98 | { 99 | "chainId": 106, 100 | "url": "https://evmexplorer.velas.com" 101 | }, 102 | { 103 | "chainId": 112, 104 | "url": "https://coinbit-explorer.chain.sbcrypto.app" 105 | }, 106 | { 107 | "chainId": 113, 108 | "url": "https://explorer.dehvo.com" 109 | }, 110 | { 111 | "chainId": 114, 112 | "url": "https://coston2-explorer.flare.network" 113 | }, 114 | { 115 | "chainId": 117, 116 | "url": "https://evm-explorer.uptick.network" 117 | }, 118 | { 119 | "chainId": 119, 120 | "url": "https://evmscan.nuls.io" 121 | }, 122 | { 123 | "chainId": 120, 124 | "url": "https://beta.evmscan.nuls.io" 125 | }, 126 | { 127 | "chainId": 134, 128 | "url": "https://blockscout.bellecour.iex.ec" 129 | }, 130 | { 131 | "chainId": 137, 132 | "url": "https://api.polygonscan.com" 133 | }, 134 | { 135 | "chainId": 139, 136 | "url": "https://explorer.wikiwoop.com" 137 | }, 138 | { 139 | "chainId": 144, 140 | "url": "https://phiscan.com" 141 | }, 142 | { 143 | "chainId": 155, 144 | "url": "https://testnet.tenetscan.io" 145 | }, 146 | { 147 | "chainId": 161, 148 | "url": "https://testnet.evascan.io" 149 | }, 150 | { 151 | "chainId": 186, 152 | "url": "https://seeleview.net" 153 | }, 154 | { 155 | "chainId": 230, 156 | "url": "https://evm.swapdex.network" 157 | }, 158 | { 159 | "chainId": 246, 160 | "url": "https://explorer.energyweb.org" 161 | }, 162 | { 163 | "chainId": 250, 164 | "url": "https://api.ftmscan.com" 165 | }, 166 | { 167 | "chainId": 252, 168 | "url": "https://api.fraxscan.com" 169 | }, 170 | { 171 | "chainId": 274, 172 | "url": "https://explorer.lachain.network" 173 | }, 174 | { 175 | "chainId": 288, 176 | "url": "https://api.bobascan.com" 177 | }, 178 | { 179 | "chainId": 301, 180 | "url": "https://blockexplorer.bobaopera.boba.network" 181 | }, 182 | { 183 | "chainId": 322, 184 | "url": "https://scan-testnet.kcc.network" 185 | }, 186 | { 187 | "chainId": 416, 188 | "url": "https://explorer.sx.technology" 189 | }, 190 | { 191 | "chainId": 418, 192 | "url": "https://testexplorer.lachain.network" 193 | }, 194 | { 195 | "chainId": 444, 196 | "url": "https://testnet.frenscan.io" 197 | }, 198 | { 199 | "chainId": 534, 200 | "url": "https://candleexplorer.com" 201 | }, 202 | { 203 | "chainId": 555, 204 | "url": "https://exp.velaverse.io" 205 | }, 206 | { 207 | "chainId": 599, 208 | "url": "https://goerli.explorer.metisdevops.link" 209 | }, 210 | { 211 | "chainId": 647, 212 | "url": "https://explorer.toronto.sx.technology" 213 | }, 214 | { 215 | "chainId": 740, 216 | "url": "https://testnet-explorer.canto.neobase.one" 217 | }, 218 | { 219 | "chainId": 741, 220 | "url": "https://testnet.ventionscan.io" 221 | }, 222 | { 223 | "chainId": 1000, 224 | "url": "https://explorer.gton.network" 225 | }, 226 | { 227 | "chainId": 1004, 228 | "url": "https://test.ektascan.io" 229 | }, 230 | { 231 | "chainId": 1071, 232 | "url": "https://explorer.evm.testnet.shimmer.network" 233 | }, 234 | { 235 | "chainId": 1088, 236 | "url": "https://andromeda-explorer.metis.io" 237 | }, 238 | { 239 | "chainId": 1101, 240 | "url": "https://api-zkevm.polygonscan.com" 241 | }, 242 | { 243 | "chainId": 1117, 244 | "url": "https://explorer.dogcoin.network" 245 | }, 246 | { 247 | "chainId": 1138, 248 | "url": "https://testnet.amstarscan.com" 249 | }, 250 | { 251 | "chainId": 1149, 252 | "url": "https://explorer.plexfinance.us" 253 | }, 254 | { 255 | "chainId": 1234, 256 | "url": "https://stepscan.io" 257 | }, 258 | { 259 | "chainId": 1243, 260 | "url": "https://app.archiescan.io" 261 | }, 262 | { 263 | "chainId": 1244, 264 | "url": "https://testnet.archiescan.io" 265 | }, 266 | { 267 | "chainId": 1246, 268 | "url": "https://omscan.omplatform.com" 269 | }, 270 | { 271 | "chainId": 1280, 272 | "url": "https://browser.halo.land" 273 | }, 274 | { 275 | "chainId": 1284, 276 | "url": "https://api-moonbeam.moonscan.io" 277 | }, 278 | { 279 | "chainId": 1285, 280 | "url": "https://api-moonriver.moonscan.io" 281 | }, 282 | { 283 | "chainId": 1294, 284 | "url": "https://blockexplorer.bobabeam.boba.network" 285 | }, 286 | { 287 | "chainId": 1297, 288 | "url": "https://blockexplorer.bobabase.boba.network" 289 | }, 290 | { 291 | "chainId": 1311, 292 | "url": "https://test.doscan.io" 293 | }, 294 | { 295 | "chainId": 1314, 296 | "url": "https://www.alyxscan.com" 297 | }, 298 | { 299 | "chainId": 1319, 300 | "url": "https://aitd-explorer-new.aitd.io" 301 | }, 302 | { 303 | "chainId": 1388, 304 | "url": "https://mainnet.amstarscan.com" 305 | }, 306 | { 307 | "chainId": 1402, 308 | "url": "https://explorer.public.zkevm-test.net" 309 | }, 310 | { 311 | "chainId": 1422, 312 | "url": "https://explorer.public.zkevm-test.net" 313 | }, 314 | { 315 | "chainId": 1433, 316 | "url": "https://rikscan.com" 317 | }, 318 | { 319 | "chainId": 1442, 320 | "url": "https://explorer.public.zkevm-test.net" 321 | }, 322 | { 323 | "chainId": 1452, 324 | "url": "https://explorer.giltestnet.com" 325 | }, 326 | { 327 | "chainId": 1455, 328 | "url": "https://ctexscan.com" 329 | }, 330 | { 331 | "chainId": 1559, 332 | "url": "https://tenetscan.io" 333 | }, 334 | { 335 | "chainId": 1662, 336 | "url": "https://yuma-explorer.horizen.io" 337 | }, 338 | { 339 | "chainId": 1663, 340 | "url": "https://gobi-explorer.horizen.io" 341 | }, 342 | { 343 | "chainId": 1777, 344 | "url": "https://explorer.gaussgang.com" 345 | }, 346 | { 347 | "chainId": 1881, 348 | "url": "https://scan.cartenz.works" 349 | }, 350 | { 351 | "chainId": 1890, 352 | "url": "https://phoenix.lightlink.io" 353 | }, 354 | { 355 | "chainId": 1891, 356 | "url": "https://pegasus.lightlink.io" 357 | }, 358 | { 359 | "chainId": 1908, 360 | "url": "https://testnet.bitciexplorer.com" 361 | }, 362 | { 363 | "chainId": 1945, 364 | "url": "https://explorer-testnet.onuschain.io" 365 | }, 366 | { 367 | "chainId": 1969, 368 | "url": "https://testnetscan.superexchain.com" 369 | }, 370 | { 371 | "chainId": 1970, 372 | "url": "https://scan.superexchain.com" 373 | }, 374 | { 375 | "chainId": 1975, 376 | "url": "https://explorer.onuschain.io" 377 | }, 378 | { 379 | "chainId": 1994, 380 | "url": "https://ektascan.io" 381 | }, 382 | { 383 | "chainId": 1995, 384 | "url": "https://explorer.testnet.edexa.com" 385 | }, 386 | { 387 | "chainId": 2000, 388 | "url": "https://explorer.dogechain.dog" 389 | }, 390 | { 391 | "chainId": 2001, 392 | "url": "https://explorer-mainnet-cardano-evm.c1.milkomeda.com" 393 | }, 394 | { 395 | "chainId": 2002, 396 | "url": "https://explorer-mainnet-algorand-rollup.a1.milkomeda.com" 397 | }, 398 | { 399 | "chainId": 2008, 400 | "url": "https://explorer.testnet.cloudwalk.io" 401 | }, 402 | { 403 | "chainId": 2009, 404 | "url": "https://explorer.mainnet.cloudwalk.io" 405 | }, 406 | { 407 | "chainId": 2016, 408 | "url": "https://explorer.mainnetz.io" 409 | }, 410 | { 411 | "chainId": 2018, 412 | "url": "https://explorer.dev.publicmint.io" 413 | }, 414 | { 415 | "chainId": 2019, 416 | "url": "https://explorer.tst.publicmint.io" 417 | }, 418 | { 419 | "chainId": 2020, 420 | "url": "https://explorer.publicmint.io" 421 | }, 422 | { 423 | "chainId": 2021, 424 | "url": "https://edgscan.live" 425 | }, 426 | { 427 | "chainId": 2047, 428 | "url": "https://web3-testnet-explorer.thestratos.org" 429 | }, 430 | { 431 | "chainId": 2077, 432 | "url": "https://explorer.qkacoin.org" 433 | }, 434 | { 435 | "chainId": 2109, 436 | "url": "https://explorer.exosama.com" 437 | }, 438 | { 439 | "chainId": 2122, 440 | "url": "https://scan.metaplayer.one" 441 | }, 442 | { 443 | "chainId": 2124, 444 | "url": "https://dubai.mp1scan.io" 445 | }, 446 | { 447 | "chainId": 2151, 448 | "url": "https://boascan.io" 449 | }, 450 | { 451 | "chainId": 2152, 452 | "url": "https://evm.findorascan.io" 453 | }, 454 | { 455 | "chainId": 2153, 456 | "url": "https://testnet-anvil.evm.findorascan.io" 457 | }, 458 | { 459 | "chainId": 2203, 460 | "url": "https://explorer.bitcoinevm.com" 461 | }, 462 | { 463 | "chainId": 2213, 464 | "url": "https://explorer.evanesco.org" 465 | }, 466 | { 467 | "chainId": 2221, 468 | "url": "https://explorer.testnet.kava.io" 469 | }, 470 | { 471 | "chainId": 2222, 472 | "url": "https://explorer.kava.io" 473 | }, 474 | { 475 | "chainId": 2300, 476 | "url": "https://bombscan.com" 477 | }, 478 | { 479 | "chainId": 2357, 480 | "url": "https://blockscout.sepolia.kroma.network" 481 | }, 482 | { 483 | "chainId": 2399, 484 | "url": "https://explorer.bombchain-testnet.ankr.com" 485 | }, 486 | { 487 | "chainId": 2400, 488 | "url": "https://explorer.tcgverse.xyz" 489 | }, 490 | { 491 | "chainId": 2569, 492 | "url": "https://tpcscan.com" 493 | }, 494 | { 495 | "chainId": 2611, 496 | "url": "https://redlightscan.finance" 497 | }, 498 | { 499 | "chainId": 2888, 500 | "url": "https://testnet.bobascan.com" 501 | }, 502 | { 503 | "chainId": 3068, 504 | "url": "https://explorer.mainnet.thebifrost.io" 505 | }, 506 | { 507 | "chainId": 3306, 508 | "url": "https://explorer.debounce.network" 509 | }, 510 | { 511 | "chainId": 3333, 512 | "url": "https://explorer.testnet.web3q.io" 513 | }, 514 | { 515 | "chainId": 3334, 516 | "url": "https://explorer.galileo.web3q.io" 517 | }, 518 | { 519 | "chainId": 3400, 520 | "url": "https://explorer.paribu.network" 521 | }, 522 | { 523 | "chainId": 3500, 524 | "url": "https://testnet.paribuscan.com" 525 | }, 526 | { 527 | "chainId": 3501, 528 | "url": "https://exp.jfinchain.com" 529 | }, 530 | { 531 | "chainId": 3693, 532 | "url": "https://explorer.empirenetwork.io" 533 | }, 534 | { 535 | "chainId": 3737, 536 | "url": "https://scan.crossbell.io" 537 | }, 538 | { 539 | "chainId": 3797, 540 | "url": "https://alveyscan.com" 541 | }, 542 | { 543 | "chainId": 3939, 544 | "url": "https://test.doscan.io" 545 | }, 546 | { 547 | "chainId": 4051, 548 | "url": "https://blockexplorer.testnet.bobaopera.boba.network" 549 | }, 550 | { 551 | "chainId": 4062, 552 | "url": "https://explorer.testnet.n3.nahmii.io" 553 | }, 554 | { 555 | "chainId": 4090, 556 | "url": "https://oasis.ftnscan.com" 557 | }, 558 | { 559 | "chainId": 4096, 560 | "url": "https://testnet.bitindiscan.com" 561 | }, 562 | { 563 | "chainId": 4099, 564 | "url": "https://bitindiscan.com" 565 | }, 566 | { 567 | "chainId": 4141, 568 | "url": "https://testnet.tipboxcoin.net" 569 | }, 570 | { 571 | "chainId": 4328, 572 | "url": "https://blockexplorer.testnet.avax.boba.network" 573 | }, 574 | { 575 | "chainId": 4777, 576 | "url": "https://testnet-explorer.blackfort.network" 577 | }, 578 | { 579 | "chainId": 4918, 580 | "url": "https://evm-testnet.venidiumexplorer.com" 581 | }, 582 | { 583 | "chainId": 4919, 584 | "url": "https://evm.venidiumexplorer.com" 585 | }, 586 | { 587 | "chainId": 4999, 588 | "url": "https://explorer.blackfort.network" 589 | }, 590 | { 591 | "chainId": 5001, 592 | "url": "https://explorer.testnet.mantle.xyz" 593 | }, 594 | { 595 | "chainId": 5551, 596 | "url": "https://explorer.nahmii.io" 597 | }, 598 | { 599 | "chainId": 5555, 600 | "url": "https://explorer.chainverse.info" 601 | }, 602 | { 603 | "chainId": 5700, 604 | "url": "https://tanenbaum.io" 605 | }, 606 | { 607 | "chainId": 5729, 608 | "url": "https://scan-testnet.hika.network" 609 | }, 610 | { 611 | "chainId": 5758, 612 | "url": "https://satoshiscan.io" 613 | }, 614 | { 615 | "chainId": 6065, 616 | "url": "https://explorer-test.tresleches.finance" 617 | }, 618 | { 619 | "chainId": 6066, 620 | "url": "https://explorer.tresleches.finance" 621 | }, 622 | { 623 | "chainId": 6552, 624 | "url": "https://testnet-explorer.scolcoin.com" 625 | }, 626 | { 627 | "chainId": 6626, 628 | "url": "https://scan.chain.pixie.xyz" 629 | }, 630 | { 631 | "chainId": 6969, 632 | "url": "https://tombscout.com" 633 | }, 634 | { 635 | "chainId": 7070, 636 | "url": "https://evm.planq.network" 637 | }, 638 | { 639 | "chainId": 7575, 640 | "url": "https://testnet.adilchain-scan.io" 641 | }, 642 | { 643 | "chainId": 7576, 644 | "url": "https://adilchain-scan.io" 645 | }, 646 | { 647 | "chainId": 7700, 648 | "url": "https://tuber.build" 649 | }, 650 | { 651 | "chainId": 7701, 652 | "url": "https://testnet.tuber.build" 653 | }, 654 | { 655 | "chainId": 7979, 656 | "url": "https://doscan.io" 657 | }, 658 | { 659 | "chainId": 8453, 660 | "url": "https://api.basescan.org" 661 | }, 662 | { 663 | "chainId": 8738, 664 | "url": "https://explorer.alph.network" 665 | }, 666 | { 667 | "chainId": 8888, 668 | "url": "https://xanachain.xana.net" 669 | }, 670 | { 671 | "chainId": 8898, 672 | "url": "https://mmtscan.io" 673 | }, 674 | { 675 | "chainId": 8899, 676 | "url": "https://exp-l1.jibchain.net" 677 | }, 678 | { 679 | "chainId": 8989, 680 | "url": "https://scan.gmmtchain.io" 681 | }, 682 | { 683 | "chainId": 9000, 684 | "url": "https://evm.evmos.dev" 685 | }, 686 | { 687 | "chainId": 9339, 688 | "url": "https://testnet.dogcoin.network" 689 | }, 690 | { 691 | "chainId": 9728, 692 | "url": "https://blockexplorer.testnet.bnb.boba.network" 693 | }, 694 | { 695 | "chainId": 9768, 696 | "url": "https://testnet.mainnetz.io" 697 | }, 698 | { 699 | "chainId": 9997, 700 | "url": "https://testnet-rollup-explorer.altlayer.io" 701 | }, 702 | { 703 | "chainId": 10024, 704 | "url": "https://gonscan.com" 705 | }, 706 | { 707 | "chainId": 10200, 708 | "url": "https://blockscout.chiadochain.net" 709 | }, 710 | { 711 | "chainId": 10946, 712 | "url": "https://explorer.quadrans.io" 713 | }, 714 | { 715 | "chainId": 10947, 716 | "url": "https://explorer.testnet.quadrans.io" 717 | }, 718 | { 719 | "chainId": 11119, 720 | "url": "https://explorer.hashbit.org" 721 | }, 722 | { 723 | "chainId": 11235, 724 | "url": "https://explorer.haqq.network" 725 | }, 726 | { 727 | "chainId": 11437, 728 | "url": "https://bx.testnet.shyft.network" 729 | }, 730 | { 731 | "chainId": 11612, 732 | "url": "https://testnet.sardisnetwork.com" 733 | }, 734 | { 735 | "chainId": 12345, 736 | "url": "https://testnet.stepscan.io" 737 | }, 738 | { 739 | "chainId": 12715, 740 | "url": "https://testnet.rikscan.com" 741 | }, 742 | { 743 | "chainId": 13381, 744 | "url": "https://phoenixplorer.com" 745 | }, 746 | { 747 | "chainId": 15557, 748 | "url": "https://explorer.testnet.evm.eosnetwork.com" 749 | }, 750 | { 751 | "chainId": 16888, 752 | "url": "https://testnet.ivarscan.com" 753 | }, 754 | { 755 | "chainId": 18159, 756 | "url": "https://memescan.io" 757 | }, 758 | { 759 | "chainId": 19011, 760 | "url": "https://explorer.oasys.homeverse.games" 761 | }, 762 | { 763 | "chainId": 19845, 764 | "url": "https://btcixscan.com" 765 | }, 766 | { 767 | "chainId": 20001, 768 | "url": "https://scan.camelark.com" 769 | }, 770 | { 771 | "chainId": 20736, 772 | "url": "https://explorer.p12.games" 773 | }, 774 | { 775 | "chainId": 21816, 776 | "url": "https://explorer.omchain.io" 777 | }, 778 | { 779 | "chainId": 22023, 780 | "url": "https://taycan-evmscan.hupayx.io" 781 | }, 782 | { 783 | "chainId": 23118, 784 | "url": "https://opside.info" 785 | }, 786 | { 787 | "chainId": 23294, 788 | "url": "https://explorer.sapphire.oasis.io" 789 | }, 790 | { 791 | "chainId": 23295, 792 | "url": "https://testnet.explorer.sapphire.oasis.dev" 793 | }, 794 | { 795 | "chainId": 25888, 796 | "url": "https://www.hammerchain.io" 797 | }, 798 | { 799 | "chainId": 25925, 800 | "url": "https://testnet.bkcscan.com" 801 | }, 802 | { 803 | "chainId": 26863, 804 | "url": "https://scan.oasischain.io" 805 | }, 806 | { 807 | "chainId": 31223, 808 | "url": "https://scan.cloudtx.finance" 809 | }, 810 | { 811 | "chainId": 31224, 812 | "url": "https://explorer.cloudtx.finance" 813 | }, 814 | { 815 | "chainId": 33333, 816 | "url": "https://avescan.io" 817 | }, 818 | { 819 | "chainId": 35011, 820 | "url": "https://exp.j2o.io" 821 | }, 822 | { 823 | "chainId": 35441, 824 | "url": "https://explorer.q.org" 825 | }, 826 | { 827 | "chainId": 35443, 828 | "url": "https://explorer.qtestnet.org" 829 | }, 830 | { 831 | "chainId": 39815, 832 | "url": "https://ohoscan.com" 833 | }, 834 | { 835 | "chainId": 42161, 836 | "url": "https://api.arbiscan.io" 837 | }, 838 | { 839 | "chainId": 42220, 840 | "url": "https://explorer.celo.org/mainnet" 841 | }, 842 | { 843 | "chainId": 42261, 844 | "url": "https://testnet.explorer.emerald.oasis.dev" 845 | }, 846 | { 847 | "chainId": 42262, 848 | "url": "https://explorer.emerald.oasis.dev" 849 | }, 850 | { 851 | "chainId": 43114, 852 | "url": "https://api.snowtrace.io" 853 | }, 854 | { 855 | "chainId": 43288, 856 | "url": "https://blockexplorer.avax.boba.network" 857 | }, 858 | { 859 | "chainId": 44444, 860 | "url": "https://frenscan.io" 861 | }, 862 | { 863 | "chainId": 44787, 864 | "url": "https://api.celoscan.io" 865 | }, 866 | { 867 | "chainId": 45000, 868 | "url": "https://explorer.autobahn.network" 869 | }, 870 | { 871 | "chainId": 47805, 872 | "url": "https://scan.rei.network" 873 | }, 874 | { 875 | "chainId": 49049, 876 | "url": "https://floripa-explorer.wireshape.org" 877 | }, 878 | { 879 | "chainId": 49088, 880 | "url": "https://explorer.testnet.thebifrost.io" 881 | }, 882 | { 883 | "chainId": 50021, 884 | "url": "https://explorer.testnet.gton.network" 885 | }, 886 | { 887 | "chainId": 51712, 888 | "url": "https://contract-mainnet.sardisnetwork.com" 889 | }, 890 | { 891 | "chainId": 54211, 892 | "url": "https://explorer.testedge2.haqq.network" 893 | }, 894 | { 895 | "chainId": 55555, 896 | "url": "https://reiscan.com" 897 | }, 898 | { 899 | "chainId": 55556, 900 | "url": "https://testnet.reiscan.com" 901 | }, 902 | { 903 | "chainId": 57000, 904 | "url": "https://rollux.tanenbaum.io" 905 | }, 906 | { 907 | "chainId": 59140, 908 | "url": "https://explorer.goerli.linea.build" 909 | }, 910 | { 911 | "chainId": 63000, 912 | "url": "https://explorer.ecredits.com" 913 | }, 914 | { 915 | "chainId": 63001, 916 | "url": "https://explorer.tst.ecredits.com" 917 | }, 918 | { 919 | "chainId": 65450, 920 | "url": "https://explorer.scolcoin.com" 921 | }, 922 | { 923 | "chainId": 73927, 924 | "url": "https://scan.mvm.dev" 925 | }, 926 | { 927 | "chainId": 77612, 928 | "url": "https://ventionscan.io" 929 | }, 930 | { 931 | "chainId": 84531, 932 | "url": "https://base-goerli.blockscout.com" 933 | }, 934 | { 935 | "chainId": 88880, 936 | "url": "https://scoville-explorer.chiliz.com" 937 | }, 938 | { 939 | "chainId": 88888, 940 | "url": "https://ivarscan.com" 941 | }, 942 | { 943 | "chainId": 92001, 944 | "url": "https://explorer.lambda.top" 945 | }, 946 | { 947 | "chainId": 101010, 948 | "url": "https://testnet.soverun.com" 949 | }, 950 | { 951 | "chainId": 108801, 952 | "url": "https://explorer.brochain.org" 953 | }, 954 | { 955 | "chainId": 111111, 956 | "url": "https://explorer.main.siberium.net.ru" 957 | }, 958 | { 959 | "chainId": 123456, 960 | "url": "https://devnet.adilchain-scan.io" 961 | }, 962 | { 963 | "chainId": 200101, 964 | "url": "https://explorer-devnet-cardano-evm.c1.milkomeda.com" 965 | }, 966 | { 967 | "chainId": 200202, 968 | "url": "https://explorer-devnet-algorand-rollup.a1.milkomeda.com" 969 | }, 970 | { 971 | "chainId": 202624, 972 | "url": "https://jellie.twala.io" 973 | }, 974 | { 975 | "chainId": 224168, 976 | "url": "https://ecoscan.tafchain.com" 977 | }, 978 | { 979 | "chainId": 247253, 980 | "url": "https://explorer-testnet.saakuru.network" 981 | }, 982 | { 983 | "chainId": 330844, 984 | "url": "https://tscscan.com" 985 | }, 986 | { 987 | "chainId": 373737, 988 | "url": "https://blockscout-test.hap.land" 989 | }, 990 | { 991 | "chainId": 381931, 992 | "url": "https://metalscan.io" 993 | }, 994 | { 995 | "chainId": 381932, 996 | "url": "https://tahoe.metalscan.io" 997 | }, 998 | { 999 | "chainId": 404040, 1000 | "url": "https://tipboxcoin.net" 1001 | }, 1002 | { 1003 | "chainId": 420420, 1004 | "url": "https://mainnet-explorer.kekchain.com" 1005 | }, 1006 | { 1007 | "chainId": 424242, 1008 | "url": "https://testnet.ftnscan.com" 1009 | }, 1010 | { 1011 | "chainId": 534353, 1012 | "url": "https://blockscout.scroll.io" 1013 | }, 1014 | { 1015 | "chainId": 535037, 1016 | "url": "https://Bescscan.io" 1017 | }, 1018 | { 1019 | "chainId": 641230, 1020 | "url": "https://brnkscan.bearnetwork.net" 1021 | }, 1022 | { 1023 | "chainId": 751230, 1024 | "url": "https://brnktest-scan.bearnetwork.net" 1025 | }, 1026 | { 1027 | "chainId": 800001, 1028 | "url": "https://explorer.octa.space" 1029 | }, 1030 | { 1031 | "chainId": 910000, 1032 | "url": "https://explorer-testnet.posichain.org" 1033 | }, 1034 | { 1035 | "chainId": 1313114, 1036 | "url": "https://explorer.ethoprotocol.com" 1037 | }, 1038 | { 1039 | "chainId": 7225878, 1040 | "url": "https://explorer.saakuru.network" 1041 | }, 1042 | { 1043 | "chainId": 7668378, 1044 | "url": "https://testnet.qom.one" 1045 | }, 1046 | { 1047 | "chainId": 7777777, 1048 | "url": "https://explorer.zora.energy" 1049 | }, 1050 | { 1051 | "chainId": 8794598, 1052 | "url": "https://blockscout.hap.land" 1053 | }, 1054 | { 1055 | "chainId": 10101010, 1056 | "url": "https://explorer.soverun.com" 1057 | }, 1058 | { 1059 | "chainId": 27082017, 1060 | "url": "https://testnet-explorer.exlscan.com" 1061 | }, 1062 | { 1063 | "chainId": 27082022, 1064 | "url": "https://exlscan.com" 1065 | }, 1066 | { 1067 | "chainId": 65010000, 1068 | "url": "https://bakerloo.autonity.org" 1069 | }, 1070 | { 1071 | "chainId": 65100000, 1072 | "url": "https://piccadilly.autonity.org" 1073 | }, 1074 | { 1075 | "chainId": 88888888, 1076 | "url": "https://teamblockchain.team" 1077 | }, 1078 | { 1079 | "chainId": 192837465, 1080 | "url": "https://explorer.gather.network" 1081 | }, 1082 | { 1083 | "chainId": 278611351, 1084 | "url": "https://turbulent-unique-scheat.explorer.mainnet.skalenodes.com" 1085 | }, 1086 | { 1087 | "chainId": 344106930, 1088 | "url": "https://staging-utter-unripe-menkar.explorer.staging-v3.skalenodes.com" 1089 | }, 1090 | { 1091 | "chainId": 356256156, 1092 | "url": "https://testnet-explorer.gather.network" 1093 | }, 1094 | { 1095 | "chainId": 503129905, 1096 | "url": "https://staging-faint-slimy-achird.explorer.staging-v3.skalenodes.com" 1097 | }, 1098 | { 1099 | "chainId": 1273227453, 1100 | "url": "https://wan-red-ain.explorer.mainnet.skalenodes.com" 1101 | }, 1102 | { 1103 | "chainId": 1351057110, 1104 | "url": "https://staging-fast-active-bellatrix.explorer.staging-v3.skalenodes.com" 1105 | }, 1106 | { 1107 | "chainId": 1482601649, 1108 | "url": "https://green-giddy-denebola.explorer.mainnet.skalenodes.com" 1109 | }, 1110 | { 1111 | "chainId": 1564830818, 1112 | "url": "https://honorable-steel-rasalhague.explorer.mainnet.skalenodes.com" 1113 | }, 1114 | { 1115 | "chainId": 2046399126, 1116 | "url": "https://elated-tan-skat.explorer.mainnet.skalenodes.com" 1117 | }, 1118 | { 1119 | "chainId": 11297108099, 1120 | "url": "https://explorer.palm-uat.xyz" 1121 | }, 1122 | { 1123 | "chainId": 11297108109, 1124 | "url": "https://explorer.palm.io" 1125 | }, 1126 | { 1127 | "chainId": 111222333444, 1128 | "url": "https://scan.alphabetnetwork.org" 1129 | }, 1130 | { 1131 | "chainId": 197710212031, 1132 | "url": "https://blockscout.haradev.com" 1133 | }, 1134 | { 1135 | "chainId": 666301171999, 1136 | "url": "https://scan.ipdc.io" 1137 | } 1138 | ] 1139 | -------------------------------------------------------------------------------- /src/components/FileDiff.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import ReactDiffViewer from "react-diff-viewer-continued"; 3 | import { 4 | List, 5 | ListItemButton, 6 | ListItemText, 7 | ListItem, 8 | ListItemIcon, 9 | Box, 10 | IconButton, 11 | Tooltip, 12 | } from "@mui/material"; 13 | import { 14 | ExpandMore, 15 | UnfoldMore, 16 | MoreHoriz, 17 | ContentCopy, 18 | OpenInNew, 19 | } from "@mui/icons-material"; 20 | import { useState } from "react"; 21 | import { 22 | useSelectExplorer1, 23 | useSelectExplorer2, 24 | useSelectSelectedFile, 25 | } from "../hooks"; 26 | import { shortenAddress } from "../utils/string"; 27 | 28 | const TitleWrapper = styled.div` 29 | display: inline-block; 30 | white-space: nowrap; 31 | overflow: hidden; 32 | text-overflow: ellipsis; 33 | left: 80px; 34 | position: absolute; 35 | right: 30px; 36 | `; 37 | 38 | const AddressWrap = styled.div` 39 | display: flex; 40 | justify-content: space-between; 41 | color: rgb(153, 153, 153); 42 | left: 5px; 43 | position: relative; 44 | height: 25px; 45 | `; 46 | 47 | const MoreWrap = styled.div` 48 | position: absolute; 49 | right: 0px; 50 | top: -4px; 51 | `; 52 | 53 | const AddressTitleWrap = styled.div` 54 | position: absolute; 55 | left: 0; 56 | `; 57 | 58 | const SourceHeader = styled.div` 59 | background-color: #121519; 60 | width: 100%; 61 | line-height: 32px; 62 | color: white; 63 | padding: 10px 20px; 64 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, 65 | Liberation Mono, monospace; 66 | display: flex; 67 | position: relative; 68 | justify-content: space-between; 69 | `; 70 | 71 | const FileMore = styled.div` 72 | padding-right: 10px; 73 | opacity: 0.5; 74 | `; 75 | 76 | const Wrapper = styled.div` 77 | margin-bottom: 20px; 78 | border-radius: 6px; 79 | border: ${(props) => 80 | props.selected ? "2px solid #2f81f7" : "1px solid #30363d"}; 81 | overflow: hidden; 82 | `; 83 | 84 | const HideIfCollapsed = styled.div` 85 | display: ${(props) => (props.collapsed === "true" ? "none" : "")}; 86 | `; 87 | 88 | const customStyles = { 89 | // diffContainer: { padding: '0px', margin: '0px' }, 90 | diffRemoved: { 91 | "padding-top": "0px", 92 | "padding-bottom": "0px", 93 | "margin-top": "0px", 94 | "margin-bottom": "0px", 95 | }, 96 | diffAdded: { 97 | "padding-top": "0px", 98 | "padding-bottom": "0px", 99 | "margin-top": "0px", 100 | "margin-bottom": "0px", 101 | }, 102 | // marker: { padding: "0px", margin: "0px" }, 103 | // emptyGutter: { padding: "0px", margin: "0px" }, 104 | highlightedLine: { padding: "0px", margin: "0px" }, 105 | // lineNumber: { padding: "0px", margin: "0px" }, 106 | // highlightedGutter: { padding: "0px", margin: "0px" }, 107 | // contentText: { padding: "0px", margin: "0px" }, 108 | // gutter: { padding: "0px", margin: "0px" }, 109 | // line: { padding: "0px", margin: "0px" }, 110 | wordDiff: { padding: "0px", margin: "0px" }, 111 | wordAdded: { padding: "0px", margin: "0px" }, 112 | wordRemoved: { padding: "0px", margin: "0px" }, 113 | // codeFoldGutter: { padding: "0px", margin: "0px" }, 114 | codeFold: { padding: "0px", margin: "0px" }, 115 | // emptyLine: { padding: "0px", margin: "0px" }, 116 | // content: { padding: "0px", margin: "0px" }, 117 | // titleBlock: { padding: "0px", margin: "0px" }, 118 | // splitView: { padding: "0px", margin: "0px" }, 119 | }; 120 | 121 | const codeFoldMessageRenderer = (_str) => { 122 | return ( 123 | 124 | 125 | 126 | 127 | 128 | ); 129 | }; 130 | 131 | // eslint-disable-next-line 132 | const highlightSyntax = (str) => ( 133 |
140 | );
141 | 
142 | const copy = (text) => {
143 |   navigator.clipboard.writeText(text);
144 | };
145 | 
146 | export default ({
147 |   oldCode,
148 |   newCode,
149 |   splitView,
150 |   fileName,
151 |   address1,
152 |   address2,
153 |   similarity,
154 | }) => {
155 |   const [collapsed, setCollapsed] = useState("false");
156 |   const [expandAll, setExpandAll] = useState(false);
157 | 
158 |   const toggleCollapsed = () => {
159 |     setCollapsed(collapsed === "true" ? "false" : "true");
160 |   };
161 | 
162 |   const explorer1 = useSelectExplorer1();
163 |   const explorer2 = useSelectExplorer2();
164 | 
165 |   console.log(fileName, similarity * 100);
166 |   // Never round similarity up to 100%
167 |   const similarityDigits = similarity * 100 >= 99.5 ? 1 : 0;
168 | 
169 |   const renderAddress = (address, field, source) => (
170 |     
171 |       
172 |         {shortenAddress(address, 10)}
173 |       
174 |       {address && (
175 |         
176 |           
194 |                 
195 |                   
196 |                     
206 |                      copy(source)}>
207 |                       
208 |                         
209 |                       
210 |                       
211 |                     
212 |                   
213 | 
214 |                   
215 |                      {
217 |                         window
218 |                           .open(
219 |                             `${
220 |                               field === 1 ? explorer1 : explorer2
221 |                             }/address/${address}#code`,
222 |                             "_blank"
223 |                           )
224 |                           .focus();
225 |                       }}
226 |                     >
227 |                       
228 |                         
229 |                       
230 |                       
231 |                     
232 |                   
233 |                 
234 |               
235 |             }
236 |           >
237 |             
238 |               
239 |                 
240 |               
241 |             
242 |           
243 |         
244 |       )}
245 |     
246 |   );
247 | 
248 |   const selectedFile = useSelectSelectedFile();
249 |   return (
250 |     
251 |       
252 |         
253 | 257 | 266 | 273 | 274 | 275 | 276 | setExpandAll(true)} 284 | > 285 | 291 | 292 | 293 | 294 | {fileName} ({(similarity * 100).toFixed(similarityDigits)}% match) 295 | 296 |
297 |
298 | 299 | 300 | 316 | 317 |
318 | ); 319 | }; 320 | -------------------------------------------------------------------------------- /src/components/FileList.js: -------------------------------------------------------------------------------- 1 | import { SvgIcon } from "@mui/material"; 2 | import styled from "styled-components"; 3 | import Filter from "./Filter"; 4 | import { getEndOfPath, highlight } from "../utils/string"; 5 | import { useState } from "react"; 6 | import { setSelectedFile } from "../store/options"; 7 | import { useDispatch } from "react-redux"; 8 | 9 | const LeftNav = styled.div` 10 | width: 290px; 11 | height: 79px; 12 | position: sticky; 13 | top: 79px; 14 | display: ${(props) => (props.hidefiles === "true" ? "none" : "grid")}; 15 | 16 | @media (max-width: 990px) { 17 | display: none; 18 | } 19 | grid-template-rows: 60px auto; 20 | `; 21 | 22 | const FileList = styled.div` 23 | overflow-y: auto; 24 | overflow-x: hidden; 25 | `; 26 | 27 | const FileHeader = styled.div` 28 | height: 35px; 29 | display: flex; 30 | justify-content: space-between; 31 | cursor: pointer; 32 | &:hover { 33 | background-color: #333; 34 | } 35 | user-select: none; 36 | align-items: center; 37 | padding: 0px 10px; 38 | `; 39 | 40 | const FileName = styled.div` 41 | > div { 42 | max-width: 220px; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | white-space: nowrap; 46 | } 47 | `; 48 | 49 | const EditedShift = styled.div` 50 | position: relative; 51 | left: 11px; 52 | top: 7px; 53 | `; 54 | 55 | const EditedIcon = ( 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | 63 | const AddedIcon = ( 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | 71 | const RemovedIcon = ( 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | 79 | export default ({ 80 | hidefiles, 81 | contracts, 82 | filteredContracts, 83 | setFilteredContracts, 84 | fileDiffCounts, 85 | }) => { 86 | const [filter, setFilter] = useState(""); 87 | const dispatch = useDispatch(); 88 | const scrollTo = (id) => { 89 | const target = document.getElementById(id).getBoundingClientRect(); 90 | dispatch(setSelectedFile(id)); 91 | window.scroll(0, window.scrollY + target.top - 79 - 2); 92 | }; 93 | 94 | const files = ( 95 | 96 | {filteredContracts.map((contract) => { 97 | const fileName = getEndOfPath(contract.name); 98 | const diffs = fileDiffCounts[contract.name] || {}; 99 | const { _added, _removed, modificationType } = diffs; 100 | return ( 101 | scrollTo(contract.name)} 105 | > 106 | {highlight(filter, fileName)} 107 | {modificationType === "added" 108 | ? AddedIcon 109 | : modificationType === "removed" 110 | ? RemovedIcon 111 | : EditedIcon} 112 | 113 | ); 114 | })} 115 | 116 | ); 117 | 118 | return ( 119 | 120 | 126 | {files} 127 | 128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /src/components/Filter.js: -------------------------------------------------------------------------------- 1 | import { TextField, IconButton, InputAdornment } from "@mui/material"; 2 | import styled from "styled-components"; 3 | import { Search } from "@mui/icons-material"; 4 | import { useEffect } from "react"; 5 | import { getEndOfPath } from "../utils/string"; 6 | 7 | const SearchIconWrapper = styled.div` 8 | position: relative; 9 | width: 15px; 10 | left: -7px; 11 | `; 12 | 13 | export default ({ contracts, setFilteredContracts, filter, setFilter }) => { 14 | useEffect(() => { 15 | if (filter === "") { 16 | setFilteredContracts(contracts); 17 | return; 18 | } 19 | const filtered = []; 20 | contracts.forEach((contract) => { 21 | const fileName = getEndOfPath(contract.name); 22 | if (fileName.toLowerCase().indexOf(filter.toLowerCase()) !== -1) { 23 | filtered.push(contract); 24 | } 25 | setFilteredContracts(filtered); 26 | }); 27 | }, [filter, contracts]); 28 | return ( 29 | setFilter(evt.target.value)} 37 | InputProps={{ 38 | placeholder: "Filter changed files", 39 | startAdornment: ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ), 48 | }} 49 | /> 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/HighlightSyntaxSelector.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { setHighlightSyntax } from "../store/options.js"; 3 | import { useHighlightSyntax } from "../hooks"; 4 | 5 | export default () => { 6 | const dispatch = useDispatch(); 7 | const highlightSyntax = useHighlightSyntax(); 8 | 9 | const onChange = () => { 10 | if (highlightSyntax) { 11 | dispatch(setHighlightSyntax(false)); 12 | } else { 13 | dispatch(setHighlightSyntax(true)); 14 | } 15 | }; 16 | return ( 17 | <> 18 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { useTheme } from "../hooks"; 3 | import ChainLoader from "./ChainLoader"; 4 | 5 | const Wrapper = styled.div``; 6 | 7 | const Content = styled.div` 8 | display: flex; 9 | justify-content: center; 10 | `; 11 | 12 | export default ({ children }) => { 13 | const theme = useTheme(); 14 | return ( 15 | 16 | 17 | {/* */} 18 | {children} 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import ThemeSelector from "./ThemeSelector"; 3 | import HighlightSyntaxSelector from "./HighlightSyntaxSelector"; 4 | import AutoExpandSelector from "./AutoExpandSelector"; 5 | 6 | const Wrapper = styled.div` 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | position: relative; 11 | `; 12 | 13 | const Right = styled.div` 14 | display: flex; 15 | grid-gap: 5px; 16 | position: absolute; 17 | right: 5px; 18 | `; 19 | 20 | const Options = styled.div` 21 | display: flex; 22 | grid-gap: 10px; 23 | flex-direction: row; 24 | `; 25 | 26 | export default () => { 27 | return ( 28 | 29 |
30 | {/* {navItems} */} 31 | 32 | {" "} 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/ThemeSelector.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { setTheme } from "../store/options.js"; 3 | import { useTheme } from "../hooks"; 4 | 5 | export default () => { 6 | const dispatch = useDispatch(); 7 | const theme = useTheme(); 8 | 9 | const onChange = () => { 10 | if (theme === "dark") { 11 | dispatch(setTheme("light")); 12 | } else { 13 | dispatch(setTheme("dark")); 14 | } 15 | }; 16 | return ( 17 | <> 18 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { 3 | selectTheme, 4 | selectHideFiles, 5 | selectSplitView, 6 | selectNetwork1, 7 | selectNetwork2, 8 | selectExplorer1, 9 | selectExplorer2, 10 | selectChain1, 11 | selectChain2, 12 | selectSelectedFile, 13 | } from "./store/options"; 14 | 15 | import { selectChains } from "./store/chains"; 16 | 17 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 18 | export const useAppDispatch = () => useDispatch(); 19 | export const useAppSelector = useSelector; 20 | export const useTheme = () => useSelector(selectTheme); 21 | export const useSplitView = () => useSelector(selectSplitView); 22 | export const useHideFiles = () => useSelector(selectHideFiles); 23 | export const useSelectChains = () => useSelector(selectChains); 24 | export const useSelectNetwork1 = () => useSelector(selectNetwork1); 25 | export const useSelectNetwork2 = () => useSelector(selectNetwork2); 26 | export const useSelectExplorer1 = () => useSelector(selectExplorer1); 27 | export const useSelectExplorer2 = () => useSelector(selectExplorer2); 28 | export const useSelectChain1 = () => useSelector(selectChain1); 29 | export const useSelectChain2 = () => useSelector(selectChain2); 30 | export const useSelectSelectedFile = () => useSelector(selectSelectedFile); 31 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import "../vendor/prism.css"; 3 | import "../vendor/prism.js"; 4 | import { ThemeProvider, createTheme } from "@mui/material/styles"; 5 | const darkTheme = createTheme({ 6 | palette: { 7 | mode: "dark", 8 | }, 9 | }); 10 | 11 | import Layout from "../components/Layout"; 12 | import { Provider } from "react-redux"; 13 | import { PersistGate } from "redux-persist/integration/react"; 14 | import Head from "next/head"; 15 | 16 | import { store, persistor } from "../store"; 17 | 18 | export default function MyApp({ Component, pageProps }) { 19 | return ( 20 | 21 | 22 | 23 | 24 | Contract Diffs 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/diff.js: -------------------------------------------------------------------------------- 1 | import stringSimilarity from "string-similarity"; 2 | import { uuid as uuidV4 } from "uuidv4"; 3 | import { useState, useEffect } from "react"; 4 | import prettier from "prettier"; 5 | import prettierPluginSolidity from "prettier-plugin-solidity"; 6 | import styled from "styled-components"; 7 | import { useDispatch } from "react-redux"; 8 | import { 9 | useSplitView, 10 | useHideFiles, 11 | useSelectNetwork1, 12 | useSelectNetwork2, 13 | useSelectChains, 14 | useSelectChain1, 15 | useSelectChain2, 16 | } from "../hooks"; 17 | 18 | import { 19 | Box, 20 | ToggleButtonGroup, 21 | SvgIcon, 22 | ToggleButton, 23 | Tooltip, 24 | IconButton, 25 | Switch, 26 | FormGroup, 27 | FormControlLabel, 28 | } from "@mui/material"; 29 | 30 | import { GitHub, Twitter, HelpOutlined } from "@mui/icons-material"; 31 | 32 | import { setSplitView, setHideFiles } from "../store/options"; 33 | import ChainSelector from "../components/ChainSelector"; 34 | import AddressInput from "../components/AddressInput"; 35 | import FileList from "../components/FileList"; 36 | import FileDiff from "../components/FileDiff"; 37 | 38 | const prettierPlugins = [prettierPluginSolidity]; 39 | 40 | const Flag = styled.img` 41 | height: 24px; 42 | `; 43 | 44 | const HeaderLeft = styled.div` 45 | display: flex; 46 | grid-gap: 5px; 47 | align-items: center; 48 | cursor: pointer; 49 | `; 50 | 51 | const Header = styled.div` 52 | display: flex; 53 | justify-content: space-between; 54 | padding: 10px 30px; 55 | margin-bottom: 20px; 56 | @media (max-width: 990px) { 57 | padding: 10px 10px; 58 | } 59 | `; 60 | 61 | const HeaderRight = styled.div` 62 | display: flex; 63 | grid-gap: 10px; 64 | cursor: pointer; 65 | `; 66 | 67 | const CollapseAndText = styled.div` 68 | display: flex; 69 | align-items: center; 70 | flex-direction: row; 71 | `; 72 | const CollapseWrap = styled.div` 73 | cursor: pointer; 74 | display: inline-flex; 75 | position: relative; 76 | top: -2px; 77 | margin-right: 15px; 78 | opacity: 0.5; 79 | &:hover { 80 | opacity: 1; 81 | } 82 | transform: ${(props) => (props.hidefiles === "true" ? "rotate(180deg)" : "")}; 83 | `; 84 | 85 | const Summary = styled.div` 86 | height: 65px; 87 | padding-top: 15px; 88 | padding-bottom: 10px; 89 | z-index: 1; 90 | width: 100%; 91 | padding-left: 30px; 92 | padding-right: 30px; 93 | background-color: rgb(13, 17, 23); 94 | display: flex; 95 | justify-content: space-between; 96 | position: sticky; 97 | top: 0px; 98 | @media (max-width: 990px) { 99 | display: none; 100 | } 101 | `; 102 | 103 | const SearchField = styled.div` 104 | padding: 0px 30px; 105 | display: grid; 106 | grid-gap: 20px; 107 | grid-template-columns: 1fr 1fr; 108 | @media (max-width: 990px) { 109 | grid-template-rows: 1fr 1fr; 110 | grid-template-columns: unset; 111 | padding: 0px 10px; 112 | } 113 | `; 114 | 115 | const LineChanges = styled.div` 116 | display: inline-flex; 117 | align-items: center; 118 | `; 119 | 120 | const Contract = styled.div` 121 | display: grid; 122 | grid-gap: 5px; 123 | grid-template-columns: auto 150px; 124 | width: 100%; 125 | @media (max-width: 990px) { 126 | margin-bottom: 20px; 127 | } 128 | `; 129 | 130 | const Wrapper = styled.div` 131 | margin: 0px 0px; 132 | padding-botom: 20px; 133 | position: absolute; 134 | left: 0px; 135 | right: 0px; 136 | `; 137 | 138 | const Layout = styled.div` 139 | display: grid; 140 | grid-template-columns: ${(props) => 141 | props.hidefiles === "true" ? "auto" : "300px auto"}; 142 | margin: 0px 30px; 143 | @media (max-width: 990px) { 144 | grid-template-columns: auto; 145 | margin: 0px 10px; 146 | } 147 | grid-gap: 20px; 148 | `; 149 | 150 | const Results = styled.div` 151 | display: ${(props) => (props.hide === "true" ? "none" : "")}; 152 | @media (max-width: 990px) { 153 | margin-top: 20px; 154 | overflow: auto; 155 | margin-right: 10px; 156 | } 157 | `; 158 | 159 | const HaventStarted = styled.div` 160 | width: 100%; 161 | display: flex; 162 | justify-content: center; 163 | top: 200px; 164 | margin-top: 100px; 165 | margin-bottom: 100px; 166 | @media (max-width: 990px) { 167 | top: 100px; 168 | } 169 | display: ${(props) => (props.hide === "true" ? "none" : "")}; 170 | `; 171 | 172 | const HaventStartedText = styled.div` 173 | font-size: 40px; 174 | @media (max-width: 990px) { 175 | font-size: 20px; 176 | } 177 | `; 178 | 179 | const Footer = styled.div` 180 | display: flex; 181 | justify-content: center; 182 | padding: 10px 30px; 183 | margin-bottom: 20px; 184 | @media (max-width: 990px) { 185 | padding: 10px 10px; 186 | } 187 | `; 188 | 189 | const openAddress = (url) => { 190 | window.open(url, "_blank").focus(); 191 | }; 192 | 193 | function App() { 194 | const hidefiles = useHideFiles(); 195 | const splitView = useSplitView(); 196 | const dispatch = useDispatch(); 197 | 198 | const [addedText, setAddedText] = useState(""); 199 | const [removedText, setRemovedText] = useState(""); 200 | const [changedText, setChangedText] = useState("2 changed files"); 201 | 202 | const [fileDiffCounts, setFileDiffCounts] = useState({}); 203 | const [perfectMatch, setPerfectMatch] = useState(false); 204 | 205 | const [helperTextOverride1, setHelperTextOverride1] = useState(null); 206 | const [helperTextOverride2, setHelperTextOverride2] = useState(null); 207 | const [errorOverride1, setErrorOverride1] = useState(null); 208 | const [errorOverride2, setErrorOverride2] = useState(null); 209 | 210 | const [contracts, setContracts] = useState([]); 211 | const [initialLoad, setInitialLoad] = useState(true); 212 | const [filteredContracts, setFilteredContracts] = useState(contracts); 213 | const [code1, setCode1] = useState([]); 214 | const [code2, setCode2] = useState([]); 215 | const width = 216 | window.innerWidth || 217 | document.documentElement.clientWidth || 218 | document.body.clientWidth; 219 | const [mobileMode, setMobileMode] = useState(width <= 990); 220 | 221 | const [timeoutLeft, setTimeoutLeft] = useState(); 222 | const [timeoutRight, setTimeoutRight] = useState(); 223 | 224 | const [address1State, setAddress1State] = useState({ 225 | valid: false, 226 | value: "", 227 | address: "", 228 | }); 229 | const [address2State, setAddress2State] = useState({ 230 | valid: false, 231 | value: "", 232 | address: "", 233 | }); 234 | 235 | const network1 = useSelectNetwork1(); 236 | const network2 = useSelectNetwork2(); 237 | 238 | const [hasResults, setHasResults] = useState(false); 239 | const [previousAddress1, setPreviousAddress1] = useState(""); 240 | const [previousAddress2, setPreviousAddress2] = useState(""); 241 | const [previousNetwork1, setPreviousNetwork1] = useState(network1); 242 | const [previousNetwork2, setPreviousNetwork2] = useState(network2); 243 | 244 | // Dense mode removes all comments and reduce 2+ new lines to 1 new line 245 | const [isDenseMode, setIsDenseMode] = useState(false); 246 | const denseMode = (str) => 247 | str 248 | .replace(/^\s*\/\/.*$/gm, "") 249 | .replace(/\/\*[\s\S]*?\*\//gm, "") 250 | .replace(/^\s*[\r\n]/gm, "") 251 | .trim(); 252 | 253 | const chains = useSelectChains(); 254 | const chain1 = useSelectChain1(); 255 | const chain2 = useSelectChain2(); 256 | 257 | const hasChains = Object.keys(chains).length; 258 | const addressesValid = address1State.valid && address2State.valid; 259 | 260 | const handleScroll = () => { 261 | if (filteredContracts && !filteredContracts.length) { 262 | return; 263 | } 264 | const summaryBar = document.getElementById("summary-bar"); 265 | const filelist = document.getElementById("filelist"); 266 | const summaryBarRect = summaryBar.getBoundingClientRect(); 267 | filelist.setAttribute( 268 | "style", 269 | `height: calc(100vh - 79px - 61px - ${summaryBarRect.top}px` 270 | ); 271 | }; 272 | 273 | const handleResize = () => { 274 | const width = 275 | window.innerWidth || 276 | document.documentElement.clientWidth || 277 | document.body.clientWidth; 278 | if (width <= 990) { 279 | setMobileMode(true); 280 | } else { 281 | setMobileMode(false); 282 | } 283 | }; 284 | 285 | useEffect(() => { 286 | setInitialLoad(true); 287 | setTimeout(() => { 288 | setInitialLoad(false); 289 | }, 300); 290 | }, []); 291 | 292 | useEffect(() => { 293 | window.addEventListener("scroll", handleScroll); 294 | window.addEventListener("resize", handleResize); 295 | return () => { 296 | window.removeEventListener("scroll", handleScroll); 297 | window.removeEventListener("resize", handleResize); 298 | }; 299 | }, []); 300 | 301 | useEffect(() => { 302 | handleScroll(); 303 | }); 304 | 305 | // Initial network state 306 | useEffect(() => { 307 | if (!previousNetwork1 && network1) { 308 | setPreviousNetwork1(network1); 309 | } 310 | if (!previousNetwork2 && network2) { 311 | setPreviousNetwork2(network2); 312 | } 313 | }, [network1, network2]); 314 | 315 | // Merge sources 316 | useEffect(() => { 317 | if (!(code1 && code1.length && code2 && code2.length)) { 318 | return; 319 | } 320 | 321 | const diffTree = {}; 322 | 323 | for (const _code1 of code1) { 324 | let highestSimilarity = 0; 325 | let matchingFile; 326 | for (const _code2 of code2) { 327 | const similarity = stringSimilarity.compareTwoStrings( 328 | formatCode(_code1.source), 329 | formatCode(_code2.source) 330 | ); 331 | 332 | if (similarity > highestSimilarity) { 333 | highestSimilarity = similarity; 334 | matchingFile = { ..._code2 }; 335 | } 336 | } 337 | const uuid = uuidV4(); 338 | if (highestSimilarity < 0.5) { 339 | matchingFile = null; 340 | highestSimilarity = 0; 341 | } 342 | const obj = { 343 | name: _code1.name, 344 | address1: _code1.address, 345 | address2: matchingFile && matchingFile.address, 346 | source1: _code1.source, 347 | source2: matchingFile && matchingFile.source, 348 | similarity: highestSimilarity, 349 | }; 350 | diffTree[uuid] = obj; 351 | } 352 | 353 | for (const _code2 of code2) { 354 | let highestSimilarity = 0; 355 | for (const _code1 of code1) { 356 | const similarity = stringSimilarity.compareTwoStrings( 357 | formatCode(_code1.source), 358 | formatCode(_code2.source) 359 | ); 360 | 361 | if (similarity > highestSimilarity) { 362 | highestSimilarity = similarity; 363 | } 364 | } 365 | const uuid = uuidV4(); 366 | if (highestSimilarity < 0.5) { 367 | const obj = { 368 | name: _code2.name, 369 | address2: _code2.address, 370 | source2: _code2.source, 371 | similarity: 0, 372 | }; 373 | diffTree[uuid] = obj; 374 | } 375 | } 376 | 377 | const merged = Object.values(diffTree); 378 | 379 | let mergedAndUnique = merged.filter((contracts) => 380 | contracts.source1 && contracts.source2 381 | ? formatCode(contracts.source1) !== formatCode(contracts.source2) 382 | : contracts 383 | ); 384 | 385 | setContracts(mergedAndUnique); 386 | if (Object.keys(mergedAndUnique).length === 0) { 387 | setPerfectMatch(true); 388 | } else { 389 | setPerfectMatch(false); 390 | } 391 | }, [code1, code2, isDenseMode]); 392 | 393 | const delay = (time) => { 394 | return new Promise((resolve) => setTimeout(resolve, time)); 395 | }; 396 | 397 | const clearAddressHelper1 = () => { 398 | setHelperTextOverride1(""); 399 | setErrorOverride1(false); 400 | }; 401 | 402 | const clearAddressHelper2 = () => { 403 | setHelperTextOverride2(""); 404 | setErrorOverride2(false); 405 | }; 406 | 407 | const setHelperTextOverride1Fn = (msg) => { 408 | setHelperTextOverride1(msg); 409 | clearTimeout(timeoutLeft); 410 | }; 411 | 412 | const setHelperTextOverride2Fn = (msg) => { 413 | setHelperTextOverride2(msg); 414 | clearTimeout(timeoutRight); 415 | }; 416 | 417 | const getSourceCode = async (field, address) => { 418 | let explorerApi; 419 | 420 | let apiKey; 421 | if (field === 1) { 422 | apiKey = chain1.apiKey; 423 | setErrorOverride1(false); 424 | setHelperTextOverride1Fn("Loading..."); 425 | explorerApi = chain1.explorerApiUrl; 426 | } else { 427 | apiKey = chain2.apiKey; 428 | setErrorOverride2(false); 429 | setHelperTextOverride2Fn("Loading..."); 430 | explorerApi = chain2.explorerApiUrl; 431 | } 432 | const api = apiKey ? `&apiKey=${apiKey}` : ""; 433 | 434 | const url = `${explorerApi}/api?module=contract&action=getsourcecode&address=${address}${api}`; 435 | 436 | let data = await fetch(url).then((res) => res.json()); 437 | // console.log("raw resp", data); 438 | const notOk = data.status === "0"; 439 | if (notOk) { 440 | await delay(1000); 441 | console.log("retry"); 442 | getSourceCode(field, address); 443 | return; 444 | } 445 | const notVerified = "Source not verified"; 446 | if (data.result[0].SourceCode === "") { 447 | if (field === 1) { 448 | setErrorOverride1(true); 449 | setHelperTextOverride1Fn(notVerified); 450 | } else { 451 | setErrorOverride2(true); 452 | setHelperTextOverride2Fn(notVerified); 453 | } 454 | return; 455 | } 456 | if (!(data.result && data.result[0] && data.result[0].SourceCode)) { 457 | if (field === 1) { 458 | setErrorOverride1(true); 459 | setHelperTextOverride1Fn(notVerified); 460 | } else { 461 | setErrorOverride2(true); 462 | setHelperTextOverride2Fn(notVerified); 463 | } 464 | return; 465 | } 466 | 467 | if (field === 1) { 468 | setErrorOverride1(false); 469 | setHelperTextOverride1("Successfully loaded contract"); 470 | setTimeoutLeft( 471 | setTimeout(() => { 472 | setHelperTextOverride1(null); 473 | }, 3000) 474 | ); 475 | } else { 476 | setErrorOverride2(false); 477 | setHelperTextOverride2("Successfully loaded contract"); 478 | setTimeoutRight( 479 | setTimeout(() => { 480 | setHelperTextOverride2(null); 481 | }, 3000) 482 | ); 483 | } 484 | 485 | let contractData = {}; 486 | try { 487 | contractData = JSON.parse(data.result[0].SourceCode.slice(1, -1)).sources; 488 | } catch (e) { 489 | const firstResult = data.result[0]; 490 | if (typeof firstResult.SourceCode === "string") { 491 | contractData[firstResult.ContractName] = { 492 | content: firstResult.SourceCode, 493 | }; 494 | } else { 495 | contractData = JSON.parse(data.result[0].SourceCode); 496 | } 497 | } 498 | 499 | const sources = []; 500 | for (const [name, sourceObj] of Object.entries(contractData)) { 501 | const source = sourceObj.content; 502 | sources.push({ name, source, address }); 503 | } 504 | if (field === 1) { 505 | setCode1(sources); 506 | } else { 507 | setCode2(sources); 508 | } 509 | }; 510 | 511 | useEffect(() => { 512 | const address1Changed = address1State.value !== previousAddress1; 513 | const address2Changed = address2State.value !== previousAddress2; 514 | const network1Changed = network1 !== previousNetwork1; 515 | const network2Changed = network2 !== previousNetwork2; 516 | 517 | if (address1Changed && address1State.valid && hasChains) { 518 | getSourceCode(1, address1State.value); 519 | setPreviousAddress1(address1State.value); 520 | } else if (network1Changed) { 521 | setPreviousNetwork1(network1); 522 | if (address1State.valid) { 523 | getSourceCode(1, address1State.value); 524 | } 525 | } 526 | if (address2Changed && address2State.valid && hasChains) { 527 | getSourceCode(2, address2State.value); 528 | setPreviousAddress2(address2State.value); 529 | } else if (network2Changed) { 530 | setPreviousNetwork2(network2); 531 | if (address2State.valid) { 532 | getSourceCode(2, address2State.value); 533 | } 534 | } 535 | 536 | if (address1State.value && address2State.value) 537 | window.history.replaceState( 538 | {}, 539 | "", 540 | `/diff?address1=${address1State.value}&chain1=${network1}&address2=${address2State.value}&chain2=${network2}` 541 | ); 542 | const hasAddresses = 543 | address1State.value !== "" && address2State.value !== ""; 544 | if (hasAddresses && hasChains) { 545 | const address1Changed = address1State.value !== previousAddress1; 546 | const address2Changed = address2State.value !== previousAddress2; 547 | const addressesChanged = address1Changed || address2Changed; 548 | 549 | if (addressesValid && addressesChanged) { 550 | setHasResults(true); 551 | } 552 | } else { 553 | if (hasAddresses) { 554 | setHasResults(false); 555 | } 556 | } 557 | }, [address1State.value, address2State.value, network1, network2, hasChains]); 558 | 559 | useEffect(() => { 560 | const added = document.querySelectorAll( 561 | "[class*='gutter'][class*='diff-added']" 562 | ).length; 563 | const removed = document.querySelectorAll( 564 | "[class*='gutter'][class*='diff-removed']" 565 | ).length; 566 | 567 | let addedRemoved = {}; 568 | filteredContracts.forEach(({ name, source1, source2 }) => { 569 | const removedForFile = document 570 | .getElementById(name) 571 | .querySelectorAll("[class*='gutter'][class*='diff-removed']").length; 572 | const addedForFile = document 573 | .getElementById(name) 574 | .querySelectorAll("[class*='gutter'][class*='diff-added']").length; 575 | let modificationType; 576 | if (source1 && !source2) { 577 | modificationType = "removed"; 578 | } else if (!source1 && source2) { 579 | modificationType = "added"; 580 | } else if (source1 !== source2) { 581 | modificationType = "modified"; 582 | } 583 | addedRemoved[name] = { 584 | added: addedForFile, 585 | removed: removedForFile, 586 | modificationType, 587 | }; 588 | }); 589 | setFileDiffCounts(addedRemoved); 590 | 591 | const changed = filteredContracts.length; 592 | const addedSuffix = added === 0 || added > 1 ? "s" : ""; 593 | const removedSuffix = removed === 0 || removed > 1 ? "s" : ""; 594 | const changedSuffix = changed === 0 || changed > 1 ? "s" : ""; 595 | setChangedText( 596 | 597 | {changed} changed file{changedSuffix} 598 | 599 | ); 600 | setAddedText( 601 | 602 | {added} addition{addedSuffix} 603 | 604 | ); 605 | setRemovedText( 606 | 607 | {removed} deletion{removedSuffix} 608 | 609 | ); 610 | }, [filteredContracts, isDenseMode]); 611 | 612 | const toggleHideFiles = () => { 613 | dispatch(setHideFiles(hidefiles === "true" ? "fasle" : "true")); 614 | }; 615 | 616 | const Collapse = ( 617 | 621 | 622 | 626 | 627 | 628 | 629 | 630 | 631 | ); 632 | 633 | const onViewChange = (evt) => { 634 | dispatch(setSplitView(evt.target.value === "split" ? true : false)); 635 | }; 636 | 637 | const formatCode = (code) => { 638 | // Format the code using Prettier. 639 | const formattedCode = prettier.format(code, { 640 | parser: "solidity-parse", 641 | plugins: prettierPlugins, 642 | printWidth: 100, 643 | }); 644 | 645 | // Apply dense mode filter if required. 646 | return isDenseMode ? denseMode(formattedCode) : formattedCode; 647 | }; 648 | 649 | const diffs = 650 | filteredContracts && 651 | filteredContracts.map((item) => ( 652 | 662 | )); 663 | 664 | return ( 665 | 666 |
667 | openAddress("/")}> 668 | 669 |
Contract Diffs
670 |
671 | 672 | 675 | openAddress("https://github.com/mds1/contract-diff-tool") 676 | } 677 | /> 678 | openAddress("https://x.com/msolomon44")} 681 | /> 682 | 683 |
684 | 685 | 686 | 695 | 696 | 697 | 698 | 707 | 708 | 709 | 710 | 720 | 721 | {(errorOverride1 && helperTextOverride1) || 722 | (errorOverride2 && helperTextOverride2) || 723 | "Enter contract addresses above"} 724 | 725 | 726 | 738 | Contracts are identical 739 | 740 | 750 | 751 | 752 | 753 | 754 | 767 | 768 | 769 | {Collapse} 770 | 771 |
772 | Showing {changedText} with {addedText} and {removedText}. 773 |
774 |
775 |
776 | 777 | setIsDenseMode(event.target.checked)} 782 | /> 783 | } 784 | label={ 785 | 786 | Dense Mode 787 | 788 | 789 | 790 | 791 | 792 | 793 | } 794 | /> 795 | 796 | 803 | 807 | Split 808 | 809 | 813 | Unified 814 | 815 | 816 | 817 |
818 | 819 | 820 | 828 |
{diffs}
829 |
830 |
831 | 844 |
845 | ); 846 | } 847 | 848 | export default App; 849 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | function App() { 4 | const router = useRouter(); 5 | router.replace("/diff"); // Redirect to /diff on load 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /src/store/chains.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | list: [], 5 | }; 6 | 7 | const slice = createSlice({ 8 | name: "chains", 9 | initialState, 10 | reducers: { 11 | setChains(state, action) { 12 | state.list = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setChains } = slice.actions; 18 | export const selectChains = (state) => state.chains.list; 19 | 20 | export default slice.reducer; 21 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import { 3 | persistStore, 4 | persistReducer, 5 | FLUSH, 6 | REHYDRATE, 7 | PAUSE, 8 | PERSIST, 9 | PURGE, 10 | REGISTER, 11 | } from "redux-persist"; 12 | 13 | import storage from "redux-persist/lib/storage"; 14 | import reduxWebsocket from "@giantmachines/redux-websocket"; 15 | 16 | import rootReducer from "./reducers"; 17 | 18 | const persistConfig = { 19 | key: "root", 20 | version: 1, 21 | storage, 22 | }; 23 | 24 | const persistedReducer = persistReducer(persistConfig, rootReducer); 25 | 26 | // Create the middleware instance. 27 | const websocketMiddlewareOptions = { 28 | dateSerializer: (date) => date.getTime(), 29 | prefix: "websocket/REDUX_WEBSOCKET", 30 | serializer: (data) => { 31 | return JSON.stringify(data); 32 | }, 33 | reconnectOnClose: true, 34 | onOpen: (_ws) => {}, 35 | }; 36 | const reduxWebsocketMiddleware = reduxWebsocket(websocketMiddlewareOptions); 37 | 38 | const websocketOnOpenMiddleware = (_store) => (next) => (action) => { 39 | if (action.type === "REDUX_WEBSOCKET::OPEN") { 40 | // store.dispatch({ type: "REDUX_WEBSOCKET/OPEN" }); 41 | } 42 | let result = next(action); 43 | return result; 44 | }; 45 | 46 | export const store = configureStore({ 47 | reducer: persistedReducer, 48 | middleware: (getDefaultMiddleware) => 49 | getDefaultMiddleware({ 50 | serializableCheck: { 51 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 52 | ignoredActionPaths: ["payload", "payload.event"], 53 | }, 54 | }).concat(reduxWebsocketMiddleware, websocketOnOpenMiddleware), 55 | }); 56 | 57 | export const persistor = persistStore(store); 58 | -------------------------------------------------------------------------------- /src/store/options.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | mode: "dark", 5 | hideFiles: "false", 6 | splitView: true, 7 | network1: 1, 8 | network2: 1, 9 | selectedFile: "", 10 | }; 11 | 12 | const slice = createSlice({ 13 | name: "options", 14 | initialState, 15 | reducers: { 16 | setTheme(state, action) { 17 | state.mode = action.payload; 18 | }, 19 | setHideFiles(state, action) { 20 | state.hideFiles = action.payload; 21 | }, 22 | setSplitView(state, action) { 23 | state.splitView = action.payload; 24 | }, 25 | setNetwork1(state, action) { 26 | state.network1 = action.payload; 27 | }, 28 | setNetwork2(state, action) { 29 | state.network2 = action.payload; 30 | }, 31 | setSelectedFile(state, action) { 32 | state.selectedFile = action.payload; 33 | }, 34 | }, 35 | }); 36 | 37 | export const { 38 | setTheme, 39 | setSplitView, 40 | setHideFiles, 41 | setNetwork1, 42 | setNetwork2, 43 | setSelectedFile, 44 | } = slice.actions; 45 | export const selectTheme = (state) => state.options.mode; 46 | export const selectNetwork1 = (state) => state.options.network1; 47 | export const selectNetwork2 = (state) => state.options.network2; 48 | export const selectHideFiles = (state) => state.options.hideFiles; 49 | export const selectSplitView = (state) => state.options.splitView; 50 | export const selectSelectedFile = (state) => state.options.selectedFile; 51 | 52 | // Explorers 53 | export const selectExplorer1 = (state) => 54 | state.chains.list.length && 55 | state.options.network1 && 56 | state.chains.list.find( 57 | (chain) => chain.chainId === parseInt(state.options.network1) 58 | ).explorers[0]?.url; 59 | export const selectExplorer2 = (state) => 60 | state.chains.list.length && 61 | state.options.network2 && 62 | state.chains.list.find( 63 | (chain) => chain.chainId === parseInt(state.options.network2) 64 | ).explorers[0]?.url; 65 | 66 | // Chains 67 | export const selectChain1 = (state) => 68 | state.chains.list.length && 69 | state.options.network1 && 70 | state.chains.list.find( 71 | (chain) => chain.chainId === parseInt(state.options.network1) 72 | ); 73 | 74 | export const selectChain2 = (state) => 75 | state.chains.list.length && 76 | state.options.network2 && 77 | state.chains.list.find( 78 | (chain) => chain.chainId === parseInt(state.options.network2) 79 | ); 80 | 81 | export default slice.reducer; 82 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import options from "./options"; 3 | import swap from "./swap"; 4 | import chains from "./chains"; 5 | 6 | const rootReducer = combineReducers({ 7 | options, 8 | swap, 9 | chains, 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /src/store/swap.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | network: 1, 5 | }; 6 | 7 | const slice = createSlice({ 8 | name: "swap", 9 | initialState, 10 | reducers: { 11 | setNetwork(state, action) { 12 | state.network = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setNetwork } = slice.actions; 18 | export const selectNetwork = (state) => state.swap.network; 19 | 20 | export default slice.reducer; 21 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | top: 0px; 6 | bottom: 0px; 7 | left: 0px; 8 | right: 0px; 9 | position: absolute; 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", 11 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 12 | background-color: rgb(13, 17, 23); 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | .main-wrapper { 20 | top: 0px; 21 | bottom: 0px; 22 | left: 0px; 23 | right: 0px; 24 | position: absolute; 25 | } 26 | 27 | .dark.main-wrapper { 28 | color: white; 29 | } 30 | 31 | [class*="code-fold"] > td > a { 32 | font-size: 20px; 33 | text-decoration-line: none !important; 34 | position: relative; 35 | top: 0px; 36 | width: 50px; 37 | height: 40px; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | [class*="code-fold"] > td > a > div { 44 | position: relative; 45 | top: -2px; 46 | left: -2px; 47 | } 48 | 49 | [class*="code-fold"] > td:nth-of-type(4) { 50 | position: relative; 51 | right: 0px; 52 | height: 40px; 53 | left: -106px; 54 | display: flex; 55 | align-items: center; 56 | } 57 | 58 | [class*="split-view"] [class*="code-fold"] > td:nth-of-type(3) { 59 | position: relative; 60 | right: 0px; 61 | height: 40px; 62 | left: -81px; 63 | display: flex; 64 | align-items: center; 65 | } 66 | 67 | [class*="split-view"] [class*="code-fold"] > td:nth-of-type(4) { 68 | display: table-cell; 69 | left: 0px; 70 | position: auto; 71 | height: auto; 72 | } 73 | 74 | .loader { 75 | width: 48px; 76 | height: 48px; 77 | border: 5px solid #fff; 78 | border-bottom-color: transparent; 79 | border-radius: 50%; 80 | display: inline-block; 81 | box-sizing: border-box; 82 | animation: rotation 0.5s linear infinite; 83 | } 84 | 85 | @keyframes rotation { 86 | 0% { 87 | transform: rotate(0deg); 88 | } 89 | 100% { 90 | transform: rotate(360deg); 91 | } 92 | } 93 | 94 | a { 95 | color: white; 96 | } 97 | 98 | .MuiFormHelperText-root { 99 | position: absolute; 100 | top: 40px; 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | const baseUrl = "http://localhost:80"; 2 | 3 | export default { 4 | get: async (route) => { 5 | const url = `${baseUrl}${route}`; 6 | console.log("get", url); 7 | const response = await fetch(url, { 8 | method: "GET", 9 | headers: { 10 | "Content-Type": "application/json", 11 | }, 12 | }); 13 | const result = await response.json(); 14 | 15 | return result; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | const units = { 2 | year: 24 * 60 * 60 * 1000 * 365, 3 | month: (24 * 60 * 60 * 1000 * 365) / 12, 4 | day: 24 * 60 * 60 * 1000, 5 | hour: 60 * 60 * 1000, 6 | minute: 60 * 1000, 7 | second: 1000, 8 | }; 9 | 10 | const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); 11 | 12 | export const getRelativeTime = (d1, d2 = new Date()) => { 13 | const elapsed = d1 - d2; 14 | 15 | // "Math.abs" accounts for both "past" & "future" scenarios 16 | for (const u in units) 17 | if (Math.abs(elapsed) > units[u] || u == "second") 18 | return rtf.format(Math.round(elapsed / units[u]), u); 19 | }; 20 | 21 | export function shortenAddress(address, chars = 4) { 22 | if (address === "") { 23 | return ""; 24 | } 25 | if (address.endsWith(".eth")) { 26 | return address; 27 | } 28 | return `${address.substring(0, chars + 2)}...${address.substring( 29 | 42 - chars 30 | )}`; 31 | } 32 | 33 | export const getEndOfPath = (path) => { 34 | const parts = path.split("/"); 35 | const name = parts[parts.length - 1]; 36 | return name; 37 | }; 38 | 39 | export const highlight = (needle, haystack) => { 40 | const reg = new RegExp(needle, "gi"); 41 | return ( 42 |
"" + str + ""), 45 | }} 46 | /> 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/vendor/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+solidity */ 3 | code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} 4 | -------------------------------------------------------------------------------- /src/vendor/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+solidity */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 5 | Prism.languages.solidity=Prism.languages.extend("clike",{"class-name":{pattern:/(\b(?:contract|enum|interface|library|new|struct|using)\s+)(?!\d)[\w$]+/,lookbehind:!0},keyword:/\b(?:_|anonymous|as|assembly|assert|break|calldata|case|constant|constructor|continue|contract|default|delete|do|else|emit|enum|event|external|for|from|function|if|import|indexed|inherited|interface|internal|is|let|library|mapping|memory|modifier|new|payable|pragma|private|public|pure|require|returns?|revert|selfdestruct|solidity|storage|struct|suicide|switch|this|throw|using|var|view|while)\b/,operator:/=>|->|:=|=:|\*\*|\+\+|--|\|\||&&|<<=?|>>=?|[-+*/%^&|<>!=]=?|[~?]/}),Prism.languages.insertBefore("solidity","keyword",{builtin:/\b(?:address|bool|byte|u?int(?:8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)?|string|bytes(?:[1-9]|[12]\d|3[0-2])?)\b/}),Prism.languages.insertBefore("solidity","number",{version:{pattern:/([<>]=?|\^)\d+\.\d+\.\d+\b/,lookbehind:!0,alias:"number"}}),Prism.languages.sol=Prism.languages.solidity; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "noEmit": true, 8 | "incremental": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | } 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/pages/_document.js"], 20 | "exclude": ["node_modules"] 21 | } 22 | --------------------------------------------------------------------------------