commit
bc3ba4c7b0
@ -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,84 @@ |
||||
defmodule Explorer.Chain.Import.Runner.InternalTransactionsIndexedAtBlocks do |
||||
@moduledoc """ |
||||
Bulk updates `internal_transactions_indexed_at` for provided blocks |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.Multi |
||||
alias Explorer.Chain.Block |
||||
alias Explorer.Chain.Import.Runner |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
@behaviour Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [%{number: Block.block_number()}] |
||||
|
||||
@impl Runner |
||||
def ecto_schema_module, do: Block |
||||
|
||||
@impl Runner |
||||
def option_key, do: :internal_transactions_indexed_at_blocks |
||||
|
||||
@impl Runner |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[%{number: Explorer.Chain.Block.block_number()}]", |
||||
value_description: "List of block numbers to set `internal_transactions_indexed_at` field for" |
||||
} |
||||
end |
||||
|
||||
@impl Runner |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) when is_map(options) do |
||||
transactions_timeout = options[Runner.Transactions.option_key()][:timeout] || Runner.Transactions.timeout() |
||||
|
||||
update_transactions_options = %{timeout: transactions_timeout, timestamps: timestamps} |
||||
|
||||
multi |
||||
|> Multi.run(:internal_transactions_indexed_at_blocks, fn repo, _ -> |
||||
update_blocks(repo, changes_list, update_transactions_options) |
||||
end) |
||||
end |
||||
|
||||
@impl Runner |
||||
def timeout, do: @timeout |
||||
|
||||
defp update_blocks(_repo, [], %{}), do: {:ok, []} |
||||
|
||||
defp update_blocks(repo, block_numbers, %{ |
||||
timeout: timeout, |
||||
timestamps: timestamps |
||||
}) |
||||
when is_list(block_numbers) do |
||||
ordered_block_numbers = |
||||
block_numbers |
||||
|> Enum.map(fn %{number: number} -> number end) |
||||
|> Enum.sort() |
||||
|
||||
query = |
||||
from( |
||||
b in Block, |
||||
where: b.number in ^ordered_block_numbers and b.consensus, |
||||
update: [ |
||||
set: [ |
||||
internal_transactions_indexed_at: ^timestamps.updated_at |
||||
] |
||||
] |
||||
) |
||||
|
||||
block_count = Enum.count(ordered_block_numbers) |
||||
|
||||
try do |
||||
{^block_count, result} = repo.update_all(query, [], timeout: timeout) |
||||
|
||||
{:ok, result} |
||||
rescue |
||||
postgrex_error in Postgrex.Error -> |
||||
{:error, %{exception: postgrex_error, block_numbers: ordered_block_numbers}} |
||||
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 |
Loading…
Reference in new issue