diff --git a/README.md b/README.md index 9a66a2b..3175410 100644 --- a/README.md +++ b/README.md @@ -44,22 +44,13 @@ Currently, ExW3 supports a handful of json rpc commands. Mostly just the useful Check out the [documentation](https://hexdocs.pm/exw3/ExW3.html) -```elixir +``` iex(1)> accounts = ExW3.accounts() -["0xb5c17637ccc1a5d91715429de76949fbe49d36f0", - "0xecf00f60a29acf81d7fdf696fd2ca1fa82b623b0", - "0xbf11365685e07ad86387098f27204700d7568ee2", - "0xba76d611c29fb25158e5a7409cb627cf1bd220cf", - "0xbb209f51ef097cc5ca320264b5373a48f7ee0fba", - "0x31b7a2c8b2f82a92bf4cb5fd13971849c6c956fc", - "0xeb943cee8ec3723ab3a06e45dc2a75a3caa04288", - "0x59315d9706ac567d01860d7ede03720876972162", - "0x4dbd23f361a4df1ef5e517b68e099bf2fcc77b10", - "0x150eb320428b9bc93453b850b4ea454a35308f17"] +["0x00a329c0648769a73afac7f9381e08fb43dbea72"] iex(2)> ExW3.balance(Enum.at(accounts, 0)) -99999999999962720359 +1606938044258990275541962092341162602522200978938292835291376 iex(3)> ExW3.block_number() -835 +1252 iex(4)> simple_storage_abi = ExW3.load_abi("test/examples/build/SimpleStorage.abi") %{ "get" => %{ @@ -81,19 +72,22 @@ iex(4)> simple_storage_abi = ExW3.load_abi("test/examples/build/SimpleStorage.ab "type" => "function" } } -iex(5)> ExW3.Contract.start_link(SimpleStorage, abi: simple_storage_abi) -{:ok, #PID<0.239.0>} -iex(6)> {:ok, address, tx_hash} = ExW3.Contract.deploy(SimpleStorage, bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), options: %{gas: 300_000, from: Enum.at(accounts, 0)}) -{:ok, "0xd99306b81bd61cb0ecdd3f2c946af513b3395088"} -iex(7)> ExW3.Contract.at(SimpleStorage, address) +iex(5)> ExW3.Contract.start_link +{:ok, #PID<0.265.0>} +iex(6)> ExW3.Contract.register(:SimpleStorage, abi: simple_storage_abi) :ok -iex(8)> ExW3.Contract.call(SimpleStorage, :get) +iex(7)> {:ok, address, tx_hash} = ExW3.Contract.deploy(:SimpleStorage, bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), options: %{gas: 300_000, from: Enum.at(accounts, 0)}) +{:ok, "0x22018c2bb98387a39e864cf784e76cb8971889a5", + "0x4ea539048c01194476004ef69f407a10628bed64e88ee8f8b17b4d030d0e7cb7"} +iex(8)> ExW3.Contract.at(:SimpleStorage, address) +:ok +iex(9)> ExW3.Contract.call(:SimpleStorage, :get) {:ok, 0} -iex(9)> ExW3.Contract.send(SimpleStorage, :set, [1], %{from: Enum.at(accounts, 0), gas: 50_000}) -{:ok, "0xb7e9cbdd2cec8ca017e675059a3af063d754496c960f156e1a41fe51ea82f3b8"} -iex(10)> ExW3.Contract.call(SimpleStorage, :get) +iex(10)> ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(accounts, 0), gas: 50_000}) +{:ok, "0x88838e84a401a1d6162290a1a765507c4a83f5e050658a83992a912f42149ca5"} +iex(11)> ExW3.Contract.call(:SimpleStorage, :get) {:ok, 1} -``` +``` ## Listening for Events @@ -105,15 +99,13 @@ Whenever a change is detected it will send a message to whichever process is lis # 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. +# 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. -filter_id = ExW3.Contract.filter(EventTester, "Simple", self()) +filter_id = ExW3.Contract.filter(:EventTester, "Simple", self()) -# 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. +# 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 @@ -121,7 +113,7 @@ 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. +# 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 diff --git a/lib/exw3.ex b/lib/exw3.ex index 3a000ba..0403130 100644 --- a/lib/exw3.ex +++ b/lib/exw3.ex @@ -454,60 +454,62 @@ defmodule ExW3 do # Client - @spec start_link(atom(), list()) :: {:ok, pid()} - @doc "Begins a Contract GenServer with specified name and state" - def start_link(name, state) do - GenServer.start_link(__MODULE__, state, name: name) + @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) end - @spec deploy(pid(), []) :: {:ok, []} + @spec deploy(keyword(), []) :: {:ok, binary(), []} @doc "Deploys contracts with given arguments" - def deploy(pid, args) do - GenServer.call(pid, {:deploy, args}) + def deploy(name, args) do + GenServer.call(ContractManager, {:deploy, {name, args}}) end - @spec at(pid(), binary()) :: :ok - @doc "Sets the current Contract GenServer's address to given address" - def at(pid, address) do - GenServer.cast(pid, {:at, address}) + @spec register(keyword(), []) :: :ok + @doc "Registers the contract with the ContractManager process. Only :abi is required field." + def register(name, contract_info) do + GenServer.cast(ContractManager, {:register, {name, contract_info}}) end - @spec address(pid()) :: {:ok, binary()} + @spec at(keyword(), binary()) :: :ok + @doc "Sets the address for the contract specified by the name argument" + def at(name, address) do + GenServer.cast(ContractManager, {:at, {name, address}}) + end + + @spec address(keyword()) :: {:ok, binary()} @doc "Returns the current Contract GenServer's address" - def address(pid) do - GenServer.call(pid, :address) + def address(name) do + GenServer.call(ContractManager, {:address, name}) end - @spec call(pid(), keyword(), []) :: {:ok, any()} + @spec call(keyword(), keyword(), []) :: {:ok, any()} @doc "Use a Contract's method with an eth_call" - def call(pid, method_name, args \\ []) do - GenServer.call(pid, {:call, {method_name, args}}) + def call(contract_name, method_name, args \\ []) do + GenServer.call(ContractManager, {:call, {contract_name, method_name, args}}) end - @spec send(pid(), keyword(), [], %{}) :: {:ok, binary()} + @spec send(keyword(), keyword(), [], %{}) :: {:ok, binary()} @doc "Use a Contract's method with an eth_sendTransaction" - def send(pid, method_name, args, options) do - GenServer.call(pid, {:send, {method_name, args, options}}) + def send(contract_name, method_name, args, options) do + GenServer.call(ContractManager, {:send, {contract_name, method_name, args, options}}) end - @spec tx_receipt(pid(), binary()) :: %{} + @spec tx_receipt(keyword(), binary()) :: %{} @doc "Returns a formatted transaction receipt for the given transaction hash(id)" - def tx_receipt(pid, tx_hash) do - GenServer.call(pid, {:tx_receipt, tx_hash}) + def tx_receipt(contract_name, tx_hash) do + GenServer.call(ContractManager, {:tx_receipt, {contract_name, tx_hash}}) end - def filter(pid, event_name, other_pid, event_data \\ %{}) do - GenServer.call(pid, {:filter, {event_name, other_pid, event_data}}) + def filter(contract_name, event_name, other_pid, event_data \\ %{}) do + GenServer.call(ContractManager, {:filter, {contract_name, event_name, other_pid, event_data}}) end # Server def init(state) do - if state[:abi] do - {:ok, state ++ init_events(state[:abi])} - else - raise "ABI not provided upon initialization" - end + {:ok, state} end defp init_events(abi) do @@ -611,6 +613,13 @@ defmodule ExW3 do ) end + defp add_helper(contract_info) do + if contract_info[:abi] do + contract_info ++ init_events(contract_info[:abi]) + else + raise "ABI not provided upon initialization" + end + end # Options' checkers @@ -622,66 +631,83 @@ defmodule ExW3 do # Casts - def handle_cast({:at, address}, state) do - {:noreply, [{:address, address} | state]} + def handle_cast({:at, {name, address}}, state) do + contract_info = state[name] + {:noreply, Map.put(state, name, contract_info ++ [address: address])} end - def handle_call({:filter, {event_name, other_pid, event_data}}, _from, state) do + def handle_cast({:register, {name, contract_info}}, state) do + {:noreply, Map.put(state, name, add_helper(contract_info))} + end + + # Calls + + def handle_call({:filter, {contract_name, event_name, other_pid, event_data}}, _from, state) do + + contract_info = state[contract_name] + 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) + + payload = Map.merge(%{address: contract_info[:address], topics: [contract_info[:event_names][event_name]]}, event_data) filter_id = ExW3.new_filter(payload) - event_signature = state[:events][state[:event_names][event_name]][:signature] - event_fields = state[:events][state[:event_names][event_name]][:names] + event_signature = contract_info[:events][contract_info[:event_names][event_name]][:signature] + event_fields = contract_info[:events][contract_info[:event_names][event_name]][:names] EventListener.filter(filter_id, event_signature, event_fields, other_pid) - {:reply, filter_id, state ++ [event_name, filter_id]} + {:reply, filter_id, Map.put(state, contract_name, contract_info ++ [event_name, filter_id])} end - # Calls + def handle_call({:deploy, {name, args}}, _from, state) do - def handle_call({:deploy, args}, _from, state) do + contract_info = state[name] + with {:ok, _} <- check_option(args[:options][:from], :missing_sender), {:ok,_} <- check_option(args[:options][:gas], :missing_gas), {:ok, bin} <- check_option([state[:bin], args[:bin]], :missing_binary) - do - {contract_addr, tx_hash} = deploy_helper(bin, state[:abi], args) + do + {contract_addr, tx_hash} = deploy_helper(bin, contract_info[:abi], args) result = {:ok, contract_addr, tx_hash} {:reply, result , state} - else - err -> {:reply, err, state} - end + else + err -> {:reply, err, state} + end end - def handle_call(:address, _from, state) do - {:reply, state[:address], state} + def handle_call({:address, name}, _from, state) do + {:reply, state[name][:address], state} end - def handle_call({:call, {method_name, args}}, _from, state) do - with {:ok, address} <- check_option(state[:address], :missing_address) + def handle_call({:call, {contract_name, method_name, args}}, _from, state) do + contract_info = state[contract_name] + + with {:ok, address} <- check_option(contract_info[:address], :missing_address) do - result = eth_call_helper(address, state[:abi], Atom.to_string(method_name), args) + result = eth_call_helper(address, contract_info[:abi], Atom.to_string(method_name), args) {:reply, result, state} else err -> {:reply, err, state} end end - def handle_call({:send, {method_name, args, options}}, _from, state) do - with {:ok, address} <- check_option(state[:address], :missing_address), + def handle_call({:send, {contract_name, method_name, args, options}}, _from, state) do + contract_info = state[contract_name] + with {:ok, address} <- check_option(contract_info[:address], :missing_address), {:ok, _} <- check_option(options[:from], :missing_sender), {:ok, _} <- check_option(options[:gas], :missing_gas) do - result = eth_send_helper(address, state[:abi], Atom.to_string(method_name), args, options) + result = eth_send_helper(address, contract_info[:abi], Atom.to_string(method_name), args, options) {:reply, result, state} else err -> {:reply, err, state} end end - def handle_call({:tx_receipt, tx_hash}, _from, state) do + def handle_call({:tx_receipt, {contract_name, tx_hash}}, _from, state) do + contract_info = state[contract_name] + receipt = ExW3.tx_receipt(tx_hash) - events = state[:events] + events = contract_info[:events] logs = receipt["logs"] formatted_logs = diff --git a/test/exw3_test.exs b/test/exw3_test.exs index 33d9d18..34f61eb 100644 --- a/test/exw3_test.exs +++ b/test/exw3_test.exs @@ -3,6 +3,8 @@ defmodule EXW3Test do doctest ExW3 setup_all do + ExW3.Contract.start_link + %{ simple_storage_abi: ExW3.load_abi("test/examples/build/SimpleStorage.abi"), array_tester_abi: ExW3.load_abi("test/examples/build/ArrayTester.abi"), @@ -58,11 +60,13 @@ defmodule EXW3Test do end test "starts a Contract GenServer for simple storage contract", context do - ExW3.Contract.start_link(SimpleStorage, abi: context[:simple_storage_abi]) + + + ExW3.Contract.register(:SimpleStorage, abi: context[:simple_storage_abi]) {:ok, address, _} = ExW3.Contract.deploy( - SimpleStorage, + :SimpleStorage, bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ @@ -71,27 +75,27 @@ defmodule EXW3Test do } ) - ExW3.Contract.at(SimpleStorage, address) + ExW3.Contract.at(:SimpleStorage, address) - assert address == ExW3.Contract.address(SimpleStorage) + assert address == ExW3.Contract.address(:SimpleStorage) - {:ok, data} = ExW3.Contract.call(SimpleStorage, :get) + {:ok, data} = ExW3.Contract.call(:SimpleStorage, :get) assert data == 0 - ExW3.Contract.send(SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0), gas: 50_000}) + ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0), gas: 50_000}) - {:ok, data} = ExW3.Contract.call(SimpleStorage, :get) + {:ok, data} = ExW3.Contract.call(:SimpleStorage, :get) assert data == 1 end test "starts a Contract GenServer for array tester contract", context do - ExW3.Contract.start_link(ArrayTester, abi: context[:array_tester_abi]) + ExW3.Contract.register(:ArrayTester, abi: context[:array_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( - ArrayTester, + :ArrayTester, bin: ExW3.load_bin("test/examples/build/ArrayTester.bin"), options: %{ gas: 300_000, @@ -99,27 +103,27 @@ defmodule EXW3Test do } ) - ExW3.Contract.at(ArrayTester, address) + ExW3.Contract.at(:ArrayTester, address) - assert address == ExW3.Contract.address(ArrayTester) + assert address == ExW3.Contract.address(:ArrayTester) arr = [1, 2, 3, 4, 5] - {:ok, result} = ExW3.Contract.call(ArrayTester, :staticUint, [arr]) + {:ok, result} = ExW3.Contract.call(:ArrayTester, :staticUint, [arr]) assert result == arr - {:ok, result} = ExW3.Contract.call(ArrayTester, :dynamicUint, [arr]) + {:ok, result} = ExW3.Contract.call(:ArrayTester, :dynamicUint, [arr]) assert result == arr end test "starts a Contract GenServer for event tester contract", context do - ExW3.Contract.start_link(EventTester, abi: context[:event_tester_abi]) + ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( - EventTester, + :EventTester, bin: ExW3.load_bin("test/examples/build/EventTester.bin"), options: %{ gas: 300_000, @@ -127,17 +131,17 @@ defmodule EXW3Test do } ) - ExW3.Contract.at(EventTester, address) + ExW3.Contract.at(:EventTester, address) - assert address == ExW3.Contract.address(EventTester) + assert address == ExW3.Contract.address(:EventTester) {:ok, tx_hash} = - ExW3.Contract.send(EventTester, :simple, ["Hello, World!"], %{ + ExW3.Contract.send(:EventTester, :simple, ["Hello, World!"], %{ from: Enum.at(context[:accounts], 0), gas: 30_000 }) - {:ok, {receipt, logs}} = ExW3.Contract.tx_receipt(EventTester, tx_hash) + {:ok, {receipt, logs}} = ExW3.Contract.tx_receipt(:EventTester, tx_hash) assert receipt |> is_map @@ -151,11 +155,11 @@ defmodule EXW3Test do end test "starts a Contract GenServer and uses the event listener", context do - ExW3.Contract.start_link(EventTester, abi: context[:event_tester_abi]) + ExW3.Contract.register(:EventTester, abi: context[:event_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( - EventTester, + :EventTester, bin: ExW3.load_bin("test/examples/build/EventTester.bin"), options: %{ gas: 300_000, @@ -163,17 +167,17 @@ defmodule EXW3Test do } ) - ExW3.Contract.at(EventTester, address) + ExW3.Contract.at(:EventTester, address) {:ok, agent} = Agent.start_link(fn -> [] end) ExW3.EventListener.start_link() - filter_id = ExW3.Contract.filter(EventTester, "Simple", self()) + filter_id = ExW3.Contract.filter(:EventTester, "Simple", self()) {:ok, _tx_hash} = ExW3.Contract.send( - EventTester, + :EventTester, :simple, ["Hello, World!"], %{from: Enum.at(context[:accounts], 0), gas: 30_000} @@ -189,6 +193,7 @@ defmodule EXW3Test do state = Agent.get(agent, fn list -> list end) event_log = Enum.at(state, 0) assert event_log |> is_map + log_data = Map.get(event_log, "data") assert log_data |> is_map assert Map.get(log_data, "num") == 42 @@ -198,11 +203,11 @@ defmodule EXW3Test do end test "starts a Contract GenServer for Complex contract", context do - ExW3.Contract.start_link(Complex, abi: context[:complex_abi]) + ExW3.Contract.register(:Complex, abi: context[:complex_abi]) {:ok, address, _} = ExW3.Contract.deploy( - Complex, + :Complex, bin: ExW3.load_bin("test/examples/build/Complex.bin"), args: [42, "Hello, world!"], options: %{ @@ -211,11 +216,11 @@ defmodule EXW3Test do } ) - ExW3.Contract.at(Complex, address) + ExW3.Contract.at(:Complex, address) - assert address == ExW3.Contract.address(Complex) + assert address == ExW3.Contract.address(:Complex) - {:ok, foo, foobar} = ExW3.Contract.call(Complex, :getBoth) + {:ok, foo, foobar} = ExW3.Contract.call(:Complex, :getBoth) assert foo == 42 @@ -223,11 +228,11 @@ defmodule EXW3Test do end test "starts a Contract GenServer for AddressTester contract", context do - ExW3.Contract.start_link(AddressTester, abi: context[:address_tester_abi]) + ExW3.Contract.register(:AddressTester, abi: context[:address_tester_abi]) {:ok, address, _} = ExW3.Contract.deploy( - AddressTester, + :AddressTester, bin: ExW3.load_bin("test/examples/build/AddressTester.bin"), options: %{ from: Enum.at(context[:accounts], 0), @@ -235,15 +240,15 @@ defmodule EXW3Test do } ) - ExW3.Contract.at(AddressTester, address) + ExW3.Contract.at(:AddressTester, address) - assert address == ExW3.Contract.address(AddressTester) + assert address == ExW3.Contract.address(:AddressTester) formatted_address = Enum.at(context[:accounts], 0) |> ExW3.format_address - {:ok, same_address} = ExW3.Contract.call(AddressTester, :get, [formatted_address]) + {:ok, same_address} = ExW3.Contract.call(:AddressTester, :get, [formatted_address]) assert ExW3.to_address(same_address) == Enum.at(context[:accounts], 0) end @@ -283,11 +288,11 @@ defmodule EXW3Test do test "returns proper error messages at contract deployment", context do - ExW3.Contract.start_link(SimpleStorage, abi: context[:simple_storage_abi]) + ExW3.Contract.register(:SimpleStorage, abi: context[:simple_storage_abi]) assert {:error, :missing_gas} == ExW3.Contract.deploy( - SimpleStorage, + :SimpleStorage, bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ @@ -297,7 +302,7 @@ defmodule EXW3Test do assert {:error, :missing_sender} == ExW3.Contract.deploy( - SimpleStorage, + :SimpleStorage, bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ @@ -307,7 +312,7 @@ defmodule EXW3Test do assert {:error, :missing_binary} == ExW3.Contract.deploy( - SimpleStorage, + :SimpleStorage, args: [], options: %{ gas: 300_000, @@ -318,11 +323,11 @@ defmodule EXW3Test do end test "return proper error messages at send and call", context do - ExW3.Contract.start_link(SimpleStorage, abi: context[:simple_storage_abi]) + ExW3.Contract.register(:SimpleStorage, abi: context[:simple_storage_abi]) {:ok, address, _} = ExW3.Contract.deploy( - SimpleStorage, + :SimpleStorage, bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), args: [], options: %{ @@ -331,13 +336,13 @@ defmodule EXW3Test do } ) - assert {:error, :missing_address} == ExW3.Contract.call(SimpleStorage, :get) - assert {:error, :missing_address} == ExW3.Contract.send(SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0), gas: 50_000}) + assert {:error, :missing_address} == ExW3.Contract.call(:SimpleStorage, :get) + assert {:error, :missing_address} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0), gas: 50_000}) - ExW3.Contract.at(SimpleStorage, address) + ExW3.Contract.at(:SimpleStorage, address) - assert {:error, :missing_sender} == ExW3.Contract.send(SimpleStorage, :set, [1], %{gas: 50_000}) - assert {:error, :missing_gas} == ExW3.Contract.send(SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0)}) + assert {:error, :missing_sender} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{gas: 50_000}) + assert {:error, :missing_gas} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0)}) end