Merge pull request #8431 from blockscout/np-fix-contracts-output-decoding

Fix contracts' output decoding
pull/8446/head
Victor Baranov 1 year ago committed by GitHub
commit 1c060b8f40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 27
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  3. 81
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  4. 142
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs
  5. 49
      apps/block_scout_web/test/block_scout_web/views/smart_contract_view_test.exs
  6. 83
      apps/explorer/lib/explorer/smart_contract/reader.ex

@ -15,6 +15,7 @@
### Fixes
- [#8431](https://github.com/blockscout/blockscout/pull/8431) - Fix contracts' output decoding
- [#8354](https://github.com/blockscout/blockscout/pull/8354) - Hotfix for proper addresses' tokens displaying
- [#8350](https://github.com/blockscout/blockscout/pull/8350) - Add Base Mainnet support for tx actions
- [#8282](https://github.com/blockscout/blockscout/pull/8282) - NFT fetcher improvements

@ -1,6 +1,8 @@
defmodule BlockScoutWeb.API.V2.SmartContractView do
use BlockScoutWeb, :view
import Explorer.SmartContract.Reader, only: [zip_tuple_values_with_types: 2]
alias ABI.FunctionSelector
alias BlockScoutWeb.API.V2.{Helper, TransactionView}
alias BlockScoutWeb.SmartContractView
@ -284,28 +286,31 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
%{"type" => type, "value" => render_json(value, type)}
end
def render_json(value, type) when type in [:address, "address", "address payable"] do
SmartContractView.cast_address(value)
end
def render_json(value, type) when type in [:string, "string"] do
to_string(value)
end
def render_json(value, type) when is_tuple(value) do
value
|> SmartContractView.zip_tuple_values_with_types(type)
|> zip_tuple_values_with_types(type)
|> Enum.map(fn {type, value} ->
render_json(value, type)
end)
end
def render_json(value, type) when is_list(value) do
type =
if String.ends_with?(type, "[]") do
String.slice(type, 0..-3)
else
type
end
value |> Enum.map(&render_json(&1, type))
end
def render_json(value, _type) when is_binary(value) do
SmartContractView.binary_to_utf_string(value)
def render_json(value, type) when type in [:address, "address", "address payable"] do
SmartContractView.cast_address(value)
end
def render_json(value, type) when type in [:string, "string"] do
to_string(value)
end
def render_json(value, _type) do

@ -1,6 +1,8 @@
defmodule BlockScoutWeb.SmartContractView do
use BlockScoutWeb, :view
import Explorer.SmartContract.Reader, only: [zip_tuple_values_with_types: 2]
alias Explorer.Chain
alias Explorer.Chain.{Address, Transaction}
alias Explorer.Chain.Hash.Address, as: HashAddress
@ -72,7 +74,7 @@ defmodule BlockScoutWeb.SmartContractView do
String.starts_with?(type, "bytes") ->
values =
value
|> Enum.map_join(", ", &binary_to_utf_string(&1))
|> Enum.join(", ")
render_array_type_value(type, values, fetch_name(names, index))
@ -107,6 +109,9 @@ defmodule BlockScoutWeb.SmartContractView do
def values_with_type(value, string, names, index, _components) when string in ["string", :string],
do: render_type_value("string", Helper.sanitize_input(value), fetch_name(names, index))
def values_with_type(value, "bytes" <> _ = bytes_type, names, index, _components),
do: render_type_value(bytes_type, Helper.sanitize_input(value), fetch_name(names, index))
def values_with_type(value, bytes, names, index, _components) when bytes in [:bytes],
do: render_type_value("bytes", Helper.sanitize_input(value), fetch_name(names, index))
@ -114,7 +119,7 @@ defmodule BlockScoutWeb.SmartContractView do
do: render_type_value("bool", Helper.sanitize_input(to_string(value)), fetch_name(names, index))
def values_with_type(value, type, names, index, _components),
do: render_type_value(type, Helper.sanitize_input(binary_to_utf_string(value)), fetch_name(names, index))
do: render_type_value(type, Helper.sanitize_input(value), fetch_name(names, index))
def values_with_type(value, :error, _components),
do: render_type_value("error", Helper.sanitize_input(value), "error")
@ -158,78 +163,6 @@ defmodule BlockScoutWeb.SmartContractView do
end)
end
def zip_tuple_values_with_types(value, type) do
types_string =
type
|> String.slice(6..-2)
types =
if String.trim(types_string) == "" do
[]
else
types_string
|> String.split(",")
end
{tuple_types, _} =
types
|> Enum.reduce({[], nil}, fn val, acc ->
{arr, to_merge} = acc
if to_merge do
compose_array_if_to_merge(arr, val, to_merge)
else
compose_array_else(arr, val, to_merge)
end
end)
values_list =
value
|> Tuple.to_list()
Enum.zip(tuple_types, values_list)
end
def compose_array_if_to_merge(arr, val, to_merge) do
if count_string_symbols(val)["]"] > count_string_symbols(val)["["] do
updated_arr = update_last_list_item(arr, val)
{updated_arr, !to_merge}
else
updated_arr = update_last_list_item(arr, val)
{updated_arr, to_merge}
end
end
def compose_array_else(arr, val, to_merge) do
if count_string_symbols(val)["["] > count_string_symbols(val)["]"] do
# credo:disable-for-next-line
{arr ++ [val], !to_merge}
else
# credo:disable-for-next-line
{arr ++ [val], to_merge}
end
end
defp update_last_list_item(arr, new_val) do
arr
|> Enum.with_index()
|> Enum.map(fn {item, index} ->
if index == Enum.count(arr) - 1 do
item <> "," <> new_val
else
item
end
end)
end
defp count_string_symbols(str) do
str
|> String.graphemes()
|> Enum.reduce(%{"[" => 0, "]" => 0}, fn char, acc ->
Map.update(acc, char, 1, &(&1 + 1))
end)
end
def binary_to_utf_string(item) do
case Integer.parse(to_string(item)) do
{item_integer, ""} ->

@ -702,6 +702,148 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"method_id" => Base.encode16(id, case: :lower)
} in response
end
test "get correct bytes value 1", %{conn: conn} do
abi = [
%{
"inputs" => [],
"name" => "all_messages_hash",
"outputs" => [
%{
"internalType" => "bytes32",
"name" => "",
"type" => "bytes32"
}
],
"stateMutability" => "view",
"type" => "function"
}
]
id_1 =
abi
|> ABI.parse_specification()
|> Enum.at(0)
|> Map.fetch!(:method_id)
target_contract = insert(:smart_contract, abi: abi)
address_hash_string = to_string(target_contract.address_hash)
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [
%{
id: id,
method: "eth_call",
params: [
%{data: "0x1dd69d06", to: ^address_hash_string},
"latest"
]
}
],
_opts ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
]}
end
)
request = get(conn, "/api/v2/smart-contracts/#{target_contract.address_hash}/methods-read")
assert response = json_response(request, 200)
assert %{
"inputs" => [],
"name" => "all_messages_hash",
"outputs" => [
%{
"value" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"type" => "bytes32"
}
],
"stateMutability" => "view",
"type" => "function",
"method_id" => Base.encode16(id_1, case: :lower),
"names" => ["bytes32"]
} in response
end
test "get correct bytes value 2", %{conn: conn} do
abi = [
%{
"inputs" => [],
"name" => "FRAUD_STRING",
"outputs" => [
%{
"internalType" => "bytes",
"name" => "",
"type" => "bytes"
}
],
"stateMutability" => "view",
"type" => "function"
}
]
id_2 =
abi
|> ABI.parse_specification()
|> Enum.at(0)
|> Map.fetch!(:method_id)
target_contract = insert(:smart_contract, abi: abi)
address_hash_string = to_string(target_contract.address_hash)
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [
%{
id: id,
method: "eth_call",
params: [
%{data: "0x46b2eb9b", to: ^address_hash_string},
"latest"
]
}
],
_opts ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result:
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000322d2d5468697320697320612062616420737472696e672e204e6f626f64792073617973207468697320737472696e672e2d2d0000000000000000000000000000"
}
]}
end
)
request = get(conn, "/api/v2/smart-contracts/#{target_contract.address_hash}/methods-read")
assert response = json_response(request, 200)
assert %{
"inputs" => [],
"name" => "FRAUD_STRING",
"outputs" => [
%{
"value" =>
"0x2d2d5468697320697320612062616420737472696e672e204e6f626f64792073617973207468697320737472696e672e2d2d",
"type" => "bytes"
}
],
"stateMutability" => "view",
"type" => "function",
"method_id" => Base.encode16(id_2, case: :lower),
"names" => ["bytes"]
} in response
end
end
describe "/smart-contracts/{address_hash}/query-read-method" do

