Merge pull request #279 from poanetwork/ln-read-smart-contract-back-end

(Feature) Smart contract function reading
pull/426/head
Amanda 6 years ago committed by GitHub
commit 6620dca137
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      README.md
  2. 52
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  3. 110
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex
  4. 2
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex
  5. 4
      apps/ethereum_jsonrpc/mix.exs
  6. 207
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs
  7. 0
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
  8. 9
      apps/explorer/config/dev.exs
  9. 12
      apps/explorer/config/dev/geth.exs
  10. 16
      apps/explorer/config/dev/parity.exs
  11. 6
      apps/explorer/config/prod.exs
  12. 12
      apps/explorer/config/prod/geth.exs
  13. 16
      apps/explorer/config/prod/parity.exs
  14. 14
      apps/explorer/config/test.exs
  15. 8
      apps/explorer/config/test/geth.exs
  16. 9
      apps/explorer/config/test/parity.exs
  17. 11
      apps/explorer/lib/explorer/chain.ex
  18. 220
      apps/explorer/lib/explorer/smart_contract/reader.ex
  19. 4
      apps/explorer/mix.exs
  20. 8
      apps/explorer/test/explorer/chain_test.exs
  21. 199
      apps/explorer/test/explorer/smart_contract/reader_test.exs
  22. 34
      apps/explorer/test/support/factory.ex
  23. 2
      apps/explorer/test/test_helper.exs
  24. 1
      apps/explorer_web/assets/css/app.scss
  25. 1
      apps/explorer_web/assets/js/app.js
  26. 31
      apps/explorer_web/assets/js/lib/smart_contract/read_function.js
  27. 2
      apps/explorer_web/assets/package-lock.json
  28. 60
      apps/explorer_web/lib/explorer_web/controllers/address_read_contract_controller.ex
  29. 7
      apps/explorer_web/lib/explorer_web/router.ex
  30. 8
      apps/explorer_web/lib/explorer_web/templates/address_contract/index.html.eex
  31. 8
      apps/explorer_web/lib/explorer_web/templates/address_internal_transaction/index.html.eex
  32. 7
      apps/explorer_web/lib/explorer_web/templates/address_read_contract/_function_response.html.eex
  33. 99
      apps/explorer_web/lib/explorer_web/templates/address_read_contract/index.html.eex
  34. 8
      apps/explorer_web/lib/explorer_web/templates/address_transaction/index.html.eex
  35. 2
      apps/explorer_web/lib/explorer_web/views/address_contract_view.ex
  36. 3
      apps/explorer_web/lib/explorer_web/views/address_internal_transaction_view.ex
  37. 9
      apps/explorer_web/lib/explorer_web/views/address_read_contract_view.ex
  38. 3
      apps/explorer_web/lib/explorer_web/views/address_transaction_view.ex
  39. 6
      apps/explorer_web/lib/explorer_web/views/address_view.ex
  40. 3
      apps/explorer_web/package-lock.json
  41. 75
      apps/explorer_web/priv/gettext/default.pot
  42. 77
      apps/explorer_web/priv/gettext/en/LC_MESSAGES/default.po
  43. 24
      apps/explorer_web/test/explorer_web/controllers/address_read_contract_controller_test.exs
  44. 19
      apps/explorer_web/test/explorer_web/views/address_read_contract_view_test.exs
  45. 67
      apps/explorer_web/test/explorer_web/views/address_view_test.exs
  46. 6
      mix.lock

