Transaction lifecycle state machine.
Every transaction goes through explicit states. Pattern matching forces you to
handle every case -- no "transaction failed silently" bugs. The state machine
follows the TLA+ spec in proofs/tla/TransactionSpec.tla:
terminal states never transition out (except via TxReset),
and confirmation counts only increase.
case tx.status of
Idle ->
viewBuyButton
AwaitingSignature ->
viewSigningSpinner
Submitted hash ->
viewPendingWithHash hash
Confirming hash n ->
viewConfirming n
Confirmed receipt ->
viewSuccess receipt
RevertedOnChain receipt ->
viewRevertedOnChain receipt
Failed err ->
viewError err
Rejected ->
viewRejectedConfirmed means mined AND successful. A transaction that was mined with
receipt.status == false reverted on chain: it consumed the user's gas, it is
permanently on chain, and it did not do what they asked. That is
RevertedOnChain, not Confirmed. Before 3.0.0 both landed in
Confirmed and the example above rendered a revert as a success, which is
exactly the class of silent wrongness this library exists to make impossible.
RevertedOnChain is terminal, same as Confirmed.
Correlating replies. Tag a write with
Web3.Contract.Send.withId and every reply about
it -- submitted, confirmation, confirmed, failed -- carries that id
back. msgId reads it out, so two transactions in flight at once
are told apart by the compiler rather than by guesswork:
Web3Msg incoming ->
case Tx.msgId incoming of
Just id ->
( { model | txs = Dict.update id (Maybe.map (Tx.update incoming)) model.txs }
, Cmd.none
)
Nothing ->
( model, Cmd.none )To poll for a receipt after submission, encode a RequestReceipt command:
case tx.status of
Submitted hash ->
( model
, web3Cmd (Tx.encodeCmd (Tx.RequestReceipt hash "my-tx"))
)
_ ->
( model, Cmd.none )To reset a terminal transaction back to Idle:
Tx.update Tx.TxReset tx.statusRelated modules: Web3.Contract.Send to build and encode write calls;
Web3.Contract.Call to simulate writes before broadcasting;
Web3.Error for the typed failure taxonomy behind a failed reply.
Messages from the JS transaction port.
Every reply on the write path carries the correlation id of the write that
caused it, or Nothing when the write was sent without one (see
msgId). TxReceiptNotFound always has an id: it answers an
explicit RequestReceipt, which cannot be issued without one.
A correlation id for one write. You mint it, you attach it with
Web3.Contract.Send.withId, and the port echoes it on every reply about that
transaction. Any string will do as long as it is unique among the writes you
have in flight.
A mined transaction receipt, including emitted logs.
status is the EVM's own success flag: True for a transaction that ran to
completion, False for one that reverted after being mined. contractAddress
is Just only for a deployment -- it is the address the new contract landed
at, which is otherwise unrecoverable from the Elm side.
A single log entry emitted during a transaction.
Update transaction status from a port message.
Transitions are guarded to match the TLA+ GuardedNext specification:
The correlation id on an incoming message is NOT consulted here: routing a
message to the right transaction is the caller's job (see msgId),
and update only ever sees the status it was handed.
Is this status terminal (no more updates expected)?
RevertedOnChain is terminal for the same reason Confirmed is: the
transaction is mined and the chain has spoken. Nothing further will arrive
about it.
Number of confirmations for a confirmed transaction given the current block number.
The correlation id a port message was tagged with, if any.
This is what makes two transactions in flight at the same time separable: the
id you attached with Web3.Contract.Send.withId comes back on every reply
about that write, and nothing else in this module needs to know it exists.
TxReset is app-generated and never has one.
Apply a list of event decoders to the logs in a receipt, collecting all successfully decoded events.
type MyEvent
= Transfer { from : Address, to : Address, amount : BigInt }
| Approval { owner : Address, spender : Address, amount : BigInt }
decodeTransfer : EventLog -> Maybe MyEvent
decodeTransfer log =
-- match topic[0] to Transfer signature, then decode
...
events : List MyEvent
events =
parseReceiptEvents [ decodeTransfer, decodeApproval ] receipt
Transaction lifecycle status. Cannot be in an invalid state.
ConfirmedandRevertedOnChainare both mined and both terminal; they differ onreceipt.status, i.e. on whether the transaction did anything.