intrepidshape/elm-web3/3.0.0

Web3.Contract.Event

Historical contract event queries via eth_getLogs.

This module answers "what happened?" -- it fetches a bounded block range in one request and hands back the logs that matched. For "what is happening now?", use Web3.Subscription, which opens a long-lived eth_subscribe push over a WebSocket.

-- every Transfer this token emitted between block 1000 and head
Event.getLogs
    { contract = tokenAddress
    , fromBlock = T.BlockNum 1000
    , toBlock = T.Latest
    , topics = [ Just transferTopic ]
    }
    |> web3Cmd


-- ...and the reply, { tag: "logs", logs: [...] }
D.decodeValue (Event.logsDecoder D.string) portValue

Queries

type alias GetLogsQuery =
{ contract : Address
, fromBlock : BlockNumber
, toBlock : BlockNumber
, topics : List (Maybe String)
}

A bounded eth_getLogs query. topics is positional: index 0 is the event signature hash, indices 1-3 are the indexed parameters in declaration order, and Nothing at a position means "match anything".

Public RPC providers cap the block span they will serve (commonly 10k blocks, sometimes far less) and reject the whole request rather than truncating it. Page a wide range yourself.

getLogs : GetLogsQuery -> Value

Encode a getLogs query for the JS port. The reply arrives as { tag: "logs", logs: [...] } -- read it with logsDecoder.

The field names below are the ones the shim's getLogs handler reads (cmd.contract, cmd.fromBlock, cmd.toBlock, cmd.topics); the regression suite pins the exact key set.

Reading logs back

type alias EventLog a =
{ data : a
, contract : Address
, topics : List String
, blockNumber : Int
, txHash : TxHash
, logIndex : Int
}

One decoded log from a getLogs reply.

data is whatever your decoder makes of the log's non-indexed payload; the port sends it as a hex string, so D.string is the usual choice and the ABI decoding happens after.

There is deliberately no removed field here. The shim's logs reply drops the reorg flag, so any value this module reported would be a constant rather than a fact. The live path does carry it -- see Web3.Subscription.LogEvent.

logsDecoder : Decoder a -> Decoder (List (EventLog a))

Decode the { tag: "logs", logs: [...] } reply to getLogs with a decoder for each log's data.

Each entry uses the field names the shim's getLogs handler builds (contract, txHash), which are NOT the names on the live eventLog push (address, transactionHash). That asymmetry is the shim's, not this module's; decoding the live push is Web3.Subscription.eventDecoder's job.

Live subscriptions live elsewhere

This module used to also expose a watchEvent command. It emitted the watchEvent cmd tag with a payload the shim does not read -- the contract address under a contract key the handler never looks at, and no subscription id -- so the shim subscribed with address: undefined, which is a subscription to every log on the chain, keyed under undefined. Its log decoder read a contract field the eventLog reply has never had, so it could not have decoded an event even if a correct one had arrived.

Web3.Subscription already spoke that wire format correctly and offers nothing less: Subscription.logs |> atAddress |> withTopic |> open sid. It is now the single owner of the watchEvent cmd tag, which is what scripts/check-port-parity.ts enforces. Migrate with:

-- before (never worked)
Event.watchEvent
    { contract = tokenAddress, event = "Transfer", topics = [ Just transferTopic ] }


-- after
Subscription.logs
    |> Subscription.atAddress tokenAddress
    |> Subscription.withTopic 0 transferTopic
    |> Subscription.open (Subscription.subscriptionId "token:Transfer")

Note that event = "Transfer" had no counterpart: nothing on either side of the boundary hashed an event name into topic 0. Topic 0 is the keccak256 of the event signature and you must supply it -- see Web3.Crypto.keccak256.