defmodule ExW3 do Module.register_attribute(__MODULE__, :unit_map, persist: true, accumulate: false) @unit_map %{ :noether => 0, :wei => 1, :kwei => 1_000, :Kwei => 1_000, :babbage => 1_000, :femtoether => 1_000, :mwei => 1_000_000, :Mwei => 1_000_000, :lovelace => 1_000_000, :picoether => 1_000_000, :gwei => 1_000_000_000, :Gwei => 1_000_000_000, :shannon => 1_000_000_000, :nanoether => 1_000_000_000, :nano => 1_000_000_000, :szabo => 1_000_000_000_000, :microether => 1_000_000_000_000, :micro => 1_000_000_000_000, :finney => 1_000_000_000_000_000, :milliether => 1_000_000_000_000_000, :milli => 1_000_000_000_000_000, :ether => 1_000_000_000_000_000_000, :kether => 1_000_000_000_000_000_000_000, :grand => 1_000_000_000_000_000_000_000, :mether => 1_000_000_000_000_000_000_000_000, :gether => 1_000_000_000_000_000_000_000_000_000, :tether => 1_000_000_000_000_000_000_000_000_000_000 } @spec get_unit_map() :: %{} @doc "Returns the map used for ether unit conversion" def get_unit_map do @unit_map end def to_wei(num, key) do if @unit_map[key] do num * @unit_map[key] else throw("#{key} not valid unit") end end def from_wei(num, key) do if @unit_map[key] do num / @unit_map[key] else throw("#{key} not valid unit") end end @spec keccak256(binary()) :: binary() @doc "Returns a 0x prepended 32 byte hash of the input string" def keccak256(string) do Enum.join(["0x", ExthCrypto.Hash.Keccak.kec(string) |> Base.encode16(case: :lower)], "") end @spec bytes_to_string(binary()) :: binary() @doc "converts Ethereum style bytes to string" def bytes_to_string(bytes) do bytes |> Base.encode16(case: :lower) |> String.replace_trailing("0", "") |> Base.decode16!(case: :lower) end @spec format_address(binary()) :: integer() @doc "Converts an Ethereum address into a form that can be used by the ABI encoder" def format_address(address) do address |> String.slice(2..-1) |> Base.decode16!(case: :lower) |> :binary.decode_unsigned() end @spec to_address(binary()) :: binary() @doc "Converts bytes to Ethereum address" def to_address(bytes) do Enum.join(["0x", bytes |> Base.encode16(case: :lower)], "") end @spec to_checksum_address(binary()) :: binary() @doc "returns a checksummed address" def to_checksum_address(address) do address = String.replace(address, ~r/^0x/, "") hash = ExthCrypto.Hash.Keccak.kec(String.downcase(address)) |> Base.encode16(case: :lower) |> String.replace(~r/^0x/, "") keccak_hash_list = hash |> String.split("", trim: true) |> Enum.map(fn x -> elem(Integer.parse(x, 16), 0) end) list_arr = for n <- 0..(String.length(address) - 1) do number = Enum.at(keccak_hash_list, n) cond do number >= 8 -> String.upcase(String.at(address, n)) true -> String.downcase(String.at(address, n)) end end "0x" <> List.to_string(list_arr) end @doc "checks if the address is a valid checksummed address" @spec is_valid_checksum_address(binary()) :: boolean() def is_valid_checksum_address(address) do to_checksum_address(address) == address end @spec accounts() :: list() @doc "returns all available accounts" def accounts do case Ethereumex.HttpClient.eth_accounts() do {:ok, accounts} -> accounts err -> err end end @spec to_decimal(binary()) :: number() @doc "Converts ethereum hex string to decimal number" def to_decimal(hex_string) do hex_string |> String.slice(2..-1) |> String.to_integer(16) end @spec block_number() :: integer() @doc "Returns the current block number" def block_number do case Ethereumex.HttpClient.eth_block_number() do {:ok, block_number} -> block_number |> to_decimal err -> err end end @spec balance(binary()) :: integer() @doc "Returns current balance of account" def balance(account) do case Ethereumex.HttpClient.eth_get_balance(account) do {:ok, balance} -> balance |> to_decimal err -> err end end @spec keys_to_decimal(%{}, []) :: %{} def keys_to_decimal(map, keys) do Map.new( Enum.map(keys, fn k -> {k, Map.get(map, k) |> to_decimal} end) ) end @spec tx_receipt(binary()) :: %{} @doc "Returns transaction receipt for specified transaction hash(id)" def tx_receipt(tx_hash) do case Ethereumex.HttpClient.eth_get_transaction_receipt(tx_hash) do {:ok, receipt} -> Map.merge( receipt, keys_to_decimal(receipt, ["blockNumber", "cumulativeGasUsed", "gasUsed"]) ) err -> err end end @spec block(integer()) :: any() @doc "Returns block data for specified block number" def block(block_number) do case Ethereumex.HttpClient.eth_get_block_by_number(block_number, true) do {:ok, block} -> block err -> err end end @spec new_filter(%{}) :: binary() @doc "Creates a new filter, returns filter id" def new_filter(map) do case Ethereumex.HttpClient.eth_new_filter(map) do {:ok, filter_id} -> filter_id err -> err end end def get_filter_changes(filter_id) do case Ethereumex.HttpClient.eth_get_filter_changes(filter_id) do {:ok, changes} -> changes err -> err end end @spec uninstall_filter(binary()) :: boolean() def uninstall_filter(filter_id) do case Ethereumex.HttpClient.eth_uninstall_filter(filter_id) do {:ok, result} -> result err -> err end end @spec mine(integer()) :: any() @doc "Mines number of blocks specified. Default is 1" def mine(num_blocks \\ 1) do for _ <- 0..(num_blocks - 1) do Ethereumex.HttpClient.request("evm_mine", [], []) end end @spec encode_event(binary()) :: binary() @doc "Encodes event based on signature" def encode_event(signature) do ExthCrypto.Hash.Keccak.kec(signature) |> Base.encode16(case: :lower) end @spec decode_event(binary(), binary()) :: any() @doc "Decodes event based on given data and provided signature" def decode_event(data, signature) do formatted_data = data |> String.slice(2..-1) |> Base.decode16!(case: :lower) fs = ABI.FunctionSelector.decode(signature) ABI.TypeDecoder.decode(formatted_data, fs) end @spec reformat_abi([]) :: %{} @doc "Reformats abi from list to map with event and function names as keys" def reformat_abi(abi) do abi |> Enum.map(&map_abi/1) |> Map.new() end @spec load_abi(binary()) :: [] @doc "Loads the abi at the file path and reformats it to a map" 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 @spec load_bin(binary()) :: binary() @doc "Loads the bin ar the file path" 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 @spec decode_data(binary(), binary()) :: any() @doc "Decodes data based on given type signature" def decode_data(types_signature, data) do {:ok, trim_data} = String.slice(data, 2..String.length(data)) |> Base.decode16(case: :lower) ABI.decode(types_signature, trim_data) |> List.first() end @spec decode_output(%{}, binary(), binary()) :: [] @doc "Decodes output based on specified functions return signature" 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 @spec types_signature(%{}, binary()) :: binary() @doc "Returns the type signature of a given function" 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 @spec method_signature(%{}, binary()) :: binary() @doc "Returns the 4 character method id based on the hash of the method signature" 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 <> = input_signature init else raise "#{name} method not found in the given abi" end end @spec encode_data(binary(), []) :: binary() @doc "Encodes data into Ethereum hex string based on types signature" def encode_data(types_signature, data) do ABI.TypeEncoder.encode_raw( [List.to_tuple(data)], ABI.FunctionSelector.decode_raw(types_signature) ) end @spec encode_option(integer()) :: binary() @doc "Encodes options into Ethereum JSON RPC hex string" def encode_option(0), do: "0x0" def encode_option(value) do "0x" <> (value |> :binary.encode_unsigned() |> Base.encode16(case: :lower) |> String.trim_leading("0")) end @spec encode_method_call(%{}, binary(), []) :: binary() @doc "Encodes data and appends it to the encoded method id" 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 @spec encode_input(%{}, binary(), []) :: binary() @doc "Encodes input from a method call based on function signature" 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 <> = 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 # ABI mapper defp map_abi(x) do case {x["name"], x["type"]} do {nil, "constructor"} -> {:constructor, x} {nil, "fallback"} -> {:fallback, x} {name, _} -> {name, x} end end defmodule Contract do use GenServer # Client @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__, %{filters: %{}}, name: ContractManager) end @spec deploy(keyword(), []) :: {:ok, binary(), []} @doc "Deploys contracts with given arguments" def deploy(name, args) do GenServer.call(ContractManager, {:deploy, {name, args}}) end @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 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(name) do GenServer.call(ContractManager, {:address, name}) end @spec call(keyword(), keyword(), []) :: {:ok, any()} @doc "Use a Contract's method with an eth_call" def call(contract_name, method_name, args \\ []) do GenServer.call(ContractManager, {:call, {contract_name, method_name, args}}) end @spec send(keyword(), keyword(), [], %{}) :: {:ok, binary()} @doc "Use a Contract's method with an eth_sendTransaction" def send(contract_name, method_name, args, options) do GenServer.call(ContractManager, {:send, {contract_name, method_name, args, options}}) end @spec call_async(keyword(), keyword(), []) :: {:ok, any()} @doc "Use a Contract's method with an eth_call. Returns a Task to be awaited." def call_async(contract_name, method_name, args \\ []) do Task.async(fn -> GenServer.call(ContractManager, {:call, {contract_name, method_name, args}}) end) end @spec send_async(keyword(), keyword(), [], %{}) :: {:ok, binary()} @doc "Use a Contract's method with an eth_sendTransaction. Returns a Task to be awaited." def send_async(contract_name, method_name, args, options) do Task.async(fn -> GenServer.call(ContractManager, {:send, {contract_name, method_name, args, options}}) end) end @spec tx_receipt(keyword(), binary()) :: %{} @doc "Returns a formatted transaction receipt for the given transaction hash(id)" def tx_receipt(contract_name, tx_hash) do GenServer.call(ContractManager, {:tx_receipt, {contract_name, tx_hash}}) end @spec filter(keyword(), binary(), %{}) :: {:ok, binary()} def filter(contract_name, event_name, event_data \\ %{}) do GenServer.call( ContractManager, {:filter, {contract_name, event_name, event_data}} ) end @spec get_filter_changes(binary(), integer()) :: {:ok, []} def get_filter_changes(filter_id, seconds \\ 0) do GenServer.call( ContractManager, {:get_filter_changes, {filter_id, seconds}} ) end # Server 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 = "0x#{ExW3.encode_event(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 # Helpers 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.encode_data(types_signature, arguments) |> Base.encode16(case: :lower)) else # IO.warn("Could not find a constructor") bin end else bin end gas = ExW3.encode_option(args[:options][:gas]) tx = %{ from: args[:options][:from], data: "0x#{constructor_arg_data}", gas: gas } {:ok, tx_hash} = Ethereumex.HttpClient.eth_send_transaction(tx) {:ok, tx_receipt} = Ethereumex.HttpClient.eth_get_transaction_receipt(tx_hash) {tx_receipt["contractAddress"], tx_hash} end def eth_call_helper(address, abi, method_name, args) do result = Ethereumex.HttpClient.eth_call(%{ to: address, data: "0x#{ExW3.encode_method_call(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 gas = ExW3.encode_option(options[:gas]) Ethereumex.HttpClient.eth_send_transaction( Map.merge( %{ to: address, data: "0x#{ExW3.encode_method_call(abi, method_name, args)}" }, Map.put(options, :gas, gas) ) ) 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_info = state[name] {:noreply, Map.put(state, name, contract_info ++ [address: address])} end def handle_cast({:register, {name, contract_info}}, state) do {:noreply, Map.put(state, name, register_helper(contract_info))} 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.encode_data(topic_type, [t]) |> Base.encode16(case: :lower)) end) else topic_type = Enum.at(topic_types, i) "0x" <> (ExW3.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.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.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.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.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.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, seconds}}, _from, state) do :timer.sleep(1000 * seconds) filter_info = Map.get(state[:filters], filter_id) event_attributes = get_event_attributes(state, filter_info[:contract_name], filter_info[:event_name]) logs = ExW3.get_filter_changes(filter_id) formatted_logs = if logs != [] do Enum.map(logs, fn log -> formatted_log = Enum.reduce( [ ExW3.keys_to_decimal(log, [ "blockNumber", "logIndex", "transactionIndex", "transactionLogIndex" ]), 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] 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.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.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 end