@ -8,47 +8,32 @@ defmodule BlockScoutWeb.SmartContractViewTest do
describe "values_with_type/1" do
test "complex data type case" do
value =
{<<156, 209, 70, 119, 249, 170, 85, 105, 179, 187, 179, 81, 252, 214, 125, 17, 21, 170, 86, 58, 225, 98, 66,
118, 211, 212, 230, 127, 179, 214, 249, 38>>, 23_183_417, true,
{"0x9cd14677f9aa5569b3bbb351fcd67d1115aa563ae1624276d3d4e67fb3d6f926", 23_183_417, true,
[
{<<164, 118, 64, 69, 133, 31, 23, 170, 96, 182, 200, 232, 182, 32, 114, 190, 169, 83, 133, 33>>,
[
<<15, 103, 152, 165, 96, 121, 58, 84, 195, 188, 254, 134, 169, 60, 222, 30, 115, 8, 125, 148, 76, 14, 162,
5, 68, 19, 125, 65, 33, 57, 104, 133>>,
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 61, 111, 131, 12, 226, 99, 202, 233, 135, 25, 57, 130, 25, 44,
217, 144, 68, 43, 83>>
],
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 178, 96, 212, 241, 78, 0, 0>>},
"0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
"0x000000000000000000000000bf3d6f830ce263cae987193982192cd990442b53"
], "0x000000000000000000000000000000000000000000000000aab260d4f14e0000"},
{<<164, 118, 64, 69, 133, 31, 23, 170, 96, 182, 200, 232, 182, 32, 114, 190, 169, 83, 133, 33>>,
[
<<221, 242, 82, 173, 27, 226, 200, 155, 105, 194, 176, 104, 252, 55, 141, 170, 149, 43, 167, 241, 99, 196,
161, 22, 40, 245, 90, 77, 245, 35, 179, 239>>,
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>,
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 61, 111, 131, 12, 226, 99, 202, 233, 135, 25, 57, 130, 25, 44,
217, 144, 68, 43, 83>>
],
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 178, 96, 212, 241, 78, 0, 0>>},
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x000000000000000000000000bf3d6f830ce263cae987193982192cd990442b53"
], "0x000000000000000000000000000000000000000000000000aab260d4f14e0000"},
{<<166, 139, 214, 89, 169, 22, 127, 61, 60, 1, 186, 151, 118, 161, 32, 141, 174, 143, 0, 59>>,
[
<<47, 154, 96, 152, 212, 80, 58, 18, 119, 121, 186, 151, 95, 95, 107, 4, 248, 66, 54, 43, 24, 9, 243, 70,
152, 158, 154, 188, 11, 77, 237, 182>>,
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 61, 111, 131, 12, 226, 99, 202, 233, 135, 25, 57, 130, 25, 44,
217, 144, 68, 43, 83>>,
<<0, 5, 0, 0, 36, 155, 252, 47, 60, 200, 214, 143, 107, 107, 247, 35, 14, 160, 168, 237, 133, 61, 231, 49,
0, 0, 0, 0, 0, 0, 2, 79>>
],
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 178, 96, 212, 241, 78, 0, 0>>},
"0x2f9a6098d4503a127779ba975f5f6b04f842362b1809f346989e9abc0b4dedb6",
"0x000000000000000000000000bf3d6f830ce263cae987193982192cd990442b53",
"0x00050000249bfc2f3cc8d68f6b6bf7230ea0a8ed853de731000000000000024f"
], "0x000000000000000000000000000000000000000000000000aab260d4f14e0000"},
{<<254, 68, 107, 239, 29, 191, 122, 254, 36, 232, 30, 5, 188, 139, 39, 28, 27, 169, 165, 96>>,
[
<<39, 51, 62, 219, 139, 220, 212, 10, 10, 233, 68, 251, 18, 27, 94, 45, 98, 234, 120, 38, 131, 148, 102,
84, 160, 245, 230, 7, 169, 8, 213, 120>>,
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 95, 197, 45, 138, 86, 59, 47, 24, 28, 106, 82, 125, 66, 46, 21,
146, 201, 236, 250>>,
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 139, 214, 89, 169, 22, 127, 61, 60, 1, 186, 151, 118, 161, 32,
141, 174, 143, 0, 59>>,
<<0, 5, 0, 0, 36, 155, 252, 47, 60, 200, 214, 143, 107, 107, 247, 35, 14, 160, 168, 237, 133, 61, 231, 49,
0, 0, 0, 0, 0, 0, 2, 79>>
], <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>}
"0x27333edb8bdcd40a0ae944fb121b5e2d62ea782683946654a0f5e607a908d578",
"0x0000000000000000000000002a5fc52d8a563b2f181c6a527d422e1592c9ecfa",
"0x000000000000000000000000a68bd659a9167f3d3c01ba9776a1208dae8f003b",
"0x00050000249bfc2f3cc8d68f6b6bf7230ea0a8ed853de731000000000000024f"
], "0x0000000000000000000000000000000000000000000000000000000000000001"}
]}
type = "tuple[bytes32,uint256,bool,tuple[address,bytes32[],bytes][]]"

