Skip to content

feat(errors): add create coded error class#1576

Draft
amilz wants to merge 7 commits into
anza-xyz:mainfrom
amilz:feat(errors)/add-createCodedErrorClass
Draft

feat(errors): add create coded error class#1576
amilz wants to merge 7 commits into
anza-xyz:mainfrom
amilz:feat(errors)/add-createCodedErrorClass

Conversation

@amilz

@amilz amilz commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

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

  • Add createCodedErrorClass so downstream packages can define strongly typed coded error classes with a generated constructor, code-narrowing guard, and human-readable message helper.
  • Refactor SolanaError to use the shared coded-error factory while preserving dev/prod messages, decode hints, cause handling, frozen contexts, and instruction-index suffix behavior.
  • Harden coded-error guards against same-name foreign errors and align exposed context types with the runtime frozen context.

Test Plan

  • pnpm --filter @solana/errors test:typecheck
  • pnpm --filter @solana/errors test:lint
  • pnpm --filter @solana/errors test:prettier
  • pnpm --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.ts
  • pnpm --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.ts

Concept

import { createCodedErrorClass } from '@solana/errors';

export const KORA_ERROR__ACCOUNT_NOT_FOUND = -32050 as const;
export const KORA_ERROR__RATE_LIMIT_EXCEEDED = -32030 as const;

type KoraErrorCode = typeof KORA_ERROR__ACCOUNT_NOT_FOUND | typeof KORA_ERROR__RATE_LIMIT_EXCEEDED;
type KoraErrorContext = {
    [KORA_ERROR__ACCOUNT_NOT_FOUND]: { address: string };
    [KORA_ERROR__RATE_LIMIT_EXCEEDED]: undefined;
};

export const { ErrorClass: KoraError, isError: isKoraError } = createCodedErrorClass<KoraErrorCode, KoraErrorContext>({
    messages: {
        [KORA_ERROR__ACCOUNT_NOT_FOUND]: 'Account $address not found',
        [KORA_ERROR__RATE_LIMIT_EXCEEDED]: 'Rate limit exceeded',
    },
    name: 'KoraError',
});

try {
    /* ... */
} catch (e) {
    if (isKoraError(e, KORA_ERROR__ACCOUNT_NOT_FOUND)) {
        displayError(`Missing account ${e.context.address}`);
    }
}

@changeset-bot

changeset-bot Bot commented Apr 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 573efbd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/errors Minor
@solana/errors-core Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/compat Minor
@solana/instruction-plans Minor
@solana/instructions Minor
@solana/keys Minor
@solana/kit Minor
@solana/offchain-messages Minor
@solana/options Minor
@solana/program-client-core Minor
@solana/programs Minor
@solana/react Minor
@solana/rpc-api Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-subscriptions Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/rpc Minor
@solana/signers Minor
@solana/subscribable Minor
@solana/sysvars Minor
@solana/transaction-confirmation Minor
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/wallet-account-signer Minor
@solana/plugin-interfaces Minor
@solana/rpc-graphql Minor
@solana/rpc-parsed-types Minor
@solana/rpc-subscriptions-api Minor
@solana/codecs Minor
@solana/fast-stable-stringify Minor
@solana/functional Minor
@solana/nominal-types Minor
@solana/plugin-core Minor
@solana/promises Minor
@solana/rpc-spec-types Minor
@solana/webcrypto-ed25519-polyfill Minor

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

@amilz amilz changed the title Feat(errors)/add create coded error class feat(errors): add create coded error class Apr 27, 2026
@amilz
amilz marked this pull request as draft April 27, 2026 16:04
Comment thread packages/errors-core/src/coded-error.ts
Comment thread packages/errors-core/src/coded-error.ts
Comment thread packages/errors/src/message-formatter.ts Outdated
Comment thread packages/errors-core/src/coded-error.ts
@raushan728

Copy link
Copy Markdown

First time reviewing a PR still a 2nd year student. Happy to be corrected if I've misread anything.

