lib/script.ex

defmodule BitcoinLib.Script do
  @moduledoc """
  Script manipulation module
  """

  alias BitcoinLib.Script.{Analyzer, Encoder, Parser, Runner}
  alias BitcoinLib.Signing.Psbt.CompactInteger

  @doc """
  Transforms a script in the bitstring form into a list of opcodes

  ## Examples
      iex> <<0x76a914fde0a08625e327ba400644ad62d5c571d2eec3de88ac::200>>
      ...> |> BitcoinLib.Script.parse()
      {
        :ok,
        [
          %BitcoinLib.Script.Opcodes.Stack.Dup{},
          %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
          %BitcoinLib.Script.Opcodes.Data{
            value: <<0xfde0a08625e327ba400644ad62d5c571d2eec3de::160>>
          },
          %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
          %BitcoinLib.Script.Opcodes.Crypto.CheckSig{
            script: <<0x76a914fde0a08625e327ba400644ad62d5c571d2eec3de88ac::200>>
          }
        ]
      }
  """
  @spec parse(bitstring()) :: {:ok, list()} | {:error, binary()}
  def parse(script) when is_bitstring(script) do
    script
    |> Parser.parse()
  end

  @doc """
  Transforms a script in the bitstring form into a list of opcodes

  ## Examples
      iex> <<0x76a914fde0a08625e327ba400644ad62d5c571d2eec3de88ac::200>>
      ...> |> BitcoinLib.Script.parse!()
      [
        %BitcoinLib.Script.Opcodes.Stack.Dup{},
        %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
        %BitcoinLib.Script.Opcodes.Data{
          value: <<0xfde0a08625e327ba400644ad62d5c571d2eec3de::160>>
        },
        %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
        %BitcoinLib.Script.Opcodes.Crypto.CheckSig{
          script: <<0x76a914fde0a08625e327ba400644ad62d5c571d2eec3de88ac::200>>
        }
      ]
  """
  @spec parse!(bitstring()) :: list()
  def parse!(script) when is_bitstring(script) do
    parse(script)
    |> elem(1)
  end

  @doc """
  Transforms a list of opcodes into a compact integer reprensenting the size of the script,
  and the script itself

  ## Examples
      iex> [
      ...>   %BitcoinLib.Script.Opcodes.Stack.Dup{},
      ...>   %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
      ...>   %BitcoinLib.Script.Opcodes.Data{
      ...>     value: <<0xfde0a08625e327ba400644ad62d5c571d2eec3de::160>>
      ...>   },
      ...>   %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
      ...>   %BitcoinLib.Script.Opcodes.Crypto.CheckSig{}
      ...> ] |> BitcoinLib.Script.encode
      {<<0x19>>, <<0x76a914fde0a08625e327ba400644ad62d5c571d2eec3de88ac::200>>}
  """
  @spec encode(list()) :: {bitstring(), bitstring()}
  def encode(script) when is_list(script) do
    encoded_script =
      script
      |> Encoder.to_bitstring()

    encoded_script_size =
      byte_size(encoded_script)
      |> CompactInteger.encode()

    {encoded_script_size, encoded_script}
  end

  @spec execute(bitstring(), list()) :: {:ok, boolean()} | {:error, binary()}
  def execute(script, stack) when is_bitstring(script) do
    case Parser.parse(script) do
      {:ok, parsed_script} -> execute(parsed_script, stack)
      {:error, message} -> {:error, message}
    end
  end

  @spec execute(list(), list()) :: {:ok, boolean()} | {:error, binary()}
  def execute(script, stack) when is_list(script) do
    script
    |> Runner.execute(stack)
  end

  @spec validate(bitstring(), list()) :: {:ok, boolean()} | {:error, binary()}
  def validate(script, stack) when is_bitstring(script) do
    case Parser.parse(script) do
      {:ok, parsed_script} -> validate(parsed_script, stack)
      {:error, message} -> {:error, message}
    end
  end

  @spec validate(list(), list()) :: {:ok, boolean()} | {:error, binary()}
  def validate(script, stack) when is_list(script) do
    script
    |> Runner.validate(stack)
  end

  @spec identify(bitstring() | list()) ::
          {:unknown} | {:p2pk | :p2pkh | :p2sh | :p2wsh | :p2wpkh, bitstring()}
  def identify(script) do
    script
    |> Analyzer.identify()
  end
end