@ -41,6 +41,10 @@ The [development stack page](https://github.com/poanetwork/poa-explorer/wiki/Dev
* [Elixir 1.6.5](https://elixir-lang.org/) * [Elixir 1.6.5](https://elixir-lang.org/)
* [Postgres 10.3](https://www.postgresql.org/) * [Postgres 10.3](https://www.postgresql.org/)
* [Node.js 10.5.0](https://nodejs.org/en/) * [Node.js 10.5.0](https://nodejs.org/en/)
* [Automake](https://www.gnu.org/software/automake/)
* For Mac OSX users: `brew install automake`
* [Libtool](https://www.gnu.org/software/libtool/)
* For Mac OSX users: `brew install libtool`
* GitHub for code storage * GitHub for code storage
### Setup Instructions ### Setup Instructions
@ -60,6 +64,7 @@ The [development stack page](https://github.com/poanetwork/poa-explorer/wiki/Dev
5. Install Node.js dependencies. 5. Install Node.js dependencies.
`cd apps/explorer_web/assets && npm install; cd -` `cd apps/explorer_web/assets && npm install; cd -`
`cd apps/explorer && npm install; cd -` `cd apps/explorer && npm install; cd -`
6. Start Phoenix Server. 6. Start Phoenix Server.

@ -83,6 +83,52 @@ defmodule EthereumJSONRPC do
""" """
@type timestamp :: String.t() @type timestamp :: String.t()
@typedoc """
JSONRPC request id can be a `String.t` or Integer
"""
@type request_id :: String.t() | non_neg_integer()
@doc """
Execute smart contract functions.
Receives a list of smart contract functions to execute. Each function is
represented by a map. The contract_address key is the address of the smart
contract being queried, the data key indicates which function should be
executed, as well as what are their arguments, and the id key is the id that
is going to be sent with the JSON-RPC call.
## Examples
Execute the "sum" function that receives two arguments (20 and 22) and returns their sum (42):
iex> EthereumJSONRPC.execute_contract_functions([%{
...> contract_address: "0x7e50612682b8ee2a8bb94774d50d6c2955726526",
...> data: "0xcad0899b00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000016",
...> id: "sum"
...> }])
{:ok,
[
%{
"id" => "sum",
"jsonrpc" => "2.0",
"result" => "0x000000000000000000000000000000000000000000000000000000000000002a"
}
]}
"""
@spec execute_contract_functions(
[%{contract_address: String.t(), data: String.t(), id: String.t()}],
json_rpc_named_arguments
) :: {:ok, []}
def execute_contract_functions(functions, json_rpc_named_arguments) do
functions
|> Enum.map(&build_eth_call_payload/1)
|> json_rpc(json_rpc_named_arguments)
end
defp build_eth_call_payload(%{contract_address: address, data: data, id: id}) do
params = [%{to: address, data: data}]
request(%{id: id, method: "eth_call", params: params})
end
@doc """ @doc """
Fetches balance for each address `hash` at the `block_number` Fetches balance for each address `hash` at the `block_number`
""" """
@ -240,9 +286,9 @@ defmodule EthereumJSONRPC do
@doc """ @doc """
A request payload for a JSONRPC. A request payload for a JSONRPC.
""" """
@spec request(%{id: non_neg_integer(), method: String.t(), params: list()}) :: Transport.request() @spec request(%{id: request_id, method: String.t(), params: list()}) :: Transport.request()
def request(%{id: id, method: method, params: params} = map) def request(%{method: method, params: params} = map)
when is_integer(id) and is_binary(method) and is_list(params) do when is_binary(method) and is_list(params) do
Map.put(map, :jsonrpc, "2.0") Map.put(map, :jsonrpc, "2.0")
end end

@ -0,0 +1,110 @@
defmodule EthereumJSONRPC.Encoder do
@moduledoc """
Deals with encoding and decoding data to be sent to, or that is
received from, the blockchain.
"""
alias ABI.TypeDecoder
@doc """
Given an ABI and a set of functions, returns the data the blockchain expects.
"""
@spec encode_abi([map()], %{String.t() => [any()]}) :: map()
def encode_abi(abi, functions) do
abi
|> ABI.parse_specification()
|> get_selectors(functions)
|> Enum.map(&encode_function_call/1)
|> Map.new()
end
@doc """
Given a list of function selectors from the ABI lib, and a list of functions names with their arguments, returns a list of selectors with their functions.
"""
@spec get_selectors([%ABI.FunctionSelector{}], %{String.t() => [term()]}) :: [{%ABI.FunctionSelector{}, [term()]}]
def get_selectors(abi, functions) do
Enum.map(functions, fn {function_name, args} ->
{get_selector_from_name(abi, function_name), args}
end)
end
@doc """
Given a list of function selectors from the ABI lib, and a function name, get the selector for that function.
"""
@spec get_selector_from_name([%ABI.FunctionSelector{}], String.t()) :: %ABI.FunctionSelector{}
def get_selector_from_name(abi, function_name) do
Enum.find(abi, fn selector -> function_name == selector.function end)
end
@doc """
Given a function selector and a list of arguments, returns their econded versions.
This is what is expected on the Json RPC data parameter.
"""
@spec encode_function_call({%ABI.FunctionSelector{}, [term()]}) :: {String.t(), String.t()}
def encode_function_call({function_selector, args}) do
encoded_args =
function_selector
|> ABI.encode(parse_args(args))
|> Base.encode16(case: :lower)
{function_selector.function, "0x" <> encoded_args}
end
defp parse_args(args) do
args
|> Enum.map(fn
<<"0x", hexadecimal_digits::binary>> ->
Base.decode16!(hexadecimal_digits, case: :mixed)
item ->
item
end)
end
@doc """
Given a result set from the blockchain, and the functions selectors, returns the results decoded.
This functions assumes the result["id"] is the name of the function the result is for.
"""
@spec decode_abi_results({any(), [map()]}, [map()], %{String.t() => [any()]}) :: map()
def decode_abi_results({:ok, results}, abi, functions) do
selectors =
abi
|> ABI.parse_specification()
|> get_selectors(functions)
|> Enum.map(fn {selector, _args} -> selector end)
results
|> Stream.map(&join_result_and_selector(&1, selectors))
|> Stream.map(&decode_result/1)
|> Map.new()
end
defp join_result_and_selector(result, selectors) do
{result, Enum.find(selectors, &(&1.function == result[:id]))}
end
@doc """
Given a result from the blockchain, and the function selector, returns the result decoded.
"""
@spec decode_result({map(), %ABI.FunctionSelector{}}) :: {String.t(), [String.t()]}
def decode_result({%{error: %{code: code, message: message}, id: id}, _selector}) do
{id, ["#{code} => #{message}"]}
end
def decode_result({%{id: id, result: result}, function_selector}) do
types_list = format_list_types(function_selector.returns)
decoded_result =
result
|> String.slice(2..-1)
|> Base.decode16!(case: :lower)
|> TypeDecoder.decode_raw(types_list)
{id, decoded_result}
end
defp format_list_types(:string), do: [{:array, :string, 1}]
defp format_list_types(return_types), do: List.wrap(return_types)
end

@ -124,7 +124,7 @@ defmodule EthereumJSONRPC.HTTP do
# restrict response to only those fields supported by the JSON-RPC 2.0 standard, which means that level of keys is # restrict response to only those fields supported by the JSON-RPC 2.0 standard, which means that level of keys is
# validated, so we can indicate that with switch to atom keys. # validated, so we can indicate that with switch to atom keys.
defp standardize_response(%{"jsonrpc" => "2.0" = jsonrpc, "id" => id} = unstandardized) when is_integer(id) do defp standardize_response(%{"jsonrpc" => "2.0" = jsonrpc, "id" => id} = unstandardized) do
standardized = %{jsonrpc: jsonrpc, id: id} standardized = %{jsonrpc: jsonrpc, id: id}
case unstandardized do case unstandardized do

@ -72,7 +72,9 @@ defmodule EthereumJsonrpc.MixProject do
# Mocking `EthereumJSONRPC.Transport` and `EthereumJSONRPC.HTTP` so we avoid hitting real chains for local testing # Mocking `EthereumJSONRPC.Transport` and `EthereumJSONRPC.HTTP` so we avoid hitting real chains for local testing
{:mox, "~> 0.3.2", only: [:test]}, {:mox, "~> 0.3.2", only: [:test]},
# Convert unix timestamps in JSONRPC to DateTimes # Convert unix timestamps in JSONRPC to DateTimes
{:timex, "~> 3.1.24"} {:timex, "~> 3.1.24"},
# Encode/decode function names and arguments
{:ex_abi, "~> 0.1.13"}
] ]
end end
end end

@ -0,0 +1,207 @@
defmodule EthereumJSONRPC.EncoderTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Encoder
alias EthereumJSONRPC.Encoder
describe "encode_function_call/2" do
test "generates the correct encoding with no arguments" do
function_selector = %ABI.FunctionSelector{
function: "get",
returns: {:uint, 256},
types: []
}
assert Encoder.encode_function_call({function_selector, []}) == {"get", "0x6d4ce63c"}
end
test "generates the correct encoding with arguments" do
function_selector = %ABI.FunctionSelector{
function: "get",
returns: {:uint, 256},
types: [{:uint, 256}]
}
assert Encoder.encode_function_call({function_selector, [10]}) ==
{"get", "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a"}
end
test "generates the correct encoding with addresses arguments" do
function_selector = %ABI.FunctionSelector{
function: "tokens",
returns: {:uint, 256},
types: [:address, :address]
}
args = ["0xdab1c67232f92b7707f49c08047b96a4db7a9fc6", "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"]
assert Encoder.encode_function_call({function_selector, args}) ==
{"tokens",
"0x508493bc000000000000000000000000dab1c67232f92b7707f49c08047b96a4db7a9fc60000000000000000000000006937cb25eb54bc013b9c13c47ab38eb63edd1493"}
end
end
describe "get_selectors/2" do
test "return the selectors of the desired functions with their arguments" do
abi = [
%ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
},
%ABI.FunctionSelector{
function: "fn2",
returns: {:uint, 256},
types: [uint: 256]
}
]
fn1 = %ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
}
assert Encoder.get_selectors(abi, %{"fn1" => [10]}) == [{fn1, [10]}]
end
end
describe "get_selector_from_name/2" do
test "return the selector of the desired function" do
abi = [
%ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
},
%ABI.FunctionSelector{
function: "fn2",
returns: {:uint, 256},
types: [uint: 256]
}
]
fn1 = %ABI.FunctionSelector{
function: "fn1",
returns: {:uint, 256},
types: [uint: 256]
}
assert Encoder.get_selector_from_name(abi, "fn1") == fn1
end
end
describe "decode_abi_results/3" do
test "separates the selectors and map the results" do
result =
{:ok,
[
%{
id: "get1",
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000000000000000000000000002a"
},
%{
id: "get2",
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000000000000000000000000002a"
},
%{
id: "get3",
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000020"
}
]}
abi = [
%{
"constant" => false,
"inputs" => [],
"name" => "get1",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get2",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get3",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
functions = %{
"get1" => [],
"get2" => [],
"get3" => []
}
assert Encoder.decode_abi_results(result, abi, functions) == %{
"get1" => [42],
"get2" => [42],
"get3" => [32]
}
end
end
describe "decode_result/1" do
test "correclty decodes the blockchain result" do
result = %{
id: "sum",
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000000000000000000000000002a"
}
selector = %ABI.FunctionSelector{
function: "get",
returns: {:uint, 256},
types: [{:uint, 256}]
}
assert Encoder.decode_result({result, selector}) == {"sum", [42]}
end
test "correclty handles the blockchain error response" do
result = %{
error: %{
code: -32602,
message: "Invalid params: Invalid hex: Invalid character 'x' at position 134."
},
id: "sum",
jsonrpc: "2.0"
}
selector = %ABI.FunctionSelector{
function: "get",
returns: {:uint, 256},
types: [{:uint, 256}]
}
assert Encoder.decode_result({result, selector}) ==
{"sum", ["-32602 => Invalid params: Invalid hex: Invalid character 'x' at position 134."]}
end
test "correclty decodes string types" do
result =
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000441494f4e00000000000000000000000000000000000000000000000000000000"
selector = %ABI.FunctionSelector{function: "name", types: [], returns: :string}
assert Encoder.decode_result({%{id: "storedName", result: result}, selector}) == {"storedName", [["AION"]]}
end
end
end

@ -7,6 +7,13 @@ config :explorer, Explorer.Repo,
hostname: "localhost", hostname: "localhost",
loggers: [], loggers: [],
pool_size: 20, pool_size: 20,
pool_timeout: 60_000 pool_timeout: 60_000,
timeout: 80_000
import_config "dev.secret.exs" import_config "dev.secret.exs"
variant = System.get_env("ETHEREUM_JSONRPC_VARIANT") || "parity"
# Import variant specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "dev/#{variant}.exs"

@ -0,0 +1,12 @@
use Mix.Config
config :explorer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.HTTP,
transport_options: [
http: EthereumJSONRPC.HTTP.HTTPoison,
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY",
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
],
variant: EthereumJSONRPC.Geth
]

@ -0,0 +1,16 @@
use Mix.Config
config :explorer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.HTTP,
transport_options: [
http: EthereumJSONRPC.HTTP.HTTPoison,
url: "https://sokol.poa.network",
method_to_url: [
eth_getBalance: "https://sokol-trace.poa.network",
trace_replayTransaction: "https://sokol-trace.poa.network"
],
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
],
variant: EthereumJSONRPC.Parity
]

@ -8,3 +8,9 @@ config :explorer, Explorer.Repo,
ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true"), ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true"),
prepare: :unnamed, prepare: :unnamed,
timeout: 60_000 timeout: 60_000
variant = System.get_env("ETHEREUM_JSONRPC_VARIANT") || "parity"
# Import variant specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "prod/#{variant}.exs"

@ -0,0 +1,12 @@
use Mix.Config
config :explorer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.HTTP,
transport_options: [
http: EthereumJSONRPC.HTTP.HTTPoison,
url: "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY",
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
],
variant: EthereumJSONRPC.Geth
]

@ -0,0 +1,16 @@
use Mix.Config
config :explorer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.HTTP,
transport_options: [
http: EthereumJSONRPC.HTTP.HTTPoison,
url: "https://sokol.poa.network",
method_to_url: [
eth_getBalance: "https://sokol-trace.poa.network",
trace_replayTransaction: "https://sokol-trace.poa.network"
],
http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]]
],
variant: EthereumJSONRPC.Parity
]

@ -18,3 +18,17 @@ config :explorer, Explorer.ExchangeRates, enabled: false
config :explorer, Explorer.Indexer.Supervisor, enabled: false config :explorer, Explorer.Indexer.Supervisor, enabled: false
config :explorer, Explorer.Market.History.Cataloger, enabled: false config :explorer, Explorer.Market.History.Cataloger, enabled: false
variant =
if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do
"parity"
else
System.get_env("ETHEREUM_JSONRPC_VARIANT")
|> String.split(".")
|> List.last()
|> String.downcase()
end
# Import variant specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "test/#{variant}.exs"

@ -0,0 +1,8 @@
use Mix.Config
config :explorer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
variant: EthereumJSONRPC.Geth
]

@ -0,0 +1,9 @@
use Mix.Config
config :explorer,
transport: EthereumJSONRPC.HTTP,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
variant: EthereumJSONRPC.Parity
]

@ -2207,6 +2207,17 @@ defmodule Explorer.Chain do
|> Repo.insert() |> Repo.insert()
end end
@spec address_hash_to_smart_contract(%Explorer.Chain.Hash{}) :: %Explorer.Chain.SmartContract{}
def address_hash_to_smart_contract(%Explorer.Chain.Hash{} = address_hash) do
query =
from(
smart_contract in SmartContract,
where: smart_contract.address_hash == ^address_hash
)
Repo.one(query)
end
@spec changes_list(params :: [map], [{:for, module} | {:with, atom}]) :: {:ok, [map]} | {:error, [Changeset.t()]} @spec changes_list(params :: [map], [{:for, module} | {:with, atom}]) :: {:ok, [map]} | {:error, [Changeset.t()]}
defp changes_list(params, options) when is_list(options) do defp changes_list(params, options) when is_list(options) do
ecto_schema_module = Keyword.fetch!(options, :for) ecto_schema_module = Keyword.fetch!(options, :for)

@ -0,0 +1,220 @@
defmodule Explorer.SmartContract.Reader do
@moduledoc """
Reads Smart Contract functions from the blockchain.
For information on smart contract's Application Binary Interface (ABI), visit the
[wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI).
"""
alias Explorer.Chain
alias EthereumJSONRPC.Encoder
alias Explorer.Chain.Hash
@doc """
Queries the contract functions on the blockchain and returns the call results.
## Examples
Note that for this example to work the database must be up to date with the
information available in the blockchain.
```
$ Explorer.SmartContract.Reader.query_contract(
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
},
%{"sum" => [20, 22]}
)
# => %{"sum" => [42]}
$ Explorer.SmartContract.Reader.query_contract(
%Explorer.Chain.Hash{
byte_count: 20,
bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
},
%{"sum" => [1, "abc"]}
)
# => %{"sum" => ["Data overflow encoding int, data `abc` cannot fit in 256 bits"]}
```
"""
@spec query_contract(%Explorer.Chain.Hash{}, %{String.t() => [term()]}) :: map()
def query_contract(address_hash, functions) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
contract_address = Hash.to_string(address_hash)
abi =
address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi)
try do
blockchain_result =
abi
|> Encoder.encode_abi(functions)
|> Enum.map(&setup_call_payload(&1, contract_address))
|> EthereumJSONRPC.execute_contract_functions(json_rpc_named_arguments)
Encoder.decode_abi_results(blockchain_result, abi, functions)
rescue
error ->
format_error(functions, error.message)
end
end
defp format_error(functions, message) do
functions
|> Enum.map(fn {function_name, _args} ->
%{function_name => [message]}
end)
|> List.first()
end
@doc """
Given the encoded data that references a function and its arguments in the blockchain, as well as the contract address, returns what EthereumJSONRPC.execute_contract_functions expects.
"""
@spec setup_call_payload({%ABI.FunctionSelector{}, [term()]}, String.t()) :: map()
def setup_call_payload({function_name, data}, contract_address) do
%{
contract_address: contract_address,
data: data,
id: function_name
}
end
@doc """
List all the smart contract functions with its current value from the
blockchain, following the ABI order.
Functions that require arguments can be queryable but won't list the current
value at this moment.
## Examples
$ Explorer.SmartContract.Reader.read_only_functions("0x798465571ae21a184a272f044f991ad1d5f87a3f")
=> [
%{
"constant" => true,
"inputs" => [],
"name" => "get",
"outputs" => [%{"name" => "", "type" => "uint256", "value" => 0}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [%{"name" => "x", "type" => "uint256"}],
"name" => "with_arguments",
"outputs" => [%{"name" => "", "type" => "bool", "value" => ""}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
"""
@spec read_only_functions(%Explorer.Chain.Hash{}) :: [%{}]
def read_only_functions(contract_address_hash) do
contract_address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi, [])
|> Enum.filter(& &1["constant"])
|> fetch_current_value_from_blockchain(contract_address_hash, [])
|> Enum.reverse()
end
def fetch_current_value_from_blockchain([%{"inputs" => []} = function | tail], contract_address_hash, acc) do
values =
fetch_from_blockchain(contract_address_hash, %{
name: function["name"],
args: function["inputs"],
outputs: function["outputs"]
})
formatted = Map.replace!(function, "outputs", values)
fetch_current_value_from_blockchain(tail, contract_address_hash, [formatted | acc])
end
def fetch_current_value_from_blockchain([function | tail], contract_address_hash, acc) do
values = link_outputs_and_values(%{}, Map.get(function, "outputs", []), function["name"])
formatted = Map.replace!(function, "outputs", values)
fetch_current_value_from_blockchain(tail, contract_address_hash, [formatted | acc])
end
def fetch_current_value_from_blockchain([], _contract_address_hash, acc), do: acc
@doc """
Fetches the blockchain value of a function that requires arguments.
"""
@spec query_function(String.t(), %{name: String.t(), args: nil}) :: [%{}]
def query_function(contract_address_hash, %{name: name, args: nil}) do
query_function(contract_address_hash, %{name: name, args: []})
end
@spec query_function(%Explorer.Chain.Hash{}, %{name: String.t(), args: [term()]}) :: [%{}]
def query_function(contract_address_hash, %{name: name, args: args}) do
function =
contract_address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi, [])
|> Enum.filter(fn function -> function["name"] == name end)
|> List.first()
fetch_from_blockchain(contract_address_hash, %{name: name, args: args, outputs: function["outputs"]})
end
defp fetch_from_blockchain(contract_address_hash, %{name: name, args: args, outputs: outputs}) do
contract_address_hash
|> query_contract(%{name => normalize_args(args)})
|> link_outputs_and_values(outputs, name)
end
@doc """
The type of the arguments passed to the blockchain interferes in the output,
but we always get strings from the front, so it is necessary to normalize it.
"""
def normalize_args(args) do
Enum.map(args, &parse_item/1)
end
defp parse_item("true"), do: true
defp parse_item("false"), do: false
defp parse_item(item) do
response = Integer.parse(item)
case response do
{integer, remainder_of_binary} when remainder_of_binary == "" -> integer
_ -> item
end
end
def link_outputs_and_values(blockchain_values, outputs, function_name) do
values = Map.get(blockchain_values, function_name, [""])
for output <- outputs, value <- values do
new_value(output, value)
end
end
defp new_value(%{"type" => "address"} = output, value) do
Map.put_new(output, "value", bytes_to_string(value))
end
defp new_value(%{"type" => "bytes" <> _number} = output, value) do
Map.put_new(output, "value", bytes_to_string(value))
end
defp new_value(output, value) do
Map.put_new(output, "value", value)
end
@spec bytes_to_string(<<_::_*8>>) :: String.t()
defp bytes_to_string(value) do
Hash.to_string(%Hash{byte_count: byte_size(value), bytes: value})
end
end

