From 4448f93e37a638b45195eb23f150d40ad1ad243d Mon Sep 17 00:00:00 2001 From: hswick Date: Wed, 19 Sep 2018 16:49:10 -0500 Subject: [PATCH] Using a single process to manage smart contract interactions --- lib/exw3.ex | 134 +++++++++++++++++++++++++++------------------ test/exw3_test.exs | 95 +++++++++++++++++--------------- 2 files changed, 130 insertions(+), 99 deletions(-) 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