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 identify(bitstring() | list()) :: :unknown | :p2pk | :p2pkh | :p2sh
  def identify(script) do
    script
    |> Analyzer.identify()
  end
end