lib/script/parser.ex

defmodule BitcoinLib.Script.Parser do
  @moduledoc """
  Converts scripts into opcode lists
  """

  alias BitcoinLib.Script.{OpcodeManager}
  alias BitcoinLib.Script.Opcodes.Data

  @doc """
  Takes a binary script and converts it into an opcode list

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

  defp iterate(%{error: message}), do: {:error, message}

  defp iterate(%{script: <<>>, opcodes: opcodes}), do: {:ok, Enum.reverse(opcodes)}

  defp iterate(%{script: script, opcodes: opcodes, whole_script: whole_script}) do
    %{script: script, opcodes: opcodes, whole_script: whole_script}
    |> extract_next_opcode()
    |> iterate()
  end

  defp extract_next_opcode(%{script: script, opcodes: opcodes, whole_script: whole_script} = map) do
    case OpcodeManager.extract_from_script(script, whole_script) do
      {:empty_script} ->
        map

      {:error, message, remaining} ->
        map
        |> Map.put(:error, message)
        |> Map.put(:script, remaining)

      {:opcode, opcode, remaining} ->
        %{map | script: remaining, opcodes: [opcode | opcodes]}

      {:data, data, remaining} ->
        opcode = %Data{value: data}

        %{map | script: remaining, opcodes: [opcode | opcodes]}
    end
  end
end