Skip to content

[2.x] Reject values of the wrong type when encoding dynamic codecs - #1193

Merged
lorisleiva merged 1 commit into
mainfrom
09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs
Sep 30, 2026
Merged

lorisleiva merged 1 commit into
mainfrom
09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

This PR makes the @codama/dynamic-codecs encoders reject values of the wrong type. Kit encoders silently encode many of them, e.g. 'abc' or undefined as a u16 zero, 1.5 as 1 or 'no' as true, which could produce unexpected instruction data or PDAs.

  • Every type checks the values it encodes and throws DYNAMIC_CLIENT__UNEXPECTED_VALUE_TYPE otherwise: integers accept integer numbers or bigints, public keys base58 addresses, bytes a Uint8Array or an [encoding, data] tuple, structs and maps plain objects (so a Map or a Date is rejected), arrays, sets and tuples arrays, enums an identifier or a { __kind } object, and standalone enum variants a { __kind } object of that variant.
  • Missing options encode as None (previously Some of a zero value) and missing structs as structs with missing fields, so their default values apply. Other missing values throw.
  • The error context gains the nodePath of the node that rejected the value, e.g. [root, program, instruction, data, amountField, amountType].
  • Renames DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE to DYNAMIC_CLIENT__UNEXPECTED_VALUE_TYPE, keeping its code.

@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7f7c898

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Adds encode-side type validation to @codama/dynamic-codecs so that values Kit would silently coerce ('abc' → u16 zero, 1.5 → 1, short base58 strings padded into a 32-byte pubkey, etc.) now throw DYNAMIC_CLIENT__UNEXPECTED_VALUE_TYPE instead. The mechanism is a single assertValueType helper that wraps each leaf/container codec in an encode-only transformCodec, capturing stack.getPath() at codec-creation time so the error carries the exact nodePath even after the stack unwinds. Options gain undefined → None, structs gain undefined → {} so field defaults apply, and the error constant is renamed (same code).

The design is sound. A few things I checked and that hold up:

  • Layering order is right. Validation is applied inside next(node), so the interceptVisitor transform layer (fixedSize, sizePrefix, offsets…) sits outside the check and the check still sees the raw user value. For structs the undefined → {} transform is correctly outside assertValueType, so encode(undefined) doesn't trip the isObjectRecord check.
  • stack.getPath() returns a copy ([...this.currentPath]), so the captured nodePath closures are stable. The node-path tests confirm this through links, enums, options and maps.
  • transformCodec preserves fixedSize, so wrapping option/struct items doesn't break the assertIsFixedSize calls in visitOptionType / visitZeroableOptionType.
  • nodePath: readonly Node[] in the error context has precedent (CANNOT_RESOLVE_PATH, LINKED_NODE_NOT_FOUND), and encodeContextObject stringifies objects as [object Object], so production error messages won't balloon with the full IDL.
  • Bigint range checks remain Kit's responsibility (NUMBER_OUT_OF_RANGE), which is the right split — isInteger only guards the shape.

Things to watch

  • Map instances slip through visitMapType — see inline. isObjectRecord(new Map(...)) is true, and Object.entries(map) is [], so a Map silently encodes as an empty map. That's the same class of footgun this PR is eliminating; sets already reject Set instances via Array.isArray, so maps should be symmetric.
  • No changeset in the diff. CONTRIBUTING.md says any user-facing change needs one, and this touches three published packages (@codama/errors in the fixed group, @codama/dynamic-codecs, @codama/dynamic-client) with a behaviour change and a renamed export. If the 2.x seeded major changeset is meant to cover this, fine — just confirming it's deliberate.
  • Renamed export is a hard break. CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE disappears from @codama/errors and @codama/dynamic-client. The repo kept FAILED_TO_EXECUTE_RESOLVER as a @deprecated constant rather than removing it — see inline for an optional alias in the same spirit.
  • Standalone enumVariantTypeNode without data still accepts anything. In visitEnumVariantType, if (node.data === undefined) return undefined; runs before any shape check, so getNodeValueCodec([enumVariantTypeNode('quit')]).encode(42) encodes to empty bytes. Inside an enum the union's __kind lookup already guards this, so it only affects the standalone case — low priority, but it's the one remaining node kind that doesn't validate.

For subsequent reviewers

  • The @solana/addresses runtime dependency is new to dynamic-codecs (for isAddress). Same ^8.4.0 range as @solana/codecs, and the sibling dynamic packages already depend on it, so no version skew — but worth knowing it's there.
  • nodeKind in the error context is now redundant with nodePath[nodePath.length - 1].kind; it's kept because $nodeKind is interpolated into the message. Reasonable.
  • formatValueType deliberately includes the value for numbers only (number (1.5)), not for strings — presumably to avoid leaking long/sensitive strings into error messages. Consistent with the tests.

