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 2.0.0 found (5 same-name repos checked); no cached copy. Use another version.
Type-safe EVM blockchain interaction for Elm. Zero runtime exceptions.
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 for building EVM dapps in Elm. It provides:
The JS layer is ~500 lines with zero npm dependencies. No ethers. No viem. No web3.js. Just window.ethereum.request().
The Elm layer depends only on the Elm standard library (elm/core, elm/json, elm/http, elm/time). No third-party Elm packages.
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>Define your Elm ports:
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 msgWeb3.TypesOpaque types that prevent mixing up addresses, transaction hashes, and chain IDs.
address : String -> Maybe Address -- validates 0x + 40 hex chars
txHash : String -> Maybe TxHash -- validates 0x + 64 hex chars
chainId : Int -> ChainId
encodeBlockNumber : BlockNumber -> E.ValueYou can't pass a TxHash where an Address is expected. The compiler catches it at build time.
Web3.WalletWallet connection as an explicit state machine. The central type is State:
type State
= Disconnected
| ReadOnly -- rpcUrl configured, no wallet
| Connecting
| Connected { address : Address, chainId : ChainId }
| WrongChain { address : Address, chainId : ChainId } ChainId
| Error StringKey functions:
startConnect : State -> State -- Disconnected → Connecting
update : ChainId -> Msg -> State -> State -- drive state machine from port messages
connect, disconnect : WalletCmd
switchChain : ChainId -> WalletCmd
selectWallet : String -> WalletCmd -- EIP-6963 RDNS identifier
addChain : ChainConfig -> WalletCmd -- EIP-3085
watchAsset : { address, symbol, decimals, image } -> WalletCmd -- EIP-747
requestPermissions, getPermissions : WalletCmd -- EIP-2255
encode : WalletCmd -> E.Value
decoder : D.Decoder MsgWeb3.TransactionTransaction lifecycle as a state machine. The central type is Status:
type Status
= Idle
| AwaitingSignature
| Submitted TxHash
| Confirming TxHash Int -- hash + confirmation count
| Confirmed Receipt
| Failed String -- includes decoded revert reason when available
| RejectedKey functions:
update : Msg -> Status -> Status
isTerminal : Status -> Bool
isPending : Status -> Bool
transactionConfirmations : Int -> Receipt -> Int
encodeCmd : TxCmd -> E.Value -- encode RequestReceipt for port
decoder : D.Decoder Msg
parseReceiptEvents : List (EventLog -> Maybe a) -> Receipt -> List aWeb3.Contract.CallTyped read-only contract calls (eth_call).
readCall :
{ contract : Address, method : String, args : List E.Value
, decoder : D.Decoder a, id : String }
-> ReadCall a
withBlock : BlockNumber -> ReadCall a -> ReadCall a
withFrom : Address -> ReadCall a -> ReadCall a -- simulate write without broadcasting
encode : ReadCall a -> E.Value
responseDecoder : ReadCall a -> D.Decoder aWeb3.Contract.SendTyped write calls (eth_sendTransaction).
writeCall : { contract, method, args } -> WriteCall
payableCall : { contract, method, args, value : BigInt } -> WriteCall
withGasLimit : Int -> WriteCall -> WriteCall
encode : WriteCall -> E.Value
estimateGas : WriteCall -> E.Value
deployCall : { bytecode, args, gasLimit } -> E.Value
encodeRawSend : String -> E.Value -- broadcast pre-signed transactionWeb3.Contract.EventEvent subscriptions and historical log queries.
watchEvent : EventFilter -> E.Value
getLogs : GetLogsQuery -> E.Value
decoder : D.Decoder a -> D.Decoder (EventLog a)
logsDecoder : D.Decoder a -> D.Decoder (List (EventLog a))EventLog a carries data, contract, topics, blockNumber, txHash, and logIndex.
Web3.MulticallBatch multiple contract reads into one eth_call via the Multicall3 contract
(0xcA11bde05977b3631167028862bE2a173976CA11, deployed on all major networks).
callSpec : Address -> String -> List E.Value -> CallSpec
batch : String -> List CallSpec -> MulticallRequest
encode : MulticallRequest -> E.Value
responseDecoder : List (D.Decoder a) -> D.Decoder (List (Result String a))One RPC round-trip instead of N. Essential for dashboards.
Web3.SignEIP-712 typed data signing and EIP-191 personal signing.
typedData : { domain, types, primaryType, message } -> TypedData
encode : String -> Address -> TypedData -> E.Value -- send via port
personalSign : String -> Address -> String -> E.Value
type SignState = SignIdle | SignPending String | Signed String String
| SignFailed String String | SignRejected String
startSign : String -> SignState -> SignState
signUpdate : SignMsg -> SignState -> SignState
isSignTerminal : SignState -> Bool
signatureDecoder : D.Decoder StringWeb3.BalanceNative ETH/PLS balance queries. Each query carries a correlation ID so multiple queries can be in flight at once.
getBalance : Address -> String -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder Msg -- GotBalance id weiWeb3.BlockBlock number queries, block data fetching, and block number watching.
getBlockNumber : String -> Cmd
getBlock : BlockNumber -> String -> Cmd
watchBlockNumber : String -> Cmd -- polls every 4 seconds
getBlockTransactionCount : BlockNumber -> String -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder Msg -- GotBlockNumber | GotBlock | GotBlockTxCountWeb3.FeeGas price and EIP-1559 fee history.
getGasPrice : String -> Cmd
getFeeHistory : String -> Int -> Cmd -- id, blockCount
encode : Cmd -> E.Value
decoder : D.Decoder Msg -- GotGasPrice | GotFeeHistoryWeb3.QueryOn-chain read queries: transaction count (nonce), storage slots, contract bytecode, and transaction lookup.
getTxCount : Address -> String -> Cmd
getStorageAt : Address -> Int -> String -> Cmd
getCode : Address -> String -> Cmd
getTransaction : TxHash -> String -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder Msg -- GotTxCount | GotStorageAt | GotCode | GotTransaction | TransactionNotFoundWeb3.UnitsPure-Elm ETH and ERC-20 unit conversion. No port required.
formatEther : BigInt -> String -- Wei to ETH string, trailing zeros trimmed
parseEther : String -> Maybe BigInt -- ETH string to Wei
formatUnits : Int -> BigInt -> String -- custom decimals
parseUnits : Int -> String -> Maybe BigIntWeb3.ChainChain definitions for all major EVM networks:
ethereum, sepolia : Chain
pulsechain, pulsechainTestnet : Chain
bsc, polygon, arbitrum, optimism, base : Chain
avalanche, zksync, fantom, gnosis, linea, scroll : Chain
custom : { chainId, name, rpcUrl, blockExplorer, nativeCurrency } -> Chain
chainId : Chain -> T.ChainId
name, rpcUrl, blockExplorer : Chain -> StringWeb3.BigIntArbitrary-precision integers for uint256 and int256 values. No external dependencies — base-10^7 digit representation, safe within Elm's 53-bit integer range.
fromInt : Int -> BigInt
fromString : String -> Maybe BigInt
fromIntString : String -> Maybe BigInt
fromHexString : String -> Maybe BigInt -- 0x-prefixed
toString : BigInt -> String
add, sub, mul : BigInt -> BigInt -> BigInt
div, mod : BigInt -> BigInt -> Maybe BigInt
compare : BigInt -> BigInt -> Order
gt, gte, lt, lte, eq : BigInt -> BigInt -> Bool
zero : BigInt
isZero : BigInt -> Bool
decoder : D.Decoder BigIntWeb3.Abi.Encode / Web3.Abi.DecodeHelpers for encoding contract call parameters and decoding return values.
Encoders: address, uint256, int256, bool, string, bytes, bytes32, bytesN, list, tuple2, tuple3
Decoders: address, uint256, int256, bool, string, bytes32, uint8, uint16, uint32, uint64, uint128
Hex-slot decoders (for decoding raw ABI-encoded hex return values without a JS ABI library):
hexSlot, uint256Slot, addressSlot, boolSlot, stringSlot, listSlot, tuple2Hex, tuple3Hex
Revert reason extraction:
decodeRevertReason : String -> Maybe String -- hex data -> human-readable stringWeb3.CryptoKeccak256 hashing via port (JS side uses the built-in implementation).
keccak256 : String -> String -> Cmd -- message, id
encode : Cmd -> E.Value
decoder : D.Decoder Msg -- GotKeccak256 id hashtype alias Model =
{ wallet : Wallet.State
, availableWallets : List Wallet.WalletProvider
}
type Msg
= ConnectWallet
| SelectWallet String
| Web3Response D.Value
expectedChain : T.ChainId
expectedChain = Web3.Chain.chainId Web3.Chain.pulsechain
update msg model =
case msg of
ConnectWallet ->
( { model | wallet = Wallet.startConnect model.wallet }
, Ports.web3Cmd (Wallet.encode Wallet.connect)
)
SelectWallet rdns ->
( model
, Ports.web3Cmd (Wallet.encode (Wallet.selectWallet rdns))
)
Web3Response value ->
case D.decodeValue Wallet.decoder value of
Ok walletMsg ->
let
newWallet =
Wallet.update expectedChain walletMsg model.wallet
wallets =
case walletMsg of
Wallet.WalletsDiscovered providers ->
providers
_ ->
model.availableWallets
in
( { model | wallet = newWallet, availableWallets = wallets }
, Cmd.none
)
Err _ ->
( model, Cmd.none )
view model =
case model.wallet of
Wallet.Disconnected ->
button [ onClick ConnectWallet ] [ text "Connect Wallet" ]
Wallet.Connecting ->
text "Connecting..."
Wallet.Connected info ->
text ("Connected: " ++ T.addressToString info.address)
Wallet.WrongChain _ expectedId ->
button
[ onClick (Web3Response (Wallet.encode (Wallet.switchChain expectedId) |> always D.value)) ]
[ text "Switch to PulseChain" ]
Wallet.ReadOnly ->
text "Read-only mode"
Wallet.Error err ->
text ("Error: " ++ err)import Web3.Contract.Send as Send
import Web3.Transaction as Tx
type Msg
= Buy
| TxResponse D.Value
buyCall : T.Address -> T.Wei -> Send.WriteCall
buyCall router value =
Send.payableCall
{ contract = router
, method = "buy(uint256)"
, args = [ Encode.uint256 minTokensOut ]
, value = value
}
update msg model =
case msg of
Buy ->
case Wallet.getAddress model.wallet of
Just _ ->
( { model | tx = Tx.AwaitingSignature }
, Ports.web3Cmd (Send.encode (buyCall routerAddress weiAmount))
)
Nothing ->
( model, Cmd.none )
TxResponse value ->
case D.decodeValue Tx.decoder value of
Ok txMsg ->
( { model | tx = Tx.update txMsg model.tx }, Cmd.none )
Err _ ->
( model, Cmd.none )
viewTx model =
case model.tx of
Tx.Idle -> text ""
Tx.AwaitingSignature -> text "Sign in wallet..."
Tx.Submitted h -> text ("Pending: " ++ T.txHashToString h)
Tx.Confirming h n -> text (String.fromInt n ++ " confirmations")
Tx.Confirmed r -> text "Confirmed!"
Tx.Failed err -> text ("Failed: " ++ err)
Tx.Rejected -> text "Rejected"import Web3.Contract.Call as Call
import Web3.Multicall as Multicall
import Web3.Abi.Encode as Encode
import Web3.Abi.Decode as Decode
-- Single read
balanceOfCall : T.Address -> T.Address -> Call.ReadCall T.Wei
balanceOfCall token holder =
Call.readCall
{ contract = token
, method = "balanceOf(address)"
, args = [ Encode.address holder ]
, decoder = Decode.uint256
, id = "balance-" ++ T.addressToString holder
}
-- Batch read (one RPC call)
batchBalances : T.Address -> List T.Address -> Multicall.MulticallRequest
batchBalances token holders =
Multicall.batch "all-balances"
(List.map
(\holder ->
Multicall.callSpec token "balanceOf(address)" [ Encode.address holder ]
)
holders
)import Web3.Abi.Encode as E
-- function transfer(address to, uint256 amount) returns (bool)
transferArgs : T.Address -> BigInt -> List Json.Encode.Value
transferArgs to amount =
[ E.address to
, E.uint256 amount
]
-- function multicall((address target, bytes callData)[] calls)
multicallArgs : List ( T.Address, String ) -> List Json.Encode.Value
multicallArgs calls =
[ E.list
(E.tuple2 E.address E.bytes)
calls
]import Web3.Sign as Sign
import Dict
permitRequest : T.Address -> T.Address -> BigInt -> Int -> Sign.TypedData
permitRequest owner spender value nonce =
Sign.typedData
{ domain =
{ name = Just "MyToken"
, version = Just "1"
, chainId = Just 369
, verifyingContract = Just tokenAddress
, salt = Nothing
}
, types =
Dict.fromList
[ ( "Permit"
, [ { name = "owner", typeName = "address" }
, { name = "spender", typeName = "address" }
, { name = "value", typeName = "uint256" }
, { name = "nonce", typeName = "uint256" }
, { name = "deadline", typeName = "uint256" }
]
)
]
, primaryType = "Permit"
, message =
Json.Encode.object
[ ( "owner", Json.Encode.string (T.addressToString owner) )
, ( "spender", Json.Encode.string (T.addressToString spender) )
, ( "value", Json.Encode.string (BigInt.toString value) )
, ( "nonce", Json.Encode.int nonce )
, ( "deadline", Json.Encode.int 9999999999 )
]
}
-- Send: web3Cmd (Sign.encode "permit-1" signerAddress (permitRequest ...))
-- Receive: case D.decodeValue Sign.signatureDecoder incoming of Ok sig -> ...import Web3.Chain as Chain
-- Switch to a known chain
switchToPulseChain : Cmd msg
switchToPulseChain =
Ports.web3Cmd
(Wallet.encode (Wallet.switchChain (Chain.chainId Chain.pulsechain)))
-- Add a custom chain to the wallet
addCustomChain : Cmd msg
addCustomChain =
Ports.web3Cmd
(Wallet.encode
(Wallet.addChain
{ chainId = 369
, chainName = "PulseChain"
, rpcUrls = [ "https://rpc.pulsechain.com" ]
, nativeCurrency = { name = "Pulse", symbol = "PLS", decimals = 18 }
, blockExplorerUrls = [ "https://scan.pulsechain.com" ]
}
)
)The proofs/ directory contains machine-checked Lean 4 proofs and TLA+ model-checked specifications. All proofs use only core Lean 4 — no Mathlib.
Fully proved (zero sorry):
Address.lean — soundness, injectivity, and roundtrip for address/addressToStringTxHash.lean — same three properties for txHashHexString.lean — same three properties for hexStringWalletCodec.lean — encode/decode roundtrip, injectivity, partial inverse, and tag separation for WalletCmdBigInt.lean — nine arithmetic theorems (natNormalize, natAdd, natMulSmall, natAddSmall, shiftLeft, natSub, parseUnsigned, and more)AbiCodec.lean — bytes32 and address codec soundness/completeness/roundtripRevertReason.lean — six theorems covering hex parsing, UTF-8 roundtrip, and selector/length guardsTLA+ model-checked:
WalletSpec.tla — wallet state invariants and no-deadlock livenessTransactionSpec.tla — terminal states stay terminal, confirmation count is monotonicSee proofs/COVERAGE.md for the full coverage map.
bun codegen/generate.ts \
out/MyContract.sol/MyContract.json \
Generated.MyContract \
src/Generated/MyContract.elmGenerates typed Elm modules from Solidity ABI JSON. Each function becomes a typed encoder; each event becomes a typed decoder. No magic strings in your app code.
| Chain | ID | Constructor |
|---|---|---|
| Ethereum | 1 | Chain.ethereum |
| Sepolia | 11155111 | Chain.sepolia |
| PulseChain | 369 | Chain.pulsechain |
| PulseChain Testnet | 943 | Chain.pulsechainTestnet |
| BNB Smart Chain | 56 | Chain.bsc |
| Polygon | 137 | Chain.polygon |
| Arbitrum One | 42161 | Chain.arbitrum |
| Optimism | 10 | Chain.optimism |
| Base | 8453 | Chain.base |
| Avalanche C-Chain | 43114 | Chain.avalanche |
| zkSync Era | 324 | Chain.zksync |
| Fantom | 250 | Chain.fantom |
| Gnosis | 100 | Chain.gnosis |
| Linea | 59144 | Chain.linea |
| Scroll | 534352 | Chain.scroll |
| Any EVM chain | custom | Chain.custom |
cmditch/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