@ -84,7 +84,9 @@ defmodule Explorer.Mixfile do
{:postgrex, ">= 0.0.0"}, {:postgrex, ">= 0.0.0"},
{:sobelow, ">= 0.7.0", only: [:dev, :test], runtime: false}, {:sobelow, ">= 0.7.0", only: [:dev, :test], runtime: false},
{:timex, "~> 3.1.24"}, {:timex, "~> 3.1.24"},
{:timex_ecto, "~> 3.2.1"} {:timex_ecto, "~> 3.2.1"},
# JSONRPC access to query smart contracts
{:ethereum_jsonrpc, in_umbrella: true}
] ]
end end

@ -1076,4 +1076,12 @@ defmodule Explorer.ChainTest do
test "circulating_supply/0" do test "circulating_supply/0" do
assert Chain.circulating_supply() == ProofOfAuthority.circulating() assert Chain.circulating_supply() == ProofOfAuthority.circulating()
end end
describe "address_hash_to_smart_contract/1" do
test "fetches a smart contract" do
smart_contract = insert(:smart_contract)
assert ^smart_contract = Chain.address_hash_to_smart_contract(smart_contract.address_hash)
end
end
end end

@ -0,0 +1,199 @@
defmodule Explorer.SmartContract.ReaderTest do
use EthereumJSONRPC.Case
use Explorer.DataCase
doctest Explorer.SmartContract.Reader
alias Explorer.SmartContract.Reader
import Mox
setup :verify_on_exit!
describe "query_contract/2" do
test "correctly returns the results of the smart contract functions" do
hash =
:smart_contract
|> insert()
|> Map.get(:address_hash)
blockchain_get_function_mock()
assert Reader.query_contract(hash, %{"get" => []}) == %{"get" => [0]}
end
test "won't raise error when there is a problem with the params to consult the blockchain" do
smart_contract =
insert(
:smart_contract,
abi: [
%{
"constant" => true,
"inputs" => [
%{"name" => "a", "type" => "int256"},
%{"name" => "b", "type" => "int256"},
%{"name" => "c", "type" => "int256"},
%{"name" => "d", "type" => "int256"}
],
"name" => "sum",
"outputs" => [%{"name" => "", "type" => "int256"}],
"payable" => false,
"stateMutability" => "pure",
"type" => "function"
}
]
)
wrong_args = %{"sum" => [1, 1, 1, "abc"]}
assert %{"sum" => ["Data overflow encoding int, data `abc` cannot fit in 256 bits"]} =
Reader.query_contract(smart_contract.address_hash, wrong_args)
end
end
describe "setup_call_payload/2" do
test "returns the expected payload" do
function_name = "get"
contract_address = "0x123789abc"
data = "0x6d4ce63c"
assert Reader.setup_call_payload(
{function_name, data},
contract_address
) == %{contract_address: "0x123789abc", data: "0x6d4ce63c", id: "get"}
end
end
describe "read_only_functions/1" do
test "fetches the smart contract read only functions with the blockchain value" do
smart_contract =
insert(
:smart_contract,
abi: [
%{
"constant" => true,
"inputs" => [],
"name" => "get",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [%{"name" => "x", "type" => "uint256"}],
"name" => "with_arguments",
"outputs" => [%{"name" => "", "type" => "bool"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
)
blockchain_get_function_mock()
response = Reader.read_only_functions(smart_contract.address_hash)
assert [
%{
"constant" => true,
"inputs" => [],
"name" => "get",
"outputs" => [%{"name" => "", "type" => "uint256", "value" => 0}],
"payable" => _,
"stateMutability" => _,
"type" => _
},
%{
"constant" => true,
"inputs" => [%{"name" => "x", "type" => "uint256"}],
"name" => "with_arguments",
"outputs" => [%{"name" => "", "type" => "bool", "value" => ""}],
"payable" => _,
"stateMutability" => _,
"type" => _
}
] = response
end
end
describe "query_function/2" do
test "given the arguments, fetches the function value from the blockchain" do
smart_contract = insert(:smart_contract)
blockchain_get_function_mock()
assert [
%{
"name" => "",
"type" => "uint256",
"value" => 0
}
] = Reader.query_function(smart_contract.address_hash, %{name: "get", args: []})
end
end
describe "normalize_args/1" do
test "converts argument when is a number" do
assert [0] = Reader.normalize_args(["0"])
assert ["0x798465571ae21a184a272f044f991ad1d5f87a3f"] =
Reader.normalize_args(["0x798465571ae21a184a272f044f991ad1d5f87a3f"])
end
test "converts argument when is a boolean" do
assert [true] = Reader.normalize_args(["true"])
assert [false] = Reader.normalize_args(["false"])
assert ["some string"] = Reader.normalize_args(["some string"])
end
end
describe "link_outputs_and_values/2" do
test "links the ABI outputs with the values retrieved from the blockchain" do
blockchain_values = %{
"getOwner" => [
<<105, 55, 203, 37, 235, 84, 188, 1, 59, 156, 19, 196, 122, 179, 142, 182, 62, 221, 20, 147>>
]
}
outputs = [%{"name" => "", "type" => "address"}]
function_name = "getOwner"
assert [%{"name" => "", "type" => "address", "value" => "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493"}] =
Reader.link_outputs_and_values(blockchain_values, outputs, function_name)
end
test "correctly shows returns of 'bytes' type" do
blockchain_values = %{
"get" => [
<<0, 10, 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>>
]
}
outputs = [%{"name" => "", "type" => "bytes32"}]
function_name = "get"
assert [
%{
"name" => "",
"type" => "bytes32",
"value" => "0x000a000000000000000000000000000000000000000000000000000000000000"
}
] = Reader.link_outputs_and_values(blockchain_values, outputs, function_name)
end
end
defp blockchain_get_function_mock() do
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [%{id: id, method: _, params: [%{data: _, to: _}]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000000"}]}
end
)
end
end

@ -15,7 +15,8 @@ defmodule Explorer.Factory do
Hash, Hash,
InternalTransaction, InternalTransaction,
Log, Log,
Transaction Transaction,
SmartContract
} }
alias Explorer.Market.MarketHistory alias Explorer.Market.MarketHistory
@ -277,6 +278,37 @@ defmodule Explorer.Factory do
data(:transaction_input) data(:transaction_input)
end end
def smart_contract_factory() do
%SmartContract{
address_hash: insert(:address).hash,
compiler_version: "0.4.24",
name: "SimpleStorage",
contract_source_code:
"pragma solidity ^0.4.24; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }",
optimization: false,
abi: [
%{
"constant" => false,
"inputs" => [%{"name" => "x", "type" => "uint256"}],
"name" => "set",
"outputs" => [],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
},
%{
"constant" => true,
"inputs" => [],
"name" => "get",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
}
end
defmacrop left + right do defmacrop left + right do
quote do quote do
fragment("? + ?", unquote(left), unquote(right)) fragment("? + ?", unquote(left), unquote(right))

@ -15,3 +15,5 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual)
Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source) Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source)
Mox.defmock(Explorer.Market.History.Source.TestSource, for: Explorer.Market.History.Source) Mox.defmock(Explorer.Market.History.Source.TestSource, for: Explorer.Market.History.Source)
Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport)

@ -32,6 +32,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "node_modules/bootstrap/scss/utilities/text"; @import "node_modules/bootstrap/scss/utilities/text";
@import "node_modules/bootstrap/scss/utilities/background"; @import "node_modules/bootstrap/scss/utilities/background";
@import "node_modules/bootstrap/scss/utilities/position"; @import "node_modules/bootstrap/scss/utilities/position";
@import "node_modules/bootstrap/scss/utilities/borders";
// Bootstrap Components // Bootstrap Components
@import "node_modules/bootstrap/scss/dropdown"; @import "node_modules/bootstrap/scss/dropdown";

@ -23,6 +23,7 @@ import './lib/from_now'
import './lib/market_history_chart' import './lib/market_history_chart'
import './lib/reload_button' import './lib/reload_button'
import './lib/tooltip' import './lib/tooltip'
import './lib/smart_contract/read_function'
import './pages/address' import './pages/address'
import './pages/transaction' import './pages/transaction'

@ -0,0 +1,31 @@
import $ from 'jquery'
const readFunction = (element) => {
const $element = $(element)
const $form = $element.find('[data-function-form]')
const $responseContainer = $element.find('[data-function-response]')
$form.on('submit', (event) => {
event.preventDefault()
const url = $form.data('url')
const $functionName = $form.find('input[name=function_name]')
const $functionInputs = $form.find('input[name=function_input]')
const args = $.map($functionInputs, element => {
return $(element).val()
})
const data = {
function_name: $functionName.val(),
args
}
$.get(url, data, response => $responseContainer.html(response))
})
}
$('[data-function]').each((_, element) => {
readFunction(element)
})

@ -11468,7 +11468,7 @@
"cross-spawn": { "cross-spawn": {
"version": "6.0.5", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=",
"dev": true, "dev": true,
"requires": { "requires": {
"nice-try": "^1.0.4", "nice-try": "^1.0.4",

@ -0,0 +1,60 @@
defmodule ExplorerWeb.AddressReadContractController do
use ExplorerWeb, :controller
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.Reader
import ExplorerWeb.AddressController, only: [transaction_count: 1]
def index(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash) do
read_only_functions = Reader.read_only_functions(address_hash)
render(
conn,
"index.html",
read_only_functions: read_only_functions,
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
transaction_count: transaction_count(address)
)
else
:error ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
def show(conn, params) do
with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(params["address_id"]),
outputs =
Reader.query_function(
address_hash,
%{name: params["function_name"], args: params["args"]}
) do
conn
|> put_status(200)
|> put_layout(false)
|> render(
"_function_response.html",
function_name: params["function_name"],
outputs: outputs
)
else
_ ->
not_found(conn)
end
end
defp ajax?(conn) do
case get_req_header(conn, "x-requested-with") do
[value] -> value in ["XMLHttpRequest", "xmlhttprequest"]
[] -> false
end
end
end

@ -85,6 +85,13 @@ defmodule ExplorerWeb.Router do
only: [:new, :create], only: [:new, :create],
as: :verify_contract as: :verify_contract
) )
resources(
"/read_contract",
AddressReadContractController,
only: [:index, :show],
as: :read_contract
)
end end
get("/search", ChainController, :search) get("/search", ChainController, :search)

@ -31,6 +31,14 @@
<% end %> <% end %>
<% end %> <% end %>
</li> </li>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]),
class: "nav-link")%>
</li>
<% end %>
</ul> </ul>
</div> </div>

