4
0
Fork 0

Using a single process to manage smart contract interactions

pull/18/head
hswick 7 years ago
parent eb41fa5659
commit 4448f93e37
  1. 134
      lib/exw3.ex
  2. 95
      test/exw3_test.exs

@ -454,60 +454,62 @@ defmodule ExW3 do
# Client # Client
@spec start_link(atom(), list()) :: {:ok, pid()} @spec start_link() :: {:ok, pid()}
@doc "Begins a Contract GenServer with specified name and state" @doc "Begins the Contract process to manage all interactions with smart contracts"
def start_link(name, state) do def start_link() do
GenServer.start_link(__MODULE__, state, name: name) GenServer.start_link(__MODULE__, %{}, name: ContractManager)
end end
@spec deploy(pid(), []) :: {:ok, []} @spec deploy(keyword(), []) :: {:ok, binary(), []}
@doc "Deploys contracts with given arguments" @doc "Deploys contracts with given arguments"
def deploy(pid, args) do def deploy(name, args) do
GenServer.call(pid, {:deploy, args}) GenServer.call(ContractManager, {:deploy, {name, args}})
end end
@spec at(pid(), binary()) :: :ok @spec register(keyword(), []) :: :ok
@doc "Sets the current Contract GenServer's address to given address" @doc "Registers the contract with the ContractManager process. Only :abi is required field."
def at(pid, address) do def register(name, contract_info) do
GenServer.cast(pid, {:at, address}) GenServer.cast(ContractManager, {:register, {name, contract_info}})
end 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" @doc "Returns the current Contract GenServer's address"
def address(pid) do def address(name) do
GenServer.call(pid, :address) GenServer.call(ContractManager, {:address, name})
end end
@spec call(pid(), keyword(), []) :: {:ok, any()} @spec call(keyword(), keyword(), []) :: {:ok, any()}
@doc "Use a Contract's method with an eth_call" @doc "Use a Contract's method with an eth_call"
def call(pid, method_name, args \\ []) do def call(contract_name, method_name, args \\ []) do
GenServer.call(pid, {:call, {method_name, args}}) GenServer.call(ContractManager, {:call, {contract_name, method_name, args}})
end end
@spec send(pid(), keyword(), [], %{}) :: {:ok, binary()} @spec send(keyword(), keyword(), [], %{}) :: {:ok, binary()}
@doc "Use a Contract's method with an eth_sendTransaction" @doc "Use a Contract's method with an eth_sendTransaction"
def send(pid, method_name, args, options) do def send(contract_name, method_name, args, options) do
GenServer.call(pid, {:send, {method_name, args, options}}) GenServer.call(ContractManager, {:send, {contract_name, method_name, args, options}})
end end
@spec tx_receipt(pid(), binary()) :: %{} @spec tx_receipt(keyword(), binary()) :: %{}
@doc "Returns a formatted transaction receipt for the given transaction hash(id)" @doc "Returns a formatted transaction receipt for the given transaction hash(id)"
def tx_receipt(pid, tx_hash) do def tx_receipt(contract_name, tx_hash) do
GenServer.call(pid, {:tx_receipt, tx_hash}) GenServer.call(ContractManager, {:tx_receipt, {contract_name, tx_hash}})
end end
def filter(pid, event_name, other_pid, event_data \\ %{}) do def filter(contract_name, event_name, other_pid, event_data \\ %{}) do
GenServer.call(pid, {:filter, {event_name, other_pid, event_data}}) GenServer.call(ContractManager, {:filter, {contract_name, event_name, other_pid, event_data}})
end end
# Server # Server
def init(state) do def init(state) do
if state[:abi] do {:ok, state}
{:ok, state ++ init_events(state[:abi])}
else
raise "ABI not provided upon initialization"
end
end end
defp init_events(abi) do defp init_events(abi) do
@ -611,6 +613,13 @@ defmodule ExW3 do
) )
end 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 # Options' checkers
@ -622,66 +631,83 @@ defmodule ExW3 do
# Casts # Casts
def handle_cast({:at, address}, state) do def handle_cast({:at, {name, address}}, state) do
{:noreply, [{:address, address} | state]} contract_info = state[name]
{:noreply, Map.put(state, name, contract_info ++ [address: address])}
end 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 unless Process.whereis(Listener) do
raise "EventListener process not alive. Call ExW3.EventListener.start_link before using ExW3.Contract.subscribe" raise "EventListener process not alive. Call ExW3.EventListener.start_link before using ExW3.Contract.subscribe"
end 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) filter_id = ExW3.new_filter(payload)
event_signature = state[:events][state[:event_names][event_name]][:signature] event_signature = contract_info[:events][contract_info[:event_names][event_name]][:signature]
event_fields = state[:events][state[:event_names][event_name]][:names] event_fields = contract_info[:events][contract_info[:event_names][event_name]][:names]
EventListener.filter(filter_id, event_signature, event_fields, other_pid) 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 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), with {:ok, _} <- check_option(args[:options][:from], :missing_sender),
{:ok,_} <- check_option(args[:options][:gas], :missing_gas), {:ok,_} <- check_option(args[:options][:gas], :missing_gas),
{:ok, bin} <- check_option([state[:bin], args[:bin]], :missing_binary) {:ok, bin} <- check_option([state[:bin], args[:bin]], :missing_binary)
do do
{contract_addr, tx_hash} = deploy_helper(bin, state[:abi], args) {contract_addr, tx_hash} = deploy_helper(bin, contract_info[:abi], args)
result = {:ok, contract_addr, tx_hash} result = {:ok, contract_addr, tx_hash}
{:reply, result , state} {:reply, result , state}
else else
err -> {:reply, err, state} err -> {:reply, err, state}
end end
end end
def handle_call(:address, _from, state) do def handle_call({:address, name}, _from, state) do
{:reply, state[:address], state} {:reply, state[name][:address], state}
end end
def handle_call({:call, {method_name, args}}, _from, state) do def handle_call({:call, {contract_name, method_name, args}}, _from, state) do
with {:ok, address} <- check_option(state[:address], :missing_address) contract_info = state[contract_name]
with {:ok, address} <- check_option(contract_info[:address], :missing_address)
do 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} {:reply, result, state}
else else
err -> {:reply, err, state} err -> {:reply, err, state}
end end
end end
def handle_call({:send, {method_name, args, options}}, _from, state) do def handle_call({:send, {contract_name, method_name, args, options}}, _from, state) do
with {:ok, address} <- check_option(state[:address], :missing_address), 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[:from], :missing_sender),
{:ok, _} <- check_option(options[:gas], :missing_gas) {:ok, _} <- check_option(options[:gas], :missing_gas)
do 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} {:reply, result, state}
else else
err -> {:reply, err, state} err -> {:reply, err, state}
end end
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) receipt = ExW3.tx_receipt(tx_hash)
events = state[:events] events = contract_info[:events]
logs = receipt["logs"] logs = receipt["logs"]
formatted_logs = formatted_logs =

