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 @@
<%= gettext "Balance" %>
-
<%= Decimal.div(Decimal.new(@address.balance), Decimal.new(1_000_000_000_000_000_000)) |> Decimal.to_string(:normal) %> <%= gettext "POA" %>
+
<%= format_balance(@address.balance) %> <%= gettext "POA" %>
diff --git a/lib/explorer_web/views/address_view.ex b/lib/explorer_web/views/address_view.ex index a4608e1813..cc18780d85 100644 --- a/lib/explorer_web/views/address_view.ex +++ b/lib/explorer_web/views/address_view.ex @@ -1,4 +1,13 @@ defmodule ExplorerWeb.AddressView do use ExplorerWeb, :view @dialyzer :no_match + + def format_balance(nil), do: "0" + + def format_balance(balance) do + balance + |> Decimal.new() + |> Decimal.div(Decimal.new(1_000_000_000_000_000_000)) + |> Decimal.to_string(:normal) + end end diff --git a/priv/repo/migrations/20180224004300_update_credit_debit_materialized_view.exs b/priv/repo/migrations/20180224004300_update_credit_debit_materialized_view.exs index 19182a7a3b..1cb1350201 100644 --- a/priv/repo/migrations/20180224004300_update_credit_debit_materialized_view.exs +++ b/priv/repo/migrations/20180224004300_update_credit_debit_materialized_view.exs @@ -2,8 +2,8 @@ defmodule Explorer.Repo.Migrations.UpdateCreditDebitMaterializedView do use Ecto.Migration def up do - execute "DROP MATERIALIZED VIEW credits;" - execute "DROP MATERIALIZED VIEW debits;" + execute "DROP MATERIALIZED VIEW IF EXISTS credits;" + execute "DROP MATERIALIZED VIEW IF EXISTS debits;" execute """ CREATE MATERIALIZED VIEW credits AS @@ -43,8 +43,8 @@ defmodule Explorer.Repo.Migrations.UpdateCreditDebitMaterializedView do end def down do - execute "DROP MATERIALIZED VIEW credits;" - execute "DROP MATERIALIZED VIEW debits;" + execute "DROP MATERIALIZED VIEW IF EXISTS credits;" + execute "DROP MATERIALIZED VIEW IF EXISTS debits;" execute """ CREATE MATERIALIZED VIEW credits AS diff --git a/priv/repo/migrations/20180227225553_add_balance_and_balance_updated_at_to_address.exs b/priv/repo/migrations/20180227225553_add_balance_and_balance_updated_at_to_address.exs new file mode 100644 index 0000000000..911ea44dbc --- /dev/null +++ b/priv/repo/migrations/20180227225553_add_balance_and_balance_updated_at_to_address.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Migrations.AddBalanceAndBalanceUpdatedAtToAddress do + use Ecto.Migration + + def change do + alter table(:addresses) do + add :balance, :numeric, precision: 100 + add :balance_updated_at, :utc_datetime + end + end +end diff --git a/test/explorer/address_test.exs b/test/explorer/address_test.exs index 76878d1790..206a72f5cd 100644 --- a/test/explorer/address_test.exs +++ b/test/explorer/address_test.exs @@ -13,26 +13,23 @@ defmodule Explorer.AddressTest do changeset = Address.changeset(%Address{}, %{dog: "woodstock"}) refute changeset.valid? end - end - describe "find_or_create_by_hash/1" do - test "that it creates a new address when one does not exist" do - Address.find_or_create_by_hash("0xFreshPrince") - last_address = Address |> order_by(desc: :inserted_at) |> Repo.one() - assert last_address.hash == "0xfreshprince" + test "it downcases hashes on the way in" do + params = params_for(:address, hash: "0xALLCAPS") + changeset = Address.changeset(%Address{}, params) + assert Ecto.Changeset.get_change(changeset, :hash) == "0xallcaps" end + end - test "when the address already exists it doesn't insert a new address" do - insert(:address, %{hash: "bigmouthbillybass"}) - Address.find_or_create_by_hash("bigmouthbillybass") - number_of_addresses = Address |> Repo.all() |> length - assert number_of_addresses == 1 + describe "balance_changeset/2" do + test "with a new balance" do + changeset = Address.balance_changeset(%Address{}, %{balance: 99}) + assert changeset.valid? end - test "when there is no hash it blows up" do - assert_raise Ecto.InvalidChangesetError, fn -> - Address.find_or_create_by_hash("") - end + test "with other attributes" do + changeset = Address.balance_changeset(%Address{}, %{hash: "0xraisinets"}) + refute changeset.valid? end end end diff --git a/test/explorer/ethereum/ethereum_test.exs b/test/explorer/ethereum/ethereum_test.exs new file mode 100644 index 0000000000..2117bb2682 --- /dev/null +++ b/test/explorer/ethereum/ethereum_test.exs @@ -0,0 +1,18 @@ +defmodule Explorer.EthereumTest do + use Explorer.DataCase + + alias Explorer.Ethereum + + describe "decode_integer_field/1" do + test "returns the integer value of a hex value" do + assert(Ethereum.decode_integer_field("0x7f2fb") == 520_955) + end + end + + describe "decode_time_field/1" do + test "returns the date value of a hex value" do + the_seventies = Timex.parse!("1970-01-01T00:00:18-00:00", "{ISO:Extended}") + assert(Ethereum.decode_time_field("0x12") == the_seventies) + end + end +end diff --git a/test/explorer/forms/address_form_test.exs b/test/explorer/forms/address_form_test.exs deleted file mode 100644 index c8bcf662a3..0000000000 --- a/test/explorer/forms/address_form_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Explorer.AddressFormTest do - use Explorer.DataCase - - alias Explorer.AddressForm - alias Explorer.Credit - alias Explorer.Debit - - describe "build/1" do - test "returns a balance" do - recipient = insert(:address) - transaction = insert(:transaction, value: 10, to_address_id: recipient.id) - block = insert(:block) - insert(:block_transaction, block: block, transaction: transaction) - insert(:receipt, transaction: transaction, status: 1) - - Credit.refresh() - Debit.refresh() - - assert AddressForm.build(Repo.preload(recipient, [:debit, :credit])).balance == - Decimal.new(10) - end - - test "returns a zero balance when the address does not have balances" do - address = insert(:address, %{hash: "bert"}) - insert(:transaction, value: 5, to_address_id: address.id) - insert(:transaction, value: 5, to_address_id: address.id) - - assert AddressForm.build(Repo.preload(address, [:debit, :credit])).balance == Decimal.new(0) - end - end -end diff --git a/test/explorer/importers/balance_importer_test.exs b/test/explorer/importers/balance_importer_test.exs new file mode 100644 index 0000000000..698b0609a1 --- /dev/null +++ b/test/explorer/importers/balance_importer_test.exs @@ -0,0 +1,32 @@ +defmodule Explorer.BalanceImporterTest do + use Explorer.DataCase + + alias Explorer.Address.Service, as: Address + alias Explorer.BalanceImporter + + describe "import/1" do + test "it updates the balance for an address" do + insert(:address, hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", balance: 5) + BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + assert address.balance == Decimal.new(1_572_374_181_095_000_000) + end + + test "it updates the balance update time for an address" do + insert( + :address, + hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", + balance_updated_at: nil + ) + + BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + refute is_nil(address.balance_updated_at) + end + + test "it creates an address if one does not exist" do + BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + assert Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + end + end +end diff --git a/test/explorer/importers/block_importer_test.exs b/test/explorer/importers/block_importer_test.exs index e24934c7ca..74f34711fc 100644 --- a/test/explorer/importers/block_importer_test.exs +++ b/test/explorer/importers/block_importer_test.exs @@ -120,17 +120,4 @@ defmodule Explorer.BlockImporterTest do ) end end - - describe "decode_integer_field/1" do - test "returns the integer value of a hex value" do - assert(BlockImporter.decode_integer_field("0x7f2fb") == 520_955) - end - end - - describe "decode_time_field/1" do - test "returns the date value of a hex value" do - the_seventies = Timex.parse!("1970-01-01T00:00:18-00:00", "{ISO:Extended}") - assert(BlockImporter.decode_time_field("0x12") == the_seventies) - end - end end diff --git a/test/explorer/importers/transaction_importer_test.exs b/test/explorer/importers/transaction_importer_test.exs index e7e70fbd38..c550adf012 100644 --- a/test/explorer/importers/transaction_importer_test.exs +++ b/test/explorer/importers/transaction_importer_test.exs @@ -218,6 +218,18 @@ defmodule Explorer.TransactionImporterTest do assert last_transaction.hash == "0xmunchos" end + + test "gets balances for addresses" do + TransactionImporter.import( + "0xdc533d4227734a7cacd75a069e8dc57ac571b865ed97bae5ea4cb74b54145f4c" + ) + + from_address = Address |> Repo.get_by(hash: "0xb2867180771b196518651c174c9240d5e8bd0ecd") + to_address = Address |> Repo.get_by(hash: "0x24e5b8528fe83257d5fe3497ef616026713347f8") + + assert(from_address.balance == Decimal.new(1_572_374_181_095_000_000)) + assert(to_address.balance == Decimal.new(1_572_374_181_095_000_000)) + end end describe "find/1" do @@ -296,42 +308,4 @@ defmodule Explorer.TransactionImporterTest do refute block_transaction.updated_at == updated_block_transaction.updated_at end end - - describe "create_from_address/1" do - test "that it creates a new address when one does not exist" do - TransactionImporter.create_from_address("0xbb8") - last_address = Address |> order_by(desc: :inserted_at) |> Repo.one() - - assert last_address.hash == "0xbb8" - end - - test "when the address already exists it does not insert a new address" do - insert(:address, hash: "0xbb8") - TransactionImporter.create_from_address("0xbb8") - - assert Address |> Repo.all() |> length == 1 - end - end - - describe "create_to_address/1" do - test "that it creates a new address when one does not exist" do - TransactionImporter.create_to_address("0xFreshPrince") - last_address = Address |> order_by(desc: :inserted_at) |> Repo.one() - - assert last_address.hash == "0xfreshprince" - end - - test "when the address already exists it does not insert a new address" do - insert(:address, hash: "bigmouthbillybass") - TransactionImporter.create_to_address("bigmouthbillybass") - - assert Address |> Repo.all() |> length == 1 - end - end - - describe "decode_integer_field/1" do - test "returns the integer value of a hex value" do - assert(TransactionImporter.decode_integer_field("0x7f2fb") == 520_955) - end - end end diff --git a/test/explorer/services/address_test.exs b/test/explorer/services/address_test.exs new file mode 100644 index 0000000000..bccd2fcd6e --- /dev/null +++ b/test/explorer/services/address_test.exs @@ -0,0 +1,56 @@ +defmodule Explorer.Address.ServiceTest do + use Explorer.DataCase + + alias Explorer.Address.Service + alias Explorer.Address + + describe "by_hash/1" do + test "it returns an address with that hash" do + address = insert(:address, hash: "0xandesmints") + result = Service.by_hash("0xandesmints") + assert result.id == address.id + end + end + + describe "update_balance/2" do + test "it updates the balance" do + insert(:address, hash: "0xwarheads") + Service.update_balance(5, "0xwarheads") + result = Service.by_hash("0xwarheads") + assert result.balance == Decimal.new(5) + end + + test "it updates the balance timestamp" do + insert(:address, hash: "0xtwizzlers") + Service.update_balance(88, "0xtwizzlers") + result = Service.by_hash("0xtwizzlers") + refute is_nil(result.balance_updated_at) + end + + test "it creates an address if one does not exist" do + Service.update_balance(88, "0xtwizzlers") + result = Service.by_hash("0xtwizzlers") + assert result.balance == Decimal.new(88) + end + end + + describe "find_or_create_by_hash/1" do + test "that it creates a new address when one does not exist" do + Service.find_or_create_by_hash("0xFreshPrince") + assert Service.by_hash("0xfreshprince") + end + + test "when the address already exists it doesn't insert a new address" do + insert(:address, %{hash: "bigmouthbillybass"}) + Service.find_or_create_by_hash("bigmouthbillybass") + number_of_addresses = Address |> Repo.all() |> length + assert number_of_addresses == 1 + end + + test "when there is no hash it blows up" do + assert_raise Ecto.InvalidChangesetError, fn -> + Service.find_or_create_by_hash("") + end + end + end +end diff --git a/test/explorer/workers/import_balance_test.exs b/test/explorer/workers/import_balance_test.exs new file mode 100644 index 0000000000..7504744966 --- /dev/null +++ b/test/explorer/workers/import_balance_test.exs @@ -0,0 +1,33 @@ +defmodule Explorer.Workers.ImportBalanceTest do + import Mock + + alias Explorer.Workers.ImportBalance + alias Explorer.Address.Service, as: Address + + use Explorer.DataCase + + describe "perform/1" do + test "imports the balance for an address" do + ImportBalance.perform("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a") + address = Address.by_hash("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a") + assert address.balance == Decimal.new(1_572_374_181_095_000_000) + end + end + + describe "perform_later/1" do + test "delays the import of the balance for an address" do + with_mock Exq, + enqueue: fn _, _, _, _ -> + insert( + :address, + hash: "0xskateboards", + balance: 66 + ) + end do + ImportBalance.perform_later("0xskateboards") + address = Address.by_hash("0xskateboards") + assert address.balance == Decimal.new(66) + end + end + end +end diff --git a/test/explorer_web/features/contributor_browsing_test.exs b/test/explorer_web/features/contributor_browsing_test.exs index 68f99a511c..168abf5e42 100644 --- a/test/explorer_web/features/contributor_browsing_test.exs +++ b/test/explorer_web/features/contributor_browsing_test.exs @@ -197,4 +197,12 @@ defmodule ExplorerWeb.UserListTest do |> click(css(".address__link", text: "Transactions From")) |> assert_has(css(".transactions__link--long-hash", text: "0xrazerscooter")) end + + test "views addresses", %{session: session} do + insert(:address, hash: "0xthinmints", balance: 500) + + session + |> visit("/en/addresses/0xthinmints") + |> assert_has(css(".address__balance", text: "0.0000000000000005")) + end end