Unavailable. Nobody can download this version any more, with elm or with the Lamdera compiler: github.com/quietgarden/elm-web3 is gone (deleted or private); Software Heritage not asked yet (this run's --swh-calls were used up); no fork with tag 1.0.0 found (5 same-name repos checked); no cached copy. Use another version.
Type-safe EVM blockchain interaction for Elm.
80% of your frontend code gets zero runtime exceptions. Not fewer. Zero. The Elm compiler guarantees it.
The remaining 20% is a ~500-line JavaScript port bridge that catches every error and routes it back to Elm as a typed message. Your app never white-screens.
A package that lets Elm applications interact with EVM blockchains (Ethereum, PulseChain, etc.) through the browser wallet. It provides:
The JS layer is ~500 lines with zero dependencies. No ethers. No viem. No web3.js. Just window.ethereum.request().
elm install quietgarden/elm-web3Then copy js/elm-web3-ports.js into your project and load it after your compiled Elm JS:
<script src="elm.js"></script>
<script src="elm-web3-ports.js"></script>
<script>
var app = Elm.Main.init({ node: document.getElementById('app') });
setupPorts(app);
</script>port module Ports exposing (..)
import Json.Encode as E
import Json.Decode as D
port web3Cmd : E.Value -> Cmd msg
port web3Sub : (D.Value -> msg) -> Sub msgimport Web3.Wallet as Wallet
import Web3.Transaction as Tx
import Web3.Types as T
type alias Model =
{ wallet : Wallet.State
, tx : Tx.Status
}
type Msg
= ConnectWallet
| Web3Response D.Value
| BuyTokens
update msg model =
case msg of
ConnectWallet ->
( { model | wallet = Wallet.Connecting }
, Ports.web3Cmd (Wallet.encode Wallet.connect)
)
Web3Response value ->
case D.decodeValue Wallet.decoder value of
Ok walletMsg ->
( { model | wallet = Wallet.update expectedChain walletMsg model.wallet }
, Cmd.none
)
Err _ ->
-- Try transaction decoder, etc.
( model, Cmd.none )
BuyTokens ->
case Wallet.getAddress model.wallet of
Just addr ->
-- Compiler won't let you get here without a connected wallet
( { model | tx = Tx.AwaitingSignature }
, Ports.web3Cmd (buyCall addr)
)
Nothing ->
-- Can't buy without wallet. This branch is explicit.
( model, Cmd.none )See the Install section above. That's it. The wallet connection, chain switching, transaction signing, confirmation polling, and error handling all flow through typed Elm messages.
Every line of JavaScript can throw at runtime. An unhandled promise rejection in a wallet hook white-screens your app. A null reference in transaction state crashes the UI mid-sign. Users see blank pages and lose trust.
You mitigate this with discipline, testing, and error boundaries. But you can never prove it won't happen.
The compiler checks every code path. If your app compiles, it runs. There is no undefined is not a function. There is no unhandled null. Every wallet state (disconnected, connecting, connected, wrong chain) is a variant you must match. Every transaction state (idle, signing, pending, confirmed, failed, rejected) forces you to handle it.
This isn't a library feature. It's a language property. The Elm compiler physically cannot generate JavaScript that throws a runtime exception.
The bridge between Elm's guarantees and the browser wallet API:
| Layer | Lines | Runtime exceptions |
|---|---|---|
| Elm modules | 2,105 | Impossible |
| Your app (Elm) | varies | Impossible |
| Generated contract bindings (Elm) | varies | Impossible |
| JS port bridge | 508 | Caught and typed |
Web3.TypesOpaque types that prevent mixing up addresses, transaction hashes, and chain IDs:
address : String -> Maybe Address -- validates 0x + 40 hex
txHash : String -> Maybe TxHash -- validates 0x + 64 hex
chainId : Int -> ChainIdYou can't accidentally pass a TxHash where an Address is expected. The compiler catches it.
Web3.WalletWallet connection as an explicit state machine:
type State
= Disconnected
| Connecting
| Connected { address : Address, chainId : ChainId }
| WrongChain { address : Address, chainId : ChainId } ChainId
| Error StringEvery state is a variant. Pattern matching forces exhaustive handling. There is no "wallet is connected but address is null" state.
Includes EIP-6963 multi-wallet discovery — when multiple wallets are installed (MetaMask + Rabby), the user picks which one.
Web3.TransactionTransaction lifecycle as a state machine:
type Status
= Idle
| AwaitingSignature
| Submitted TxHash
| Confirming TxHash Int -- hash + confirmation count
| Confirmed Receipt
| Failed String -- includes decoded revert reason when available
| Rejected -- user rejected in walletYour UI must handle every state. The compiler enforces this. Confirmed receipts include parsed event logs.
Web3.Contract.CallTyped read-only contract calls:
readCall :
{ contract : Address
, method : String
, args : List E.Value
, decoder : D.Decoder a
, id : String
}
-> ReadCall aThe return type is parameterized. Your decoder is checked at compile time. The id field lets you match responses when multiple calls are in flight.
Web3.Contract.SendTyped write calls with gas estimation:
-- Non-payable
writeCall : { contract : Address, method : String, args : List E.Value } -> WriteCall
-- Payable (sends value)
payableCall : { contract : Address, method : String, args : List E.Value, value : BigInt } -> WriteCall
-- Estimate gas before sending
estimateGas : WriteCall -> E.ValueWeb3.Contract.EventEvent subscriptions and historical log queries:
-- Watch live events
watchEvent : EventFilter -> E.Value
-- Query past events
getLogs : GetLogsQuery -> E.Value
-- Decode event logs
decoder : D.Decoder a -> D.Decoder (EventLog a)Web3.MulticallBatch multiple read calls into a single RPC request via the Multicall3 contract:
multicall : List (ReadCall a) -> E.ValueOne RPC call instead of ten. Every dapp with a dashboard needs this.
Web3.SignEIP-712 typed data signing for permits, gasless approvals, and off-chain order books:
signTypedData : TypedData -> E.ValueWeb3.ChainChain definitions:
pulsechain : Chain -- chain 369
ethereum : Chain -- chain 1
sepolia : Chain -- chain 11155111
custom : { ... } -> ChainWeb3.BigIntArbitrary-precision integers for uint256 and int256 values. Implemented natively with no external dependencies — base-10^7 digit representation, safe within Elm's 53-bit integer range. All EVM integers cross the port boundary as decimal strings and are parsed into BigInt on the Elm side.
Web3.Abi.Encode / Web3.Abi.DecodeHelpers for encoding contract call parameters and decoding return values. Includes decodeRevertReason for extracting human-readable error messages from failed transactions.
The package includes an ABI-to-Elm code generator. Given a Foundry/Hardhat ABI JSON file, it outputs a typed Elm module:
bun codegen/generate.ts \
out/MyContract.sol/MyContract.json \
Generated.MyContract \
src/Generated/MyContract.elmFor each contract function:
For each event:
-- Generated from: function buy(uint256 minTokensOut) payable returns (uint256)
type alias BuyParams =
{ minTokensOut : String -- uint256 as string (BigInt)
, value : String -- wei to send
}
encodeBuy : BuyParams -> E.Value
encodeBuy params =
E.object
[ ( "method", E.string "buy(uint256)" )
, ( "args", E.list identity [ E.string params.minTokensOut ] )
, ( "value", E.string params.value )
]
decodeBuyReturn : D.Decoder String
decodeBuyReturn =
D.index 0 D.stringNo magic strings in your app code. Every contract method is a typed function.
bun codegen/generate-all.ts \
--artifacts out/ \
--output src/Generated/js/elm-web3-ports.js — ~500 lines, zero dependencies.
It handles:
eth_call) and writes (eth_sendTransaction)eth_estimateGas)eth_signTypedData_v4)eth_getLogs)Every call is wrapped in try/catch. Errors become typed messages ({ tag: 'failed', error: '...' }) that Elm decodes into Failed String or WalletError String.
The ABI encoding (function selectors + parameter encoding) is done in pure JS with a minimal keccak256 implementation. No external dependencies.
The proofs/ directory contains machine-checked Lean 4 proofs and TLA+ model-checked specifications. All proofs use only core Lean 4 — no Mathlib.
sorry)Address.lean — three theorems about Web3.Types.address:
address s = Just a implies a satisfies the validity predicate (starts 0x, length 42, lowercase hex body)addressToString a = addressToString b → a = baddress (addressToString a) = Just a — re-parsing a valid address always succeedsTxHash.lean proves the same three properties for txHash (length 66, 64-char hex body). HexString.lean proves them for hexString (no length constraint, mixed-case hex).
WalletCodec.lean — four theorems about the WalletCmd port encoder/decoder:
∀ cmd, decode(encode(cmd)) = Some cmdencode c₁ = encode c₂ → c₁ = c₂cmd implies re-encoding cmd decodes back to itselfBigInt.lean — nine arithmetic theorems via a natVal : List Int → Int semantic interpretation:
natNormalize_val: stripping trailing zeros preserves valuenatAdd_val: natVal (natAdd a b) = natVal a + natVal bnatMulSmall_val: natVal (natMulSmall ds k) = k * natVal dsnatAddSmall_val: natVal (natAddSmall ds v) = natVal ds + vshiftLeft_val: natVal (shiftLeft n ds) = bigBase^n * natVal dsnatSub_val: natVal a ≥ natVal b → natVal (natSub a b) = natVal a - natVal bparseUnsigned_step: the decimal accumulation step 10 * n + digit is faithfully modelledAbiCodec.lean — bytes32 decoder soundness, completeness, and full characterization; address codec roundtrip.
RevertReason.lean — six theorems: hexDigitVal range (0 ≤ result < 16), hexToInt correctness, hexToBytes correspondence, UTF-8/ASCII round-trip for printable characters, 0x08c379a0 selector guard, and ABI-encoded length guard.
TLA+ (proofs/tla/):
WalletSpec.tla — wallet state invariants and no-deadlock liveness, model-checked by TLCTransactionSpec.tla — terminal states stay terminal, transition guards match code, confirmation count is monotonicFour theorems carry sorry with written proof strategies:
natMul_val — full multiplication via indexed foldl (~40 lines; index coordination across recursive case)natCompare_spec — lexicographic comparison reflects numeric order (~80 lines)natDivMod_spec — quotient/remainder algorithm correctness (~150 lines)fromString_toString_roundtrip — decimal encoding is an isomorphism (~200 lines); uint256_codec_roundtrip and decodeRevertReason_correct depend on thisThese cover the hardest algorithmic proofs. Every stated theorem is true — the sorry markers are genuine TODOs, not hidden bugs. The proof sketches describe correct strategies.
See proofs/COVERAGE.md for the full coverage map.
Any EVM chain with a browser wallet (MetaMask, Rabby, etc.):
Web3.Chain.customcmditch/elm-ethereum (2018) — web3.js era, Task-based, no longer maintainedpurescript-web3 — similar concept in PureScript, stronger types but smaller ecosystemwindow.ethereum) with zero external JS dependenciesMIT