@ -3,6 +3,8 @@ defmodule EXW3Test do
doctest ExW3 doctest ExW3
setup_all do setup_all do
ExW3.Contract.start_link
%{ %{
simple_storage_abi: ExW3.load_abi("test/examples/build/SimpleStorage.abi"), simple_storage_abi: ExW3.load_abi("test/examples/build/SimpleStorage.abi"),
array_tester_abi: ExW3.load_abi("test/examples/build/ArrayTester.abi"), array_tester_abi: ExW3.load_abi("test/examples/build/ArrayTester.abi"),
@ -58,11 +60,13 @@ defmodule EXW3Test do
end end
test "starts a Contract GenServer for simple storage contract", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
SimpleStorage, :SimpleStorage,
bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"),
args: [], args: [],
options: %{ 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 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 assert data == 1
end end
test "starts a Contract GenServer for array tester contract", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
ArrayTester, :ArrayTester,
bin: ExW3.load_bin("test/examples/build/ArrayTester.bin"), bin: ExW3.load_bin("test/examples/build/ArrayTester.bin"),
options: %{ options: %{
gas: 300_000, 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] 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 assert result == arr
{:ok, result} = ExW3.Contract.call(ArrayTester, :dynamicUint, [arr]) {:ok, result} = ExW3.Contract.call(:ArrayTester, :dynamicUint, [arr])
assert result == arr assert result == arr
end end
test "starts a Contract GenServer for event tester contract", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
EventTester, :EventTester,
bin: ExW3.load_bin("test/examples/build/EventTester.bin"), bin: ExW3.load_bin("test/examples/build/EventTester.bin"),
options: %{ options: %{
gas: 300_000, 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} = {:ok, tx_hash} =
ExW3.Contract.send(EventTester, :simple, ["Hello, World!"], %{ ExW3.Contract.send(:EventTester, :simple, ["Hello, World!"], %{
from: Enum.at(context[:accounts], 0), from: Enum.at(context[:accounts], 0),
gas: 30_000 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 assert receipt |> is_map
@ -151,11 +155,11 @@ defmodule EXW3Test do
end end
test "starts a Contract GenServer and uses the event listener", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
EventTester, :EventTester,
bin: ExW3.load_bin("test/examples/build/EventTester.bin"), bin: ExW3.load_bin("test/examples/build/EventTester.bin"),
options: %{ options: %{
gas: 300_000, 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) {:ok, agent} = Agent.start_link(fn -> [] end)
ExW3.EventListener.start_link() ExW3.EventListener.start_link()
filter_id = ExW3.Contract.filter(EventTester, "Simple", self()) filter_id = ExW3.Contract.filter(:EventTester, "Simple", self())
{:ok, _tx_hash} = {:ok, _tx_hash} =
ExW3.Contract.send( ExW3.Contract.send(
EventTester, :EventTester,
:simple, :simple,
["Hello, World!"], ["Hello, World!"],
%{from: Enum.at(context[:accounts], 0), gas: 30_000} %{from: Enum.at(context[:accounts], 0), gas: 30_000}
@ -189,6 +193,7 @@ defmodule EXW3Test do
state = Agent.get(agent, fn list -> list end) state = Agent.get(agent, fn list -> list end)
event_log = Enum.at(state, 0) event_log = Enum.at(state, 0)
assert event_log |> is_map assert event_log |> is_map
log_data = Map.get(event_log, "data") log_data = Map.get(event_log, "data")
assert log_data |> is_map assert log_data |> is_map
assert Map.get(log_data, "num") == 42 assert Map.get(log_data, "num") == 42
@ -198,11 +203,11 @@ defmodule EXW3Test do
end end
test "starts a Contract GenServer for Complex contract", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
Complex, :Complex,
bin: ExW3.load_bin("test/examples/build/Complex.bin"), bin: ExW3.load_bin("test/examples/build/Complex.bin"),
args: [42, "Hello, world!"], args: [42, "Hello, world!"],
options: %{ 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 assert foo == 42
@ -223,11 +228,11 @@ defmodule EXW3Test do
end end
test "starts a Contract GenServer for AddressTester contract", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
AddressTester, :AddressTester,
bin: ExW3.load_bin("test/examples/build/AddressTester.bin"), bin: ExW3.load_bin("test/examples/build/AddressTester.bin"),
options: %{ options: %{
from: Enum.at(context[:accounts], 0), 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 = formatted_address =
Enum.at(context[:accounts], 0) Enum.at(context[:accounts], 0)
|> ExW3.format_address |> 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) assert ExW3.to_address(same_address) == Enum.at(context[:accounts], 0)
end end
@ -283,11 +288,11 @@ defmodule EXW3Test do
test "returns proper error messages at contract deployment", context 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} == assert {:error, :missing_gas} ==
ExW3.Contract.deploy( ExW3.Contract.deploy(
SimpleStorage, :SimpleStorage,
bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"),
args: [], args: [],
options: %{ options: %{
@ -297,7 +302,7 @@ defmodule EXW3Test do
assert {:error, :missing_sender} == assert {:error, :missing_sender} ==
ExW3.Contract.deploy( ExW3.Contract.deploy(
SimpleStorage, :SimpleStorage,
bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"),
args: [], args: [],
options: %{ options: %{
@ -307,7 +312,7 @@ defmodule EXW3Test do
assert {:error, :missing_binary} == assert {:error, :missing_binary} ==
ExW3.Contract.deploy( ExW3.Contract.deploy(
SimpleStorage, :SimpleStorage,
args: [], args: [],
options: %{ options: %{
gas: 300_000, gas: 300_000,
@ -318,11 +323,11 @@ defmodule EXW3Test do
end end
test "return proper error messages at send and call", context do 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, _} = {:ok, address, _} =
ExW3.Contract.deploy( ExW3.Contract.deploy(
SimpleStorage, :SimpleStorage,
bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"), bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"),
args: [], args: [],
options: %{ options: %{
@ -331,13 +336,13 @@ defmodule EXW3Test do
} }
) )
assert {:error, :missing_address} == ExW3.Contract.call(SimpleStorage, :get) 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.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_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_gas} == ExW3.Contract.send(:SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0)})
end end

Loading…
Cancel
Save