Cache Balances for Addresses

pull/42/head
CJ Bryan and Desmond Bowe 7 years ago
parent c82d7ea79a
commit 18e0d9afde
  1. 3
      config/config.exs
  2. 2
      config/test.exs
  3. 19
      lib/explorer/ethereum/ethereum.ex
  4. 14
      lib/explorer/ethereum/live.ex
  5. 9
      lib/explorer/ethereum/test.ex
  6. 13
      lib/explorer/forms/address_form.ex
  7. 18
      lib/explorer/importers/balance_importer.ex
  8. 24
      lib/explorer/importers/block_importer.ex
  9. 14
      lib/explorer/importers/internal_transaction_importer.ex
  10. 2
      lib/explorer/importers/receipt_importer.ex
  11. 58
      lib/explorer/importers/transaction_importer.ex
  12. 29
      lib/explorer/schemas/address.ex
  13. 58
      lib/explorer/services/address.ex
  14. 12
      lib/explorer/services/transaction.ex
  15. 13
      lib/explorer/workers/import_balance.ex
  16. 20
      lib/explorer_web/controllers/address_controller.ex
  17. 34
      lib/explorer_web/controllers/address_transaction_from_controller.ex
  18. 34
      lib/explorer_web/controllers/address_transaction_to_controller.ex
  19. 2
      lib/explorer_web/router.ex
  20. 2
      lib/explorer_web/templates/address/show.html.eex
  21. 9
      lib/explorer_web/views/address_view.ex
  22. 8
      priv/repo/migrations/20180224004300_update_credit_debit_materialized_view.exs
  23. 10
      priv/repo/migrations/20180227225553_add_balance_and_balance_updated_at_to_address.exs
  24. 27
      test/explorer/address_test.exs
  25. 18
      test/explorer/ethereum/ethereum_test.exs
  26. 31
      test/explorer/forms/address_form_test.exs
  27. 32
      test/explorer/importers/balance_importer_test.exs
  28. 13
      test/explorer/importers/block_importer_test.exs
  29. 50
      test/explorer/importers/transaction_importer_test.exs
  30. 56
      test/explorer/services/address_test.exs
  31. 33
      test/explorer/workers/import_balance_test.exs
  32. 8
      test/explorer_web/features/contributor_browsing_test.exs

@ -43,12 +43,15 @@ config :exq,
max_retries: 10, max_retries: 10,
queues: [ queues: [
{"default", 1}, {"default", 1},
{"balances", 1},
{"blocks", 1}, {"blocks", 1},
{"internal_transactions", 1}, {"internal_transactions", 1},
{"transactions", 1}, {"transactions", 1},
{"receipts", 1} {"receipts", 1}
] ]
config :explorer, :ethereum, backend: Explorer.Ethereum.Live
config :exq_ui, server: false config :exq_ui, server: false
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom

@ -25,3 +25,5 @@ config :wallaby, screenshot_on_failure: true
# Configure ethereumex # Configure ethereumex
config :ethereumex, url: "https://sokol-trace.poa.network" config :ethereumex, url: "https://sokol-trace.poa.network"
config :explorer, :ethereum, backend: Explorer.Ethereum.Test

@ -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

@ -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

@ -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

@ -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

@ -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

