lib/block/transactions.ex

defmodule BitcoinLib.Block.Transactions do
  @moduledoc """
  Converts a list of bytes into a list of transactions
  """

  alias BitcoinLib.Signing.Psbt.CompactInteger
  alias BitcoinLib.Transaction

  @spec decode(binary()) :: {:ok, list()} | {:error, binary()}
  def decode(transactions_data) do
    %CompactInteger{
      remaining: remaining,
      value: transaction_count
    } = CompactInteger.extract_from(transactions_data)

    with {:ok, transactions} <- scan(transaction_count, remaining) do
      {:ok, transactions}
    else
      {:error, message} -> {:error, message}
    end
  end

  defp scan(expected_transaction_count, remaining) do
    with {:ok, transactions} <- extract_transactions(remaining) do
      validate(expected_transaction_count, transactions)
    else
      {:error, message} -> {:error, message}
    end
  end

  defp extract_transactions(remaining) do
    with {:ok, coinbase, remaining} <- extract_coinbase(remaining) do
      extract_next_transaction([coinbase], remaining)
    else
      {:error, message} -> {:error, message}
    end
  end

  defp extract_coinbase(remaining) do
    Transaction.decode(remaining)
  end

  defp extract_next_transaction(transactions, <<>>), do: {:ok, transactions |> Enum.reverse()}

  defp extract_next_transaction(transactions, remaining) do
    with {:ok, transaction, remaining} <- Transaction.decode(remaining) do
      extract_next_transaction([transaction | transactions], remaining)
    else
      {:error, message} ->
        transaction_count = Enum.count(transactions)
        {:error, "#{transaction_count} transactions in the block, #{message}"}
    end
  end

  defp validate(expected_transaction_count, transactions) do
    case Enum.count(transactions) do
      ^expected_transaction_count ->
        {:ok, transactions}

      transaction_count ->
        {:error, "got #{transaction_count} transactions, expected #{expected_transaction_count}"}
    end
  end
end