Current section
6 Versions
Jump to
Current section
6 Versions
Compare versions
14
files changed
+1180
additions
-307
deletions
| @@ -17,7 +17,7 @@ blockchain over websockets. | |
| 17 17 | 6. Automatically decode events when a corresponding ABI is available. |
| 18 18 | 7. Always pass the event to the handler, even when it cannot be decoded. |
| 19 19 | 8. Support starting of multiple listeners pointing to different websocket urls. |
| 20 | - 9. Support integration with `Singleton` for clustered environments. |
| 20 | + 9. Support integration with distributed supervisors for clustered environments. |
| 21 21 | |
| 22 22 | ## Installation |
| 23 23 | |
| @@ -26,7 +26,7 @@ The package can be installed by adding `w3ws` to your list of dependencies in `m | |
| 26 26 | ```elixir |
| 27 27 | def deps do |
| 28 28 | [ |
| 29 | - {:w3ws, "~> 0.1.0"} |
| 29 | + {:w3ws, "~> 0.2.0"} |
| 30 30 | ] |
| 31 31 | end |
| 32 32 | ``` |
| @@ -35,66 +35,70 @@ end | |
| 35 35 | |
| 36 36 | Documentation can be found at <https://hexdocs.pm/w3ws>. |
| 37 37 | |
| 38 | + ### Event Listener |
| 39 | + |
| 38 40 | Configure the listeners for your application: |
| 39 41 | |
| 40 42 | ```elixir |
| 41 43 | # in your config.exs |
| 42 | - config :my_app, W3WS, listeners: [%{ |
| 43 | - # the uri of the ethereum jsonrpc websocket server |
| 44 | - uri: "ws://localhost:8545", |
| 44 | + config :my_app, W3WS, listeners: [ |
| 45 | + [ |
| 46 | + # the uri of the ethereum jsonrpc websocket server |
| 47 | + uri: "ws://localhost:8545", |
| 45 48 | |
| 46 | - # enable block ping every 10 seconds. this will cause the listener to |
| 47 | - # fetch and log the current block every 10 seconds. the last fetched block |
| 48 | - # will be available from `Listener.get_block/1`. Defaults to `nil` which |
| 49 | - # disables block ping. |
| 50 | - block_ping: :timer.seconds(10), |
| 49 | + # enable block ping every 10 seconds. this will cause the listener to |
| 50 | + # fetch and log the current block every 10 seconds. the last fetched block |
| 51 | + # will be available from `Listener.get_block/1`. Defaults to `nil` which |
| 52 | + # disables block ping. |
| 53 | + block_ping: :timer.seconds(10), |
| 51 54 | |
| 52 | - # a helper setting for dealing with finicky local nodes (ie hardhat) where the |
| 53 | - # server stops sending subscription events after some time. setting this to |
| 54 | - # a number of milliseconds will cause the listener to unsubscribe and resubscribe |
| 55 | - # all configured subscriptions every `resubscribe` milliseconds. Defaults to `nil` |
| 56 | - # which disables resubscribing. |
| 57 | - # https://github.com/NomicFoundation/hardhat/issues/2053 |
| 58 | - resubscribe: :timer.minutes(5), |
| 55 | + # a helper setting for dealing with finicky local nodes (ie hardhat) where the |
| 56 | + # server stops sending subscription events after some time. setting this to |
| 57 | + # a number of milliseconds will cause the listener to unsubscribe and resubscribe |
| 58 | + # all configured subscriptions every `resubscribe` milliseconds. Defaults to `nil` |
| 59 | + # which disables resubscribing. |
| 60 | + # https://github.com/NomicFoundation/hardhat/issues/2053 |
| 61 | + resubscribe: :timer.minutes(5), |
| 59 62 | |
| 60 | - # subscriptions to setup on this websocket connection. each listener |
| 61 | - # can support many subscriptions and will call the corresponding handler |
| 62 | - # for each subscription event, using the provided subscription abi, if any, |
| 63 | - # to decode events for the subscription. |
| 64 | - subscriptions: [ |
| 65 | - %{ |
| 66 | - # one of `abi` or `abi_files` is necessary to decode events. |
| 67 | - # neither are required if you jsut want to listen for encoded events. |
| 68 | - abi: abi, # decoded json abi |
| 69 | - abi_files: ["path/to/abi.json"], # list of paths to abi json files |
| 63 | + # subscriptions to setup on this websocket connection. each listener |
| 64 | + # can support many subscriptions and will call the corresponding handler |
| 65 | + # for each subscription event, using the provided subscription abi, if any, |
| 66 | + # to decode events for the subscription. |
| 67 | + subscriptions: [ |
| 68 | + [ |
| 69 | + # one of `abi` or `abi_files` is necessary to decode events. |
| 70 | + # neither are required if you jsut want to listen for encoded events. |
| 71 | + abi: abi, # decoded json abi |
| 72 | + abi_files: ["path/to/abi.json"], # list of paths to abi json files |
| 70 73 | |
| 71 | - # an optional `context` to provide in the `W3WS.Env` struct for any events |
| 72 | - # received from this subscription. Defaults to `%{}`. |
| 73 | - context: %{chain_id: 1}, |
| 74 | + # an optional `context` to provide in the `W3WS.Env` struct for any events |
| 75 | + # received from this subscription. Defaults to `%{}`. |
| 76 | + context: %{chain_id: 1}, |
| 74 77 | |
| 75 | - # handler to call for each received event. can be either a module which `use`s |
| 76 | - # `W3WS.Handler` and defines a `handle_event/1` function, an anonymous function |
| 77 | - # which accepts a `%W3WS.Env{}` struct, or an MFA tuple. In the MFA tuple case |
| 78 | - # the arguments will be a `%W3WS.Env{}` struct followed by any arguments provided. |
| 79 | - # defaults to `W3WS.Handler.DefaultHandler` which logs received events. |
| 80 | - handler: MyApp.Handler, |
| 78 | + # handler to call for each received event. can be either a module which `use`s |
| 79 | + # `W3WS.Handler` and defines a `handle_event/1` function, an anonymous function |
| 80 | + # which accepts a `%W3WS.Env{}` struct, or an MFA tuple. In the MFA tuple case |
| 81 | + # the arguments will be a `%W3WS.Env{}` struct followed by any arguments provided. |
| 82 | + # defaults to `W3WS.Handler.DefaultHandler` which logs received events. |
| 83 | + handler: MyApp.Handler, |
| 81 84 | |
| 82 | - # a list of log event topics to subscribe to for the given subscription. this is |
| 83 | - # optional. not passing `:topics` will subscribe to all log events. See |
| 84 | - # https://ethereum.org/en/developers/tutorials/using-websockets/#eth-subscribe |
| 85 | - # documentation for more details. If an abi is provided you can use short-hand event |
| 86 | - # names or event signatures (e.g. `Transfer(address,address,uint256)`) as topics. |
| 87 | - # Short-hand is also supported in nested "or" topics. Regardless of providing an abi, |
| 88 | - # you can always use hex topics (e.g. |
| 89 | - # `0x0148cba56e5d3a8d32fbcea206eae9e449ec0f0def4f642994b3edcd38561deb`). |
| 90 | - topics: ["Transfer"], |
| 85 | + # a list of log event topics to subscribe to for the given subscription. this is |
| 86 | + # optional. not passing `:topics` will subscribe to all log events. See |
| 87 | + # https://ethereum.org/en/developers/tutorials/using-websockets/#eth-subscribe |
| 88 | + # documentation for more details. If an abi is provided you can use short-hand event |
| 89 | + # names or event signatures (e.g. `Transfer(address,address,uint256)`) as topics. |
| 90 | + # Short-hand is also supported in nested "or" topics. Regardless of providing an abi, |
| 91 | + # you can always use hex topics (e.g. |
| 92 | + # `0x0148cba56e5d3a8d32fbcea206eae9e449ec0f0def4f642994b3edcd38561deb`). |
| 93 | + topics: ["Transfer"], |
| 91 94 | |
| 92 | - # address to limit the subscription to. this is optional. if not provided |
| 93 | - # events will be received for all addresses. |
| 94 | - address: "0x73d578371eb449726d727376393b02bb3b8e6a57" |
| 95 | - } |
| 95 | + # address to limit the subscription to. this is optional. if not provided |
| 96 | + # events will be received for all addresses. |
| 97 | + address: "0x73d578371eb449726d727376393b02bb3b8e6a57" |
| 98 | + ] |
| 99 | + ] |
| 96 100 | ] |
| 97 | - }] |
| 101 | + ] |
| 98 102 | ``` |
| 99 103 | |
| 100 104 | Add the `ListenerManager` to your supervision tree: |
| @@ -108,13 +112,64 @@ children = [ | |
| 108 112 | Supervisor.start_link(children, strategy: :one_for_one) |
| 109 113 | ``` |
| 110 114 | |
| 115 | + ### Replay Events |
| 116 | + |
| 117 | + Replay events for a set of blocks. |
| 118 | + |
| 119 | + ```elixir |
| 120 | + W3WS.Replayer.replay( |
| 121 | + abi_files: abi_paths, |
| 122 | + uri: uri, |
| 123 | + from_block: 1_000_000, |
| 124 | + chunk_size: 10_000, |
| 125 | + chunk_sleep: 5_000, |
| 126 | + handler: MyApp.Handler, |
| 127 | + replays: [ |
| 128 | + [address: "0x0000000000000000000000000000000000000000", topics: ["Transfer"]], |
| 129 | + [address: "0x9999999999999999999999999999999999999999", topics: ["Mint"]] |
| 130 | + [ |
| 131 | + address: "0x5555555555555555555555555555555555555555", |
| 132 | + from_block: 1_500_000, |
| 133 | + handler: MyApp.SwapHandler, |
| 134 | + topics: ["Swap"] |
| 135 | + ] |
| 136 | + ] |
| 137 | + ) |
| 138 | + ``` |
| 139 | + |
| 140 | + Add a replay task to your task supervisor: |
| 141 | + |
| 142 | + ```elixir |
| 143 | + W3WS.Replayer.start( |
| 144 | + supervisor, |
| 145 | + abi_files: abi_paths, |
| 146 | + uri: uri, |
| 147 | + from_block: 1_000_000, |
| 148 | + chunk_size: 10_000, |
| 149 | + chunk_sleep: 5_000, |
| 150 | + handler: MyApp.Handler, |
| 151 | + replays: [ |
| 152 | + [address: "0x0000000000000000000000000000000000000000", topics: ["Transfer"]], |
| 153 | + [address: "0x9999999999999999999999999999999999999999", topics: ["Mint"]] |
| 154 | + [ |
| 155 | + address: "0x5555555555555555555555555555555555555555", |
| 156 | + from_block: 1_500_000, |
| 157 | + handler: MyApp.SwapHandler, |
| 158 | + topics: ["Swap"] |
| 159 | + ] |
| 160 | + ] |
| 161 | + ) |
| 162 | + ``` |
| 163 | + |
| 164 | + ### Handler |
| 165 | + |
| 111 166 | Define a handler: |
| 112 167 | |
| 113 168 | ```elixir |
| 114 169 | defmodule MyApp.Handler do |
| 115 170 | use W3WS.Handler |
| 116 171 | |
| 117 | - @impl true |
| 172 | + @impl W3WS.Handler |
| 118 173 | def handle_event( |
| 119 174 | %Env{ |
| 120 175 | decoded?: true, |
| @@ -126,11 +181,53 @@ end | |
| 126 181 | ``` |
| 127 182 | |
| 128 183 | Each handler is executed in an unlinked process so handler errors will not bring down |
| 129 | - the listener process. Events are not retried on failure so be sure your handler |
| 184 | + the calling process. Events are not retried on failure so be sure your handler |
| 130 185 | properly handles errors or queues event processing into a resilient framework. The |
| 131 | - listener does not wait on the handler to complete. If serialized events are important |
| 132 | - to you, you will need to serialize them yourself. The return value from the handler is |
| 133 | - ignored. |
| 186 | + listener does not wait on the handler to complete. If serial processing of events is |
| 187 | + important to you, you will need to handle this yourself. The return value from the |
| 188 | + handler is ignored. |
| 189 | + |
| 190 | + ### RPC Calls |
| 191 | + |
| 192 | + It is also possible to use the underlying `W3WS.Rpc` API directly. This is a lower-level library |
| 193 | + which does no ABI decoding. If you want to subscribe to logs and receive decoded events use |
| 194 | + `W3WS.Listener` or `W3WS.ListenerManager`. If you want to get past decoded events use `W3WS.Replayer`. |
| 195 | + |
| 196 | + Perform synchonous calls: |
| 197 | + |
| 198 | + ```elixir |
| 199 | + {:ok, rpc} = W3WS.Rpc.new(uri: "ws://localhost:8545") |
| 200 | + {:ok, %{"result" => block_number}} = W3WS.Rpc.send_message(rpc, W3WS.Message.eth_block_number()) |
| 201 | + ``` |
| 202 | + |
| 203 | + Perform asynchronous calls: |
| 204 | + |
| 205 | + ```elixir |
| 206 | + {:ok, rpc} = W3WS.Rpc.start_link(uri: "ws://localhost:8545") |
| 207 | + receipt = W3WS.Rpc.async_message(rpc, eth_block_number()) |
| 208 | + |
| 209 | + receive do |
| 210 | + {:eth_response, ^receipt, response, request} -> |
| 211 | + Logger.debug("received response") |
| 212 | + end |
| 213 | + ``` |
| 214 | + |
| 215 | + Subscribe to events: |
| 216 | + |
| 217 | + ```elixir |
| 218 | + {:ok, rpc} = W3WS.Rpc.start_link(uri: "ws://localhost:8545") |
| 219 | + |
| 220 | + # synchronous subscribe |
| 221 | + {:ok, %{"result" => subscription}} = W3WS.Rpc.send_message(rpc, W3WS.Message.eth_subscribe_logs([])) |
| 222 | + |
| 223 | + # or async subscribe |
| 224 | + receipt = W3WS.Rpc.async_message(rpc, W3WS.Message.eth_subscribe_logs([])) |
| 225 | + |
| 226 | + receive do |
| 227 | + {:eth_subscription, event} -> |
| 228 | + Logger.debug("received subscription") |
| 229 | + end |
| 230 | + ``` |
| 134 231 | |
| 135 232 | See the [documentation](https://hexdocs.pm/w3ws) for additional usage. |
| 136 233 | |
| @@ -148,7 +245,6 @@ See the [documentation](https://hexdocs.pm/w3ws) for additional usage. | |
| 148 245 | - No performance testing has been done. If you have a performance problem, please |
| 149 246 | open an issue. If you do performance testing and want to report results for |
| 150 247 | inclusion here please open an issue. |
| 151 | - - The library may be expanded in the future to support explicitly making rpc calls |
| 152 | - to the Ethereum node over websockets. |
| 153 248 | - This library is under active development and is subject to change without notice. |
| 249 | + - Semantic versioning will not be followed until the library is stable. |
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/stocks29/w3ws">>}]}. |
| 2 2 | {<<"name">>,<<"w3ws">>}. |
| 3 | - {<<"version">>,<<"0.1.0">>}. |
| 3 | + {<<"version">>,<<"0.2.0">>}. |
| 4 4 | {<<"description">>,<<"Ethereum websocket library for Elixir">>}. |
| 5 5 | {<<"elixir">>,<<"~> 1.15">>}. |
| 6 6 | {<<"app">>,<<"w3ws">>}. |
| @@ -28,8 +28,10 @@ | |
| 28 28 | {<<"repository">>,<<"hexpm">>}]]}. |
| 29 29 | {<<"files">>, |
| 30 30 | [<<"lib">>,<<"lib/w3ws">>,<<"lib/w3ws/util.ex">>,<<"lib/w3ws/message.ex">>, |
| 31 | - <<"lib/w3ws/event.ex">>,<<"lib/w3ws/env.ex">>,<<"lib/w3ws/handler.ex">>, |
| 31 | + <<"lib/w3ws/event.ex">>,<<"lib/w3ws/replayer.ex">>,<<"lib/w3ws/env.ex">>, |
| 32 | + <<"lib/w3ws/handler.ex">>,<<"lib/w3ws/rpc_base.ex">>, |
| 32 33 | <<"lib/w3ws/raw_event.ex">>,<<"lib/w3ws/listener.ex">>, |
| 33 | - <<"lib/w3ws/listener_manager.ex">>,<<"lib/w3ws/abi.ex">>,<<"lib/w3ws.ex">>, |
| 34 | - <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}. |
| 34 | + <<"lib/w3ws/listener_manager.ex">>,<<"lib/w3ws/rpc.ex">>, |
| 35 | + <<"lib/w3ws/abi.ex">>,<<"lib/w3ws.ex">>,<<".formatter.exs">>,<<"mix.exs">>, |
| 36 | + <<"README.md">>,<<"LICENSE.md">>]}. |
| 35 37 | {<<"build_tools">>,[<<"mix">>]}. |
| @@ -1,3 +1,3 @@ | |
| 1 1 | defmodule W3WS do |
| 2 | - @moduledoc File.read!("README.md") |
| 2 | + @moduledoc false |
| 3 3 | end |
| @@ -3,6 +3,8 @@ defmodule W3WS.ABI do | |
| 3 3 | Ethereum ABI Functions |
| 4 4 | """ |
| 5 5 | |
| 6 | + @type t :: list(%ABI.FunctionSelector{}) |
| 7 | + |
| 6 8 | @doc """ |
| 7 9 | Loads ABIs from an `Enumerable` of abi file paths |
| @@ -7,10 +7,10 @@ defmodule W3WS.Env do | |
| 7 7 | event: W3WS.Event.t() | nil, |
| 8 8 | context: map(), |
| 9 9 | decoded?: boolean(), |
| 10 | - jsonrpc: String.t(), |
| 11 | - method: String.t(), |
| 10 | + jsonrpc: String.t() | nil, |
| 11 | + method: String.t() | nil, |
| 12 12 | raw: W3WS.RawEvent.t(), |
| 13 | - subscription: String.t() |
| 13 | + subscription: String.t() | nil |
| 14 14 | } |
| 15 15 | defstruct event: nil, |
| 16 16 | context: %{}, |
| @@ -88,6 +88,51 @@ defmodule W3WS.Env do | |
| 88 88 | } |
| 89 89 | end |
| 90 90 | |
| 91 | + @doc """ |
| 92 | + Create an envelope from a log, like those returned by `eth_getLogs` |
| 93 | + |
| 94 | + ## Examples |
| 95 | + |
| 96 | + iex> from_log(%{ |
| 97 | + ...> "address" => "0xAddress", |
| 98 | + ...> "blockHash" => "0xBlockHash", |
| 99 | + ...> "blockNumber" => "0x1", |
| 100 | + ...> "data" => "0xData", |
| 101 | + ...> "logIndex" => "0x0", |
| 102 | + ...> "removed" => false, |
| 103 | + ...> "topics" => ["0xTopic"], |
| 104 | + ...> "transactionHash" => "0xTransactionHash", |
| 105 | + ...> "transactionIndex" => "0x2" |
| 106 | + ...> }, %{chain_id: 1}) |
| 107 | + %W3WS.Env{ |
| 108 | + context: %{chain_id: 1}, |
| 109 | + decoded?: false, |
| 110 | + event: nil, |
| 111 | + jsonrpc: nil, |
| 112 | + method: nil, |
| 113 | + raw: %W3WS.RawEvent{ |
| 114 | + address: "0xAddress", |
| 115 | + block_hash: "0xBlockHash", |
| 116 | + block_number: "0x1", |
| 117 | + data: "0xData", |
| 118 | + log_index: "0x0", |
| 119 | + removed: false, |
| 120 | + topics: ["0xTopic"], |
| 121 | + transaction_hash: "0xTransactionHash", |
| 122 | + transaction_index: "0x2" |
| 123 | + }, |
| 124 | + subscription: nil |
| 125 | + } |
| 126 | + |
| 127 | + """ |
| 128 | + @spec from_log(response :: map(), context :: map()) :: t() |
| 129 | + def from_log(log, context) do |
| 130 | + %__MODULE__{ |
| 131 | + context: context, |
| 132 | + raw: W3WS.RawEvent.from_map(log) |
| 133 | + } |
| 134 | + end |
| 135 | + |
| 91 136 | @doc """ |
| 92 137 | Adds a decoded event to the envelope and sets `decoded?: true` if |
| 93 138 | the event is decoded. |
Loading more files…