From b2ea6562396f9610062834fce62d22aedab1778e Mon Sep 17 00:00:00 2001 From: ElFantasma Date: Fri, 22 Sep 2023 13:21:20 -0300 Subject: [PATCH] Added support for simple arrays (#182) * Added support for simple arrays * Added support for complex types --- lib/starknet_explorer/calldata.ex | 216 +++++++++++++----- .../live/transaction_live.ex | 2 +- lib/starknet_explorer_web/live/utils.ex | 35 ++- 3 files changed, 188 insertions(+), 65 deletions(-) diff --git a/lib/starknet_explorer/calldata.ex b/lib/starknet_explorer/calldata.ex index 6afbef9f..10f29867 100644 --- a/lib/starknet_explorer/calldata.ex +++ b/lib/starknet_explorer/calldata.ex @@ -20,8 +20,10 @@ defmodule StarknetExplorer.Calldata do Enum.map( calldata, fn call -> - input = get_input_data(block_id, call.address, call.selector, network) - Map.put(call, :call, as_fn_call(input, call.calldata)) + functions_data = get_functions_data(block_id, call.address, network) + {function, structs} = get_input_data(functions_data, call.selector) + + Map.put(call, :call, as_fn_call(function, call.calldata, structs)) end ) end @@ -91,43 +93,98 @@ defmodule StarknetExplorer.Calldata do ("0x" <> Integer.to_string(result, 16)) |> String.downcase() end - def as_fn_call(nil, _calldata) do + def as_fn_call(nil, _calldata, _structs) do nil end - def as_fn_call(input, calldata) do - %{:name => input["name"], :args => as_fn_inputs(input["inputs"], calldata)} + def as_fn_call(function, calldata, structs) do + {inputs, _} = as_fn_inputs(function["inputs"], calldata, structs) + %{:name => function["name"], :args => inputs} end - def as_fn_inputs(inputs, calldata) do - {result, _} = + def as_fn_inputs(inputs, calldata, structs) do + {result, rest} = List.foldl( inputs, {[], calldata}, - fn input, {acc_current, acc_calldata} -> - {fn_input, calldata_rest} = as_fn_input(input, acc_calldata) - {[fn_input | acc_current], calldata_rest} + fn input, {so_far, acc_calldata} -> + {fn_input, calldata_rest} = as_fn_input(input, so_far, acc_calldata, structs) + {[fn_input | so_far], calldata_rest} end ) - Enum.reverse(result) + {Enum.reverse(result), rest} end - def as_fn_input(input, calldata) do - {value, calldata_rest} = get_value_for_type(input["type"], calldata) + def as_fn_input(input, so_far, calldata, structs) do + {value, calldata_rest} = get_value_for_type(input, so_far, calldata, structs) {%{:name => input["name"], :type => input["type"], :value => value}, calldata_rest} end - def get_value_for_type("Uint256", [value1, value2 | rest]) do + def get_value_for_type( + %{"type" => "core::array::Array::<" <> inner_type}, + _so_far, + [ + array_length | rest + ], + structs + ) do + inner_type = String.replace_suffix(inner_type, ">", "") + get_multiple_values_for_type(felt_to_int(array_length), inner_type, rest, structs) + end + + def get_value_for_type(%{"type" => type, "name" => name}, so_far, calldata, structs) do + case String.ends_with?(type, "*") do + true -> + type = String.replace_suffix(type, "*", "!") + get_multiple_values_for_type(get_array_len(name, so_far), type, calldata, structs) + + _ -> + get_value_for_single_type(type, calldata, structs) + end + end + + def get_value_for_single_type("Uint256", [value1, value2 | rest], _structs) do {[value2, value1], rest} end - def get_value_for_type("core::integer::u256", [value1, value2 | rest]) do + def get_value_for_single_type("core::integer::u256", [value1, value2 | rest], _structs) do {[value2, value1], rest} end - def get_value_for_type(_, [value | rest]) do - {value, rest} + def get_value_for_single_type(type, [value | rest], structs) do + case Map.get(structs, type) do + nil -> + {value, rest} + + %{"members" => members} -> + get_value_for_complex_type(members, [value | rest], structs) + end + end + + def get_multiple_values_for_type(0, _type, list, _structs) do + {[], list} + end + + def get_multiple_values_for_type(n, type, list, structs) do + {value, rest} = get_value_for_single_type(type, list, structs) + {values, final_rest} = get_multiple_values_for_type(n - 1, type, rest, structs) + {[value | values], final_rest} + end + + def get_value_for_complex_type(members, list, structs) do + as_fn_inputs(members, list, structs) + end + + def get_array_len(name, input_data) do + Enum.find( + input_data, + fn %{:name => input_name} -> + name <> "_len" == input_name + end + ) + |> Map.get(:value) + |> felt_to_int end def felt_to_int(<<"0x", hexa_value::binary>>) do @@ -135,10 +192,10 @@ defmodule StarknetExplorer.Calldata do value end - def get_input_data(block_id, address, selector, network) do + def get_functions_data(block_id, address, network) do case Rpc.get_class_at(block_id, address, network) do {:ok, class} -> - selectors = parse_class(class) + selectors = get_selectors(class) implementation_selector = Enum.find( @@ -148,23 +205,31 @@ defmodule StarknetExplorer.Calldata do end ) - case implementation_selector do - nil -> - find_by_selector(class, selector) + implementation_data = + case implementation_selector do + nil -> + nil - _ -> - {:ok, [implementation_address_or_hash]} = - Rpc.call(block_id, address, implementation_selector, network) + _ -> + {:ok, [implementation_address_or_hash]} = + Rpc.call(block_id, address, implementation_selector, network) - case Rpc.get_class(block_id, implementation_address_or_hash, network) do - {:ok, class} -> - find_by_selector(class, selector) + case Rpc.get_class(block_id, implementation_address_or_hash, network) do + {:ok, class} -> + process_class_data(class, implementation_address_or_hash, :hash) - {:error, _error} -> - {:ok, class} = Rpc.get_class_at(block_id, implementation_address_or_hash, network) - find_by_selector(class, selector) - end - end + {:error, _error} -> + {:ok, class} = + Rpc.get_class_at(block_id, implementation_address_or_hash, network) + + process_class_data(class, implementation_address_or_hash, :address) + end + end + + %{ + :main => process_class_data(class, address, :address), + :implementation => implementation_data + } {:error, error} -> error |> IO.inspect() @@ -172,7 +237,33 @@ defmodule StarknetExplorer.Calldata do end end - def parse_class(class) do + def process_class_data(class, id, id_type) do + class = normalize_abi(class) + {functions, structs} = functions_by_selector(class) + + %{ + :id => id, + :id_type => id_type, + :class => class, + :functions_by_selector => functions, + :structs_by_name => structs + } + end + + def get_input_data(functions_data, selector) do + {functions, structs} = + case functions_data.implementation do + nil -> + {functions_data.main.functions_by_selector, functions_data.main.structs_by_name} + + implementation -> + {implementation.functions_by_selector, implementation.structs_by_name} + end + + {functions[selector], structs} + end + + def get_selectors(class) do List.foldl( class["entry_points_by_type"]["EXTERNAL"], MapSet.new(), @@ -182,18 +273,35 @@ defmodule StarknetExplorer.Calldata do ) end - def get_input_data_for_hash(block_id, class_hash, selector, network) do - case Rpc.get_class(block_id, class_hash, network) do - {:ok, class} -> - find_by_selector(class, selector) + def functions_by_selector(class) do + functions_by_selector_and_version(class.abi, class["contract_class_version"]) + end - {:error, error} -> - error |> IO.inspect() - nil - end + def functions_by_selector_and_version(abi, nil) do + {abi + |> List.foldl(%{}, &Map.put(&2, &1["name"] |> keccak(), &1)), %{}} + end + + # we assume contract_class_version 0.1.0 + def functions_by_selector_and_version(abi, _contract_class_version) do + abi + |> List.foldl( + {%{}, %{}}, + fn + elem = %{"type" => "interface"}, {fn_acc, struct_acc} -> + {List.foldl(elem["items"], fn_acc, &Map.put(&2, &1["name"] |> keccak(), &1)), + struct_acc} + + elem = %{"type" => "struct"}, {fn_acc, struct_acc} -> + {fn_acc, Map.put(struct_acc, elem["name"], elem)} + + _elem, {fn_acc, struct_acc} -> + {fn_acc, struct_acc} + end + ) end - def find_by_selector(class, selector) do + def normalize_abi(class) do abi = case class["abi"] do abi when is_binary(abi) -> @@ -203,24 +311,8 @@ defmodule StarknetExplorer.Calldata do abi end - find_by_selector_and_version(abi, class["contract_class_version"], selector) - end - - def find_by_selector_and_version(abi, nil, selector) do - Enum.find( - abi, - fn elem -> - elem["name"] |> keccak() == selector - end - ) - end - - # we assume contract_class_version 0.1.0 - def find_by_selector_and_version(abi, _contract_class_version, selector) do - abi - |> Enum.map(& &1["items"]) - |> Enum.filter(& &1) - |> Enum.concat() - |> Enum.find(&(&1["name"] |> keccak() == selector)) + class + |> Map.delete("abi") + |> Map.put(:abi, abi) end end diff --git a/lib/starknet_explorer_web/live/transaction_live.ex b/lib/starknet_explorer_web/live/transaction_live.ex index d1126b3d..a409c2cd 100644 --- a/lib/starknet_explorer_web/live/transaction_live.ex +++ b/lib/starknet_explorer_web/live/transaction_live.ex @@ -535,7 +535,7 @@ defmodule StarknetExplorerWeb.TransactionLive do
Value
- <%= Utils.format_arg_value(arg) %> +
<%= Utils.format_arg_value(arg) %>
diff --git a/lib/starknet_explorer_web/live/utils.ex b/lib/starknet_explorer_web/live/utils.ex index 3df1b5e1..9a1f85a1 100644 --- a/lib/starknet_explorer_web/live/utils.ex +++ b/lib/starknet_explorer_web/live/utils.ex @@ -68,6 +68,17 @@ defmodule StarknetExplorerWeb.Utils do shorten_block_hash(value) end + def format_arg_value(%{:type => "core::felt252", :value => value}) do + shorten_block_hash(value) + end + + def format_arg_value(%{ + :type => "core::starknet::contract_address::ContractAddress", + :value => value + }) do + shorten_block_hash(value) + end + def format_arg_value(%{ :type => "Uint256", :value => [<<"0x", high::binary>>, <<"0x", low::binary>>] @@ -88,7 +99,27 @@ defmodule StarknetExplorerWeb.Utils do |> String.downcase()) end - def format_arg_value(%{:value => value}) do - shorten_block_hash(value) + def format_arg_value(%{ + :type => "core::array::Array::<" <> _rest, + :value => value + }) do + value + |> Jason.encode!() + |> Jason.Formatter.pretty_print() + end + + def format_arg_value(%{:type => type, :value => value}) do + case String.ends_with?(type, "*") do + true -> + type = String.replace_suffix(type, "*", "") + + value + |> Enum.map(&format_arg_value(%{:type => type, :value => &1})) + |> Jason.encode!() + |> Jason.Formatter.pretty_print() + + _ -> + value + end end end