@ -36,6 +36,14 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]),
class: "nav-link")%>
</li>
<% end %>
</ul> </ul>
<!-- MOBILE DROPDOWN NAV --> <!-- MOBILE DROPDOWN NAV -->

@ -0,0 +1,7 @@
<span>
[ <%= @function_name %> method Response ] <br />
<%= for item <- @outputs do %>
<i class="fa fa-angle-double-right"></i> <%= item["type"] %> : <%= item["value"] %>
<% end %>
</span>

@ -0,0 +1,99 @@
<section class="container-fluid">
<%= render ExplorerWeb.AddressView, "overview.html", assigns %>
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<%= link(
gettext("Transactions"),
class: "nav-link",
to: address_transaction_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"])
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Internal Transactions"),
class: "nav-link",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"])
) %>
</li>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]),
class: "nav-link") do %>
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
</li>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]),
class: "nav-link active")%>
</li>
</ul>
</div>
<div class="card-body">
<h2>
<i class="fas fa-book"></i>
<%= gettext("Read Contract Information") %>
</h2>
<%= for {function, counter} <- Enum.with_index(@read_only_functions, 1) do %>
<div class="d-flex py-2 flex-wrap border-bottom" data-function>
<div class="py-2 pr-2">
<%= counter %>.
<%= function["name"] %>
&#8594;
</div>
<%= if queryable?(function["inputs"]) do %>
<form class="form-inline" data-function-form data-url="<%= address_read_contract_path(@conn, :show, :en, @address, @address.hash) %>">
<input type="hidden" name="function_name" value='<%= function["name"] %>' />
<%= for input <- function["inputs"] do %>
<div class="form-group pr-2">
<input type="text" name="function_input" class="form-control form-control-sm mb-1" placeholder='<%= input["name"] %>(<%= input["type"] %>)' />
</div>
<% end %>
<input type="submit" value='<%= gettext("Query")%>' class="button button--secondary button--xsmall py-0" />
</form>
<% else %>
<span class="py-2">
<%= for output <- function["outputs"] do %>
<%= if address?(output["type"]) do %>
<%= link(
output["value"],
to: address_path(@conn, :show, @conn.assigns.locale, output["value"])
) %>
<% else %>
<%= output["value"] %>
<% end %>
<% end %>
</span>
<% end %>
<div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'>
<%= if (queryable?(function["inputs"])), do: raw "&#8627;" %>
<%= for output <- function["outputs"] do %>
<%= output["type"] %>
<% end %>
</div>
<div data-function-response></div>
</div>
<% end %>
</div>
</div>
</section>

@ -36,6 +36,14 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.assigns.locale, @conn.params["address_id"]),
class: "nav-link")%>
</li>
<% end %>
</ul> </ul>
<!-- MOBILE DROPDOWN NAV --> <!-- MOBILE DROPDOWN NAV -->

@ -1,7 +1,7 @@
defmodule ExplorerWeb.AddressContractView do defmodule ExplorerWeb.AddressContractView do
use ExplorerWeb, :view use ExplorerWeb, :view
import ExplorerWeb.AddressView, only: [smart_contract_verified?: 1] import ExplorerWeb.AddressView, only: [smart_contract_verified?: 1, smart_contract_with_read_only_functions?: 1]
def format_smart_contract_abi(abi), do: Poison.encode!(abi, pretty: true) def format_smart_contract_abi(abi), do: Poison.encode!(abi, pretty: true)

@ -1,7 +1,8 @@
defmodule ExplorerWeb.AddressInternalTransactionView do defmodule ExplorerWeb.AddressInternalTransactionView do
use ExplorerWeb, :view use ExplorerWeb, :view
import ExplorerWeb.AddressView, only: [contract?: 1, smart_contract_verified?: 1] import ExplorerWeb.AddressView,
only: [contract?: 1, smart_contract_verified?: 1, smart_contract_with_read_only_functions?: 1]
def format_current_filter(filter) do def format_current_filter(filter) do
case filter do case filter do

@ -0,0 +1,9 @@
defmodule ExplorerWeb.AddressReadContractView do
use ExplorerWeb, :view
import ExplorerWeb.AddressView, only: [smart_contract_verified?: 1]
def queryable?(inputs), do: Enum.any?(inputs)
def address?(type), do: type == "address"
end

