diff --git a/config/config.exs b/config/config.exs index 49bdf2d24d..3b20eda7c4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -43,12 +43,15 @@ config :exq, max_retries: 10, queues: [ {"default", 1}, + {"balances", 1}, {"blocks", 1}, {"internal_transactions", 1}, {"transactions", 1}, {"receipts", 1} ] +config :explorer, :ethereum, backend: Explorer.Ethereum.Live + config :exq_ui, server: false # Import environment specific config. This must remain at the bottom diff --git a/config/test.exs b/config/test.exs index c09d0908dd..442717e8d5 100644 --- a/config/test.exs +++ b/config/test.exs @@ -25,3 +25,5 @@ config :wallaby, screenshot_on_failure: true # Configure ethereumex config :ethereumex, url: "https://sokol-trace.poa.network" + +config :explorer, :ethereum, backend: Explorer.Ethereum.Test diff --git a/lib/explorer/ethereum/ethereum.ex b/lib/explorer/ethereum/ethereum.ex new file mode 100644 index 0000000000..6b4e4f6cd9 --- /dev/null +++ b/lib/explorer/ethereum/ethereum.ex @@ -0,0 +1,19 @@ +defmodule Explorer.Ethereum do + @client Application.get_env(:explorer, :ethereum)[:backend] + + defmodule API do + @moduledoc false + @callback download_balance(String.t()) :: String.t() + end + + defdelegate download_balance(hash), to: @client + + def decode_integer_field(hex) do + {"0x", base_16} = String.split_at(hex, 2) + String.to_integer(base_16, 16) + end + + def decode_time_field(field) do + field |> decode_integer_field() |> Timex.from_unix() + end +end diff --git a/lib/explorer/ethereum/live.ex b/lib/explorer/ethereum/live.ex new file mode 100644 index 0000000000..08eb02f8d6 --- /dev/null +++ b/lib/explorer/ethereum/live.ex @@ -0,0 +1,14 @@ +defmodule Explorer.Ethereum.Live do + @moduledoc """ + An implementation for Ethereum that uses the actual node. + """ + + @behaviour Explorer.Ethereum.API + + import Ethereumex.HttpClient, only: [eth_get_balance: 1] + + def download_balance(hash) do + {:ok, result} = eth_get_balance(hash) + result + end +end diff --git a/lib/explorer/ethereum/test.ex b/lib/explorer/ethereum/test.ex new file mode 100644 index 0000000000..743e240e59 --- /dev/null +++ b/lib/explorer/ethereum/test.ex @@ -0,0 +1,9 @@ +defmodule Explorer.Ethereum.Test do + @moduledoc """ + An interface for the Ethereum node that does not hit the network + """ + @behaviour Explorer.Ethereum.API + def download_balance(_hash) do + "0x15d231fca629c7c0" + end +end diff --git a/lib/explorer/forms/address_form.ex b/lib/explorer/forms/address_form.ex deleted file mode 100644 index 0dd25a4979..0000000000 --- a/lib/explorer/forms/address_form.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Explorer.AddressForm do - @moduledoc false - - alias Explorer.Credit - alias Explorer.Debit - - def build(address) do - credit = address.credit || Credit.null() - debit = address.debit || Debit.null() - balance = Decimal.sub(credit.value, debit.value) - Map.put(address, :balance, balance) - end -end diff --git a/lib/explorer/importers/balance_importer.ex b/lib/explorer/importers/balance_importer.ex new file mode 100644 index 0000000000..1cb817ff41 --- /dev/null +++ b/lib/explorer/importers/balance_importer.ex @@ -0,0 +1,18 @@ +defmodule Explorer.BalanceImporter do + @moduledoc "Imports a balance for a given address." + + alias Explorer.Address.Service, as: Address + alias Explorer.Ethereum + + def import(hash) do + hash + |> Ethereum.download_balance() + |> persist_balance(hash) + end + + defp persist_balance(balance, hash) do + balance + |> Ethereum.decode_integer_field() + |> Address.update_balance(hash) + end +end diff --git a/lib/explorer/importers/block_importer.ex b/lib/explorer/importers/block_importer.ex index d9057889ce..3c86c449d6 100644 --- a/lib/explorer/importers/block_importer.ex +++ b/lib/explorer/importers/block_importer.ex @@ -5,6 +5,7 @@ defmodule Explorer.BlockImporter do import Ethereumex.HttpClient, only: [eth_get_block_by_number: 2] alias Explorer.Block + alias Explorer.Ethereum alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Workers.ImportTransaction @@ -53,15 +54,15 @@ defmodule Explorer.BlockImporter do def extract_block(raw_block) do %{ hash: raw_block["hash"], - number: raw_block["number"] |> decode_integer_field, - gas_used: raw_block["gasUsed"] |> decode_integer_field, - timestamp: raw_block["timestamp"] |> decode_time_field, + number: raw_block["number"] |> Ethereum.decode_integer_field(), + gas_used: raw_block["gasUsed"] |> Ethereum.decode_integer_field(), + timestamp: raw_block["timestamp"] |> Ethereum.decode_time_field(), parent_hash: raw_block["parentHash"], miner: raw_block["miner"], - difficulty: raw_block["difficulty"] |> decode_integer_field, - total_difficulty: raw_block["totalDifficulty"] |> decode_integer_field, - size: raw_block["size"] |> decode_integer_field, - gas_limit: raw_block["gasLimit"] |> decode_integer_field, + difficulty: raw_block["difficulty"] |> Ethereum.decode_integer_field(), + total_difficulty: raw_block["totalDifficulty"] |> Ethereum.decode_integer_field(), + size: raw_block["size"] |> Ethereum.decode_integer_field(), + gas_limit: raw_block["gasLimit"] |> Ethereum.decode_integer_field(), nonce: raw_block["nonce"] || "0" } end @@ -78,13 +79,4 @@ defmodule Explorer.BlockImporter do end defp encode_number(number), do: "0x" <> Integer.to_string(number, 16) - - def decode_integer_field(hex) do - {"0x", base_16} = String.split_at(hex, 2) - String.to_integer(base_16, 16) - end - - def decode_time_field(field) do - field |> decode_integer_field |> Timex.from_unix() - end end diff --git a/lib/explorer/importers/internal_transaction_importer.ex b/lib/explorer/importers/internal_transaction_importer.ex index 56a30455d1..c09c6fd79f 100644 --- a/lib/explorer/importers/internal_transaction_importer.ex +++ b/lib/explorer/importers/internal_transaction_importer.ex @@ -3,7 +3,8 @@ defmodule Explorer.InternalTransactionImporter do import Ecto.Query - alias Explorer.Address + alias Explorer.Address.Service, as: Address + alias Explorer.Ethereum alias Explorer.EthereumexExtensions alias Explorer.InternalTransaction alias Explorer.Repo @@ -48,9 +49,9 @@ defmodule Explorer.InternalTransactionImporter do to_address_id: trace |> to_address() |> address_id(), from_address_id: trace |> from_address() |> address_id(), trace_address: trace["traceAddress"], - value: trace["action"]["value"] |> decode_integer_field, - gas: trace["action"]["gas"] |> decode_integer_field, - gas_used: trace["result"]["gasUsed"] |> decode_integer_field, + value: trace["action"]["value"] |> Ethereum.decode_integer_field(), + gas: trace["action"]["gas"] |> Ethereum.decode_integer_field(), + gas_used: trace["result"]["gasUsed"] |> Ethereum.decode_integer_field(), input: trace["action"]["input"], output: trace["result"]["output"] } @@ -75,11 +76,6 @@ defmodule Explorer.InternalTransactionImporter do end) end - defp decode_integer_field(hex) do - {"0x", base_16} = String.split_at(hex, 2) - String.to_integer(base_16, 16) - end - defp address_id(hash) do Address.find_or_create_by_hash(hash).id end diff --git a/lib/explorer/importers/receipt_importer.ex b/lib/explorer/importers/receipt_importer.ex index 286691e802..2ff01b7e2d 100644 --- a/lib/explorer/importers/receipt_importer.ex +++ b/lib/explorer/importers/receipt_importer.ex @@ -4,7 +4,7 @@ defmodule Explorer.ReceiptImporter do import Ecto.Query import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1] - alias Explorer.Address + alias Explorer.Address.Service, as: Address alias Explorer.Repo alias Explorer.Transaction alias Explorer.Receipt diff --git a/lib/explorer/importers/transaction_importer.ex b/lib/explorer/importers/transaction_importer.ex index 6d10144167..ae02a67953 100644 --- a/lib/explorer/importers/transaction_importer.ex +++ b/lib/explorer/importers/transaction_importer.ex @@ -4,11 +4,13 @@ defmodule Explorer.TransactionImporter do import Ecto.Query import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1] - alias Explorer.Address + alias Explorer.Address.Service, as: Address alias Explorer.Block alias Explorer.BlockTransaction + alias Explorer.Ethereum alias Explorer.Repo alias Explorer.Transaction + alias Explorer.BalanceImporter def import(hash) when is_binary(hash) do hash |> download_transaction() |> persist_transaction() @@ -27,17 +29,31 @@ defmodule Explorer.TransactionImporter do found_transaction true -> + to_address = + raw_transaction + |> to_address() + |> fetch_address() + + from_address = + raw_transaction + |> from_address() + |> fetch_address() + changes = raw_transaction |> extract_attrs() - |> Map.put(:to_address_id, create_to_address(raw_transaction).id) - |> Map.put(:from_address_id, create_from_address(raw_transaction).id) + |> Map.put(:to_address_id, to_address.id) + |> Map.put(:from_address_id, from_address.id) found_transaction |> Transaction.changeset(changes) |> Repo.insert!() end transaction |> create_block_transaction(raw_transaction["blockHash"]) + + refresh_account_balances(raw_transaction) + + transaction end def find(hash) do @@ -59,11 +75,11 @@ defmodule Explorer.TransactionImporter do def extract_attrs(raw_transaction) do %{ hash: raw_transaction["hash"], - value: raw_transaction["value"] |> decode_integer_field, - gas: raw_transaction["gas"] |> decode_integer_field, - gas_price: raw_transaction["gasPrice"] |> decode_integer_field, + value: raw_transaction["value"] |> Ethereum.decode_integer_field(), + gas: raw_transaction["gas"] |> Ethereum.decode_integer_field(), + gas_price: raw_transaction["gasPrice"] |> Ethereum.decode_integer_field(), input: raw_transaction["input"], - nonce: raw_transaction["nonce"] |> decode_integer_field, + nonce: raw_transaction["nonce"] |> Ethereum.decode_integer_field(), public_key: raw_transaction["publicKey"], r: raw_transaction["r"], s: raw_transaction["s"], @@ -102,22 +118,28 @@ defmodule Explorer.TransactionImporter do transaction end - def create_to_address(%{"to" => to}) when not is_nil(to), do: fetch_address(to) - - def create_to_address(%{"creates" => creates}) when not is_nil(creates), - do: fetch_address(creates) - - def create_to_address(hash) when is_bitstring(hash), do: fetch_address(hash) + def to_address(%{"to" => to}) when not is_nil(to), do: to + def to_address(%{"creates" => creates}) when not is_nil(creates), do: creates + def to_address(hash) when is_bitstring(hash), do: hash - def create_from_address(%{"from" => from}), do: fetch_address(from) - def create_from_address(hash) when is_bitstring(hash), do: fetch_address(hash) + def from_address(%{"from" => from}), do: from + def from_address(hash) when is_bitstring(hash), do: hash def fetch_address(hash) when is_bitstring(hash) do Address.find_or_create_by_hash(hash) end - def decode_integer_field(hex) do - {"0x", base_16} = String.split_at(hex, 2) - String.to_integer(base_16, 16) + defp refresh_account_balances(raw_transaction) do + raw_transaction + |> to_address() + |> update_balance() + + raw_transaction + |> from_address() + |> update_balance() + end + + defp update_balance(address_hash) do + BalanceImporter.import(address_hash) end end diff --git a/lib/explorer/schemas/address.ex b/lib/explorer/schemas/address.ex index 5a943dfa39..4e757d1dfc 100644 --- a/lib/explorer/schemas/address.ex +++ b/lib/explorer/schemas/address.ex @@ -8,32 +8,19 @@ defmodule Explorer.Address do alias Explorer.Address alias Explorer.Credit alias Explorer.Debit - alias Explorer.Repo.NewRelic, as: Repo schema "addresses" do has_one(:credit, Credit) has_one(:debit, Debit) field(:hash, :string) + field(:balance, :decimal) + field(:balance_updated_at, Timex.Ecto.DateTime) timestamps() end @required_attrs ~w(hash)a @optional_attrs ~w()a - def find_or_create_by_hash(hash) do - query = - from( - a in Address, - where: fragment("lower(?)", a.hash) == ^String.downcase(hash), - limit: 1 - ) - - case query |> Repo.one() do - nil -> Repo.insert!(Address.changeset(%Address{}, %{hash: hash})) - address -> address - end - end - def changeset(%Address{} = address, attrs) do address |> cast(attrs, @required_attrs, @optional_attrs) @@ -41,4 +28,16 @@ defmodule Explorer.Address do |> update_change(:hash, &String.downcase/1) |> unique_constraint(:hash) end + + def balance_changeset(%Address{} = address, attrs) do + address + |> cast(attrs, [:balance]) + |> validate_required([:balance]) + |> put_balance_updated_at() + end + + defp put_balance_updated_at(changeset) do + changeset + |> put_change(:balance_updated_at, Timex.now()) + end end diff --git a/lib/explorer/services/address.ex b/lib/explorer/services/address.ex new file mode 100644 index 0000000000..91c86089ae --- /dev/null +++ b/lib/explorer/services/address.ex @@ -0,0 +1,58 @@ +defmodule Explorer.Address.Service do + @moduledoc "Service module for interacting with Addresses" + + alias Explorer.Address + alias Explorer.Repo.NewRelic, as: Repo + alias Explorer.Address.Service.Query + + def by_hash(hash) do + Address + |> Query.by_hash(hash) + |> Query.include_credit_and_debit() + |> Repo.one() + end + + def update_balance(balance, hash) do + changes = %{ + balance: balance + } + + hash + |> find_or_create_by_hash() + |> Address.balance_changeset(changes) + |> Repo.update() + end + + def find_or_create_by_hash(hash) do + Address + |> Query.by_hash(hash) + |> Repo.one() + |> case do + nil -> Repo.insert!(Address.changeset(%Address{}, %{hash: hash})) + address -> address + end + end + + defmodule Query do + @moduledoc "Query module for pulling in aspects of Addresses." + + import Ecto.Query, only: [from: 2] + + def by_hash(query, hash) do + from( + q in query, + where: fragment("lower(?)", q.hash) == ^String.downcase(hash), + limit: 1 + ) + end + + def include_credit_and_debit(query) do + from( + q in query, + left_join: credit in assoc(q, :credit), + left_join: debit in assoc(q, :debit), + preload: [:credit, :debit] + ) + end + end +end diff --git a/lib/explorer/services/transaction.ex b/lib/explorer/services/transaction.ex index 7d2e7617fe..0763fce7f6 100644 --- a/lib/explorer/services/transaction.ex +++ b/lib/explorer/services/transaction.ex @@ -17,6 +17,14 @@ defmodule Explorer.Transaction.Service do import Ecto.Query, only: [from: 2] + def to_address(query, to_address_id) do + from(q in query, where: q.to_address_id == ^to_address_id) + end + + def from_address(query, from_address_id) do + from(q in query, where: q.from_address_id == ^from_address_id) + end + def recently_seen(query, last_seen) do from( q in query, @@ -102,5 +110,9 @@ defmodule Explorer.Transaction.Service do preload: [:to_address, :from_address] ) end + + def chron(query) do + from(q in query, order_by: [desc: q.inserted_at]) + end end end diff --git a/lib/explorer/workers/import_balance.ex b/lib/explorer/workers/import_balance.ex new file mode 100644 index 0000000000..7e20b2e337 --- /dev/null +++ b/lib/explorer/workers/import_balance.ex @@ -0,0 +1,13 @@ +defmodule Explorer.Workers.ImportBalance do + @moduledoc "A worker that imports the balance for a given address." + + alias Explorer.BalanceImporter + + def perform(hash) do + BalanceImporter.import(hash) + end + + def perform_later(hash) do + Exq.enqueue(Exq.Enqueuer, "balances", __MODULE__, [hash]) + end +end diff --git a/lib/explorer_web/controllers/address_controller.ex b/lib/explorer_web/controllers/address_controller.ex index 4ac6784b44..01f0f0471b 100644 --- a/lib/explorer_web/controllers/address_controller.ex +++ b/lib/explorer_web/controllers/address_controller.ex @@ -1,24 +1,10 @@ defmodule ExplorerWeb.AddressController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Address - alias Explorer.AddressForm - alias Explorer.Repo.NewRelic, as: Repo + alias Explorer.Address.Service, as: Address def show(conn, %{"id" => id}) do - hash = String.downcase(id) - - query = - from( - address in Address, - where: fragment("lower(?)", address.hash) == ^hash, - preload: [:credit, :debit], - limit: 1 - ) - - address = Repo.one(query) - render(conn, "show.html", address: AddressForm.build(address)) + address = id |> Address.by_hash() + render(conn, "show.html", address: address) end end diff --git a/lib/explorer_web/controllers/address_transaction_from_controller.ex b/lib/explorer_web/controllers/address_transaction_from_controller.ex index 98f70792be..4d65b31f32 100644 --- a/lib/explorer_web/controllers/address_transaction_from_controller.ex +++ b/lib/explorer_web/controllers/address_transaction_from_controller.ex @@ -5,38 +5,22 @@ defmodule ExplorerWeb.AddressTransactionFromController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Address + alias Explorer.Address.Service, as: Address alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Transaction + alias Explorer.Transaction.Service.Query alias Explorer.TransactionForm def index(conn, %{"address_id" => address_id} = params) do - hash = String.downcase(address_id) - - address = - Repo.one( - from( - address in Address, - where: fragment("lower(?)", address.hash) == ^hash, - limit: 1 - ) - ) - - address_id = address.id + address = Address.by_hash(address_id) query = - from( - transaction in Transaction, - join: block in assoc(transaction, :block), - join: receipt in assoc(transaction, :receipt), - join: from_address in assoc(transaction, :from_address), - join: to_address in assoc(transaction, :to_address), - preload: [:block, :receipt, :to_address, :from_address], - order_by: [desc: transaction.inserted_at], - where: from_address.id == ^address_id - ) + Transaction + |> Query.from_address(address.id) + |> Query.include_addresses() + |> Query.require_receipt() + |> Query.require_block() + |> Query.chron() page = Repo.paginate(query, params) entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) diff --git a/lib/explorer_web/controllers/address_transaction_to_controller.ex b/lib/explorer_web/controllers/address_transaction_to_controller.ex index 9f0f03998e..2f62f53055 100644 --- a/lib/explorer_web/controllers/address_transaction_to_controller.ex +++ b/lib/explorer_web/controllers/address_transaction_to_controller.ex @@ -5,38 +5,22 @@ defmodule ExplorerWeb.AddressTransactionToController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Address + alias Explorer.Address.Service, as: Address alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Transaction + alias Explorer.Transaction.Service.Query alias Explorer.TransactionForm def index(conn, %{"address_id" => address_id} = params) do - hash = String.downcase(address_id) - - address = - Repo.one( - from( - address in Address, - where: fragment("lower(?)", address.hash) == ^hash, - limit: 1 - ) - ) - - address_id = address.id + address = Address.by_hash(address_id) query = - from( - transaction in Transaction, - join: block in assoc(transaction, :block), - join: receipt in assoc(transaction, :receipt), - join: from_address in assoc(transaction, :from_address), - join: to_address in assoc(transaction, :to_address), - preload: [:block, :receipt, :to_address, :from_address], - order_by: [desc: transaction.inserted_at], - where: to_address.id == ^address_id - ) + Transaction + |> Query.to_address(address.id) + |> Query.include_addresses() + |> Query.require_receipt() + |> Query.require_block() + |> Query.chron() page = Repo.paginate(query, params) entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) diff --git a/lib/explorer_web/router.ex b/lib/explorer_web/router.ex index e02e58ebf2..d8736955f2 100644 --- a/lib/explorer_web/router.ex +++ b/lib/explorer_web/router.ex @@ -79,7 +79,7 @@ defmodule ExplorerWeb.Router do resources "/addresses", AddressController, only: [:show] do resources( - "/transactions", + "/transactions_to", AddressTransactionToController, only: [:index], as: :transaction_to diff --git a/lib/explorer_web/templates/address/show.html.eex b/lib/explorer_web/templates/address/show.html.eex index a12cfaa40f..6db1960425 100644 --- a/lib/explorer_web/templates/address/show.html.eex +++ b/lib/explorer_web/templates/address/show.html.eex @@ -13,7 +13,7 @@