@etherscan-npm/cli 1.0.1 → 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.
@@ -1,262 +1,362 @@
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
+ [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
+ [switch]$Uninstall
9
+ )
10
+
11
+ $ErrorActionPreference = "Stop"
12
+ $ProgressPreference = "SilentlyContinue"
13
+
14
+ $Repository = "etherscan/etherscan-cli"
15
+ $DownloadBaseUrl = $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL
16
+ $InstallMarkerName = ".etherscan-cli-path-added"
17
+ $InstallMarkerContent = "etherscan-cli:path-added:v1"
18
+
19
+ function Get-EtherscanArchitecture {
20
+ $architecture = $env:PROCESSOR_ARCHITEW6432
21
+ if ([string]::IsNullOrWhiteSpace($architecture)) {
22
+ $architecture = $env:PROCESSOR_ARCHITECTURE
23
+ }
24
+
25
+ switch -Regex ($architecture) {
26
+ "^(AMD64|x86_64)$" { return "amd64" }
27
+ "^(ARM64|aarch64)$" { return "arm64" }
28
+ default { throw "Unsupported Windows architecture: $architecture. Etherscan CLI supports amd64 and arm64." }
29
+ }
30
+ }
31
+
32
+ function Get-GitHubApiHeaders {
33
+ $headers = @{
34
+ Accept = "application/vnd.github+json"
35
+ "User-Agent" = "etherscan-cli-installer"
36
+ "X-GitHub-Api-Version" = "2022-11-28"
37
+ }
38
+ if (-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) {
39
+ $headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)"
40
+ }
41
+ return $headers
42
+ }
43
+
44
+ function Resolve-EtherscanVersion {
45
+ param([string]$RequestedVersion)
46
+
47
+ if (-not [string]::IsNullOrWhiteSpace($RequestedVersion) -and $RequestedVersion -ne "latest") {
48
+ $tag = if ($RequestedVersion.StartsWith("v")) { $RequestedVersion } else { "v$RequestedVersion" }
49
+ }
50
+ else {
51
+ if (-not [string]::IsNullOrWhiteSpace($DownloadBaseUrl)) {
52
+ throw "A version is required when the installer test download source is used."
53
+ }
54
+ $release = Invoke-RestMethod `
55
+ -Uri "https://api.github.com/repos/$Repository/releases/latest" `
56
+ -Headers (Get-GitHubApiHeaders)
57
+ $tag = [string]$release.tag_name
58
+ }
59
+
60
+ if ($tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$') {
61
+ throw "Invalid release version: $tag"
62
+ }
63
+
64
+ return @{
65
+ Tag = $tag
66
+ Version = $tag.Substring(1)
67
+ }
68
+ }
69
+
70
+ function Copy-InstallerFile {
71
+ param(
72
+ [string]$Base,
73
+ [string]$Name,
74
+ [string]$Destination
75
+ )
76
+
77
+ if (Test-Path -LiteralPath $Base -PathType Container) {
78
+ Copy-Item -LiteralPath (Join-Path $Base $Name) -Destination $Destination
79
+ return
80
+ }
81
+
82
+ $uri = "$($Base.TrimEnd('/'))/$Name"
83
+ $parsedUri = [Uri]$uri
84
+ if ($parsedUri.Scheme -ne "https") {
85
+ throw "Remote downloads must use HTTPS: $uri"
86
+ }
87
+
88
+ $headers = @{
89
+ "User-Agent" = "etherscan-cli-installer"
90
+ }
91
+ if ($parsedUri.Host -in @("github.com", "api.github.com") -and
92
+ -not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) {
93
+ $headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)"
94
+ }
95
+
96
+ Invoke-WebRequest -Uri $uri -OutFile $Destination -Headers $headers -UseBasicParsing
97
+ }
98
+
99
+ function Get-SHA256FileHash {
100
+ param([string]$Path)
101
+
102
+ $sha256 = [Security.Cryptography.SHA256]::Create()
103
+ try {
104
+ $stream = [IO.File]::OpenRead($Path)
105
+ try {
106
+ return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace("-", "").ToLowerInvariant()
107
+ }
108
+ finally {
109
+ $stream.Dispose()
110
+ }
111
+ }
112
+ finally {
113
+ $sha256.Dispose()
114
+ }
115
+ }
116
+
117
+ function Add-EtherscanToUserPath {
118
+ param([string]$Directory)
119
+
120
+ $fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
121
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
122
+ $entries = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
123
+ $alreadyPresent = $entries | Where-Object {
124
+ $entry = $_
125
+ try {
126
+ $expandedEntry = [Environment]::ExpandEnvironmentVariables($entry)
127
+ [IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
128
+ }
129
+ catch {
130
+ $entry.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
131
+ }
132
+ }
133
+
134
+ if (-not $alreadyPresent) {
135
+ $newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) {
136
+ $fullDirectory
137
+ }
138
+ else {
139
+ "$($userPath.TrimEnd(';'));$fullDirectory"
140
+ }
141
+ [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
142
+ Write-Host "Added $fullDirectory to your user PATH."
143
+ $pathAdded = $true
144
+ }
145
+ else {
146
+ $pathAdded = $false
147
+ }
148
+
149
+ $processEntries = @($env:Path -split ';')
150
+ if (-not ($processEntries | Where-Object { $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) })) {
151
+ $env:Path = "$env:Path;$fullDirectory"
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"
190
+ }
191
+
192
+ if ($env:OS -ne "Windows_NT") {
193
+ throw "This installer supports Windows only. Use install.sh on macOS or Linux."
194
+ }
195
+
196
+ if ([string]::IsNullOrWhiteSpace($InstallDir)) {
197
+ $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
198
+ $InstallDir = Join-Path $localAppData "Programs\Etherscan\bin"
199
+ }
200
+ if ($InstallDir.Contains(';')) {
201
+ throw "The installation directory cannot contain a semicolon."
202
+ }
203
+ if ($InstallDir.IndexOfAny([char[]]"`r`n") -ge 0) {
204
+ throw "The installation directory cannot contain a line break."
205
+ }
206
+ if ($WaitForProcessId -gt 0) {
207
+ Wait-Process -Id $WaitForProcessId -ErrorAction SilentlyContinue
208
+ }
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
+
262
+ $resolved = Resolve-EtherscanVersion -RequestedVersion $Version
263
+ $architecture = Get-EtherscanArchitecture
264
+ $archiveName = "etherscan_$($resolved.Version)_windows_$architecture.zip"
265
+ $baseUrl = if ([string]::IsNullOrWhiteSpace($DownloadBaseUrl)) {
266
+ "https://github.com/$Repository/releases/download/$($resolved.Tag)"
267
+ }
268
+ else {
269
+ $DownloadBaseUrl
270
+ }
271
+
272
+ $tempDirectory = Join-Path ([IO.Path]::GetTempPath()) "etherscan-install-$PID-$([Guid]::NewGuid().ToString('N'))"
273
+ $archivePath = Join-Path $tempDirectory $archiveName
274
+ $checksumPath = Join-Path $tempDirectory "checksums.txt"
275
+ $sourceExecutable = Join-Path $tempDirectory "etherscan.exe"
276
+
277
+ try {
278
+ New-Item -ItemType Directory -Path $tempDirectory -Force | Out-Null
279
+
280
+ Write-Host "Downloading Etherscan CLI $($resolved.Version) for windows/$architecture..."
281
+ Copy-InstallerFile -Base $baseUrl -Name $archiveName -Destination $archivePath
282
+ Copy-InstallerFile -Base $baseUrl -Name "checksums.txt" -Destination $checksumPath
283
+
284
+ $pattern = '^([0-9A-Fa-f]{64})\s+\*?' + [Regex]::Escape($archiveName) + '$'
285
+ $checksumLine = Get-Content -LiteralPath $checksumPath | Where-Object { $_ -match $pattern } | Select-Object -First 1
286
+ if (-not $checksumLine -or $checksumLine -notmatch $pattern) {
287
+ throw "No checksum was published for $archiveName."
288
+ }
289
+
290
+ $expectedHash = $Matches[1].ToLowerInvariant()
291
+ $actualHash = Get-SHA256FileHash -Path $archivePath
292
+ if ($actualHash -ne $expectedHash) {
293
+ throw "Checksum verification failed for $archiveName. Expected $expectedHash, received $actualHash."
294
+ }
295
+
296
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
297
+ $zip = [IO.Compression.ZipFile]::OpenRead($archivePath)
298
+ try {
299
+ $executableEntries = @($zip.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq "etherscan.exe" })
300
+ if ($executableEntries.Count -ne 1) {
301
+ throw "$archiveName must contain exactly one root-level etherscan.exe."
302
+ }
303
+
304
+ $inputStream = $executableEntries[0].Open()
305
+ $outputStream = [IO.File]::Create($sourceExecutable)
306
+ try {
307
+ $inputStream.CopyTo($outputStream)
308
+ }
309
+ finally {
310
+ $outputStream.Dispose()
311
+ $inputStream.Dispose()
312
+ }
313
+ }
314
+ finally {
315
+ $zip.Dispose()
316
+ }
317
+
318
+ New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
319
+ $targetExecutable = Join-Path $InstallDir "etherscan.exe"
320
+ $stagedExecutable = Join-Path $InstallDir ".etherscan.exe.new-$PID"
321
+ $backupExecutable = Join-Path $InstallDir ".etherscan.exe.old-$PID"
322
+ Copy-Item -LiteralPath $sourceExecutable -Destination $stagedExecutable -Force
323
+
324
+ try {
325
+ if (Test-Path -LiteralPath $targetExecutable) {
326
+ Move-Item -LiteralPath $targetExecutable -Destination $backupExecutable -Force
327
+ }
328
+ Move-Item -LiteralPath $stagedExecutable -Destination $targetExecutable -Force
329
+ Remove-Item -LiteralPath $backupExecutable -Force -ErrorAction SilentlyContinue
330
+ }
331
+ catch {
332
+ Remove-Item -LiteralPath $stagedExecutable -Force -ErrorAction SilentlyContinue
333
+ if ((Test-Path -LiteralPath $backupExecutable) -and -not (Test-Path -LiteralPath $targetExecutable)) {
334
+ Move-Item -LiteralPath $backupExecutable -Destination $targetExecutable -Force
335
+ }
336
+ throw
337
+ }
338
+
339
+ if (-not $NoPathUpdate) {
340
+ $pathAdded = Add-EtherscanToUserPath -Directory $InstallDir
341
+ if ($pathAdded) {
342
+ Set-Content -LiteralPath (Join-Path $InstallDir $InstallMarkerName) -Value $InstallMarkerContent -NoNewline
343
+ }
344
+ }
345
+
346
+ Write-Host ""
347
+ Write-Host "Etherscan CLI $($resolved.Version) installed successfully."
348
+ Write-Host "Installed to: $targetExecutable"
349
+ if ($NoPathUpdate) {
350
+ Write-Host "Add $InstallDir to PATH to run etherscan from any directory."
351
+ }
352
+ else {
353
+ Write-Host "Run 'etherscan version' to verify the installation."
354
+ Write-Host "Open a new terminal if the command is not yet available."
355
+ }
356
+ }
357
+ finally {
358
+ Remove-Item -LiteralPath $tempDirectory -Recurse -Force -ErrorAction SilentlyContinue
359
+ if ($CleanupScript -and -not [string]::IsNullOrWhiteSpace($PSCommandPath)) {
360
+ Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
361
+ }
362
+ }