lib/bsv/out_point.ex

defmodule BSV.OutPoint do
  @moduledoc """
  An OutPoint is a data structure representing a reference to a single
  `t:BSV.TxOut.t/0`.

  An OutPoint consists of a 32 byte `t:BSV.Tx.hash/0` and 4 byte
  `t:BSV.TxOut.vout/0`.

  Conceptually, an OutPoint can be seen as an edge in a graph of Bitcoin
  transactions, linking inputs to previous outputs.
  """
  alias BSV.{Serializable, Tx, TxOut}
  import BSV.Util, only: [decode: 2, encode: 2, reverse_bin: 1]

  @coinbase_hash <<0::256>>
  @coinbase_sequence 0xFFFFFFFF

  defstruct hash: nil, vout: nil

  @typedoc "OutPoint struct"
  @type t() :: %__MODULE__{
    hash: Tx.hash(),
    vout: TxOut.vout()
  }

  @doc """
  Parses the given binary into a `t:BSV.OutPoint.t/0`.

  Returns the result in an `:ok` / `:error` tuple pair.

  ## Options

  The accepted options are:

  * `:encoding` - Optionally decode the binary with either the `:base64` or `:hex` encoding scheme.
  """
  @spec from_binary(binary(), keyword()) :: {:ok, t()} | {:error, term()}
  def from_binary(data, opts \\ []) when is_binary(data) do
    encoding = Keyword.get(opts, :encoding)

    with {:ok, data} <- decode(data, encoding),
         {:ok, outpoint, _rest} <- Serializable.parse(%__MODULE__{}, data)
    do
      {:ok, outpoint}
    end
  end

  @doc """
  Parses the given binary into a `t:BSV.OutPoint.t/0`.

  As `from_binary/2` but returns the result or raises an exception.
  """
  @spec from_binary!(binary(), keyword()) :: t()
  def from_binary!(data, opts \\ []) when is_binary(data) do
    case from_binary(data, opts) do
      {:ok, outpoint} ->
        outpoint

      {:error, error} ->
         raise BSV.DecodeError, error
    end
  end

  @doc """
  Returns the `t:BSV.Tx.txid/0` from the OutPoint.
  """
  @spec get_txid(t()) :: Tx.txid()
  def get_txid(%__MODULE__{hash: hash}),
    do: hash |> reverse_bin() |> encode(:hex)

  @doc """
  Checks if the given OutPoint is a null.

  The first transaction in a block is used to distrbute the block reward to
  miners. These transactions (known as Coinbase transactions) do not spend a
  previous output, and thus the OutPoint is null.
  """
  @spec is_null?(t()) :: boolean()
  def is_null?(%__MODULE__{hash: @coinbase_hash, vout: @coinbase_sequence}), do: true
  def is_null?(%__MODULE__{}), do: false

  @doc """
  Serialises the given `t:BSV.OutPoint.t/0` into a binary.

  ## Options

  The accepted options are:

  * `:encoding` - Optionally encode the binary with either the `:base64` or `:hex` encoding scheme.
  """
  @spec to_binary(t()) :: binary()
  def to_binary(%__MODULE__{} = outpoint, opts \\ []) do
    encoding = Keyword.get(opts, :encoding)

    outpoint
    |> Serializable.serialize()
    |> encode(encoding)
  end


  defimpl Serializable do
    @impl true
    def parse(outpoint, data) do
      with <<hash::bytes-32, vout::little-32, rest::binary>> <- data do
        {:ok, struct(outpoint, [
          hash: hash,
          vout: vout
        ]), rest}
      end
    end

    @impl true
    def serialize(%{hash: hash, vout: vout}),
      do: <<hash::binary, vout::little-32>>
  end
end