Current section

Files

Jump to
signet lib signet sleuth.ex
Raw

lib/signet/sleuth.ex

defmodule Signet.Sleuth do
@moduledoc ~S"""
Sleuth allows you to run a contract call as a single
`eth_call` call.
Note: Signet.Contract.Sleuth generated from `mix signet.gen --prefix signet/contract ./priv/Sleuth.json`
"""
use Signet.Hex
@sleuth_address ~h[0xFd946Bf25C47A1Bff567B28bA78a961bf78FF9d2]
def query(bytecode, query, selector, opts \\ []),
do: query_internal(bytecode, query, selector, false, opts)
def query_annotated(bytecode, query, selector, opts \\ []),
do: query_internal(bytecode, query, selector, true, opts)
def query_by(mod, fun) when is_atom(mod) and is_atom(fun), do: query_by(mod, fun, [])
def query_by(mod, opts) when is_atom(mod) and is_list(opts), do: query_by(mod, :query, opts)
def query_by(mod), do: query_by(mod, :query, [])
def query_by(mod, fun, opts) when is_atom(mod) and is_atom(fun) and is_list(opts) do
bytecode = try_apply(mod, :bytecode, [])
query_val = try_apply(mod, String.to_atom("encode_" <> to_string(fun)), [])
selector = try_apply(mod, String.to_atom(to_string(fun) <> "_selector"), [])
query_internal(bytecode, query_val, selector, false, opts)
end
defp query_internal(bytecode, query, selector, annotated, opts)
when is_binary(bytecode) and is_list(opts) do
{sleuth_address, opts} = Keyword.pop(opts, :sleuth_address, @sleuth_address)
{decode_binaries, rpc_opts} = Keyword.pop(opts, :decode_binaries, true)
with {:ok, query_res_bytes} <-
Signet.Contract.Sleuth.call_query(sleuth_address, bytecode, query, rpc_opts),
{:ok, query_res} <- try_decode_bytes(query_res_bytes),
{:ok, res} <- try_decode(query_res, selector, false) do
{:ok,
postprocess(res, selector.returns,
annotated: annotated,
decode_binaries: decode_binaries,
be_obvious: false
)}
end
end
def query_v2(bytecode, query, selector, opts \\ []) do
{annotated, opts} = Keyword.pop(opts, :annotated, false)
{sleuth_address, opts} = Keyword.pop(opts, :sleuth_address, @sleuth_address)
{decode_binaries, opts} = Keyword.pop(opts, :decode_binaries, true)
{decode_structs, opts} = Keyword.pop(opts, :decode_structs, true)
{named_returns, rpc_opts} = Keyword.pop(opts, :named_returns, false)
with {:ok, query_res_bytes} <-
Signet.Contract.Sleuth.call_query(sleuth_address, bytecode, query, rpc_opts),
{:ok, query_res} <- try_decode_bytes(query_res_bytes),
{:ok, res} <- try_decode(query_res, selector, decode_structs) do
{:ok,
postprocess(res, selector.returns,
annotated: annotated,
decode_binaries: decode_binaries,
named_returns: named_returns,
be_obvious: true
)}
end
end
defp try_decode_bytes(bytes) do
try do
[decoded] = ABI.decode("(bytes)", bytes)
{:ok, decoded}
rescue
e ->
{:error, "error decoding bytes: #{inspect(e)}"}
end
end
defp try_decode(query_res, selector, decode_structs) do
try do
{:ok,
ABI.decode(
%ABI.FunctionSelector{types: selector.returns},
query_res,
decode_structs: decode_structs
)}
rescue
e ->
{:error, "error decoding: #{inspect(e)}"}
end
end
# NOTE: decode_structs weirdly also does dynamic return types with named
# returns, which interacts poorly with our named_returns opt.
#
# so we have to take the unordered map, and re-order the values by
# referencing the ordering of the named_types.
#
defp postprocess(results, named_types, opts) when is_map(results) and is_list(named_types) do
results_values =
named_types
|> Enum.map(fn %{name: name} ->
{_, v} = Enum.find(results, fn {k, _} -> to_string(k) == Macro.underscore(name) end)
v
end)
postprocess(results_values, named_types, opts)
end
defp postprocess(results, named_types, opts) when is_list(results) and is_list(named_types) do
be_obvious = Keyword.get(opts, :be_obvious, false)
named_returns = Keyword.get(opts, :named_returns, false)
results
|> Enum.zip(named_types)
|> Enum.map(fn {it, t} -> {t.name, postprocess(it, t.type, opts)} end)
|> then(fn
processed_results when not be_obvious ->
case processed_results do
[] ->
[]
[{nil, result}] ->
result
[{"", result}] ->
result
[_more | _than_one] = processed_results ->
processed_results
|> Enum.with_index()
|> Enum.map(fn {{name, it}, i} ->
name =
if is_nil(name) or name == "" do
"var#{i}"
else
name
end
{name, it}
end)
|> Enum.into(%{})
end
processed_results when be_obvious ->
if not named_returns do
Enum.map(processed_results, fn {_, v} -> v end)
else
Enum.map(processed_results, fn {name, v} ->
keyword =
if is_nil(name) or name == "" do
:__unnamed__
else
String.to_atom(Macro.underscore(name))
end
{keyword, v}
end)
end
end)
end
defp postprocess(item, {:tuple, named_types}, opts)
when is_tuple(item) and is_list(named_types) do
item
|> Tuple.to_list()
|> Enum.zip(named_types)
|> Enum.map(fn {item, %{type: type, name: name}} ->
{name, postprocess(item, type, opts)}
end)
|> Enum.into(%{})
end
defp postprocess(item, {:tuple, named_types}, opts)
when is_map(item) and is_list(named_types) do
item
|> Enum.map(fn {k, v} ->
%{type: type} =
Enum.find(named_types, fn %{name: name} -> Macro.underscore(name) == to_string(k) end)
{k, postprocess(v, type, opts)}
end)
|> Enum.into(%{})
end
defp postprocess(item, {:array, type}, opts) when is_list(item) do
Enum.map(item, &postprocess(&1, type, opts))
end
defp postprocess(item, {:array, type, _}, opts) when is_list(item),
do: postprocess(item, {:array, type}, opts)
defp postprocess(item, type, opts) do
item_encoded =
if not Keyword.get(opts, :decode_binaries, true) do
case type do
:address -> to_hex(item)
:bytes -> to_hex(item)
{:bytes, _size} -> to_hex(item)
_nonbinary_scalar -> item
end
else
item
end
if Keyword.get(opts, :annotated, false) do
{type, item_encoded}
else
item_encoded
end
end
defp try_apply(mod, fun, args) do
try do
apply(mod, fun, args)
rescue
_ ->
raise "Sleuth module #{mod} does not define required \"#{fun}/#{Enum.count(args)}\" function"
end
end
end