Skip to content

feat: add ens forwardresolve; fix --all JSON types, config save, terminal escapes, Windows emoji input - #20

Open
samuel-jw wants to merge 3 commits into
masterfrom
feat/ens-forwardresolve
Open

samuel-jw wants to merge 3 commits into
masterfrom
feat/ens-forwardresolve

Conversation

@samuel-jw

@samuel-jw samuel-jw commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Adds the ens forwardresolve command and bundles five independent fixes found while working on it. Each section below says what changed, why, and which tests cover it. Most of the changes live in separate files (config/, tui/, output/), so each one can be reviewed or reverted on its own.

# Change Main files
1 New ens forwardresolve command cli/registry.go, README.md
2 --all keeps raw JSON types and honours --compact cli/root.go
3 Pretty JSON keeps numbers exact output/output.go
4 Atomic config save with 0600 permissions and symlink resolution config/config.go
5 Terminal-escape sanitising of API text output/output.go, cmd/etherscan/main.go, tui/tui.go
6 Windows: UTF-16 surrogate-pair joining in the TUI tui/tui.go

1. ens forwardresolve

  • etherscan ens forwardresolve <name> resolves an ENS name to its address. The command is marked MainnetOnly, because the API serves ENS on Ethereum mainnet only.
  • Adds the registry entry, the ens command group, the TUI sidebar entry and a README row.
  • Test: TestEnsForwardResolve

2. --all keeps raw JSON types and honours --compact

  • Why: runAllPages turned every page into display rows (map[string]string) before combining them. As a result, --all -o json printed numbers, booleans and nulls as strings, and arrays such as getLogs topics as JSON-encoded strings. A single-page call printed them correctly. --all also ignored --compact.
  • Now: rows stay json.RawMessage until output.Write, which converts them only for table and CSV output. --compact is passed through.
  • ⚠️ Behaviour change: for --all JSON output, non-string fields change type, so scripts that parsed the stringified form need updating.
  • Test: TestAllPagesPreservesJSONTypes

3. Pretty JSON keeps numbers exact

  • Why: output.Write decoded into float64, which rounded integers above 2^53 and rewrote decimals such as 1.50 as 1.5.
  • Now: it decodes with UseNumber, so every number prints exactly as the API sent it. --compact was never affected.
  • Test: TestWriteJSONKeepsExactNumbers

4. Config save: atomic, 0600, symlink-aware

  • Why: the config file holds the API key, and the old save had three problems:
    • The 0600 mode on os.OpenFile only applies when the file is created, so an existing world-readable config stayed world-readable.
    • O_TRUNC meant a failed write left a truncated or empty config.
    • Saving through a symlink was not considered.
  • Now: Save writes to a temp file in the same directory, then sets it to 0600, syncs it to disk and renames it into place. A failed save leaves the old config intact.
  • A plain rename would replace a symlink with a regular file, so a symlinked config (for example from a dotfiles repo) is resolved first and updated at its target. This also covers dangling and relative links. Paths that are not symlinks skip the resolution step.
  • Tests:
    • TestSaveTightensExistingPermissions
    • TestFailedSaveCleansUpTempFile
    • TestSaveWritesThroughSymlink
    • TestSaveWritesThroughDanglingSymlink
    • TestSaveWritesThroughDanglingRelativeSymlink
    • TestSaveResolvesRelativeLinksPhysically
    • TestSaveRoundTrips

5. Terminal-escape sanitising (output.Sanitize)

  • Why: token names and symbols are chosen by whoever deploys the contract. A raw ESC in one can smuggle in a terminal escape sequence, for example OSC 52, which many terminals honour as "overwrite the clipboard". A raw \r can return the cursor to column 0 and overwrite the address column in a table.
  • Now: Sanitize replaces control characters and invalid UTF-8 with U+FFFD in every text output: table, CSV, single values, and error messages in both the CLI and the TUI.
  • Tabs, newlines and other whitespace characters are kept, so contract source survives in CSV and single-value output. Table cells and headers turn them into a single space, including when they appear alone or at either edge of a cell (review follow-ups).
  • JSON output is unchanged, because the encoder already escapes every control character.
  • Tests:
    • TestTextOutputStripsTerminalEscapes
    • TestJSONOutputKeepsEscapedControls
    • TestTableCollapsesCursorControls
    • TestSanitizeKeepsWhitespaceAndText
    • TestCSVKeepsCRLFSource

