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.1 found (5 same-name repos checked); no cached copy. Use another version.
EVM blockchain interaction for Elm applications.
Provides typed wrappers around the browser wallet API (window.ethereum), a small JavaScript port bridge, and pure-Elm utilities for working with blockchain data. The goal is to make wallet connection, transaction submission, and contract interaction feel like ordinary Elm — pattern-matched state machines, opaque types, and explicit update functions rather than callbacks or effects hidden in subscriptions.
elm install quietgarden/elm-web3Copy js/elm-web3-ports.js into your project and wire it up after your compiled Elm bundle:
<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>Declare two ports in your Elm app:
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 msgThe package communicates with the browser over two ports — one outgoing (web3Cmd) and one incoming (web3Sub). Everything in the JS bridge is wrapped in try/catch so errors arrive as typed Msg values rather than uncaught exceptions.
State machines are central to the design. Wallet.State, Transaction.Status, and Sign.SignState are all explicit union types — the compiler will tell you if you forget a branch.
The JS bridge has no npm dependencies. It calls window.ethereum.request() directly.
Web3.TypesOpaque types for the primitives that appear everywhere:
address : String -> Maybe Address -- "0x" + 40 hex chars
txHash : String -> Maybe TxHash -- "0x" + 64 hex chars
chainId : Int -> ChainId
addressToString : Address -> String
txHashToString : TxHash -> String
chainIdToInt : ChainId -> IntPassing a TxHash where an Address is expected is a compile error.
Web3.WalletWallet state as an explicit state machine:
type State
= Disconnected
| ReadOnly -- rpcUrl set, no wallet
| Connecting
| Connected { address : Address, chainId : ChainId }
| WrongChain { address : Address, chainId : ChainId } ChainId
| Error StringstartConnect : State -> State
update : ChainId -> Msg -> State -> State
connect, disconnect : WalletCmd
switchChain : ChainId -> WalletCmd
selectWallet : String -> WalletCmd -- EIP-6963 RDNS
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 MsgA minimal wallet flow:
type alias Model =
{ wallet : Wallet.State
, providers : List Wallet.WalletProvider
}
type Msg
= ConnectWallet
| PickWallet String
| Web3Msg D.Value
expectedChain : T.ChainId
expectedChain =
Chain.chainId Chain.pulsechain
update msg model =
case msg of
ConnectWallet ->
( { model | wallet = Wallet.startConnect model.wallet }
, Ports.web3Cmd (Wallet.encode Wallet.connect)
)
PickWallet rdns ->
( model
, Ports.web3Cmd (Wallet.encode (Wallet.selectWallet rdns))
)
Web3Msg raw ->
case D.decodeValue Wallet.decoder raw of
Ok walletMsg ->
let
newWallet =
Wallet.update expectedChain walletMsg model.wallet
providers =
case walletMsg of
Wallet.WalletsDiscovered ps -> ps
_ -> model.providers
in
( { model | wallet = newWallet, providers = providers }
, Cmd.none
)
Err _ ->
( model, Cmd.none )
view model =
case model.wallet of
Wallet.Disconnected ->
button [ onClick ConnectWallet ] [ text "Connect" ]
Wallet.Connecting ->
text "Connecting…"
Wallet.Connected info ->
text (T.addressToString info.address)
Wallet.WrongChain _ expected ->
button [ onClick (PickWallet "") ] [ text "Switch network" ]
Wallet.ReadOnly ->
text "Read-only"
Wallet.Error err ->
text ("Error: " ++ err)Web3.TransactionTransaction lifecycle:
type Status
= Idle
| AwaitingSignature
| Submitted TxHash
| Confirming TxHash Int -- confirmation count
| Confirmed Receipt
| Failed String -- includes decoded revert reason when available
| Rejectedupdate : Msg -> Status -> Status
isPending : Status -> Bool
isTerminal : Status -> Bool
encodeCmd : TxCmd -> E.Value -- encode RequestReceipt for port
decoder : D.Decoder Msg
parseReceiptEvents : List (EventLog -> Maybe a) -> Receipt -> List aSending a transaction:
import Web3.Contract.Send as Send
import Web3.Transaction as Tx
type Msg
= Submit
| TxMsg D.Value
update msg model =
case msg of
Submit ->
( { model | tx = Tx.AwaitingSignature }
, Ports.web3Cmd
(Send.encode
(Send.payableCall
{ contract = routerAddress
, method = "buy(uint256)"
, args = [ Encode.uint256 minOut ]
, value = weiAmount
}
)
)
)
TxMsg raw ->
case D.decodeValue Tx.decoder raw of
Ok txMsg ->
( { model | tx = Tx.update txMsg model.tx }, Cmd.none )
Err _ ->
( model, Cmd.none )
viewTx status =
case status of
Tx.Idle -> text ""
Tx.AwaitingSignature -> text "Sign in your wallet…"
Tx.Submitted h -> text ("Pending: " ++ T.txHashToString h)
Tx.Confirming h n -> text (String.fromInt n ++ " confirmations")
Tx.Confirmed _ -> text "Confirmed"
Tx.Failed err -> text ("Failed: " ++ err)
Tx.Rejected -> text "Rejected"Web3.Contract.CallRead-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
encode : ReadCall a -> E.Value
responseDecoder : ReadCall a -> D.Decoder aWeb3.Contract.SendWrite calls and deployments:
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.ValueWeb3.Contract.EventEvent subscriptions and 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 reads into one eth_call using the Multicall3 contract
(0xcA11bde05977b3631167028862bE2a173976CA11):
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))Web3.SignEIP-712 typed data signing and EIP-191 personal signing:
typedData : { domain, types, primaryType, message } -> TypedData
encode : String -> Address -> TypedData -> E.Value
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 StringEIP-712 permit example:
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 )
]
}Web3.BalanceNative balance queries with correlation IDs:
getBalance : Address -> String -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder Msg -- GotBalance id weiWeb3.BlockBlock queries and polling:
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 MsgWeb3.FeeGas price and EIP-1559 fee history:
getGasPrice : String -> Cmd
getFeeHistory : String -> Int -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder MsgWeb3.QueryMiscellaneous on-chain reads:
getTxCount : Address -> String -> Cmd
getStorageAt : Address -> Int -> String -> Cmd
getCode : Address -> String -> Cmd
getTransaction : TxHash -> String -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder MsgWeb3.UnitsPure-Elm unit conversion, no port needed:
formatEther : BigInt -> String
parseEther : String -> Maybe BigInt
formatUnits : Int -> BigInt -> String
parseUnits : Int -> String -> Maybe BigIntWeb3.ChainChain definitions:
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 : Chain -> String
rpcUrl : Chain -> String
blockExplorer : Chain -> String| 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 |
Web3.BigIntArbitrary-precision integers for uint256 and int256 values. Base-10⁷ digit representation, no external dependencies.
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.DecodeABI parameter helpers.
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 raw ABI hex without a JS ABI library):
hexSlot, uint256Slot, addressSlot, boolSlot, stringSlot, listSlot, tuple2Hex, tuple3Hex
Revert reason decoding:
decodeRevertReason : String -> Maybe StringWeb3.CryptoKeccak256 via port:
keccak256 : String -> String -> Cmd
encode : Cmd -> E.Value
decoder : D.Decoder MsgGenerates typed Elm modules from Solidity ABI JSON:
bun codegen/generate.ts \
out/MyContract.sol/MyContract.json \
Generated.MyContract \
src/Generated/MyContract.elmEach ABI function becomes a typed encoder; each event becomes a typed decoder.
The proofs/ directory contains Lean 4 proofs and TLA+ specifications.
Lean 4 (all proofs close without 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 (normalize, add, multiply, shift, subtract, parse)AbiCodec.lean — bytes32 and address codec soundness, completeness, and roundtripRevertReason.lean — six theorems covering hex parsing, UTF-8 roundtrip, and selector/length guardsTLA+ model-checked:
WalletSpec.tla — wallet state invariants, no-deadlock livenessTransactionSpec.tla — terminal states stay terminal, confirmation count is monotonicSee proofs/COVERAGE.md for the full coverage map. All proofs use only core Lean 4 — no Mathlib.
cmditch/elm-ethereum — web3.js era, Task-based, no longer maintainedpurescript-web3 — similar concept in PureScriptMIT © quietgarden