All notable changes to this project will be documented in this file.
[Unreleased]
[0.1.0] — 2026-04-30
First active release under the cartouche namespace. Ports the signet codebase under the Cartouche module tree with Elixir 1.20 compatibility, a published-on-hex ABI dep (hieroglyph), and a cleaned-up dialyzer baseline.
Fixed
ROADMAP Phase 1 closeout —
Cartouche.Wei.to_wei/1spec narrowedinteger() | {integer(), :wei | :gwei}) :: integer()→non_neg_integer() | {non_neg_integer(), :wei | :gwei}) :: non_neg_integer(), with matchingamount >= 0guards on all three clauses. Wei is a discrete count by domain — every internal caller (Cartouche.Transactionconstructors at:67/:70/:384/:386/:389,Cartouche.RPCfee-suggestion fallbacks at:1569/:1584/:1591) already passes non-negative values; the spec was simply loose. Negative inputs now raiseFunctionClauseErrorat the contract boundary instead of silently propagating a nonsense wei count downstream. Newdescribe "spec boundaries (Phase 1.2)"block intest/wei_test.exspins the zero boundary in all three clauses, the large-value identity round-trip, the:gweimultiplier, and the negative-input rejection — grounded as ExUnit assertions perfeedback_doctests_not_substitute_for_tests.md.Cartouche.Weicoverage stays at 100%; full suite green; dialyzer clean onwei.ex. Closes ROADMAP Tasks 7+8+9, completing Phase 1 (1.1RecoveryBit, 1.3Signermfa(), 1.4Hexreturns already shipped). The downstream onchain@dialyzer {:no_match}strip acrossOnchain.Hex/ ABI / ERC / ENS / Multicall callers becomes load-bearing once cartouche0.1.xships (Task 6).Cartouche.Trace.deserialize/1no longer raisesProtocol.UndefinedErroron RPC payloads with missing orniltraceAddress. TheEnum.map(params["traceAddress"], &decode_address_or_number/1)line attrace.ex:423(tracked underTODO(Task 55)since Tasks 51 + 52) now routes through adecode_trace_address/1helper that raisesArgumentError, "missing traceAddress in trace_transaction result element"when the key is absent ornil, and maps the list otherwise. The audit attached to ROADMAP Task 55 (Codex consultation 2026-04-26 reviewing OpenEthereum + Infuratrace_transactionschemas) confirmedtraceAddressis mandatory at the wire — the root call shows[], not omission — so the right contract is "loud reject", not soft|| [](which would silently coerce corrupt nodes' output). Newdescribe "deserialize/1 — traceAddress absent/nil (Task 55)"block intest/trace_test.exspins both the missing-key and explicit-nil shapes viaassert_raise ArgumentError, ~r/missing traceAddress/.Cartouche.Tracecoverage stays at 100%; full suite green; dialyzer clean ontrace.ex. Closes ROADMAP Task 55.Cartouche.RPC.get_block_by_number/2and 9 companion JSON-RPC callsites no longer send raw integers as block parameters. The@specdeclarednon_neg_integer() | String.t()and the doctest atlib/cartouche/rpc.ex:450exercisedget_block_by_number(55)— butsend_rpc/3Jason.encode!s params verbatim, so the integer hit the wire as[55, false]and any real Ethereum node responded{:error, %{code: -32602, message: "Invalid params"}}(verified 2026-04-28 against a live mainnet reth tunnel). The mock test client attest/support/client.exaccepted any value, masking the bug for the entire history of the upstream codebase. Fix shape: a new publicCartouche.Hex.encode_quantity/1produces JSON-RPC-spec-compliant lowercase quantity strings (0→"0x0", no leading zeros, lowercase hex digits) — distinct fromencode_short_hex/1, which is uppercase by design for transaction-field encoding. A privatenormalize_block_param/1helper insideCartouche.RPC(integer →Cartouche.Hex.encode_quantity/1, binary passthrough) is applied at all 10 callsites that forward a block tag:get_block_by_number/2,get_nonce/2,call_trx/2,estimate_gas/2,get_code/2,get_balance/2,get_transaction_count/2,trace_call/3,trace_call_many/2, anddebug_trace_call/2. The existing integer doctest at:450continues to pass — the mock returns the same fixture for any block param — and now accurately documents real-node behavior. New ExUnit coverage: adescribe "encode_quantity/1"block intest/hex_test.exs(zero, small int, single-digit, large multi-byte block number, all-letter hex, negative-rejection) percritical-rules.md"Doctests Are Documentation, Not Tests"; and adescribe "block-param wire encoding"block intest/rpc_test.exsusing aCaptureClienttest double that delegates toCartouche.Test.Clientwhilesending the decoded JSON-RPC body back to the test pid — pins the integer-in →"0x37"-on-wire wiring through the publicget_block_by_number/2path and proves companion normalization viaget_balance/2+get_nonce/2opt-reader assertions. Pre-mutation coverage gate:Cartouche.Hexalready 100% (Phase 1.4 closeout);Cartouche.RPC91.55% module-level, but the touched functions are 100% covered — the 18 uncovered RPC lines are entirely in untouched error-path code (Solidity Panic decoding, prepare_trx, execute_trx, fee history, trace_revert) and tracked separately. README's RPC example block restored to chaineth_block_number/0 → get_block_by_number(int)(the original honest shape that motivated the bug discovery) alongside the"latest"form. Closes ROADMAP Task 60.ROADMAP Phase 1.4 —
Cartouche.Hexreturn-shape spec audit closeout. The five spec corrections (decode_hex/1,from_hex/1,from_hex!/1,decode_hex_number/1, plus the privatedecode_hex_/1) had previously shipped as a drive-by under commit8d4bc18("doctor, credo fixes", 2026-04-26):{:ok, t()} | :error→{:ok, t()} | :invalid_hexon the four soft-return functions andt() -> String.t()→t() -> t()onfrom_hex!/1, matching the actual runtime return shape of the private helper. This commit grounds the corrections so a future onchain audit doesn't have to reverse-engineer them: (a) failure-path doctest added tofrom_hex/1(parity withdecode_hex/1— both now show:invalid_hexon bad input); (b) raise-path doctest added tofrom_hex!/1(parity withdecode_hex!/1— both now show theCartouche.Hex.InvalidHexexception); (c) newdescribe "spec boundaries (Phase 1.4)"block intest/hex_test.exspinning all four corrected return shapes as ExUnit assertions per the project rule that doctests are documentation, not load-bearing tests for spec contracts. Bundled with the closeout: adescribe "deep_encode_binaries/1"block covering the four previously-uncovered lines of the@doc falserecursive helper, liftingCartouche.Hexcoverage 94.29% → 100% and clearing the ≥95% critical-tier gate prophylactically for any future Hex mutation. No runtime behavior change. Dialyzer outcome: 0hex.exwarnings (unchanged — the spec corrections were already absorbed); totalinvalid_contractcount remains 8 (the 6 insleuth.exare tracked under Task 54, the 2 intyped.exunder Tasks 19+20). Onchain's@dialyzer {:no_match}strip onOnchain.Hexand the cascading ABI / ERC / ENS / Multicall callers becomes load-bearing once cartouche0.1.xships (Task 6). Closes ROADMAP Tasks 10+11+12+13.Cartouche.Transaction.V1r/s/v storage unified to integers throughout, settling a Schrödinger spec that produced three different runtime shapes for the same field.V1.t()declaredr: integer(), s: integer(), v: integer(), butadd_signature/2(lib/cartouche/transaction.ex:170) stored r/s as 32-byte binaries (matched out of<<r::binary-size(32), s::binary-size(32), v::binary>>), whileV1.new/7andV1.decode/1stored them as integers. The mismatch was latent until a public-API path exercised it:V1.decode → recover_signerraisedArgumentErroron any signed legacy RLP transaction, becauseget_signature/1's second clause built the signature with<<r::binary-size(32), …>>anddecode/1had stored r/s via:binary.decode_unsigned/1.add_signature/2now:binary.decode_unsigned/1s the incoming r/s segments so they match the spec and the other constructors;get_signature/1's second clause now rebuilds the signature with<<r::big-256, s::big-256, v_enc::binary>>— byte-equivalent to the prior 32-byte binary form, soadd_signature → get_signatureround-trips bit-for-bit and the chain-side recovery doctest still passes unchanged. Side benefit:add_signature(...) |> encode()now produces canonical RLP for r/s (leading zeros stripped) — the prior binary form was technically non-canonical on the wire. Bundled hardening from the staged-review pass:V1.decode/1now guardsbyte_size(r) <= 32 and byte_size(s) <= 32on the 9-element RLP shape, returning{:error, "invalid legacy transaction"}on adversarial input with >32-byte r/s — without the guard, the new<<r::big-256>>reconstruction inget_signature/1would raiseArgumentErroron r ≥ 2^256 reachable throughdecode → recover_signer. Theadd_signaturedoctest intransaction.exwas updated to showr: 1, s: 2post-call (the only doctest whose expected output changed). New ExUnitdescribe "V1 (Task 53)"block intest/transaction_test.exscovers the malformed-RLP fallback indecode/1(closing the coverage gate), the fullbuild_signed_trx → encode → decode → recover_signerround-trip (the previously-broken path; failed pre-fix exactly as the type system predicted), the empty-sig boundary (r:0, s:0→recover_signerreports missing signature), and the adversarial 33-byte r/s fixture asserting{:error, "invalid legacy transaction"}— four cases grounded as ExUnit assertions percritical-rules.md"Doctests Are Documentation, Not Tests". Coverage onCartouche.Transaction.V193.33% → 100%; dialyzer dropstransaction.ex:169 invalid_contract(count 9 → 8); 748/748 tests green. Closes ROADMAP Task 53; Phase 0.4 is fully cleared and Task 6 (mix hex.publish) is the only remaining0.1.0item.Cartouche.Solana.Transaction.deserialize/1andsign_partial/2were specced as well-formed but raised on malformed input.deserialize/1(specced{:ok, t()} | {:error, term()}) raisedFunctionClauseErroron empty / truncated compact-u16 prefixes (bothdecode_compact_u16_acc/3clauses required<<byte, rest::binary>>),FunctionClauseErroron sub-3-byte message headers (the onlydeserialize_message/1head-clause required three header bytes), andMatchErroron truncated instruction bodies (three raw<<...>> = restmatches inread_instructions/3body). Hardened with a privatesafe_decode_compact_u16/1returning{:ok, val, rest} | {:error, :truncated_compact_u16}(publicdecode_compact_u16/1tuple contract preserved); aread_instructions/3rewrite usingwith+ aread_size_prefixed/2guard helper that mirrors the existingread_signatures/3/read_pubkeys/3shape, plus a fallback(_, _, _) -> {:error, :insufficient_instruction_data}clause; and adeserialize_message(_) -> {:error, :invalid_message_header}fallback.sign_partial/2evaluatedEnum.map(0..-1, …)on a 0-signer message and produced two zero-filled placeholder signatures instead of the empty list its[<<_::512>>]field type implies; the range now carries an explicit//1step (0..(n - 1)//1), giving the empty range whenn == 0(also silences the Elixir 1.20 deprecation on the unstepped form). Both behaviours grounded by new ExUnitdescribeblocks intest/solana/transaction_test.exscovering<<>>, truncation at every section boundary (compact-u16, signatures, header, pubkeys, blockhash, instruction header / accounts / data), and the synthetic zero-signer boundary — 10 new test cases, allassert {:error, _}/ boundary-shape assertions percritical-rules.md"NEVER HIDE TEST FAILURES". Coverage onCartouche.Solana.Transaction95.56% → 98.97%; full suite green; dialyzer clean on the touched file. Closes ROADMAP Tasks 56 and 57.Cartouche.Trace.t().trace_addressandCartouche.TraceCall.t().tracewere typed as singular values but always built as lists at runtime —trace_addressviaEnum.map(params["traceAddress"], …)andtraceviaCartouche.Trace.deserialize_many/1(whose@specreturns[t()]). Consumer code pattern-matching on the documented singular shape would have raisedMatchErroron every realtrace_transaction/trace_callManyresponse. Specs narrowed to[<<_::160>> | integer()]and[Cartouche.Trace.t()]respectively. Behavior-preserving — the runtime always returned lists; only the contract documentation was wrong. Dialyzer drops thetrace.ex:412andtrace_call.ex:124invalid_contractwarnings (invalid_contractcount 11 → 9). New focused ExUnit blocks intest/trace_test.exsandtest/trace_call_test.exsground the list shapes against edge cases (mixed-element union[42, <<_::160>>], empty list,deserialize_many/1round-trip) — the existing doctests cover the happy path as documentation but read as prose, so the new assertions pin the spec shape against boundary conditions doctests don't exercise. Closes ROADMAP Tasks 51 + 52. A latent crash on missing/niltraceAddress(theEnum.map(nil, _)path attrace.ex:423) is tracked underTODO(Task 55)and filed as ROADMAP Task 55 for follow-up — the test suite intentionally does not pin the broken behavior withassert_raisepercritical-rules.md"NEVER HIDE TEST FAILURES".
Documentation
- README's Solana example block modernized to use the
Cartouche.Solana.SignerGenServer (Signer.address/0,Signer.sign/1) instead of raw seed handling, paralleling the Ethereum example's reliance onCartouche.Signer. AddsCartouche.Solana.Transaction.serialize_message/1to the example for explicit message-byte handoff to the signer. Old shape used an undefinedfee_payer_seedvariable inherited from upstream — the new example is runnable end-to-end against a configured signer. A trailing prose note links offline (raw-seed) signing viaTransaction.sign/2and sponsored-transaction signing viasign_partial/2+add_signature/3. README-only; no library code change. - Doctor-driven typespec + docstring sweep across
lib/cartouche/**. Adds.doctor.exs(min_*_coverage: 100,failed: false, ignoresMix.Tasks.Cartouche.Genandlib/cartouche/contract/— Doctor's source-level AST walker counts the generator'sdef unquote(name)(args)literals insidequote doblocks as defs of the Mix task itself, producing 23 false-positive missing-doc warnings; BEAM introspection confirms onlyrun/1is exported). Adds missing@specand/or@doc/@doc falseentries on 18 modules:Cartouche,Cartouche.{Application, Assembly, Block, Erc20, Filter, Hash, Hex, Keys, OpenChain, Recover, RPC, Signer, Sleuth, Transaction, Typed, VM}, andCartouche.Solana.Programs. Behavior-preserving — pure documentation/typespec coverage.
Fixed
Cartouche.Signer.start_link/1andCartouche.Signer.sign_direct/4specs: replace the Erlangmfa()BIF (which is{module(), atom(), arity :: non_neg_integer()}) with{module(), atom(), [any()]}to match what the impl actually receives — an args list, not an arity. Fixes the dialyzersigner.ex:141 invalid_contractwarning that ROADMAP Phase 1.3 tracked, and forestalls the same regression that was about to ship via the newstart_linkspec. Bundled with the typespec sweep above per the touched-files credo rule.Cartouche.get_contract_address/1spec: widen the input type fromCartouche.contract()(which excludes hex strings —address() :: <<_::160>>) tobinary() | atom()to match the impl, which routes anyis_binary/1value throughCartouche.Hex.decode_hex_input!/1(handles both 20-byte raw binaries and"0x..."hex strings, per the function's own doctest).Cartouche.Transaction.V1.add_signature/2spec: return type tightened from the%__MODULE__{}literal tot()for consistency withV2.add_signature/2,4.Cartouche.Transaction.V2.add_signature/2(binary form): tighten the second-arg spec from loosebinary()to<<_::512, _::_*8>>to match the pattern (r::32, s::32, v::binary— at least 64 bytes), aligning with V1's spec.
Security
Cartouche.DebugTrace.StructLog.deserialize/1no longer mints atoms from RPC input. The previous implementation calledString.to_atom(params["op"])for every entry of aneth_debug_traceCallstructLogsarray — thousands of opcodes per trace — which let a buggy or compromised RPC node permanently grow the BEAM atom table (default cap ~1M; exhaustion crashes the VM). The hardened path defines a closed compile-time map of every Cancun-era EVM opcode (single-name +PUSH1..32/DUP1..16/SWAP1..16/LOG0..4ranged families) and resolves opcodes viaMap.fetch/2. The whitelist also carries two alias pairs verified against go-ethereum'score/vm/opcodes.gomaster:KECCAK256/SHA3for opcode 0x20 (SHA3covers pre-1.8 Geth and some non-Geth nodes), andDIFFICULTY/PREVRANDAOfor opcode 0x44 (current Geth still emits"DIFFICULTY"— the rename TODO is still open in go-ethereum master, so withoutDIFFICULTYthe whitelist would crash on every modern Geth trace;PREVRANDAOis forward-compat for clients that already emit the post-Merge name). Unknown strings raiseArgumentErrorcarrying the offending value, surfacing future-EVM additions as visible failures instead of silent corruption. New regression coverage intest/debug_trace_test.exs(per-family boundary tests + nil/invalid rejections) and a non-async atom-table-stability test intest/debug_trace_atom_safety_test.exs(1000-iteration loop with novel-looking opcode strings; asserts the atom_count delta is ≪ iterations after a warmup pass that lets ExUnit/Logger machinery settle). Sobelow'sDOS.StringToAtomfinding atlib/cartouche/debug_trace.ex:71(fingerprint4F16CCA) is removed from.sobelow-skips. Audit of remainingString.to_atomcallsites inlib/cartouche/sleuth.ex(thequery_by/3atom-deriving pair plusname_keyword/1) is tracked as ROADMAP Task 48 — those are bounded by compile-time atoms but warrant the sameString.to_existing_atomtreatment after raising Sleuth coverage to the 95% gate. The Mix-task callsite atlib/mix/cartouche.gen.ex:817is dev-time only and stays in.sobelow-skips.
Changed
- Breaking: rename
Cartouche.Hex.HexError→Cartouche.Hex.InvalidHexandCartouche.VM.VmError→Cartouche.VM.InvalidVmfor consistency with the codebase'sInvalid*exception strategy (InvalidAssembly,InvalidCode,InvalidOpcode,InvalidFileError). Public callers thatrescue Cartouche.Hex.HexError/rescue Cartouche.VM.VmErrormust update. Settles cleanup.md B7 in favor of consistency over the no-breakage option, since cartouche is pre-1.x. Affects alldecode_hex!/decode_address!/decode_word!/decode_sized!/decode_hex_number!/encode_addressraise paths andCartouche.VM.exec/3's error-rescue path. - Tooling: add
.credo.exs(strict, withTagTODO: [exit_status: 2]to keep TODOs visible per project policy andRefactor.FunctionArity max_arity: 12to permitTransaction.V2's EIP-1559-mirroring constructors),.sobelow-conf(exit: "Low"), and.sobelow-skips(fingerprints for two runtimeString.to_atomcallsites inCartouche.Sleuth.query_by/3bounded by compile-time-known atoms (full audit tracked as Task 48), one build-timeString.to_atomcallsite in the generator's contract-name binding (lib/mix/cartouche.gen.ex:817), and threeFile.*traversal flags in the generator's IO writers). The previously-suppressed runtimeString.to_atominCartouche.DebugTrace.StructLog.deserialize/1is now hardened (see Security above) rather than suppressed. - Cross-module helper-extraction refactor to bring
mix credo --strictto zero issues onlib/cartouche/{assembly,hex,open_chain,rpc,sleuth,solana/token,solana/transaction,typed,vm}.ex. Extractions:Assembly.resolve_jump_ptr/2;OpenChain.decode_response/1+pick_signature/3clauses;RPC.error_matches?/2+classify_decoded_error/2(Panic dispatch table) +build_revert_data/2+decode_revert_error/2+decode_result/4+log_decode_error/4+resolve_gas_limit/4+maybe_trace_revert/6+do_estimate_and_verify/2+apply_trace/6;Sleuth.{with_indexed_name,fallback_name,to_named_pair,name_keyword,obvious_results}/*;Solana.Token.{summarize_balance,accumulate_token_amount,maybe_include_token_2022}/*;Solana.Transaction.{merge_instruction_accounts,merge_account_meta}/2;Typed.type_fields_match?/2;VM.Operations.{do_sign_extend,extend_with_sign}/*,VM.{handle_static_call_result,pad_or_truncate_return}/*, ~25do_*opcode-handler helpers, and puresafe_floor_div/safe_rem/safe_addmod/safe_mulmod/int_lt/int_gt/int_eq/int_is_zerocallbacks for theunsigned_op*/signed_op*dispatch. Behavior-preserving —mix test.json --quietgreen pre-commit. Two# credo:disable-for-next-line Refactor.CyclomaticComplexityretained onAssembly.show_opcode/1andVM.run_single_op/3(EVM opcode-dispatch tables — splitting would add indirection without reducing real complexity), oneReadability.FunctionNamesdisable onBase58.sigil_B58/2(Elixir requires sigil names start with uppercase), and twocredo:disable-for-this-file Readability.{MaxLineLength,FunctionNames}on test-support files for generated bytestring fixtures and JSON-RPC method-name parity respectively.
Fixed
Cartouche.Sleuth.try_applynow usesreraise __STACKTRACE__instead ofraiseinside the rescue, preserving the original exception's stacktrace alongside the descriptiveRuntimeErrormessage about the missingbytecode/0(or other required) function. Settles cleanup.md B8.Cartouche.OpenChain.lookup_*decode_response/1now returns{:error, "unexpected response shape: ..."}for any successfully-decoded JSON envelope that doesn't match the OpenChain{ok: true, result: …}/{ok: false, error: …}contract, instead of raisingCaseClauseError. Pre-existing gap surfaced when the inline case was extracted into the helper.Cartouche.OpenChain.lookup/3raise_on_multiple: truepath:Enum.join(found_signatures, ",")was iterating a list of{sig, name}tuples and crashingProtocol.UndefinedErrorinstead of returning{:error, "Multiple matching signatures: ..."}. Fixed atlib/cartouche/open_chain.ex:200withEnum.map_join(found_signatures, ",", fn {_, name} -> name end)so the error message now lists the actual signature names. Surfaced during the Task 43 coverage push while writing the multi-result error-path test; percritical-rules.md"NEVER HIDE TEST FAILURES" the test asserts the corrected return shape rather than pinning the broken raise.
Tests
- Pre-credo coverage push (ROADMAP Task 43) on the six modules slated for credo-strict cleanup, so the refactor session can rename / restructure / silence flags safely. New ExUnit blocks added to
test/assembly_test.exs,test/receipt_test.exs,test/open_chain_test.exs,test/transaction_test.exs,test/sleuth_test.exs,test/solana/signer_test.exs. Highlights: data-drivenshow_opcode/1table covering every named arm inCartouche.Assembly(PUSH/DUP/SWAP/INVALID tuple coverage + show-only atoms:blobhash/:blobbasefee/:log) pluscompile/13–7-operand cases,transform_jumpsmissing-jump-dest path, full PUSH1–32 / DUP1–16 / SWAP1–16 disassembly, per-clauseopcode_size/1, and exception-struct defaults; contract-creationCartouche.Receiptwithto: nil/contractAddresspopulated and log shapes for empty / 2-topic / 4-topic data;Cartouche.OpenChainTest.TestClientextended with magic-byte signature dispatch (0xee000001–05) routing took=false/ non-JSON-body / transport-error / multi-result / empty-result paths in one async-safe inline client, pluslookup_error/lookup_error_and_valuesshort-binary clauses andSignatures.deserialize/1filter behaviour; V2 transaction roundtrip throughCartouche.Test.Signer.start_signer/0+ signer recovery, plusV2.new/9chain-id-nil fallback,V2.new/12nil-fee passthrough, ABI-tuple vs raw-binarybuild_trx_v2call-data, callback-short-circuit onbuild_signed_trx_v2, andV2.decode/1malformed-RLP rejection;Cartouche.Sleuth.try_applyrescue exercising the descriptiveRuntimeErrorwhen the contract module is missingbytecode/0; explicit cache-hit test forCartouche.Solana.Signerusing:sys.get_state/1to confirm the:addresskey is populated after the firstaddress/1call. No new test files; no new test dependencies (compile-time module substitution viaconfig/test.exsalready in place — no Mox/Bypass).
Changed
- Generator gates
exec_vm_*emission on real bytecode.Mix.Tasks.Cartouche.Gennow treatsnil, blank strings,"0x", and"0x" <> whitespaceas missing bytecode (newblank_bytecode?/1predicate inlib/mix/cartouche.gen.ex), and the:puredispatch branch now requireshas_bytecodebefore emittingexec_vm_fn/exec_vm_raw_fn. Without both, the generator was emittingdef bytecode, do: hex!("0x")(compile-time<<>>) plus a:purebranch that always emittedexec_vm_*— producing 762exec_vm_*functions inCartouche.Contract.IConsole(Hardhat console.log interface, no on-chain bytecode) that calledCartouche.VM.exec_call(<<>>, ...)and always raisedVmError. Dialyzer flagged each asno_return, cascading to 1534 of 1626 total warnings. New regression tests intest/mix/cartouche_gen_test.exscover the four blank-bytecode shapes plus the working real-bytecode path. DropsCartouche.Contract.IConsole.{bytecode/0, deployed_bytecode/0, exec_vm_*}from the generated module (RPC-sideencode_*/call_*/execute_*family preserved); regenerated file is 18705 lines (was 28084). Bundled with the bug fix: the four pre-existing credo issues inlib/mix/cartouche.gen.ex(L153 nesting inrename_dups/1, L170 cyclomatic inget_encode_calls/2, L247 cyclomatic inencode_function_call/3, L337 nesting) are resolved by extractingaccumulate_named_abi/2+dedup_named_abi/5+maybe_rename_dup_fn/5(rename-dups),merge_encode_call_result/2(encode-calls reducer), and ~16build_*_fn/1helpers +select_emitted_fns/3+ supporting argument-spec helpers (encode-function dispatch). Generator output AST is byte-identical to pre-refactor fori_console.ex(verified via the newcartouche_gen_test.exssuite). One additional post-process:strip_zero_arity_def_parens/1rewritesdef name()→def nameon emitted defs (the macro source must keep the parens —def unquote(name)()is the canonical AST shape; without parens,unquote(:foo)produces a literal-atom AST and won't compile), eliminating ~382ParenthesesOnZeroArityDefsflags from generatedi_console.exwithout manual annotation.String.to_atom/1callsites in 6 generator helpers carry# sobelow_skip ["DOS.StringToAtom"]annotations (build-time codegen, not runtime input). - Delete
Cartouche.Utilgrab-bag module. Helpers redistributed into five focused modules:Cartouche.Address.from_public_key/1(renamed fromUtil.get_eth_address/1),Cartouche.Chain.parse_id/1+ chain registry (renamed fromUtil.parse_chain_id/1),Cartouche.Wei.to_wei/1,Cartouche.HTTP.normalize_finch_result/1, andCartouche.RecoveryBit(promoted from the nestedCartouche.Util.RecoveryBitsubmodule). The five hex helpersdecode_hex_input!/1,encode_bytes/2,pad/2,nibbles/1, andchecksum_address/1move toCartouche.Hex. The seven@deprecateddecode/encode aliases (decode_hex/1,decode_hex!/1,decode_sized_hex!/2,decode_word!/1,decode_address!/1,decode_hex_number!/1,encode_hex/2) and thekeccak/1defdelegate are removed — modern equivalents already exist inCartouche.Hex/Cartouche.Hash.nil_map/2is inlined as a module-local private helper inCartouche.TraceandCartouche.Trace.Action(its only consumers).
Fixed
Cartouche.RecoveryBit.normalize/2andnormalize_signature/2specs: literal atom:no_returnreplaced with theno_return()type (ROADMAP Phase 1.1). Dialyzer silently accepts unknown atoms in unions, so this was semantically meaningless; now matches the documented raise behaviour.
Changed
- Reset
mix.exsversion from the inherited signet pin1.6.1to0.1.0-devahead of the first hex publish under thecartouchenamespace (ROADMAP Phase 0, Task 1). - Swap the
:abipath dep (path: "../abi", override: true) for the published hex package{:hieroglyph, "~> 1.0", override: true}. ZenHive'sabifork is now on hex.pm ashieroglyph 1.0.0; hex package name ishieroglyphbut the Elixir module namespace remainsABI, so no callsite changes. Unblocksmix hex.publishfor cartouche, which rejects path/git deps (ROADMAP Phase 0, Task 6). - Update
mix.exs:packagefor the publish cut:maintainers: ["ZenHive"](was["Geoffrey Hayes"]— attribution preserved inLICENSEand in[0.0.1]below); droptest/supportfrom:files(test helpers aren't part of the public surface), addCHANGELOG*; addCHANGELOG.mdtodocs[:extras]so hexdocs renders the release history; add aChangelogentry topackage[:links].
Fixed
- Pin bitstring size variables in binary matches across
Cartouche.Solana.Transaction.read_instructions,Cartouche.Assembly.disassemble_opcode, andCartouche.VM.{Memory,Operations}/Cartouche.VM.static_callfor Elixir 1.20 compatibility. Behaviour-preserving; resolves allvariable "X" is accessed inside size(...) ... must precede it with the pin operatorwarnings under 1.20-rc.4 (cleanup.md C1). - Pin bitstring size variable in
Cartouche.VmTestHelpers.word/2(test/support/vm_test_helpers.ex:11) — missed in the initial C1 sweep; same Elixir 1.20 compat fix. - Remove leading-underscore on
expectedinCartouche.Solana.PDATest"wrong bump"test (test/solana/pda_test.exs:137) — variable is used inside thematch?/2guard at line 143, so the underscore was misleading and fired an Elixir 1.20 warning. - Cut dialyzer noise floor from 6,620 to 1,626 warnings by fixing typespecs in the upstream
:abilibrary. Root cause was thatABI.encode/2,ABI.decode/2-3,ABI.decode_event/3-4,ABI.TypeEncoder.encode/2, andABI.TypeDecoder.decode_raw/3lacked@specdeclarations, andABI.FunctionSelector.t()declaredreturns: type(singular) while the runtime and ABI's own doctests usereturns: [argument_type]. Dialyzer's inferred success typing forABI.encode/2collapsed the struct branch tofunction: nil, types: []only, so every populated selector at every cartouche callsite was flagged aswill never return, cascading throughlib/cartouche/contract/i_console.ex. Fixed in thezenhive/abifork and published to hex.pm ashieroglyph 1.0.0(hex package name only;ABImodule namespace preserved). cartouche consumes the patched library via{:hieroglyph, "~> 1.0", override: true}(see### Changedabove). ABI typespec fixes will be upstreamed via PR topoanetwork/ex_abi. (cleanup.md A1+A2; residual cascade tracked under follow-up A1b.) - Restore
Cartouche.Signer@moduledoc(was@moduledoc falsewith module-level prose stuck in a@docthat collided withstart_link/1's@doc). Eliminates the last compile warning under Elixir 1.20-rc.4 and aligns with cleanup.md's documentation policy (avoid@moduledoc false). - Replace
@moduledoc falsewith descriptive@moduledocon six submodules whoset()types are referenced from outer public specs:Cartouche.VM.Input,Cartouche.VM.Context,Cartouche.VM.ExecutionResult,Cartouche.Trace.Action,Cartouche.Receipt.Log,Cartouche.DebugTrace.StructLog. Eliminates all "documentation references type X but the module is hidden" warnings frommix docs; clean docs build for the publish cut (ROADMAP Task 36). - IAL/markdown collision in four
Cartouche.Hexdoctest blocks (decode_hex/1,from_hex/1,decode_hex_number/1,encode_hex_result/1): bumped doctest source indent from 4 to 6 spaces so the heredoc-stripped output reaches the 4-space code-block threshold instead of being parsed as prose lines starting with{. - Drop
/aritysuffix on private-function references in this CHANGELOG (Cartouche.Solana.Transaction.read_instructions,Cartouche.VM.static_call) so ex_doc no longer attempts to auto-link non-public functions and emit broken-link warnings.
Documentation
- Correct
DEV.mdSleuth regeneration command — the canonical ABI source is./priv/Sleuth.json(vendored), not the previously documented../sleuth/out/Sleuth.sol/Sleuth.jsonexternal path.
[0.0.1] — 2026-04-22
Initial placeholder release. Claims the cartouche hex namespace under ZenHive ownership.
Active development (fork of hayesgm/signet) lands in 0.1.x.
Attribution
Cartouche is an attributed fork of hayesgm/signet, originally authored by Geoffrey Hayes at Compound Labs, Inc. (2022). The upstream MIT license is preserved alongside the ZenHive copyright in LICENSE.