CLAUDE.md@helius-cli · git:20261103.ae8c094 · 2026-11-03 · sha256 339f8bd416b071bf

CLAUDE.md@helius-cli git:20261103.ae8c094A

Immutable. This exact content never changes and is served at /api/v1/blob/339f8bd416b071bf.

# helius-cli

CLI tool for managing Helius accounts and querying Solana blockchain data via the Helius platform.

## Directory Structure

```
bin/helius.ts          # Entry point — Commander.js program, registers all commands
src/
  commands/            # One file per command group
    balance.ts         # balance, tokens, token-holders
    tx.ts              # tx parse, tx history, tx transfers, tx scan, tx fees
    asset.ts           # 12 DAS API subcommands
    account.ts         # account info
    network-status.ts  # network status
    block.ts           # block by slot
    wallet.ts          # Wallet API (REST, not SDK) — identity, balances, history, transfers, funded-by
    webhook.ts         # webhook CRUD
    program.ts         # program accounts
    stake.ts           # staking commands
    zk.ts              # 24 ZK compression subcommands
    send.ts            # tx helpers (broadcast, raw, sender, poll, compute-units)
    reclaim.ts         # close empty SPL token accounts, reclaim rent (via Sender)
    ws.ts              # WebSocket streaming (account, logs, slot, signature, program)
    config-cmd.ts      # config show/set-api-key/set-network/set-project/clear
    signup.ts          # account signup (existing)
    login.ts           # wallet auth (existing)
    projects.ts        # list projects (existing)
    plans.ts           # list available plans and pricing
    status.ts          # account status: plan, credits, billing cycle
    ...
  lib/
    helius.ts          # SDK client wrapper — resolveApiKey(), getClient(), restRequest(), HeliusHttpError
    formatters.ts      # formatSol(), formatAddress(), formatTimestamp(), formatTable()
    config.ts          # ~/.helius/config.json — jwt, apiKey, network, projectId
    output.ts          # ExitCode enum, classifyError(), handleCommandError(), outputJson(), exitWithError()
    api.ts             # Helius management API (signup, projects, API keys)
    payment.ts         # USDC payment for signup
    wallet.ts          # Keypair loading
  constants.ts         # VERSION, API_URL, treasury, USDC mints
```

## Command Pattern

All data commands follow this pattern:
1. Create spinner (unless `--json`)
2. Resolve API key via `resolveApiKey(options)` — flag > env > config > JWT auto-resolve
3. Get SDK client via `getClient(apiKey, network)`
4. Call SDK method
5. Output: formatted terminal (chalk) or `outputJson(payload)` for `--json`. As of v1.3, `outputJson()` auto-wraps its argument in `{ ok: true, v: 1, data: <payload> }` — callers still pass the raw payload.
6. Error: use `handleCommandError(error, options, spinner)` — classifies the error, emits the error envelope (`{ ok: false, v: 1, error_code, category, error, recoverable, suggestion, details? }`) or spinner output, and exits with the classified exit code

Standard catch block template (all commands except `ws.ts`):
```ts
} catch (error) {
  handleCommandError(error, options, spinner);
}
```

`handleCommandError()` in `src/lib/output.ts` calls `classifyError()` internally, formats JSON or spinner output (with retryable hint), and exits with the classified exit code. All error output logic lives in this single function — do not duplicate it inline. Payment-specific errors (Insufficient SOL/USDC, Checkout failed/expired/timeout) are classified via keyword matching in `classifyError()`.

WebSocket commands (`ws.ts`) use a variant that preserves the AbortError early-return and uses `console.error` instead of spinner.

## API Key Resolution Chain

1. `--api-key <key>` CLI flag
2. `HELIUS_API_KEY` environment variable
3. `~/.helius/config.json` → `apiKey` field
4. Auto-resolve from JWT: fetch project details → first API key

## Exit Code Ranges

- 0: success
- 1: general error
- 10-19: auth errors (NOT_LOGGED_IN=10, KEYPAIR_NOT_FOUND=11, AUTH_FAILED=12)
- 20-29: balance/payment errors (INSUFFICIENT_SOL=20, INSUFFICIENT_USDC=21, PAYMENT_FAILED=22)
- 30-39: project errors (NO_PROJECTS=30, PROJECT_NOT_FOUND=31, MULTIPLE_PROJECTS=32, PROJECT_EXISTS=33)
- 40-49: API errors (API_ERROR=40, NO_API_KEYS=41)
- 50-59: SDK/data errors:
  - 50 `NO_API_KEY` — resolveApiKey() found nothing; permanent, fix config
  - 51 `SDK_ERROR` — unclassified SDK error
  - 52 `INVALID_ADDRESS` — bad base58/pubkey format; permanent
  - 53 `INVALID_INPUT` — bad parameters or HTTP 400; permanent
  - 54 `INVALID_API_KEY` — HTTP 401/403 or "invalid api key"; permanent, fix the key
  - 55 `NOT_FOUND` — HTTP 404 or resource not found; permanent
  - 56 `RATE_LIMITED` — HTTP 429 or rate limit message; **transient, safe to retry**
  - 57 `SERVER_ERROR` — HTTP 5xx or server error message; **transient, safe to retry**
  - 58 `NETWORK_ERROR` — ECONNREFUSED/ETIMEDOUT/fetch failed; **transient, safe to retry**

