Skip to content

Commit 4d3c3d4

Browse files
authored
support more networks (#21)
* Change all occurences of [Ethereum Viewer] to [Ethereum Code Viewer] * Get API name from subdomain or search params * Add explorer API keys * Change vercel.json * Remove trailing slash * Test called URL * Handle unverified contracts * Test unverified error message
1 parent 3d4a3d7 commit 4d3c3d4

File tree

22 files changed

+1887
-108
lines changed

22 files changed

+1887
-108
lines changed

.mocharc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
require: ["ts-node/register/transpile-only"],
3+
extension: ["ts"],
4+
watchExtensions: ["ts"],
5+
// Extension tests run inside of VSCode instance, so we don't include them here
6+
spec: ["packages/vscode-host/src/**/*.test.ts"],
7+
};

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,27 @@
88
"copy-and-serve": "cd packages/vscode-host && yarn copy-and-serve",
99
"lint": "eslint --ext .ts ./packages/*/src/**/*.ts",
1010
"lint:fix": "pnpm lint -- --fix",
11-
"prepare-deploy": "cp ./vercel.json ./packages/vscode-host/dist"
11+
"prepare-deploy": "cp ./vercel.json ./packages/vscode-host/dist",
12+
"test": "mocha"
1213
},
1314
"devDependencies": {
15+
"@types/jsdom": "^16.2.14",
1416
"@types/mocha": "^9.0.0",
1517
"@types/node": "^16.11.12",
1618
"@types/webpack": "^5.28.0",
1719
"@types/webpack-env": "^1.16.2",
1820
"@typescript-eslint/eslint-plugin": "^5.3.0",
1921
"@typescript-eslint/parser": "^5.3.0",
2022
"assert": "^2.0.0",
23+
"earljs": "^0.1.12",
2124
"eslint": "^7",
2225
"eslint-config-typestrict": "^1.0.2",
2326
"eslint-plugin-import": "^2.25.2",
2427
"eslint-plugin-no-only-tests": "^2.6.0",
2528
"eslint-plugin-simple-import-sort": "^7.0.0",
2629
"eslint-plugin-sonarjs": "^0.10.0",
2730
"eslint-plugin-unused-imports": "^1.1.5",
31+
"jsdom": "^19.0.0",
2832
"mocha": "^9.1.3",
2933
"process": "^0.11.10",
3034
"serve": "^13.0.2",

packages/ethereum-viewer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"engines": {
2828
"vscode": "^1.63.0"
2929
},
30-
"displayName": "Ethereum Viewer",
30+
"displayName": "Ethereum Code Viewer",
3131
"description": "View source of deployed Ethereum contracts in VSCode",
3232
"publisher": "hasparus",
3333
"categories": [
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as vscode from "vscode";
2+
3+
import type {
4+
ExecuteHostCommand,
5+
// @ts-ignore - this import won't exist at runtime, we're using it only for better DX
6+
} from "../../vscode-host/src/deth/commands/ethViewerCommands";
7+
8+
export const executeHostCommand: ExecuteHostCommand = (command, ...args) =>
9+
vscode.commands.executeCommand<any>(
10+
`dethcrypto.vscode-host.${command}`,
11+
...args
12+
);

packages/ethereum-viewer/src/etherscan.ts renamed to packages/ethereum-viewer/src/explorer/fetchFiles.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import { join } from "path";
22
import { assert, StrictOmit } from "ts-essentials";
33

4-
import { fetch } from "./util/fetch";
5-
import { prettyStringify } from "./util/stringify";
6-
7-
const ETHERSCAN_API_KEY = "862Y3WJ4JB4B34PZQRFEV3IK6SZ8GNR9N5";
4+
import { fetch as _fetch } from "../util/fetch";
5+
import { prettyStringify } from "../util/stringify";
6+
import { ApiName, explorerApiKeys, explorerApiUrls } from "./networks";
87

98
export async function fetchFiles(
10-
network: Network,
11-
contractAddress: string
9+
apiName: ApiName,
10+
contractAddress: string,
11+
fetch: typeof _fetch = _fetch
1212
): Promise<FetchFilesResult> {
13-
const api = `https://api${
14-
network === "mainnet" ? "" : `-${network}`
15-
}.etherscan.io/api`;
16-
17-
const url = `${api}?module=contract&action=getsourcecode&address=${contractAddress}&apikey=${ETHERSCAN_API_KEY}`;
13+
const apiUrl = explorerApiUrls[apiName];
14+
const url =
15+
apiUrl +
16+
"?module=contract" +
17+
"&action=getsourcecode" +
18+
`&address=${contractAddress}` +
19+
`&apikey=${explorerApiKeys[apiName]}`;
1820

1921
const response = (await fetch(url)) as Etherscan.ContractSourceResponse;
2022

@@ -25,7 +27,7 @@ export async function fetchFiles(
2527

2628
const {
2729
SourceCode: sourceCode,
28-
ABI: _abi,
30+
ABI: abi,
2931
Implementation: implementationAddr,
3032
..._info
3133
} = response.result[0];
@@ -38,6 +40,15 @@ export async function fetchFiles(
3840
// set of curly braces
3941
const isFlattened = !sourceCode.startsWith("{");
4042

43+
if (!info.ContractName && abi === "Contract source code not verified") {
44+
return {
45+
files: {
46+
"error.md": contractNotVerifiedErrorMsg(apiName, contractAddress),
47+
},
48+
info,
49+
};
50+
}
51+
4152
if (isFlattened) {
4253
files[info.ContractName + ".sol"] = sourceCode;
4354
} else {
@@ -55,7 +66,7 @@ export async function fetchFiles(
5566
}
5667

5768
if (implementationAddr) {
58-
const implementation = await fetchFiles(network, implementationAddr);
69+
const implementation = await fetchFiles(apiName, implementationAddr);
5970
Object.assign(
6071
files,
6172
prefixFiles(implementation.files, implementation.info.ContractName)
@@ -95,19 +106,22 @@ export type FilePath = string & { __brand?: "Path" };
95106

96107
export type FileContent = string & { __brand?: "FileContent" };
97108

98-
export type Network = "mainnet" | "ropsten" | "rinkeby" | "kovan" | "goerli";
99-
100-
declare namespace Etherscan {
109+
export declare namespace Etherscan {
101110
interface ContractSourceResponse {
111+
status: "1" | "0";
102112
message: string;
103113
result: Etherscan.ContractInfo[];
104114
}
105115

106-
type MultipleSourceCodes = `{${string}}`;
107-
type FlattenedSourceCode = `//${string}}`;
116+
type UnverifiedSourceCode = "";
117+
type MultipleSourceCodes = `{}`;
118+
type FlattenedSourceCode = FileContent;
108119

109120
interface ContractInfo {
110-
SourceCode: MultipleSourceCodes | FlattenedSourceCode;
121+
SourceCode:
122+
| MultipleSourceCodes
123+
| FlattenedSourceCode
124+
| UnverifiedSourceCode;
111125
ABI: string;
112126
ContractName: string;
113127
CompilerVersion: string;
@@ -139,3 +153,23 @@ declare namespace Etherscan {
139153

140154
type Abi = object[];
141155
}
156+
157+
function contractNotVerifiedErrorMsg(
158+
apiName: ApiName,
159+
contractAddress: string
160+
) {
161+
const websiteUrl = apiUrlToWebsite(explorerApiUrls[apiName]);
162+
return `\
163+
Oops! It seems this contract source code is not verified on ${websiteUrl}.
164+
165+
Take a look at ${websiteUrl}/address/${contractAddress}.
166+
`;
167+
}
168+
169+
function apiUrlToWebsite(url: string) {
170+
// This is a bit of a hack, but they all have the same URL scheme.
171+
return url
172+
.replace("//api.", "//")
173+
.replace("//api-", "//")
174+
.replace(/\/api$/, "");
175+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./fetchFiles";
2+
export * from "./networks";
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* mapping from Ethereum Code Viewer subdomain to Etherscan-like API URL
3+
*/
4+
export const explorerApiUrls = {
5+
etherscan: "https://api.etherscan.io/api",
6+
"ropsten.etherscan": "https://api-ropsten.etherscan.io/api",
7+
"rinkeby.etherscan": "https://api-rinkeby.etherscan.io/api",
8+
"goerli.etherscan": "https://api-goerli.etherscan.io/api",
9+
"kovan.etherscan": "https://api-kovan.etherscan.io/api",
10+
bscscan: "https://api.bscscan.com/api",
11+
"testnet.bscscan": "https://api-testnet.bscscan.com/api",
12+
hecoinfo: "https://api.hecoinfo.com/api",
13+
"testnet.hecoinfo": "https://api-testnet.hecoinfo.com/api",
14+
ftmscan: "https://api.ftmscan.com/api",
15+
"testnet.ftmscan": "https://api-testnet.ftmscan.com/api",
16+
"optimistic.etherscan": "https://api-optimistic.etherscan.io/api",
17+
"kovan-optimistic.etherscan": "https://api-kovan-optimistic.etherscan.io/api",
18+
polygonscan: "https://api.polygonscan.com/api",
19+
"testnet.polygonscan": "https://api-testnet.polygonscan.com/api",
20+
arbiscan: "https://api.arbiscan.io/api",
21+
"testnet.arbiscan": "https://api-testnet.arbiscan.io/api",
22+
snowtrace: "https://api.snowtrace.io/api",
23+
"testnet.snowtrace": "https://api-testnet.snowtrace.io/api",
24+
};
25+
26+
/**
27+
* subdomain of ethereum code viewer
28+
*/
29+
export type ApiName = keyof typeof explorerApiUrls;
30+
31+
/**
32+
* mapping from Ethereum Code Viewer subdomain to memfs root directory name
33+
*/
34+
export const networkNames: Record<ApiName, string> = {
35+
etherscan: "mainnet",
36+
"ropsten.etherscan": "ropsten",
37+
"rinkeby.etherscan": "rinkeby",
38+
"goerli.etherscan": "goerli",
39+
"kovan.etherscan": "kovan",
40+
bscscan: "bsc",
41+
"testnet.bscscan": "bscTestnet",
42+
hecoinfo: "heco",
43+
"testnet.hecoinfo": "hecoTestnet",
44+
ftmscan: "fantom",
45+
"testnet.ftmscan": "ftmTestnet",
46+
"optimistic.etherscan": "optimism",
47+
"kovan-optimistic.etherscan": "optimismKovan",
48+
polygonscan: "polygon",
49+
"testnet.polygonscan": "polygonMumbai",
50+
arbiscan: "arbitrumOne",
51+
"testnet.arbiscan": "arbitrumTestnet",
52+
snowtrace: "avalanche",
53+
"testnet.snowtrace": "avalancheTestnet",
54+
};
55+
56+
const ETHERSCAN_KEY = "862Y3WJ4JB4B34PZQRFEV3IK6SZ8GNR9N5";
57+
const BSCSCAN_KEY = "HFUM7BBA5MRUQCN5UMEQPUZBUPPRHIQT3Y";
58+
const FTMSCAN_KEY = "EH9NPZVF1HMNAQMAUZKA4VF7EC23X37DGS";
59+
const HECOINFO_KEY = "XEUTJF2439EP4HHD23H2AFEFQJHFGSG57R";
60+
const SNOWTRACE_KEY = "IQEHAJ43W674REN5XV79WF47X37VEB8PIC";
61+
const ARBISCAN_KEY = "X3ZWJBXC14HTIR3B9DNYGEUICEIKKZ9ENZ";
62+
const POLYGONSCAN_KEY = "RV4YXDXEMIHXMC7ZXB8T82G4F56FRZ1SZQ";
63+
64+
// @todo this should be possible to override using VSCode settings
65+
export const explorerApiKeys: Record<ApiName, string> = {
66+
etherscan: ETHERSCAN_KEY,
67+
"ropsten.etherscan": ETHERSCAN_KEY,
68+
"rinkeby.etherscan": ETHERSCAN_KEY,
69+
"goerli.etherscan": ETHERSCAN_KEY,
70+
"kovan.etherscan": ETHERSCAN_KEY,
71+
72+
"optimistic.etherscan": ETHERSCAN_KEY,
73+
"kovan-optimistic.etherscan": ETHERSCAN_KEY,
74+
75+
arbiscan: ARBISCAN_KEY,
76+
"testnet.arbiscan": ARBISCAN_KEY,
77+
78+
bscscan: BSCSCAN_KEY,
79+
"testnet.bscscan": BSCSCAN_KEY,
80+
81+
ftmscan: FTMSCAN_KEY,
82+
"testnet.ftmscan": FTMSCAN_KEY,
83+
84+
hecoinfo: HECOINFO_KEY,
85+
"testnet.hecoinfo": HECOINFO_KEY,
86+
87+
polygonscan: POLYGONSCAN_KEY,
88+
"testnet.polygonscan": POLYGONSCAN_KEY,
89+
90+
snowtrace: SNOWTRACE_KEY,
91+
"testnet.snowtrace": SNOWTRACE_KEY,
92+
};

0 commit comments

Comments
 (0)