Skip to content

Commit dfd9b3b

Browse files
Merge pull request #5 from sourcifyeth/fix-import-from-etherscan
Use the new Util "Import From Etherscan" in lib-sourcify
2 parents eb75977 + bc23687 commit dfd9b3b

File tree

4 files changed

+886
-297
lines changed

4 files changed

+886
-297
lines changed

app/utils/etherscanApi.ts

Lines changed: 59 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -1,253 +1,83 @@
1-
import type { Language, VerificationMethod } from "../types/verification";
21
import type { VyperVersion } from "../contexts/CompilerVersionsContext";
3-
import type { SolidityCompilerSettings, VyperCompilerSettings } from "./sourcifyApi";
2+
import {
3+
EtherscanUtils,
4+
EtherscanImportError,
5+
type EtherscanResult,
6+
type ProcessedEtherscanResult,
7+
} from "@ethereum-sourcify/lib-sourcify";
48

59
// Function to convert Vyper short version to long version using pre-loaded versions
6-
export const getVyperLongVersionFromList = (shortVersion: string, vyperVersions: VyperVersion[]): string => {
7-
const foundVersion = vyperVersions.find((version) => version.version === shortVersion);
10+
export const getVyperLongVersionFromList = (
11+
shortVersion: string,
12+
vyperVersions: VyperVersion[]
13+
): string => {
14+
const foundVersion = vyperVersions.find(
15+
(version) => version.version === shortVersion
16+
);
817
return foundVersion ? foundVersion.longVersion : shortVersion;
918
};
1019

11-
// Helper function to generate compiler settings from Etherscan result
12-
const generateCompilerSettings = (
13-
language: Language,
14-
etherscanResult: EtherscanResult
15-
): SolidityCompilerSettings | VyperCompilerSettings => {
16-
if (language === "solidity") {
17-
const settings: SolidityCompilerSettings = {
18-
optimizerEnabled: etherscanResult.OptimizationUsed === "1",
19-
optimizerRuns: parseInt(etherscanResult.Runs),
20-
};
21-
22-
// Only include evmVersion if it's not "default"
23-
if (etherscanResult.EVMVersion.toLowerCase() !== "default") {
24-
settings.evmVersion = etherscanResult.EVMVersion;
25-
}
26-
27-
return settings;
28-
} else {
29-
// For Vyper, no optimization settings
30-
const settings: VyperCompilerSettings = {};
31-
32-
// Only include evmVersion if it's not "default"
33-
if (etherscanResult.EVMVersion.toLowerCase() !== "default") {
34-
settings.evmVersion = etherscanResult.EVMVersion;
35-
}
36-
37-
return settings;
38-
}
39-
};
40-
41-
export interface EtherscanResult {
42-
SourceCode: string;
43-
ABI: string;
44-
ContractName: string;
45-
CompilerVersion: string;
46-
OptimizationUsed: string;
47-
Runs: string;
48-
ConstructorArguments: string;
49-
EVMVersion: string;
50-
Library: string;
51-
LicenseType: string;
52-
Proxy: string;
53-
Implementation: string;
54-
SwarmSource: string;
55-
ContractFileName?: string;
56-
}
57-
58-
export interface ProcessedEtherscanResult {
59-
language: Language;
60-
verificationMethod: VerificationMethod;
61-
compilerVersion: string;
62-
contractName: string;
63-
contractPath: string;
64-
files: File[];
65-
compilerSettings?: SolidityCompilerSettings | VyperCompilerSettings;
66-
}
67-
68-
export interface ProcessEtherscanOptions {
69-
vyperVersions?: VyperVersion[];
70-
}
71-
72-
export const isEtherscanJsonInput = (sourceCodeObject: string): boolean => {
73-
return sourceCodeObject.startsWith("{{");
74-
};
75-
76-
export const isEtherscanMultipleFilesObject = (sourceCodeObject: string): boolean => {
77-
try {
78-
return Object.keys(JSON.parse(sourceCodeObject)).length > 0;
79-
} catch (e) {
80-
return false;
81-
}
82-
};
83-
84-
export const parseEtherscanJsonInput = (sourceCodeObject: string) => {
85-
// Etherscan wraps the json object: {{ ... }}
86-
return JSON.parse(sourceCodeObject.slice(1, -1));
87-
};
88-
89-
export const isVyperResult = (etherscanResult: EtherscanResult): boolean => {
90-
return etherscanResult.CompilerVersion.startsWith("vyper");
91-
};
92-
93-
export const getContractPathFromSoliditySources = (contractName: string, sources: any): string | undefined => {
94-
// Look for a file that contains the contract definition
95-
for (const [filePath, source] of Object.entries(sources)) {
96-
const content = typeof source === "string" ? source : (source as any).content;
97-
if (content && typeof content === "string") {
98-
// Look for contract definition in the file
99-
const contractRegex = new RegExp(`contract\\s+${contractName}\\s*[\\s\\S]*?\\{`, "g");
100-
const interfaceRegex = new RegExp(`interface\\s+${contractName}\\s*[\\s\\S]*?\\{`, "g");
101-
const libraryRegex = new RegExp(`library\\s+${contractName}\\s*[\\s\\S]*?\\{`, "g");
102-
103-
if (contractRegex.test(content) || interfaceRegex.test(content) || libraryRegex.test(content)) {
104-
return filePath;
105-
}
106-
}
107-
}
108-
return undefined;
109-
};
110-
11120
export const fetchFromEtherscan = async (
11221
chainId: string,
11322
address: string,
114-
apiKey: string
23+
apiKey: string = ""
11524
): Promise<EtherscanResult> => {
116-
const url = `https://api.etherscan.io/v2/api?chainid=${chainId}&module=contract&action=getsourcecode&address=${address}&apikey=${apiKey}`;
117-
118-
let response: Response;
119-
12025
try {
121-
response = await fetch(url);
26+
return await EtherscanUtils.fetchFromEtherscan(chainId, address, apiKey);
12227
} catch (error) {
123-
throw new Error(`Network error: Failed to connect to Etherscan API`);
124-
}
125-
126-
if (!response.ok) {
127-
throw new Error(`Etherscan API responded with status ${response.status}`);
128-
}
129-
130-
const resultJson = await response.json();
131-
132-
if (resultJson.message === "NOTOK" && resultJson.result.includes("rate limit reached")) {
133-
throw new Error("Etherscan API rate limit reached, please try again later");
134-
}
135-
136-
if (resultJson.message === "NOTOK") {
137-
throw new Error(`Etherscan API error: ${resultJson.result}`);
138-
}
139-
140-
if (resultJson.result[0].SourceCode === "") {
141-
throw new Error("This contract is not verified on Etherscan");
28+
if (error instanceof EtherscanImportError) {
29+
// Convert EtherscanImportError to regular Error for compatibility
30+
switch (error.code) {
31+
case "etherscan_network_error":
32+
throw new Error("Network error: Failed to connect to Etherscan API");
33+
case "etherscan_http_error":
34+
throw new Error(
35+
`Etherscan API responded with status ${(error as any).status}`
36+
);
37+
case "etherscan_rate_limit":
38+
throw new Error(
39+
"Etherscan API rate limit reached, please try again later"
40+
);
41+
case "etherscan_api_error":
42+
throw new Error(
43+
`Etherscan API error: ${(error as any).apiErrorMessage}`
44+
);
45+
case "etherscan_not_verified":
46+
throw new Error("This contract is not verified on Etherscan");
47+
default:
48+
throw new Error(`Etherscan error: ${error.message}`);
49+
}
50+
}
51+
throw error;
14252
}
143-
144-
return resultJson.result[0] as EtherscanResult;
14553
};
14654

14755
export const processEtherscanResult = async (
148-
etherscanResult: EtherscanResult,
149-
options: ProcessEtherscanOptions = {}
56+
etherscanResult: EtherscanResult
15057
): Promise<ProcessedEtherscanResult> => {
151-
const sourceCodeObject = etherscanResult.SourceCode;
152-
const contractName = etherscanResult.ContractName;
153-
154-
// Determine language
155-
const language: Language = isVyperResult(etherscanResult) ? "vyper" : "solidity";
156-
157-
// Process compiler version
158-
let compilerVersion = etherscanResult.CompilerVersion;
159-
160-
if (compilerVersion.startsWith("vyper:")) {
161-
const shortVersion = compilerVersion.slice(6);
162-
// Convert short version to long version for Vyper using pre-loaded versions
163-
if (options.vyperVersions) {
164-
compilerVersion = getVyperLongVersionFromList(shortVersion, options.vyperVersions);
165-
} else {
166-
// Fallback to short version if no versions provided
167-
compilerVersion = shortVersion;
168-
}
169-
} else if (compilerVersion.charAt(0) === "v") {
170-
compilerVersion = compilerVersion.slice(1);
171-
}
172-
173-
let verificationMethod: VerificationMethod;
174-
let files: File[] = [];
175-
let contractPath: string;
176-
let compilerSettings: SolidityCompilerSettings | VyperCompilerSettings | undefined;
177-
178-
// Determine verification method and create files
179-
if (isEtherscanJsonInput(sourceCodeObject)) {
180-
// std-json method - compiler settings are already in the JSON input
181-
verificationMethod = "std-json";
182-
const jsonInput = parseEtherscanJsonInput(sourceCodeObject);
58+
try {
59+
let processedResult;
18360

184-
// Use ContractFileName if available, otherwise search in sources
185-
if (etherscanResult.ContractFileName) {
186-
contractPath = etherscanResult.ContractFileName;
61+
if (EtherscanUtils.isVyperResult(etherscanResult)) {
62+
processedResult = await EtherscanUtils.processVyperResultFromEtherscan(
63+
etherscanResult
64+
);
18765
} else {
188-
const foundPath = getContractPathFromSoliditySources(contractName, jsonInput.sources);
189-
if (!foundPath) {
190-
throw new Error("Could not find contract path in sources");
191-
}
192-
contractPath = foundPath;
66+
processedResult =
67+
EtherscanUtils.processSolidityResultFromEtherscan(etherscanResult);
19368
}
19469

195-
// Create a single JSON file
196-
const jsonContent = JSON.stringify(jsonInput, null, 2);
197-
const jsonFile = new File([jsonContent], `${contractName}-input.json`, { type: "application/json" });
198-
files = [jsonFile];
199-
200-
// For std-json, we don't generate compiler settings since they're in the JSON input
201-
// compilerSettings will be undefined
202-
} else if (isEtherscanMultipleFilesObject(sourceCodeObject)) {
203-
// multiple-files method
204-
verificationMethod = "multiple-files";
205-
const sourcesObject = JSON.parse(sourceCodeObject) as { [key: string]: { content: string } };
206-
207-
// Use ContractFileName if available, otherwise search in sources
208-
if (etherscanResult.ContractFileName) {
209-
contractPath = etherscanResult.ContractFileName;
210-
} else {
211-
const foundPath = getContractPathFromSoliditySources(contractName, sourcesObject);
212-
if (!foundPath) {
213-
throw new Error("Could not find contract path in sources");
214-
}
215-
contractPath = foundPath;
216-
}
217-
218-
// Create files from sources object
219-
files = Object.entries(sourcesObject).map(([filename, object]) => {
220-
return new File([object.content as string], filename, { type: "text/plain" });
221-
});
222-
223-
// Generate compiler settings for multiple-files method
224-
compilerSettings = generateCompilerSettings(language, etherscanResult);
225-
} else {
226-
// single-file method
227-
verificationMethod = "single-file";
228-
const extension = language === "vyper" ? "vy" : "sol";
229-
230-
// Use ContractFileName if available, otherwise construct filename
231-
if (etherscanResult.ContractFileName) {
232-
contractPath = etherscanResult.ContractFileName;
233-
} else {
234-
contractPath = `${contractName}.${extension}`;
70+
return {
71+
compilerVersion: processedResult.compilerVersion,
72+
contractName: processedResult.contractName,
73+
contractPath: processedResult.contractPath,
74+
jsonInput: processedResult.jsonInput,
75+
};
76+
} catch (error) {
77+
if (error instanceof EtherscanImportError) {
78+
// Convert EtherscanImportError to regular Error for compatibility
79+
throw new Error(error.message);
23580
}
236-
237-
const sourceFile = new File([sourceCodeObject], contractPath, { type: "text/plain" });
238-
files = [sourceFile];
239-
240-
// Generate compiler settings for single-file method
241-
compilerSettings = generateCompilerSettings(language, etherscanResult);
81+
throw error;
24282
}
243-
244-
return {
245-
language,
246-
verificationMethod,
247-
compilerVersion,
248-
contractName,
249-
contractPath,
250-
files,
251-
compilerSettings,
252-
};
25383
};

0 commit comments

Comments
 (0)