A translation table from the two JavaScript libraries most readers (human or
model) already know to the Cartouche calls that do the same job — and, in the
last column, where the semantics differ. Cartouche is not a port of either:
it is an idiomatic Elixir substrate that happens to cover the same ground. The
differences column is the part worth reading; the name mappings are the part
worth searching.
Verified against cartouche 0.9.1 (2026-09-16). ethers refers to v6, viem to 2.x.
ABI rows point into hieroglyph, Cartouche's ABI dependency, because that is
where the ABI.* modules live.
Read this first — four rules that are not in the table
Bytes, not hex strings. Every address, hash, signature, calldata and
return value is a raw binary (<<_::160>> for an address, <<_::256>> for
a word). Hex strings exist only at the JSON-RPC boundary and in your own
logs. Cartouche.Hex.from_hex!/1 on the way in, Cartouche.Hex.to_hex/1
on the way out — or use Cartouche.Hex and write ~h[0x…]. Passing a
"0x…" string where an address is expected does not error early; it
produces a 42-byte binary and fails somewhere downstream.
Cartouche.Signer.sign/3 is not signMessage. It keccaks the bytes it
is given and signs that digest — no EIP-191 prefix, and v is
EIP-155-encoded (chain_id * 2 + 35 + recid) by default, which can exceed
one byte. Cartouche.Signer.sign_message/3 is the wallet-compatible form —
it adds the prefix and fixes v to 27/28; use it wherever ethers or viem
would call signMessage.
A signer is a process, not a value.new Wallet(pk) becomes a supervised
Cartouche.Signer GenServer (started from config or
Cartouche.Signer.start_link/1), addressed by name or pid through the
signer: option. Cartouche.Signer.Curvy is the raw-key backend if you
need a function call without a process.
Results are {:ok, value} | {:error, reason} tuples; !-suffixed functions
raise. Options are trailing keyword lists. Integers are native arbitrary
precision — there is no BigNumber/bigint distinction anywhere.
input is Decimal or integer, never a string or float; the atom is :eth (:ether is rejected); a fraction finer than 1 wei raises — viem rounds, ethers throws
parseUnits("5", "gwei")
parseGwei("5")
Cartouche.Wei.to_wei({5, :gwei})
integer gwei only — there is no {Decimal, :gwei} clause
parseUnits(x, decimals)
parseUnits(x, decimals)
—
no arbitrary-decimals helper; multiply integers yourself
formatEther, formatUnits
formatEther, formatUnits
—
not in Cartouche; nothing renders wei back to a decimal string
a bigint amount
a bigint amount
an integer, or {n, :wei | :gwei | :eth} anywhere a fee or value is taken
the tagged tuple is accepted by every constructor and by execute_trx/3 options
expects domain / types / value keys — a wallet payload's message must be renamed; primaryType is not read (inferred from value's field names)
Transactions
Cartouche has one struct per envelope type and no field-based type inference:
Cartouche.Transaction.V1 (legacy), V_2930 (type 1), V2 (EIP-1559),
V3 (EIP-4844), V4 (EIP-7702).
ethers v6
viem
Cartouche
Difference
Transaction.from({ to, value, … })
prepareTransactionRequest
Cartouche.Transaction.V2.new(nonce, max_priority_fee, max_fee, gas_limit, to, value, data, access_list, chain_id)
positional constructor per type, not a request object; pick the struct yourself
V1/V2 have no sign/2 — the builder is the signing path; its :callback option runs between construction and signing
wallet.sendTransaction(tx)
sendTransaction / writeContract
Cartouche.RPC.execute_trx(to, calldata, opts)
fetches nonce, fees and gas, runs eth_call first (verify: true is the default), signs, broadcasts — simulateContract + writeContract in one call; Cartouche.RPC.prepare_trx/3 is the same without the broadcast
There is no provider or client value: every function takes ethereum_node: url
in its trailing opts (defaulting to config :cartouche, :ethereum_node). A
client struct is roadmap task 9017 in onchain.
ethers v6
viem
Cartouche
Difference
new JsonRpcProvider(url)
createPublicClient({ transport: http(url) })
—
ethereum_node: option per call, or application config
a supervised GenServer polling eth_getFilterChanges every check_delay ms (default 3000); delivers %Cartouche.Filter.Log{}messages to the listening pid — no callback