@ -1,7 +1,8 @@
defmodule ExplorerWeb.AddressTransactionView do defmodule ExplorerWeb.AddressTransactionView do
use ExplorerWeb, :view use ExplorerWeb, :view
import ExplorerWeb.AddressView, only: [contract?: 1, smart_contract_verified?: 1] import ExplorerWeb.AddressView,
only: [contract?: 1, smart_contract_verified?: 1, smart_contract_with_read_only_functions?: 1]
def format_current_filter(filter) do def format_current_filter(filter) do
case filter do case filter do

@ -72,4 +72,10 @@ defmodule ExplorerWeb.AddressView do
end end
def trimmed_hash(_), do: "" def trimmed_hash(_), do: ""
def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(address.smart_contract.abi, & &1["constant"])
end
def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false
end end

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

@ -40,10 +40,11 @@ msgstr ""
#: lib/explorer_web/templates/address/overview.html.eex:21 #: lib/explorer_web/templates/address/overview.html.eex:21
#: lib/explorer_web/templates/address_contract/index.html.eex:10 #: lib/explorer_web/templates/address_contract/index.html.eex:10
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:13 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:13
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:47 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:55
#: lib/explorer_web/templates/address_read_contract/index.html.eex:10
#: lib/explorer_web/templates/address_transaction/index.html.eex:13 #: lib/explorer_web/templates/address_transaction/index.html.eex:13
#: lib/explorer_web/templates/address_transaction/index.html.eex:47 #: lib/explorer_web/templates/address_transaction/index.html.eex:55
#: lib/explorer_web/templates/address_transaction/index.html.eex:123 #: lib/explorer_web/templates/address_transaction/index.html.eex:131
#: lib/explorer_web/templates/block/index.html.eex:19 #: lib/explorer_web/templates/block/index.html.eex:19
#: lib/explorer_web/templates/block_transaction/index.html.eex:124 #: lib/explorer_web/templates/block_transaction/index.html.eex:124
#: lib/explorer_web/templates/chain/_transactions.html.eex:4 #: lib/explorer_web/templates/chain/_transactions.html.eex:4
@ -142,13 +143,13 @@ msgstr ""
msgid "Address" msgid "Address"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:98 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:106
#: lib/explorer_web/templates/address_transaction/index.html.eex:109 #: lib/explorer_web/templates/address_transaction/index.html.eex:117
#: lib/explorer_web/templates/block_transaction/index.html.eex:142 #: lib/explorer_web/templates/block_transaction/index.html.eex:142
#: lib/explorer_web/templates/transaction/overview.html.eex:79 #: lib/explorer_web/templates/transaction/overview.html.eex:79
#: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:31 #: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:31
#: lib/explorer_web/views/address_internal_transaction_view.ex:9 #: lib/explorer_web/views/address_internal_transaction_view.ex:10
#: lib/explorer_web/views/address_transaction_view.ex:9 #: lib/explorer_web/views/address_transaction_view.ex:10
msgid "From" msgid "From"
msgstr "" msgstr ""
@ -161,13 +162,13 @@ msgstr ""
msgid "Success" msgid "Success"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:86 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:94
#: lib/explorer_web/templates/address_transaction/index.html.eex:97 #: lib/explorer_web/templates/address_transaction/index.html.eex:105
#: lib/explorer_web/templates/block_transaction/index.html.eex:144 #: lib/explorer_web/templates/block_transaction/index.html.eex:144
#: lib/explorer_web/templates/transaction/overview.html.eex:91 #: lib/explorer_web/templates/transaction/overview.html.eex:91
#: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:32 #: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:32
#: lib/explorer_web/views/address_internal_transaction_view.ex:8 #: lib/explorer_web/views/address_internal_transaction_view.ex:9
#: lib/explorer_web/views/address_transaction_view.ex:8 #: lib/explorer_web/views/address_transaction_view.ex:9
msgid "To" msgid "To"
msgstr "" msgstr ""
@ -312,10 +313,11 @@ msgstr ""
#: lib/explorer_web/templates/address_contract/index.html.eex:17 #: lib/explorer_web/templates/address_contract/index.html.eex:17
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:20 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:20
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:52 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:60
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:111 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:119
#: lib/explorer_web/templates/address_read_contract/index.html.eex:17
#: lib/explorer_web/templates/address_transaction/index.html.eex:20 #: lib/explorer_web/templates/address_transaction/index.html.eex:20
#: lib/explorer_web/templates/address_transaction/index.html.eex:52 #: lib/explorer_web/templates/address_transaction/index.html.eex:60
#: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:11 #: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:11
#: lib/explorer_web/templates/transaction_log/index.html.eex:11 #: lib/explorer_web/templates/transaction_log/index.html.eex:11
msgid "Internal Transactions" msgid "Internal Transactions"
@ -353,10 +355,10 @@ msgstr ""
msgid "Wei" msgid "Wei"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:80 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:88
#: lib/explorer_web/templates/address_transaction/index.html.eex:91 #: lib/explorer_web/templates/address_transaction/index.html.eex:99
#: lib/explorer_web/views/address_internal_transaction_view.ex:10 #: lib/explorer_web/views/address_internal_transaction_view.ex:11
#: lib/explorer_web/views/address_transaction_view.ex:10 #: lib/explorer_web/views/address_transaction_view.ex:11
msgid "All" msgid "All"
msgstr "" msgstr ""
@ -473,8 +475,8 @@ msgstr ""
msgid "There are no Transactions" msgid "There are no Transactions"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:125 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:133
#: lib/explorer_web/templates/address_transaction/index.html.eex:138 #: lib/explorer_web/templates/address_transaction/index.html.eex:146
#: lib/explorer_web/templates/block/index.html.eex:60 #: lib/explorer_web/templates/block/index.html.eex:60
#: lib/explorer_web/templates/block_transaction/index.html.eex:198 #: lib/explorer_web/templates/block_transaction/index.html.eex:198
#: lib/explorer_web/templates/pending_transaction/index.html.eex:54 #: lib/explorer_web/templates/pending_transaction/index.html.eex:54
@ -486,15 +488,16 @@ msgstr ""
msgid "Contract Source Code" msgid "Contract Source Code"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_contract/index.html.eex:44 #: lib/explorer_web/templates/address_contract/index.html.eex:52
msgid "Verify and Publish" msgid "Verify and Publish"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_contract/index.html.eex:27 #: lib/explorer_web/templates/address_contract/index.html.eex:27
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:31 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:31
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:61 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:69
#: lib/explorer_web/templates/address_read_contract/index.html.eex:27
#: lib/explorer_web/templates/address_transaction/index.html.eex:31 #: lib/explorer_web/templates/address_transaction/index.html.eex:31
#: lib/explorer_web/templates/address_transaction/index.html.eex:61 #: lib/explorer_web/templates/address_transaction/index.html.eex:69
msgid "Code" msgid "Code"
msgstr "" msgstr ""
@ -589,7 +592,7 @@ msgid "block confirmations"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_transaction/index.html.eex:81 #: lib/explorer_web/templates/address_transaction/index.html.eex:89
msgid "Connection Lost, click to load newer transactions" msgid "Connection Lost, click to load newer transactions"
msgstr "" msgstr ""
@ -605,7 +608,7 @@ msgid "Internal Transaction"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_transaction/index.html.eex:76 #: lib/explorer_web/templates/address_transaction/index.html.eex:84
msgid "More messages have come in" msgid "More messages have come in"
msgstr "" msgstr ""
@ -615,12 +618,12 @@ msgid "QR Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:118 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:126
msgid "There are no internal transactions for this address." msgid "There are no internal transactions for this address."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_transaction/index.html.eex:131 #: lib/explorer_web/templates/address_transaction/index.html.eex:139
msgid "There are no transactions for this address." msgid "There are no transactions for this address."
msgstr "" msgstr ""
@ -664,3 +667,21 @@ msgstr ""
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:29 #: lib/explorer_web/templates/address_transaction/_transaction.html.eex:29
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_read_contract/index.html.eex:69
msgid "Query"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_contract/index.html.eex:37
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:42
#: lib/explorer_web/templates/address_read_contract/index.html.eex:36
#: lib/explorer_web/templates/address_transaction/index.html.eex:42
msgid "Read Contract"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_read_contract/index.html.eex:46
msgid "Read Contract Information"
msgstr ""

