CHANGELOG.md

# Unreleased

# 1.2.0 - 2026-05-01

## Public Surface Pass

* **Public Surface Pass bundle — `decode_event/4` error contract narrowed.** The `decode_event/4` `@spec` previously declared `{:ok, ...} | {:error, term()}`, but the runtime path raised on malformed payloads instead of returning `{:error, _}` — so the typespec was a lie wherever a topic-matched log carried truncated/garbage data. Wrapped the `decode_raw`-driven payload-decode path in a `try/rescue` and converted raised exceptions into `{:error, {:malformed_data, message}}`. `verify_event_signature/2` already returned `{:error, ...}` for signature mismatches but used a string format; tightened to the atom-tagged shape `{:error, {:event_signature_mismatch, %{expected: ..., got: ...}}}` so callers can pattern-match the failure mode without parsing strings. Added `@type ABI.Event.decode_error/0` enumerating the closed error set (`{:event_signature_mismatch, _}`, `{:topics_length_mismatch, _}`, `{:malformed_data, _}`); narrowed `ABI.decode_event/4`'s `@spec` from `{:error, term()}` to `{:error, Event.decode_error()}`. The `api()` declaration now carries an `errors:` block mirroring `decode_call/3`'s pattern, so the manifest exposes the closed error set to agent consumers. Bugfix-honoring-typespec — not a breaking change in any sensible sense (the prior contract was unreachable for malformed payloads), but downstream callers that previously caught raises around `decode_event/4` should switch to matching `{:error, {:malformed_data, _}}`.
* **Public Surface Pass bundle — `encode_bytes/1` flipped from `def` to `defp`.** Hygiene-only flip. Already `@doc false` (lib/abi/type_encoder.ex), zero callers outside `type_encoder.ex` itself across the local monorepo (`cartouche`, `onchain`, `onchain_{aave,evm,js,tempo}`, `mpp`), and the agent-economy hint-rot test had already excluded it as a deliberate internal helper. The `def` was a leftover from before `encode_raw/2` became the canonical raw-encoding entrypoint. Removed the now-redundant `@doc false` and the explicit exclusion entry from `test/abi/agent_economy_test.exs` (the function is no longer in `module_info(:exports)`, so the hint-rot cross-check skips it naturally). Manifest user-declared count unchanged (already excluded). ROADMAP's "(breaking, if changed)" wording was overcautious — the codebase already treated this as internal.
* **New API — `ABI.decode_error/2`.** Decodes Solidity 0.8.4+ custom-error revert data: matches the first-4-byte selector in `revert_data` against a list of known error definitions (signature strings or pre-parsed `FunctionSelector` structs, mixed accepted) and decodes the payload of whichever matches. Returns `{:ok, %{error: name, args: [...]}}` on a hit, `{:error, :no_match}` when no definition's selector matches (or the list is empty), and `{:error, :calldata_too_short}` on `<4` bytes. Mirrors `decode_call/3`'s contract: malformed payload after a selector match still raises (same behavior as `decode/3`). The first definition with a matching selector wins — definition order is the disambiguation lever. Reuses `ABI.method_id/1` (selector computation), `ABI.decode/3` (payload decode), `ABI.Parser.parse!/2` (signature normalization). `api()` declaration carries the closed error set under namespace `/abi`. Solidity 0.8.4+ custom-error revert data is selector-prefixed exactly like calldata, so the implementation is structurally identical to `decode_call/3` modulo the multi-definition list.
* **New API — `ABI.encode_packed/2`.** Solidity's [non-standard packed encoding](https://docs.soliditylang.org/en/stable/abi-spec.html#non-standard-packed-mode) — used for Merkle airdrop leaves and `keccak256(abi.encodePacked(...))` signature schemes. Per the spec: types <32 bytes concatenate tight (no padding); dynamic types (`bytes`, `string`) inline as raw payload (no length prefix); array elements are padded to 32 bytes (or 32-byte multiples for `string`/`bytes`) so element boundaries are recoverable; tuples/structs and nested arrays are explicitly unsupported and raise `ArgumentError` with a spec link. The wrapper accepts the same polymorphic first arg as `encode/2` (`binary() | FunctionSelector.t()`); paren-only signatures like `"(uint256,address)"` parse as a single-tuple parameter (same shape as `"foo((uint256,address))"`) and therefore raise — pass a struct-arg-free signature like `"foo(uint256,address)"` for a comma-separated arg list. Cross-checked against the canonical spec example (`int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!")` → `0xffff42000348656c6c6f2c20776f726c6421`) and locked as a golden vector; Merkle-leaf golden vector (`address ++ uint256` → 52 bytes pre-hash) included for airdrop consumers. Implementation lives next to `encode_type/2` in `lib/abi/type_encoder.ex` with private `packed_top/2`, `packed_array/2`, `pack_uint/2`, `pack_int/2` helpers — does NOT thread through `Math.pad/4` (which always rounds to 32-byte multiples) since packed mode is the inverse of standard ABI. Inside an array, however, scalar elements DO route through `encode_type/2` to reuse the 32-byte rounding correctly per spec.

