Merge branch 'master' into feature/#1476-add-styles-for-POSDAO-network

* master:
  do not allow update code
  mix format
  return full responses
  mix format
  create separate migration
  allow creation different smart contract for address_hash
  add response bodies
  handle invalid address_hash
  add upsert for decompied smart contacts
  add changelog entry
  send response
  Revert "Fix flaky tests"
  add moduledoc
  add auth
  add endpoint to create decompiled smart contracts
  create decompiled smart contracts
pull/1704/head
Gabriel Rodriguez Alsina 6 years ago
commit 3ed4a3eb14
  1. 15
      .circleci/config.yml
  2. 2
      CHANGELOG.md
  3. 3
      apps/block_scout_web/config/config.exs
  4. 79
      apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex
  5. 2
      apps/block_scout_web/lib/block_scout_web/router.ex
  6. 99
      apps/block_scout_web/test/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller_test.exs
  7. 26
      apps/explorer/lib/explorer/chain.ex
  8. 33
      apps/explorer/lib/explorer/chain/decompiled_smart_contract.ex
  9. 10
      apps/explorer/lib/explorer/chain/hash.ex
  10. 15
      apps/explorer/priv/repo/migrations/20190319081821_create_decompiled_smart_contracts.exs
  11. 11
      apps/explorer/priv/repo/migrations/20190325081658_remove_unique_address_hash_decompiled_contracts.exs
  12. 20
      apps/explorer/test/explorer/chain/decompiled_smart_contract_test.exs
  13. 70
      apps/explorer/test/explorer/chain_test.exs
  14. 17
      apps/explorer/test/support/factory.ex

@ -557,6 +557,9 @@ workflows:
# This unfortunately will only fire if all the tests pass because of how `requires` works # This unfortunately will only fire if all the tests pass because of how `requires` works
- coveralls_merge: - coveralls_merge:
requires: requires:
- test_parity_http_websocket
- test_parity_mox
- test_geth_http_websocket
- test_geth_mox - test_geth_mox
- credo: - credo:
requires: requires:
@ -574,7 +577,8 @@ workflows:
- eslint - eslint
- jest - jest
- sobelow - sobelow
# This makes these synchronous, instead of asynchronous - test_parity_http_websocket
- test_parity_mox
- test_geth_http_websocket - test_geth_http_websocket
- test_geth_mox - test_geth_mox
- dialyzer: - dialyzer:
@ -600,13 +604,10 @@ workflows:
- build - build
- test_parity_mox: - test_parity_mox:
requires: requires:
# This makes these synchronous, instead of asynchronous - build
- test_parity_http_websocket
- test_geth_http_websocket: - test_geth_http_websocket:
requires: requires:
# This makes these synchronous, instead of asynchronous - build
- test_parity_mox
- test_geth_mox: - test_geth_mox:
requires: requires:
# This makes these synchronous, instead of asynchronous - build
- test_geth_http_websocket

