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+""+i.tag+">"},!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 |
--------------------------------------------------------------------------------