Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 61 additions & 27 deletions lib/exw3/contract.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
defmodule ExW3.Contract do
use GenServer

@log_integer_attrs [
"blockNumber",
"logIndex",
"transactionIndex"
]

@doc "Begins the Contract process to manage all interactions with smart contracts"
@spec start_link() :: {:ok, pid()}
def start_link(_ \\ :ok) do
Expand Down Expand Up @@ -73,6 +79,15 @@ defmodule ExW3.Contract do
)
end

@doc "Returns formatted event logs for a registered contract"
@spec get_logs(atom(), map()) :: {:ok, list()}
def get_logs(contract_name, event_data \\ %{}) do
GenServer.call(
ContractManager,
{:get_logs, contract_name, event_data}
)
end

def init(state) do
{:ok, state}
end
Expand Down Expand Up @@ -169,9 +184,7 @@ defmodule ExW3.Contract do
input_types_count = Enum.count(input_types)

if input_types_count != arg_count do
raise "Number of provided arguments to constructor is incorrect. Was given #{
arg_count
} args, looking for #{input_types_count}."
raise "Number of provided arguments to constructor is incorrect. Was given #{arg_count} args, looking for #{input_types_count}."
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mix format

end

bin <>
Expand Down Expand Up @@ -335,9 +348,10 @@ defmodule ExW3.Contract do
if Enum.member?(["latest", "earliest", "pending"], event_data[key]) do
event_data[key]
else
"0x" <>
(ExW3.Abi.encode_data("(uint256)", [event_data[key]])
|> Base.encode16(case: :lower))
event_data[key]
|> Integer.to_string(16)
|> String.downcase()
|> String.replace_prefix("", "0x")
Comment on lines +351 to +354
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to change these lines because most RPC don't accept numbers with leading 0's like:

  Errors encountered in param 0: Invalid value "0x014f7c5b" supplied to : RpcFilterRequest/fromBlock: (QUANTITY | "earliest" | "latest" | "pending") | undefined/0: QUANTITY, Invalid value "0x0000000000000000000000000000000000000000000000000000000000000001" supplied to
: RpcFilterRequest/fromBlock: (QUANTITY | "earliest" | "latest" | "pending") | undefined/1: "earliest" | "latest" | "pending", Invalid value "0x0000000000000000000000000000000000000000000000000000000000000001" supplied to : RpcFilterRequest/toBlock: (QUANTITY | "earlie
st" | "latest" | "pending") | undefined/0: QUANTITY, Invalid value "0x0000000000000000000000000000000000000000000000000000000000000002" supplied to : RpcFilterRequest/toBlock: (QUANTITY | "earliest" | "latest" | "pending") | undefined/1: "earliest" | "latest" | "pendin
g"

Tested against hardhat native node & https://polygon-rpc.com & alchemy.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember going about adding the 0x prefix a long time ago to fix other bugs. Maybe it isn't needed for logs.

@rupurt do you think this would break on other eth clients?

end

Map.put(event_data, key, new_param)
Expand All @@ -362,6 +376,10 @@ defmodule ExW3.Contract do
Enum.zip(names, ExW3.Abi.decode_event(data, signature)) |> Enum.into(%{})
end

defp format_log_data(_log, event_attributes) when event_attributes == %{} do
%{}
end

defp format_log_data(log, event_attributes) do
non_indexed_fields =
extract_non_indexed_fields(
Expand Down Expand Up @@ -394,6 +412,16 @@ defmodule ExW3.Contract do
Map.put(log, "data", new_data)
end

defp format_log(log, event_attributes) do
Enum.reduce(
[
ExW3.Normalize.transform_to_integer(log, @log_integer_attrs),
format_log_data(log, event_attributes)
],
&Map.merge/2
)
end

def handle_call({:filter, {contract_name, event_name, event_data}}, _from, state) do
contract_info = state[contract_name]

Expand Down Expand Up @@ -428,29 +456,35 @@ defmodule ExW3.Contract do
event_attributes =
get_event_attributes(state, filter_info[:contract_name], filter_info[:event_name])

logs = ExW3.Rpc.get_filter_changes(filter_id)

formatted_logs =
if logs != [] do
Enum.map(logs, fn log ->
formatted_log =
Enum.reduce(
[
ExW3.Normalize.transform_to_integer(log, [
"blockNumber",
"logIndex",
"transactionIndex"
]),
format_log_data(log, event_attributes)
],
&Map.merge/2
)
filter_id
|> ExW3.Rpc.get_filter_changes()
|> Enum.map(&format_log(&1, event_attributes))

formatted_log
end)
else
logs
end
{:reply, {:ok, formatted_logs}, state}
end

def handle_call({:get_logs, contract_name, event_data}, _from, state) do
contract_info = state[contract_name]

{:ok, logs} =
event_data
|> Map.merge(%{address: contract_info[:address]})
|> event_data_format_helper()
|> ExW3.Rpc.get_logs()

formatted_logs =
logs
|> Enum.map(fn log ->
# Per definition:
# "The first topic usually consists of the signature (a keccak256 hash)
# of the name of the event that occurred."
# But just in case we use find
event_topic = log["topics"] |> Enum.find(fn t -> contract_info[:events][t] end)
event_attributes = contract_info[:events] |> Map.get(event_topic, %{})

log |> format_log(event_attributes)
end)

{:reply, {:ok, formatted_logs}, state}
end
Expand Down
2 changes: 1 addition & 1 deletion test/exw3/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule ExW3.ClientTest do
test ".call_client/2 calls the JSON-RPC method with the given arguments" do
assert {:ok, accounts} = ExW3.Client.call_client(:eth_accounts)
assert Enum.count(accounts) > 0
assert ["0x" <> _ = account] = accounts
assert ["0x" <> _ = account | _] = accounts
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When hardhat node or any other pre-populated you always have more than 1 account so, just ignore it =)


assert {:ok, balance} = ExW3.Client.call_client(:eth_get_balance, [account])
assert "0x" <> _ = balance
Expand Down
105 changes: 104 additions & 1 deletion test/exw3_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ defmodule ExW3Test do
ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0)})
end

