intrepidshape/elm-web3/3.0.0

Web3.Transaction

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 ->
        viewRejected

Confirmed 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.status

Related 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.

type Status
= Idle
| AwaitingSignature
| Submitted TxHash
| Confirming TxHash Int
| Confirmed Receipt
| RevertedOnChain Receipt
| Failed String
| Rejected

Transaction lifecycle status. Cannot be in an invalid state.

Confirmed and RevertedOnChain are both mined and both terminal; they differ on receipt.status, i.e. on whether the transaction did anything.

type Msg
= TxSubmitted (Maybe TxId) String
| TxConfirmation (Maybe TxId) String Int
| TxConfirmed (Maybe TxId) ReceiptJson
| TxFailed (Maybe TxId) String
| TxRejected (Maybe TxId)
| TxReset
| TxReceiptNotFound TxId

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.

type TxCmd
= RequestReceipt TxHash String

Commands to send to the JS transaction port.

type alias TxId =
String

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.

type alias Receipt =
{ txHash : TxHash
, blockNumber : Int
, gasUsed : String
, status : Bool
, contractAddress : Maybe Address
, logs : List EventLog
}

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.

type alias EventLog =
{ address : Address
, topics : List String
, data : String
, blockNumber : Int
, logIndex : Int
}

A single log entry emitted during a transaction.

Update transaction status from a port message.

Transitions are guarded to match the TLA+ GuardedNext specification:

  • Terminal states (Confirmed, RevertedOnChain, Failed, Rejected) never transition out, except via TxReset.
  • TxSubmitted is only accepted from AwaitingSignature.
  • TxConfirmation is only accepted from Submitted or Confirming.
  • TxConfirmed lands in Confirmed when the receipt reports success and in RevertedOnChain when it does not.
  • TxFailed is accepted from any non-terminal state.
  • TxRejected from AwaitingSignature -> Rejected; from Submitted/Confirming -> Failed.
  • TxReset from any terminal state -> Idle; from non-terminal states -> no-op.
  • TxReceiptNotFound is a no-op; the app can schedule another RequestReceipt.

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.

isTerminal : Status -> Bool

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.

isPending : Status -> Bool

Is this status still pending?

transactionConfirmations : Int -> Receipt -> Int

Number of confirmations for a confirmed transaction given the current block number.

msgId : Msg -> Maybe TxId

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.

encodeCmd : TxCmd -> Value

Encode a TxCmd for the JS port.

decoder : Decoder Msg

Decode transaction messages from JS port.

parseReceiptEvents : List (EventLog -> Maybe a) -> Receipt -> List a

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