@etherscan-npm/cli 1.0.2 → 1.0.3

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
@@ -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
 
@@ -5,6 +5,7 @@
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const { spawnSync } = require("node:child_process");
8
+ const packageInfo = require("../../package.json");
8
9
 
9
10
  const packageRoot = path.resolve(__dirname, "..", "..");
10
11
  const executable = path.join(
@@ -26,6 +27,8 @@ const result = spawnSync(executable, process.argv.slice(2), {
26
27
  env: {
27
28
  ...process.env,
28
29
  ETHERSCAN_INSTALL_METHOD: "npm",
30
+ ETHERSCAN_NPM_PACKAGE: packageInfo.name,
31
+ ETHERSCAN_NPM_WRAPPER_PID: String(process.pid),
29
32
  },
30
33
  });
31
34
 
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.3",
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",
@@ -4,7 +4,8 @@ param(
4
4
  [string]$InstallDir = $env:ETHERSCAN_INSTALL_DIR,
5
5
  [switch]$NoPathUpdate,
6
6
  [int]$WaitForProcessId = 0,
7
- [switch]$CleanupScript
7
+ [switch]$CleanupScript,
8
+ [switch]$Uninstall
8
9
  )
9
10
 
10
11
  $ErrorActionPreference = "Stop"
@@ -12,6 +13,8 @@ $ProgressPreference = "SilentlyContinue"
12
13
 
13
14
  $Repository = "etherscan/etherscan-cli"
14
15
  $DownloadBaseUrl = $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL
16
+ $InstallMarkerName = ".etherscan-cli-path-added"
17
+ $InstallMarkerContent = "etherscan-cli:path-added:v1"
15
18
 
16
19
  function Get-EtherscanArchitecture {
17
20
  $architecture = $env:PROCESSOR_ARCHITEW6432
@@ -118,12 +121,13 @@ function Add-EtherscanToUserPath {
118
121
  $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
119
122
  $entries = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
120
123
  $alreadyPresent = $entries | Where-Object {
124
+ $entry = $_
121
125
  try {
122
- $expandedEntry = [Environment]::ExpandEnvironmentVariables($_)
126
+ $expandedEntry = [Environment]::ExpandEnvironmentVariables($entry)
123
127
  [IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
124
128
  }
125
129
  catch {
126
- $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
130
+ $entry.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
127
131
  }
128
132
  }
129
133
 
@@ -136,12 +140,53 @@ function Add-EtherscanToUserPath {
136
140
  }
137
141
  [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
138
142
  Write-Host "Added $fullDirectory to your user PATH."
143
+ $pathAdded = $true
144
+ }
145
+ else {
146
+ $pathAdded = $false
139
147
  }
140
148
 
141
149
  $processEntries = @($env:Path -split ';')
142
150
  if (-not ($processEntries | Where-Object { $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) })) {
143
151
  $env:Path = "$env:Path;$fullDirectory"
144
152
  }
153
+ return $pathAdded
154
+ }
155
+
156
+ function Remove-EtherscanFromUserPath {
157
+ param([string]$Directory)
158
+
159
+ $fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
160
+ $entries = @([Environment]::GetEnvironmentVariable("Path", "User") -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
161
+ $kept = @($entries | Where-Object {
162
+ $entry = $_
163
+ try {
164
+ $expandedEntry = [Environment]::ExpandEnvironmentVariables($entry)
165
+ -not [IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
166
+ }
167
+ catch {
168
+ -not $entry.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
169
+ }
170
+ })
171
+ if ($kept.Count -ne $entries.Count) {
172
+ [Environment]::SetEnvironmentVariable("Path", ($kept -join ';'), "User")
173
+ Write-Host "Removed $fullDirectory from your user PATH."
174
+ }
175
+ }
176
+
177
+ function Test-EtherscanInstallMarker {
178
+ param([string]$Path)
179
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
180
+ $item = Get-Item -LiteralPath $Path -Force
181
+ if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $false }
182
+ return (Get-Content -LiteralPath $Path -Raw) -eq $InstallMarkerContent
183
+ }
184
+
185
+ function Get-EtherscanConfigDirectory {
186
+ if (-not [string]::IsNullOrWhiteSpace($env:XDG_CONFIG_HOME)) {
187
+ return Join-Path $env:XDG_CONFIG_HOME "etherscan"
188
+ }
189
+ return Join-Path $env:USERPROFILE ".etherscan"
145
190
  }
146
191
 
147
192
  if ($env:OS -ne "Windows_NT") {
@@ -162,6 +207,58 @@ if ($WaitForProcessId -gt 0) {
162
207
  Wait-Process -Id $WaitForProcessId -ErrorAction SilentlyContinue
163
208
  }
164
209
 
210
+ if ($Uninstall) {
211
+ try {
212
+ $targetExecutable = Join-Path $InstallDir "etherscan.exe"
213
+ $marker = Join-Path $InstallDir $InstallMarkerName
214
+ $validMarker = Test-EtherscanInstallMarker -Path $marker
215
+ $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
216
+ $defaultInstallDir = Join-Path $localAppData "Programs\Etherscan\bin"
217
+ $legacyDefault = [IO.Path]::GetFullPath($InstallDir).TrimEnd('\').Equals(
218
+ [IO.Path]::GetFullPath($defaultInstallDir).TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
219
+ $removed = $false
220
+
221
+ if (Test-Path -LiteralPath $targetExecutable) {
222
+ Remove-Item -LiteralPath $targetExecutable -Force
223
+ Write-Host "Removed $targetExecutable"
224
+ $removed = $true
225
+ }
226
+
227
+ $otherEntries = @()
228
+ if (Test-Path -LiteralPath $InstallDir -PathType Container) {
229
+ $otherEntries = @(Get-ChildItem -LiteralPath $InstallDir -Force | Where-Object {
230
+ -not ($validMarker -and $_.FullName -eq $marker)
231
+ } | Select-Object -First 1)
232
+ }
233
+ if (-not $NoPathUpdate -and ($validMarker -or $legacyDefault) -and $otherEntries.Count -eq 0) {
234
+ Remove-EtherscanFromUserPath -Directory $InstallDir
235
+ if ($validMarker) { Remove-Item -LiteralPath $marker -Force }
236
+ Remove-Item -LiteralPath $InstallDir -Force -ErrorAction SilentlyContinue
237
+ $removed = $true
238
+ }
239
+ elseif (-not $NoPathUpdate -and (Test-Path -LiteralPath $InstallDir)) {
240
+ Write-Host "Left $InstallDir on PATH because ownership was not proven or the directory is shared."
241
+ }
242
+
243
+ $configDirectory = Get-EtherscanConfigDirectory
244
+ if (Test-Path -LiteralPath $configDirectory) {
245
+ Remove-Item -LiteralPath $configDirectory -Recurse -Force
246
+ Write-Host "Removed $configDirectory"
247
+ $removed = $true
248
+ }
249
+ if ($removed) { Write-Host "Etherscan CLI uninstalled." } else { Write-Host "Nothing to remove." }
250
+ if (-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_API_KEY)) {
251
+ Write-Warning "ETHERSCAN_API_KEY remains set; unset it in your shell."
252
+ }
253
+ }
254
+ finally {
255
+ if ($CleanupScript -and -not [string]::IsNullOrWhiteSpace($PSCommandPath)) {
256
+ Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
257
+ }
258
+ }
259
+ return
260
+ }
261
+
165
262
  $resolved = Resolve-EtherscanVersion -RequestedVersion $Version
166
263
  $architecture = Get-EtherscanArchitecture
167
264
  $archiveName = "etherscan_$($resolved.Version)_windows_$architecture.zip"
@@ -240,7 +337,10 @@ try {
240
337
  }
241
338
 
242
339
  if (-not $NoPathUpdate) {
243
- Add-EtherscanToUserPath -Directory $InstallDir
340
+ $pathAdded = Add-EtherscanToUserPath -Directory $InstallDir
341
+ if ($pathAdded) {
342
+ Set-Content -LiteralPath (Join-Path $InstallDir $InstallMarkerName) -Value $InstallMarkerContent -NoNewline
343
+ }
244
344
  }
245
345
 
246
346
  Write-Host ""
@@ -7,6 +7,9 @@ version=${ETHERSCAN_VERSION:-}
7
7
  install_dir=${ETHERSCAN_INSTALL_DIR:-"$HOME/.local/bin"}
8
8
  download_base=${ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL:-}
9
9
  update_path=1
10
+ uninstall=0
11
+ marker_name=.etherscan-cli-path-added
12
+ marker_content=etherscan-cli:path-added:v1
10
13
 
11
14
  usage() {
12
15
  cat <<'EOF'
@@ -18,6 +21,7 @@ Options:
18
21
  --version VERSION Install a specific version (for example, v1.1.0).
19
22
  --install-dir DIRECTORY Install into DIRECTORY (default: ~/.local/bin).
20
23
  --no-path-update Do not update the shell profile.
24
+ --uninstall Remove the CLI and saved configuration.
21
25
  -h, --help Show this help.
22
26
  EOF
23
27
  }
@@ -43,6 +47,10 @@ while [ "$#" -gt 0 ]; do
43
47
  update_path=0
44
48
  shift
45
49
  ;;
50
+ --uninstall)
51
+ uninstall=1
52
+ shift
53
+ ;;
46
54
  -h|--help)
47
55
  usage
48
56
  exit 0
@@ -57,6 +65,126 @@ if printf '%s' "$install_dir" | LC_ALL=C grep '[[:cntrl:]]' >/dev/null 2>&1; the
57
65
  die "the installation directory cannot contain control characters"
58
66
  fi
59
67
 
68
+ config_dir() {
69
+ if [ -n "${XDG_CONFIG_HOME:-}" ]; then
70
+ printf '%s\n' "$XDG_CONFIG_HOME/etherscan"
71
+ else
72
+ printf '%s\n' "$HOME/.etherscan"
73
+ fi
74
+ }
75
+
76
+ profile_has_block() {
77
+ profile=$1
78
+ target=$2
79
+ [ -f "$profile" ] && [ ! -L "$profile" ] || return 1
80
+ ETHERSCAN_PROFILE_TARGET=$target awk '
81
+ BEGIN { target = ENVIRON["ETHERSCAN_PROFILE_TARGET"] }
82
+ previous == "# Etherscan CLI" && $0 == target { found = 1; exit }
83
+ { previous = $0 }
84
+ END { exit found ? 0 : 1 }
85
+ ' "$profile"
86
+ }
87
+
88
+ remove_profile_block() {
89
+ profile=$1
90
+ target=$2
91
+ profile_has_block "$profile" "$target" || return 0
92
+ tmp=$(mktemp "${profile}.etherscan-uninstall.XXXXXX") || die "could not create a profile temporary file"
93
+ if ! cp -p "$profile" "$tmp"; then
94
+ rm -f "$tmp"
95
+ die "could not preserve permissions for $profile"
96
+ fi
97
+ if ! ETHERSCAN_PROFILE_TARGET=$target awk '
98
+ BEGIN { target = ENVIRON["ETHERSCAN_PROFILE_TARGET"] }
99
+ {
100
+ if (pending) {
101
+ if ($0 == target) { pending = 0; next }
102
+ print "# Etherscan CLI"
103
+ pending = 0
104
+ }
105
+ if ($0 == "# Etherscan CLI") { pending = 1; next }
106
+ print
107
+ }
108
+ END { if (pending) print "# Etherscan CLI" }
109
+ ' "$profile" >"$tmp"; then
110
+ rm -f "$tmp"
111
+ die "could not update $profile"
112
+ fi
113
+ mv -f "$tmp" "$profile"
114
+ printf 'Removed the PATH entry from %s\n' "$profile"
115
+ }
116
+
117
+ directory_empty_except_marker() {
118
+ directory=$1
119
+ marker=$2
120
+ [ -d "$directory" ] || return 0
121
+ for entry in "$directory"/.[!.]* "$directory"/..?* "$directory"/*; do
122
+ [ -e "$entry" ] || [ -L "$entry" ] || continue
123
+ [ "$entry" = "$marker" ] && continue
124
+ return 1
125
+ done
126
+ return 0
127
+ }
128
+
129
+ if [ "$uninstall" -eq 1 ]; then
130
+ binary="$install_dir/etherscan"
131
+ marker="$install_dir/$marker_name"
132
+ marker_valid=0
133
+ if [ -f "$marker" ] && [ ! -L "$marker" ] && [ "$(cat "$marker")" = "$marker_content" ]; then
134
+ marker_valid=1
135
+ fi
136
+
137
+ escaped_install_dir=$(printf '%s' "$install_dir" | sed 's/[\\"$`]/\\&/g')
138
+ posix_path_line="export PATH=\"$escaped_install_dir:\$PATH\""
139
+ fish_path_line="fish_add_path \"$escaped_install_dir\""
140
+ legacy_provenance=0
141
+ for candidate in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do
142
+ if profile_has_block "$candidate" "$posix_path_line"; then
143
+ legacy_provenance=1
144
+ fi
145
+ done
146
+ if profile_has_block "$HOME/.config/fish/config.fish" "$fish_path_line"; then
147
+ legacy_provenance=1
148
+ fi
149
+
150
+ removed=0
151
+ if [ -e "$binary" ] || [ -L "$binary" ]; then
152
+ rm -f "$binary"
153
+ printf 'Removed %s\n' "$binary"
154
+ removed=1
155
+ fi
156
+
157
+ if [ "$update_path" -eq 1 ] && { [ "$marker_valid" -eq 1 ] || [ "$legacy_provenance" -eq 1 ]; } && directory_empty_except_marker "$install_dir" "$marker"; then
158
+ for candidate in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do
159
+ remove_profile_block "$candidate" "$posix_path_line"
160
+ done
161
+ remove_profile_block "$HOME/.config/fish/config.fish" "$fish_path_line"
162
+ if [ "$marker_valid" -eq 1 ]; then
163
+ rm -f "$marker"
164
+ fi
165
+ rmdir "$install_dir" 2>/dev/null || true
166
+ removed=1
167
+ elif [ "$update_path" -eq 1 ] && [ -d "$install_dir" ]; then
168
+ printf 'Left %s on PATH because ownership was not proven or the directory is shared.\n' "$install_dir"
169
+ fi
170
+
171
+ etherscan_config=$(config_dir)
172
+ if [ -e "$etherscan_config" ] || [ -L "$etherscan_config" ]; then
173
+ rm -rf "$etherscan_config"
174
+ printf 'Removed %s\n' "$etherscan_config"
175
+ removed=1
176
+ fi
177
+ if [ "$removed" -eq 1 ]; then
178
+ printf 'Etherscan CLI uninstalled.\n'
179
+ else
180
+ printf 'Nothing to remove.\n'
181
+ fi
182
+ if [ -n "${ETHERSCAN_API_KEY:-}" ]; then
183
+ printf 'note: ETHERSCAN_API_KEY remains set; unset it in your shell.\n' >&2
184
+ fi
185
+ exit 0
186
+ fi
187
+
60
188
  fetch_stdout() {
61
189
  url=$1
62
190
  if command -v curl >/dev/null 2>&1; then
@@ -195,6 +323,7 @@ if [ "$update_path" -eq 1 ]; then
195
323
  printf '%s\n' "$path_line"
196
324
  } >>"$profile"
197
325
  path_updated=1
326
+ printf '%s' "$marker_content" >"$install_dir/$marker_name"
198
327
  fi
199
328
  ;;
200
329
  esac