@ -754,6 +754,27 @@ defmodule Explorer.SmartContract.Reader do
Map.put_new(output, "value", Encoder.unescape(value))
end
defp new_value(%{"type" => "tuple" <> _types = type} = output, values, index) do
value = Enum.at(values, index)
result =
if String.ends_with?(type, "[]") do
value
|> Enum.map(fn tuple -> new_value(%{"type" => String.slice(type, 0..-3)}, [tuple], 0) end)
|> flat_arrays_map()
else
value
|> zip_tuple_values_with_types(type)
|> Enum.map(fn {type, part_value} ->
new_value(%{"type" => type}, [part_value], 0)
end)
|> flat_arrays_map()
|> List.to_tuple()
end
Map.put_new(output, "value", result)
end
defp new_value(output, [value], _index) do
Map.put_new(output, "value", value)
end
@ -762,6 +783,68 @@ defmodule Explorer.SmartContract.Reader do
Map.put_new(output, "value", Enum.at(values, index))
end
defp flat_arrays_map(%{"value" => value}) do
flat_arrays_map(value)
end
defp flat_arrays_map(value) when is_list(value) do
Enum.map(value, &flat_arrays_map/1)
end
defp flat_arrays_map(value) when is_tuple(value) do
value
|> Tuple.to_list()
|> flat_arrays_map()
|> List.to_tuple()
end
defp flat_arrays_map(value) do
value
end
@spec zip_tuple_values_with_types(tuple, binary) :: [{binary, any}]
def zip_tuple_values_with_types(value, type) do
types_string =
type
|> String.slice(6..-2)
types =
if String.trim(types_string) == "" do
[]
else
types_string
|> String.graphemes()
end
tuple_types =
types
|> Enum.reduce(
{[""], 0},
fn
",", {types_acc, 0} ->
{["" | types_acc], 0}
char, {[acc | types_acc], bracket_stack} ->
new_bracket_stack =
case char do
"[" -> bracket_stack + 1
"]" -> bracket_stack - 1
_ -> bracket_stack
end
{[acc <> char | types_acc], new_bracket_stack}
end
)
|> elem(0)
|> Enum.reverse()
values_list =
value
|> Tuple.to_list()
Enum.zip(tuple_types, values_list)
end
@spec bytes_to_string(<<_::_*8>>) :: String.t()
defp bytes_to_string(value) do
if value do

Loading…
Cancel
Save