Packages
signet
1.3.7
1.6.1
1.6.0
1.5.0
1.4.2
1.4.1
1.4.0
1.3.12
1.3.11
1.3.10
1.3.9
1.3.8
1.3.7
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0-echo8
1.0.0-echo7
1.0.0-echo6
1.0.0-echo5
1.0.0-echo4
1.0.0-echo3
1.0.0-echo2
1.0.0-echo1
1.0.0-delta8
1.0.0-delta7
1.0.0-delta6
1.0.0-delta5
1.0.0-delta4
1.0.0-delta3
1.0.0-delta2
1.0.0-delta1
1.0.0-charlie9
1.0.0-charlie8
1.0.0-charlie7
1.0.0-charlie6
1.0.0-charlie5
1.0.0-charlie4
1.0.0-charlie3
1.0.0-charlie2
1.0.0-charlie1
1.0.0-beta9
1.0.0-beta8
1.0.0-beta7
1.0.0-beta6
1.0.0-beta5
1.0.0-beta4
1.0.0-beta3
1.0.0-beta2
1.0.0-beta1
1.0.0-alpha9
1.0.0-alpha8
1.0.0-alpha7
1.0.0-alpha6
1.0.0-alpha5
1.0.0-alpha4
1.0.0-alpha3
1.0.0-alpha2
1.0.0-alpha10
1.0.0-alpha1
0.2.0-alpha6
0.2.0-alpha5
0.2.0-alpha4
0.2.0-alpha1
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.1.0-rc7
0.1.0-rc6
0.1.0-rc5
0.1.0-rc4
0.1.0-rc3
0.1.0-rc2
0.1.0-rc1
0.1.0-rc0
Lightweight Ethereum and Solana RPC client for Elixir
Current section
Files
Jump to
Current section
Files
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