Skip to content

Commit 23876b7

Browse files
authored
Merge pull request #331 from AugustoL/feat/vyper-support
feat(address): add Vyper smart contract support and syntax highlighting
2 parents a2f4b87 + c42acb3 commit 23876b7

8 files changed

Lines changed: 133 additions & 9 deletions

File tree

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"i18next": "^25.8.0",
2222
"i18next-browser-languagedetector": "^8.2.0",
2323
"jszip": "^3.10.1",
24+
"prismjs": "^1.30.0",
2425
"react": "^19.1.0",
2526
"react-dom": "^19.1.0",
2627
"react-i18next": "^16.5.4",
@@ -68,16 +69,17 @@
6869
"devDependencies": {
6970
"@biomejs/biome": "^2.3.11",
7071
"@playwright/test": "^1.57.0",
72+
"@testing-library/dom": "^10.4.0",
73+
"@testing-library/jest-dom": "^6.6.3",
74+
"@testing-library/react": "^16.2.0",
75+
"@testing-library/user-event": "^13.5.0",
76+
"@types/prismjs": "^1.26.6",
7177
"@types/react": "^19.1.8",
7278
"@types/react-dom": "^19.1.6",
7379
"@vitejs/plugin-react": "^5.1.2",
7480
"dotenv": "^17.2.3",
7581
"happy-dom": "^20.1.0",
7682
"vite": "^7.3.1",
77-
"@testing-library/dom": "^10.4.0",
78-
"@testing-library/jest-dom": "^6.6.3",
79-
"@testing-library/react": "^16.2.0",
80-
"@testing-library/user-event": "^13.5.0",
8183
"vitest": "^4.0.14"
8284
}
8385
}

src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import "./styles/rainbowkit.css";
2020
import "./styles/responsive.css";
2121
import "./styles/ai-analysis.css";
2222
import "./styles/rpcs.css";
23+
import "./styles/code-highlight.css";
2324
import "./styles/helper-tooltip.css";
2425

2526
import Loading from "./components/common/Loading";
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useMemo } from "react";
2+
import Prism from "prismjs";
3+
import "prismjs/components/prism-python";
4+
import "prismjs/components/prism-solidity";
5+
6+
function detectLanguage(fileName?: string): string | undefined {
7+
if (!fileName) return undefined;
8+
const ext = fileName.split(".").pop()?.toLowerCase();
9+
if (ext === "sol") return "solidity";
10+
if (ext === "vy") return "python";
11+
if (ext === "json") return "json";
12+
return undefined;
13+
}
14+
15+
interface CodeBlockProps {
16+
code: string;
17+
fileName?: string;
18+
language?: string;
19+
}
20+
21+
const CodeBlock: React.FC<CodeBlockProps> = ({ code, fileName, language }) => {
22+
const lang = language ?? detectLanguage(fileName);
23+
const grammar = lang ? Prism.languages[lang] : undefined;
24+
25+
const highlighted = useMemo(() => {
26+
if (!grammar || !lang) return null;
27+
return Prism.highlight(code, grammar, lang);
28+
}, [code, grammar, lang]);
29+
30+
if (highlighted) {
31+
return (
32+
<pre className="source-file-code">
33+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: Prism.highlight output is safe — it only tokenizes source code we control */}
34+
<code className={`language-${lang}`} dangerouslySetInnerHTML={{ __html: highlighted }} />
35+
</pre>
36+
);
37+
}
38+
39+
return (
40+
<pre className="source-file-code">
41+
<code>{code}</code>
42+
</pre>
43+
);
44+
};
45+
46+
export default CodeBlock;

src/components/pages/evm/address/shared/ContractDetails.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type React from "react";
22
import { useState } from "react";
33
import type { ABI } from "../../../../../types";
44
import { useTranslation } from "react-i18next";
5+
import CodeBlock from "../../../../common/CodeBlock";
56
import FieldLabel from "../../../../common/FieldLabel";
67
import ContractInteraction from "./ContractInteraction";
78

