4
0
Fork 0

Merge pull request #2 from hswick/contracts

Contracts
pull/5/head v0.1
Harley Swick 7 years ago committed by GitHub
commit 975c0f3279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 300
      lib/exw3.ex
  2. 8
      mix.exs
  3. 114
      test/exw3_test.exs

@ -1,55 +1,10 @@
defmodule ExW3 do
def reformat_abi(abi) do
Map.new(Enum.map(abi, fn x -> {x["name"], x} end))
end
def load_abi(file_path) do
file = File.read(Path.join(System.cwd(), file_path))
case file do
{:ok, abi} -> reformat_abi(Poison.Parser.parse!(abi))
err -> err
end
end
def decode_output(abi, name, output) do
{:ok, trim_output} =
String.slice(output, 2..String.length(output)) |> Base.decode16(case: :lower)
output_types = Enum.map(abi[name]["outputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(output_types, ","), ")"])
output_signature = "#{name}(#{types_signature})"
outputs = ABI.decode(output_signature, trim_output) |> Enum.at(0) |> Tuple.to_list
outputs
end
def encode_input(abi, name, input) do
if abi[name]["inputs"] do
input_types = Enum.map(abi[name]["inputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"])
input_signature = "#{name}#{types_signature}" |> ExthCrypto.Hash.Keccak.kec()
# Take first four bytes
<<init::binary-size(4), _rest::binary>> = input_signature
encoded_input =
init <>
ABI.TypeEncoder.encode_raw(
[List.to_tuple(input)],
ABI.FunctionSelector.decode_raw(types_signature)
)
encoded_input |> Base.encode16(case: :lower)
else
raise "#{name} method not found with the given abi"
end
end
def bytes_to_string bytes do
bytes
|> Base.encode16(case: :lower)
|> String.replace_trailing("0", "")
|> Hexate.decode
|> Base.decode16!(case: :lower)
end
def accounts do
@ -132,15 +87,139 @@ defmodule ExW3 do
ABI.TypeDecoder.decode(formatted_data, fs)
end
def reformat_abi(abi) do
Map.new(Enum.map(abi, fn x -> {x["name"], x} end))
end
def load_abi(file_path) do
file = File.read(Path.join(System.cwd(), file_path))
case file do
{:ok, abi} -> reformat_abi(Poison.Parser.parse!(abi))
err -> err
end
end
def load_bin(file_path) do
file = File.read(Path.join(System.cwd(), file_path))
case file do
{:ok, bin} -> bin
err -> err
end
end
def decode_output(abi, name, output) do
{:ok, trim_output} =
String.slice(output, 2..String.length(output)) |> Base.decode16(case: :lower)
output_types = Enum.map(abi[name]["outputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(output_types, ","), ")"])
output_signature = "#{name}(#{types_signature})"
outputs =
ABI.decode(output_signature, trim_output)
|> List.first
|> Tuple.to_list
outputs
end
def types_signature(abi, name) do
input_types = Enum.map(abi[name]["inputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"])
types_signature
end
def method_signature(abi, name) do
if abi[name] do
input_signature = "#{name}#{types_signature(abi, name)}" |> ExthCrypto.Hash.Keccak.kec()
# Take first four bytes
<<init::binary-size(4), _rest::binary>> = input_signature
init
else
raise "#{name} method not found in the given abi"
end
end
def encode_data(types_signature, data) do
ABI.TypeEncoder.encode_raw(
[List.to_tuple(data)],
ABI.FunctionSelector.decode_raw(types_signature)
)
end
def encode_method_call(abi, name, input) do
encoded_method_call = method_signature(abi, name) <> encode_data(types_signature(abi, name), input)
encoded_method_call |> Base.encode16(case: :lower)
end
def encode_input(abi, name, input) do
if abi[name]["inputs"] do
input_types = Enum.map(abi[name]["inputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"])
input_signature = "#{name}#{types_signature}" |> ExthCrypto.Hash.Keccak.kec()
# Take first four bytes
<<init::binary-size(4), _rest::binary>> = input_signature
encoded_input =
init <>
ABI.TypeEncoder.encode_raw(
[List.to_tuple(input)],
ABI.FunctionSelector.decode_raw(types_signature)
)
encoded_input |> Base.encode16(case: :lower)
else
raise "#{name} method not found with the given abi"
end
end
defmodule Contract do
use Agent
use GenServer
# Client
def start_link(name, state) do
GenServer.start_link(__MODULE__, state, name: name)
end
def deploy(pid, args) do
GenServer.call(pid, {:deploy, args})
end
def at(pid, address) do
GenServer.cast(pid, {:at, address})
end
def address(pid) do
GenServer.call(pid, :address)
end
def at(abi, address) do
{:ok, pid} = Agent.start_link(fn -> %{abi: abi, address: address, events: setup_events(abi)} end)
pid
def call(pid, method_name, args \\ []) do
GenServer.call(pid, {:call, {method_name, args}})
end
defp setup_events(abi) do
def send(pid, method_name, args, options) do
GenServer.call(pid, {:send, {method_name, args, options}})
end
def tx_receipt(pid, tx_hash) do
GenServer.call(pid, {:tx_receipt, tx_hash})
end
# Server
def init(state) do
if state[:abi] do
{:ok, [{:events, init_events(state[:abi])} | state]}
else
raise "ABI not provided upon initialization"
end
end
defp init_events(abi) do
events = Enum.filter abi, fn {_, v} ->
v["type"] == "event"
end
@ -153,27 +232,29 @@ defmodule ExW3 do
{"0x#{ExW3.encode_event(signature)}", %{signature: signature, names: names}}
end
Enum.into signature_types_map, %{}
Enum.into(signature_types_map, %{})
end
def get(contract, key) do
Agent.get(contract, &Map.get(&1, key))
end
# Helpers
@doc """
Puts the `value` for the given `key` in the `contract`.
"""
def put(contract, key, value) do
Agent.update(contract, &Map.put(&1, key, value))
end
def deploy_helper(bin, abi, args) do
def deploy(bin_filename, options) do
{:ok, bin} = File.read(Path.join(System.cwd(), bin_filename))
constructor_arg_data =
if args[:args] do
{_, constructor} = Enum.find abi, fn {_, v} ->
v["type"] == "constructor"
end
input_types = Enum.map(constructor["inputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"])
bin <> (ExW3.encode_data(types_signature, args[:args]) |> Base.encode16(case: :lower))
else
bin
end
tx = %{
from: options[:from],
data: bin,
gas: options[:gas]
from: args[:options][:from],
data: constructor_arg_data,
gas: args[:options][:gas]
}
{:ok, tx_receipt_id} = Ethereumex.HttpClient.eth_send_transaction(tx)
@ -183,39 +264,73 @@ defmodule ExW3 do
tx_receipt["contractAddress"]
end
def method(contract_agent, method_name, args \\ [], options \\ %{}) do
method_name =
case method_name |> is_atom do
true -> Inflex.camelize(method_name, :lower)
false -> method_name
end
def eth_call_helper(address, abi, method_name, args) do
result = Ethereumex.HttpClient.eth_call(%{
to: address,
data: ExW3.encode_method_call(abi, method_name, args)
})
input = ExW3.encode_input(get(contract_agent, :abi), method_name, args)
case result do
{:ok, data} -> ([:ok] ++ ExW3.decode_output(abi, method_name, data)) |> List.to_tuple()
{:error, err} -> {:error, err}
end
end
def eth_send_helper(address, abi, method_name, args, options) do
Ethereumex.HttpClient.eth_send_transaction(
Map.merge(
%{
to: address,
data: ExW3.encode_method_call(abi, method_name, args)
},
options
)
)
end
# Casts
def handle_cast({:at, address}, state) do
{:noreply, [{:address, address} | state]}
end
# Calls
def handle_call({:deploy, args}, _from, state) do
case {args[:bin], state[:bin]} do
{nil, nil} -> {:reply, {:error, "contract binary was never provided"}, state}
{bin, nil} -> {:reply, {:ok, deploy_helper(bin, state[:abi], args)}, state}
{nil, bin} -> {:reply, {:ok, deploy_helper(bin, state[:abi], args)}, state}
end
end
if get(contract_agent, :abi)[method_name]["constant"] do
{:ok, output} =
Ethereumex.HttpClient.eth_call(%{
to: get(contract_agent, :address),
data: input
})
def handle_call(:address, _from, state) do
{:reply, state[:address], state}
end
([:ok] ++ ExW3.decode_output(get(contract_agent, :abi), method_name, output)) |> List.to_tuple()
def handle_call({:call, {method_name, args}}, _from, state) do
address = state[:address]
if address do
result = eth_call_helper(address, state[:abi], Atom.to_string(method_name), args)
{:reply, result, state}
else
Ethereumex.HttpClient.eth_send_transaction(
Map.merge(
%{
to: get(contract_agent, :address),
data: input
},
options
)
)
{:reply, {:error, "contract address not available"}, state}
end
end
def tx_receipt(contract_agent, tx_hash) do
def handle_call({:send, {method_name, args, options}}, _from, state) do
address = state[:address]
if address do
result = eth_send_helper(address, state[:abi], Atom.to_string(method_name), args, options)
{:reply, result, state}
else
{:reply, {:error, "contract address not available"}, state}
end
end
def handle_call({:tx_receipt, tx_hash}, _from, state) do
receipt = ExW3.tx_receipt tx_hash
events = get(contract_agent, :events)
events = state[:events]
logs = receipt["logs"]
formatted_logs = Enum.map logs, fn log ->
topic = Enum.at log["topics"], 0
@ -226,8 +341,9 @@ defmodule ExW3 do
nil
end
end
{:ok, {receipt, formatted_logs}}
{:reply, {:ok, {receipt, formatted_logs}}, state}
end
end
end

@ -3,8 +3,8 @@ defmodule ExW3.MixProject do
def project do
[app: :exw3,
version: "0.0.2",
elixir: "~> 1.1-dev",
version: "0.1.0",
elixir: "~> 1.6.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps(),
@ -36,9 +36,7 @@ defmodule ExW3.MixProject do
{:ex_doc, ">= 0.0.0", only: :dev},
{:ethereumex, "~> 0.3.2"},
{:abi, "~> 0.1.8"},
{:poison, "~> 3.1"},
{:inflex, "~> 1.10.0" },
{:hexate, ">= 0.6.0"}
{:poison, "~> 3.1"}
]
end

@ -7,6 +7,7 @@ defmodule EXW3Test do
simple_storage_abi: ExW3.load_abi("test/examples/build/SimpleStorage.abi"),
array_tester_abi: ExW3.load_abi("test/examples/build/ArrayTester.abi"),
event_tester_abi: ExW3.load_abi("test/examples/build/EventTester.abi"),
complex_abi: ExW3.load_abi("test/examples/build/Complex.abi"),
accounts: ExW3.accounts
}
end
@ -39,48 +40,83 @@ defmodule EXW3Test do
assert ExW3.block_number == block_number + 5
end
test "deploys simple storage and uses it", context do
contract_address = ExW3.Contract.deploy(
"test/examples/build/SimpleStorage.bin",
%{
from: Enum.at(context[:accounts], 0),
gas: 150000
test "starts a Contract GenServer for simple storage contract", context do
ExW3.Contract.start_link(SimpleStorage, abi: context[:simple_storage_abi])
{:ok, address} = ExW3.Contract.deploy(
SimpleStorage,
bin: ExW3.load_bin("test/examples/build/SimpleStorage.bin"),
options: %{
gas: 300000,
from: Enum.at(context[:accounts], 0)
}
)
storage = ExW3.Contract.at context[:simple_storage_abi], contract_address
ExW3.Contract.at(SimpleStorage, address)
assert address == ExW3.Contract.address(SimpleStorage)
{:ok, result} = ExW3.Contract.method storage, :get
{:ok, data} = ExW3.Contract.call(SimpleStorage, :get)
assert result == 0
assert data == 0
{:ok, tx_hash} = ExW3.Contract.method(storage, :set, [1], %{from: Enum.at(context[:accounts], 0)})
ExW3.Contract.send(SimpleStorage, :set, [1], %{from: Enum.at(context[:accounts], 0)})
{:ok, result} = ExW3.Contract.method storage, :get
{:ok, data} = ExW3.Contract.call(SimpleStorage, :get)
assert result == 1
assert data == 1
end
test "deploys event tester and uses it", context do
contract_address = ExW3.Contract.deploy(
"test/examples/build/EventTester.bin",
%{
from: Enum.at(context[:accounts], 0),
gas: 300000
test "starts a Contract GenServer for array tester contract", context do
ExW3.Contract.start_link(ArrayTester, abi: context[:array_tester_abi])
{:ok, address} = ExW3.Contract.deploy(
ArrayTester,
bin: ExW3.load_bin("test/examples/build/ArrayTester.bin"),
options: %{
gas: 300000,
from: Enum.at(context[:accounts], 0)
}
)
event_tester = ExW3.Contract.at context[:event_tester_abi], contract_address
ExW3.Contract.at(ArrayTester, address)
assert address == ExW3.Contract.address(ArrayTester)
{:ok, tx_hash} = ExW3.Contract.method(
event_tester,
:simple,
["Hello, World!"],
%{from: Enum.at(context[:accounts], 0)}
arr = [1, 2, 3, 4, 5]
{:ok, result} = ExW3.Contract.call(ArrayTester, :staticUint, [arr])
assert result == 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])
{:ok, address} = ExW3.Contract.deploy(
EventTester,
bin: ExW3.load_bin("test/examples/build/EventTester.bin"),
options: %{
gas: 300000,
from: Enum.at(context[:accounts], 0)
}
)
{:ok, {receipt, logs}} = ExW3.Contract.tx_receipt(event_tester, tx_hash)
ExW3.Contract.at(EventTester, address)
assert address == ExW3.Contract.address(EventTester)
{:ok, tx_hash} = ExW3.Contract.send(EventTester, :simple, ["Hello, World!"], %{from: Enum.at(context[:accounts], 0)})
{:ok, {receipt, logs}} = ExW3.Contract.tx_receipt(EventTester, tx_hash)
assert receipt |> is_map
data =
logs
@ -92,29 +128,29 @@ defmodule EXW3Test do
end
test "deploys array tester and uses it", context do
contract_address = ExW3.Contract.deploy(
"test/examples/build/ArrayTester.bin",
%{
test "starts a Contract GenServer for Complex contract", context do
ExW3.Contract.start_link(Complex, abi: context[:complex_abi])
{:ok, address} = ExW3.Contract.deploy(
Complex,
bin: ExW3.load_bin("test/examples/build/Complex.bin"),
args: [42, "Hello, world!"],
options: %{
from: Enum.at(context[:accounts], 0),
gas: 300000
}
)
array_tester = ExW3.Contract.at context[:array_tester_abi], contract_address
ExW3.Contract.at(Complex, address)
arr = [1, 2, 3, 4, 5]
# This is supposed to fail
# {:ok, result} = ExW3.Contract.method array_tester, :static_int, [arr]
assert address == ExW3.Contract.address(Complex)
# assert result == arr
{:ok, foo, foobar} = ExW3.Contract.call(Complex, :getBoth)
#0x5d4e0342
# This is currently failing
{:ok, result} = ExW3.Contract.method array_tester, :dynamic_uint, [arr]
assert foo == 42
assert result == arr
assert ExW3.bytes_to_string(foobar) == "Hello, world!"
end

Loading…
Cancel
Save