@etherscan-npm/cli 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ Or run it once without keeping a global installation:
52
52
  npx @etherscan-npm/cli version
53
53
  ```
54
54
 
55
- The npm package downloads the matching native release archive and verifies its SHA-256 checksum during installation. Lifecycle scripts must be enabled.
55
+ The npm package selects a platform-specific optional dependency containing the native binary. It does not run installation lifecycle scripts. Installing with `--omit=optional` is not supported.
56
56
 
57
57
  > The package is currently published as `@etherscan-npm/cli` while the `@etherscan` npm scope is being transferred. The `etherscan` command is unchanged.
58
58
 
@@ -94,6 +94,30 @@ Ensure your Go binary directory (`GOBIN`, or `GOPATH/bin` by default) is on `PAT
94
94
 
95
95
  Download the archive for your operating system and architecture plus `checksums.txt` from [GitHub Releases](https://github.com/etherscan/etherscan-cli/releases/latest). Verify the archive's SHA-256 checksum, extract it, and place `etherscan` (or `etherscan.exe`) on your `PATH`.
96
96
 
97
+ ## Uninstall
98
+
99
+ Run:
100
+
101
+ ```sh
102
+ etherscan uninstall
103
+ ```
104
+
105
+ The command shows the exact package-manager action or executable and configuration paths before asking for confirmation. Use `--yes` to skip the prompt. Homebrew and npm installations are removed through their owning package manager. Installer, Go, manual, and source builds remove only the exact running executable; custom directories and `PATH` entries are left alone unless the installer recorded that it added them and the directory contains no other files.
106
+
107
+ The command never requests administrator privileges. If a manually copied binary is in a protected location, it leaves the saved configuration intact and prints the exact command needed to remove the binary. Manually created aliases or separate symlinks may also need to be removed by hand.
108
+
109
+ The installer scripts can remove a script installation without downloading a release archive:
110
+
111
+ ```sh
112
+ curl -fsSL https://raw.githubusercontent.com/etherscan/etherscan-cli/master/scripts/install.sh | sh -s -- --uninstall
113
+ ```
114
+
115
+ ```powershell
116
+ & ([scriptblock]::Create((irm https://raw.githubusercontent.com/etherscan/etherscan-cli/master/scripts/install.ps1))) -Uninstall
117
+ ```
118
+
119
+ Uninstall removes the saved configuration directory. If `ETHERSCAN_API_KEY` is set, unset it separately in the shell or environment where it is defined.
120
+
97
121
  ## Get started
98
122
 
99
123
  ### 1. Create an API key
@@ -143,7 +167,7 @@ Run `etherscan tui` to open the full-screen endpoint explorer:
143
167
  etherscan tui
144
168
  ```
145
169
 
146
- The explorer can be opened before authentication and asks you to validate and save a key when you submit an API-backed endpoint. Running `etherscan` with no command prints the Quick Start guide.
170
+ The explorer can be opened before authentication and asks you to validate and save a key when you submit an API-backed endpoint. It is read-only; source and proxy verification submissions remain available through the traditional commands. Running `etherscan` with no command prints the Quick Start guide.
147
171
 
148
172
  ## Practical workflows
149
173
 
@@ -168,6 +192,33 @@ etherscan contract getabi 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
168
192
  etherscan contract getsourcecode 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
169
193
  ```
170
194
 
195
+ ### Verify a contract
196
+
197
+ Verification lives in its own command group, `etherscan contractverification`, so that `etherscan contract` stays purely for reading on-chain contract data. Both groups call the same `contract` API module.
198
+
199
+ Use `--file` for Solidity, Abstract zkSync-stack, or Vyper source payloads. Stylus verification takes a public Git repository URL instead.
200
+
201
+ ```sh
202
+ # Solidity standard JSON
203
+ etherscan contractverification verify 0xYourContract --file input.json --codeformat solidity-standard-json-input --contractname contracts/Verified.sol:Verified --compilerversion v0.8.24+commit.e11b9ed9
204
+
205
+ # Abstract uses the zkSync compiler stack (Abstract Mainnet and Sepolia only)
206
+ etherscan --chain abstract contractverification verify-zksync 0xYourContract --file input.json --codeformat solidity-standard-json-input --contractname contracts/Verified.sol:Verified --compilerversion v0.8.24+commit.e11b9ed9 --zksolc-version v1.5.7
207
+
208
+ # Vyper JSON verification, and Stylus repository verification (Arbitrum One and Sepolia only)
209
+ etherscan contractverification verify-vyper 0xYourContract --file input.json --contractname contracts/Verified.vy:Verified --compilerversion vyper:0.4.0 --optimization-used 0
210
+ etherscan --chain arbitrum contractverification verify-stylus 0xYourContract --source-code https://github.com/example/project --contractname project --compilerversion stylus:0.5.3 --license-type 3
211
+ ```
212
+
213
+ Each submission returns a GUID. Poll it with `check-status` for source verification, or `check-proxy` for a proxy submission:
214
+
215
+ ```sh
216
+ etherscan contractverification check-status <guid>
217
+ etherscan contractverification check-proxy <guid>
218
+ ```
219
+
220
+ Constructor arguments may be supplied as bare ABI-encoded hex or with a `0x` prefix; the CLI sends the documented bare form. Verification source files are limited to 3,000,000 bytes. Run any verification command with `--help` for its exact options.
221
+
171
222
  ### Switch chains
172
223
 
173
224
  ```sh
@@ -209,7 +260,7 @@ Every command includes built-in parameter and usage help:
209
260
  ```sh
210
261
  etherscan --help
211
262
  etherscan account txlist --help
212
- etherscan contract verify --help
263
+ etherscan contractverification verify --help
213
264
  ```
214
265
 
215
266
  <details>
@@ -223,7 +274,7 @@ etherscan contract verify --help
223
274
  | `etherscan tui` | Launch the interactive explorer |
224
275
  | `etherscan login` | Validate and store an API key |
225
276
  | `etherscan logout` | Remove the stored API key |
226
- | `etherscan uninstall` | Remove all CLI configuration |
277
+ | `etherscan uninstall` | Remove the CLI and saved configuration |
227
278
  | `etherscan update` | Update a Homebrew or installer-script installation |
228
279
  | `etherscan whoami` | Show the active chain and masked API key |
229
280
  | `etherscan config` | Get, list, or set CLI configuration |
@@ -264,10 +315,20 @@ etherscan contract verify --help
264
315
  | `etherscan contract getabi` | Get a verified contract's ABI | [getabi](https://docs.etherscan.io/api-reference/endpoint/getabi.md) |
265
316
  | `etherscan contract getsourcecode` | Get verified source code and contract metadata | [getsourcecode](https://docs.etherscan.io/api-reference/endpoint/getsourcecode.md) |
266
317
  | `etherscan contract getcontractcreation` | Get creator and creation transaction data for contracts | [getcontractcreation](https://docs.etherscan.io/api-reference/endpoint/getcontractcreation.md) |
267
- | `etherscan contract verify` | Submit contract source code for verification | [verifysourcecode](https://docs.etherscan.io/api-reference/endpoint/verifysourcecode.md) |
268
- | `etherscan contract verify-status` | Check a source verification submission | [checkverifystatus](https://docs.etherscan.io/api-reference/endpoint/checkverifystatus.md) |
269
- | `etherscan contract verify-proxy` | Submit a proxy contract for verification | [verifyproxycontract](https://docs.etherscan.io/api-reference/endpoint/verifyproxycontract.md) |
270
- | `etherscan contract check-proxy` | Check a proxy verification submission | [checkproxyverification](https://docs.etherscan.io/api-reference/endpoint/checkproxyverification.md) |
318
+
319
+ ### Contract verification
320
+
321
+ Grouped separately from the contract data commands above. All of these call the `contract` API module.
322
+
323
+ | Command | Description | API docs |
324
+ | --- | --- | --- |
325
+ | `etherscan contractverification verify` | Submit Solidity source code for verification | [verifysourcecode](https://docs.etherscan.io/api-reference/endpoint/verifysourcecode.md) |
326
+ | `etherscan contractverification verify-zksync` | Submit zkSync-stack source code on Abstract | [verifyzksyncsourcecode](https://docs.etherscan.io/api-reference/endpoint/verifyzksyncsourcecode.md) |
327
+ | `etherscan contractverification verify-vyper` | Submit Vyper source code for verification | [verifyvyper](https://docs.etherscan.io/api-reference/endpoint/verifyvyper.md) |
328
+ | `etherscan contractverification verify-stylus` | Submit Stylus source code for verification | [verifystylus](https://docs.etherscan.io/api-reference/endpoint/verifystylus.md) |
329
+ | `etherscan contractverification verify-proxy` | Submit a proxy contract for verification | [verifyproxycontract](https://docs.etherscan.io/api-reference/endpoint/verifyproxycontract.md) |
330
+ | `etherscan contractverification check-status` | Check a source verification submission | [checkverifystatus](https://docs.etherscan.io/api-reference/endpoint/checkverifystatus.md) |
331
+ | `etherscan contractverification check-proxy` | Check a proxy verification submission | [checkproxyverification](https://docs.etherscan.io/api-reference/endpoint/checkproxyverification.md) |
271
332
 
272
333
  ### Transaction
273
334
 
@@ -427,9 +488,20 @@ go build -o etherscan ./cmd/etherscan
427
488
 
428
489
  Installer changes can be checked with `sh scripts/test-install.sh` on macOS/Linux or `./scripts/test-install.ps1` in PowerShell on Windows.
429
490
 
430
- The npm distribution can be checked with `sh scripts/test-npm.sh` on macOS/Linux or `./scripts/test-npm.ps1` in PowerShell. These tests pack and install the package against local fixture release archives; they do not publish to npm.
491
+ The npm distribution can be checked with `sh scripts/test-npm.sh` on macOS/Linux or `./scripts/test-npm.ps1` in PowerShell. These tests pack and install the umbrella and current-platform packages with lifecycle scripts disabled; they do not publish to npm.
492
+
493
+ The first seven-package npm release requires a one-time bootstrap by an npm administrator with publish access to the `@etherscan-npm` scope. After the matching GitHub release succeeds, check out its exact tag and run:
494
+
495
+ ```sh
496
+ gh release download v1.0.4 --dir dist
497
+ npm login
498
+ npm whoami
499
+ VERSION=1.0.4 node npm/publish.js
500
+ ```
501
+
502
+ The publisher verifies all six archives against `checksums.txt`, creates the six public platform packages by publishing them first, and publishes `@etherscan-npm/cli` last. It safely skips exact versions that already exist so a partial publication can be retried. Replace `1.0.4` with the actual unused release version if necessary.
431
503
 
432
- For the first npm release, publish the GitHub release assets before publishing the package. From an exact release-tag checkout, run `npm version --no-git-tag-version <version>` followed by `npm publish --access public`. Then configure npm trusted publishing for `etherscan/etherscan-cli` and `.github/workflows/release.yml`, and set the `NPM_PUBLISH_ENABLED` repository variable to `true` for later tagged releases.
504
+ After the bootstrap, configure npm trusted publishing for the umbrella and all six platform packages, targeting `etherscan/etherscan-cli` and `.github/workflows/release.yml`. Set the `NPM_PUBLISH_ENABLED` repository variable to `true`; subsequent tagged releases publish through GitHub Actions with provenance.
433
505
 
434
506
  ## API coverage and support
435
507
 
@@ -3,21 +3,45 @@
3
3
  "use strict";
4
4
 
5
5
  const fs = require("node:fs");
6
+ const os = require("node:os");
6
7
  const path = require("node:path");
7
8
  const { spawnSync } = require("node:child_process");
9
+ const packageInfo = require("../../package.json");
10
+ const { PLATFORMS, platformPackage } = require("../platform");
8
11
 
9
12
  const packageRoot = path.resolve(__dirname, "..", "..");
10
- const executable = path.join(
11
- packageRoot,
12
- "vendor",
13
- process.platform === "win32" ? "etherscan.exe" : "etherscan",
14
- );
13
+
14
+ // Resolve these once so the package lookup and executable name cannot disagree.
15
+ const platform = os.platform();
16
+ const arch = os.arch();
17
+ const binaryName = platform === "win32" ? "etherscan.exe" : "etherscan";
18
+ const reinstallHint = `Reinstall ${packageInfo.name} without --omit=optional.`;
19
+
20
+ function getExecutable() {
21
+ const packageName = platformPackage(platform, arch);
22
+ if (!packageName) {
23
+ console.error(
24
+ `Etherscan CLI does not support ${platform} ${arch}. ` +
25
+ `Supported platforms: ${Object.keys(PLATFORMS).join(", ")}.`,
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ try {
31
+ const manifest = require.resolve(`${packageName}/package.json`, {
32
+ paths: [packageRoot],
33
+ });
34
+ return path.join(path.dirname(manifest), binaryName);
35
+ } catch {
36
+ console.error(`The platform package ${packageName} is not installed. ${reinstallHint}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ const executable = getExecutable();
15
42
 
16
43
  if (!fs.existsSync(executable)) {
17
- console.error(
18
- "Etherscan CLI is not installed in this npm package. " +
19
- "Reinstall @etherscan-npm/cli without --ignore-scripts.",
20
- );
44
+ console.error(`The platform package executable is missing: ${executable}. ${reinstallHint}`);
21
45
  process.exit(1);
22
46
  }
23
47
 
@@ -26,6 +50,8 @@ const result = spawnSync(executable, process.argv.slice(2), {
26
50
  env: {
27
51
  ...process.env,
28
52
  ETHERSCAN_INSTALL_METHOD: "npm",
53
+ ETHERSCAN_NPM_PACKAGE: packageInfo.name,
54
+ ETHERSCAN_NPM_WRAPPER_PID: String(process.pid),
29
55
  },
30
56
  });
31
57
 
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ const PLATFORMS = Object.freeze({
4
+ "darwin arm64": "@etherscan-npm/cli-darwin-arm64",
5
+ "darwin x64": "@etherscan-npm/cli-darwin-x64",
6
+ "linux arm64": "@etherscan-npm/cli-linux-arm64",
7
+ "linux x64": "@etherscan-npm/cli-linux-x64",
8
+ "win32 arm64": "@etherscan-npm/cli-win32-arm64",
9
+ "win32 x64": "@etherscan-npm/cli-win32-x64",
10
+ });
11
+
12
+ function platformPackage(platform, arch) {
13
+ return PLATFORMS[`${platform} ${arch}`] || null;
14
+ }
15
+
16
+ module.exports = { PLATFORMS, platformPackage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etherscan-npm/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Command-line client and interactive explorer for the Etherscan V2 API",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/etherscan/etherscan-cli",
@@ -16,7 +16,6 @@
16
16
  "etherscan": "npm/bin/etherscan.js"
17
17
  },
18
18
  "scripts": {
19
- "postinstall": "node npm/postinstall.js",
20
19
  "prepublishOnly": "node npm/prepublish-check.js",
21
20
  "test:package": "npm pack --dry-run",
22
21
  "test:release": "node npm/publish.test.js",
@@ -24,13 +23,18 @@
24
23
  },
25
24
  "files": [
26
25
  "npm/bin/etherscan.js",
27
- "npm/postinstall.js",
28
- "npm/prepublish-check.js",
29
- "scripts/install.sh",
30
- "scripts/install.ps1",
26
+ "npm/platform.js",
31
27
  "README.md",
32
28
  "LICENSE"
33
29
  ],
30
+ "optionalDependencies": {
31
+ "@etherscan-npm/cli-darwin-arm64": "1.0.4",
32
+ "@etherscan-npm/cli-darwin-x64": "1.0.4",
33
+ "@etherscan-npm/cli-linux-arm64": "1.0.4",
34
+ "@etherscan-npm/cli-linux-x64": "1.0.4",
35
+ "@etherscan-npm/cli-win32-arm64": "1.0.4",
36
+ "@etherscan-npm/cli-win32-x64": "1.0.4"
37
+ },
34
38
  "engines": {
35
39
  "node": ">=18"
36
40
  },
@@ -1,58 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
- const { spawnSync } = require("node:child_process");
6
- const packageInfo = require("../package.json");
7
-
8
- const packageRoot = path.resolve(__dirname, "..");
9
- const installDir = path.join(packageRoot, "vendor");
10
- const version = `v${packageInfo.version}`;
11
-
12
- let command;
13
- let args;
14
- if (process.platform === "win32") {
15
- command = "powershell.exe";
16
- args = [
17
- "-NoLogo",
18
- "-NoProfile",
19
- "-NonInteractive",
20
- "-ExecutionPolicy",
21
- "Bypass",
22
- "-File",
23
- path.join(packageRoot, "scripts", "install.ps1"),
24
- "-Version",
25
- version,
26
- "-InstallDir",
27
- installDir,
28
- "-NoPathUpdate",
29
- ];
30
- } else {
31
- command = "sh";
32
- args = [
33
- path.join(packageRoot, "scripts", "install.sh"),
34
- "--version",
35
- version,
36
- "--install-dir",
37
- installDir,
38
- "--no-path-update",
39
- ];
40
- }
41
-
42
- const result = spawnSync(command, args, { stdio: "inherit", env: process.env });
43
- if (result.error) {
44
- console.error(`Unable to run the Etherscan CLI installer: ${result.error.message}`);
45
- process.exit(1);
46
- }
47
- if (result.status !== 0) {
48
- process.exit(result.status === null ? 1 : result.status);
49
- }
50
-
51
- const executable = path.join(
52
- installDir,
53
- process.platform === "win32" ? "etherscan.exe" : "etherscan",
54
- );
55
- if (!fs.existsSync(executable)) {
56
- console.error(`The installer did not create ${executable}.`);
57
- process.exit(1);
58
- }
@@ -1,81 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
- const packageInfo = require("../package.json");
6
-
7
- const versionPattern =
8
- /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
9
-
10
- // npm treats these as glob metacharacters in package.json "files".
11
- const globPattern = /[*?[\]{}]/;
12
-
13
- function checkVersion(version) {
14
- if (version === "0.0.0-development") {
15
- throw new Error("the development placeholder version cannot be published");
16
- }
17
- if (!versionPattern.test(version)) {
18
- throw new Error(`invalid release version: ${version}`);
19
- }
20
- }
21
-
22
- // `npm pack` copies the working tree, not the committed blobs. On Windows a
23
- // checkout with core.autocrlf=true turns the LF blobs into CRLF, and a CRLF
24
- // scripts/install.sh cannot be run by /bin/sh on Linux or macOS, which is how
25
- // @etherscan-npm/cli@1.0.1 shipped an installer that failed on both.
26
- function checkFileLineEndings(filePath, label) {
27
- const contents = fs.readFileSync(filePath);
28
- // Skip binaries using the same NUL-byte heuristic git applies for text=auto.
29
- if (contents.includes(0x00)) {
30
- return;
31
- }
32
- if (contents.includes(0x0d)) {
33
- throw new Error(`${label} contains CRLF line endings`);
34
- }
35
- }
36
-
37
- // Checks the literal entries of package.json "files". This fails closed: npm
38
- // also accepts globs and directories there, and silently skipping what it cannot
39
- // resolve would let a future "files" edit disable the gate while leaving the
40
- // publish green — the same silent-pass shape that let 1.0.1 ship.
41
- function checkLineEndings(baseDir, files = packageInfo.files) {
42
- for (const entry of files) {
43
- if (globPattern.test(entry)) {
44
- throw new Error(
45
- `${entry} is a glob pattern; this check only understands literal file paths, so update it before publishing`,
46
- );
47
- }
48
- const filePath = path.join(baseDir, entry);
49
- let stats;
50
- try {
51
- stats = fs.statSync(filePath);
52
- } catch (error) {
53
- if (error.code === "ENOENT") {
54
- throw new Error(`${entry} is listed in package.json "files" but does not exist`);
55
- }
56
- throw error;
57
- }
58
- if (stats.isDirectory()) {
59
- throw new Error(
60
- `${entry} is a directory; this check only understands literal file paths, so update it before publishing`,
61
- );
62
- }
63
- checkFileLineEndings(filePath, entry);
64
- }
65
- }
66
-
67
- function main() {
68
- checkVersion(packageInfo.version);
69
- checkLineEndings(path.resolve(__dirname, ".."));
70
- }
71
-
72
- module.exports = { checkVersion, checkFileLineEndings, checkLineEndings };
73
-
74
- if (require.main === module) {
75
- try {
76
- main();
77
- } catch (error) {
78
- console.error(`Refusing to publish @etherscan/cli: ${error.message}`);
79
- process.exit(1);
80
- }
81
- }
@@ -1,262 +0,0 @@
1
- [CmdletBinding()]
2
- param(
3
- [string]$Version = $env:ETHERSCAN_VERSION,
4
- [string]$InstallDir = $env:ETHERSCAN_INSTALL_DIR,
5
- [switch]$NoPathUpdate,
6
- [int]$WaitForProcessId = 0,
7
- [switch]$CleanupScript
8
- )
9
-
10
- $ErrorActionPreference = "Stop"
11
- $ProgressPreference = "SilentlyContinue"
12
-
13
- $Repository = "etherscan/etherscan-cli"
14
- $DownloadBaseUrl = $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL
15
-
16
- function Get-EtherscanArchitecture {
17
- $architecture = $env:PROCESSOR_ARCHITEW6432
18
- if ([string]::IsNullOrWhiteSpace($architecture)) {
19
- $architecture = $env:PROCESSOR_ARCHITECTURE
20
- }
21
-
22
- switch -Regex ($architecture) {
23
- "^(AMD64|x86_64)$" { return "amd64" }
24
- "^(ARM64|aarch64)$" { return "arm64" }
25
- default { throw "Unsupported Windows architecture: $architecture. Etherscan CLI supports amd64 and arm64." }
26
- }
27
- }
28
-
29
- function Get-GitHubApiHeaders {
30
- $headers = @{
31
- Accept = "application/vnd.github+json"
32
- "User-Agent" = "etherscan-cli-installer"
33
- "X-GitHub-Api-Version" = "2022-11-28"
34
- }
35
- if (-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) {
36
- $headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)"
37
- }
38
- return $headers
39
- }
40
-
41
- function Resolve-EtherscanVersion {
42
- param([string]$RequestedVersion)
43
-
44
- if (-not [string]::IsNullOrWhiteSpace($RequestedVersion) -and $RequestedVersion -ne "latest") {
45
- $tag = if ($RequestedVersion.StartsWith("v")) { $RequestedVersion } else { "v$RequestedVersion" }
46
- }
47
- else {
48
- if (-not [string]::IsNullOrWhiteSpace($DownloadBaseUrl)) {
49
- throw "A version is required when the installer test download source is used."
50
- }
51
- $release = Invoke-RestMethod `
52
- -Uri "https://api.github.com/repos/$Repository/releases/latest" `
53
- -Headers (Get-GitHubApiHeaders)
54
- $tag = [string]$release.tag_name
55
- }
56
-
57
- if ($tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$') {
58
- throw "Invalid release version: $tag"
59
- }
60
-
61
- return @{
62
- Tag = $tag
63
- Version = $tag.Substring(1)
64
- }
65
- }
66
-
67
- function Copy-InstallerFile {
68
- param(
69
- [string]$Base,
70
- [string]$Name,
71
- [string]$Destination
72
- )
73
-
74
- if (Test-Path -LiteralPath $Base -PathType Container) {
75
- Copy-Item -LiteralPath (Join-Path $Base $Name) -Destination $Destination
76
- return
77
- }
78
-
79
- $uri = "$($Base.TrimEnd('/'))/$Name"
80
- $parsedUri = [Uri]$uri
81
- if ($parsedUri.Scheme -ne "https") {
82
- throw "Remote downloads must use HTTPS: $uri"
83
- }
84
-
85
- $headers = @{
86
- "User-Agent" = "etherscan-cli-installer"
87
- }
88
- if ($parsedUri.Host -in @("github.com", "api.github.com") -and
89
- -not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) {
90
- $headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)"
91
- }
92
-
93
- Invoke-WebRequest -Uri $uri -OutFile $Destination -Headers $headers -UseBasicParsing
94
- }
95
-
96
- function Get-SHA256FileHash {
97
- param([string]$Path)
98
-
99
- $sha256 = [Security.Cryptography.SHA256]::Create()
100
- try {
101
- $stream = [IO.File]::OpenRead($Path)
102
- try {
103
- return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace("-", "").ToLowerInvariant()
104
- }
105
- finally {
106
- $stream.Dispose()
107
- }
108
- }
109
- finally {
110
- $sha256.Dispose()
111
- }
112
- }
113
-
114
- function Add-EtherscanToUserPath {
115
- param([string]$Directory)
116
-
117
- $fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
118
- $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
119
- $entries = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
120
- $alreadyPresent = $entries | Where-Object {
121
- try {
122
- $expandedEntry = [Environment]::ExpandEnvironmentVariables($_)
123
- [IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
124
- }
125
- catch {
126
- $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
127
- }
128
- }
129
-
130
- if (-not $alreadyPresent) {
131
- $newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) {
132
- $fullDirectory
133
- }
134
- else {
135
- "$($userPath.TrimEnd(';'));$fullDirectory"
136
- }
137
- [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
138
- Write-Host "Added $fullDirectory to your user PATH."
139
- }
140
-
141
- $processEntries = @($env:Path -split ';')
142
- if (-not ($processEntries | Where-Object { $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) })) {
143
- $env:Path = "$env:Path;$fullDirectory"
144
- }
145
- }
146
-
147
- if ($env:OS -ne "Windows_NT") {
148
- throw "This installer supports Windows only. Use install.sh on macOS or Linux."
149
- }
150
-
151
- if ([string]::IsNullOrWhiteSpace($InstallDir)) {
152
- $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
153
- $InstallDir = Join-Path $localAppData "Programs\Etherscan\bin"
154
- }
155
- if ($InstallDir.Contains(';')) {
156
- throw "The installation directory cannot contain a semicolon."
157
- }
158
- if ($InstallDir.IndexOfAny([char[]]"`r`n") -ge 0) {
159
- throw "The installation directory cannot contain a line break."
160
- }
161
- if ($WaitForProcessId -gt 0) {
162
- Wait-Process -Id $WaitForProcessId -ErrorAction SilentlyContinue
163
- }
164
-
165
- $resolved = Resolve-EtherscanVersion -RequestedVersion $Version
166
- $architecture = Get-EtherscanArchitecture
167
- $archiveName = "etherscan_$($resolved.Version)_windows_$architecture.zip"
168
- $baseUrl = if ([string]::IsNullOrWhiteSpace($DownloadBaseUrl)) {
169
- "https://github.com/$Repository/releases/download/$($resolved.Tag)"
170
- }
171
- else {
172
- $DownloadBaseUrl
173
- }
174
-
175
- $tempDirectory = Join-Path ([IO.Path]::GetTempPath()) "etherscan-install-$PID-$([Guid]::NewGuid().ToString('N'))"
176
- $archivePath = Join-Path $tempDirectory $archiveName
177
- $checksumPath = Join-Path $tempDirectory "checksums.txt"
178
- $sourceExecutable = Join-Path $tempDirectory "etherscan.exe"
179
-
180
- try {
181
- New-Item -ItemType Directory -Path $tempDirectory -Force | Out-Null
182
-
183
- Write-Host "Downloading Etherscan CLI $($resolved.Version) for windows/$architecture..."
184
- Copy-InstallerFile -Base $baseUrl -Name $archiveName -Destination $archivePath
185
- Copy-InstallerFile -Base $baseUrl -Name "checksums.txt" -Destination $checksumPath
186
-
187
- $pattern = '^([0-9A-Fa-f]{64})\s+\*?' + [Regex]::Escape($archiveName) + '$'
188
- $checksumLine = Get-Content -LiteralPath $checksumPath | Where-Object { $_ -match $pattern } | Select-Object -First 1
189
- if (-not $checksumLine -or $checksumLine -notmatch $pattern) {
190
- throw "No checksum was published for $archiveName."
191
- }
192
-
193
- $expectedHash = $Matches[1].ToLowerInvariant()
194
- $actualHash = Get-SHA256FileHash -Path $archivePath
195
- if ($actualHash -ne $expectedHash) {
196
- throw "Checksum verification failed for $archiveName. Expected $expectedHash, received $actualHash."
197
- }
198
-
199
- Add-Type -AssemblyName System.IO.Compression.FileSystem
200
- $zip = [IO.Compression.ZipFile]::OpenRead($archivePath)
201
- try {
202
- $executableEntries = @($zip.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq "etherscan.exe" })
203
- if ($executableEntries.Count -ne 1) {
204
- throw "$archiveName must contain exactly one root-level etherscan.exe."
205
- }
206
-
207
- $inputStream = $executableEntries[0].Open()
208
- $outputStream = [IO.File]::Create($sourceExecutable)
209
- try {
210
- $inputStream.CopyTo($outputStream)
211
- }
212
- finally {
213
- $outputStream.Dispose()
214
- $inputStream.Dispose()
215
- }
216
- }
217
- finally {
218
- $zip.Dispose()
219
- }
220
-
221
- New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
222
- $targetExecutable = Join-Path $InstallDir "etherscan.exe"
223
- $stagedExecutable = Join-Path $InstallDir ".etherscan.exe.new-$PID"
224
- $backupExecutable = Join-Path $InstallDir ".etherscan.exe.old-$PID"
225
- Copy-Item -LiteralPath $sourceExecutable -Destination $stagedExecutable -Force
226
-
227
- try {
228
- if (Test-Path -LiteralPath $targetExecutable) {
229
- Move-Item -LiteralPath $targetExecutable -Destination $backupExecutable -Force
230
- }
231
- Move-Item -LiteralPath $stagedExecutable -Destination $targetExecutable -Force
232
- Remove-Item -LiteralPath $backupExecutable -Force -ErrorAction SilentlyContinue
233
- }
234
- catch {
235
- Remove-Item -LiteralPath $stagedExecutable -Force -ErrorAction SilentlyContinue
236
- if ((Test-Path -LiteralPath $backupExecutable) -and -not (Test-Path -LiteralPath $targetExecutable)) {
237
- Move-Item -LiteralPath $backupExecutable -Destination $targetExecutable -Force
238
- }
239
- throw
240
- }
241
-
242
- if (-not $NoPathUpdate) {
243
- Add-EtherscanToUserPath -Directory $InstallDir
244
- }
245
-
246
- Write-Host ""
247
- Write-Host "Etherscan CLI $($resolved.Version) installed successfully."
248
- Write-Host "Installed to: $targetExecutable"
249
- if ($NoPathUpdate) {
250
- Write-Host "Add $InstallDir to PATH to run etherscan from any directory."
251
- }
252
- else {
253
- Write-Host "Run 'etherscan version' to verify the installation."
254
- Write-Host "Open a new terminal if the command is not yet available."
255
- }
256
- }
257
- finally {
258
- Remove-Item -LiteralPath $tempDirectory -Recurse -Force -ErrorAction SilentlyContinue
259
- if ($CleanupScript -and -not [string]::IsNullOrWhiteSpace($PSCommandPath)) {
260
- Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
261
- }
262
- }
@@ -1,211 +0,0 @@
1
- #!/bin/sh
2
-
3
- set -eu
4
-
5
- repository="etherscan/etherscan-cli"
6
- version=${ETHERSCAN_VERSION:-}
7
- install_dir=${ETHERSCAN_INSTALL_DIR:-"$HOME/.local/bin"}
8
- download_base=${ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL:-}
9
- update_path=1
10
-
11
- usage() {
12
- cat <<'EOF'
13
- Install Etherscan CLI.
14
-
15
- Usage: install.sh [options]
16
-
17
- Options:
18
- --version VERSION Install a specific version (for example, v1.1.0).
19
- --install-dir DIRECTORY Install into DIRECTORY (default: ~/.local/bin).
20
- --no-path-update Do not update the shell profile.
21
- -h, --help Show this help.
22
- EOF
23
- }
24
-
25
- die() {
26
- printf 'error: %s\n' "$*" >&2
27
- exit 1
28
- }
29
-
30
- while [ "$#" -gt 0 ]; do
31
- case "$1" in
32
- --version)
33
- [ "$#" -ge 2 ] || die "--version requires a value"
34
- version=$2
35
- shift 2
36
- ;;
37
- --install-dir)
38
- [ "$#" -ge 2 ] || die "--install-dir requires a value"
39
- install_dir=$2
40
- shift 2
41
- ;;
42
- --no-path-update)
43
- update_path=0
44
- shift
45
- ;;
46
- -h|--help)
47
- usage
48
- exit 0
49
- ;;
50
- *)
51
- die "unknown option: $1"
52
- ;;
53
- esac
54
- done
55
-
56
- if printf '%s' "$install_dir" | LC_ALL=C grep '[[:cntrl:]]' >/dev/null 2>&1; then
57
- die "the installation directory cannot contain control characters"
58
- fi
59
-
60
- fetch_stdout() {
61
- url=$1
62
- if command -v curl >/dev/null 2>&1; then
63
- curl -fsSL -A etherscan-cli-installer "$url"
64
- elif command -v wget >/dev/null 2>&1; then
65
- wget -qO- --user-agent=etherscan-cli-installer "$url"
66
- else
67
- die "curl or wget is required"
68
- fi
69
- }
70
-
71
- fetch_file() {
72
- base=$1
73
- name=$2
74
- destination=$3
75
-
76
- if [ -d "$base" ]; then
77
- cp "$base/$name" "$destination"
78
- return
79
- fi
80
-
81
- case "$base" in
82
- file://*)
83
- cp "${base#file://}/$name" "$destination"
84
- ;;
85
- https://*)
86
- if command -v curl >/dev/null 2>&1; then
87
- curl -fsSL -A etherscan-cli-installer "$base/$name" -o "$destination"
88
- elif command -v wget >/dev/null 2>&1; then
89
- wget -q --user-agent=etherscan-cli-installer "$base/$name" -O "$destination"
90
- else
91
- die "curl or wget is required"
92
- fi
93
- ;;
94
- *)
95
- die "invalid download base URL or directory: $base"
96
- ;;
97
- esac
98
- }
99
-
100
- if [ -z "$version" ] || [ "$version" = latest ]; then
101
- [ -z "$download_base" ] || die "a version is required with the installer test download source"
102
- release_json=$(fetch_stdout "https://api.github.com/repos/$repository/releases/latest")
103
- version=$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')
104
- [ -n "$version" ] || die "could not resolve the latest Etherscan CLI version"
105
- fi
106
-
107
- case "$version" in
108
- v*) tag=$version; release_version=${version#v} ;;
109
- *) tag="v$version"; release_version=$version ;;
110
- esac
111
-
112
- printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$' || die "invalid release version: $tag"
113
-
114
- system_name=${ETHERSCAN_INSTALL_TEST_OS:-$(uname -s)}
115
- case "$system_name" in
116
- Linux|linux) os=linux ;;
117
- Darwin|darwin) os=darwin ;;
118
- *) die "unsupported operating system: $system_name" ;;
119
- esac
120
-
121
- machine_arch=${ETHERSCAN_INSTALL_TEST_ARCH:-$(uname -m)}
122
- case "$machine_arch" in
123
- x86_64|amd64) arch=amd64 ;;
124
- arm64|aarch64) arch=arm64 ;;
125
- *) die "unsupported architecture: $machine_arch. Etherscan CLI supports amd64 and arm64." ;;
126
- esac
127
-
128
- archive_name="etherscan_${release_version}_${os}_${arch}.tar.gz"
129
- if [ -z "$download_base" ]; then
130
- download_base="https://github.com/$repository/releases/download/$tag"
131
- fi
132
-
133
- temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t etherscan-install)
134
- trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM
135
- archive_path="$temp_dir/$archive_name"
136
- checksum_path="$temp_dir/checksums.txt"
137
- source_executable="$temp_dir/etherscan"
138
-
139
- printf 'Downloading Etherscan CLI %s for %s/%s...\n' "$release_version" "$os" "$arch"
140
- fetch_file "$download_base" "$archive_name" "$archive_path"
141
- fetch_file "$download_base" checksums.txt "$checksum_path"
142
-
143
- expected_hash=$(awk -v name="$archive_name" '$2 == name || $2 == ("*" name) { print tolower($1); exit }' "$checksum_path")
144
- [ -n "$expected_hash" ] || die "no checksum was published for $archive_name"
145
- printf '%s\n' "$expected_hash" | grep -Eq '^[0-9a-f]{64}$' || die "invalid checksum published for $archive_name"
146
-
147
- if command -v sha256sum >/dev/null 2>&1; then
148
- actual_hash=$(sha256sum "$archive_path" | awk '{ print tolower($1) }')
149
- elif command -v shasum >/dev/null 2>&1; then
150
- actual_hash=$(shasum -a 256 "$archive_path" | awk '{ print tolower($1) }')
151
- else
152
- die "sha256sum or shasum is required to verify the download"
153
- fi
154
-
155
- [ "$actual_hash" = "$expected_hash" ] || die "checksum verification failed for $archive_name"
156
-
157
- entry_count=$(tar -tzf "$archive_path" | awk '$0 == "etherscan" { count++ } END { print count + 0 }')
158
- [ "$entry_count" -eq 1 ] || die "$archive_name must contain exactly one root-level etherscan"
159
- tar -xOzf "$archive_path" etherscan >"$source_executable"
160
- [ -s "$source_executable" ] || die "$archive_name contains an empty etherscan executable"
161
-
162
- mkdir -p "$install_dir"
163
- staged_executable="$install_dir/.etherscan.new.$$"
164
- cp "$source_executable" "$staged_executable"
165
- chmod 0755 "$staged_executable"
166
- mv -f "$staged_executable" "$install_dir/etherscan"
167
-
168
- path_updated=0
169
- if [ "$update_path" -eq 1 ]; then
170
- case ":$PATH:" in
171
- *:"$install_dir":*) ;;
172
- *)
173
- shell_name=${SHELL:-sh}
174
- shell_name=${shell_name##*/}
175
- escaped_install_dir=$(printf '%s' "$install_dir" | sed 's/[\\"$`]/\\&/g')
176
- if [ "$shell_name" = fish ]; then
177
- profile="$HOME/.config/fish/config.fish"
178
- mkdir -p "$(dirname "$profile")"
179
- path_line="fish_add_path \"$escaped_install_dir\""
180
- else
181
- case "$shell_name" in
182
- zsh) profile="$HOME/.zshrc" ;;
183
- bash) profile="$HOME/.bashrc" ;;
184
- *) profile="$HOME/.profile" ;;
185
- esac
186
- path_line="export PATH=\"$escaped_install_dir:\$PATH\""
187
- fi
188
-
189
- # Match the exact line we would write (whole-line, fixed-string) so an
190
- # unrelated profile line that merely contains the path does not suppress
191
- # the update, and a genuine duplicate is not appended.
192
- if ! [ -f "$profile" ] || ! grep -Fx -e "$path_line" "$profile" >/dev/null 2>&1; then
193
- {
194
- printf '\n# Etherscan CLI\n'
195
- printf '%s\n' "$path_line"
196
- } >>"$profile"
197
- path_updated=1
198
- fi
199
- ;;
200
- esac
201
- fi
202
-
203
- printf '\nEtherscan CLI %s installed successfully.\n' "$release_version"
204
- printf 'Installed to: %s\n' "$install_dir/etherscan"
205
- if [ "$update_path" -eq 0 ]; then
206
- printf 'Add %s to PATH to run etherscan from any directory.\n' "$install_dir"
207
- elif [ "$path_updated" -eq 1 ]; then
208
- printf 'Open a new terminal, then run: etherscan version\n'
209
- else
210
- printf 'Run: etherscan version\n'
211
- fi