@@ -201,7 +202,7 @@ const ContractDetails: React.FC<ContractDetailsProps> = ({
201202
{sourceFiles.map((file) => (
202203
<div key={file.path} className="source-file-container">
203204
<div className="source-file-header">📄 {file.name || file.path}</div>
204-
<pre className="source-file-code">{file.content}</pre>
205+
<CodeBlock code={file.content} fileName={file.name || file.path} />
205206
</div>
206207
))}
207208
</div>

src/components/pages/evm/address/shared/ContractInfoCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useState } from "react";
33
import { Link } from "react-router-dom";
44
import type { Address, ABI } from "../../../../../types";
55
import { useTranslation } from "react-i18next";
6+
import CodeBlock from "../../../../common/CodeBlock";
67
import FieldLabel from "../../../../common/FieldLabel";
78
import ContractInteraction from "./ContractInteraction";
89
import type { VerificationSource } from "../../../../../hooks/useContractVerification";
@@ -357,7 +358,7 @@ const ContractInfoCard: React.FC<ContractInfoCardProps> = ({
357358
{sourceFiles.map((file) => (
358359
<div key={file.path} className="source-file-container">
359360
<div className="source-file-header">📄 {file.name || file.path}</div>
360-
<pre className="source-file-code">{file.content}</pre>
361+
<CodeBlock code={file.content} fileName={file.name || file.path} />
361362
</div>
362363
))}
363364
</div>

src/hooks/useEtherscan.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface StandardJsonSources {
3939
function parseSourceFiles(
4040
sourceCode: string,
4141
contractName: string,
42+
compilerVersion?: string,
4243
): { name: string; path: string; content: string }[] {
4344
if (!sourceCode) return [];
4445

@@ -62,8 +63,9 @@ function parseSourceFiles(
6263
}
6364
}
6465

65-
// Plain Solidity source
66-
const fileName = `${contractName || "Contract"}.sol`;
66+
// Detect language from compiler version — Vyper compilers start with "vyper:"
67+
const ext = compilerVersion?.toLowerCase().startsWith("vyper:") ? ".vy" : ".sol";
68+
const fileName = `${contractName || "Contract"}${ext}`;
6769
return [{ name: fileName, path: fileName, content: sourceCode }];
6870
}
6971

@@ -159,14 +161,20 @@ export function useEtherscan(
159161
abi = undefined;
160162
}
161163

162-
const files = parseSourceFiles(result.SourceCode, result.ContractName);
164+
const files = parseSourceFiles(
165+
result.SourceCode,
166+
result.ContractName,
167+
result.CompilerVersion,
168+
);
163169
const evmVersion =
164170
result.EVMVersion && result.EVMVersion !== "Default" ? result.EVMVersion : undefined;
165171

172+
const isVyper = result.CompilerVersion?.toLowerCase().startsWith("vyper:");
166173
const contractDetails: SourcifyContractDetails = {
167174
name: result.ContractName || undefined,
168175
compilerVersion: result.CompilerVersion || undefined,
169176
evmVersion,
177+
language: isVyper ? "Vyper" : "Solidity",
170178
abi,
171179
files,
172180
match: "perfect",

src/styles/code-highlight.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* Prism.js syntax highlighting theme for OpenScan */
2+
.source-file-code code[class*="language-"] {
3+
text-shadow: none;
4+
}
5+
6+
.source-file-code .token.comment,
7+
.source-file-code .token.prolog,
8+
.source-file-code .token.doctype,
9+
.source-file-code .token.cdata {
10+
color: #6a9955;
11+
}
12+
13+
.source-file-code .token.punctuation {
14+
color: #d4d4d4;
15+
}
16+
17+
.source-file-code .token.property,
18+
.source-file-code .token.tag,
19+
.source-file-code .token.boolean,
20+
.source-file-code .token.number,
21+
.source-file-code .token.constant,
22+
.source-file-code .token.symbol {
23+
color: #b5cea8;
24+
}
25+
26+
.source-file-code .token.selector,
27+
.source-file-code .token.attr-name,
28+
.source-file-code .token.string,
29+
.source-file-code .token.char,
30+
.source-file-code .token.builtin {
31+
color: #ce9178;
32+
}
33+
34+
.source-file-code .token.operator,
35+
.source-file-code .token.entity,
36+
.source-file-code .token.url {
37+
color: #d4d4d4;
38+
}
39+
40+
.source-file-code .token.atrule,
41+
.source-file-code .token.attr-value,
42+
.source-file-code .token.keyword {
43+
color: #569cd6;
44+
}
45+
46+
.source-file-code .token.function,
47+
.source-file-code .token.class-name {
48+
color: #dcdcaa;
49+
}
50+
51+
.source-file-code .token.regex,
52+
.source-file-code .token.important,
53+
.source-file-code .token.variable {
54+
color: #d16969;
55+
}
56+
57+
.source-file-code .token.decorator {
58+
color: #dcdcaa;
59+
}

0 commit comments

Comments
 (0)