@ -3,6 +3,8 @@
### Features ### Features
- [1611](https://github.com/poanetwork/blockscout/pull/1611) - allow setting the first indexing block - [1611](https://github.com/poanetwork/blockscout/pull/1611) - allow setting the first indexing block
- [1596](https://github.com/poanetwork/blockscout/pull/1596) - add endpoint to create decompiled contracts
### Fixes ### Fixes

@ -10,7 +10,8 @@ config :block_scout_web,
namespace: BlockScoutWeb, namespace: BlockScoutWeb,
ecto_repos: [Explorer.Repo], ecto_repos: [Explorer.Repo],
version: System.get_env("BLOCKSCOUT_VERSION"), version: System.get_env("BLOCKSCOUT_VERSION"),
release_link: System.get_env("RELEASE_LINK") release_link: System.get_env("RELEASE_LINK"),
decompiled_smart_contract_token: System.get_env("DECOMPILED_SMART_CONTRACT_TOKEN")
config :block_scout_web, BlockScoutWeb.Chain, config :block_scout_web, BlockScoutWeb.Chain,
network: System.get_env("NETWORK"), network: System.get_env("NETWORK"),

@ -0,0 +1,79 @@
defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.Chain.Hash.Address
def create(conn, params) do
if auth_token(conn) == actual_token() do
with {:ok, hash} <- validate_address_hash(params["address_hash"]),
:ok <- smart_contract_exists?(hash),
:ok <- decompiled_contract_exists?(params["address_hash"], params["decompiler_version"]) do
case Chain.create_decompiled_smart_contract(params) do
{:ok, decompiled_smart_contract} ->
send_resp(conn, :created, Jason.encode!(decompiled_smart_contract))
{:error, changeset} ->
errors =
changeset.errors
|> Enum.into(%{}, fn {field, {message, _}} ->
{field, message}
end)
send_resp(conn, :unprocessable_entity, encode(errors))
end
else
:invalid_address ->
send_resp(conn, :unprocessable_entity, encode(%{error: "address_hash is invalid"}))
:not_found ->
send_resp(conn, :unprocessable_entity, encode(%{error: "address is not found"}))
:contract_exists ->
send_resp(
conn,
:unprocessable_entity,
encode(%{error: "decompiled code already exists for the decompiler version"})
)
end
else
send_resp(conn, :forbidden, "")
end
end
defp smart_contract_exists?(address_hash) do
case Chain.hash_to_address(address_hash) do
{:ok, _address} -> :ok
_ -> :not_found
end
end
defp validate_address_hash(address_hash) do
case Address.cast(address_hash) do
{:ok, hash} -> {:ok, hash}
:error -> :invalid_address
end
end
defp decompiled_contract_exists?(address_hash, decompiler_version) do
case Chain.decompiled_code(address_hash, decompiler_version) do
{:ok, _} -> :contract_exists
_ -> :ok
end
end
defp auth_token(conn) do
case get_req_header(conn, "auth_token") do
[token] -> token
other -> other
end
end
defp actual_token do
Application.get_env(:block_scout_web, :decompiled_smart_contract_token)
end
defp encode(data) do
Jason.encode!(data)
end
end

@ -22,6 +22,8 @@ defmodule BlockScoutWeb.Router do
pipe_through(:api) pipe_through(:api)
get("/supply", SupplyController, :supply) get("/supply", SupplyController, :supply)
resources("/decompiled_smart_contract", DecompiledSmartContractController, only: [:create])
end end
scope "/api", BlockScoutWeb.API.RPC do scope "/api", BlockScoutWeb.API.RPC do

@ -0,0 +1,99 @@
defmodule BlockScoutWeb.API.V1.DecompiledControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Repo
alias Explorer.Chain.DecompiledSmartContract
import Ecto.Query,
only: [from: 2]
@secret "secret"
describe "when used authorized" do
setup %{conn: conn} = context do
Application.put_env(:block_scout_web, :decompiled_smart_contract_token, @secret)
auth_conn = conn |> put_req_header("auth_token", @secret)
{:ok, Map.put(context, :conn, auth_conn)}
end
test "returns unprocessable_entity status when params are invalid", %{conn: conn} do
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create))
assert request.status == 422
assert request.resp_body == "{\"error\":\"address_hash is invalid\"}"
end
test "returns unprocessable_entity when code is empty", %{conn: conn} do
decompiler_version = "test_decompiler"
address = insert(:address)
params = %{
"address_hash" => to_string(address.hash),
"decompiler_version" => decompiler_version
}
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create), params)
assert request.status == 422
assert request.resp_body == "{\"decompiled_source_code\":\"can't be blank\"}"
end
test "can not update code for the same decompiler version", %{conn: conn} do
address_hash = to_string(insert(:address, hash: "0x0000000000000000000000000000000000000001").hash)
decompiler_version = "test_decompiler"
decompiled_source_code = "hello world"
insert(:decompiled_smart_contract,
address_hash: address_hash,
decompiler_version: decompiler_version,
decompiled_source_code: decompiled_source_code
)
params = %{
"address_hash" => address_hash,
"decompiler_version" => decompiler_version,
"decompiled_source_code" => decompiled_source_code
}
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create), params)
assert request.status == 422
assert request.resp_body == "{\"error\":\"decompiled code already exists for the decompiler version\"}"
end
test "creates decompiled smart contract", %{conn: conn} do
address_hash = to_string(insert(:address, hash: "0x0000000000000000000000000000000000000001").hash)
decompiler_version = "test_decompiler"
decompiled_source_code = "hello world"
params = %{
"address_hash" => address_hash,
"decompiler_version" => decompiler_version,
"decompiled_source_code" => decompiled_source_code
}
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create), params)
assert request.status == 201
assert request.resp_body ==
"{\"address_hash\":\"0x0000000000000000000000000000000000000001\",\"decompiler_version\":\"test_decompiler\",\"decompiled_source_code\":\"hello world\"}"
decompiled_smart_contract = Repo.one!(from(d in DecompiledSmartContract, where: d.address_hash == ^address_hash))
assert to_string(decompiled_smart_contract.address_hash) == address_hash
assert decompiled_smart_contract.decompiler_version == decompiler_version
assert decompiled_smart_contract.decompiled_source_code == decompiled_source_code
end
end
describe "when user is not authorized" do
test "returns forbedden", %{conn: conn} do
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create))
assert request.status == 403
end
end
end

