From 8683b66250401805d09d17674fff201c4b3e4400 Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Mon, 29 Apr 2019 13:17:55 -0400 Subject: [PATCH] feat: show raw transaction traces --- CHANGELOG.md | 1 + .../raw_trace/code_highlighting.js | 7 + .../transaction_raw_trace_controller.ex | 55 ++++++ .../lib/block_scout_web/router.ex | 7 + .../templates/transaction/_tabs.html.eex | 5 + .../transaction_raw_trace/_metatags.html.eex | 1 + .../transaction_raw_trace/index.html.eex | 18 ++ .../views/transaction_raw_trace_view.ex | 18 ++ .../block_scout_web/views/transaction_view.ex | 3 +- apps/block_scout_web/priv/gettext/default.pot | 7 + .../priv/gettext/en/LC_MESSAGES/default.po | 7 + .../explorer/chain/internal_transaction.ex | 119 ++++++++++- .../chain/internal_transaction/action.ex | 42 ++++ .../chain/internal_transaction/result.ex | 32 +++ .../chain/internal_transaction_test.exs | 187 +++++++++++++++++- mix.lock | 2 +- 16 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex create mode 100644 apps/explorer/lib/explorer/chain/internal_transaction/action.ex create mode 100644 apps/explorer/lib/explorer/chain/internal_transaction/result.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 66676df93b47..18716d9ff086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [#1815](https://github.com/poanetwork/blockscout/pull/1815) - able to search without prefix "0x" - [#1813](https://github.com/poanetwork/blockscout/pull/1813) - add total blocks counter to the main page - [#1806](https://github.com/poanetwork/blockscout/pull/1806) - verify contracts with a post request +- [#1859](https://github.com/poanetwork/blockscout/pull/1859) - feat: show raw transaction traces ### Fixes diff --git a/apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js b/apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js new file mode 100644 index 000000000000..2ffb1efa6fd9 --- /dev/null +++ b/apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js @@ -0,0 +1,7 @@ +import $ from 'jquery' +import hljs from 'highlight.js' + +// only activate highlighting on pages with this selector +if ($('[data-activate-highlight]').length > 0) { + hljs.initHighlightingOnLoad() +} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex new file mode 100644 index 000000000000..a73290faab77 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex @@ -0,0 +1,55 @@ +defmodule BlockScoutWeb.TransactionRawTraceController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.TransactionView + alias Explorer.{Chain, Market} + alias Explorer.ExchangeRates.Token + + def index(conn, %{"transaction_id" => hash_string}) do + with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string), + {:ok, transaction} <- + Chain.hash_to_transaction( + hash, + necessity_by_association: %{ + :block => :optional, + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + [to_address: :smart_contract] => :optional, + :token_transfers => :optional + } + ) do + options = [ + necessity_by_association: %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional + } + ] + + internal_transactions = Chain.transaction_to_internal_transactions(transaction, options) + + render( + conn, + "index.html", + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + internal_transactions: internal_transactions, + block_height: Chain.block_height(), + show_token_transfers: Chain.transaction_has_token_transfers?(hash), + transaction: transaction + ) + else + :error -> + conn + |> put_status(422) + |> put_view(TransactionView) + |> render("invalid.html", transaction_hash: hash_string) + + {:error, :not_found} -> + conn + |> put_status(404) + |> put_view(TransactionView) + |> render("not_found.html", transaction_hash: hash_string) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index a6e5a4b96818..c0e975793f87 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -99,6 +99,13 @@ defmodule BlockScoutWeb.Router do as: :internal_transaction ) + resources( + "/raw_trace", + TransactionRawTraceController, + only: [:index], + as: :raw_trace + ) + resources("/logs", TransactionLogController, only: [:index], as: :log) resources("/token_transfers", TransactionTokenTransferController, diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex index fb60b05e19a7..83bdb3372f28 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex @@ -20,4 +20,9 @@ "data-test": "transaction_logs_link" ) %> + <%= link( + gettext("Raw Trace"), + class: "nav-link #{tab_status("raw_trace", @conn.request_path)}", + to: transaction_raw_trace_path(@conn, :index, @transaction) + ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex new file mode 100644 index 000000000000..85c3d6675f4a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex new file mode 100644 index 000000000000..2695dea13ba7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex @@ -0,0 +1,18 @@ +
+ <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> + +
+
+ <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> +
+ +
+

<%= gettext "Raw Trace" %>

+ <%= if Enum.count(@internal_transactions) > 0 do %> +
<%= for {line, number} <- raw_traces_with_lines(@internal_transactions) do %>
<%= line %>
<% end %>
+ <% else %> + No trace entries found. + <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex new file mode 100644 index 000000000000..3f56e4e94898 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.TransactionRawTraceView do + use BlockScoutWeb, :view + @dialyzer :no_match + + alias Explorer.Chain.InternalTransaction + + def render("scripts.html", %{conn: conn}) do + render_scripts(conn, "raw_trace/code_highlighting.js") + end + + def raw_traces_with_lines(internal_transactions) do + internal_transactions + |> InternalTransaction.internal_transactions_to_raw() + |> Jason.encode!(pretty: true) + |> String.split("\n") + |> Enum.with_index() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 8fa133550ff1..418a7161deae 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.TransactionView do import BlockScoutWeb.Gettext import BlockScoutWeb.Tokens.Helpers - @tabs ["token_transfers", "internal_transactions", "logs"] + @tabs ["token_transfers", "internal_transactions", "logs", "raw_trace"] defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction] @@ -338,6 +338,7 @@ defmodule BlockScoutWeb.TransactionView do defp tab_name(["token_transfers"]), do: gettext("Token Transfers") defp tab_name(["internal_transactions"]), do: gettext("Internal Transactions") defp tab_name(["logs"]), do: gettext("Logs") + defp tab_name(["raw_trace"]), do: gettext("Raw Trace") defp decode_params(params, types) do params diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index eb4adbee6db3..6f198f87938c 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -1732,3 +1732,10 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:225 msgid "Gas" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:10 +#: lib/block_scout_web/views/transaction_view.ex:341 +msgid "Raw Trace" +msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 9d15643bdcf0..64201bee6548 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -1732,3 +1732,10 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:225 msgid "Gas" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:10 +#: lib/block_scout_web/views/transaction_view.ex:341 +msgid "Raw Trace" +msgstr "" diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 6a8fa7b6bfe2..60a0f3ed0885 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -4,7 +4,7 @@ defmodule Explorer.Chain.InternalTransaction do use Explorer.Schema alias Explorer.Chain.{Address, Data, Gas, Hash, Transaction, Wei} - alias Explorer.Chain.InternalTransaction.{CallType, Type} + alias Explorer.Chain.InternalTransaction.{Action, CallType, Result, Type} @typedoc """ * `block_number` - the `t:Explorer.Chain.Block.t/0` `number` that the `transaction` is collated into. @@ -497,4 +497,121 @@ defmodule Explorer.Chain.InternalTransaction do def where_block_number_is_not_null(query) do where(query, [t], not is_nil(t.block_number)) end + + def internal_transactions_to_raw(internal_transactions) when is_list(internal_transactions) do + internal_transactions + |> Enum.map(&internal_transaction_to_raw/1) + |> add_subtraces() + end + + defp internal_transaction_to_raw(%{type: :call} = transaction) do + %{ + call_type: call_type, + to_address_hash: to_address_hash, + from_address_hash: from_address_hash, + input: input, + gas: gas, + value: value, + trace_address: trace_address + } = transaction + + action = %{ + "callType" => call_type, + "to" => to_address_hash, + "from" => from_address_hash, + "input" => input, + "gas" => gas, + "value" => value + } + + %{ + "type" => "call", + "action" => Action.to_raw(action), + "traceAddress" => trace_address + } + |> put_raw_call_error_or_result(transaction) + end + + defp internal_transaction_to_raw(%{type: :create} = transaction) do + %{ + from_address_hash: from_address_hash, + gas: gas, + init: init, + trace_address: trace_address, + value: value + } = transaction + + action = %{"from" => from_address_hash, "gas" => gas, "init" => init, "value" => value} + + %{ + "type" => "create", + "action" => Action.to_raw(action), + "traceAddress" => trace_address + } + |> put_raw_create_error_or_result(transaction) + end + + defp internal_transaction_to_raw(%{type: :selfdestruct} = transaction) do + %{ + to_address_hash: to_address_hash, + from_address_hash: from_address_hash, + trace_address: trace_address, + value: value + } = transaction + + action = %{ + "address" => from_address_hash, + "balance" => value, + "refundAddress" => to_address_hash + } + + %{ + "type" => "suicide", + "action" => Action.to_raw(action), + "traceAddress" => trace_address + } + end + + defp add_subtraces(traces) do + Enum.map(traces, fn trace -> + Map.put(trace, "subtraces", count_subtraces(trace, traces)) + end) + end + + defp count_subtraces(%{"traceAddress" => trace_address}, traces) do + Enum.count(traces, fn %{"traceAddress" => trace_address_candidate} -> + direct_descendant?(trace_address, trace_address_candidate) + end) + end + + defp direct_descendant?([], [_]), do: true + + defp direct_descendant?([elem | remaining_left], [elem | remaining_right]), + do: direct_descendant?(remaining_left, remaining_right) + + defp direct_descendant?(_, _), do: false + + defp put_raw_call_error_or_result(raw, %{error: error}) when not is_nil(error) do + Map.put(raw, "error", error) + end + + defp put_raw_call_error_or_result(raw, %{gas_used: gas_used, output: output}) do + Map.put(raw, "result", Result.to_raw(%{"gasUsed" => gas_used, "output" => output})) + end + + defp put_raw_create_error_or_result(raw, %{error: error}) when not is_nil(error) do + Map.put(raw, "error", error) + end + + defp put_raw_create_error_or_result(raw, %{ + created_contract_code: code, + created_contract_address_hash: created_contract_address_hash, + gas_used: gas_used + }) do + Map.put( + raw, + "result", + Result.to_raw(%{"gasUsed" => gas_used, "code" => code, "address" => created_contract_address_hash}) + ) + end end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/action.ex b/apps/explorer/lib/explorer/chain/internal_transaction/action.ex new file mode 100644 index 000000000000..663e30651501 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/internal_transaction/action.ex @@ -0,0 +1,42 @@ +defmodule Explorer.Chain.InternalTransaction.Action do + @moduledoc """ + The action that was performed in a `t:EthereumJSONRPC.Parity.Trace.t/0` + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1] + alias Explorer.Chain.{Data, Hash, Wei} + + def to_raw(action) when is_map(action) do + Enum.into(action, %{}, &entry_to_raw/1) + end + + defp entry_to_raw({key, %Data{} = data}) when key in ~w(init input) do + {key, Data.to_string(data)} + end + + defp entry_to_raw({key, %Hash{} = address}) when key in ~w(address from refundAddress to) do + {key, to_string(address)} + end + + defp entry_to_raw({"callType", type}) do + {"callType", Atom.to_string(type)} + end + + defp entry_to_raw({"gas" = key, %Decimal{} = decimal}) do + value = + decimal + |> Decimal.round() + |> Decimal.to_integer() + + {key, integer_to_quantity(value)} + end + + defp entry_to_raw({key, %Wei{value: value}}) when key in ~w(balance value) do + rounded = + value + |> Decimal.round() + |> Decimal.to_integer() + + {key, integer_to_quantity(rounded)} + end +end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/result.ex b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex new file mode 100644 index 000000000000..5b4e3102fc3e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex @@ -0,0 +1,32 @@ +defmodule Explorer.Chain.InternalTransaction.Result do + @moduledoc """ + The result of performing the `t:EthereumJSONRPC.Parity.Action.t/0` in a `t:EthereumJSONRPC.Parity.Trace.t/0`. + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + alias Explorer.Chain.{Data, Hash} + + def to_raw(result) when is_map(result) do + Enum.into(result, %{}, &entry_to_raw/1) + end + + defp entry_to_raw({"output" = key, %Data{} = data}) do + {key, Data.to_string(data)} + end + + defp entry_to_raw({"address" = key, %Hash{} = hash}) do + {key, to_string(hash)} + end + + defp entry_to_raw({"code", _} = entry), do: entry + + defp entry_to_raw({key, decimal}) when key in ~w(gasUsed) do + integer = + decimal + |> Decimal.round() + |> Decimal.to_integer() + + {key, integer_to_quantity(integer)} + end +end diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index 2b92339515b9..54ace519bdf8 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1,7 +1,10 @@ defmodule Explorer.Chain.InternalTransactionTest do use Explorer.DataCase - alias Explorer.Chain.InternalTransaction + alias Explorer.Chain.{InternalTransaction, Wei} + alias Explorer.Factory + + import EthereumJSONRPC, only: [integer_to_quantity: 1] doctest InternalTransaction @@ -54,4 +57,186 @@ defmodule Explorer.Chain.InternalTransactionTest do assert Repo.insert(changeset) end end + + defp call_type(opts) do + defaults = [ + type: :call, + call_type: :call, + to_address_hash: Factory.address_hash(), + from_address_hash: Factory.address_hash(), + input: Factory.transaction_input(), + output: Factory.transaction_input(), + gas: Decimal.new(50_000), + gas_used: Decimal.new(25_000), + value: %Wei{value: 100}, + index: 0, + trace_address: [] + ] + + struct!(InternalTransaction, Keyword.merge(defaults, opts)) + end + + defp create_type(opts) do + defaults = [ + type: :create, + from_address_hash: Factory.address_hash(), + gas: Decimal.new(50_000), + gas_used: Decimal.new(25_000), + value: %Wei{value: 100}, + index: 0, + init: Factory.transaction_input(), + trace_address: [] + ] + + struct!(InternalTransaction, Keyword.merge(defaults, opts)) + end + + defp selfdestruct_type(opts) do + defaults = [ + type: :selfdestruct, + from_address_hash: Factory.address_hash(), + to_address_hash: Factory.address_hash(), + gas: Decimal.new(50_000), + gas_used: Decimal.new(25_000), + value: %Wei{value: 100}, + index: 0, + trace_address: [] + ] + + struct!(InternalTransaction, Keyword.merge(defaults, opts)) + end + + describe "internal_transactions_to_raw" do + test "it adds subtrace count" do + transactions = [ + call_type(trace_address: []), + call_type(trace_address: [0]), + call_type(trace_address: [1]), + call_type(trace_address: [2]), + call_type(trace_address: [0, 0]), + call_type(trace_address: [0, 1]), + call_type(trace_address: [1, 0]), + call_type(trace_address: [0, 0, 0]), + call_type(trace_address: [0, 0, 1]), + call_type(trace_address: [0, 0, 2]), + call_type(trace_address: [0, 1, 0]), + call_type(trace_address: [0, 1, 1]) + ] + + subtraces = + transactions + |> InternalTransaction.internal_transactions_to_raw() + |> Enum.map(&Map.get(&1, "subtraces")) + + assert subtraces == [3, 2, 1, 0, 3, 2, 0, 0, 0, 0, 0, 0] + end + + test "it correctly formats a call" do + from = Factory.address_hash() + to = Factory.address_hash() + gas = 50_000 + gas_used = 25_000 + input = Factory.transaction_input() + value = 50 + output = Factory.transaction_input() + + call_transaction = + call_type( + from_address_hash: from, + to_address_hash: to, + gas: Decimal.new(gas), + gas_used: Decimal.new(gas_used), + input: input, + value: %Wei{value: value}, + output: output + ) + + [call] = InternalTransaction.internal_transactions_to_raw([call_transaction]) + + assert call == %{ + "action" => %{ + "callType" => "call", + "from" => to_string(from), + "gas" => integer_to_quantity(gas), + "input" => to_string(input), + "to" => to_string(to), + "value" => integer_to_quantity(value) + }, + "result" => %{ + "gasUsed" => integer_to_quantity(gas_used), + "output" => to_string(output) + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "call" + } + end + + test "it correctly formats a create" do + contract_code = Factory.contract_code_info().bytecode + contract_address = Factory.address_hash() + from = Factory.address_hash() + gas = 50_000 + gas_used = 25_000 + init = Factory.transaction_input() + value = 50 + + create_transaction = + create_type( + from_address_hash: from, + created_contract_code: contract_code, + created_contract_address_hash: contract_address, + gas: Decimal.new(gas), + gas_used: Decimal.new(gas_used), + init: init, + value: %Wei{value: value} + ) + + [create] = InternalTransaction.internal_transactions_to_raw([create_transaction]) + + assert create == %{ + "action" => %{ + "from" => to_string(from), + "gas" => integer_to_quantity(gas), + "init" => to_string(init), + "value" => integer_to_quantity(value) + }, + "result" => %{ + "address" => to_string(contract_address), + "code" => to_string(contract_code), + "gasUsed" => integer_to_quantity(gas_used) + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "create" + } + end + + test "it correctly formats a selfdestruct" do + from_address = Factory.address_hash() + to_address = Factory.address_hash() + + value = 50 + + selfdestruct_transaction = + selfdestruct_type( + to_address_hash: to_address, + from_address_hash: from_address, + value: %Wei{value: value} + ) + + [selfdestruct] = InternalTransaction.internal_transactions_to_raw([selfdestruct_transaction]) + + assert selfdestruct == %{ + "action" => %{ + "address" => to_string(from_address), + "balance" => integer_to_quantity(value), + "refundAddress" => to_string(to_address) + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "suicide" + } + end + end end diff --git a/mix.lock b/mix.lock index e9066554b1ef..4ed50a45fe9f 100644 --- a/mix.lock +++ b/mix.lock @@ -33,7 +33,7 @@ "ecto": {:hex, :ecto, "3.0.7", "44dda84ac6b17bbbdeb8ac5dfef08b7da253b37a453c34ab1a98de7f7e5fec7f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.0.4", "e7a0feb0b2484b90981c56d5cd03c52122c1c31ded0b95ed213b7c5c07ae6737", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, - "ex_abi": {:hex, :ex_abi, "0.1.18", "19db9bffdd201edbdff97d7dd5849291218b17beda045c1b76bff5248964f37d", [:mix], [{:exth_crypto, "~> 0.1.4", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_abi": {:hex, :ex_abi, "0.2.1", "e8224ef22782b58d535bf3e29e73379af48df52cc9a941746980d14b32e793c2", [:mix], [{:exth_crypto, "~> 0.1.6", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "ex_cldr": {:hex, :ex_cldr, "1.3.2", "8f4a00c99d1c537b8e8db7e7903f4bd78d82a7289502d080f70365392b13921b", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, optional: true]}]}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.2.0", "ef27299922da913ffad1ed296cacf28b6452fc1243b77301dc17c03276c6ee34", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, optional: false]}, {:ex_cldr, "~> 1.3", [hex: :ex_cldr, optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, optional: false]}]}, "ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, optional: false]}]},