Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionChain pagination in the explorer #773

Merged
5 changes: 5 additions & 0 deletions lib/archethic/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ defmodule Archethic.DB do
fields :: list(),
opts :: [paging_state: nil | binary(), after: DateTime.t()]
) :: Enumerable.t()
@callback get_transaction_chain_desc(
binary(),
fields :: list(),
opts :: [paging_state: nil | binary(), after: DateTime.t()]
) :: Enumerable.t()
@callback write_transaction(Transaction.t(), storage_type()) :: :ok
@callback write_beacon_summary(Summary.t()) :: :ok
@callback clear_beacon_summaries() :: :ok
Expand Down
15 changes: 14 additions & 1 deletion lib/archethic/db/embedded_impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ defmodule Archethic.DB.EmbeddedImpl do
end

@doc """
Get a transaction chain
Get a transaction chain - chronological order

The returned values will be splitted according to the pagination state presents in the options
"""
Expand All @@ -200,6 +200,19 @@ defmodule Archethic.DB.EmbeddedImpl do
ChainReader.get_transaction_chain(address, fields, opts, db_path())
end

@doc """
Get a transaction chain - anti-chronological order

The returned values will be splitted according to the pagination state presents in the options
"""
@spec get_transaction_chain_desc(address :: binary(), fields :: list(), opts :: list()) ::
{transactions_by_page :: list(Transaction.t()), more? :: boolean(),
paging_state :: nil | binary()}
def get_transaction_chain_desc(address, fields \\ [], opts \\ [])
when is_binary(address) and is_list(fields) and is_list(opts) do
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
ChainReader.get_transaction_chain_desc(address, fields, opts, db_path())
end

@doc """
Return the size of a transaction chain
"""
Expand Down
75 changes: 75 additions & 0 deletions lib/archethic/db/embedded_impl/chain_reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do
end
end

@spec get_transaction_chain_desc(
address :: binary(),
fields :: list(),
opts :: list(),
db_path :: String.t()
) ::
{transactions_by_page :: list(Transaction.t()), more? :: boolean(),
paging_state :: nil | binary()}
def get_transaction_chain_desc(address, fields, opts, db_path) do
start = System.monotonic_time()

case ChainIndex.get_tx_entry(address, db_path) do
{:error, :not_exists} ->
{[], false, ""}

{:ok, %{genesis_address: genesis_address}} ->
filepath = ChainWriter.chain_path(db_path, genesis_address)
fd = File.open!(filepath, [:binary, :read])

{transactions, more?, paging_state} =
process_get_chain_desc(fd, genesis_address, fields, opts, db_path)

:telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{
query: "get_transaction_chain_desc"
})

{transactions, more?, paging_state}
end
end

@doc """
List all the transactions in io storage
"""
Expand Down Expand Up @@ -239,6 +269,51 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do
{transactions, more?, paging_state}
end

# in order to read the file sequentially in DESC (faster than random access)
# we have to determined the correct paging_state and limit_address
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
# then we can use the process_get_chain that does the ASC read
defp process_get_chain_desc(fd, genesis_address, fields, opts, db_path) do
all_addresses_asc =
ChainIndex.list_chain_addresses(genesis_address, db_path)
|> Enum.map(&elem(&1, 0))

{limit_address, paging_state, more?, new_paging_state} =
case Keyword.get(opts, :paging_state) do
nil ->
chain_length = Enum.count(all_addresses_asc)

if chain_length <= @page_size do
{nil, nil, false, ""}
else
idx = chain_length - 1 - @page_size

{nil, all_addresses_asc |> Enum.at(idx), true, all_addresses_asc |> Enum.at(idx + 1)}
end

paging_state ->
paging_state_idx =
all_addresses_asc
|> Enum.find_index(&(&1 == paging_state))

limit_address = Enum.at(all_addresses_asc, paging_state_idx - 1)

if paging_state_idx < @page_size do
{limit_address, nil, false, ""}
else
idx = paging_state_idx - 1 - @page_size

{limit_address, all_addresses_asc |> Enum.at(idx), true,
all_addresses_asc |> Enum.at(idx + 1)}
end
end

# call the ASC function and ignore the more? and paging_state
{transactions, _more?, _paging_state} =
process_get_chain(fd, limit_address, fields, [paging_state: paging_state], db_path)

{Enum.reverse(transactions), more?, new_paging_state}
end

defp read_transaction(fd, fields, limit, position, acc \\ %{})

defp read_transaction(_fd, _fields, limit, position, acc) when limit == position, do: acc
Expand Down
14 changes: 11 additions & 3 deletions lib/archethic/p2p/message.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1360,13 +1360,21 @@ defmodule Archethic.P2P.Message do
def process(
%GetTransactionChain{
address: tx_address,
paging_state: paging_state
paging_state: paging_state,
order: order
},
_
) do
{chain, more?, paging_state} =
tx_address
|> TransactionChain.get([], paging_state: paging_state)
case order do
:asc ->
tx_address
|> TransactionChain.get([], paging_state: paging_state)
bchamagne marked this conversation as resolved.
Show resolved Hide resolved

:desc ->
tx_address
|> TransactionChain.get_desc([], paging_state: paging_state)
end

# empty list for fields/cols to be processed
# new_page_state contains binary offset for the next page
Expand Down
5 changes: 3 additions & 2 deletions lib/archethic/p2p/message/get_transaction_chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ defmodule Archethic.P2P.Message.GetTransactionChain do
Represents a message to request an entire transaction chain
"""
@enforce_keys [:address]
defstruct [:address, :paging_state]
defstruct [:address, :paging_state, order: :asc]

alias Archethic.Crypto

@type t :: %__MODULE__{
address: Crypto.versioned_hash(),
paging_state: nil | binary()
paging_state: nil | binary(),
order: :desc | :asc
}
end
28 changes: 25 additions & 3 deletions lib/archethic/transaction_chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ defmodule Archethic.TransactionChain do
Enumerable.t() | {list(Transaction.t()), boolean(), binary()}
defdelegate get(address, fields \\ [], opts \\ []), to: DB, as: :get_transaction_chain

@doc """
Retrieve an entire chain from the last transaction
The returned list is ordered anti-chronologically.
"""
@spec get_desc(binary(), list()) ::
Enumerable.t() | {list(Transaction.t()), boolean(), binary()}
defdelegate get_desc(address, fields \\ [], opts \\ []), to: DB, as: :get_transaction_chain_desc

@doc """
Persist only one transaction
"""
Expand Down Expand Up @@ -763,7 +771,7 @@ defmodule Archethic.TransactionChain do
end

@doc """
Get 10 transactions in a chain after a paging address
Get 10 transactions in a chain after a paging address - chronological order
"""
@spec fetch_transaction_chain(list(Node.t()), binary(), binary()) ::
{:ok, list(Transaction.t())} | {:error, :network_issue}
Expand All @@ -774,7 +782,19 @@ defmodule Archethic.TransactionChain do
end
end

defp do_fetch_transaction_chain(nodes, address, paging_state) do
@doc """
Get 10 transactions in a chain before a paging address - anti-chronological order
"""
@spec fetch_transaction_chain_desc(list(Node.t()), binary(), nil | binary()) ::
{:ok, list(Transaction.t())} | {:error, :network_issue}
def fetch_transaction_chain_desc(nodes, address, paging_address) do
case do_fetch_transaction_chain(nodes, address, paging_address, order: :desc) do
{transactions, _more?, _paging_state} -> {:ok, transactions}
error -> error
end
end

defp do_fetch_transaction_chain(nodes, address, paging_state, opts \\ []) do
conflict_resolver = fn results ->
results
|> Enum.sort(
Expand All @@ -783,12 +803,14 @@ defmodule Archethic.TransactionChain do
|> List.first()
end

order = Keyword.get(opts, :order, :asc)

# We got transactions by batch of 10 transactions
timeout = Message.get_max_timeout() + Message.get_max_timeout() * 10

case P2P.quorum_read(
nodes,
%GetTransactionChain{address: address, paging_state: paging_state},
%GetTransactionChain{address: address, paging_state: paging_state, order: order},
conflict_resolver,
timeout
) do
Expand Down
123 changes: 0 additions & 123 deletions lib/archethic_web/controllers/explorer_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ defmodule ArchethicWeb.ExplorerController do

use ArchethicWeb, :controller

alias Archethic.OracleChain
alias Archethic.Crypto
alias Archethic.TransactionChain.Transaction

Expand All @@ -23,126 +22,4 @@ defmodule ArchethicWeb.ExplorerController do
render(conn, "404.html")
end
end

def chain(conn, _params = %{"address" => address, "last" => "on"}) do
with {:ok, addr} <- Base.decode16(address, case: :mixed),
true <- Crypto.valid_address?(addr),
{:ok, %Transaction{address: last_address}} <- Archethic.get_last_transaction(addr),
{:ok, chain} <- Archethic.get_transaction_chain(last_address),
{:ok, %{uco: uco_balance}} <- Archethic.get_balance(addr),
uco_price <- DateTime.utc_now() |> OracleChain.get_uco_price() do
render(conn, "chain.html",
transaction_chain: List.flatten(chain),
chain_size: Enum.count(chain),
address: addr,
uco_balance: uco_balance,
last_checked?: true,
uco_price: uco_price
)
else
:error ->
render(conn, "chain.html",
transaction_chain: [],
chain_size: 0,
address: "",
last_checked?: true,
error: :invalid_address,
uco_balance: 0,
uco_price: [eur: 0.05, usd: 0.07]
)

{:error, _} ->
render(conn, "chain.html",
transaction_chain: [],
chain_size: 0,
address: "",
last_checked?: true,
error: :network_issue,
uco_balance: 0,
uco_price: [eur: 0.05, usd: 0.07]
)

false ->
render(conn, "chain.html",
transaction_chain: [],
chain_size: 0,
address: "",
last_checked?: true,
error: :invalid_address,
uco_balance: 0,
uco_price: [eur: 0.05, usd: 0.07]
)

_ ->
render(conn, "chain.html",
transaction_chain: [],
chain_size: 0,
address: Base.decode16!(address, case: :mixed),
last_checked?: true,
uco_balance: 0,
uco_price: [eur: 0.05, usd: 0.07]
)
end
end

def chain(conn, _params = %{"address" => address}) do
with {:ok, addr} <- Base.decode16(address, case: :mixed),
true <- Crypto.valid_address?(addr),
{:ok, chain} <- Archethic.get_transaction_chain(addr),
{:ok, %{uco: uco_balance}} <- Archethic.get_balance(addr),
uco_price <- DateTime.utc_now() |> OracleChain.get_uco_price() do
render(conn, "chain.html",
transaction_chain: List.flatten(chain),
address: addr,
chain_size: Enum.count(chain),
uco_balance: uco_balance,
last_checked?: false,
uco_price: uco_price
)
else
:error ->
render(conn, "chain.html",
transaction_chain: [],
address: "",
chain_size: 0,
uco_balance: 0,
last_checked?: false,
error: :invalid_address,
uco_price: [eur: 0.05, usd: 0.07]
)

false ->
render(conn, "chain.html",
transaction_chain: [],
address: "",
chain_size: 0,
uco_balance: 0,
last_checked?: false,
error: :invalid_address,
uco_price: [eur: 0.05, usd: 0.07]
)

{:error, _} ->
render(conn, "chain.html",
transaction_chain: [],
address: "",
chain_size: 0,
uco_balance: 0,
last_checked?: false,
error: :network_issue,
uco_price: [eur: 0.05, usd: 0.07]
)
end
end

def chain(conn, _params) do
render(conn, "chain.html",
transaction_chain: [],
address: "",
chain_size: 0,
last_checked?: false,
uco_balance: 0,
uco_price: [eur: 0.05, usd: 0.07]
)
end
end
3 changes: 1 addition & 2 deletions lib/archethic_web/explorer_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ defmodule ArchethicWeb.ExplorerRouter do

live("/transaction/:address", TransactionDetailsLive)

get("/chain", ExplorerController, :chain)

live("/chain", TransactionChainLive)
live("/chain/oracle", OracleChainLive)
live("/chain/beacon", BeaconChainLive)
live("/chain/rewards", RewardChainLive)
Expand Down
Loading