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
@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 =

@ -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

Loading…
Cancel
Save