lib/bsv/tx.ex

defmodule BSV.Tx do
  @moduledoc """
  A Tx is a data structure representing a Bitcoin transaction.

  A Tx consists of a version number, a list of inputs, list of outputs, and a
  locktime value.

  A Bitcoin transaction is used to transfer custody of Bitcoins. It can also be
  used for smart contracts, recording and timestamping data, and many other
  functionalities.

  The Tx module is used for parsing and serialising transaction data. Use the
  `BSV.TxBuilder` module for building transactions.
  """
  alias BSV.{Hash, OutPoint, Serializable, TxIn, TxOut, VarInt}
  import BSV.Util, only: [decode: 2, encode: 2, reverse_bin: 1]

  defstruct version: 1, inputs: [], outputs: [], lock_time: 0

  @typedoc "Tx struct"
  @type t() :: %__MODULE__{
    version: non_neg_integer(),
    inputs: list(TxIn.t()),
    outputs: list(TxOut.t()),
    lock_time: non_neg_integer()
  }

  @typedoc """
  Tx hash

  Result of hashing the transaction data through the SHA-256 algorithm twice.
  """
  @type hash() :: <<_::256>>

  @typedoc """
  TXID

  Result of reversing and hex-encoding the `t:BSV.Tx.hash/0`.
  """
  @type txid() :: String.t()

  @doc """
  Adds the given `t:BSV.TxIn.t/0` to the transaction.
  """
  @spec add_input(t(), TxIn.t()) :: t()
  def add_input(%__MODULE__{} = tx, %TxIn{} = txin),
    do: update_in(tx.inputs, & &1 ++ [txin])

  @doc """
  Adds the given `t:BSV.TxOut.t/0` to the transaction.
  """
  @spec add_output(t(), TxOut.t()) :: t()
  def add_output(%__MODULE__{} = tx, %TxOut{} = txout),
    do: update_in(tx.outputs, & &1 ++ [txout])

  @doc """
  Returns true if the given `t:BSV.Tx.t/0` is a coinbase transaction (the first
  transaction in a block, containing the miner block reward).
  """
  @spec is_coinbase?(t()) :: boolean()
  def is_coinbase?(%__MODULE__{inputs: [txin]}),
    do: OutPoint.is_null?(txin.outpoint)

  def is_coinbase?(%__MODULE__{}), do: false

  @doc """
  Parses the given binary into a `t:BSV.Tx.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, tx, _rest} <- Serializable.parse(%__MODULE__{}, data)
    do
      {:ok, tx}
    end
  end

  @doc """
  Parses the given binary into a `t:BSV.Tx.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, tx} ->
        tx

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

  @doc """
  Returns the `t:BSV.Tx.hash/0` of the given transaction.
  """
  @spec get_hash(t()) :: hash()
  def get_hash(%__MODULE__{} = tx) do
    tx
    |> to_binary()
    |> Hash.sha256_sha256()
  end

  @doc """
  Returns the number of bytes of the given `t:BSV.Tx.t/0`.
  """
  @spec get_size(t()) :: non_neg_integer()
  def get_size(%__MODULE__{} = tx),
    do: to_binary(tx) |> byte_size()

  @doc """
  Returns the `t:BSV.Tx.txid/0` of the given transaction.
  """
  @spec get_txid(t()) :: txid()
  def get_txid(%__MODULE__{} = tx) do
    tx
    |> get_hash()
    |> reverse_bin()
    |> encode(:hex)
  end

  @doc """
  Serialises the given `t:BSV.TxIn.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__{} = tx, opts \\ []) do
    encoding = Keyword.get(opts, :encoding)

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

  defimpl Serializable do
    @impl true
    def parse(tx, data) do
      with <<version::little-32, data::binary>> <- data,
           {:ok, inputs, data} <- VarInt.parse_items(data, TxIn),
           {:ok, outputs, data} <- VarInt.parse_items(data, TxOut),
           <<lock_time::little-32, rest::binary>> = data
      do
        {:ok, struct(tx, [
          version: version,
          inputs: inputs,
          outputs: outputs,
          lock_time: lock_time
        ]), rest}
      end
    end

    @impl true
    def serialize(%{version: version, inputs: inputs, outputs: outputs, lock_time: lock_time}) do
      inputs_data = Enum.reduce(inputs, VarInt.encode(length(inputs)), fn input, data ->
        data <> Serializable.serialize(input)
      end)
      outputs_data = Enum.reduce(outputs, VarInt.encode(length(outputs)), fn output, data ->
        data <> Serializable.serialize(output)
      end)

      <<
        version::little-32,
        inputs_data::binary,
        outputs_data::binary,
        lock_time::little-32
      >>
    end
  end

end