From a2d8edfa52e3cfa1607fe2b99c3ca4d23f1b91d4 Mon Sep 17 00:00:00 2001 From: hswick Date: Mon, 25 Jun 2018 15:19:41 -0400 Subject: [PATCH 01/10] Moved poller and listener into lib, setup test --- lib/exw3.ex | 70 ++++++++++++++++++++++++++++++++++++++++++++++ test/exw3_test.exs | 34 +++++++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index 6bbe97c..9a19178 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -462,4 +462,74 @@ defmodule ExW3 do {:reply, {:ok, {receipt, formatted_logs}}, state} end end + + defmodule Poller do + use GenServer + + def start_link do + GenServer.start_link(__MODULE__, [], name: EventPoller) + end + + def subscribe(event) do + GenServer.cast(EventPoller, {:subscribe, event}) + end + + @impl true + def init(state) do + schedule_work() # Schedule work to be performed on start + {:ok, state} + end + + @impl true + def handle_cast({:subscribe, event}, state) do + {:noreply, [event | state]} + end + + @impl true + def handle_info(:work, state) do + # Do the desired work here + Enum.each state, fn event -> + send EventListener, {:event, event, "Hello, World"} + end + + schedule_work() # Reschedule once more + {:noreply, state} + end + + defp schedule_work() do + Process.send_after(self(), :work, 1000) # In 1 sec + end + end + + defmodule Listener do + def start_link do + Poller.start_link() + {:ok, pid} = Task.start_link(fn -> loop(%{}) end) + Process.register(pid, EventListener) + :ok + end + + def subscribe(event, pid) do + Poller.subscribe(event) + send EventListener, {:subscribe, event, pid} + end + + def listen(callback) do + receive do + {:event, result} -> apply callback, [result] + end + listen(callback) + end + + defp loop(map) do + receive do + {:subscribe, event, pid} -> + loop(Map.put(map, event, pid)) + {:event, event, data} -> + send Map.get(map, event), {:event, {event, data}} + loop(map) + end + end + end + end diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 8cf1ced..ac101d6 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -133,6 +133,39 @@ defmodule EXW3Test do assert data == "Hello, World!" end + test "starts a Contract GenServer and uses the event listener", context do + ExW3.Contract.start_link(EventTester, abi: context[:event_tester_abi]) + + {:ok, address} = + ExW3.Contract.deploy( + EventTester, + bin: ExW3.load_bin("test/examples/build/EventTester.bin"), + options: %{ + gas: 300_000, + from: Enum.at(context[:accounts], 0) + } + ) + + ExW3.Contract.at(EventTester, address) + + {:ok, tx_hash} = + ExW3.Contract.send( + EventTester, + :simple, + ["Hello, World!"], + %{from: Enum.at(context[:accounts], 0)} + ) + + ExW3.Listener.start_link() + + ExW3.Listener.subscribe("Hello", self()) + + receive do + {:event, {"Frank", data}} -> IO.inspect data + after 3_000 -> raise "Never received event" + end + end + test "starts a Contract GenServer for Complex contract", context do ExW3.Contract.start_link(Complex, abi: context[:complex_abi]) @@ -217,4 +250,3 @@ defmodule EXW3Test do assert ExW3.is_valid_checksum_address("0x2f015c60e0be116b1f0cd534704db9c92118fb6a") == false end end - From ab94dcb4a907123c8e92f2e44044432a94fccbc3 Mon Sep 17 00:00:00 2001 From: hswick Date: Mon, 25 Jun 2018 15:43:55 -0400 Subject: [PATCH 02/10] Added filter methods --- lib/exw3.ex | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/exw3.ex b/lib/exw3.ex index 9a19178..63df920 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -127,6 +127,30 @@ defmodule ExW3 do end end + @spec new_filter(%{}) :: binary() + @doc "Creates a new filter, returns filter id" + def new_filter(map) do + case Ethereumex.HttpClient.eth_new_filter(map) do + {:ok, filter_id} -> filter_id + err -> err + end + end + + def get_filter_changes(filter_id) do + case Ethereumex.HttpClient.eth_get_filter_changes(filter_id) do + {:ok, changes} -> changes + err -> err + end + end + + @spec uninstall_filter(binary()) :: boolean() + def uninstall_filter(filter_id) do + case Ethereumex.HttpClient.eth_uninstall_filter(filter_id) do + {:ok, result} -> result + err -> err + end + end + @spec mine(integer()) :: any() @doc "Mines number of blocks specified. Default is 1" def mine(num_blocks \\ 1) do From dbd6262a4049b9f4ef08a0cf81ad74695473a81a Mon Sep 17 00:00:00 2001 From: hswick Date: Mon, 25 Jun 2018 17:33:01 -0400 Subject: [PATCH 03/10] Able to receive event from listener --- lib/exw3.ex | 169 +++++++++++++++++++++++++-------------------- test/exw3_test.exs | 16 +++-- 2 files changed, 106 insertions(+), 79 deletions(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index 63df920..621bf42 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -288,6 +288,75 @@ defmodule ExW3 do end end + defmodule Poller do + use GenServer + + def start_link do + GenServer.start_link(__MODULE__, [], name: EventPoller) + end + + def subscribe(filter_id) do + GenServer.cast(EventPoller, {:subscribe, filter_id}) + end + + @impl true + def init(state) do + schedule_work() # Schedule work to be performed on start + {:ok, state} + end + + @impl true + def handle_cast({:subscribe, filter_id}, state) do + {:noreply, [filter_id | state]} + end + + @impl true + def handle_info(:work, state) do + # Do the desired work here + Enum.each state, fn filter_id -> + send EventListener, {:event, filter_id, ExW3.get_filter_changes(filter_id)} + end + + schedule_work() # Reschedule once more + {:noreply, state} + end + + defp schedule_work() do + Process.send_after(self(), :work, 1000) # In 1 sec + end + end + + defmodule Listener do + def start_link do + Poller.start_link() + {:ok, pid} = Task.start_link(fn -> loop(%{}) end) + Process.register(pid, EventListener) + :ok + end + + def subscribe(filter_id, pid) do + Poller.subscribe(filter_id) + send EventListener, {:subscribe, filter_id, pid} + end + + def listen(callback) do + receive do + {:event, result} -> apply callback, [result] + end + listen(callback) + end + + defp loop(map) do + receive do + {:subscribe, filter_id, pid} -> + loop(Map.put(map, filter_id, pid)) + {:event, filter_id, data} -> + send Map.get(map, filter_id), {:event, {filter_id, data}} + loop(map) + end + end + end + defmodule Contract do use GenServer @@ -335,11 +404,15 @@ defmodule ExW3 do GenServer.call(pid, {:tx_receipt, tx_hash}) end + def subscribe(pid, event_name, other_pid, event_data \\ %{}) do + GenServer.call(pid, {:subscribe, {event_name, other_pid, event_data}}) + end + # Server def init(state) do if state[:abi] do - {:ok, [{:events, init_events(state[:abi])} | state]} + {:ok, state ++ init_events(state[:abi])} else raise "ABI not provided upon initialization" end @@ -351,16 +424,28 @@ defmodule ExW3 do v["type"] == "event" end) - signature_types_map = + names_and_signature_types_map = Enum.map(events, fn {name, v} -> types = Enum.map(v["inputs"], &Map.get(&1, "type")) names = Enum.map(v["inputs"], &Map.get(&1, "name")) signature = Enum.join([name, "(", Enum.join(types, ","), ")"]) - {"0x#{ExW3.encode_event(signature)}", %{signature: signature, names: names}} + encoded_event_signature = "0x#{ExW3.encode_event(signature)}" + + {{encoded_event_signature, %{signature: signature, names: names}}, {name, encoded_event_signature}} end) - Enum.into(signature_types_map, %{}) + signature_types_map = + Enum.map(names_and_signature_types_map, fn {signature_types, _} -> + signature_types + end) + + names_map = + Enum.map(names_and_signature_types_map, fn {_, names} -> + names + end) + + [events: Enum.into(signature_types_map, %{}), event_names: Enum.into(names_map, %{})] end # Helpers @@ -429,6 +514,13 @@ defmodule ExW3 do {:noreply, [{:address, address} | state]} end + def handle_call({:subscribe, {event_name, other_pid, event_data}}, _from, state) do + payload = Map.merge(%{address: state[:address], topics: [state[:event_names][event_name]]}, event_data) + filter_id = ExW3.new_filter(payload) + Listener.subscribe(filter_id, other_pid) + {:reply, filter_id, state ++ [event_name, filter_id]} + end + # Calls def handle_call({:deploy, args}, _from, state) do @@ -486,74 +578,5 @@ defmodule ExW3 do {:reply, {:ok, {receipt, formatted_logs}}, state} end end - - defmodule Poller do - use GenServer - - def start_link do - GenServer.start_link(__MODULE__, [], name: EventPoller) - end - - def subscribe(event) do - GenServer.cast(EventPoller, {:subscribe, event}) - end - - @impl true - def init(state) do - schedule_work() # Schedule work to be performed on start - {:ok, state} - end - - @impl true - def handle_cast({:subscribe, event}, state) do - {:noreply, [event | state]} - end - - @impl true - def handle_info(:work, state) do - # Do the desired work here - Enum.each state, fn event -> - send EventListener, {:event, event, "Hello, World"} - end - - schedule_work() # Reschedule once more - {:noreply, state} - end - - defp schedule_work() do - Process.send_after(self(), :work, 1000) # In 1 sec - end - end - defmodule Listener do - def start_link do - Poller.start_link() - {:ok, pid} = Task.start_link(fn -> loop(%{}) end) - Process.register(pid, EventListener) - :ok - end - - def subscribe(event, pid) do - Poller.subscribe(event) - send EventListener, {:subscribe, event, pid} - end - - def listen(callback) do - receive do - {:event, result} -> apply callback, [result] - end - listen(callback) - end - - defp loop(map) do - receive do - {:subscribe, event, pid} -> - loop(Map.put(map, event, pid)) - {:event, event, data} -> - send Map.get(map, event), {:event, {event, data}} - loop(map) - end - end - end - end diff --git a/test/exw3_test.exs b/test/exw3_test.exs index ac101d6..158553c 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -147,6 +147,10 @@ defmodule EXW3Test do ) ExW3.Contract.at(EventTester, address) + + ExW3.Listener.start_link() + + filter_id = ExW3.Contract.subscribe(EventTester, "Simple", self()) {:ok, tx_hash} = ExW3.Contract.send( @@ -156,14 +160,14 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0)} ) - ExW3.Listener.start_link() - - ExW3.Listener.subscribe("Hello", self()) - receive do - {:event, {"Frank", data}} -> IO.inspect data - after 3_000 -> raise "Never received event" + {:event, {filter_id, data}} -> + IO.inspect data + after 3_000 -> + raise "Never received event" end + + ExW3.uninstall_filter(filter_id) end test "starts a Contract GenServer for Complex contract", context do From f19651ef4d9221fa938f4ff34c0bd1359df16fe9 Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 12:46:22 -0400 Subject: [PATCH 04/10] Added event filtering, with formatted data --- lib/exw3.ex | 50 +++++++++++++++++++++++++++------------------- test/exw3_test.exs | 15 +++++++++----- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index 621bf42..aac35b4 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -295,8 +295,8 @@ defmodule ExW3 do GenServer.start_link(__MODULE__, [], name: EventPoller) end - def subscribe(filter_id) do - GenServer.cast(EventPoller, {:subscribe, filter_id}) + def filter(filter_id) do + GenServer.cast(EventPoller, {:filter, filter_id}) end @impl true @@ -306,7 +306,7 @@ defmodule ExW3 do end @impl true - def handle_cast({:subscribe, filter_id}, state) do + def handle_cast({:filter, filter_id}, state) do {:noreply, [filter_id | state]} end @@ -314,7 +314,7 @@ defmodule ExW3 do def handle_info(:work, state) do # Do the desired work here Enum.each state, fn filter_id -> - send EventListener, {:event, filter_id, ExW3.get_filter_changes(filter_id)} + send Listener, {:event, filter_id, ExW3.get_filter_changes(filter_id)} end schedule_work() # Reschedule once more @@ -322,21 +322,21 @@ defmodule ExW3 do end defp schedule_work() do - Process.send_after(self(), :work, 1000) # In 1 sec + Process.send_after(self(), :work, 500) # In 1/2 sec end end - defmodule Listener do + defmodule EventListener do def start_link do Poller.start_link() {:ok, pid} = Task.start_link(fn -> loop(%{}) end) - Process.register(pid, EventListener) + Process.register(pid, Listener) :ok end - def subscribe(filter_id, pid) do - Poller.subscribe(filter_id) - send EventListener, {:subscribe, filter_id, pid} + def filter(filter_id, event_signature, pid) do + Poller.filter(filter_id) + send Listener, {:filter, filter_id, event_signature, pid} end def listen(callback) do @@ -346,13 +346,19 @@ defmodule ExW3 do listen(callback) end - defp loop(map) do + defp loop(state) do receive do - {:subscribe, filter_id, pid} -> - loop(Map.put(map, filter_id, pid)) - {:event, filter_id, data} -> - send Map.get(map, filter_id), {:event, {filter_id, data}} - loop(map) + {:filter, filter_id, event_signature, pid} -> + loop(Map.put(state, filter_id, %{pid: pid, signature: event_signature})) + {:event, filter_id, logs} -> + filter_attributes = Map.get(state, filter_id) + Enum.each(logs, fn log -> + data = Map.get(log, "data") + new_data = ExW3.decode_event(data, filter_attributes[:signature]) + new_log = Map.put(log, :data, new_data) + send filter_attributes[:pid], {:event, {filter_id, new_log}} + end) + loop(state) end end end @@ -404,8 +410,8 @@ defmodule ExW3 do GenServer.call(pid, {:tx_receipt, tx_hash}) end - def subscribe(pid, event_name, other_pid, event_data \\ %{}) do - GenServer.call(pid, {:subscribe, {event_name, other_pid, event_data}}) + def filter(pid, event_name, other_pid, event_data \\ %{}) do + GenServer.call(pid, {:filter, {event_name, other_pid, event_data}}) end # Server @@ -514,10 +520,14 @@ defmodule ExW3 do {:noreply, [{:address, address} | state]} end - def handle_call({:subscribe, {event_name, other_pid, event_data}}, _from, state) do + def handle_call({:filter, {event_name, other_pid, event_data}}, _from, state) do + unless Process.whereis(Listener) do + raise "EventListener process not alive. Call ExW3.EventListener.start_link before using ExW3.Contract.subscribe" + end payload = Map.merge(%{address: state[:address], topics: [state[:event_names][event_name]]}, event_data) filter_id = ExW3.new_filter(payload) - Listener.subscribe(filter_id, other_pid) + event_signature = state[:events][state[:event_names][event_name]][:signature] + EventListener.filter(filter_id, event_signature, other_pid) {:reply, filter_id, state ++ [event_name, filter_id]} end diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 158553c..c3f5cda 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -147,10 +147,12 @@ defmodule EXW3Test do ) ExW3.Contract.at(EventTester, address) - - ExW3.Listener.start_link() - filter_id = ExW3.Contract.subscribe(EventTester, "Simple", self()) + {:ok, agent} = Agent.start_link(fn -> [] end) + + ExW3.EventListener.start_link() + + filter_id = ExW3.Contract.filter(EventTester, "Simple", self()) {:ok, tx_hash} = ExW3.Contract.send( @@ -159,14 +161,17 @@ defmodule EXW3Test do ["Hello, World!"], %{from: Enum.at(context[:accounts], 0)} ) - + receive do {:event, {filter_id, data}} -> - IO.inspect data + Agent.update(agent, fn list -> [data | list] end) after 3_000 -> raise "Never received event" end + state = Agent.get(agent, fn list -> list end) + assert Enum.at(state, 0) |> is_map + ExW3.uninstall_filter(filter_id) end From 5e55d59b7fa81c7a86ab129c43f65aa7202e5f2a Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 12:53:01 -0400 Subject: [PATCH 05/10] Don't emit event if log is empty --- lib/exw3.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index aac35b4..ffbf25c 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -352,12 +352,14 @@ defmodule ExW3 do loop(Map.put(state, filter_id, %{pid: pid, signature: event_signature})) {:event, filter_id, logs} -> filter_attributes = Map.get(state, filter_id) - Enum.each(logs, fn log -> - data = Map.get(log, "data") - new_data = ExW3.decode_event(data, filter_attributes[:signature]) - new_log = Map.put(log, :data, new_data) - send filter_attributes[:pid], {:event, {filter_id, new_log}} - end) + unless logs == [] do + Enum.each(logs, fn log -> + data = Map.get(log, "data") + new_data = ExW3.decode_event(data, filter_attributes[:signature]) + new_log = Map.put(log, :data, new_data) + send filter_attributes[:pid], {:event, {filter_id, new_log}} + end) + end loop(state) end end From ac777373c21aa1a139fb897b22a89bec99a87757 Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 13:02:16 -0400 Subject: [PATCH 06/10] Added version to ganache-cli --- .travis.yml | 2 +- travis_test.sh | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 travis_test.sh diff --git a/.travis.yml b/.travis.yml index 8507f64..9e0060f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ elixir: install: - sudo apt-get update - sudo apt-get install nodejs npm - - npm install -g ganache-cli + - npm install -g ganache-cli@6.1.3 - mix local.rebar --force # for Elixir 1.3.0 and up - mix local.hex --force - mix deps.get diff --git a/travis_test.sh b/travis_test.sh old mode 100644 new mode 100755 From e713645c240694075781c66378532085276b9877 Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 14:43:33 -0400 Subject: [PATCH 07/10] Added documentation about listening for events --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index eb4df98..668ab1f 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,50 @@ iex(10)> ExW3.Contract.call(SimpleStorage, :get) {:ok, 1} ``` +## Listening for Events + +Elixir doesn't have event listeners like say JS. However, we can simulate that behavior with message passing. The way ExW3 handles event filters, is that it starts a background process that will call eth_getFilterChanges every cycle. Whenever a change is detected it will send a message to the listener. + +``` +# Start the background listener +ExW3.EventListener.start_link + +# Assuming we have already setup our contract called EventTester +# We can then add a filter for the event listener to look out for +# by passing in the event name, and the process we want to receive the messages when an event is triggered. +# For now we are going to use the main process, however, we could pass in a pid of a different process. + +filter_id = ExW3.Contract.filter(EventTester, "Simple", self()) + +# We can then wait for the event. Using the typical receive will wait for the first instance of the event, and then continue. +# This is useful for testing. +receive do + {:event, {filter_id, data}} -> IO.inspect data +end + +# We can then uninstall the filter after we are done using it +ExW3.uninstall_filter(filter_id) + +# ExW3 also provides a helper method to continuously listen for events. One use is to combine all of our filters and using pattern matching +ExW3.EventListener.listen(fn result -> + case result do + {filter_id, data} -> IO.inspect data + {filter_id2, data} -> IO.inspect data + end +end + +# The listen method is a simple receive loop waiting for `{:event, _}` messages. +# It looks like this: + def listen(callback) do + receive do + {:event, result} -> apply callback, [result] + end + listen(callback) + end +``` + +# You could do something similar with your own process, whether it is a simple Task or a more involved GenServer. + # Compiling Soldity To compile the test solidity contracts after making a change run this command: From 6cae0ba3502ae9aeac33bc19508c0fb45caad993 Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 14:44:56 -0400 Subject: [PATCH 08/10] README formatting --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 668ab1f..2186cad 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ iex(10)> ExW3.Contract.call(SimpleStorage, :get) Elixir doesn't have event listeners like say JS. However, we can simulate that behavior with message passing. The way ExW3 handles event filters, is that it starts a background process that will call eth_getFilterChanges every cycle. Whenever a change is detected it will send a message to the listener. -``` +```elixir # Start the background listener ExW3.EventListener.start_link @@ -114,12 +114,12 @@ end # The listen method is a simple receive loop waiting for `{:event, _}` messages. # It looks like this: - def listen(callback) do - receive do - {:event, result} -> apply callback, [result] - end - listen(callback) - end +def listen(callback) do + receive do + {:event, result} -> apply callback, [result] + end + listen(callback) +end ``` # You could do something similar with your own process, whether it is a simple Task or a more involved GenServer. From 05511542fca5cb9981a064015ee818c776be7b31 Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 14:45:31 -0400 Subject: [PATCH 09/10] README fix --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2186cad..2c13120 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,11 @@ def listen(callback) do end listen(callback) end -``` # You could do something similar with your own process, whether it is a simple Task or a more involved GenServer. +``` + + # Compiling Soldity From cd254c2b68b3113c20af44a036a594baee349ad2 Mon Sep 17 00:00:00 2001 From: hswick Date: Tue, 26 Jun 2018 14:49:47 -0400 Subject: [PATCH 10/10] README fix --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2c13120..0efcc6e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ iex(10)> ExW3.Contract.call(SimpleStorage, :get) ## Listening for Events -Elixir doesn't have event listeners like say JS. However, we can simulate that behavior with message passing. The way ExW3 handles event filters, is that it starts a background process that will call eth_getFilterChanges every cycle. Whenever a change is detected it will send a message to the listener. +Elixir doesn't have event listeners like say JS. However, we can simulate that behavior with message passing. +The way ExW3 handles event filters is with a background process that calls eth_getFilterChanges every cycle. +Whenever a change is detected it will send a message to whichever process is listening. ```elixir # Start the background listener @@ -95,8 +97,8 @@ ExW3.EventListener.start_link filter_id = ExW3.Contract.filter(EventTester, "Simple", self()) -# We can then wait for the event. Using the typical receive will wait for the first instance of the event, and then continue. -# This is useful for testing. +# We can then wait for the event. Using the typical receive keyword we wait for the first instance +# of the event, and then continue with the rest of the code. This is useful for testing. receive do {:event, {filter_id, data}} -> IO.inspect data end @@ -104,7 +106,8 @@ end # We can then uninstall the filter after we are done using it ExW3.uninstall_filter(filter_id) -# ExW3 also provides a helper method to continuously listen for events. One use is to combine all of our filters and using pattern matching +# ExW3 also provides a helper method to continuously listen for events. +# One use is to combine all of our filters with pattern matching ExW3.EventListener.listen(fn result -> case result do {filter_id, data} -> IO.inspect data