The `recoverable` field in `--json` error envelopes reflects this directly (renamed from pre-1.3 `retryable`).

New in v1.3: exit code 23 `INSUFFICIENT_FUNDS` — signup/upgrade paths where both SOL and USDC may be short; emit `details.missing` listing the shortfalls.

## Output Envelope (v1.3)

All `--json` output goes through one of two shapes, defined in `src/lib/output.ts`:

- **Success:** `{ ok: true, v: 1, data: <payload> }` — `outputJson(payload)` wraps automatically.
- **Error:** `{ ok: false, v: 1, error_code, category, error, recoverable, suggestion, details? }` — built by `buildErrorEnvelope()` and written by `handleCommandError()` / `exitWithError()`. `suggestion` is always present (falls back to a generic "re-run with --debug" hint if the error code has no `CLI_GUIDANCE` entry).

Exported types: `Envelope<T>`, `SuccessEnvelope<T>`, `ErrorEnvelope`, `Category`.

When adding a new error code:
1. Add it to `ExitCode` (pick the right range — 10-19 auth, 20-29 payment, etc.).
2. Add it to `errorToExitCode`.
3. Add it to `exitCodeToCategory` (pick the public `Category`).
4. Add a human-readable hint to `CLI_GUIDANCE` (required — `suggestion` is always present in JSON output; missing entries fall back to a generic "re-run with --debug" hint, which is agent-useless).

`getExitCode()` logs a `debug()` warning when handed an unknown code. Under `--debug`, run your command once; if you see the warning, the new code isn't wired up yet.

Streaming commands (`ws.ts`) do **not** use the envelope — they emit line-delimited `{ event, timestamp, data }` via direct `console.log`. See the comment at `ws.ts:10-13`.

## Error Classification

`classifyError(error)` in `src/lib/output.ts` returns `{ exitCode, errorCode, retryable }` using three tiers:

1. **`HeliusHttpError`** (from `restRequest()` — wallet.ts REST path): exact `status` property → precise code
2. **Status in message** (enhanced TX: `"Helius HTTP 429: ..."`, webhooks: `"HTTP error! status: 429 - ..."`): regex extracts status → same mapping
3. **Keyword matching** (RPC caller path — DAS, ZK, staking, raw, payment): matches phrases like `"insufficient sol"`, `"insufficient usdc"`, `"checkout failed"`, `"invalid api key"`, `"rate limit"`, `"not found"`, `"ECONNREFUSED"`, `SyntaxError`, etc.

`restRequest()` throws `HeliusHttpError` (defined in `src/lib/helius.ts`) so the REST path always hits Tier 1. The SDK paths hit Tier 2 or 3 depending on the client.

`exitWithError()` produces the same error envelope as `handleCommandError()` — both route through `buildErrorEnvelope()`. Caller-supplied `details` are nested under the envelope's `details` key so they can't collide with reserved fields (`ok`, `v`, `error`, `error_code`, `category`, `recoverable`, `suggestion`).

## SDK vs REST

- **SDK (`helius-sdk`)**: DAS API, enhanced transactions, webhooks, staking, ZK compression, WebSockets, program accounts, priority fees
- **REST (`restRequest()`)**: Wallet API (`/v1/wallet/...`) — identity, balances, history, transfers, funded-by

## Build / Dev / Test

```bash
pnpm install
pnpm build          # tsc → dist/
pnpm dev            # tsx bin/helius.ts (runs directly)
node dist/bin/helius.js --help
```

## Common Pitfalls

- **`exitWithError()` json parameter**: The 4th param defaults to `true`. Always pass `!!options.json` — bare `options.json` is `undefined` when the flag is absent, which falls through to the `true` default and outputs JSON in terminal mode.
- **Network display**: When displaying the resolved network in output, always use `resolveNetwork(options)` — never `options.network || "mainnet"`. The Commander `--network` flag has a default, but `resolveNetwork()` also checks `HELIUS_NETWORK` env var and config file.
- **Removing local variables during refactors**: When extracting inline code into a shared helper (e.g., replacing `const network = resolveNetwork(options)` with `setupClient()`), grep the rest of the function for references to the removed variable before deleting it.
- **Type annotations on generic wrappers**: When wrapping SDK calls with generic utilities like `withRetry<T>()`, let TypeScript infer the return type. Only add `as any` at call sites where the SDK returns `unknown` through the generic — not as `const result: any` on every call.

## Adding a New Command

1. Create `src/commands/mycommand.ts` exporting async handler(s)
2. Use `setupClient(spinner, options, "Loading message...")` from `src/lib/helius.js` for the standard resolve → client → spinner pattern
3. Support `--json` via `outputJson()` / formatted chalk output
4. Use `handleCommandError(error, options, spinner)` in the catch block — import from `../lib/output.js`
5. Register in `bin/helius.ts` with Commander.js
6. Run `pnpm build` and verify