Wallet connection state machine.
The wallet is modeled as an explicit state -- you can't accidentally
call a contract without a connected wallet because the compiler
won't give you an Address from a Disconnected state.
case model.wallet of
Connected info ->
-- info.address : T.Address -- only available here
buy info.address amount
WrongChain _ _ ->
-- wallet is connected but on the wrong chain
button [ onClick SwitchChain ] [ text "Switch chain" ]
ReadOnly ->
-- rpcUrl configured but no wallet; reads work, writes will fail
viewReadOnlyBanner
_ ->
showConnectButtonEIP-6963 multi-wallet discovery -- listen for WalletsDiscovered on the
port and present the list to the user; call selectWallet rdns when they pick.
Typical connection flow:
RequestId (an incrementing counter
you own), call startConnect requestId on state, send connect requestId
via port. Do this unconditionally, even if already Connecting -- a
second click (or picking a different wallet mid-prompt) is meant to
supersede the in-flight attempt, not be swallowed as a no-op.WalletsDiscovered providers arrives -> show picker if providers is non-empty.RequestId the same way, send
selectWallet requestId rdns via port.WalletConnected (Just requestId) addr chainId arrives -> update checks
the id against the active Connecting request (dropping it silently if a
newer attempt has since superseded it) and transitions to Connected or
WrongChain.WalletConnectRejected/WalletConnectFailed arrives instead, update
returns to Disconnected/Error respectively (again only if the id still
matches). Arm a timeout (e.g. 30s via Process.sleep) when entering
Connecting; on fire, call timeoutConnect requestId -- a no-op if the
request already resolved.WrongChain -> send switchChain expectedChain via port.For native balance queries, use Web3.Balance. For adding chains, use addChain with
a ChainConfig record and follow up with switchChain.
The account details of a live wallet connection: the connected address
and the chainId it is currently on. Carried by both Connected and
WrongChain -- the latter still knows who is connected, just on the
wrong network.
Identifies a single connect attempt so a stale response or timeout from a
superseded attempt (e.g. the user clicked Connect, gave up, and clicked again)
can never clobber a newer one. Callers own the counter (increment on every
startConnect call) -- this module only compares ids, it never generates them.
Why a connect attempt failed to resolve into Connected/WrongChain.
Does not cover explicit rejection (WalletConnectRejected) or a
request that's still pending elsewhere (WalletConnectPending) --
those are distinct, expected outcomes, not failures.
Messages from the JS wallet port.
WalletConnected's Maybe RequestId is Nothing only for a silent,
non-prompting reconnect on page load (no request was ever "in flight" to
match against); every user-initiated connect carries Just the id passed to
connect/selectWallet.
Commands to send to JS via port.
A wallet provider discovered via EIP-6963.
Configuration for adding a new chain to the wallet (EIP-3085).
Transition to Connecting requestId before sending the connect port command.
Call this when the user clicks the connect button -- mint a fresh RequestId
(an incrementing counter you own) -- then send connect via the port:
( { model
| wallet = Wallet.startConnect requestId model.wallet
, nextConnectId = requestId + 1
}
, web3Cmd (Wallet.encode (Wallet.connect requestId))
)Valid transitions: Disconnected -> Connecting, Error _ -> Connecting, and
-- deliberately -- Connecting _ -> Connecting with the NEW id. That last one
is what lets overlapping attempts work elegantly: the user can click Connect
again (or pick a different wallet mid-prompt) while one is already in
flight, and the new attempt simply supersedes the old one. No app-side
"already connecting, ignore this click" guard is needed -- update already
drops any response tagged with a RequestId that no longer matches the
CURRENT Connecting id, so a stale response from the superseded attempt
(if it ever resolves at all) is safely a no-op. Connected/WrongChain/
ReadOnly remain no-ops here, same as before -- there's no "already
connected, reconnect" use case this module needs to support.
Time out a Connecting requestId back to Disconnected if it's still
the active request -- a no-op if the request already resolved (into
Connected, WrongChain, Error, or back to Disconnected) or has already
been superseded by a newer one. Call this from a Process.sleep-armed
timeout started alongside startConnect, mirroring this codebase's existing
tx-timeout-watchdog pattern.
True while a connect attempt is in flight -- use this to drive a
"Connecting..." spinner/disabled visual state. Not meant for gating the click
handler itself: startConnect deliberately allows a fresh attempt to
supersede one already in flight (see its doc comment), so the click handler
should call startConnect unconditionally rather than checking this first.
The RequestId of the in-flight connect attempt, if any.
Command to request wallet connection. Pass the same RequestId given to
startConnect so the eventual response can be matched back to this attempt.
Command to request wallet disconnection.
Command to switch chain.
Command to select a specific wallet by its RDNS identifier (EIP-6963).
Pass the same RequestId given to startConnect.
selectWallet requestId "io.metamask"Request wallet_addEthereumChain (EIP-3085) to add a new network.
addChain
{ chainId = 369
, chainName = "PulseChain"
, rpcUrls = [ "https://rpc.pulsechain.com" ]
, nativeCurrency = { name = "Pulse", symbol = "PLS", decimals = 18 }
, blockExplorerUrls = [ "https://scan.pulsechain.com" ]
}Command to add a token to the wallet UI (EIP-747).
Command to request wallet permissions (EIP-2255).
Command to get current wallet permissions (EIP-2255).
True if the wallet is in the Connected state.
True if in ReadOnly mode (rpcUrl present, no wallet). Reads work; writes will fail.
Extract the connected address, if any.
Extract the connected chain ID, if any.
Wallet connection state. Every possible state is explicit.
Disconnected-- no wallet detected, no rpcUrl configuredReadOnly-- rpcUrl is present but no wallet; reads work, writes will failConnecting-- wallet connection in progressConnected-- wallet connected on the expected chainWrongChain-- wallet connected but on the wrong chainError-- unrecoverable error