@ -5,6 +5,7 @@ defmodule Explorer.BlockImporter do
import Ethereumex.HttpClient, only: [eth_get_block_by_number: 2] import Ethereumex.HttpClient, only: [eth_get_block_by_number: 2]
alias Explorer.Block alias Explorer.Block
alias Explorer.Ethereum
alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Workers.ImportTransaction alias Explorer.Workers.ImportTransaction
@ -53,15 +54,15 @@ defmodule Explorer.BlockImporter do
def extract_block(raw_block) do def extract_block(raw_block) do
%{ %{
hash: raw_block["hash"], hash: raw_block["hash"],
number: raw_block["number"] |> decode_integer_field, number: raw_block["number"] |> Ethereum.decode_integer_field(),
gas_used: raw_block["gasUsed"] |> decode_integer_field, gas_used: raw_block["gasUsed"] |> Ethereum.decode_integer_field(),
timestamp: raw_block["timestamp"] |> decode_time_field, timestamp: raw_block["timestamp"] |> Ethereum.decode_time_field(),
parent_hash: raw_block["parentHash"], parent_hash: raw_block["parentHash"],
miner: raw_block["miner"], miner: raw_block["miner"],
difficulty: raw_block["difficulty"] |> decode_integer_field, difficulty: raw_block["difficulty"] |> Ethereum.decode_integer_field(),
total_difficulty: raw_block["totalDifficulty"] |> decode_integer_field, total_difficulty: raw_block["totalDifficulty"] |> Ethereum.decode_integer_field(),
size: raw_block["size"] |> decode_integer_field, size: raw_block["size"] |> Ethereum.decode_integer_field(),
gas_limit: raw_block["gasLimit"] |> decode_integer_field, gas_limit: raw_block["gasLimit"] |> Ethereum.decode_integer_field(),
nonce: raw_block["nonce"] || "0" nonce: raw_block["nonce"] || "0"
} }
end end
@ -78,13 +79,4 @@ defmodule Explorer.BlockImporter do
end end
defp encode_number(number), do: "0x" <> Integer.to_string(number, 16) 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 end

@ -3,7 +3,8 @@ defmodule Explorer.InternalTransactionImporter do
import Ecto.Query import Ecto.Query
alias Explorer.Address alias Explorer.Address.Service, as: Address
alias Explorer.Ethereum
alias Explorer.EthereumexExtensions alias Explorer.EthereumexExtensions
alias Explorer.InternalTransaction alias Explorer.InternalTransaction
alias Explorer.Repo alias Explorer.Repo
@ -48,9 +49,9 @@ defmodule Explorer.InternalTransactionImporter do
to_address_id: trace |> to_address() |> address_id(), to_address_id: trace |> to_address() |> address_id(),
from_address_id: trace |> from_address() |> address_id(), from_address_id: trace |> from_address() |> address_id(),
trace_address: trace["traceAddress"], trace_address: trace["traceAddress"],
value: trace["action"]["value"] |> decode_integer_field, value: trace["action"]["value"] |> Ethereum.decode_integer_field(),
gas: trace["action"]["gas"] |> decode_integer_field, gas: trace["action"]["gas"] |> Ethereum.decode_integer_field(),
gas_used: trace["result"]["gasUsed"] |> decode_integer_field, gas_used: trace["result"]["gasUsed"] |> Ethereum.decode_integer_field(),
input: trace["action"]["input"], input: trace["action"]["input"],
output: trace["result"]["output"] output: trace["result"]["output"]
} }
@ -75,11 +76,6 @@ defmodule Explorer.InternalTransactionImporter do
end) end)
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 defp address_id(hash) do
Address.find_or_create_by_hash(hash).id Address.find_or_create_by_hash(hash).id
end end

@ -4,7 +4,7 @@ defmodule Explorer.ReceiptImporter do
import Ecto.Query import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1] import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1]
alias Explorer.Address alias Explorer.Address.Service, as: Address
alias Explorer.Repo alias Explorer.Repo
alias Explorer.Transaction alias Explorer.Transaction
alias Explorer.Receipt alias Explorer.Receipt

@ -4,11 +4,13 @@ defmodule Explorer.TransactionImporter do
import Ecto.Query import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1] import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1]
alias Explorer.Address alias Explorer.Address.Service, as: Address
alias Explorer.Block alias Explorer.Block
alias Explorer.BlockTransaction alias Explorer.BlockTransaction
alias Explorer.Ethereum
alias Explorer.Repo alias Explorer.Repo
alias Explorer.Transaction alias Explorer.Transaction
alias Explorer.BalanceImporter
def import(hash) when is_binary(hash) do def import(hash) when is_binary(hash) do
hash |> download_transaction() |> persist_transaction() hash |> download_transaction() |> persist_transaction()
@ -27,17 +29,31 @@ defmodule Explorer.TransactionImporter do
found_transaction found_transaction
true -> true ->
to_address =
raw_transaction
|> to_address()
|> fetch_address()
from_address =
raw_transaction
|> from_address()
|> fetch_address()
changes = changes =
raw_transaction raw_transaction
|> extract_attrs() |> extract_attrs()
|> Map.put(:to_address_id, create_to_address(raw_transaction).id) |> Map.put(:to_address_id, to_address.id)
|> Map.put(:from_address_id, create_from_address(raw_transaction).id) |> Map.put(:from_address_id, from_address.id)
found_transaction |> Transaction.changeset(changes) |> Repo.insert!() found_transaction |> Transaction.changeset(changes) |> Repo.insert!()
end end
transaction transaction
|> create_block_transaction(raw_transaction["blockHash"]) |> create_block_transaction(raw_transaction["blockHash"])
refresh_account_balances(raw_transaction)
transaction
end end
def find(hash) do def find(hash) do
@ -59,11 +75,11 @@ defmodule Explorer.TransactionImporter do
def extract_attrs(raw_transaction) do def extract_attrs(raw_transaction) do
%{ %{
hash: raw_transaction["hash"], hash: raw_transaction["hash"],
value: raw_transaction["value"] |> decode_integer_field, value: raw_transaction["value"] |> Ethereum.decode_integer_field(),
gas: raw_transaction["gas"] |> decode_integer_field, gas: raw_transaction["gas"] |> Ethereum.decode_integer_field(),
gas_price: raw_transaction["gasPrice"] |> decode_integer_field, gas_price: raw_transaction["gasPrice"] |> Ethereum.decode_integer_field(),
input: raw_transaction["input"], input: raw_transaction["input"],
nonce: raw_transaction["nonce"] |> decode_integer_field, nonce: raw_transaction["nonce"] |> Ethereum.decode_integer_field(),
public_key: raw_transaction["publicKey"], public_key: raw_transaction["publicKey"],
r: raw_transaction["r"], r: raw_transaction["r"],
s: raw_transaction["s"], s: raw_transaction["s"],
@ -102,22 +118,28 @@ defmodule Explorer.TransactionImporter do
transaction transaction
end end
def create_to_address(%{"to" => to}) when not is_nil(to), do: fetch_address(to) 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_to_address(%{"creates" => creates}) when not is_nil(creates), def from_address(%{"from" => from}), do: from
do: fetch_address(creates) def from_address(hash) when is_bitstring(hash), do: hash
def create_to_address(hash) when is_bitstring(hash), do: fetch_address(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 fetch_address(hash) when is_bitstring(hash) do def fetch_address(hash) when is_bitstring(hash) do
Address.find_or_create_by_hash(hash) Address.find_or_create_by_hash(hash)
end end
def decode_integer_field(hex) do defp refresh_account_balances(raw_transaction) do
{"0x", base_16} = String.split_at(hex, 2) raw_transaction
String.to_integer(base_16, 16) |> to_address()
|> update_balance()
raw_transaction
|> from_address()
|> update_balance()
end
defp update_balance(address_hash) do
BalanceImporter.import(address_hash)
end end
end end

@ -8,32 +8,19 @@ defmodule Explorer.Address do
alias Explorer.Address alias Explorer.Address
alias Explorer.Credit alias Explorer.Credit
alias Explorer.Debit alias Explorer.Debit
alias Explorer.Repo.NewRelic, as: Repo
schema "addresses" do schema "addresses" do
has_one(:credit, Credit) has_one(:credit, Credit)
has_one(:debit, Debit) has_one(:debit, Debit)
field(:hash, :string) field(:hash, :string)
field(:balance, :decimal)
field(:balance_updated_at, Timex.Ecto.DateTime)
timestamps() timestamps()
end end
@required_attrs ~w(hash)a @required_attrs ~w(hash)a
@optional_attrs ~w()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 def changeset(%Address{} = address, attrs) do
address address
|> cast(attrs, @required_attrs, @optional_attrs) |> cast(attrs, @required_attrs, @optional_attrs)
@ -41,4 +28,16 @@ defmodule Explorer.Address do
|> update_change(:hash, &String.downcase/1) |> update_change(:hash, &String.downcase/1)
|> unique_constraint(:hash) |> unique_constraint(:hash)
end 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 end

@ -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

@ -17,6 +17,14 @@ defmodule Explorer.Transaction.Service do
import Ecto.Query, only: [from: 2] 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 def recently_seen(query, last_seen) do
from( from(
q in query, q in query,
@ -102,5 +110,9 @@ defmodule Explorer.Transaction.Service do
preload: [:to_address, :from_address] preload: [:to_address, :from_address]
) )
end end
def chron(query) do
from(q in query, order_by: [desc: q.inserted_at])
end
end end
end end

@ -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

@ -1,24 +1,10 @@
defmodule ExplorerWeb.AddressController do defmodule ExplorerWeb.AddressController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query alias Explorer.Address.Service, as: Address
alias Explorer.Address
alias Explorer.AddressForm
alias Explorer.Repo.NewRelic, as: Repo
def show(conn, %{"id" => id}) do def show(conn, %{"id" => id}) do
hash = String.downcase(id) address = id |> Address.by_hash()
render(conn, "show.html", address: address)
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))
end end
end end

@ -5,38 +5,22 @@ defmodule ExplorerWeb.AddressTransactionFromController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query alias Explorer.Address.Service, as: Address
alias Explorer.Address
alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias Explorer.TransactionForm alias Explorer.TransactionForm
def index(conn, %{"address_id" => address_id} = params) do def index(conn, %{"address_id" => address_id} = params) do
hash = String.downcase(address_id) address = Address.by_hash(address_id)
address =
Repo.one(
from(
address in Address,
where: fragment("lower(?)", address.hash) == ^hash,
limit: 1
)
)
address_id = address.id
query = query =
from( Transaction
transaction in Transaction, |> Query.from_address(address.id)
join: block in assoc(transaction, :block), |> Query.include_addresses()
join: receipt in assoc(transaction, :receipt), |> Query.require_receipt()
join: from_address in assoc(transaction, :from_address), |> Query.require_block()
join: to_address in assoc(transaction, :to_address), |> Query.chron()
preload: [:block, :receipt, :to_address, :from_address],
order_by: [desc: transaction.inserted_at],
where: from_address.id == ^address_id
)
page = Repo.paginate(query, params) page = Repo.paginate(query, params)
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1)

@ -5,38 +5,22 @@ defmodule ExplorerWeb.AddressTransactionToController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query alias Explorer.Address.Service, as: Address
alias Explorer.Address
alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias Explorer.TransactionForm alias Explorer.TransactionForm
def index(conn, %{"address_id" => address_id} = params) do def index(conn, %{"address_id" => address_id} = params) do
hash = String.downcase(address_id) address = Address.by_hash(address_id)
address =
Repo.one(
from(
address in Address,
where: fragment("lower(?)", address.hash) == ^hash,
limit: 1
)
)
address_id = address.id
query = query =
from( Transaction
transaction in Transaction, |> Query.to_address(address.id)
join: block in assoc(transaction, :block), |> Query.include_addresses()
join: receipt in assoc(transaction, :receipt), |> Query.require_receipt()
join: from_address in assoc(transaction, :from_address), |> Query.require_block()
join: to_address in assoc(transaction, :to_address), |> Query.chron()
preload: [:block, :receipt, :to_address, :from_address],
order_by: [desc: transaction.inserted_at],
where: to_address.id == ^address_id
)
page = Repo.paginate(query, params) page = Repo.paginate(query, params)
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1)

@ -79,7 +79,7 @@ defmodule ExplorerWeb.Router do
resources "/addresses", AddressController, only: [:show] do resources "/addresses", AddressController, only: [:show] do
resources( resources(
"/transactions", "/transactions_to",
AddressTransactionToController, AddressTransactionToController,
only: [:index], only: [:index],
as: :transaction_to as: :transaction_to

@ -13,7 +13,7 @@
<dl> <dl>
<div class="address__item"> <div class="address__item">
<dt class="address__item-key"><%= gettext "Balance" %></dt> <dt class="address__item-key"><%= gettext "Balance" %></dt>
<dd class="address__item-value" title="<%= @address.hash %>"><%= Decimal.div(Decimal.new(@address.balance), Decimal.new(1_000_000_000_000_000_000)) |> Decimal.to_string(:normal) %> <%= gettext "POA" %></dd> <dd class="address__item-value address__balance" title="<%= @address.hash %>"><%= format_balance(@address.balance) %> <%= gettext "POA" %></dd>
</div> </div>
</dl> </dl>
</div> </div>

@ -1,4 +1,13 @@
defmodule ExplorerWeb.AddressView do defmodule ExplorerWeb.AddressView do
use ExplorerWeb, :view use ExplorerWeb, :view
@dialyzer :no_match @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 end

@ -2,8 +2,8 @@ defmodule Explorer.Repo.Migrations.UpdateCreditDebitMaterializedView do
use Ecto.Migration use Ecto.Migration
def up do def up do
execute "DROP MATERIALIZED VIEW credits;" execute "DROP MATERIALIZED VIEW IF EXISTS credits;"
execute "DROP MATERIALIZED VIEW debits;" execute "DROP MATERIALIZED VIEW IF EXISTS debits;"
execute """ execute """
CREATE MATERIALIZED VIEW credits AS CREATE MATERIALIZED VIEW credits AS
@ -43,8 +43,8 @@ defmodule Explorer.Repo.Migrations.UpdateCreditDebitMaterializedView do
end end
def down do def down do
execute "DROP MATERIALIZED VIEW credits;" execute "DROP MATERIALIZED VIEW IF EXISTS credits;"
execute "DROP MATERIALIZED VIEW debits;" execute "DROP MATERIALIZED VIEW IF EXISTS debits;"
execute """ execute """
CREATE MATERIALIZED VIEW credits AS CREATE MATERIALIZED VIEW credits AS

@ -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

@ -13,26 +13,23 @@ defmodule Explorer.AddressTest do
changeset = Address.changeset(%Address{}, %{dog: "woodstock"}) changeset = Address.changeset(%Address{}, %{dog: "woodstock"})
refute changeset.valid? refute changeset.valid?
end end
end
describe "find_or_create_by_hash/1" do test "it downcases hashes on the way in" do
test "that it creates a new address when one does not exist" do params = params_for(:address, hash: "0xALLCAPS")
Address.find_or_create_by_hash("0xFreshPrince") changeset = Address.changeset(%Address{}, params)
last_address = Address |> order_by(desc: :inserted_at) |> Repo.one() assert Ecto.Changeset.get_change(changeset, :hash) == "0xallcaps"
assert last_address.hash == "0xfreshprince"
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
end end
test "when there is no hash it blows up" do describe "balance_changeset/2" do
assert_raise Ecto.InvalidChangesetError, fn -> test "with a new balance" do
Address.find_or_create_by_hash("") changeset = Address.balance_changeset(%Address{}, %{balance: 99})
assert changeset.valid?
end end
test "with other attributes" do
changeset = Address.balance_changeset(%Address{}, %{hash: "0xraisinets"})
refute changeset.valid?
end end
end end
end end

@ -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

@ -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

@ -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

@ -120,17 +120,4 @@ defmodule Explorer.BlockImporterTest do
) )
end end
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 end

@ -218,6 +218,18 @@ defmodule Explorer.TransactionImporterTest do
assert last_transaction.hash == "0xmunchos" assert last_transaction.hash == "0xmunchos"
end 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 end
describe "find/1" do describe "find/1" do
@ -296,42 +308,4 @@ defmodule Explorer.TransactionImporterTest do
refute block_transaction.updated_at == updated_block_transaction.updated_at refute block_transaction.updated_at == updated_block_transaction.updated_at
end end
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 end

@ -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

@ -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

@ -197,4 +197,12 @@ defmodule ExplorerWeb.UserListTest do
|> click(css(".address__link", text: "Transactions From")) |> click(css(".address__link", text: "Transactions From"))
|> assert_has(css(".transactions__link--long-hash", text: "0xrazerscooter")) |> assert_has(css(".transactions__link--long-hash", text: "0xrazerscooter"))
end 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 end

Loading…
Cancel
Save