6. Windows: emoji input in the TUI

  • Why: on Windows, Bubble Tea v1 delivers each UTF-16 code unit as its own key event. An emoji or any other character outside the Basic Multilingual Plane typed into a TUI text field became two U+FFFD characters.
  • Now: joinSurrogates recombines the two halves before key handling. This mirrors the fix in Bubble Tea v2, so it can be removed when we migrate.
  • Tests: TestFormJoinsWindowsSurrogatePairs, TestChainPickerFilterJoinsWindowsSurrogatePairs

Release notes

  • New: etherscan ens forwardresolve <name> (Ethereum mainnet).
  • Changed: --all JSON output keeps native JSON types instead of stringifying them, and --all now honours --compact.
  • Fixed: pretty JSON keeps large integers and decimals exact.
  • Fixed: saving the config is atomic, always tightens permissions to 0600, and preserves symlinked configs.
  • Fixed: control characters in API text can no longer inject terminal escape sequences or spoof table columns.
  • Fixed: emoji input in TUI text fields on Windows.

Testing

go vet ./... and go test ./... pass.

@samuel-jw
samuel-jw requested a review from homeyong October 7, 2026 02:55
@samuel-jw samuel-jw self-assigned this Oct 7, 2026
@samuel-jw samuel-jw added the enhancement New feature or request label Oct 7, 2026
Comment thread internal/output/output.go Outdated
}

func formatTableCell(column, value string) string {
value = Sanitize(value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuel-jw Sanitize keeps \t \n \v \f \r everywhere, table cells included. \r in a table cell is still a display-spoofing vector: a token name like "USDC\rFAKE…" sends the cursor back to column 0 and the following text overwrites the address column, which is the case this function is meant to stop.

Keeping CR/LF makes sense for scalar output (e.g. contract source), but table cells should also replace \r, \v and \f (and probably \n, since tablewriter splits on it). Could you take a look?

@samuel-jw samuel-jw Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feedback updated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuel-jw thanks, collapseWhitespace covers most of it, but a cell that is only whitespace still gets through. formatTableCell returns early before the collapse runs:

value = Sanitize(value)
trimmed := strings.TrimSpace(value)
if trimmed == "" {
    return value   // still holds the raw "\r"
}

Repro: [{"to":"0xabc","tokenName":"\r","tokenSymbol":"USDC"}] renders as "0xabc \r USDC", so the next column overwrites the address again. The TUI goes through the same writeTable path.

Fix: return trimmed (or "") in that branch, and add a whitespace-only case like "\r" to TestTableCollapsesCursorControls.

@samuel-jw
samuel-jw requested a review from homeyong October 19, 2026 08:30
@homeyong

Copy link
Copy Markdown
Collaborator

@samuel-jw one note on the PR overall: the title says "ENS endpoint + some fixes" and the description is empty, but it bundles five independent changes:

  1. ens forwardresolve endpoint
  2. --all keeping raw JSON types (+ honouring --compact)
  3. Atomic config save with 0600 permissions and symlink resolution
  4. Terminal escape sanitising of API text (output.Sanitize)
  5. Windows UTF-16 surrogate-pair joining in the TUI

The ENS part is ~10 lines; the rest is ~790. Could you split these into separate PRs, or at least add a description listing each change and why? It would make review and git log / revert much easier, especially for the config-save and sanitising changes.

@samuel-jw

samuel-jw commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

okay will add a more detail description, will separate them out as different PR in the future enh

@samuel-jw samuel-jw changed the title feat: add ENS endpoint + some fixes feat: add ens forwardresolve; fix --all JSON types, config save, terminal escapes, Windows emoji input Oct 19, 2026
@samuel-jw

Copy link
Copy Markdown
Collaborator Author

latest feedback updated, with a slightly different approach that also covers "\r" at the start or end of a cell (e.g. "\rUSDC"), which returning trimmed alone would miss.

updated title and description as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants