Packages

Ethereum Contract interaction via json-rpc for multi-chain based on ex_abi

Retired package: Release invalid - version fault

Current section

Files

Jump to
ethex lib abi.ex
Raw

lib/abi.ex

defmodule Ethex.Abi do
@moduledoc """
Parse `xxx.abi.json`
"""
alias Ethex.Utils
alias Ethex.Web3.JsonRpc
alias Ethex.Web3.Structs.{BlockRange, Event}
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@abi_path opts[:abi_path] || raise("abi_path is required for parsing functions.")
@contract_address opts[:contract_address] || raise("contract_address is required.")
@rpc opts[:rpc] || raise("rpc is required to know which endpoint to request.")
## Handle Function
abi =
@abi_path
|> File.read!()
|> Jason.decode!()
|> ABI.parse_specification(include_events?: true)
fns =
Enum.filter(abi, fn fs ->
fs.type == :function and not String.starts_with?(fs.function, "_")
end)
for fs <- fns do
name = Macro.underscore(fs.function)
args = Macro.generate_arguments(Enum.count(fs.input_names), __MODULE__)
params =
Enum.zip(fs.input_names, fs.types)
|> Enum.map(fn {name, type} -> "#{name}(#{inspect(type)})" end)
doc = "#{name}\n\nparams: #{Enum.join(params, ", ")}"
@doc doc
def unquote(:"#{name}")(unquote_splicing(args)) do
data = encode_data(unquote(Macro.escape(fs)), unquote(args))
params = %{to: @contract_address, data: data}
case JsonRpc.eth_call(@rpc, params) do
{:ok, result} -> {:ok, decode_result(unquote(Macro.escape(fs)), result)}
error -> error
end
end
end
defp encode_data(fs, args) do
args =
for {type, arg} <- Enum.zip(fs.types, args),
into: [],
do: if(type == :address, do: decode16_address(arg), else: arg)
encoded_data = ABI.encode(fs, args, :input) |> Base.encode16(case: :lower)
"0x#{encoded_data}"
end
defp decode_result(fs, "0x" <> result) do
decoded_data = ABI.decode(fs, Base.decode16!(result, case: :lower), :output)
returns =
for {type, return} <- Enum.zip(fs.returns, decoded_data),
into: [],
do: if(type == :address, do: encode16_address_if_need(return), else: return)
if Enum.count(returns) == 1 do
List.first(returns)
else
returns
end
end
## Handle Event
@spec gen_block_range() :: {:ok, pos_integer(), BlockRange.t()} | {:error, :atom}
def gen_block_range(), do: gen_block_range("latest")
@doc """
Generate fromBlock and toBlock params according to current block num
1. when `last_block` == "latest", fetch from newest back to 20 blocks.
2. when `cur_block` - `last_block` > 800, fetch from `last_block` forwards 800 blocks.
3. else, `cur_block` - `last_block` > 800, fetch from `last_block` to cur_block.
NOTE: the max block range in Polygon is 1000, in BSC is 5000.
"""
@spec gen_block_range(pos_integer() | String.t()) ::
{:ok, pos_integer(), BlockRange.t()} | {:error, :atom}
def gen_block_range(last_block) do
case JsonRpc.eth_block_number(@rpc) do
{:ok, cur_block} ->
cond do
is_integer(last_block) and cur_block - last_block > 800 ->
{:ok, last_block + 800,
%BlockRange{from_block: last_block, to_block: last_block + 800}}
is_integer(last_block) ->
{:ok, cur_block, %BlockRange{from_block: last_block, to_block: cur_block}}
# "latest" == last_block ->
true ->
{:ok, cur_block, %BlockRange{from_block: cur_block - 20, to_block: cur_block}}
end
error ->
error
end
end
@doc """
Combine eth_getLogs with decode
NOTE: address in filter SHOULD match abi_name, or will be discard.
"""
@spec get_logs_and_decode(BlockRange.t()) :: {:error, :atom} | {:ok, [Event.t(), ...]}
def get_logs_and_decode(%BlockRange{from_block: from, to_block: to}) do
filter = %{
fromBlock: Utils.to_hex(from),
toBlock: Utils.to_hex(to),
address: [@contract_address]
}
case JsonRpc.eth_get_logs(@rpc, filter) do
{:ok, logs} -> {:ok, Enum.map(logs, &decode_log/1)}
error -> error
end
end
evts = Enum.filter(abi, fn fs -> fs.type == :event end)
def decode_log(%{topics: topics, data: data} = log) do
ts = Enum.map(topics, &hex_to_binary/1)
{fs, result} =
ABI.Event.find_and_decode(
unquote(Macro.escape(evts)),
Enum.at(ts, 0),
Enum.at(ts, 1),
Enum.at(ts, 2),
Enum.at(ts, 3),
hex_to_binary(data)
)
returns =
Enum.map(result, fn {key, type, indexed?, value} ->
val =
if type in ["address", :address] do
encode16_address_if_need(value)
else
value
end
%{key => val}
end)
%Event{
address: log.address,
block_hash: log.blockHash,
block_number: Utils.from_hex(log.blockNumber),
block_timestamp: Utils.from_hex(log.blockTimestamp),
log_index: log.logIndex,
removed: log.removed,
transaction_hash: log.transactionHash,
transaction_index: log.transactionIndex,
event_name: fs.function,
returns: returns
}
end
## utils
defp hex_to_binary("0x" <> hex_string) do
Base.decode16!(hex_string, case: :lower)
end
defp encode16_address_if_need(address) do
if is_bitstring(address) and not String.valid?(address) do
"0x" <> Base.encode16(address, case: :lower)
else
address
end
end
defp decode16_address("0x" <> address), do: decode16_address(address)
defp decode16_address(address) do
Base.decode16!(address, case: :mixed)
end
end
end
end