Packages

A high level Ethereum JSON RPC Client for Elixir

Retired package: Renamed - Package is renamed to web3x to avoid confusion with exw3

Current section

Files

Jump to
ex_w3 lib exw3 contract.ex
Raw

lib/exw3/contract.ex

defmodule ExW3.Contract do
use GenServer
@doc "Begins the Contract process to manage all interactions with smart contracts"
@spec start_link() :: {:ok, pid()}
def start_link(_ \\ :ok) do
GenServer.start_link(__MODULE__, %{filters: %{}}, name: ContractManager)
end
@doc "Deploys contracts with given arguments"
@spec deploy(atom(), list()) :: {:ok, binary(), binary()}
def deploy(name, args) do
GenServer.call(ContractManager, {:deploy, {name, args}})
end
@doc "Registers the contract with the ContractManager process. Only :abi is required field."
@spec register(atom(), list()) :: :ok
def register(name, contract_info) do
GenServer.cast(ContractManager, {:register, {name, contract_info}})
end
@doc "Uninstalls the filter, and deletes the data associated with the filter id"
@spec uninstall_filter(binary()) :: :ok
def uninstall_filter(filter_id) do
GenServer.cast(ContractManager, {:uninstall_filter, filter_id})
end
@doc "Sets the address for the contract specified by the name argument"
@spec at(atom(), binary()) :: :ok
def at(name, address) do
GenServer.cast(ContractManager, {:at, {name, address}})
end
@doc "Returns the current Contract GenServer's address"
@spec address(atom()) :: {:ok, binary()}
def address(name) do
GenServer.call(ContractManager, {:address, name})
end
@doc "Use a Contract's method with an eth_call"
@spec call(atom(), atom(), list(), any()) :: {:ok, any()}
def call(contract_name, method_name, args \\ [], timeout \\ :infinity) do
GenServer.call(ContractManager, {:call, {contract_name, method_name, args}}, timeout)
end
@doc "Use a Contract's method with an eth_sendTransaction"
@spec send(atom(), atom(), list(), map()) :: {:ok, binary()}
def send(contract_name, method_name, args, options) do
GenServer.call(ContractManager, {:send, {contract_name, method_name, args, options}})
end
@doc "Returns a formatted transaction receipt for the given transaction hash(id)"
@spec tx_receipt(atom(), binary()) :: map()
def tx_receipt(contract_name, tx_hash) do
GenServer.call(ContractManager, {:tx_receipt, {contract_name, tx_hash}})
end
@doc "Installs a filter on the Ethereum node. This also formats the parameters, and saves relevant information to format event logs."
@spec filter(atom(), binary(), map()) :: {:ok, binary()}
def filter(contract_name, event_name, event_data \\ %{}) do
GenServer.call(
ContractManager,
{:filter, {contract_name, event_name, event_data}}
)
end
@doc "Using saved information related to the filter id, event logs are formatted properly"
@spec get_filter_changes(binary()) :: {:ok, list()}
def get_filter_changes(filter_id) do
GenServer.call(
ContractManager,
{:get_filter_changes, filter_id}
)
end
def init(state) do
{:ok, state}
end
defp data_signature_helper(name, fields) do
non_indexed_types = Enum.map(fields, &Map.get(&1, "type"))
Enum.join([name, "(", Enum.join(non_indexed_types, ","), ")"])
end
defp topic_types_helper(fields) do
if length(fields) > 0 do
Enum.map(fields, fn field ->
"(#{field["type"]})"
end)
else
[]
end
end
defp init_events(abi) do
events =
Enum.filter(abi, fn {_, v} ->
v["type"] == "event"
end)
names_and_signature_types_map =
Enum.map(events, fn {name, v} ->
types = Enum.map(v["inputs"], &Map.get(&1, "type"))
signature = Enum.join([name, "(", Enum.join(types, ","), ")"])
encoded_event_signature = ExW3.Utils.keccak256(signature)
indexed_fields =
Enum.filter(v["inputs"], fn input ->
input["indexed"]
end)
indexed_names =
Enum.map(indexed_fields, fn field ->
field["name"]
end)
non_indexed_fields =
Enum.filter(v["inputs"], fn input ->
!input["indexed"]
end)
non_indexed_names =
Enum.map(non_indexed_fields, fn field ->
field["name"]
end)
data_signature = data_signature_helper(name, non_indexed_fields)
event_attributes = %{
signature: data_signature,
non_indexed_names: non_indexed_names,
topic_types: topic_types_helper(indexed_fields),
topic_names: indexed_names
}
{{encoded_event_signature, event_attributes}, {name, encoded_event_signature}}
end)
signature_types_map =
Enum.map(names_and_signature_types_map, fn {signature_types, _} ->
signature_types
end)
names_map =
Enum.map(names_and_signature_types_map, fn {_, names} ->
names
end)
[
events: Enum.into(signature_types_map, %{}),
event_names: Enum.into(names_map, %{})
]
end
def deploy_helper(bin, abi, args) do
constructor_arg_data =
if arguments = args[:args] do
constructor_abi =
Enum.find(abi, fn {_, v} ->
v["type"] == "constructor"
end)
if constructor_abi do
{_, constructor} = constructor_abi
input_types = Enum.map(constructor["inputs"], fn x -> x["type"] end)
types_signature = Enum.join(["(", Enum.join(input_types, ","), ")"])
arg_count = Enum.count(arguments)
input_types_count = Enum.count(input_types)
if input_types_count != arg_count do
raise "Number of provided arguments to constructor is incorrect. Was given #{
arg_count
} args, looking for #{input_types_count}."
end
bin <>
(ExW3.Abi.encode_data(types_signature, arguments) |> Base.encode16(case: :lower))
else
# IO.warn("Could not find a constructor")
bin
end
else
bin
end
gas = ExW3.Abi.encode_option(args[:options][:gas])
gasPrice = ExW3.Abi.encode_option(args[:options][:gas_price])
tx = %{
from: args[:options][:from],
data: "0x#{constructor_arg_data}",
gas: gas,
gasPrice: gasPrice
}
{:ok, tx_hash} = ExW3.Rpc.eth_send([tx])
{:ok, tx_receipt} = ExW3.Rpc.tx_receipt(tx_hash)
{tx_receipt["contractAddress"], tx_hash}
end
def eth_call_helper(address, abi, method_name, args) do
result =
ExW3.Rpc.eth_call([
%{
to: address,
data: "0x#{ExW3.Abi.encode_method_call(abi, method_name, args)}"
}
])
case result do
{:ok, data} ->
([:ok] ++ ExW3.Abi.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
encoded_options =
ExW3.Abi.encode_options(
options,
[:gas, :gasPrice, :value, :nonce]
)
gas = ExW3.Abi.encode_option(args[:options][:gas])
gasPrice = ExW3.Abi.encode_option(args[:options][:gas_price])
ExW3.Rpc.eth_send([
Map.merge(
%{
to: address,
data: "0x#{ExW3.Abi.encode_method_call(abi, method_name, args)}",
gas: gas,
gasPrice: gasPrice
},
Map.merge(options, encoded_options)
)
])
end
defp register_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
defp check_option(nil, error_atom), do: {:error, error_atom}
defp check_option([], error_atom), do: {:error, error_atom}
defp check_option([head | _tail], _atom) when head != nil, do: {:ok, head}
defp check_option([_head | tail], atom), do: check_option(tail, atom)
defp check_option(value, _atom), do: {:ok, value}
# Casts
def handle_cast({:at, {name, address}}, state) do
contract_state = state[name]
contract_state = Keyword.put(contract_state, :address, address)
state = Map.put(state, name, contract_state)
{:noreply, state}
end
def handle_cast({:register, {name, contract_info}}, state) do
{:noreply, Map.put(state, name, register_helper(contract_info))}
end
def handle_cast({:uninstall_filter, filter_id}, state) do
ExW3.uninstall_filter(filter_id)
{:noreply, Map.put(state, :filters, Map.delete(state[:filters], filter_id))}
end
# Calls
defp filter_topics_helper(event_signature, event_data, topic_types, topic_names) do
topics =
if is_map(event_data[:topics]) do
Enum.map(topic_names, fn name ->
event_data[:topics][String.to_atom(name)]
end)
else
event_data[:topics]
end
if topics do
formatted_topics =
Enum.map(0..(length(topics) - 1), fn i ->
topic = Enum.at(topics, i)
if topic do
if is_list(topic) do
topic_type = Enum.at(topic_types, i)
Enum.map(topic, fn t ->
"0x" <> (ExW3.Abi.encode_data(topic_type, [t]) |> Base.encode16(case: :lower))
end)
else
topic_type = Enum.at(topic_types, i)
"0x" <> (ExW3.Abi.encode_data(topic_type, [topic]) |> Base.encode16(case: :lower))
end
else
topic
end
end)
[event_signature] ++ formatted_topics
else
[event_signature]
end
end
def from_block_helper(event_data) do
if event_data[:fromBlock] do
new_from_block =
if Enum.member?(["latest", "earliest", "pending"], event_data[:fromBlock]) do
event_data[:fromBlock]
else
ExW3.Abi.encode_data("(uint256)", [event_data[:fromBlock]])
end
Map.put(event_data, :fromBlock, new_from_block)
else
event_data
end
end
defp param_helper(event_data, key) do
if event_data[key] do
new_param =
if Enum.member?(["latest", "earliest", "pending"], event_data[key]) do
event_data[key]
else
"0x" <>
(ExW3.Abi.encode_data("(uint256)", [event_data[key]])
|> Base.encode16(case: :lower))
end
Map.put(event_data, key, new_param)
else
event_data
end
end
defp event_data_format_helper(event_data) do
event_data
|> param_helper(:fromBlock)
|> param_helper(:toBlock)
|> Map.delete(:topics)
end
def get_event_attributes(state, contract_name, event_name) do
contract_info = state[contract_name]
contract_info[:events][contract_info[:event_names][event_name]]
end
defp extract_non_indexed_fields(data, names, signature) do
Enum.zip(names, ExW3.Abi.decode_event(data, signature)) |> Enum.into(%{})
end
defp format_log_data(log, event_attributes) do
non_indexed_fields =
extract_non_indexed_fields(
Map.get(log, "data"),
event_attributes[:non_indexed_names],
event_attributes[:signature]
)
indexed_fields =
if length(log["topics"]) > 1 do
[_head | tail] = log["topics"]
decoded_topics =
Enum.map(0..(length(tail) - 1), fn i ->
topic_type = Enum.at(event_attributes[:topic_types], i)
topic_data = Enum.at(tail, i)
{decoded} = ExW3.Abi.decode_data(topic_type, topic_data)
decoded
end)
Enum.zip(event_attributes[:topic_names], decoded_topics) |> Enum.into(%{})
else
%{}
end
new_data = Map.merge(indexed_fields, non_indexed_fields)
Map.put(log, "data", new_data)
end
def handle_call({:filter, {contract_name, event_name, event_data}}, _from, state) do
contract_info = state[contract_name]
event_signature = contract_info[:event_names][event_name]
topic_types = contract_info[:events][event_signature][:topic_types]
topic_names = contract_info[:events][event_signature][:topic_names]
topics = filter_topics_helper(event_signature, event_data, topic_types, topic_names)
payload =
Map.merge(
%{address: contract_info[:address], topics: topics},
event_data_format_helper(event_data)
)
filter_id = ExW3.Rpc.new_filter(payload)
{:reply, {:ok, filter_id},
Map.put(
state,
:filters,
Map.put(state[:filters], filter_id, %{
contract_name: contract_name,
event_name: event_name
})
)}
end
def handle_call({:get_filter_changes, filter_id}, _from, state) do
filter_info = Map.get(state[:filters], filter_id)
event_attributes =
get_event_attributes(state, filter_info[:contract_name], filter_info[:event_name])
logs = ExW3.Rpc.get_filter_changes(filter_id)
formatted_logs =
if logs != [] do
Enum.map(logs, fn log ->
formatted_log =
Enum.reduce(
[
ExW3.Normalize.transform_to_integer(log, [
"blockNumber",
"logIndex",
"transactionIndex"
]),
format_log_data(log, event_attributes)
],
&Map.merge/2
)
formatted_log
end)
else
logs
end
{:reply, {:ok, formatted_logs}, state}
end
def handle_call({:deploy, {name, 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, contract_info[:abi], args)
result = {:ok, contract_addr, tx_hash}
{:reply, result, state}
else
err -> {:reply, err, state}
end
end
def handle_call({:address, name}, _from, state) do
{:reply, state[name][:address], state}
end
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, contract_info[:abi], Atom.to_string(method_name), args)
{:reply, result, state}
else
err -> {:reply, err, state}
end
end
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,
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, {contract_name, tx_hash}}, _from, state) do
contract_info = state[contract_name]
{:ok, receipt} = ExW3.tx_receipt(tx_hash)
events = contract_info[:events]
logs = receipt["logs"]
formatted_logs =
Enum.map(logs, fn log ->
topic = Enum.at(log["topics"], 0)
event_attributes = Map.get(events, topic)
if event_attributes do
non_indexed_fields =
Enum.zip(
event_attributes[:non_indexed_names],
ExW3.Abi.decode_event(log["data"], event_attributes[:signature])
)
|> Enum.into(%{})
if length(log["topics"]) > 1 do
[_head | tail] = log["topics"]
decoded_topics =
Enum.map(0..(length(tail) - 1), fn i ->
topic_type = Enum.at(event_attributes[:topic_types], i)
topic_data = Enum.at(tail, i)
{decoded} = ExW3.Abi.decode_data(topic_type, topic_data)
decoded
end)
indexed_fields =
Enum.zip(event_attributes[:topic_names], decoded_topics) |> Enum.into(%{})
Map.merge(indexed_fields, non_indexed_fields)
else
non_indexed_fields
end
else
nil
end
end)
{:reply, {:ok, {receipt, formatted_logs}}, state}
end
end