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
68 changes: 66 additions & 2 deletions lib/archethic/db/embedded_impl/chain_reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do
end
end

@doc """
Return a transaction chain.
By default, order is chronological (ASC)

Opts:
paging_state :: binary()
order :: :asc | :desc
"""
@spec get_transaction_chain(
address :: binary(),
fields :: list(),
Expand All @@ -129,10 +137,21 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do
fd = File.open!(filepath, [:binary, :read])

{transactions, more?, paging_state} =
process_get_chain(fd, address, fields, opts, db_path)
case Keyword.get(opts, :order, :asc) do
:asc ->
process_get_chain(fd, address, fields, opts, db_path)

:desc ->
process_get_chain_desc(fd, genesis_address, fields, opts, db_path)
end

# we want different metrics for ASC and DESC
:telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{
query: "get_transaction_chain"
query:
case Keyword.get(opts, :order, :asc) do
:asc -> "get_transaction_chain"
:desc -> "get_transaction_chain_reverse"
end
})

{transactions, more?, paging_state}
Expand Down Expand Up @@ -239,6 +258,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 determine the correct paging_state and limit_address
# 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
37 changes: 28 additions & 9 deletions lib/archethic/p2p/message.ex
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,24 @@ defmodule Archethic.P2P.Message do
<<3::8, tx_address::binary>>
end

def encode(%GetTransactionChain{address: tx_address, paging_state: nil}) do
<<4::8, tx_address::binary, 0::8>>
def encode(%GetTransactionChain{address: tx_address, paging_state: nil, order: order}) do
order_bit =
case order do
:asc -> 0
:desc -> 1
end

<<4::8, tx_address::binary, order_bit::1, 0::8>>
end

def encode(%GetTransactionChain{address: tx_address, paging_state: paging_state}) do
<<4::8, tx_address::binary, byte_size(paging_state)::8, paging_state::binary>>
def encode(%GetTransactionChain{address: tx_address, paging_state: paging_state, order: order}) do
order_bit =
case order do
:asc -> 0
:desc -> 1
end

<<4::8, tx_address::binary, order_bit::1, byte_size(paging_state)::8, paging_state::binary>>
end

def encode(%GetUnspentOutputs{address: tx_address, offset: offset}) do
Expand Down Expand Up @@ -690,8 +702,8 @@ defmodule Archethic.P2P.Message do
#
def decode(<<4::8, rest::bitstring>>) do
{address,
<<paging_state_size::8, paging_state::binary-size(paging_state_size), rest::bitstring>>} =
Utils.deserialize_address(rest)
<<order_bit::1, paging_state_size::8, paging_state::binary-size(paging_state_size),
rest::bitstring>>} = Utils.deserialize_address(rest)

paging_state =
case paging_state do
Expand All @@ -702,8 +714,14 @@ defmodule Archethic.P2P.Message do
paging_state
end

order =
case order_bit do
0 -> :asc
1 -> :desc
end

{
%GetTransactionChain{address: address, paging_state: paging_state},
%GetTransactionChain{address: address, paging_state: paging_state, order: order},
rest
}
end
Expand Down Expand Up @@ -1360,13 +1378,14 @@ 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)
|> TransactionChain.get([], paging_state: paging_state, order: order)

# 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
10 changes: 6 additions & 4 deletions lib/archethic/transaction_chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -767,14 +767,14 @@ defmodule Archethic.TransactionChain do
"""
@spec fetch_transaction_chain(list(Node.t()), binary(), binary()) ::
{:ok, list(Transaction.t())} | {:error, :network_issue}
def fetch_transaction_chain(nodes, address, paging_address) do
case do_fetch_transaction_chain(nodes, address, paging_address) do
def fetch_transaction_chain(nodes, address, paging_address, opts \\ []) do
case do_fetch_transaction_chain(nodes, address, paging_address, opts) do
{transactions, _more?, _paging_state} -> {:ok, transactions}
error -> error
end
end

defp do_fetch_transaction_chain(nodes, address, paging_state) do
defp do_fetch_transaction_chain(nodes, address, paging_state, opts \\ []) do
conflict_resolver = fn results ->
results
|> Enum.sort(
Expand All @@ -783,12 +783,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