* **Property Suite Expansion bundle (5 members — original 4 plus a production fix the suite surfaced).** Single PR expanding `test/abi/roundtrip_property_test.exs` plus the resulting bug fix in `lib/abi/type_decoder.ex`.
  * **`tuple[]` (dynamic array of tuples) round-trip coverage** — the existing dispatcher already composes `{:array, inner}` with `{:tuple, ...}`, so no new generator clause was needed; what was missing was explicit pin-down. Three new properties: a static-only-element `tuple[]` (exercises the array-of-static-tuples layout), a mixed-element `tuple[]` where each element is itself dynamic (the most stress-testing shape — head/tail offsets are computed both per-array AND per-element), and an empty `tuple[]` unit test. The mixed-element property is what surfaced the string-NUL bug below on its first run.
  * **Empty dynamic fields inside structs** — explicit fixtures (not properties) for the four pinned shapes that mining surfaced from real protocol calldata: `(bytes, string)` with empty bytes and non-empty string, `(bytes, bytes)` both empty, empty `tuple[]` as the only dynamic field in a struct, and empty `tuple[]` followed by non-empty bytes. Inline `test "..." do` blocks rather than the `@fixtures` pattern from `defi_calldata_test.exs` — round-trip equality is enough; no need to lock against synthetic byte strings when the property tests already exercise the layout invariants.
  * **Multiple top-level struct args** — added a `roundtrip_args/2` helper alongside the single-arg `roundtrip/2`. New property mirrors the Balancer V2 `swap(SingleSwap, FundManagement, uint256, uint256)` shape: two sibling structs of differing dynamic-rate (one mixed static+dynamic, one static-only) plus two scalar args at the ends. Exercises sibling-tuple offset arithmetic when adjacent top-level tuples are dynamic at different rates — `roundtrip/2` always wrapped a single arg as `[%{type: type}]`, so this layout was never exercised by the property suite.
  * **Deep struct nesting (depth ≥ 4) round-trip** — the recursive `composite` property's `type_and_value_gen(3)` cap was bumped to depth 5 (Pendle `swapExactTokenForPt` exercises depth 5 in real calldata). `@tag timeout: 120_000 → 300_000` to absorb the larger generation surface; `max_runs: 50` (down from the default 100) keeps CI bounded — depth-5 trees can balloon (lists of length 4 of strings up to 64 chars at multiple nesting levels), and 50 deep samples have higher information-density than 100 shallow ones.
  * **Bug fix surfaced by the bundle:** `ABI.TypeDecoder` previously called `nul_terminate_string/1` on every decoded `:string`, splitting the binary at the first `<<0>>` byte and returning only the prefix. This treated Solidity strings as C strings — wrong. Per the Solidity ABI spec, strings are length-prefixed UTF-8 and may legally contain NUL codepoints (`U+0000`); the length is exact, so right-padded zeros after the data are the only NULs that need stripping (and `decode_bytes/3 → Math.unpad/3` already handled that). Removed the `nul_terminate_string/1` helper entirely; the `:string` decode clause now delegates straight to `decode_bytes(rest, length, :right)`. Pre-existing in upstream `exthereum/abi` since 2018 (commit `bdceb719` by Levi Aul) — undetected because random `StreamData.string(:utf8, ...)` rarely starts with NUL, and most real Solidity strings (function names, error messages, event signatures) don't either. The mixed-element `tuple[]` property happened to generate a NUL-prefixed string and surfaced it. Three regression unit tests added: leading-NUL string, embedded-NUL string, and all-NULs string. Production paths affected: `ABI.decode/3`, `ABI.decode_call/3`, `ABI.decode_event/4`, `ABI.TypeDecoder.decode/3` — anywhere a `:string` is decoded. **Should be filed upstream** alongside #53/#54/#55 (or batched with the lexer `x` sub-bug) — affects every `exthereum/abi` consumer that decodes user-supplied strings.

