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. 67
      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

@ -1,6 +1,6 @@
# POA Explorer [![CircleCI](https://circleci.com/gh/poanetwork/poa-explorer.svg?style=svg&circle-token=f8823a3d0090407c11f87028c73015a331dbf604)](https://circleci.com/gh/poanetwork/poa-explorer) [![Coverage Status](https://coveralls.io/repos/github/poanetwork/poa-explorer/badge.svg?branch=master)](https://coveralls.io/github/poanetwork/poa-explorer?branch=master)
POA Explorer provides a comprehensive, easy-to-use interface for users to view, confirm, and inspect transactions on **all EVM** (Ethereum Virtual Machine) blockchains. This includes the Ethereum main and test networks as well as **Ethereum forks and sidechains**.
POA Explorer provides a comprehensive, easy-to-use interface for users to view, confirm, and inspect transactions on **all EVM** (Ethereum Virtual Machine) blockchains. This includes the Ethereum main and test networks as well as **Ethereum forks and sidechains**.
Following is an overview of the project and instructions for [getting started](#getting-started).
@ -11,7 +11,7 @@ POA Explorer is an Elixir application that allows users to search transactions,
Currently available block explorers (i.e. Etherscan and Etherchain) are closed systems which are not independently verifiable. As Ethereum sidechains continue to proliferate in both private and public settings, transparent tools are needed to analyze and validate transactions.
The first release will include a block explorer for the POA core and Sokol test networks. Additional networks will be added in upcoming versions.
### Features
@ -21,11 +21,11 @@ Development is ongoing. Please see the [project timeline](https://github.com/poa
- [x] **Real time transaction tracking**: Transactions are updated in real time - no page refresh required. Infinite scrolling is also enabled.
- [x] **Smart contract interaction**: Users can read and verify Solidity smart contracts and access pre-existing contracts to fast-track development. Support for Vyper, LLL, and Web Assembly contracts is in progress.
- [x] **Smart contract interaction**: Users can read and verify Solidity smart contracts and access pre-existing contracts to fast-track development. Support for Vyper, LLL, and Web Assembly contracts is in progress.
- [x] **ERC20 token support**: Version 1 will support ERC20 token ecosystem. Future releases will support additional token types including ERC223, ERC721, and ERC1155.
- [x] **ERC20 token support**: Version 1 will support ERC20 token ecosystem. Future releases will support additional token types including ERC223, ERC721, and ERC1155.
- [x] **User customization**: Users can easily deploy on a network and customize the Bootstrap interface.
- [x] **User customization**: Users can easily deploy on a network and customize the Bootstrap interface.
- [x] **Ethereum sidechain networks**: Version 1 supports the POA main network and Sokol test network. Future iterations will support Ethereum mainnet, Ethereum testnets, forks like Ethereum Classic, sidechains, and private EVM networks.
@ -41,38 +41,43 @@ The [development stack page](https://github.com/poanetwork/poa-explorer/wiki/Dev
* [Elixir 1.6.5](https://elixir-lang.org/)
* [Postgres 10.3](https://www.postgresql.org/)
* [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
### Setup Instructions
1. Fork and clone repository.
[`https://github.com/poanetwork/poa-explorer/fork`](https://github.com/poanetwork/poa-explorer/fork)
1. Fork and clone repository.
[`https://github.com/poanetwork/poa-explorer/fork`](https://github.com/poanetwork/poa-explorer/fork)
2. Set up default configurations.
`cp apps/explorer/config/dev.secret.exs.example apps/explorer/config/dev.secret.exs`
2. Set up default configurations.
`cp apps/explorer/config/dev.secret.exs.example apps/explorer/config/dev.secret.exs`
`cp apps/explorer_web/config/dev.secret.exs.example apps/explorer_web/config/dev.secret.exs`
3. Install dependencies.
3. Install dependencies.
`mix do deps.get, local.rebar, deps.compile, compile`
4. Create and migrate database.
4. Create and migrate database.
`mix ecto.create && mix ecto.migrate`
5. Install Node.js dependencies.
`cd apps/explorer_web/assets && npm install; cd -`
5. Install Node.js dependencies.
`cd apps/explorer_web/assets && npm install; cd -`
`cd apps/explorer && npm install; cd -`
6. Start Phoenix Server.
`mix phx.server`
6. Start Phoenix Server.
`mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
_Additional runtime options:_
* Run Phoenix Server with IEx (Interactive Elixer)
* Run Phoenix Server with IEx (Interactive Elixer)
`iex -S mix phx.server`
* Run Phoenix Server with real time indexer
* Run Phoenix Server with real time indexer
`DEBUG_INDEXER=1 iex -S mix phx.server`
@ -104,29 +109,29 @@ To monitor build status, configure your local [CCMenu](http://ccmenu.org/) with
#### Running the tests
1. Build the assets.
1. Build the assets.
`cd apps/explorer_web/assets && npm run build; cd -`
2. Format the Elixir code.
2. Format the Elixir code.
`mix format`
3. Run the test suite with coverage for whole umbrella project.
3. Run the test suite with coverage for whole umbrella project.
`mix coveralls.html --umbrella`
4. Lint the Elixir code.
4. Lint the Elixir code.
`mix credo --strict`
5. Run the dialyzer.
5. Run the dialyzer.
`mix dialyzer --halt-exit-status`
6. Check the Elixir code for vulnerabilities.
`cd apps/explorer && mix sobelow --config; cd -`
6. Check the Elixir code for vulnerabilities.
`cd apps/explorer && mix sobelow --config; cd -`
`cd apps/explorer_web && mix sobelow --config; cd -`
7. Lint the JavaScript code.
7. Lint the JavaScript code.
`cd apps/explorer_web/assets && npm run eslint; cd -`
8. Test the JavaScript code.
8. Test the JavaScript code.
`cd apps/explorer_web/assets && npm run test; cd -`

@ -83,6 +83,52 @@ defmodule EthereumJSONRPC do
"""
@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 """
Fetches balance for each address `hash` at the `block_number`
"""
@ -240,9 +286,9 @@ defmodule EthereumJSONRPC do
@doc """
A request payload for a JSONRPC.
"""
@spec request(%{id: non_neg_integer(), method: String.t(), params: list()}) :: Transport.request()
def request(%{id: id, method: method, params: params} = map)
when is_integer(id) and is_binary(method) and is_list(params) do
@spec request(%{id: request_id, method: String.t(), params: list()}) :: Transport.request()
def request(%{method: method, params: params} = map)
when is_binary(method) and is_list(params) do
Map.put(map, :jsonrpc, "2.0")
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
# 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}
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
{:mox, "~> 0.3.2", only: [:test]},
# 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

@ -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",
loggers: [],
pool_size: 20,
pool_timeout: 60_000
pool_timeout: 60_000,
timeout: 80_000
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"),
prepare: :unnamed,
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.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()
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()]}
defp changes_list(params, options) when is_list(options) do
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"},
{:sobelow, ">= 0.7.0", only: [:dev, :test], runtime: false},
{: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

@ -1076,4 +1076,12 @@ defmodule Explorer.ChainTest do
test "circulating_supply/0" do
assert Chain.circulating_supply() == ProofOfAuthority.circulating()
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

@ -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,
InternalTransaction,
Log,
Transaction
Transaction,
SmartContract
}
alias Explorer.Market.MarketHistory
@ -277,6 +278,37 @@ defmodule Explorer.Factory do
data(:transaction_input)
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
quote do
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.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/background";
@import "node_modules/bootstrap/scss/utilities/position";
@import "node_modules/bootstrap/scss/utilities/borders";
// Bootstrap Components
@import "node_modules/bootstrap/scss/dropdown";

@ -23,6 +23,7 @@ import './lib/from_now'
import './lib/market_history_chart'
import './lib/reload_button'
import './lib/tooltip'
import './lib/smart_contract/read_function'
import './pages/address'
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": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=",
"dev": true,
"requires": {
"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],
as: :verify_contract
)
resources(
"/read_contract",
AddressReadContractController,
only: [:index, :show],
as: :read_contract
)
end
get("/search", ChainController, :search)

@ -31,6 +31,14 @@
<% end %>
<% end %>
</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>
</div>

@ -36,6 +36,14 @@
<% end %>
</li>
<% 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>
<!-- 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 %>
</li>
<% 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>
<!-- MOBILE DROPDOWN NAV -->

@ -1,7 +1,7 @@
defmodule ExplorerWeb.AddressContractView do
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)

@ -1,7 +1,8 @@
defmodule ExplorerWeb.AddressInternalTransactionView do
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
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
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
case filter do

@ -72,4 +72,10 @@ defmodule ExplorerWeb.AddressView do
end
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

@ -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_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: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:47
#: lib/explorer_web/templates/address_transaction/index.html.eex:123
#: lib/explorer_web/templates/address_transaction/index.html.eex:55
#: lib/explorer_web/templates/address_transaction/index.html.eex:131
#: lib/explorer_web/templates/block/index.html.eex:19
#: lib/explorer_web/templates/block_transaction/index.html.eex:124
#: lib/explorer_web/templates/chain/_transactions.html.eex:4
@ -142,13 +143,13 @@ msgstr ""
msgid "Address"
msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:98
#: lib/explorer_web/templates/address_transaction/index.html.eex:109
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:106
#: lib/explorer_web/templates/address_transaction/index.html.eex:117
#: lib/explorer_web/templates/block_transaction/index.html.eex:142
#: lib/explorer_web/templates/transaction/overview.html.eex:79
#: 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_transaction_view.ex:9
#: lib/explorer_web/views/address_internal_transaction_view.ex:10
#: lib/explorer_web/views/address_transaction_view.ex:10
msgid "From"
msgstr ""
@ -161,13 +162,13 @@ msgstr ""
msgid "Success"
msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:86
#: lib/explorer_web/templates/address_transaction/index.html.eex:97
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:94
#: lib/explorer_web/templates/address_transaction/index.html.eex:105
#: lib/explorer_web/templates/block_transaction/index.html.eex:144
#: lib/explorer_web/templates/transaction/overview.html.eex:91
#: 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_transaction_view.ex:8
#: lib/explorer_web/views/address_internal_transaction_view.ex:9
#: lib/explorer_web/views/address_transaction_view.ex:9
msgid "To"
msgstr ""
@ -312,10 +313,11 @@ msgstr ""
#: 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:52
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:111
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:60
#: 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: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_log/index.html.eex:11
msgid "Internal Transactions"
@ -353,10 +355,10 @@ msgstr ""
msgid "Wei"
msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:80
#: lib/explorer_web/templates/address_transaction/index.html.eex:91
#: lib/explorer_web/views/address_internal_transaction_view.ex:10
#: lib/explorer_web/views/address_transaction_view.ex:10
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:88
#: lib/explorer_web/templates/address_transaction/index.html.eex:99
#: lib/explorer_web/views/address_internal_transaction_view.ex:11
#: lib/explorer_web/views/address_transaction_view.ex:11
msgid "All"
msgstr ""
@ -473,8 +475,8 @@ msgstr ""
msgid "There are no Transactions"
msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:125
#: lib/explorer_web/templates/address_transaction/index.html.eex:138
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:133
#: lib/explorer_web/templates/address_transaction/index.html.eex:146
#: lib/explorer_web/templates/block/index.html.eex:60
#: lib/explorer_web/templates/block_transaction/index.html.eex:198
#: lib/explorer_web/templates/pending_transaction/index.html.eex:54
@ -486,15 +488,16 @@ msgstr ""
msgid "Contract Source Code"
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"
msgstr ""
#: 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: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:61
#: lib/explorer_web/templates/address_transaction/index.html.eex:69
msgid "Code"
msgstr ""
@ -589,7 +592,7 @@ msgid "block confirmations"
msgstr ""
#, 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"
msgstr ""
@ -605,7 +608,7 @@ msgid "Internal Transaction"
msgstr ""
#, 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"
msgstr ""
@ -615,12 +618,12 @@ msgid "QR Code"
msgstr ""
#, 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."
msgstr ""
#, 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."
msgstr ""
@ -664,3 +667,21 @@ msgstr ""
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:29
msgid "OUT"
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_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: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:47
#: lib/explorer_web/templates/address_transaction/index.html.eex:123
#: lib/explorer_web/templates/address_transaction/index.html.eex:55
#: lib/explorer_web/templates/address_transaction/index.html.eex:131
#: lib/explorer_web/templates/block/index.html.eex:19
#: lib/explorer_web/templates/block_transaction/index.html.eex:124
#: lib/explorer_web/templates/chain/_transactions.html.eex:4
@ -154,13 +155,13 @@ msgstr "%{count} transactions in this block"
msgid "Address"
msgstr "Address"
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:98
#: lib/explorer_web/templates/address_transaction/index.html.eex:109
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:106
#: lib/explorer_web/templates/address_transaction/index.html.eex:117
#: lib/explorer_web/templates/block_transaction/index.html.eex:142
#: lib/explorer_web/templates/transaction/overview.html.eex:79
#: 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_transaction_view.ex:9
#: lib/explorer_web/views/address_internal_transaction_view.ex:10
#: lib/explorer_web/views/address_transaction_view.ex:10
msgid "From"
msgstr "From"
@ -173,13 +174,13 @@ msgstr "Overview"
msgid "Success"
msgstr "Success"
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:86
#: lib/explorer_web/templates/address_transaction/index.html.eex:97
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:94
#: lib/explorer_web/templates/address_transaction/index.html.eex:105
#: lib/explorer_web/templates/block_transaction/index.html.eex:144
#: lib/explorer_web/templates/transaction/overview.html.eex:91
#: 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_transaction_view.ex:8
#: lib/explorer_web/views/address_internal_transaction_view.ex:9
#: lib/explorer_web/views/address_transaction_view.ex:9
msgid "To"
msgstr "To"
@ -324,10 +325,11 @@ msgstr ""
#: 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:52
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:111
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:60
#: 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: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_log/index.html.eex:11
msgid "Internal Transactions"
@ -365,10 +367,10 @@ msgstr ""
msgid "Wei"
msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:80
#: lib/explorer_web/templates/address_transaction/index.html.eex:91
#: lib/explorer_web/views/address_internal_transaction_view.ex:10
#: lib/explorer_web/views/address_transaction_view.ex:10
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:88
#: lib/explorer_web/templates/address_transaction/index.html.eex:99
#: lib/explorer_web/views/address_internal_transaction_view.ex:11
#: lib/explorer_web/views/address_transaction_view.ex:11
msgid "All"
msgstr ""
@ -485,8 +487,8 @@ msgstr ""
msgid "There are no Transactions"
msgstr ""
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:125
#: lib/explorer_web/templates/address_transaction/index.html.eex:138
#: lib/explorer_web/templates/address_internal_transaction/index.html.eex:133
#: lib/explorer_web/templates/address_transaction/index.html.eex:146
#: lib/explorer_web/templates/block/index.html.eex:60
#: lib/explorer_web/templates/block_transaction/index.html.eex:198
#: lib/explorer_web/templates/pending_transaction/index.html.eex:54
@ -498,15 +500,16 @@ msgstr ""
msgid "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"
msgstr ""
#: 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: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:61
#: lib/explorer_web/templates/address_transaction/index.html.eex:69
msgid "Code"
msgstr ""
@ -601,7 +604,7 @@ msgid "block confirmations"
msgstr ""
#, 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"
msgstr ""
@ -617,7 +620,7 @@ msgid "Internal Transaction"
msgstr ""
#, 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"
msgstr ""
@ -627,12 +630,12 @@ msgid "QR Code"
msgstr ""
#, 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."
msgstr ""
#, 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."
msgstr ""
@ -656,7 +659,7 @@ msgstr ""
msgid "at"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:36
#: lib/explorer_web/templates/chain/_transactions.html.eex:25
#: lib/explorer_web/templates/transaction/index.html.eex:47
@ -676,3 +679,21 @@ msgstr ""
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:29
msgid "OUT"
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))
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

@ -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"},
"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_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"},
"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"},
@ -21,6 +23,7 @@
"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"},
"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_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"},
@ -29,6 +32,7 @@
"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"},
"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"},
"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"},
@ -42,6 +46,8 @@
"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"},
"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"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},

Loading…
Cancel
Save