lib/transaction/input.ex

defmodule BitcoinLib.Transaction.Input do
  @moduledoc """
  Based on https://learnmeabitcoin.com/technical/input
  """

  defstruct [:txid, :vout, :script_sig, :sequence]

  @byte 8

  alias BitcoinLib.Crypto.Bitstring
  alias BitcoinLib.Transaction.Input
  alias BitcoinLib.Script
  alias BitcoinLib.Signing.Psbt.CompactInteger

  @type t :: Input

  @doc """
  Extracts a transaction input from a bitstring

  ## Examples
      iex> <<0x7b1eabe0209b1fe794124575ef807057c77ada2138ae4fa8d6c4de0398a14f3f0000000000ffffffff01f0ca052a010000001976a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac00000000::640>>
      ...> |> BitcoinLib.Transaction.Input.extract_from(false)
      {
        :ok,
        %BitcoinLib.Transaction.Input{
          txid: "3f4fa19803dec4d6a84fae3821da7ac7577080ef75451294e71f9b20e0ab1e7b",
          vout: 0,
          script_sig: [],
          sequence: 4294967295
        },
        <<0x01f0ca052a010000001976a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac00000000::312>>
      }
  """
  @spec extract_from(binary(), bool) :: {:ok, Input.t(), bitstring()} | {:error, binary()}
  def extract_from(
        <<little_txid::bitstring-256, vout::little-32, remaining::bitstring>>,
        is_coinbase? \\ false
      ) do
    txid =
      little_txid
      |> Bitstring.reverse()
      |> Binary.to_hex()

    case extract_script_sig(remaining, is_coinbase?) do
      {:error, message} ->
        {:error, message}

      {:ok, script_sig, remaining} ->
        {sequence, remaining} = extract_sequence(remaining)

        {
          :ok,
          %Input{
            txid: txid,
            vout: vout,
            script_sig: script_sig,
            sequence: sequence
          },
          remaining
        }
    end
  end

  @doc """
  Encodes an input into a bitstring

  ## Examples
      iex> %BitcoinLib.Transaction.Input{
      ...>   sequence: 0xFFFFFFFF,
      ...>   txid: "5e2383defe7efcbdc9fdd6dba55da148b206617bbb49e6bb93fce7bfbb459d44",
      ...>   vout: 1
      ...> } |> BitcoinLib.Transaction.Input.encode()
      <<0x449d45bbbfe7fc93bbe649bb7b6106b248a15da5dbd6fdc9bdfc7efede83235e0100000000ffffffff::328>>
  """
  @spec encode(Input.t()) :: bitstring()
  def encode(%Input{} = input) do
    txid =
      input.txid
      |> Binary.from_hex()
      |> Bitstring.reverse()

    {script_size, script} =
      input.script_sig
      |> format_script_sig()

    <<txid::bitstring-256, input.vout::little-32, script_size::bitstring, script::bitstring,
      input.sequence::little-32>>
  end

  @doc """
  Replaces a potential signature in the input with []

  ## Examples
    iex> %BitcoinLib.Transaction.Input{
    ...>   txid: "77541aeb3c4dac9260b68f74f44c973081a9d4cb2ebe8038b2d70faa201b6bdb",
    ...>   vout: 1,
    ...>   script_sig: [
    ...>     %BitcoinLib.Script.Opcodes.Data{
    ...>       value: <<0x001479091972186C449EB1DED22B78E40D009BDF0089::176>>
    ...>     }
    ...>   ],
    ...>   sequence: 4_294_967_294
    ...> }
    ...> |> BitcoinLib.Transaction.Input.strip_signature
    %BitcoinLib.Transaction.Input{
      txid: "77541aeb3c4dac9260b68f74f44c973081a9d4cb2ebe8038b2d70faa201b6bdb",
      vout: 1,
      script_sig: <<>>,
      sequence: 4_294_967_294
    }
  """
  @spec strip_signature(Input.t()) :: Input.t()
  def strip_signature(%Input{} = input) do
    %{input | script_sig: <<>>}
  end

  defp format_script_sig(nil), do: {<<0::8>>, <<>>}
  defp format_script_sig(script_sig) when is_list(script_sig), do: script_sig |> Script.encode()

  defp format_script_sig(script_sig) when is_binary(script_sig) do
    script_sig_bit_size = byte_size(script_sig)

    {<<script_sig_bit_size::8>>, script_sig}
  end

  defp extract_script_sig(remaining, true = _is_coinbase?) do
    %CompactInteger{value: script_sig_size, remaining: remaining} =
      CompactInteger.extract_from(remaining)

    script_sig_bit_size = script_sig_size * @byte

    <<script_sig::bitstring-size(script_sig_bit_size), remaining::bitstring>> = remaining

    {:ok, script_sig, remaining}
  end

  defp extract_script_sig(remaining, false = _is_coinbase?) do
    %CompactInteger{value: script_sig_size, remaining: remaining} =
      CompactInteger.extract_from(remaining)

    remaining_size = byte_size(remaining)

    if script_sig_size > remaining_size do
      {:error,
       "trying to get a #{script_sig_size} bytes signature in a #{remaining_size} block of data"}
    else
      script_sig_bit_size = script_sig_size * @byte

      <<script_sig::bitstring-size(script_sig_bit_size), remaining::bitstring>> = remaining

      case Script.parse(script_sig) do
        {:ok, script_sig} -> {:ok, script_sig, remaining}
        {:error, message} -> {:error, message}
      end
    end
  end

  defp extract_sequence(remaining) do
    <<sequence::little-32, remaining::bitstring>> = remaining

    {sequence, remaining}
  end
end