* **DeFi Real-World Fixtures bundle.** Tests-only addition; no production code touched. Closes both members of the bundle in a single commit.
  * `test/abi/defi_calldata_test.exs` — 10 round-trip golden vectors captured from `defi-skills build --action <name> --json` (defi-skills v0.3.0). Fixtures live inline as `@fixtures` (the originally-proposed `test/fixtures/defi_calldata.exs` shape was discarded — no `.exs` data-loading idiom exists in the repo and inline matches the existing test convention). Each fixture asserts both directions: `ABI.encode(sig, args)` reproduces the locked calldata byte-for-byte and `ABI.decode_call(sig, calldata)` recovers the original args. Coverage: Aave V3 supply/borrow/setCollateral, Compound V3 supply/claim, Lido stake/unstake (the only fixture exercising `uint256[]` head/tail layout), EigenLayer deposit, ERC-20 transfer, WETH unwrap. The aave_supply fixture reproduces the calldata locked in the ROADMAP planting note byte-for-byte.
  * `test/abi/function_selector_real_world_test.exs` — 12 explicit `ABI.method_id/1` golden-vector assertions: ERC-20 (`transfer`, `transferFrom`), Aave V3 (`supply`, `borrow`), WETH/Curve gauge (`withdraw(uint256)` — duplicate selector by design), WETH/Rocket Pool (`deposit()` — duplicate by design), Lido (`requestWithdrawals(uint256[],address)`), Compound V3 (`claim`), Curve 3pool (`add_liquidity(uint256[3],uint256)` — fixed-size-array path), Uniswap V3 (`exactInputSingle(tuple)` — single tuple arg), Balancer V2 (`swap` — multiple top-level tuples), and EigenLayer (`queueWithdrawals(tuple[])` — dynamic `tuple[]`). A second `describe` block round-trips the four tuple/`tuple[]`/fixed-array signatures through `FunctionSelector.decode/1 ∘ encode/1` and re-asserts the selector — the 4 corner-case canonical-signature shapes were the entire reason the selector vectors are scoped this widely.
  * **Why ship this.** Before this bundle, `hieroglyph` had a single mainnet-style fixture in the entire test suite (the ERC-20 `Transfer` event doctest in `lib/abi.ex`). Every encoder/decoder change was verified only against synthetic property-suite shapes. Real-world contract evidence now lives next to the synthetic suite, giving cartouche / onchain CI a stable contract-stability surface across hieroglyph version bumps. The 4 tuple-bearing selectors specifically prove the canonical-signature serialization in `function_selector.ex` matches what real chains expect.

## Agent Economy