@ -52,10 +52,11 @@ msgstr "POA Network Explorer"
#: lib/explorer_web/templates/address/overview.html.eex:21 #: lib/explorer_web/templates/address/overview.html.eex:21
#: lib/explorer_web/templates/address_contract/index.html.eex:10 #: lib/explorer_web/templates/address_contract/index.html.eex:10
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:13 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:13
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:47 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:55
#: lib/explorer_web/templates/address_read_contract/index.html.eex:10
#: lib/explorer_web/templates/address_transaction/index.html.eex:13 #: lib/explorer_web/templates/address_transaction/index.html.eex:13
#: lib/explorer_web/templates/address_transaction/index.html.eex:47 #: lib/explorer_web/templates/address_transaction/index.html.eex:55
#: lib/explorer_web/templates/address_transaction/index.html.eex:123 #: lib/explorer_web/templates/address_transaction/index.html.eex:131
#: lib/explorer_web/templates/block/index.html.eex:19 #: lib/explorer_web/templates/block/index.html.eex:19
#: lib/explorer_web/templates/block_transaction/index.html.eex:124 #: lib/explorer_web/templates/block_transaction/index.html.eex:124
#: lib/explorer_web/templates/chain/_transactions.html.eex:4 #: lib/explorer_web/templates/chain/_transactions.html.eex:4
@ -154,13 +155,13 @@ msgstr "%{count} transactions in this block"
msgid "Address" msgid "Address"
msgstr "Address" msgstr "Address"
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:98 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:106
#: lib/explorer_web/templates/address_transaction/index.html.eex:109 #: lib/explorer_web/templates/address_transaction/index.html.eex:117
#: lib/explorer_web/templates/block_transaction/index.html.eex:142 #: lib/explorer_web/templates/block_transaction/index.html.eex:142
#: lib/explorer_web/templates/transaction/overview.html.eex:79 #: lib/explorer_web/templates/transaction/overview.html.eex:79
#: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:31 #: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:31
#: lib/explorer_web/views/address_internal_transaction_view.ex:9 #: lib/explorer_web/views/address_internal_transaction_view.ex:10
#: lib/explorer_web/views/address_transaction_view.ex:9 #: lib/explorer_web/views/address_transaction_view.ex:10
msgid "From" msgid "From"
msgstr "From" msgstr "From"
@ -173,13 +174,13 @@ msgstr "Overview"
msgid "Success" msgid "Success"
msgstr "Success" msgstr "Success"
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:86 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:94
#: lib/explorer_web/templates/address_transaction/index.html.eex:97 #: lib/explorer_web/templates/address_transaction/index.html.eex:105
#: lib/explorer_web/templates/block_transaction/index.html.eex:144 #: lib/explorer_web/templates/block_transaction/index.html.eex:144
#: lib/explorer_web/templates/transaction/overview.html.eex:91 #: lib/explorer_web/templates/transaction/overview.html.eex:91
#: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:32 #: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:32
#: lib/explorer_web/views/address_internal_transaction_view.ex:8 #: lib/explorer_web/views/address_internal_transaction_view.ex:9
#: lib/explorer_web/views/address_transaction_view.ex:8 #: lib/explorer_web/views/address_transaction_view.ex:9
msgid "To" msgid "To"
msgstr "To" msgstr "To"
@ -324,10 +325,11 @@ msgstr ""
#: lib/explorer_web/templates/address_contract/index.html.eex:17 #: lib/explorer_web/templates/address_contract/index.html.eex:17
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:20 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:20
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:52 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:60
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:111 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:119
#: lib/explorer_web/templates/address_read_contract/index.html.eex:17
#: lib/explorer_web/templates/address_transaction/index.html.eex:20 #: lib/explorer_web/templates/address_transaction/index.html.eex:20
#: lib/explorer_web/templates/address_transaction/index.html.eex:52 #: lib/explorer_web/templates/address_transaction/index.html.eex:60
#: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:11 #: lib/explorer_web/templates/transaction_internal_transaction/index.html.eex:11
#: lib/explorer_web/templates/transaction_log/index.html.eex:11 #: lib/explorer_web/templates/transaction_log/index.html.eex:11
msgid "Internal Transactions" msgid "Internal Transactions"
@ -365,10 +367,10 @@ msgstr ""
msgid "Wei" msgid "Wei"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:80 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:88
#: lib/explorer_web/templates/address_transaction/index.html.eex:91 #: lib/explorer_web/templates/address_transaction/index.html.eex:99
#: lib/explorer_web/views/address_internal_transaction_view.ex:10 #: lib/explorer_web/views/address_internal_transaction_view.ex:11
#: lib/explorer_web/views/address_transaction_view.ex:10 #: lib/explorer_web/views/address_transaction_view.ex:11
msgid "All" msgid "All"
msgstr "" msgstr ""
@ -485,8 +487,8 @@ msgstr ""
msgid "There are no Transactions" msgid "There are no Transactions"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:125 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:133
#: lib/explorer_web/templates/address_transaction/index.html.eex:138 #: lib/explorer_web/templates/address_transaction/index.html.eex:146
#: lib/explorer_web/templates/block/index.html.eex:60 #: lib/explorer_web/templates/block/index.html.eex:60
#: lib/explorer_web/templates/block_transaction/index.html.eex:198 #: lib/explorer_web/templates/block_transaction/index.html.eex:198
#: lib/explorer_web/templates/pending_transaction/index.html.eex:54 #: lib/explorer_web/templates/pending_transaction/index.html.eex:54
@ -498,15 +500,16 @@ msgstr ""
msgid "Contract Source Code" msgid "Contract Source Code"
msgstr "Contract Source Code" msgstr "Contract Source Code"
#: lib/explorer_web/templates/address_contract/index.html.eex:44 #: lib/explorer_web/templates/address_contract/index.html.eex:52
msgid "Verify and Publish" msgid "Verify and Publish"
msgstr "" msgstr ""
#: lib/explorer_web/templates/address_contract/index.html.eex:27 #: lib/explorer_web/templates/address_contract/index.html.eex:27
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:31 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:31
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:61 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:69
#: lib/explorer_web/templates/address_read_contract/index.html.eex:27
#: lib/explorer_web/templates/address_transaction/index.html.eex:31 #: lib/explorer_web/templates/address_transaction/index.html.eex:31
#: lib/explorer_web/templates/address_transaction/index.html.eex:61 #: lib/explorer_web/templates/address_transaction/index.html.eex:69
msgid "Code" msgid "Code"
msgstr "" msgstr ""
@ -601,7 +604,7 @@ msgid "block confirmations"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_transaction/index.html.eex:81 #: lib/explorer_web/templates/address_transaction/index.html.eex:89
msgid "Connection Lost, click to load newer transactions" msgid "Connection Lost, click to load newer transactions"
msgstr "" msgstr ""
@ -617,7 +620,7 @@ msgid "Internal Transaction"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_transaction/index.html.eex:76 #: lib/explorer_web/templates/address_transaction/index.html.eex:84
msgid "More messages have come in" msgid "More messages have come in"
msgstr "" msgstr ""
@ -627,12 +630,12 @@ msgid "QR Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:118 #: lib/explorer_web/templates/address_internal_transaction/index.html.eex:126
msgid "There are no internal transactions for this address." msgid "There are no internal transactions for this address."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/explorer_web/templates/address_transaction/index.html.eex:131 #: lib/explorer_web/templates/address_transaction/index.html.eex:139
msgid "There are no transactions for this address." msgid "There are no transactions for this address."
msgstr "" msgstr ""
@ -656,7 +659,7 @@ msgstr ""
msgid "at" msgid "at"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:36 #: lib/explorer_web/templates/address_transaction/_transaction.html.eex:36
#: lib/explorer_web/templates/chain/_transactions.html.eex:25 #: lib/explorer_web/templates/chain/_transactions.html.eex:25
#: lib/explorer_web/templates/transaction/index.html.eex:47 #: lib/explorer_web/templates/transaction/index.html.eex:47
@ -676,3 +679,21 @@ msgstr ""
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:29 #: lib/explorer_web/templates/address_transaction/_transaction.html.eex:29
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_read_contract/index.html.eex:69
msgid "Query"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_contract/index.html.eex:37
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:42
#: lib/explorer_web/templates/address_read_contract/index.html.eex:36
#: lib/explorer_web/templates/address_transaction/index.html.eex:42
msgid "Read Contract"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_read_contract/index.html.eex:46
msgid "Read Contract Information"
msgstr ""