@ -31,6 +31,7 @@ defmodule Explorer.Chain do
Block, Block,
BlockNumberCache, BlockNumberCache,
Data, Data,
DecompiledSmartContract,
Hash, Hash,
Import, Import,
InternalTransaction, InternalTransaction,
@ -512,6 +513,17 @@ defmodule Explorer.Chain do
|> Repo.insert() |> Repo.insert()
end end
@doc """
Creates a decompiled smart contract.
"""
@spec create_decompiled_smart_contract(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def create_decompiled_smart_contract(attrs) do
%DecompiledSmartContract{}
|> DecompiledSmartContract.changeset(attrs)
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:decompiler_version, :address_hash])
end
@doc """ @doc """
Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users efficiently. Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users efficiently.
@ -656,6 +668,20 @@ defmodule Explorer.Chain do
end end
end end
def decompiled_code(address_hash, version) do
query =
from(contract in DecompiledSmartContract,
where: contract.address_hash == ^address_hash and contract.decompiler_version == ^version
)
query
|> Repo.one()
|> case do
nil -> {:error, :not_found}
contract -> {:ok, contract.decompiled_source_code}
end
end
@spec token_contract_address_from_token_name(String.t()) :: {:ok, Hash.Address.t()} | {:error, :not_found} @spec token_contract_address_from_token_name(String.t()) :: {:ok, Hash.Address.t()} | {:error, :not_found}
def token_contract_address_from_token_name(name) when is_binary(name) do def token_contract_address_from_token_name(name) when is_binary(name) do
query = query =

@ -0,0 +1,33 @@
defmodule Explorer.Chain.DecompiledSmartContract do
@moduledoc """
The representation of a decompiled smart contract.
"""
use Explorer.Schema
alias Explorer.Chain.{Address, Hash}
@derive {Jason.Encoder, only: [:address_hash, :decompiler_version, :decompiled_source_code]}
schema "decompiled_smart_contracts" do
field(:decompiler_version, :string)
field(:decompiled_source_code, :string)
belongs_to(
:address,
Address,
foreign_key: :address_hash,
references: :hash,
type: Hash.Address
)
timestamps()
end
def changeset(%__MODULE__{} = smart_contract, attrs) do
smart_contract
|> cast(attrs, [:decompiler_version, :decompiled_source_code, :address_hash])
|> validate_required([:decompiler_version, :decompiled_source_code, :address_hash])
|> unique_constraint(:address_hash)
end
end

@ -233,4 +233,14 @@ defmodule Explorer.Chain.Hash do
|> BitString.encode(options) |> BitString.encode(options)
end end
end end
defimpl Jason.Encoder do
alias Jason.Encode
def encode(hash, opts) do
hash
|> to_string()
|> Encode.string(opts)
end
end
end end

@ -0,0 +1,15 @@
defmodule Explorer.Repo.Migrations.CreateDecompiledSmartContracts do
use Ecto.Migration
def change do
create table(:decompiled_smart_contracts) do
add(:decompiler_version, :string, null: false)
add(:decompiled_source_code, :text, null: false)
add(:address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false)
timestamps()
end
create(unique_index(:decompiled_smart_contracts, :address_hash))
end
end

@ -0,0 +1,11 @@
defmodule Explorer.Repo.Migrations.RemoveUniqueAddressHashDecompiledContracts do
use Ecto.Migration
def change do
drop(index(:decompiled_smart_contracts, [:address_hash]))
create(
unique_index(:decompiled_smart_contracts, [:address_hash, :decompiler_version], name: :address_decompiler_version)
)
end
end

@ -0,0 +1,20 @@
defmodule Explorer.Chain.DecompiledSmartContractTest do
use Explorer.DataCase
alias Explorer.Chain.DecompiledSmartContract
describe "changeset/2" do
test "with valid attributes" do
params = params_for(:decompiled_smart_contract)
changeset = DecompiledSmartContract.changeset(%DecompiledSmartContract{}, params)
assert changeset.valid?
end
test "with invalid attributes" do
changeset = DecompiledSmartContract.changeset(%DecompiledSmartContract{}, %{elixir: "erlang"})
refute changeset.valid?
end
end
end