test ".get_logs/1", context do
test "Rpc.get_logs/1", context do
ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi])

{:ok, address, _} =
Expand Down Expand Up @@ -442,4 +442,107 @@ defmodule ExW3Test do
log = Enum.at(logs, 0)
assert log["transactionHash"] == simple_tx_hash
end

test "Testing formatted get logs", context do
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on get_filter_changes test

ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi])

{:ok, address, _} =
ExW3.Contract.deploy(
:EventTester,
bin: ExW3.Abi.load_bin("test/examples/build/EventTester.bin"),
options: %{
gas: 300_000,
from: Enum.at(context[:accounts], 0)
}
)

ExW3.Contract.at(:EventTester, address)

{:ok, block_number} = ExW3.block_number()

# Test non indexed events

{:ok, []} = ExW3.Contract.get_logs(:EventTester, %{fromBlock: block_number})

{:ok, _tx_hash} =
ExW3.Contract.send(
:EventTester,
:simple,
["Hello, World!"],
%{from: Enum.at(context[:accounts], 0), gas: 30_000}
)

{:ok, [event_log]} = ExW3.Contract.get_logs(:EventTester, %{fromBlock: block_number})

assert event_log |> is_map
log_data = Map.get(event_log, "data")
assert log_data |> is_map
assert Map.get(log_data, "num") == 42
assert ExW3.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!"

# Test indexed events

{:ok, _tx_hash} =
ExW3.Contract.send(
:EventTester,
:simpleIndex,
["Hello, World!"],
%{from: Enum.at(context[:accounts], 0), gas: 30_000}
)

{:ok, logs} = ExW3.Contract.get_logs(:EventTester, %{fromBlock: block_number})

event_log = logs |> List.last()

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.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!"
assert Map.get(log_data, "otherNum") == 42

# # Test Indexing Indexed Events

{:ok, _tx_hash} =
ExW3.Contract.send(
:EventTester,
:simpleIndex,
["Hello, World!"],
%{from: Enum.at(context[:accounts], 0), gas: 30_000}
)

{:ok, logs} = ExW3.Contract.get_logs(:EventTester, %{fromBlock: block_number})

event_log = logs |> List.last()
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.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!"
assert Map.get(log_data, "otherNum") == 42

# Tests filter with map params

{:ok, _tx_hash} =
ExW3.Contract.send(
:EventTester,
:simpleIndex,
["Hello, World!"],
%{from: Enum.at(context[:accounts], 0), gas: 30_000}
)

{:ok, logs} =
ExW3.Contract.get_logs(:EventTester, %{
fromBlock: block_number,
topics: %{num: 46, data: "Hello, World!"}
})

event_log = logs |> List.last()
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.Utils.bytes_to_string(Map.get(log_data, "data")) == "Hello, World!"
assert Map.get(log_data, "otherNum") == 42
end
end