Comment thread packages/dynamic-codecs/src/codecs.ts
Comment thread packages/dynamic-codecs/src/codecs.ts Outdated
Comment thread packages/errors/src/codes.ts
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from 72e9efe to e881843 Compare September 29, 2026 16:43

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review

Re-fetched the diff. Everything raised in the first pass has been addressed, and the fixes are cleaner than what I suggested:

  • Map / non-plain objects — isObjectRecord now checks the prototype (Object.prototype or null) rather than special-casing Map, so Map, Set, Date and class instances are all rejected for structs and maps. The Object.create(null) acceptance test covers the JSON-parse-with-null-proto case too.
  • BYTES_ENCODINGS exhaustiveness — now derived from a satisfies Record<BytesEncoding, true> record, so a new encoding in the spec is a compile error here.
  • Standalone enumVariantTypeNode — now rejects anything that isn't a plain object with the matching __kind, with the expectedType string adapting to whether the variant carries data. Nice touch that actualType reports variant 'move' when the shape is right but the kind is wrong, and falls back to formatValueType otherwise. The comment noting the check is redundant inside an enum is accurate: the enum's own transform already guarantees isObjectRecord(value) && typeof __kind === 'string', and the union index lookup guarantees the kind matches.
  • visitEnumType non-string/non-object values — now throw string | { __kind: string } instead of reaching the union with a garbage __kind. Test added.
  • The @deprecated alias was optional; a clean break on 2.x is a reasonable call.

One small note, not blocking: the prototype check in isObjectRecord is realm-sensitive — an object created in another realm (Node vm context, iframe) has a different Object.prototype and will be rejected. That's an unusual scenario for instruction inputs, and the alternative (Object.prototype.toString.call(value) === '[object Object]') has its own holes with Symbol.toStringTag, so I'd leave it as is — just flagging in case a bug report ever shows up with that shape.

The changeset point from the first review still stands if it wasn't deliberate, but I assume the 2.x stack handles it collectively.

LGTM.

@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from 803123b to f4b10ab Compare September 30, 2026 13:54
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from e881843 to 2e559ef Compare September 30, 2026 13:54
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from f4b10ab to ef341d9 Compare September 30, 2026 14:07
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch 2 times, most recently from 45c081b to e3bfabb Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch 2 times, most recently from b79b1c0 to 50f84a6 Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from e3bfabb to 97d1f4c Compare September 30, 2026 14:14
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from 50f84a6 to 0b55a54 Compare September 30, 2026 14:14
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from 97d1f4c to 9812253 Compare September 30, 2026 14:15
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from 0b55a54 to a72493e Compare September 30, 2026 14:15
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from 9812253 to fcd8c62 Compare September 30, 2026 14:16
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch 2 times, most recently from a5ba85a to 6845cc7 Compare September 30, 2026 14:17
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from fcd8c62 to 375cb5e Compare September 30, 2026 14:17
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from 6845cc7 to b57efa1 Compare September 30, 2026 14:18
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch 2 times, most recently from 36c98ff to af66e5f Compare September 30, 2026 14:18
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch 2 times, most recently from 8a311bc to a773e2e Compare September 30, 2026 14:19
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from af66e5f to 608b4bd Compare September 30, 2026 14:19
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from a773e2e to add2ee0 Compare September 30, 2026 14:20
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch from 608b4bd to 1ef2655 Compare September 30, 2026 14:20
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from add2ee0 to cac2333 Compare September 30, 2026 14:21
@lorisleiva
lorisleiva force-pushed the 09-29-adapt_dynamic-address-resolution_to_codama_v2 branch 2 times, most recently from 9c01248 to 4d1c3d7 Compare September 30, 2026 14:21
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from cac2333 to eadafbd Compare September 30, 2026 14:21
Base automatically changed from 09-29-adapt_dynamic-address-resolution_to_codama_v2 to main September 30, 2026 14:22
@lorisleiva
lorisleiva force-pushed the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch from eadafbd to 7f7c898 Compare September 30, 2026 14:22
@lorisleiva
lorisleiva marked this pull request as ready for review September 30, 2026 14:22
@lorisleiva
lorisleiva merged commit b8b1be3 into main Sep 30, 2026
0 of 2 checks passed
@lorisleiva
lorisleiva deleted the 09-29-reject_values_of_the_wrong_type_when_encoding_dynamic_codecs branch September 30, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants