Current section

6 Versions

Jump to

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…