* 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 contractspull/1704/head
commit
3ed4a3eb14
@ -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 |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue