From 0aad3bcecd50de8b5984183d7b7f060b3f2dd581 Mon Sep 17 00:00:00 2001 From: hswick Date: Fri, 28 Sep 2018 19:10:24 -0500 Subject: [PATCH 1/6] Deleted EventListener and EventPoller, replaced with simple get_changes method as part of Contract module --- lib/exw3.ex | 241 ++++++++++++++++++--------------------------- test/exw3_test.exs | 88 ++++------------- 2 files changed, 113 insertions(+), 216 deletions(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index a316bcb..ad6bf03 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -384,138 +384,6 @@ defmodule ExW3 do end end - defmodule Poller do - use GenServer - - def start_link do - GenServer.start_link(__MODULE__, [], name: EventPoller) - end - - def filter(filter_id) do - GenServer.cast(EventPoller, {:filter, filter_id}) - end - - @impl true - def init(state) do - # Schedule work to be performed on start - schedule_work() - {:ok, state} - end - - @impl true - def handle_cast({:filter, 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(Listener, {:event, filter_id, ExW3.get_filter_changes(filter_id)}) - end) - - # Reschedule once more - schedule_work() - {:noreply, state} - end - - defp schedule_work() do - # In 1/2 sec - Process.send_after(self(), :work, 500) - end - end - - defmodule EventListener do - def start_link do - Poller.start_link() - {:ok, pid} = Task.start_link(fn -> loop(%{}) end) - Process.register(pid, Listener) - :ok - end - - def filter(filter_id, event_fields, pid) do - Poller.filter(filter_id) - send(Listener, {:filter, filter_id, event_fields, pid}) - end - - def listen(callback) do - receive do - {:event, result} -> apply(callback, [result]) - end - - listen(callback) - end - - defp extract_non_indexed_fields(data, names, signature) do - Enum.zip(names, ExW3.decode_event(data, signature)) |> Enum.into(%{}) - end - - defp format_log_data(log, event_attributes) do - non_indexed_fields = - extract_non_indexed_fields( - Map.get(log, "data"), - event_attributes[:non_indexed_names], - event_attributes[:signature] - ) - - indexed_fields = - if length(log["topics"]) > 1 do - [_head | tail] = log["topics"] - - decoded_topics = - Enum.map(0..(length(tail) - 1), fn i -> - topic_type = Enum.at(event_attributes[:topic_types], i) - topic_data = Enum.at(tail, i) - - {decoded} = ExW3.decode_data(topic_type, topic_data) - - decoded - end) - - Enum.zip(event_attributes[:topic_names], decoded_topics) |> Enum.into(%{}) - else - %{} - end - - new_data = Map.merge(indexed_fields, non_indexed_fields) - - Map.put(log, "data", new_data) - end - - defp loop(state) do - receive do - {:filter, filter_id, event_attributes, pid} -> - loop(Map.put(state, filter_id, %{pid: pid, event_attributes: event_attributes})) - - {:event, filter_id, logs} -> - filter_attributes = Map.get(state, filter_id) - event_attributes = filter_attributes[:event_attributes] - - unless logs == [] do - Enum.each(logs, fn log -> - formatted_log = - Enum.reduce( - [ - ExW3.keys_to_decimal(log, [ - "blockNumber", - "logIndex", - "transactionIndex", - "transactionLogIndex" - ]), - format_log_data(log, event_attributes) - ], - &Map.merge/2 - ) - - send(filter_attributes[:pid], {:event, {filter_id, formatted_log}}) - end) - end - - loop(state) - end - end - end - defmodule Contract do use GenServer @@ -524,7 +392,7 @@ defmodule ExW3 do @spec start_link() :: {:ok, pid()} @doc "Begins the Contract process to manage all interactions with smart contracts" def start_link() do - GenServer.start_link(__MODULE__, %{}, name: ContractManager) + GenServer.start_link(__MODULE__, %{filters: %{}}, name: ContractManager) end @spec deploy(keyword(), []) :: {:ok, binary(), []} @@ -585,10 +453,19 @@ defmodule ExW3 do GenServer.call(ContractManager, {:tx_receipt, {contract_name, tx_hash}}) end - def filter(contract_name, event_name, other_pid, event_data \\ %{}) do + @spec filter(keyword(), binary(), %{}) :: {:ok, binary()} + def filter(contract_name, event_name, event_data \\ %{}) do GenServer.call( ContractManager, - {:filter, {contract_name, event_name, other_pid, event_data}} + {:filter, {contract_name, event_name, event_data}} + ) + end + + @spec get_changes(binary(), integer()) :: {:ok, []} + def get_changes(filter_id, seconds \\ 0) do + GenServer.call( + ContractManager, + {:get_changes, {filter_id, seconds}} ) end @@ -668,7 +545,10 @@ defmodule ExW3 do names end) - [events: Enum.into(signature_types_map, %{}), event_names: Enum.into(names_map, %{})] + [ + events: Enum.into(signature_types_map, %{}), + event_names: Enum.into(names_map, %{}) + ] end # Helpers @@ -746,9 +626,9 @@ defmodule ExW3 do ) end - defp add_helper(contract_info) do + defp register_helper(contract_info) do if contract_info[:abi] do - contract_info ++ init_events(contract_info[:abi]) + contract_info ++ init_events(contract_info[:abi]) else raise "ABI not provided upon initialization" end @@ -770,7 +650,7 @@ defmodule ExW3 do end def handle_cast({:register, {name, contract_info}}, state) do - {:noreply, Map.put(state, name, add_helper(contract_info))} + {:noreply, Map.put(state, name, register_helper(contract_info))} end # Calls @@ -850,12 +730,49 @@ defmodule ExW3 do |> Map.delete(:topics) end - def handle_call({:filter, {contract_name, event_name, other_pid, event_data}}, _from, state) do + def get_event_attributes(state, contract_name, event_name) do contract_info = state[contract_name] + contract_info[:events][contract_info[:event_names][event_name]] + end - unless Process.whereis(Listener) do - raise "EventListener process not alive. Call ExW3.EventListener.start_link before using ExW3.Contract.subscribe" - end + defp extract_non_indexed_fields(data, names, signature) do + Enum.zip(names, ExW3.decode_event(data, signature)) |> Enum.into(%{}) + end + + defp format_log_data(log, event_attributes) do + non_indexed_fields = + extract_non_indexed_fields( + Map.get(log, "data"), + event_attributes[:non_indexed_names], + event_attributes[:signature] + ) + + indexed_fields = + if length(log["topics"]) > 1 do + [_head | tail] = log["topics"] + + decoded_topics = + Enum.map(0..(length(tail) - 1), fn i -> + topic_type = Enum.at(event_attributes[:topic_types], i) + topic_data = Enum.at(tail, i) + + {decoded} = ExW3.decode_data(topic_type, topic_data) + + decoded + end) + + Enum.zip(event_attributes[:topic_names], decoded_topics) |> Enum.into(%{}) + else + %{} + end + + new_data = Map.merge(indexed_fields, non_indexed_fields) + + Map.put(log, "data", new_data) + end + + def handle_call({:filter, {contract_name, event_name, event_data}}, _from, state) do + contract_info = state[contract_name] event_signature = contract_info[:event_names][event_name] topic_types = contract_info[:events][event_signature][:topic_types] @@ -870,11 +787,43 @@ defmodule ExW3 do ) filter_id = ExW3.new_filter(payload) - event_attributes = contract_info[:events][contract_info[:event_names][event_name]] - EventListener.filter(filter_id, event_attributes, other_pid) + {:reply, filter_id, Map.put(state, :filters, Map.put(state[:filters], filter_id, %{contract_name: contract_name, event_name: event_name}))} + end + + def handle_call({:get_changes, {filter_id, seconds}}, _from, state) do + + :timer.sleep(1000 * seconds) + + filter_info = Map.get(state[:filters], filter_id) + event_attributes = get_event_attributes(state, filter_info[:contract_name], filter_info[:event_name]) + + logs = ExW3.get_filter_changes(filter_id) + + formatted_logs = + if logs != [] do + Enum.map(logs, fn log -> + formatted_log = + Enum.reduce( + [ + ExW3.keys_to_decimal(log, [ + "blockNumber", + "logIndex", + "transactionIndex", + "transactionLogIndex" + ]), + format_log_data(log, event_attributes) + ], + &Map.merge/2 + ) + + formatted_log + end) + else + logs + end - {:reply, filter_id, Map.put(state, contract_name, contract_info ++ [event_name, filter_id])} + {:reply, {:ok, formatted_logs}, state} end def handle_call({:deploy, {name, args}}, _from, state) do diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 8081f9a..2f5b3e5 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -185,7 +185,7 @@ defmodule EXW3Test do assert data == "Hello, World!" end - test "starts a Contract GenServer and uses the event listener", context do + test "Testing formatted get filter changes", context do ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) {:ok, address, _} = @@ -200,13 +200,9 @@ defmodule EXW3Test do ExW3.Contract.at(:EventTester, address) - {:ok, agent} = Agent.start_link(fn -> [] end) - - ExW3.EventListener.start_link() - # Test non indexed events - filter_id = ExW3.Contract.filter(:EventTester, "Simple", self()) + filter_id = ExW3.Contract.filter(:EventTester, "Simple") {:ok, _tx_hash} = ExW3.Contract.send( @@ -216,16 +212,10 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - receive do - {:event, {_filter_id, data}} -> - Agent.update(agent, fn list -> [data | list] end) - after - 3_000 -> - raise "Never received event" - end + {:ok, change_logs} = ExW3.Contract.get_changes(filter_id) + - state = Agent.get(agent, fn list -> list end) - event_log = Enum.at(state, 0) + event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") @@ -237,9 +227,7 @@ defmodule EXW3Test do # Test indexed events - {:ok, agent} = Agent.start_link(fn -> [] end) - - indexed_filter_id = ExW3.Contract.filter(:EventTester, "SimpleIndex", self()) + indexed_filter_id = ExW3.Contract.filter(:EventTester, "SimpleIndex") {:ok, _tx_hash} = ExW3.Contract.send( @@ -249,16 +237,10 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - receive do - {:event, {_filter_id, data}} -> - Agent.update(agent, fn list -> [data | list] end) - after - 3_000 -> - raise "Never received event" - end + {:ok, change_logs} = ExW3.Contract.get_changes(indexed_filter_id) - state = Agent.get(agent, fn list -> list end) - event_log = Enum.at(state, 0) + event_log = Enum.at(change_logs, 0) + assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map @@ -269,13 +251,10 @@ defmodule EXW3Test do # Test Indexing Indexed Events - {:ok, agent} = Agent.start_link(fn -> [] end) - indexed_filter_id = ExW3.Contract.filter( :EventTester, "SimpleIndex", - self(), %{ topics: [nil, ["Hello, World", "Hello, World!"]], fromBlock: 1, @@ -291,51 +270,24 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - receive do - {:event, {_filter_id, data}} -> - Agent.update(agent, fn list -> [data | list] end) - after - 3_000 -> - raise "Never received event" - end + {:ok, change_logs} = ExW3.Contract.get_changes(indexed_filter_id) - state = Agent.get(agent, fn list -> list end) - event_log = Enum.at(state, 0) + event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 46 assert ExW3.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 + ExW3.uninstall_filter(indexed_filter_id) - end - - test "starts a EventTester", context do - ExW3.Contract.register(: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) - # Test Indexing Indexed Events with Map params - - ExW3.EventListener.start_link() - - {:ok, agent} = Agent.start_link(fn -> [] end) + # Tests filter with map params indexed_filter_id = ExW3.Contract.filter( :EventTester, "SimpleIndex", - self(), %{ topics: %{num: 46, data: "Hello, World!"} } @@ -349,23 +301,19 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - receive do - {:event, {_filter_id, data}} -> - Agent.update(agent, fn list -> [data | list] end) - after - 3_000 -> - raise "Never received event" - end + # Demonstrating the delay capability + {:ok, change_logs} = ExW3.Contract.get_changes(indexed_filter_id, 1) - state = Agent.get(agent, fn list -> list end) - event_log = Enum.at(state, 0) + event_log = Enum.at(change_logs, 0) assert event_log |> is_map log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 46 assert ExW3.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 + ExW3.uninstall_filter(indexed_filter_id) + end test "starts a Contract GenServer for Complex contract", context do From 477e566649b7abeee12d0fd47815f3669e5927d5 Mon Sep 17 00:00:00 2001 From: hswick Date: Fri, 28 Sep 2018 19:39:18 -0500 Subject: [PATCH 2/6] Changed get_changes to get_filter_changes, filter now returns ok --- lib/exw3.ex | 10 +++++----- test/exw3_test.exs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index ad6bf03..cc9db13 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -461,11 +461,11 @@ defmodule ExW3 do ) end - @spec get_changes(binary(), integer()) :: {:ok, []} - def get_changes(filter_id, seconds \\ 0) do + @spec get_filter_changes(binary(), integer()) :: {:ok, []} + def get_filter_changes(filter_id, seconds \\ 0) do GenServer.call( ContractManager, - {:get_changes, {filter_id, seconds}} + {:get_filter_changes, {filter_id, seconds}} ) end @@ -788,10 +788,10 @@ defmodule ExW3 do filter_id = ExW3.new_filter(payload) - {:reply, filter_id, Map.put(state, :filters, Map.put(state[:filters], filter_id, %{contract_name: contract_name, event_name: event_name}))} + {:reply, {:ok, filter_id}, Map.put(state, :filters, Map.put(state[:filters], filter_id, %{contract_name: contract_name, event_name: event_name}))} end - def handle_call({:get_changes, {filter_id, seconds}}, _from, state) do + def handle_call({:get_filter_changes, {filter_id, seconds}}, _from, state) do :timer.sleep(1000 * seconds) diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 2f5b3e5..2cc1c95 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -202,7 +202,7 @@ defmodule EXW3Test do # Test non indexed events - filter_id = ExW3.Contract.filter(:EventTester, "Simple") + {:ok, filter_id} = ExW3.Contract.filter(:EventTester, "Simple") {:ok, _tx_hash} = ExW3.Contract.send( @@ -212,7 +212,7 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - {:ok, change_logs} = ExW3.Contract.get_changes(filter_id) + {:ok, change_logs} = ExW3.Contract.get_filter_changes(filter_id) event_log = Enum.at(change_logs, 0) @@ -227,7 +227,7 @@ defmodule EXW3Test do # Test indexed events - indexed_filter_id = ExW3.Contract.filter(:EventTester, "SimpleIndex") + {:ok, indexed_filter_id} = ExW3.Contract.filter(:EventTester, "SimpleIndex") {:ok, _tx_hash} = ExW3.Contract.send( @@ -237,7 +237,7 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - {:ok, change_logs} = ExW3.Contract.get_changes(indexed_filter_id) + {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id) event_log = Enum.at(change_logs, 0) @@ -251,7 +251,7 @@ defmodule EXW3Test do # Test Indexing Indexed Events - indexed_filter_id = + {:ok, indexed_filter_id} = ExW3.Contract.filter( :EventTester, "SimpleIndex", @@ -270,7 +270,7 @@ defmodule EXW3Test do %{from: Enum.at(context[:accounts], 0), gas: 30_000} ) - {:ok, change_logs} = ExW3.Contract.get_changes(indexed_filter_id) + {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id) event_log = Enum.at(change_logs, 0) assert event_log |> is_map @@ -284,7 +284,7 @@ defmodule EXW3Test do # Tests filter with map params - indexed_filter_id = + {:ok, indexed_filter_id} = ExW3.Contract.filter( :EventTester, "SimpleIndex", @@ -302,7 +302,7 @@ defmodule EXW3Test do ) # Demonstrating the delay capability - {:ok, change_logs} = ExW3.Contract.get_changes(indexed_filter_id, 1) + {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id, 1) event_log = Enum.at(change_logs, 0) assert event_log |> is_map From 77d98d4506fa39937664bb0efee3d1a882dcb671 Mon Sep 17 00:00:00 2001 From: hswick Date: Fri, 28 Sep 2018 20:03:12 -0500 Subject: [PATCH 3/6] ExW3.Contract.uninstall_filter has been added as a cast --- README.md | 55 ++++++++++------------------------------------ lib/exw3.ex | 20 +++++++++++++++-- test/exw3_test.exs | 8 +++---- 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index a22bc3b..749861c 100644 --- a/README.md +++ b/README.md @@ -98,68 +98,38 @@ ExW3 now provides async versions of `call` and `send`. They both return a `Task` {:ok, data} = Task.await(t) ``` -## Listening for Events +## 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 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. +ExW3 allows the retrieval of event logs using filters. In this example, assume we have already deployed and registered a contract called :EventTester. ```elixir -# Start the background listener -ExW3.EventListener.start_link +# We can optionally specify extra parameters like `:fromBlock`, and `:toBlock` +{:ok, filter_id} = ExW3.Contract.filter(:EventTester, "Simple", %{fromBlock: 42, toBlock: "latest"}) -# Assuming we have already registered 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. -# We can also optionally specify extra parameters like `:fromBlock`, and `:toBlock` - -filter_id = ExW3.Contract.filter(:EventTester, "Simple", self(), %{fromBlock: 42, toBlock: "latest"}) - -# 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 +# After some point that we think there are some new changes +{:ok, changes} = ExW3.Contract.get_filter_changes(filter_id) # 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, with the `listen` method. -# 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 - {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. +ExW3.Contract.uninstall_filter(filter_id) ``` -## Listening for Indexed Events +## Indexed Events -Ethereum allows for filtering events specific to its parameters using indexing. For all of the options see [here](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter) +Ethereum allows a user to add topics to filters. This means the filter will only return events with the specific index parameters. For all of the extra options see [here](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter) If you have written your event in Solidity like this: ``` event SimpleIndex(uint256 indexed num, bytes32 indexed data, uint256 otherNum); ``` -You can add filter on which logs will be returned back to the RPC client, based on the indexed fields. ExW3 allows for 2 ways of specifying these parameters or `topics` in two ways. The first, and probably more preferred way, is with a map: +You can add filter on which logs will be returned back to the RPC client, based on the indexed fields. + +ExW3 allows for 2 ways of specifying these parameters or `topics` in two ways. The first, and probably more preferred way, is with a map: ```elixir indexed_filter_id = ExW3.Contract.filter( :EventTester, "SimpleIndex", - self(), %{ topics: %{num: 46, data: "Hello, World!"}, } @@ -171,7 +141,6 @@ The other option is with a list, but this is order dependent, and any values you ```elixir indexed_filter_id = ExW3.Contract.filter( :EventTester, - "SimpleIndex", self(), %{ topics: [nil, "Hello, World!"] diff --git a/lib/exw3.ex b/lib/exw3.ex index cc9db13..1e4dfc4 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -192,7 +192,7 @@ defmodule ExW3 do end @spec new_filter(%{}) :: binary() - @doc "Creates a new filter, returns filter id" + @doc "Creates a new filter, returns filter id. For more sophisticated use, prefer ExW3.Contract.filter." def new_filter(map) do case Ethereumex.HttpClient.eth_new_filter(map) do {:ok, filter_id} -> filter_id @@ -200,6 +200,8 @@ defmodule ExW3 do end end + @spec get_filter_changes(binary()) :: any() + @doc "Gets event changes (logs) by filter. Unlike ExW3.Contract.get_filter_changes it does not return the data in a formatted way" def get_filter_changes(filter_id) do case Ethereumex.HttpClient.eth_get_filter_changes(filter_id) do {:ok, changes} -> changes @@ -208,6 +210,7 @@ defmodule ExW3 do end @spec uninstall_filter(binary()) :: boolean() + @doc "Uninstalls filter from the ethereum node" def uninstall_filter(filter_id) do case Ethereumex.HttpClient.eth_uninstall_filter(filter_id) do {:ok, result} -> result @@ -407,6 +410,12 @@ defmodule ExW3 do GenServer.cast(ContractManager, {:register, {name, contract_info}}) end + @spec uninstall_filter(binary()) :: :ok + @doc "Uninstalls the filter, and deletes the data associated with the filter id" + def uninstall_filter(filter_id) do + GenServer.cast(ContractManager, {:uninstall_filter, filter_id}) + end + @spec at(keyword(), binary()) :: :ok @doc "Sets the address for the contract specified by the name argument" def at(name, address) do @@ -454,6 +463,7 @@ defmodule ExW3 do end @spec filter(keyword(), binary(), %{}) :: {:ok, binary()} + @doc "Installs a filter on the Ethereum node. This also formats the parameters, and saves relevant information to format event logs." def filter(contract_name, event_name, event_data \\ %{}) do GenServer.call( ContractManager, @@ -462,13 +472,14 @@ defmodule ExW3 do end @spec get_filter_changes(binary(), integer()) :: {:ok, []} + @doc "Using saved information related to the filter id, event logs are formatted properly" def get_filter_changes(filter_id, seconds \\ 0) do GenServer.call( ContractManager, {:get_filter_changes, {filter_id, seconds}} ) end - + # Server def init(state) do @@ -653,6 +664,11 @@ defmodule ExW3 do {:noreply, Map.put(state, name, register_helper(contract_info))} end + def handle_cast({:uninstall_filter, filter_id}, state) do + ExW3.uninstall_filter(filter_id) + {:noreply, Map.put(state, :filters, Map.delete(state[:filters], filter_id))} + end + # Calls defp filter_topics_helper(event_signature, event_data, topic_types, topic_names) do diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 2cc1c95..9ea3674 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -223,7 +223,7 @@ defmodule EXW3Test do assert Map.get(log_data, "num") == 42 assert ExW3.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" - ExW3.uninstall_filter(filter_id) + ExW3.Contract.uninstall_filter(filter_id) # Test indexed events @@ -247,7 +247,7 @@ defmodule EXW3Test do assert Map.get(log_data, "num") == 46 assert ExW3.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 - ExW3.uninstall_filter(indexed_filter_id) + ExW3.Contract.uninstall_filter(indexed_filter_id) # Test Indexing Indexed Events @@ -280,7 +280,7 @@ defmodule EXW3Test do assert ExW3.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 - ExW3.uninstall_filter(indexed_filter_id) + ExW3.Contract.uninstall_filter(indexed_filter_id) # Tests filter with map params @@ -312,7 +312,7 @@ defmodule EXW3Test do assert ExW3.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!" assert Map.get(log_data, "otherNum") == 42 - ExW3.uninstall_filter(indexed_filter_id) + ExW3.Contract.uninstall_filter(indexed_filter_id) end From 6529f4942085b9c0067a655fbf19bd984f31e7bb Mon Sep 17 00:00:00 2001 From: hswick Date: Fri, 28 Sep 2018 20:04:48 -0500 Subject: [PATCH 4/6] Removed the seconds delay parameter --- lib/exw3.ex | 10 ++++------ test/exw3_test.exs | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/exw3.ex b/lib/exw3.ex index 1e4dfc4..546278e 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -471,12 +471,12 @@ defmodule ExW3 do ) end - @spec get_filter_changes(binary(), integer()) :: {:ok, []} + @spec get_filter_changes(binary()) :: {:ok, []} @doc "Using saved information related to the filter id, event logs are formatted properly" - def get_filter_changes(filter_id, seconds \\ 0) do + def get_filter_changes(filter_id) do GenServer.call( ContractManager, - {:get_filter_changes, {filter_id, seconds}} + {:get_filter_changes, filter_id} ) end @@ -807,9 +807,7 @@ defmodule ExW3 do {:reply, {:ok, filter_id}, Map.put(state, :filters, Map.put(state[:filters], filter_id, %{contract_name: contract_name, event_name: event_name}))} end - def handle_call({:get_filter_changes, {filter_id, seconds}}, _from, state) do - - :timer.sleep(1000 * seconds) + def handle_call({:get_filter_changes, filter_id}, _from, state) do filter_info = Map.get(state[:filters], filter_id) event_attributes = get_event_attributes(state, filter_info[:contract_name], filter_info[:event_name]) diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 9ea3674..ed05d2c 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -302,7 +302,7 @@ defmodule EXW3Test do ) # Demonstrating the delay capability - {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id, 1) + {:ok, change_logs} = ExW3.Contract.get_filter_changes(indexed_filter_id) event_log = Enum.at(change_logs, 0) assert event_log |> is_map From 48e9bab1c87ed4e0dc8867aa55e54219cdf26ee0 Mon Sep 17 00:00:00 2001 From: hswick Date: Sat, 29 Sep 2018 12:34:39 -0500 Subject: [PATCH 5/6] Added README docs about continuous event handling --- README.md | 62 ++++++++++++++++++++++++++++------------------ test/exw3_test.exs | 4 +-- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 749861c..df3e3ac 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ end ``` ## Overview -ExW3 is a wrapper around ethereumex to provide a high level, user friendly json rpc api. It currently only supports Http. The primary feature it provides is a handy abstraction for working with smart contracts. +ExW3 is a wrapper around ethereumex to provide a high level, user friendly json rpc api. It currently ONLY supports http. The primary feature of this library is a handy abstraction for working with smart contracts. ## Usage @@ -91,16 +91,16 @@ iex(11)> ExW3.Contract.call(:SimpleStorage, :get) ## Asynchronous -ExW3 now provides async versions of `call` and `send`. They both return a `Task` that can be awaited on. +ExW3 provides async versions of `call` and `send`. They both return a `Task` that can be awaited on. ```elixir - t = ExW3.Contract.call_async(:SimpleStorage, :get) - {:ok, data} = Task.await(t) +t = ExW3.Contract.call_async(:SimpleStorage, :get) +{:ok, data} = Task.await(t) ``` ## Events -ExW3 allows the retrieval of event logs using filters. In this example, assume we have already deployed and registered a contract called :EventTester. +ExW3 allows the retrieval of event logs using filters or transaction receipts. In this example we will demonstrate a filter. Assume we have already deployed and registered a contract called EventTester. ```elixir # We can optionally specify extra parameters like `:fromBlock`, and `:toBlock` @@ -119,37 +119,51 @@ Ethereum allows a user to add topics to filters. This means the filter will only If you have written your event in Solidity like this: ``` - event SimpleIndex(uint256 indexed num, bytes32 indexed data, uint256 otherNum); +event SimpleIndex(uint256 indexed num, bytes32 indexed data, uint256 otherNum); ``` -You can add filter on which logs will be returned back to the RPC client, based on the indexed fields. +You can add a filter on which logs will be returned back to the RPC client based on the indexed fields. -ExW3 allows for 2 ways of specifying these parameters or `topics` in two ways. The first, and probably more preferred way, is with a map: +ExW3 allows for 2 ways of specifying these parameters (`:topics`) in two ways. The first, and probably more preferred way, is with a map: ```elixir - indexed_filter_id = ExW3.Contract.filter( - :EventTester, - "SimpleIndex", - %{ - topics: %{num: 46, data: "Hello, World!"}, - } - ) +indexed_filter_id = ExW3.Contract.filter( + :EventTester, + "SimpleIndex", + %{ + topics: %{num: 46, data: "Hello, World!"}, + } +) ``` -The other option is with a list, but this is order dependent, and any values you don't want to specify must be represented with a `nil`. +The other option is a list (mapped version is an abstraction over this). The downside here is this is order dependent. Any values you don't want to specify must be represented with a `nil`. This approach has been included because it is the implementation of the JSON RPC spec. ```elixir - indexed_filter_id = ExW3.Contract.filter( - :EventTester, - self(), - %{ - topics: [nil, "Hello, World!"] - } - ) +indexed_filter_id = ExW3.Contract.filter( + :EventTester, + "SimpleIndex", + %{ + topics: [nil, "Hello, World!"] + } +) ``` -In this case we are skipping the `num` topic, and only filtering on the `data` parameter. +Here we are skipping the `num` topic, and only filtering on the `data` parameter. + +NOTE!!! These two approaches are mutually exclusive, and for almost all cases you should prefer the map. + +## Continuous Event Handling +In many cases, you will want some process to continuously listen for events. We can implement this functionality using a recursive function. Since Elixir uses tail call optimization, we won't have to worry about blowing up the stack. + +```elixir +def listen_for_event do + {:ok, changes} = ExW3.Contract.get_filter_changes(filter_id) # Get our changes from the blockchain + handle_changes(changes) # Some function to deal with the data. Good place to use pattern matching. + :timer.sleep(1000) # Some delay in milliseconds. Recommended to save bandwidth, and not spam. + listen_for_event # Recurse +end +``` # Compiling Solidity diff --git a/test/exw3_test.exs b/test/exw3_test.exs index ed05d2c..0a1c519 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -186,8 +186,9 @@ defmodule EXW3Test do end test "Testing formatted get filter changes", context do + ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) - + {:ok, address, _} = ExW3.Contract.deploy( :EventTester, @@ -214,7 +215,6 @@ defmodule EXW3Test do {:ok, change_logs} = ExW3.Contract.get_filter_changes(filter_id) - event_log = Enum.at(change_logs, 0) assert event_log |> is_map From b2caaa0595ec6d5d31d7166ecb80cff8bae98ae1 Mon Sep 17 00:00:00 2001 From: hswick Date: Sat, 29 Sep 2018 12:37:58 -0500 Subject: [PATCH 6/6] Updated version to 0.3.0 --- README.md | 4 ++-- mix.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index df3e3ac..6d68c4b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ```elixir def deps do - [{:exw3, "~> 0.2.0"}] + [{:exw3, "~> 0.3.0"}] end ``` ## Overview @@ -161,7 +161,7 @@ def listen_for_event do {:ok, changes} = ExW3.Contract.get_filter_changes(filter_id) # Get our changes from the blockchain handle_changes(changes) # Some function to deal with the data. Good place to use pattern matching. :timer.sleep(1000) # Some delay in milliseconds. Recommended to save bandwidth, and not spam. - listen_for_event # Recurse + listen_for_event() # Recurse end ``` diff --git a/mix.exs b/mix.exs index 4778495..9af57e8 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule ExW3.MixProject do def project do [ app: :exw3, - version: "0.2.0", + version: "0.3.0", elixir: "~> 1.7.2", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod,