parent
8a2f87ae5b
commit
d0009784af
@ -0,0 +1,53 @@ |
||||
defmodule Explorer.Chain.Transaction.History.Historian do |
||||
use Explorer.History.Historian |
||||
alias Explorer.History.Process, as: HistoryProcess |
||||
alias Explorer.Repo |
||||
alias Explorer.Chain.Block |
||||
alias Explorer.Chain.Transaction.History.TransactionStats |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
alias Explorer.Chain.Transaction |
||||
|
||||
@behaviour Historian |
||||
|
||||
@impl Historian |
||||
def compile_records(num_days, records \\ []) do |
||||
|
||||
if num_days == 0 do |
||||
#base case |
||||
{:ok, records} |
||||
else |
||||
day_to_fetch = Date.add(date_today(), -1*(num_days-1)) |
||||
|
||||
earliest = datetime(day_to_fetch, ~T[00:00:00]) |
||||
latest = datetime(day_to_fetch, ~T[23:59:59]) |
||||
|
||||
query = from( |
||||
block in Block, |
||||
where: (block.timestamp >= ^earliest and block.timestamp <= ^latest), |
||||
join: transaction in Transaction, |
||||
on: block.hash == transaction.block_hash) |
||||
|
||||
num_transactions = Repo.aggregate query, :count, :hash |
||||
records = [%{date: day_to_fetch, number_of_transactions: num_transactions} | records ] |
||||
compile_records(num_days-1, records) |
||||
end |
||||
end |
||||
|
||||
@impl Historian |
||||
def save_records(records) do |
||||
{num_inserted, _} = Repo.insert_all(TransactionStats, records, on_conflict: :replace_all, conflict_target: [:date]) |
||||
num_inserted |
||||
end |
||||
|
||||
@spec datetime(Date.t(), Time.t()) :: DateTime.t() |
||||
defp datetime(date, time) do |
||||
{_success?, naive_dt} = NaiveDateTime.new(date, time) |
||||
DateTime.from_naive!(naive_dt, "Etc/UTC") |
||||
end |
||||
|
||||
defp date_today() do |
||||
HistoryProcess.config_or_default(:utc_today, Date.utc_today(), __MODULE__) |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
defmodule Explorer.Chain.Transaction.History.TransactionStats do |
||||
@moduledoc """ |
||||
Represents daily transaction numbers. |
||||
""" |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Repo |
||||
|
||||
|
||||
schema "transaction_stats" do |
||||
field(:date, :date) |
||||
field(:number_of_transactions, :integer) |
||||
end |
||||
|
||||
@typedoc """ |
||||
The recorded values of the number of transactions for a single day. |
||||
* `:date` - The date in UTC. |
||||
* `:number_of_transactions` - Number of transactions processed by the vm for a given date. |
||||
""" |
||||
@type t :: %__MODULE__{ |
||||
date: Date.t(), |
||||
number_of_transactions: Integer.t() |
||||
} |
||||
|
||||
@spec by_date_range(Date.t(), Date.t()) :: [__MODULE__] |
||||
def by_date_range(earliest, latest) do |
||||
# Create a query |
||||
query = from stat in __MODULE__, |
||||
where: (stat.date >= ^earliest and stat.date<=^latest), |
||||
order_by: [desc: :date] |
||||
|
||||
Repo.all(query) |
||||
end |
||||
end |
@ -0,0 +1,12 @@ |
||||
defmodule Explorer.Repo.Migrations.CreateTransactionStats do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:transaction_stats) do |
||||
add(:date, :date) |
||||
add(:number_of_transactions, :integer) |
||||
end |
||||
|
||||
create(unique_index(:transaction_stats, :date)) |
||||
end |
||||
end |
@ -0,0 +1,94 @@ |
||||
defmodule Explorer.Chain.Transaction.History.HistorianTest do |
||||
use Explorer.DataCase, async: false |
||||
|
||||
alias Explorer.Chain.Transaction.History.Historian |
||||
alias Explorer.Chain.Transaction.History.TransactionStats |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
|
||||
|
||||
setup do |
||||
Application.put_env(:explorer, Historian, utc_today: ~D[1970-01-04]) |
||||
:ok |
||||
end |
||||
|
||||
defp days_to_secs(days) do |
||||
60*60*24*days |
||||
end |
||||
|
||||
describe "compile_records/1" do |
||||
|
||||
test "fetches transactions from blocks mined in the past num_days" do |
||||
|
||||
blocks = [ |
||||
#1970-01-03 00:00:60 |
||||
insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + 60)), |
||||
|
||||
#1970-01-03 04:00:00 |
||||
insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + (4*60*60))), |
||||
|
||||
#1970-01-02 00:00:00 |
||||
insert(:block, timestamp: DateTime.from_unix!(days_to_secs(1))) |
||||
] |
||||
|
||||
insert(:transaction) |> with_block(Enum.at(blocks, 0)) |
||||
insert(:transaction) |> with_block(Enum.at(blocks, 1)) |
||||
insert(:transaction) |> with_block(Enum.at(blocks, 2)) |
||||
|
||||
expected = [ |
||||
%{date: ~D[1970-01-04], number_of_transactions: 0} |
||||
] |
||||
assert {:ok, ^expected} = Historian.compile_records 1 |
||||
|
||||
|
||||
expected = [ |
||||
%{date: ~D[1970-01-04], number_of_transactions: 0}, |
||||
%{date: ~D[1970-01-03], number_of_transactions: 2}, |
||||
] |
||||
assert {:ok, ^expected} = Historian.compile_records 2 |
||||
|
||||
|
||||
expected = [ |
||||
%{date: ~D[1970-01-04], number_of_transactions: 0}, |
||||
%{date: ~D[1970-01-03], number_of_transactions: 2}, |
||||
%{date: ~D[1970-01-02], number_of_transactions: 1} |
||||
] |
||||
assert {:ok, ^expected} = Historian.compile_records 3 |
||||
end |
||||
end |
||||
|
||||
describe "save_records/1" do |
||||
test "saves transaction history records" do |
||||
records = [ |
||||
%{date: ~D[1970-01-04], number_of_transactions: 3}, |
||||
%{date: ~D[1970-01-03], number_of_transactions: 2}, |
||||
%{date: ~D[1970-01-02], number_of_transactions: 1} |
||||
] |
||||
|
||||
Historian.save_records(records) |
||||
|
||||
query = from( |
||||
stats in TransactionStats, |
||||
select: %{date: stats.date, number_of_transactions: stats.number_of_transactions}, |
||||
order_by: [desc: stats.date]) |
||||
|
||||
results = Repo.all(query) |
||||
|
||||
assert 3 == length(results) |
||||
assert ^records = results |
||||
end |
||||
|
||||
test "overwrites records with the same date without error" do |
||||
records = [%{date: ~D[1970-01-04], number_of_transactions: 3}] |
||||
Historian.save_records(records) |
||||
records = [%{date: ~D[1970-01-04], number_of_transactions: 1}] |
||||
Historian.save_records(records) |
||||
end |
||||
end |
||||
|
||||
@tag capture_log: true |
||||
test "start_link" do |
||||
assert {:ok, _} = Historian.start_link([]) |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
defmodule Explorer.Chain.Transaction.History.TransactionStatsTest do |
||||
use Explorer.DataCase, async: false |
||||
|
||||
alias Explorer.Chain.Transaction.History.TransactionStats |
||||
alias Explorer.Repo |
||||
|
||||
test "by_date_range()" do |
||||
|
||||
some_transaction_stats = [%{date: ~D[2019-07-09], number_of_transactions: 10}, |
||||
%{date: ~D[2019-07-08], number_of_transactions: 20}, |
||||
%{date: ~D[2019-07-07], number_of_transactions: 30}] |
||||
|
||||
Repo.insert_all(TransactionStats, some_transaction_stats) |
||||
|
||||
all3 = TransactionStats.by_date_range(~D[2019-07-07], ~D[2019-07-09]) |
||||
assert 3 = length(all3) |
||||
|
||||
assert ~D[2019-07-09] = Enum.at(all3, 0).date |
||||
assert 10 == Enum.at(all3, 0).number_of_transactions |
||||
assert ~D[2019-07-08] = Enum.at(all3, 1).date |
||||
assert 20 == Enum.at(all3, 1).number_of_transactions |
||||
assert ~D[2019-07-07] = Enum.at(all3, 2).date |
||||
assert 30 == Enum.at(all3, 2).number_of_transactions |
||||
|
||||
|
||||
just2 = TransactionStats.by_date_range(~D[2019-07-08], ~D[2019-07-09]) |
||||
assert 2 == length(just2) |
||||
assert ~D[2019-07-08] = Enum.at(just2, 1).date |
||||
end |
||||
end |
Loading…
Reference in new issue