parent
67341f15bc
commit
bb4ba872a4
@ -1,4 +1,5 @@ |
||||
web: bin/start-pgbouncer-stunnel mix phx.server |
||||
worker: bin/start-pgbouncer-stunnel mix exq.start |
||||
scheduler: bin/start-pgbouncer-stunnel mix exq.start scheduler |
||||
scraper: bin/start-pgbouncer-stunnel mix scrape 1000000 |
||||
blocks: bin/start-pgbouncer-stunnel mix scrape.blocks 1000000 |
||||
receipts: bin/start-pgbouncer-stunnel mix scrape.receipts 1000000 |
||||
|
@ -0,0 +1,47 @@ |
||||
.transaction-log { |
||||
@extend %paper; |
||||
|
||||
&__container { |
||||
padding: explorer-size(-1) explorer-size(0); |
||||
& + & { padding-top: 0; } |
||||
&--title { padding-top: explorer-size(0); } |
||||
} |
||||
|
||||
&__header { @extend %section-header; } |
||||
&__heading { @extend %section-header__heading; } |
||||
&__subheading { @extend %section-header__subheading; } |
||||
&__tabs { @extend %section-tabs; } |
||||
|
||||
&__tab { |
||||
@extend %section-tabs__tab; |
||||
&--active { @extend %section-tabs__tab--active; } |
||||
} |
||||
|
||||
&__attributes { padding: explorer-size(-1) explorer-size(1); } |
||||
&__link { color: explorer-color("blue", "500"); } |
||||
|
||||
&__table { |
||||
@extend %table; |
||||
@include explorer-typography("body1"); |
||||
color: explorer-color("slate", "100"); |
||||
} |
||||
|
||||
&__column-header { @include explorer-typography("body1"); } |
||||
} |
||||
|
||||
@media (min-width: $explorer-breakpoint-lg) { |
||||
.transaction { |
||||
&__attributes { |
||||
display: flex; |
||||
align-items: top; |
||||
justify-content: top; |
||||
} |
||||
|
||||
&__column { |
||||
width: explorer-size(1); |
||||
flex: 1; |
||||
margin-right: explorer-size(1); |
||||
& + & { margin-left: explorer-size(1); } |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,58 @@ |
||||
defmodule Explorer.TransactionReceiptImporter do |
||||
@moduledoc "Imports a transaction receipt given a transaction hash." |
||||
|
||||
import Ecto.Query |
||||
import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1] |
||||
|
||||
alias Explorer.Repo |
||||
alias Explorer.Transaction |
||||
alias Explorer.TransactionReceipt |
||||
|
||||
def import(hash) do |
||||
hash |
||||
|> download_receipt() |
||||
|> ingest_receipt() |
||||
|> save_receipt() |
||||
end |
||||
|
||||
@dialyzer {:nowarn_function, download_receipt: 1} |
||||
defp download_receipt(hash) do |
||||
{:ok, receipt} = eth_get_transaction_receipt(hash) |
||||
receipt |
||||
end |
||||
|
||||
defp ingest_receipt(%{} = receipt) do |
||||
hash = String.downcase(receipt["transactionHash"]) |
||||
query = from transaction in Transaction, |
||||
left_join: receipt in assoc(transaction, :receipt), |
||||
where: transaction.hash == ^hash, |
||||
where: is_nil(receipt.id), |
||||
limit: 1 |
||||
transaction = Repo.one(query) || Transaction.null |
||||
receipt |
||||
|> extract_receipt() |
||||
|> Map.put(:transaction_id, transaction.id) |
||||
end |
||||
|
||||
defp save_receipt(receipt) do |
||||
unless is_nil(receipt.transaction_id) do |
||||
%TransactionReceipt{} |
||||
|> TransactionReceipt.changeset(receipt) |
||||
|> Repo.insert() |
||||
end |
||||
end |
||||
|
||||
defp extract_receipt(receipt) do |
||||
%{ |
||||
index: receipt["transactionIndex"] |> decode_integer_field(), |
||||
cumulative_gas_used: receipt["cumulativeGasUsed"] |> decode_integer_field(), |
||||
gas_used: receipt["gasUsed"] |> decode_integer_field(), |
||||
status: receipt["status"] |> decode_integer_field(), |
||||
} |
||||
end |
||||
|
||||
defp decode_integer_field(hex) do |
||||
{"0x", base_16} = String.split_at(hex, 2) |
||||
String.to_integer(base_16, 16) |
||||
end |
||||
end |
@ -0,0 +1,35 @@ |
||||
defmodule Explorer.Log do |
||||
@moduledoc "Captures a Web3 log entry generated by a transaction" |
||||
|
||||
use Ecto.Schema |
||||
|
||||
import Ecto.Changeset |
||||
|
||||
alias Explorer.Address |
||||
alias Explorer.Log |
||||
alias Explorer.TransactionReceipt |
||||
|
||||
@timestamps_opts [type: Timex.Ecto.DateTime, |
||||
autogenerate: {Timex.Ecto.DateTime, :autogenerate, []}] |
||||
|
||||
@required_attrs ~w(index data removed)a |
||||
|
||||
schema "logs" do |
||||
belongs_to :transaction_receipt, TransactionReceipt |
||||
belongs_to :address, Address |
||||
has_one :transaction, through: [:transaction_receipt, :transaction] |
||||
field :index, :integer |
||||
field :data, :string |
||||
field :removed, :boolean |
||||
field :first_topic, :string |
||||
field :second_topic, :string |
||||
field :third_topic, :string |
||||
timestamps() |
||||
end |
||||
|
||||
def changeset(%Log{} = log, attrs \\ %{}) do |
||||
log |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
end |
||||
end |
@ -0,0 +1,20 @@ |
||||
defmodule Explorer.SkippedReceipts do |
||||
@moduledoc """ |
||||
Find transactions that do not have a receipt. |
||||
""" |
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
alias Explorer.Transaction |
||||
alias Explorer.Repo.NewRelic, as: Repo |
||||
|
||||
def first, do: first(1) |
||||
def first(count) do |
||||
transactions = from transaction in Transaction, |
||||
left_join: receipt in assoc(transaction, :receipt), |
||||
select: fragment("hash"), |
||||
where: is_nil(receipt.id), |
||||
order_by: [desc: fragment("lower(hash)")], |
||||
limit: ^count |
||||
Repo.all(transactions) |
||||
end |
||||
end |
@ -0,0 +1,35 @@ |
||||
defmodule Explorer.TransactionReceipt do |
||||
@moduledoc "Captures a Web3 Transaction Receipt." |
||||
|
||||
use Ecto.Schema |
||||
|
||||
import Ecto.Changeset |
||||
|
||||
alias Explorer.Transaction |
||||
alias Explorer.TransactionReceipt |
||||
|
||||
@timestamps_opts [type: Timex.Ecto.DateTime, |
||||
autogenerate: {Timex.Ecto.DateTime, :autogenerate, []}] |
||||
|
||||
@required_attrs ~w(cumulative_gas_used gas_used status index)a |
||||
|
||||
schema "transaction_receipts" do |
||||
belongs_to :transaction, Transaction |
||||
field :cumulative_gas_used, :decimal |
||||
field :gas_used, :decimal |
||||
field :status, :integer |
||||
field :index, :integer |
||||
timestamps() |
||||
end |
||||
|
||||
def changeset(%TransactionReceipt{} = transaction_receipt, attrs \\ %{}) do |
||||
transaction_receipt |
||||
|> cast(attrs, [:transaction_id | @required_attrs]) |
||||
|> cast_assoc(:transaction) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:transaction_id) |
||||
|> unique_constraint(:transaction_id) |
||||
end |
||||
|
||||
def null, do: %TransactionReceipt{} |
||||
end |
@ -0,0 +1,17 @@ |
||||
defmodule ExplorerWeb.TransactionLogController do |
||||
use ExplorerWeb, :controller |
||||
|
||||
import Ecto.Query |
||||
|
||||
alias Explorer.Log |
||||
alias Explorer.Repo.NewRelic, as: Repo |
||||
|
||||
def index(conn, params) do |
||||
hash = params["transaction_id"] |
||||
logs = from log in Log, |
||||
join: transaction in assoc(log, :transaction), |
||||
preload: [:address], |
||||
where: transaction.hash == ^hash |
||||
render(conn, "index.html", logs: Repo.paginate(logs), transaction_id: hash) |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
<section class="container__section"> |
||||
<div class="transaction-log__header"> |
||||
<h1 class="transaction-log__heading"><%= gettext "Transaction Logs" %></h1> |
||||
<h3 class="transaction-log__subheading"><%= @transaction_id %></h3> |
||||
</div> |
||||
<div class="transaction-log"> |
||||
<div class="transaction-log__tabs"> |
||||
<h2 class="transaction-log__tab"><%= link(gettext("Overview"), to: transaction_path(@conn, :show, @conn.assigns.locale, @transaction_id), class: "transaction-log__link") %></h2> |
||||
<h2 class="transaction-log__tab transaction-log__tab--active"><%= link(gettext("Logs"), to: transaction_log_path(@conn, :index, @conn.assigns.locale, @transaction_id), class: "transaction-log__link transaction-log__link--active") %></h2> |
||||
</div> |
||||
<div class="transaction-log__container"> |
||||
<table class="transaction-log__table"> |
||||
<thead> |
||||
<th class="transaction-log__column-header"><%= gettext "Address" %></th> |
||||
<th class="transaction-log__column-header"><%= gettext "Topic" %></th> |
||||
</thead> |
||||
<%= for log <- @logs.entries do %> |
||||
<tgroup> |
||||
<tr> |
||||
<td><%= link(log.address.hash, to: address_path(@conn, :show, @conn.assigns.locale, log.address.hash), class: "transaction-log__link") %></td> |
||||
<td><%= log.first_topic %></td> |
||||
</tr> |
||||
<% unless is_nil(log.second_topic) do %> |
||||
<tr><td>topic[1]</td><td><%= log.second_topic %></td></tr> |
||||
<% end %> |
||||
<% unless is_nil(log.third_topic) do %> |
||||
<tr><td>topic[2]</td><td><%= log.third_topic %></td></tr> |
||||
<% end %> |
||||
<% unless is_nil(log.data) do %> |
||||
<tr><td>↠</td><td><%= log.data %></td></tr> |
||||
<% end %> |
||||
</tgroup> |
||||
<% end %> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
</section> |
@ -0,0 +1,4 @@ |
||||
defmodule ExplorerWeb.TransactionLogView do |
||||
use ExplorerWeb, :view |
||||
@dialyzer :no_match |
||||
end |
@ -1,4 +1,4 @@ |
||||
defmodule Mix.Tasks.Scrape do |
||||
defmodule Mix.Tasks.Scrape.Blocks do |
||||
@moduledoc "Scrapes blocks from web3" |
||||
use Mix.Task |
||||
alias Explorer.Repo |
@ -0,0 +1,23 @@ |
||||
defmodule Mix.Tasks.Scrape.Receipts do |
||||
@moduledoc "Scrapes blocks from web3" |
||||
use Mix.Task |
||||
|
||||
alias Explorer.Repo |
||||
alias Explorer.SkippedReceipts |
||||
alias Explorer.TransactionReceiptImporter |
||||
|
||||
def run([]), do: run(1) |
||||
def run(count) do |
||||
[:postgrex, :ecto, :ethereumex, :tzdata] |
||||
|> Enum.each(&Application.ensure_all_started/1) |
||||
Repo.start_link() |
||||
|
||||
"#{count}" |
||||
|> String.to_integer() |
||||
|> SkippedReceipts.first() |
||||
|> Enum.shuffle() |
||||
|> Flow.from_enumerable() |
||||
|> Flow.map(&TransactionReceiptImporter.import/1) |
||||
|> Enum.to_list() |
||||
end |
||||
end |
@ -0,0 +1,17 @@ |
||||
defmodule Explorer.Repo.Migrations.CreateTransactionReceipts do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:transaction_receipts) do |
||||
add :transaction_id, references(:transactions), null: false |
||||
add :cumulative_gas_used, :numeric, precision: 100, null: false |
||||
add :gas_used, :numeric, precision: 100, null: false |
||||
add :status, :integer, null: false |
||||
add :index, :integer, null: false |
||||
timestamps null: false |
||||
end |
||||
|
||||
create index(:transaction_receipts, :index) |
||||
create unique_index(:transaction_receipts, [:transaction_id, :index]) |
||||
end |
||||
end |
@ -0,0 +1,17 @@ |
||||
defmodule Explorer.Repo.Migrations.CreateLogs do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:logs) do |
||||
add :transaction_receipt_id, references(:transaction_receipts), null: false |
||||
add :address_id, references(:addresses), null: false |
||||
add :index, :integer, null: false |
||||
add :data, :string, null: false |
||||
add :removed, :boolean, null: false |
||||
add :first_topic, :string, null: true |
||||
add :second_topic, :string, null: true |
||||
add :third_topic, :string, null: true |
||||
timestamps null: false |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,33 @@ |
||||
defmodule Explorer.TransactionReceiptImporterTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.TransactionReceipt |
||||
alias Explorer.TransactionReceiptImporter |
||||
|
||||
describe "import/1" do |
||||
test "imports and saves a transaction receipt to the database" do |
||||
transaction = insert(:transaction, hash: "0xdc3a0dfd0bbffd5eabbe40fb13afbe35ac5f5c030bff148f3e50afe32974b291") |
||||
use_cassette "transaction_importer_import_1_receipt" do |
||||
TransactionReceiptImporter.import("0xdc3a0dfd0bbffd5eabbe40fb13afbe35ac5f5c030bff148f3e50afe32974b291") |
||||
receipt = TransactionReceipt |> order_by(desc: :inserted_at) |> preload([:transaction]) |> Repo.one |
||||
assert receipt.transaction == transaction |
||||
end |
||||
end |
||||
|
||||
test "does not import a receipt for a transaction that already has one" do |
||||
transaction = insert(:transaction, hash: "0xdc3a0dfd0bbffd5eabbe40fb13afbe35ac5f5c030bff148f3e50afe32974b291") |
||||
insert(:transaction_receipt, transaction: transaction) |
||||
use_cassette "transaction_importer_import_1_receipt" do |
||||
TransactionReceiptImporter.import("0xdc3a0dfd0bbffd5eabbe40fb13afbe35ac5f5c030bff148f3e50afe32974b291") |
||||
assert Repo.all(TransactionReceipt) |> Enum.count() == 1 |
||||
end |
||||
end |
||||
|
||||
test "does not import a receipt for a nonexistent transaction" do |
||||
use_cassette "transaction_importer_import_1_receipt" do |
||||
TransactionReceiptImporter.import("0xdc3a0dfd0bbffd5eabbe40fb13afbe35ac5f5c030bff148f3e50afe32974b291") |
||||
assert Repo.all(TransactionReceipt) |> Enum.count() == 0 |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
defmodule Explorer.LogTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Log |
||||
|
||||
describe "changeset/2" do |
||||
test "accepts valid attributes" do |
||||
params = params_for(:log) |
||||
changeset = Log.changeset(%Log{}, params) |
||||
assert changeset.valid? |
||||
end |
||||
|
||||
test "rejects missing attributes" do |
||||
params = params_for(:log, data: nil) |
||||
changeset = Log.changeset(%Log{}, params) |
||||
refute changeset.valid? |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,60 @@ |
||||
defmodule Explorer.SkippedReceiptsTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.SkippedReceipts |
||||
|
||||
describe "first/0 when there are no transactions" do |
||||
test "returns no transactions" do |
||||
assert SkippedReceipts.first() == [] |
||||
end |
||||
end |
||||
|
||||
describe "first/0 when there are no skipped transactions" do |
||||
test "returns no transactions" do |
||||
transaction = insert(:transaction) |
||||
insert(:transaction_receipt, transaction: transaction) |
||||
assert SkippedReceipts.first() == [] |
||||
end |
||||
end |
||||
|
||||
describe "first/0 when a transaction has been skipped" do |
||||
test "returns the first skipped transaction hash" do |
||||
insert(:transaction, %{hash: "0xBEE75"}) |
||||
assert SkippedReceipts.first() == ["0xBEE75"] |
||||
end |
||||
end |
||||
|
||||
describe "first/1 when there are no transactions" do |
||||
test "returns no transactions" do |
||||
assert SkippedReceipts.first(1) == [] |
||||
end |
||||
end |
||||
|
||||
describe "first/1 when there are no skipped transactions" do |
||||
test "returns no transactions" do |
||||
transaction = insert(:transaction) |
||||
insert(:transaction_receipt, transaction: transaction) |
||||
assert SkippedReceipts.first(1) == [] |
||||
end |
||||
end |
||||
|
||||
describe "first/1 when a transaction has been skipped" do |
||||
test "returns the skipped transaction number" do |
||||
insert(:transaction, %{hash: "0xBEE75"}) |
||||
assert SkippedReceipts.first(1) == ["0xBEE75"] |
||||
end |
||||
|
||||
test "returns up to the requested number of skipped transaction hashes in insert order" do |
||||
insert(:transaction, %{hash: "0xBEE75"}) |
||||
insert(:transaction, %{hash: "0xBE475"}) |
||||
assert SkippedReceipts.first(1) == ["0xBEE75"] |
||||
end |
||||
|
||||
test "returns all the skipped transaction hashes in random order" do |
||||
insert(:transaction, %{hash: "0xBEE75"}) |
||||
insert(:transaction, %{hash: "0xBE475"}) |
||||
transaction_hashes = SkippedReceipts.first(100) |
||||
assert("0xBEE75" in transaction_hashes and "0xBE475" in transaction_hashes) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,21 @@ |
||||
defmodule Explorer.TransactionReceiptTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.TransactionReceipt |
||||
|
||||
describe "changeset/2" do |
||||
test "accepts valid attributes" do |
||||
transaction = insert(:transaction) |
||||
params = params_for(:transaction_receipt, transaction: transaction) |
||||
changeset = TransactionReceipt.changeset(%TransactionReceipt{}, params) |
||||
assert changeset.valid? |
||||
end |
||||
|
||||
test "rejects missing attributes" do |
||||
transaction = insert(:transaction) |
||||
params = params_for(:transaction_receipt, transaction: transaction, cumulative_gas_used: nil) |
||||
changeset = TransactionReceipt.changeset(%TransactionReceipt{}, params) |
||||
refute changeset.valid? |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,25 @@ |
||||
defmodule ExplorerWeb.TransactionLogControllerTest do |
||||
use ExplorerWeb.ConnCase |
||||
|
||||
import ExplorerWeb.Router.Helpers, only: [transaction_log_path: 4] |
||||
|
||||
describe "GET index/2" do |
||||
test "returns logs for the transaction", %{conn: conn} do |
||||
transaction = insert(:transaction) |
||||
transaction_receipt = insert(:transaction_receipt, transaction: transaction) |
||||
address = insert(:address) |
||||
insert(:log, transaction_receipt: transaction_receipt, address: address) |
||||
path = transaction_log_path(ExplorerWeb.Endpoint, :index, :en, transaction.hash) |
||||
conn = get(conn, path) |
||||
first_log = List.first(conn.assigns.logs.entries) |
||||
assert first_log.transaction_receipt_id == transaction_receipt.id |
||||
end |
||||
|
||||
test "assigns no logs when there are none", %{conn: conn} do |
||||
transaction = insert(:transaction) |
||||
path = transaction_log_path(ExplorerWeb.Endpoint, :index, :en, transaction.hash) |
||||
conn = get(conn, path) |
||||
assert Enum.count(conn.assigns.logs.entries) == 0 |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,16 @@ |
||||
defmodule Explorer.LogFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def log_factory do |
||||
%Explorer.Log{ |
||||
index: sequence(""), |
||||
data: sequence("0x"), |
||||
removed: Enum.random([true, false]), |
||||
first_topic: sequence("0x"), |
||||
second_topic: sequence("0x"), |
||||
third_topic: sequence("0x"), |
||||
} |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
defmodule Explorer.TransactionReceiptFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def transaction_receipt_factory do |
||||
%Explorer.TransactionReceipt{ |
||||
cumulative_gas_used: Enum.random(21_000..100_000), |
||||
gas_used: Enum.random(21_000..100_000), |
||||
status: Enum.random(1..2), |
||||
index: sequence(""), |
||||
} |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,10 +1,12 @@ |
||||
defmodule Explorer.Factory do |
||||
@dialyzer {:nowarn_function, fields_for: 1} |
||||
use ExMachina.Ecto, repo: Explorer.Repo |
||||
use Explorer.AddressFactory |
||||
use Explorer.BlockFactory |
||||
use Explorer.TransactionFactory |
||||
use Explorer.BlockTransactionFactory |
||||
use Explorer.AddressFactory |
||||
use Explorer.ToAddressFactory |
||||
use Explorer.FromAddressFactory |
||||
use Explorer.LogFactory |
||||
use Explorer.ToAddressFactory |
||||
use Explorer.TransactionFactory |
||||
use Explorer.TransactionReceiptFactory |
||||
end |
||||
|
Loading…
Reference in new issue