@ -0,0 +1,24 @@
defmodule ExplorerWeb.AddressReadContractControllerTest do
use ExplorerWeb.ConnCase
describe "GET show/3" do
test "only responds to ajax requests", %{conn: conn} do
smart_contract = insert(:smart_contract)
path =
address_read_contract_path(
ExplorerWeb.Endpoint,
:show,
:en,
smart_contract.address_hash,
smart_contract.address_hash,
function_name: "get",
args: []
)
conn = get(conn, path)
assert conn.status == 404
end
end
end

@ -0,0 +1,19 @@
defmodule ExplorerWeb.AddressReadContractViewTest do
use ExplorerWeb.ConnCase, async: true
alias ExplorerWeb.AddressReadContractView
describe "queryable?/1" do
test "returns true if list of inputs is not empty" do
assert AddressReadContractView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true
assert AddressReadContractView.queryable?([]) == false
end
end
describe "address?/1" do
test "returns true if type equals `address`" do
assert AddressReadContractView.address?("address") == true
assert AddressReadContractView.address?("uint256") == false
end
end
end

@ -44,4 +44,71 @@ defmodule ExplorerWeb.AddressViewTest do
assert {:ok, _} = Base.decode64(AddressView.qr_code(address)) assert {:ok, _} = Base.decode64(AddressView.qr_code(address))
end end
end end
describe "smart_contract_verified?/1" do
test "returns true when smart contract is verified" do
smart_contract = insert(:smart_contract)
address = insert(:address, smart_contract: smart_contract)
assert AddressView.smart_contract_verified?(address)
end
test "returns false when smart contract is not verified" do
address = insert(:address, smart_contract: nil)
refute AddressView.smart_contract_verified?(address)
end
end
describe "smart_contract_with_read_only_functions?/1" do
test "returns true when abi has read only functions" do
smart_contract =
insert(
:smart_contract,
abi: [
%{
"constant" => true,
"inputs" => [],
"name" => "get",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
)
address = insert(:address, smart_contract: smart_contract)
assert AddressView.smart_contract_with_read_only_functions?(address)
end
test "returns false when there is no read only functions" do
smart_contract =
insert(
:smart_contract,
abi: [
%{
"constant" => false,
"inputs" => [%{"name" => "x", "type" => "uint256"}],
"name" => "set",
"outputs" => [],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
}
]
)
address = insert(:address, smart_contract: smart_contract)
refute AddressView.smart_contract_with_read_only_functions?(address)
end
test "returns false when smart contract is not verified" do
address = insert(:address, smart_contract: nil)
refute AddressView.smart_contract_with_read_only_functions?(address)
end
end
end end

@ -1,8 +1,10 @@
%{ %{
"abi": {:hex, :abi, "0.1.12", "87ae04cb09e2308db7b3c350584dc3934de0e308f6a056ba82be5756b081a1ca", [:mix], [{:exth_crypto, "~> 0.1.4", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "hexpm"},
"abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, "abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "1.0.6", "58a865939b3106d5ad4841f660955b958be6db955dda034fbbc1069dbacb97fa", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "1.0.6", "58a865939b3106d5ad4841f660955b958be6db955dda034fbbc1069dbacb97fa", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"benchee": {:hex, :benchee, "0.13.1", "bd93ca05be78bcb6159c7176230efeda2f724f7ffd485515175ca411dff4893e", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, "benchee": {:hex, :benchee, "0.13.1", "bd93ca05be78bcb6159c7176230efeda2f724f7ffd485515175ca411dff4893e", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
"benchee_csv": {:hex, :benchee_csv, "0.8.0", "0ca094677d6e2b2f601b7ee7864b754789ef9d24d079432e5e3d6f4fb83a4d80", [:mix], [{:benchee, "~> 0.12", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm"}, "benchee_csv": {:hex, :benchee_csv, "0.8.0", "0ca094677d6e2b2f601b7ee7864b754789ef9d24d079432e5e3d6f4fb83a4d80", [:mix], [{:benchee, "~> 0.12", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm"},
"binary": {:hex, :binary, "0.0.5", "20d816f7274ea34f1b673b4cff2fdb9ebec9391a7a68c349070d515c66b1b2cf", [:mix], [], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
@ -21,6 +23,7 @@
"earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.8", "a4463c0928b970f2cee722cd29aaac154e866a15882c5737e0038bbfcf03ec2c", [: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.8", "a4463c0928b970f2cee722cd29aaac154e866a15882c5737e0038bbfcf03ec2c", [: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.1", "6628b86053190a80b9072382bb9756a6c78624f208ec0ff22cb94c8977d80060", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.1", "6628b86053190a80b9072382bb9756a6c78624f208ec0ff22cb94c8977d80060", [:mix], [], "hexpm"},
"ex_abi": {:hex, :ex_abi, "0.1.13", "717d88fac1d849774d28179aa958e4e55027793934c4dd0d25aa739720716a13", [: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, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ex_cldr": {:hex, :ex_cldr, "1.3.2", "8f4a00c99d1c537b8e8db7e7903f4bd78d82a7289502d080f70365392b13921b", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.2.0", "ef27299922da913ffad1ed296cacf28b6452fc1243b77301dc17c03276c6ee34", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.3", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.2.0", "ef27299922da913ffad1ed296cacf28b6452fc1243b77301dc17c03276c6ee34", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.3", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, "ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"},
@ -29,6 +32,7 @@
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"exth_crypto": {:hex, :exth_crypto, "0.1.4", "11f9084dfd70d4f9e96f2710a472f4e6b23044b97530c719550c2b0450ffeb61", [:mix], [{:binary, "~> 0.0.4", [hex: :binary, repo: "hexpm", optional: false]}, {:keccakf1600, "~> 2.0.0", [hex: :keccakf1600_orig, repo: "hexpm", optional: false]}, {:libsecp256k1, "~> 0.1.3", [hex: :libsecp256k1, repo: "hexpm", optional: false]}], "hexpm"},
"exvcr": {:hex, :exvcr, "0.10.0", "5150808404d9f48dbda636f70f7f8fefd93e2433cd39f695f810e73b3a9d1736", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.13", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "exvcr": {:hex, :exvcr, "0.10.0", "5150808404d9f48dbda636f70f7f8fefd93e2433cd39f695f810e73b3a9d1736", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.13", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm"}, "file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm"},
"floki": {:hex, :floki, "0.20.1", "4ee125a81605f5920189ac6afbee8dbf4de7808319e726f0d57781e660185980", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.20.1", "4ee125a81605f5920189ac6afbee8dbf4de7808319e726f0d57781e660185980", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
@ -42,6 +46,8 @@
"jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"},
"junit_formatter": {:hex, :junit_formatter, "2.1.0", "ff03d2bbe9a67041f2488d8e72180ddcf4dff9e9c8a39b79eac9828fcb9e9bbf", [:mix], [], "hexpm"}, "junit_formatter": {:hex, :junit_formatter, "2.1.0", "ff03d2bbe9a67041f2488d8e72180ddcf4dff9e9c8a39b79eac9828fcb9e9bbf", [:mix], [], "hexpm"},
"keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], [], "hexpm"},
"libsecp256k1": {:hex, :libsecp256k1, "0.1.4", "42b7f76d8e32f85f578ccda0abfdb1afa0c5c231d1fd8aeab9cda352731a2d83", [:rebar3], [], "hexpm"},
"math": {:hex, :math, "0.3.0", "e14e7291115201cb155a3567e66d196bf5088a6f55b030d598107d7ae934a11c", [:mix], [], "hexpm"}, "math": {:hex, :math, "0.3.0", "e14e7291115201cb155a3567e66d196bf5088a6f55b030d598107d7ae934a11c", [:mix], [], "hexpm"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},

Loading…
Cancel
Save