@amilz
amilz marked this pull request as ready for review April 30, 2026 17:49

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread packages/errors/src/__tests__/coded-error-test.ts Outdated
Comment thread packages/errors-core/src/__tests__/coded-error-test.ts
Comment thread packages/errors/src/__tests__/coded-error-test.ts Outdated
Comment thread packages/errors/src/__tests__/coded-error-test.ts Outdated
Comment thread packages/errors-core/src/__tests__/coded-error-test.ts
Comment thread packages/errors/src/__tests__/coded-error-test.ts Outdated
Comment thread packages/errors/src/__tests__/coded-error-test.ts Outdated
Comment thread packages/errors-core/src/__tests__/coded-error-test.ts
Comment thread packages/errors/src/__tests__/error-test.ts Outdated
Comment thread packages/errors/src/__tests__/message-formatter-test.ts
@bundlemon

bundlemon Bot commented May 4, 2026

Copy link
Copy Markdown

BundleMon

Files updated (7)
Status Path Size Limits
errors/dist/index.node.mjs
20.04KB (+387B +1.92%) -
errors/dist/index.browser.mjs
20.02KB (+386B +1.92%) -
errors/dist/index.native.mjs
20.02KB (+385B +1.91%) -
wallet-account-signer/dist/index.node.mjs
17.02KB (+365B +2.14%) -
wallet-account-signer/dist/index.browser.mjs
17KB (+364B +2.14%) -
wallet-account-signer/dist/index.native.mjs
17KB (+363B +2.13%) -
@solana/kit production bundle
kit/dist/index.production.min.js
47.73KB (+261B +0.54%) -
Unchanged files (137)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
offchain-messages/dist/index.browser.mjs
4.89KB -
offchain-messages/dist/index.native.mjs
4.89KB -
offchain-messages/dist/index.node.mjs
4.89KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
kit/dist/index.browser.mjs
3.72KB -
kit/dist/index.native.mjs
3.72KB -
kit/dist/index.node.mjs
3.72KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
keys/dist/index.node.mjs
3.06KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.41KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
transaction-confirmation/dist/index.native.mj
s
2.36KB -
transaction-confirmation/dist/index.browser.m
js
2.35KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.21KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.17KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.16KB -
subscribable/dist/index.node.mjs
1.97KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
subscribable/dist/index.native.mjs
1.92KB -
subscribable/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
rpc-spec/dist/index.browser.mjs
852B -
rpc-spec/dist/index.native.mjs
851B -
rpc-spec/dist/index.node.mjs
850B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
820B -
plugin-core/dist/index.native.mjs
819B -
plugin-core/dist/index.node.mjs
817B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +2.45KB +0.49%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

amilz added 2 commits May 4, 2026 10:12
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.
@github-actions github-actions Bot added the stale label May 19, 2026
@mcintyre94 mcintyre94 removed the stale label May 26, 2026
@github-actions github-actions Bot added the stale label Jun 9, 2026
@mcintyre94 mcintyre94 removed the stale label Jun 10, 2026
@amilz
amilz marked this pull request as draft June 11, 2026 15:03
askov added a commit to hoodieshq/idl that referenced this pull request Jun 17, 2026
Per review on PR solana-foundation#10 (non-blocking): flag that IdlDecodeError should build on
anza-xyz/kit#1576's createCodedErrorClass once that ships.
askov added a commit to hoodieshq/idl that referenced this pull request Jun 18, 2026
Per review on PR solana-foundation#10 (non-blocking): flag that IdlDecodeError should build on
anza-xyz/kit#1576's createCodedErrorClass once that ships.
Woody4618 pushed a commit to solana-foundation/idl that referenced this pull request Jun 19, 2026
* 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.
@github-actions github-actions Bot added the stale label Jun 26, 2026
@mcintyre94 mcintyre94 added the do-not-close Add this tag to exempt an issue/PR from being closed by the stalebot label Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-close Add this tag to exempt an issue/PR from being closed by the stalebot stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants