feat: fix ABI decodding, and decode tx logs

pull/1119/head
zachdaniel 6 years ago
parent 4432ba4222
commit 21a1c1403c
  1. 25
      apps/block_scout_web/assets/css/components/_transaction-input.scss
  2. 155
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  3. 116
      apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex
  4. 108
      apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex
  5. 6
      apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex
  6. 91
      apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs
  7. 2
      apps/ethereum_jsonrpc/mix.exs
  8. 59
      apps/explorer/lib/explorer/chain/log.ex
  9. 2
      mix.lock

@ -1,9 +1,26 @@
.raw-transaction-input{ .raw-transaction-input{
display: none; display: none;
}
.raw-transaction-log-topics{
display: none;
}
.raw-transaction-log-data{
display: none;
} }
.transaction-input-text{ .transaction-input-text{
resize: vertical; white-space: pre;
overflow: auto; color: black;
word-break: break-all;
pre{
code{
color: black;
}
}
}
.transaction-input-table{
overflow-x: scroll;
} }

@ -75,102 +75,75 @@
</dl> </dl>
<!-- Input --> <!-- Input -->
<dl class="row mb-0"> <h3 class="text-muted"><%= gettext "Input" %></h3>
<%= case decoded_input_data(@transaction) do %> <%= case decoded_input_data(@transaction) do %>
<% {:error, :contract_not_verified} -> %> <% {:error, :contract_not_verified} -> %>
<dt class="col-sm-3 text-muted"> <%= gettext "Input" %> </dt> <div class="alert alert-danger">
<dd class="col-sm-9"> To see decoded input data, the <a href="<%= address_verify_contract_path(@conn, :new, @transaction.to_address.hash)%>" target="_blank">contract must be verified.</a>
<div class="alert alert-danger"> </div>
To see decoded input data, the <a href="<%= address_verify_contract_path(@conn, :new, @transaction.to_address.hash)%>">contract must be verified.</a> <% {:error, :could_not_decode} -> %>
</div> <div class="alert alert-danger">
</dd> Failed to decode input data. Some dynamic types are not currently supported.
<% {:error, :could_not_decode} -> %> </div>
<dt class="col-sm-3 text-muted"> <%= gettext "Input" %> </dt> <% {:ok, method_id, text, mapping} -> %>
<dd class="col-sm-9"> <table summary="Transaction Info" class="table thead-light table-bordered table-responsive transaction-info-table">
<div class="alert alert-danger"> <tr>
Failed to decode input data. Some dynamic types are not currently supported. <td>Method Id</td>
</div> <td colspan="3"><code>0x<%= method_id %></code></td>
</dd> </tr>
<% {:ok, method_id, text, mapping} -> %> <tr>
<dt class="col-sm-3 text-muted"> <%= gettext "Input" %> </dt> <td>Call</td>
<dd class="col-sm-9"> <td colspan="3"><code><%= text %></code></td>
<table summary="Transaction Info" class="table thead-light table-bordered"> </tr>
<tr> </table>
<td>Method Id</td>
<td colspan="3"><code>0x<%= method_id %></code></td> <table summary="Transaction Inputs" class="table thead-light table-bordered table-responsive">
</tr> <tr>
<th scope="col"></th>
<th scope="col"><%= gettext "Name" %></th>
<th scope="col"><%= gettext "Type" %></th>
<th scope="col"><%= gettext "Data" %></th>
<tr>
<%= for {name, type, value} <- mapping do %>
<tr> <tr>
<td>Call</td> <th scope="row">
<td colspan="3"><code><%= text %></code></td> <% copy_text = BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) %>
<button type="button" class="copy icon-link" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= copy_text %>" aria-label="Copy Value">
<i class="fas fa-clone"></i>
</button>
</th>
<td><%= name %></td>
<td><%= type %></td>
<td>
<pre class="transaction-input-text tile"><code><%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %></code></pre>
</td>
</tr> </tr>
</table> <% end %>
<table summary="Transaction Inputs" class="table thead-light table-bordered table-responsive"> </table>
<tr> <% _ -> %>
<th scope="col">#</th> <%= nil %>
<th scope="col">Name</th> <% end %>
<th scope="col">Type</th>
<th scope="col">Data</th>
<th scope="col"></th>
<tr>
<%= for {{name, type, value}, index} <- Enum.with_index(mapping) do %>
<tr>
<th scope="row"><%= index %></th>
<td><%= name %></td>
<td><%= type %></td>
<%= case type do %>
<% "address" -> %>
<% address = "0x" <> Base.encode16(value, case: :lower) %>
<td>
<div class="transaction-input-text">
<a href="<%= address_path(@conn, :show, address) %>" target="_blank"> <%= address %> </a>
</div>
</td>
<td>
<button type="button" class="copy" id="button" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= address %>" aria-label="Copy Value">
<%= gettext "copy"%>
</button>
</td>
<% _ -> %> <%= unless @transaction.input.bytes in [<<>>, nil] do %>
<td> <h3 class="text-muted"><%= gettext "Raw Input" %></h3>
<div class="transaction-input-text"><%= value %></textarea> <div swappable-item>
</td> <button swapper class="button button-primary"><%= gettext "Show Raw Input"%></button>
<td> </div>
<button type="button" class="copy" id="button" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= value %>" aria-label="Copy Value"> <div swappable-item class="raw-transaction-input">
<%= gettext "copy"%> <button swapper type="button" class="close pr-2" aria-label="Close">
</button> <span aria-hidden="true">&times;</span>
</td> </button>
<% end %> <button type="button" class="copy icon-link mb-1" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= @transaction.input %>" aria-label="Copy Value">
</tr> <i class="fas fa-clone"></i>
<% end %> </button>
</table>
</dd>
<% _ -> %>
<%= nil %>
<% end %>
<%= unless @transaction.input.bytes in [<<>>, nil] do %> <div class="tile tile-muted">
<dt class="col-sm-3 text-muted"><%= gettext "Raw Input" %></dt> <pre class="pre-scrollable pre-scrollable-shorty pre-wrap mb-0">
<dd class="col-sm-9"> <code><%= @transaction.input %></code>
<div swappable-item> </pre>
<button swapper class="button button-primary"><%= gettext "Show Raw Input"%></button>
</div> </div>
<div swappable-item class="raw-transaction-input"> </div>
<button swapper type="button" class="close pr-2" aria-label="Close"> <% end %>
<span aria-hidden="true">&times;</span>
</button>
<div class="tile tile-muted">
<button type="button" class="copy" id="button" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= @transaction.input %>" aria-label="Copy Value">
<%= gettext "copy"%>
</button>
<pre class="pre-scrollable pre-scrollable-shorty pre-wrap mb-0">
<code><%= @transaction.input %></code>
</pre>
</div>
</div>
</dd>
<% end %>
</dl>
</div> </div>
</div> </div>
</div> </div>

@ -23,39 +23,105 @@
) %> ) %>
</h3> </h3>
</dd> </dd>
<dt class="col-md-1"><%= gettext "Topics" %></dt> <dt class="col-md-1"><%= gettext "Decoded" %></dt>
<dd class="col-md-11"> <dd class="col-md-11">
<%= unless is_nil(log.first_topic) do %> <%= case decode(log, @transaction) do %>
<div class="text-dark"> <% {:error, :contract_not_verified} -> %>
<span class="text-dark">[0]</span> <div class="alert alert-danger">
<%= log.first_topic %> To see decoded input data, the <a href="<%= address_verify_contract_path(@conn, :new, @transaction.to_address.hash)%>" target="_blank">contract must be verified.</a>
</div> </div>
<% end %> <% {:error, :could_not_decode} -> %>
<%= unless is_nil(log.second_topic) do %> <div class="alert alert-danger">
<div class="text-dark"> Failed to decode log data.
<span class="">[1] </span> </div>
<%= log.second_topic %> <% {:ok, method_id, text, mapping} -> %>
</div> <table summary="Transaction Info" class="table thead-light table-bordered transaction-input-table">
<% end %> <tr>
<%= unless is_nil(log.third_topic) do %> <td>Method Id</td>
<div class="text-dark"> <td colspan="3"><code>0x<%= method_id %></code></td>
<span>[2]</span> </tr>
<%= log.third_topic %> <tr>
</div> <td>Call</td>
<% end %> <td colspan="3"><code><%= text %></code></td>
<%= unless is_nil(log.fourth_topic) do %> </tr>
<div class="text-dark"> </table>
<span>[3]</span> <table style="color: black;" summary="Log Data" class="table thead-light table-bordered table-responsive">
<%= log.fourth_topic %> <tr>
</div> <th scope="col"></th>
<th scope="col"><%= gettext "Name" %></th>
<th scope="col"><%= gettext "Type" %></th>
<th scope="col"><%= gettext "Indexed?" %></th>
<th scope="col"><%= gettext "Data" %></th>
<tr>
<%= for {name, type, indexed?, value} <- mapping do %>
<tr>
<th scope="row">
<% copy_text = BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) %>
<button type="button" class="copy icon-link" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= copy_text %>" aria-label="Copy Value">
<i class="fas fa-clone"></i>
</button>
</th>
<td><%= name %></td>
<td><%= type %></td>
<td><%= indexed? %></td>
<td>
<pre class="transaction-input-text tile"><code><%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %></code></pre>
</td>
</tr>
<% end %>
</table>
<% end %> <% end %>
</dd>
<dt class="col-md-1"><%= gettext "Topics" %></dt>
<dd class="col-md-11">
<div swappable-item>
<button swapper class="button button-primary"><%= gettext "Show Raw Topics"%></button>
</div>
<div swappable-item class="raw-transaction-log-topics">
<button swapper type="button" class="close pr-2" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<%= unless is_nil(log.first_topic) do %>
<div class="text-dark">
<span class="text-dark">[0]</span>
<%= log.first_topic %>
</div>
<% end %>
<%= unless is_nil(log.second_topic) do %>
<div class="text-dark">
<span class="">[1] </span>
<%= log.second_topic %>
</div>
<% end %>
<%= unless is_nil(log.third_topic) do %>
<div class="text-dark">
<span>[2]</span>
<%= log.third_topic %>
</div>
<% end %>
<%= unless is_nil(log.fourth_topic) do %>
<div class="text-dark">
<span>[3]</span>
<%= log.fourth_topic %>
</div>
<% end %>
</div>
</dd> </dd>
<dt class="col-md-1"> <dt class="col-md-1">
<%= gettext "Data" %> <%= gettext "Data" %>
</dt> </dt>
<dd class="col-md-11"> <dd class="col-md-11">
<%= unless is_nil(log.data) do %> <%= unless is_nil(log.data) do %>
<div class="text-dark"> <div swappable-item>
<button swapper class="button button-primary"><%= gettext "Show Raw Data"%></button>
</div>
<div swappable-item class="text-dark raw-transaction-log-data">
<button swapper type="button" class="close pr-2" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<%= log.data %> <%= log.data %>
</div> </div>
<% end %> <% end %>

@ -0,0 +1,108 @@
defmodule BlockScoutWeb.ABIEncodedValueView do
@moduledoc """
Renders a decoded value that is encoded according to an ABI.
Does not leverage an eex template because it renders formatted
values via `<pre>` tags, and that is hard to do in an eex template.
"""
use BlockScoutWeb, :view
require Logger
def value_html(type, value) do
decoded_type = ABI.FunctionSelector.decode_type(type)
do_value_html(decoded_type, value)
rescue
exception ->
Logger.warn(fn ->
["Error determining value html for #{inspect(type)}: ", Exception.format(:error, exception)]
end)
end
def copy_text(type, value) do
decoded_type = ABI.FunctionSelector.decode_type(type)
do_copy_text(decoded_type, value)
rescue
exception ->
Logger.warn(fn ->
["Error determining copy text for #{inspect(type)}: ", Exception.format(:error, exception)]
end)
end
def do_copy_text({:bytes, _type}, value) do
hex(value)
end
def do_copy_text({:array, type, _}, value) do
do_copy_text({:array, type}, value)
end
def do_copy_text({:array, type}, value) do
values =
value
|> Enum.map(&do_copy_text(type, &1))
|> Enum.intersperse(", ")
~E|[<%= values %>]|
end
def do_copy_text(_, {:dynamic, value}) do
hex(value)
end
def do_copy_text(type, value) when type in [:bytes, :address] do
hex(value)
end
def do_copy_text(_type, value) do
to_string(value)
end
defp do_value_html(type, value, depth \\ 0)
defp do_value_html({:bytes, _}, value, depth) do
do_value_html(:bytes, value, depth)
end
defp do_value_html({:array, type, _}, value, depth) do
do_value_html({:array, type}, value, depth)
end
defp do_value_html({:array, type}, value, depth) do
values =
Enum.map(value, fn inner_value ->
do_value_html(type, inner_value, depth + 1)
end)
spacing = String.duplicate(" ", depth * 2)
delimited = Enum.intersperse(values, ",\n")
~E|<%= spacing %>[<%= "\n" %><%= delimited %><%= "\n" %><%= spacing %>]|
end
defp do_value_html(type, value, depth) do
spacing = String.duplicate(" ", depth * 2)
~E|<%= spacing %><%=base_value_html(type, value)%>|
[spacing, base_value_html(type, value)]
end
def base_value_html(_, {:dynamic, value}) do
hex(value)
end
def base_value_html(:address, value) do
address = hex(value)
~E|<a href="<%= address_path(BlockScoutWeb.Endpoint, :show, address) %>" target="_blank"><%= address %></a>|
end
def base_value_html(:bytes, value) do
hex(value)
end
def base_value_html(_, value), do: Phoenix.HTML.html_escape(value)
defp hex(value), do: "0x" <> Base.encode16(value, case: :lower)
end

@ -1,4 +1,10 @@
defmodule BlockScoutWeb.TransactionLogView do defmodule BlockScoutWeb.TransactionLogView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
@dialyzer :no_match @dialyzer :no_match
alias Explorer.Chain.Log
def decode(log, transaction) do
Log.decode(log, transaction)
end
end end

@ -0,0 +1,91 @@
defmodule BlockScoutWeb.ABIEncodedValueViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.ABIEncodedValueView
defp value_html(type, value) do
type
|> ABIEncodedValueView.value_html(value)
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
end
defp copy_text(type, value) do
type
|> ABIEncodedValueView.copy_text(value)
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
end
describe "value_html/2" do
test "it formats addresses as links" do
address = "0x0000000000000000000000000000000000000000"
address_bytes = address |> String.trim_leading("0x") |> Base.decode16!()
expected = ~s(<a href=\"/address/#{address}\" target=\"_blank\">#{address}</a>)
assert value_html("address", address_bytes) == expected
end
test "it formats lists with newlines and spaces" do
expected = String.trim("""
[
1,
2,
3,
4
]
""")
assert value_html("uint[]", [1, 2, 3, 4]) == expected
end
test "it formats nested lists with nested depth" do
expected = String.trim("""
[
[
1,
2
],
[
3,
4
]
]
""")
assert value_html("uint[][]", [[1, 2], [3, 4]]) == expected
end
test "it formats lists of addresses as a list of links" do
address = "0x0000000000000000000000000000000000000000"
address_link = ~s(<a href=\"/address/#{address}\" target=\"_blank\">#{address}</a>)
expected = String.trim("""
[
#{address_link},
#{address_link},
#{address_link},
#{address_link}
]
""")
address_bytes = "0x0000000000000000000000000000000000000000" |> String.trim_leading("0x") |> Base.decode16!()
assert value_html("address[]", [address_bytes, address_bytes, address_bytes, address_bytes]) == expected
end
end
describe "copy_text/2" do
test "it skips link formatting of addresses" do
address = "0x0000000000000000000000000000000000000000"
address_bytes = address |> String.trim_leading("0x") |> Base.decode16!()
assert copy_text("address", address_bytes) == address
end
test "it skips the formatting when copying lists" do
assert copy_text("uint[]", [1, 2, 3, 4]) == "[1, 2, 3, 4]"
end
end
end

@ -79,7 +79,7 @@ defmodule EthereumJsonrpc.MixProject do
# Convert unix timestamps in JSONRPC to DateTimes # Convert unix timestamps in JSONRPC to DateTimes
{:timex, "~> 3.4"}, {:timex, "~> 3.4"},
# Encode/decode function names and arguments # Encode/decode function names and arguments
{:ex_abi, "~> 0.1.17"}, {:ex_abi, "~> 0.1.18"},
# `:verify_fun` for `Socket.Web.connect` # `:verify_fun` for `Socket.Web.connect`
{:ssl_verify_fun, "~> 1.1"}, {:ssl_verify_fun, "~> 1.1"},
# `EthereumJSONRPC.WebSocket` # `EthereumJSONRPC.WebSocket`

@ -3,6 +3,8 @@ defmodule Explorer.Chain.Log do
use Explorer.Schema use Explorer.Schema
require Logger
alias Explorer.Chain.{Address, Data, Hash, Transaction} alias Explorer.Chain.{Address, Data, Hash, Transaction}
@required_attrs ~w(address_hash data index transaction_hash)a @required_attrs ~w(address_hash data index transaction_hash)a
@ -98,4 +100,61 @@ defmodule Explorer.Chain.Log do
|> cast(attrs, @optional_attrs) |> cast(attrs, @optional_attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
end end
@doc """
Decode transaction log data.
"""
def decode(log, transaction) do
abi = transaction.to_address.smart_contract.abi
with {:ok, selector, mapping} <- find_and_decode(abi, log),
identifier <- Base.encode16(selector.method_id, case: :lower),
text <- function_call(selector.function, mapping),
do: {:ok, identifier, text, mapping}
end
defp find_and_decode(abi, log) do
with {selector, mapping} <-
abi
|> ABI.parse_specification(include_events?: true)
|> ABI.Event.find_and_decode(
decode16!(log.first_topic),
decode16!(log.second_topic),
decode16!(log.third_topic),
decode16!(log.fourth_topic),
log.data.bytes
) do
{:ok, selector, mapping}
end
rescue
_ ->
Logger.warn(fn -> ["Could not decode input data for log: ", Hash.to_iodata(log.hash)] end)
{:error, :could_not_decode}
end
defp function_call(name, mapping) do
text =
mapping
|> Stream.map(fn {name, type, indexed?, _value} ->
indexed_keyword =
if indexed? do
["indexed "]
else
[]
end
[type, " ", indexed_keyword, name]
end)
|> Enum.intersperse(", ")
IO.iodata_to_binary([name, "(", text, ")"])
end
def decode16!(nil), do: nil
def decode16!(value) do
value
|> String.trim_leading("0x")
|> Base.decode16!(case: :lower)
end
end end

@ -28,7 +28,7 @@
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.11", "4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "ecto": {:hex, :ecto, "2.2.11", "4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
"ex_abi": {:hex, :ex_abi, "0.1.17", "11822f88b3ed70773c64858a49321b3c51ed913128a3f9fc7a05fa7ceb19d8fa", [:mix], [{:exth_crypto, "~> 0.1.4", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "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_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": {: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_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]}]}, "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]}]},

Loading…
Cancel
Save