feat(errors): add create coded error class#1576
Conversation
🦋 Changeset detectedLatest commit: 573efbd The changes in this PR will be included in the next version bump. This PR includes changesets to release 47 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
First time reviewing a PR still a 2nd year student. Happy to be corrected if I've misread anything. |
There was a problem hiding this comment.
I think this is generally a good idea - I think our error infrastructure here is really good and it makes sense to make it easy for other projects in the ecosystem to do something similar.
I wonder though whether we should split the infrastructure here into a separate package, maybe @solana/errors-core based on some other package naming? Then @solana/errors is just Kit errors using these functions.
I think that'd make more space for a focused readme. It would also be useful to explain the rules that we have buried in codes.ts about not changing error codes etc if you're publishing a decode helper.
I think we should also include in that readme a minimal version of the @solana/errors decode helper (and an explanation of how to add that script to your package), because you really should be publishing one if you're using this infra. Encoded errors with context are quite useless without it.
BundleMonFiles updated (7)
Unchanged files (137)
Total files change +2.45KB +0.49% Final result: ✅ View report in BundleMon website ➡️ |
chore: cleanup
Restore the describe('in dev mode') wrapper around the migrated tests so the diff vs upstream is purely the rename to getHumanReadableErrorMessage plus removal of the production-mode block (whose coverage moved to error-test.ts and coded-error-test.ts). No test bodies changed.
Per review on PR solana-foundation#10 (non-blocking): flag that IdlDecodeError should build on anza-xyz/kit#1576's createCodedErrorClass once that ships.
Per review on PR solana-foundation#10 (non-blocking): flag that IdlDecodeError should build on anza-xyz/kit#1576's createCodedErrorClass once that ships.
* feat(idl): add reusable error handling (RPC classifier + typed decode errors) Move the IDL fetch error-handling that consumers (e.g. the Explorer) were reimplementing into the package, so the failure taxonomy lives next to the code that produces it. New, additive public surface (no behavior change to existing functions): - `classifyRpcError` / `isTransientRpcError`: classify a thrown RPC error as `transient` (retry / surface as 5xx) vs `misconfig`, returning `null` for anything that isn't a `SolanaError`. Ports the logic consumers had to hand-roll against `@solana/kit` error-code internals. - `IdlDecodeError`: thrown when an account exists but its bytes aren't a usable IDL, with `reason: 'bytes' | 'json' | 'shape'`. - `resolveAnchorIdl`: strict sibling of `fetchAnchorIdl` that returns a parsed, shape-validated IDL. Distinguishes the three outcomes the old `null` collapsed: unpublished (`null`), present-but-corrupt (`IdlDecodeError`), and RPC failure (propagated `SolanaError`). This also surfaces zlib-corrupt on-chain accounts that `fetchAnchorIdl` silently swallowed into `null`. All new code is pure JS over `@solana/kit` (no new `node:*`), keeping the public surface isomorphic. README export table updated. * fix(idl): preserve zlib cause + split decode reason; test misconfig fallback Address Greptile review on PR #10: - `resolveAnchorIdl` now decodes via a discriminated `tryDecodeAnchorIdlAccountBytes` helper instead of the `string | null` flattening, so it can distinguish a `layout` mismatch from an `inflate` failure and attach the original zlib error as `cause`. `IdlDecodeReason` splits `'bytes'` into `'layout' | 'inflate'`. The existing `decodeAnchorIdlAccountBytes` (string | null) is kept as a thin wrapper, so `fetchAnchorIdl` / `fetchAnchorIdlFromBuffer` / `fetchIdlFromBuffer` behavior is unchanged. - Add a `classifyRpcError` test for the catch-all `misconfig` branch (non-transient, non-transport SolanaError), plus `layout` and cause-preserving `inflate` tests for `resolveAnchorIdl`. * docs(idl): drop Explorer reference from TRANSIENT_RPC_ERROR_CODES comment Per review on PR #10: the package is generic, so its doc comments shouldn't name a specific downstream consumer. * docs(idl): note future adoption of kit createCodedErrorClass Per review on PR #10 (non-blocking): flag that IdlDecodeError should build on anza-xyz/kit#1576's createCodedErrorClass once that ships. * refactor(idl): dedupe Anchor fetch/resolve via shared readAnchorIdlAccount Per review on PR #10: fetchAnchorIdl and resolveAnchorIdl shared an identical derive-PDA + read-account + exists-check block, and there were two decoders with confusingly similar names. - Extract a single private `readAnchorIdlAccount` core (derive PDA, read, decode); fetchAnchorIdl and resolveAnchorIdl become thin adapters over it. - Collapse to one decoder: rename `tryDecodeAnchorIdlAccountBytes` -> `decodeAnchorIdlAccount` (discriminated result) and drop the `string | null` `decodeAnchorIdlAccountBytes` wrapper; point the *FromBuffer callers at `.ok`. - Add a convention note: `fetch* = lenient/raw/null (never throws)` vs `resolve* = strict/parsed (throws IdlDecodeError)`. No public signature or behavior change; all 93 tests (incl. integration) pass. * fix(idl): classify JSON-RPC parse error (-32700) as misconfig, not transient Per review on PR #10: SOLANA_ERROR__JSON_RPC__PARSE_ERROR (-32700) is a client-fault code — the server emits it when it can't parse the request we sent, so retrying the identical request loops forever. Drop it from TRANSIENT_RPC_ERROR_CODES so it falls through to `misconfig`, matching how its sibling client-fault codes (-32600/-32601/-32602) are already handled. Add a regression test asserting -32700 classifies as misconfig. * refactor(idl)!: collapse fetchAnchorIdl + resolveAnchorIdl into one strict fetchAnchorIdl Decided we don't want two near-identical public Anchor entry points. Keep the released name `fetchAnchorIdl` but give it the strict implementation: - `fetchAnchorIdl` now returns parsed + shape-validated `{ address, idl }`, `null` only when unpublished, and throws `IdlDecodeError` when an account is present but undecodable. `resolveAnchorIdl` and the `ResolvedAnchorIdl` type are removed; `AnchorIdl` is redefined from `{ content }` to `{ idl }`. BREAKING: fetchAnchorIdl's return shape and error behavior changed. The lenient, byte-faithful raw read survives as a private (non-exported) `fetchAnchorIdlContent`, used by `fetchIdl` and `fetchLatestIdls` so their behavior — and the CLI's bare / --latest output — is unchanged. fetchLatestIdls must keep raw on-chain bytes for hash/diff stability (see its design note) and must not throw when one source is corrupt, so it can't use the strict variant. Tests reorganized: strict fetchAnchorIdl tests move to current-idl.test.ts; errors.test.ts now covers only classifyRpcError. All 91 tests pass. * refactor(idl): drop Anchor-shape check from strict fetchAnchorIdl fetchAnchorIdl no longer rejects parsed JSON that lacks an instructions array — a valid IDL may legitimately have none. It now throws only on genuinely undecodable bytes (layout/inflate/json) and returns whatever JSON parses. Removes the now-dead 'shape' member from IdlDecodeReason. Addresses review feedback on PR #10 (Woody4618). * refactor(idl): revert decoder name to decodeAnchorIdlAccountBytes; drop lone divider The helper decodes account data bytes, not the account (readAnchorIdlAccount does the fetch), so the -Bytes name is clearer. Also remove the sole "Live Anchor IDL" section divider — the rest of the file has none and the label named the wrong axis (the file is already all live/current IDL). Addresses review feedback on PR #10 (Woody4618). * feat(idl)!: result-union fetch/error surface A fetch now throws iff the RPC call fails; every data outcome (absent / corrupt / ok) is a value on a discriminated result union. Breaking: - fetchAnchorIdl / fetchPmpIdl / fetchIdlFromBuffer / fetchAnchorIdlFromBuffer / fetchPmpIdlFromBuffer return IdlResult / PmpIdlResult, not `T | null`. - fetchIdl now returns the parsed IDL object (`T | null`); the full outcome plus byte-exact `content` moves to new fetchIdlWrapped -> FetchIdlResult. - Idl: `type` -> `source`, gains `address`, drops `programId`, now generic Idl<T>. - IdlDecodeReason is byte-level 'framing' | 'payload'; non-JSON content is now a validation failure (new IdlValidationError, IdlValidationReason 'json'|'shape'). - fetchPmpIdl / fetchPmpIdlFromBuffer take options objects ({seed,authority} / {format}) instead of positional args. - PMP RPC errors propagate (fetchLatestIdls rejects on a PMP outage); a present but undecodable PMP metadata account is corrupt(framing), not a thrown RPC error. Added: fetchIdlWrapped, unwrapIdl, unwrapIdlOrThrow, parseIdl, IdlValidationError, IdlResult, PmpIdlResult, FetchIdlResult, exported PmpDecodeFormat. Every result arm — including absent — carries the queried `address`. web: /api/idl uses fetchIdlWrapped + parseIdl (404 absent / 422 corrupt) and now depends on @solana/idl via workspace:*. README and web docs updated. * feat(web): surface present-but-invalid IDLs (broken / non-JSON) /api/idl and /api/latest now annotate JSON validity via parseIdl imported from @solana/idl (server-side, not reimplemented). The Current IDL and Latest views flag a present-but-unusable IDL — an "invalid JSON" badge + reason (json/shape) — and keep the raw on-chain content visible / downloadable as .txt instead of showing it as if it were valid JSON.
Problem
Network-related protocols (e.g., Kora or Keychain) and Client generating tools (e.g., Codama) cannot fully leverage the @solana/errors package b/c it relies on storing all errors in this package--that's not viable for scale. Codama addresses this by effectively cloning the error class.
Summary of Changes
createCodedErrorClassso downstream packages can define strongly typed coded error classes with a generated constructor, code-narrowing guard, and human-readable message helper.SolanaErrorto use the shared coded-error factory while preserving dev/prod messages, decode hints,causehandling, frozen contexts, and instruction-index suffix behavior.Test Plan
pnpm --filter @solana/errors test:typecheckpnpm --filter @solana/errors test:lintpnpm --filter @solana/errors test:prettierpnpm --dir packages/errors exec jest -c ../../node_modules/@solana/test-config/jest-unit.config.node.js --rootDir . --runTestsByPath src/__tests__/coded-error-test.ts src/__tests__/error-test.tspnpm --dir packages/errors exec jest -c ../../node_modules/@solana/test-config/jest-unit.config.browser.js --rootDir . --runTestsByPath src/__tests__/coded-error-test.ts src/__tests__/error-test.tsConcept