@ -14,6 +14,7 @@ defmodule Explorer.ChainTest do
Address, Address,
Block, Block,
Data, Data,
DecompiledSmartContract,
Hash, Hash,
InternalTransaction, InternalTransaction,
Log, Log,
@ -2586,6 +2587,75 @@ defmodule Explorer.ChainTest do
end end
end end
describe "create_decompiled_smart_contract/1" do
test "with valid params creates decompiled smart contract" do
address_hash = to_string(insert(:address).hash)
decompiler_version = "test_decompiler"
decompiled_source_code = "hello world"
params = %{
address_hash: address_hash,
decompiler_version: decompiler_version,
decompiled_source_code: decompiled_source_code
}
{:ok, decompiled_smart_contract} = Chain.create_decompiled_smart_contract(params)
assert decompiled_smart_contract.decompiler_version == decompiler_version
assert decompiled_smart_contract.decompiled_source_code == decompiled_source_code
assert address_hash == to_string(decompiled_smart_contract.address_hash)
end
test "with invalid params can't create decompiled smart contract" do
params = %{code: "cat"}
{:error, _changeset} = Chain.create_decompiled_smart_contract(params)
end
test "updates smart contract code" do
inserted_decompiled_smart_contract = insert(:decompiled_smart_contract)
code = "code2"
{:ok, _decompiled_smart_contract} =
Chain.create_decompiled_smart_contract(%{
decompiler_version: inserted_decompiled_smart_contract.decompiler_version,
decompiled_source_code: code,
address_hash: inserted_decompiled_smart_contract.address_hash
})
decompiled_smart_contract =
Repo.one(
from(ds in DecompiledSmartContract,
where:
ds.address_hash == ^inserted_decompiled_smart_contract.address_hash and
ds.decompiler_version == ^inserted_decompiled_smart_contract.decompiler_version
)
)
assert decompiled_smart_contract.decompiled_source_code == code
end
test "creates two smart contracts for different decompiler versions" do
inserted_decompiled_smart_contract = insert(:decompiled_smart_contract)
code = "code2"
version = "2"
{:ok, _decompiled_smart_contract} =
Chain.create_decompiled_smart_contract(%{
decompiler_version: version,
decompiled_source_code: code,
address_hash: inserted_decompiled_smart_contract.address_hash
})
decompiled_smart_contracts =
Repo.all(
from(ds in DecompiledSmartContract, where: ds.address_hash == ^inserted_decompiled_smart_contract.address_hash)
)
assert Enum.count(decompiled_smart_contracts) == 2
end
end
describe "create_smart_contract/1" do describe "create_smart_contract/1" do
setup do setup do
smart_contract_bytecode = smart_contract_bytecode =

@ -19,6 +19,7 @@ defmodule Explorer.Factory do
Block, Block,
ContractMethod, ContractMethod,
Data, Data,
DecompiledSmartContract,
Hash, Hash,
InternalTransaction, InternalTransaction,
Log, Log,
@ -509,7 +510,7 @@ defmodule Explorer.Factory do
} }
end end
def smart_contract_factory() do def smart_contract_factory do
contract_code_info = contract_code_info() contract_code_info = contract_code_info()
%SmartContract{ %SmartContract{
@ -522,7 +523,17 @@ defmodule Explorer.Factory do
} }
end end
def token_balance_factory() do def decompiled_smart_contract_factory do
contract_code_info = contract_code_info()
%DecompiledSmartContract{
address_hash: insert(:address, contract_code: contract_code_info.bytecode).hash,
decompiler_version: "test_decompiler",
decompiled_source_code: contract_code_info.source_code
}
end
def token_balance_factory do
%TokenBalance{ %TokenBalance{
address: build(:address), address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash, token_contract_address_hash: insert(:token).contract_address_hash,
@ -532,7 +543,7 @@ defmodule Explorer.Factory do
} }
end end
def address_current_token_balance_factory() do def address_current_token_balance_factory do
%CurrentTokenBalance{ %CurrentTokenBalance{
address: build(:address), address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash, token_contract_address_hash: insert(:token).contract_address_hash,

Loading…
Cancel
Save