* **Agent Economy — Phase 1: Descripex on `ABI` top-level.** Annotated the seven public functions (`encode/2`, `method_id/1`, `decode/3`, `decode_call/3`, `decode_event/4`, `event_signature/1`, `parse_specification/1`) with `api()` declarations under namespace `/abi`. Six of the seven take a polymorphic first arg (`binary() | FunctionSelector.t()`); the union is described in prose per the established mpp/mcp `payment_required_error/1` precedent (one `api()` block per function name; the formal union lives on `@spec`). `ABI` now `use`s `Descripex.Discoverable` with a single-module list — Phase 2 expands the list to all six annotated modules. Existing `@doc` blocks (with their doctests) preserved by ordering: `api()` first emits its generated `@doc`, then the manual `@doc """..."""` overrides slot 4 prose while `@doc hints:` survives via descripex's `__before_compile__` ETS injection. `@spec` and runtime behaviour unchanged. Manifest emission via `mix descripex.manifest --app hieroglyph` already works (returns 7 ABI entries plus the four Discoverable bookkeeping exports); the dedicated `mix hieroglyph.manifest` task lands in Phase 3. Doctor docs/specs coverage held at 100/100, dialyzer 0 warnings, credo `--strict` 0 issues. New runtime dep: `{:descripex, "~> 0.6"}` (transitively pulls `:json_spec`). Version bumped from `1.1.x` → **`1.2.0`** because adding a runtime dep changes downstream consumers' (cartouche, onchain) dependency closure.
* **Agent Economy — Phase 2: Descripex on the remaining five modules.** Annotated all 18 documented public functions across `ABI.Event` (`/selector` — 3 fns), `ABI.FunctionSelector` (`/selector` — 5 fns; the three `@doc false` internals `dynamic?/1`, `get_function_type/1`, `get_state_mutability/1` deliberately excluded), `ABI.TypeEncoder` (`/codec` — 2 fns; `encode_bytes/1` `@doc false` excluded), `ABI.TypeDecoder` (`/codec` — 4 fns), and `ABI.Math` (`/math` — 4 fns). None of these 18 functions are polymorphic, so each `api()` block follows the standard shape — name, one-sentence description, `params:` keyword list, `returns:` map. `composes_with:` links wired across the natural pairings: `Event.decode_event ↔ event_signature`, `FunctionSelector.decode ↔ encode`, `TypeEncoder.encode ↔ encode_raw`, `TypeDecoder.decode ↔ decode_raw`. Extended `use Descripex.Discoverable, modules: [...]` in `lib/abi.ex` from `[ABI]` to all six annotated modules. Manifest now emits the full **25 user-declared `api()` entries** (7 + 3 + 5 + 2 + 4 + 4) plus the 4 framework `Discoverable` exports — verified via `mix descripex.manifest --app hieroglyph` and per-module entry counts. `@doc`/doctest preservation pattern from Phase 1 carries through: existing `@doc """..."""` blocks override slot 4 prose; `@doc hints:` survives. No behaviour change, no `@spec` change.
* **Agent Economy — Phase 3: dedicated manifest task + hint-rot validation test.** New mix task `mix hieroglyph.manifest [path]` (defaults to `api_manifest.json`) emits the JSON manifest using `ABI.__descripex_modules__/0` as the single source of truth — direct port of the established `mix mpp.manifest` shape. Manifest is suitable for downstream cartouche/onchain CI to diff across hieroglyph version bumps as a contract-stability artifact. New test file `test/abi/agent_economy_test.exs` enforces the agent-discovery surface in three describe blocks plus a load-bearing cross-check: (1) every entry in each module's `__api__/0` carries `:hints.description`; (2) `ABI.describe/0..2` returns the expected modules / function lists / per-function detail; (3) namespaces (`/abi`, `/selector`, `/codec`, `/math`) match per-module assignments via `Code.fetch_docs/1`. The cross-check walks `module_info(:exports)`, strips Elixir/Descripex framework exports, plus an explicit allow-list of the four `@doc false` internal helpers (`FunctionSelector.dynamic?/1`, `get_function_type/1`, `get_state_mutability/1`, `TypeEncoder.encode_bytes/1`), then asserts every remaining export is declared with `api()` — without this gate, hints rot silently when new `def`s land without `api()`, and silent rot here propagates as silent contract drift through cartouche-generated bindings into every onchain_<protocol> package.
* **Bug fix (sub-bug of upstream [#54](https://github.com/exthereum/abi/issues/54), upstream filing deferred — will be batched into a future combined-bugs issue with PR offer):** `ABI.FunctionSelector.decode_type("fixed128x18")` and `decode_type("ufixed256x80")` (and the same-shape forms inside arrays/tuples/function signatures) raised a leaky `FunctionClauseError` instead of the friendly `ArgumentError` that bare `fixed`/`ufixed` already produced. Root cause was lexer rule ordering in `src/ethereum_abi_lexer.xrl`: the LETTERS rule (`[a-zA-Z_]+`) was listed before the standalone `'x'` terminal, so leex picked LETTERS on equal-length matches and the single `x` between the two integers tokenized as `letters`. The grammar rule `type -> typename digits 'x' digits` never fired, the parser fell through to `juxt_type(fixed, 128)`, and `juxt_type/2` had no `fixed` clause. Fix: introduced dedicated `fixed_typename` / `ufixed_typename` terminals (so the `'x'` separator is only valid in `fixed`/`ufixed` contexts), moved the `'x'` rule above `{LETTERS}`, and extended the parser's `identifier_part` to also accept `'x'`, `fixed_typename`, and `ufixed_typename` so single-char `x` and the keyword forms still work as function/argument names. The explicit-M/N forms now route through `ABI.Parser.reject_unsupported!/1` and raise the same friendly `ArgumentError` (with the upstream-#54 link) that the bare forms already produced. Yecc reports 3 shift/reduce conflicts (was 1) — the 2 new ones come from `fixed_typename`/`ufixed_typename` being able to start either a `type` or an `identifier_part`; yecc's default shift resolution is the desired behavior (`fixed128` is a type prefix, not an identifier), and is documented inline in the `.yrl` `Expect 3.` comment. Regression tests added: explicit-M/N rejection (`fixed128x18`, `ufixed256x80`) plus `x`-keyword identifier handling (function named `x`, argument named `x`, function named `fixed`/`ufixed`, and a function name containing `x` mid-string).

# 1.1.0 - 2026-05-01

* **Bug fix (upstream [#55](https://github.com/exthereum/abi/issues/55)):** `ABI.TypeEncoder.encode_int/2` rejected ALL `int<N>` values (including `0`) for small bit widths, because the overflow guard mixed up bytes and bits — it compared `byte_size(significant_bytes)` against `desired_size_bytes - 1`, which evaluates to `0` for `int8`, raising on every input. Replaced with a numeric range check against `2^(N-1)` performed up-front, so the encoder accepts the full signed range `-2^(N-1)..2^(N-1)-1` for every `int<N>`. The pre-existing `"int overflow raises data overflow"` test passed only because the encoder was broken for any value; tightened the test to assert specific in-range values encode AND specific boundary cases (`128`, `-129`) raise.
* **Bug fix:** `ABI.FunctionSelector.dynamic?/1` raised `FunctionClauseError` on `{:array, T, 0}` (zero-length fixed array). The grammar accepts `T[0]` (yrl rule allows `N >= 0`), so the type is parseable, but the existing clauses required `len > 0` and no clause matched the zero case. Added `def dynamic?({:array, _type, 0}), do: false` — a zero-length fixed array has no head/tail layout and no payload, so it is static by any sensible definition. Encoder and decoder paths already handle zero-length arrays (`encode_type({:array, _, 0})` produces an empty repeated-type tuple; `decode_type({:array, _, 0}, data, _opts) -> {[], data}`); verified by extending `roundtrip_property_test.exs`'s fixed-array length domain from `1..3` to `0..3`. Pre-existing in upstream `exthereum/abi`; not yet filed.
* `ABI.FunctionSelector.@type type` now carries a `@typedoc` clarifying that `address payable` collapses to `:address`. Solidity's ABI JSON only emits `"address"` for both forms, the on-the-wire encoding is identical (20-byte left-padded), and payability is a property of `state_mutability` rather than a separate type variant — so consumers shouldn't expect a distinct atom.
* Added `test/abi/roundtrip_property_test.exs` — property-based `decode(encode(x)) == x` coverage using `stream_data` for every type in `ABI.FunctionSelector.@type type/0`: `uint`, `int`, `address`, `bool`, `string`, `bytes`, `bytesN`, fixed and dynamic arrays, and recursively nested tuples (depth ≤ 3). Per-type properties localize failures to a single clause; the recursive composite property exercises nested `{:tuple, [{:array, ...}]}` shapes where head/tail offsets matter. Surfaced the `encode_int` bug above on its first run. Test-only dep `{:stream_data, "~> 1.1"}` added.
* Test coverage for the empty-args calldata path (`f()` shape — 4-byte selector with zero ABI-encoded args). `weth.deposit()` (selector `0xd0e30db0`), `rocket_pool.deposit()`, and similar zero-arg calls were untested by the round-trip property suite (every generator produced at least one value). Pinned both directions: `ABI.encode("deposit()", []) == <<0xD0, 0xE3, 0x0D, 0xB0>>` and `ABI.decode("deposit()", <<>>) == []`, plus the `function: nil`/`types: []` empty-bytes shape.
* Test coverage for `FunctionSelector` selector-rendering and parser edge cases: `encode/1` canonical-signature rendering of `{:int, N}`, `{:struct, _, _, _}`, dead-via-parse types (`:function`, `{:fixed, M, N}`, `{:ufixed, M, N}`) and `nil`-typed slots (defensive against partially-built typeinfo maps); plus `parse_specification/1`'s `%{"indexed" => _}`-without-`"name"` branch (older Solidity versions and hand-written ABIs may omit names on indexed event params).
* **New API:** `ABI.method_id/1` returns the 4-byte function selector (`keccak256(canonical_signature)[0..3]`) for a signature string or `FunctionSelector` struct. Returns `<<>>` for selectors with `function: nil`. Previously the same logic was private to `ABI.TypeEncoder`; exposing it is a useful primitive for callers that need to compute selectors without encoding args (selector-table routing, log-topic matching, calldata pre-validation).
* **New API:** `ABI.decode_call/3` is the symmetric counterpart to `ABI.encode/2` for selector-prefixed calldata. Splits the 4-byte prefix, verifies it matches the expected selector, and decodes the payload via the existing `decode/3` machinery. Returns `{:ok, decoded}` on match or `{:error, reason}` for `:calldata_too_short` (< 4 bytes), `:selector_mismatch` (prefix wrong), or `:no_function_name` (selector has `function: nil`, so there's nothing to verify against — caller should use `decode/3` with the payload). `ABI.decode/3` semantics are unchanged: it remains payload-only, matching `eth-abi` / `ethers` / `viem` / `alloy` conventions.

# 1.0.0 - 2026-04-24

First hex.pm release as `hieroglyph`. This is a maintained fork of [exthereum/abi](https://github.com/exthereum/abi); the module namespace is unchanged (`ABI.encode/2`, `ABI.decode/2`, etc. — consumers just swap the hex dep name). Version resets to `1.0.0` under the new package name; the `1.0.0-alpha*` / `1.0.0-bravo1` lines below this entry are the upstream's pre-release history, carried forward for context but never published to hex under `hieroglyph`.

* **Published as `hieroglyph`** — hex package renamed from the internal `abi` app name. Top-level module `ABI` preserved (the Solidity term is the correct module name). Repo renamed to `ZenHive/hieroglyph`; upstream `exthereum/abi` tracked in the package's "Upstream (fork-of)" link.
* **Bug fix (upstream [#53](https://github.com/exthereum/abi/issues/53)):** `ABI.Event.decode_event/4` now returns `{:indexed_hash, <<32 bytes>>}` for indexed parameters of *reference* type (`string`, `bytes`, all arrays — fixed-size or dynamic — and tuples/structs) instead of silently misdecoding the keccak topic as if it were a raw ABI-encoded value. Per the Solidity ABI spec, indexed reference-type values are stored in topics as `keccak256(value)` and the original is unrecoverable — the tagged tuple preserves the hash (useful for log filtering and equality checks) and makes the "unrecoverable" signal pattern-matchable. This is broader than the ABI head/tail "dynamic" rule: `uint256[2]` and tuples of all-static members are *static* for regular ABI encoding but are still hashed in event topics, and this fix handles both. Breaking for callers that consumed the previous garbage bytes directly; static value-type indexed params (`uint`/`int`/`address`/`bool`/`bytesN`/`function`/`fixed`/`ufixed`) are unchanged. Regression tests added for indexed `string`, indexed `bytes`, indexed dynamic array, indexed fixed-size static array (`uint256[2]`), indexed tuple of static members, and mixed static+dynamic+non-indexed cases.
* **Bug fix (upstream [#54](https://github.com/exthereum/abi/issues/54)):** `fixed`, `ufixed`, and `function` types now raise `ArgumentError` at parse time (in `ABI.Parser.parse!/2`, walking nested arrays and tuples) with a link to the tracking issue, instead of parsing silently into unsupported internal terms and later raising the cryptic `"Unsupported encoding type"` inside `TypeEncoder` / `TypeDecoder`. Also filled the `{:bytes, pos_integer()}` gap in `ABI.FunctionSelector.@type type` — previously omitted even though fully supported by the encoder and decoder. Note: the explicit `fixed<M>x<N>` / `ufixed<M>x<N>` forms still raise a `FunctionClauseError` upstream of this walker due to a separate pre-existing lexer-rule-ordering bug (single `x` tokenizes as `letters` instead of the `'x'` terminal) — tracked as a follow-up task. Also aligned the grammar's bare-`fixed` / bare-`ufixed` canonical expansion to the Solidity spec (`fixed128x18` / `ufixed128x18`; previously `x19`) so the rejection error message reports the correct form.
* Simplified `ABI.Parser.parse!/2`'s unsupported-type walker to drop a dead `is_list(returns)` branch — the yecc grammar only emits `nil` or a single bare type for `returns`, never a list. No behavior change.
* Extracted the 32-byte padding logic into `ABI.Math.pad/4` and `ABI.Math.unpad/3`. `ABI.TypeEncoder.encode_bytes/1`, `encode_int/2`, and `encode_uint/2` now delegate to `ABI.Math.pad/4`; `ABI.TypeDecoder.decode_bytes/3` is a thin wrapper around `ABI.Math.unpad/3`. No behavior change; resolves the long-standing `TODO: add to ABI.Math` comments in both modules.
* Renamed `ABI.FunctionSelector.is_dynamic?/1` to `ABI.FunctionSelector.dynamic?/1` to satisfy `Credo.Check.Readability.PredicateFunctionNames`. The function remains `@doc false` (internal). No deprecation shim — the old name had `@doc false` since 2017 and zero in-repo references outside three private call-sites, which were updated.
* Drove `mix credo --strict` to zero violations (was 51). Covers `Design.AliasUsage` (top-of-module aliases added across `ABI`, `ABI.Event`, `ABI.FunctionSelector`, `ABI.Parser`, `ABI.TypeDecoder`, `ABI.TypeEncoder`, and `ABI.Hex`), `Readability.MaxLineLength` (spec / docstring wraps + the big `Enum.reduce` tuple-encoder broken into an `encode_tuple_element/2` helper), `Consistency.ParameterPatternMatching` (flipped three `record = %{…}` heads to `%{…} = record`), and `Refactor.Nesting` (extracted `ABI.Event.verify_event_signature/2` and `ABI.TypeEncoder.fetch_named_field/2` + `fetch_by_name/2` helpers to drop nesting below 3).
* Added regression tests for the map-input encoder path (`ABI.TypeEncoder.data_to_list/2`): atom-keyed maps, string-keyed maps, camelCase→snake_case name resolution, string-over-atom key priority, integer values inside nested named-struct maps, and the missing-field / unnamed-type error raises. The map branch previously had zero test coverage; the string-key path was added in commit `46accc8`, and this suite also exercises integer encoding (`a43e9d5`) through the map branch.
* Added `@spec` typespecs and `@doc` strings for every previously-undeclared public function across `ABI`, `ABI.Event`, `ABI.TypeDecoder`, `ABI.TypeEncoder`, and `ABI.FunctionSelector`: `ABI.event_signature/1`, `ABI.parse_specification/1`, `ABI.TypeDecoder.decode/3`, `ABI.TypeDecoder.tuple_value/3`, `ABI.TypeEncoder.encode_raw/2`, `ABI.Event.decode_event/4` / `event_signature/1` / `canonical/2`, and `ABI.FunctionSelector.decode/1` / `decode_raw/1` / `parse_specification_item/1` / `decode_type/1` / `encode/3`; also added docs for `TypeDecoder.tuple_value/3` and `TypeDecoder.decode_bytes/3`. Matches the style widened in PR #52. Doctor spec coverage 42% → 88%, doc coverage 88% → 96%.
* Added regression tests for eleven previously-uncovered error paths: `bool` with non-boolean values, `bytes<N>` size mismatches and wrong-datatype values, unsupported type atoms across encoder / decoder / function-selector, int/uint overflow, trailing decode data, and `decode_event/4` returns for mismatched event signatures and invalid topic counts.
* README refreshed: dropped the stale "tuples with multiple elements don't parse" caveat (false since JSON-ABI support), corrected `ABI.encode/2` arity and flipped `bytes<M>` to supported in the Support checklist, migrated dead `solidity.readthedocs.io` links to `docs.soliditylang.org`, and added runnable examples for `ABI.parse_specification/1`, `ABI.Event.decode_event/4`, and map/struct input to `encode/2`.

# 1.0.0-bravo1
* Fix ABI tuple encoding for nested inlined tuples
# 1.0.0-alpha9
* Add Names to Event Signatures
# 1.0.0-alpha8
* Add Event Signature check to ABI.Event.decode_event
* Change `decode_event` to return an {:ok, event_name, event_params} tuple.
* Add ability to add `"indexed"` keyword to ABI canonicals
# 1.0.0-alpha7
* Bugfix for event decoding with dynamic parameters
# 1.0.0-alpha6
* Bugfix for is_dynamic
# 0.1.15
* Properly treat all function encodes as tuple encodings
# 0.1.14
* Fix 0-length `type[]` encoding
# 0.1.13
* Drop dependency on exth crypto and move in functionality
# 0.1.12
* Fix `string` decoding to truncate on encountering NUL
* Fix some edge-cases in `tuple` encoding/decoding
# 0.1.11
* Add support for method ID calculation of all standard types
# 0.1.10
* Fix parsing of function names containing uppercase letters/digits/underscores
* Add support for `bytes<M>`
# 0.1.9
* Add support for parsing ABI specification documents (`.abi.json` files)
* Reimplement function signature parsing using a BNF grammar
* Fix potential stack overflow during encoding/decoding
# 0.1.8
* Fix ordering of elements in tuples
# 0.1.7
* Fix support for arrays of uint types
# 0.1.6
* Add public interface to raw function versions.
# 0.1.5
* Bugfix so that addresses are still left padded.
# 0.1.4
* Bugfix for tuples to properly handle tail pointer poisition.
# 0.1.3
* Bugfix for tuples to properly handle head/tail encoding
# 0.1.2
* Add support for tuples, fixed-length and variable length arrays