Current section
Files
Jump to
Current section
Files
lib/eth_contract.ex
defmodule EthContract do
@moduledoc """
Provides a few convenience methods for getting information about tokens from ETH smart contracts. Uses JSON-RPC for requests.
"""
import EthContract.Util
alias Ethereumex.HttpClient
alias ABI.TypeDecoder
@json_rpc_client Application.get_env(:eth_contract, :json_rpc_client) || HttpClient
@callback balance_of(map()) :: map()
@callback owner_of(map()) :: map()
@callback meta(map()) :: map()
@callback parse_abi(file_path :: String.t()) :: map()
@callback decode_data(
data :: String.t(),
abi :: String.t(),
method :: String.t(),
signatures_key :: String.t()
) :: map()
@callback decode_log(
data :: String.t(),
topics :: String.t(),
abi :: String.t(),
method :: String.t()
) :: map()
@callback total_supply(map()) :: map()
@doc """
Get the wallet address given a contract and a token id
## Examples
iex> EthContract.owner_of(%{token_id: 1, contract: "0x06012c8cf97bead5deae237070f9587f8e7a266d"})
0x79bd592415ff6c91cfe69a7f9cd091354fc65a18
"""
def owner_of(%{token_id: token_id, contract: contract}) do
with {:ok, address } <- @json_rpc_client.eth_call(%{ data: "0x" <> owner_of_hex(token_id), to: contract }) do
with {:ok, address } <- address |> String.slice(2..-1) |> decode_address do
{:ok, address}
else
err -> { :error, err }
end
else
err -> { :error, err }
end
end
@doc """
Get the balance given a wallet address. This was tested against ERC20 and ERC721 standard contracts.
## Examples
iex> EthContract.balance_of(%{address: '0x123', contract: '0x234'})
2
"""
def balance_of(%{address: address, contract: contract}) do
with {:ok, signature } <- balance_of_hex(address) do
with {:ok, balance } <- @json_rpc_client.eth_call(%{ data: "0x" <> signature, to: contract }) do
{:ok, balance |> bytes_to_int }
else
err -> {:error, err }
end
else
err -> {:error, err }
end
end
@doc """
Get the account balance given a wallet address.
"""
def account_balance(%{ address: address }) do
with {:ok, balance } <- @json_rpc_client.eth_get_balance(address) do
with {balance, _ } <- balance |> String.slice(2..-1) |> Integer.parse(16) do
{:ok, balance }
else
err -> {:error, err }
end
else
err ->
{:error, err}
end
end
@doc """
Get the total supply given a contract address. This was tested against ERC20 and ERC721 standard contracts.
## Examples
iex> EthContract.total_supply(%{contract: '0x234'})
:ok
"""
def total_supply(%{contract: contract}) do
with {:ok, total_supply } <- @json_rpc_client.eth_call(%{ data: "0x" <> total_supply_hex(), to: contract}) do
{:ok, total_supply |> bytes_to_int }
else
err -> {:error, err }
end
end
@doc """
ERC721 Meta. This will return a Map with the meta information associated with a token. You have to provide the name of the method to use (ie. CryptoKitties uses getKitty)
## Examples
iex> abi = EthContract.parse_abi("test/support/crypto_kitties.json")
iex> EthContract.meta(%{token_id: 45, method: "getKitty", contract: "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", abi: abi})
%{
"birthTime" => 1511417999,
"cooldownIndex" => 0,
"generation" => 0,
"genes" => 626837621154801616088980922659877168609154386318304496692374110716999053,
"isGestating" => false,
"isReady" => true,
"matronId" => 0,
"nextActionAt" => 0,
"sireId" => 0,
"siringWithId" => 0
}
"""
def meta(%{token_id: token_id, method: method, contract: contract, abi: abi}) do
with {:ok, meta } <- @json_rpc_client.eth_call(%{ data: "0x" <> meta_for_hex(token_id, method), to: contract }) do
case meta do
"0x" ->
{:error, "Meta is 0x" }
meta ->
with {:ok, output } <- decode_data(meta, abi, method, "outputs") do
{:ok, output }
else
_err ->
{:error, "Error in decode_data"}
end
end
else
_err ->
{:error, "Error in eth_call" }
end
end
# Taken from https://github.com/hswick/exw3/blob/master/lib/exw3.ex#L159
@doc """
Parses abi into a map with the name of the method as key.
## Examples
iex> abi = EthContract.parse_abi("test/support/crypto_kitties.json")
%{}
"""
def parse_abi(file_path) do
case File.read(file_path) do
{:ok, abi } -> Poison.Parser.parse!(abi)
|> Enum.map(fn x -> {x["name"], x} end)
|> Enum.into(%{})
err -> err
end
end
defp trim_data(data) do
{:ok, trimmed_output } = data
|> String.slice(2..-1)
|> Base.decode16(case: :lower)
trimmed_output
end
defp output_signature(abi, method, signatures_key) do
output_types = Enum.map(abi[method][signatures_key], fn x -> x["type"] end)
output_names = Enum.map(abi[method][signatures_key], fn x -> x["name"] end)
types_signature = Enum.join(["(", Enum.join(output_types, ","), ")"])
output_signature = "#{method}(#{types_signature})"
%{ output_signature: output_signature, output_names: output_names }
end
@doc """
Decodes general smart contract calls
"""
def decode_data(data, abi, method, signatures_key \\ "inputs")
def decode_data("0x", _abi, _method, _signatures_key) do
{:error, %{}}
end
def decode_data(data, abi, method, signatures_key) do
trimmed_output = trim_data(data)
%{ output_signature: output_signature, output_names: output_names } = output_signature(abi, method, signatures_key)
outputs =
ABI.decode(output_signature, trimmed_output)
|> List.first()
|> Tuple.to_list()
|> Enum.map(&maybe_encode_binary/1)
{:ok, combine_data(output_names, outputs)}
end
defp maybe_encode_binary( << data :: binary>>) do
enc = Base.encode16(data, case: :lower)
"0x" <> enc
end
defp maybe_encode_binary(data), do: data
defp combine_data(names, items) do
Enum.zip(names, items)
|> Enum.into(%{})
end
@doc """
Decodes events that have indexed elements.
"""
def decode_log(data, topics, abi, method) when Kernel.length(topics) > 1 do
trimmed_data = trim_data(data)
not_indexed = Enum.reject(abi[method]["inputs"], fn x -> x["indexed"] == true end)
indexed = Enum.reject(abi[method]["inputs"], fn x -> x["indexed"] == false end)
output_types_no_index = Enum.map(not_indexed, fn x -> x["type"] end) # ["uint256"]
types_signature = Enum.join(["(", Enum.join(output_types_no_index, ","), ")"])
output_signature = "#{method}(#{types_signature})"
[ _ | topics ] = topics
decoded_data = trimmed_data
|> ABI.TypeDecoder.decode(ABI.Parser.parse!(output_signature))
|> List.first
|> Tuple.to_list
decoded_names = not_indexed
|> Enum.map( fn x -> x["name"] end)
non_indexed_result = combine_data(decoded_names, decoded_data)
res =
Enum.zip(indexed, topics)
|> Enum.map(fn x ->
{ map, data } = x
type = String.to_atom(map["type"])
decoded = case type do
:address ->
result = data
|> trim_data
|> ABI.TypeDecoder.decode_raw([type])
|> List.first
|> Base.encode16(case: :lower)
"0x" <> result
:uint256 ->
data
|> trim_data
|> :binary.decode_unsigned
end
{map["name"], decoded}
end)
indexed_result =
res
|> Enum.into(%{})
{:ok, Map.merge(indexed_result, non_indexed_result) }
end
@doc """
Decodes events with no indexes.
"""
def decode_log(data, _topics, abi, method) do
decode_data(data, abi, method)
end
defp decode_address("") do
{:ok, "0x" }
end
defp decode_address(bytes) do
{address, _} = TypeDecoder.decode_bytes(bytes, 40, :left)
{:ok, "0x" <> address }
end
defp bytes_to_int("0x") do
0
end
defp bytes_to_int(bytes) do
bytes
|> String.slice(2..-1)
|> Base.decode16!(case: :lower)
|> TypeDecoder.decode_raw([{:uint, 256}])
|> List.first
end
end