lib/script/opcode_manager.ex

defmodule BitcoinLib.Script.OpcodeManager do
  @moduledoc """
  Converts back and forts from script to opcode list
  """

  alias BitcoinLib.Script.Opcodes.{BitwiseLogic, Constants, Crypto, FlowControl, Stack}

  @byte 8

  @zero Constants.Zero.v()
  @one Constants.One.v()
  @two Constants.Two.v()
  @verify FlowControl.Verify.v()
  @return FlowControl.Return.v()
  @dup Stack.Dup.v()
  @equal BitwiseLogic.Equal.v()
  @equal_verify BitwiseLogic.EqualVerify.v()
  @hash160 Crypto.Hash160.v()
  @check_sig Crypto.CheckSig.v()
  @check_sig_verify Crypto.CheckSigVerify.v()
  @check_multi_sig Crypto.CheckMultiSig.v()
  @check_multi_sig_verify Crypto.CheckMultiSigVerify.v()

  alias BitcoinLib.Signing.Psbt.CompactInteger
  alias BitcoinLib.Script.Opcodes.Data

  @doc """
  Encode an opcode into a bitstring
  """
  @spec encode_opcode(any) :: bitstring()
  def encode_opcode(%Constants.Zero{}) do
    Constants.Zero.encode()
  end

  def encode_opcode(%Constants.One{}) do
    Constants.One.encode()
  end

  def encode_opcode(%Constants.Two{}) do
    Constants.Two.encode()
  end

  def encode_opcode(%Stack.Dup{}) do
    Stack.Dup.encode()
  end

  def encode_opcode(%Crypto.Hash160{}) do
    Crypto.Hash160.encode()
  end

  def encode_opcode(%BitwiseLogic.EqualVerify{}) do
    BitwiseLogic.EqualVerify.encode()
  end

  def encode_opcode(%BitwiseLogic.Equal{}) do
    BitwiseLogic.Equal.encode()
  end

  def encode_opcode(%Crypto.CheckSig{}) do
    Crypto.CheckSig.encode()
  end

  def encode_opcode(%Data{} = data) do
    Data.encode(data)
  end

  @doc """
  Extract the opcode on the top of the stack given as an argument
  """
  @spec extract_from_script(bitstring(), bitstring()) ::
          {:empty_script}
          | {:opcode, any(), bitstring()}
          | {:data, bitstring(), bitstring()}
          | {:error, binary(), bitstring()}
  def extract_from_script(<<>>, _whole_script), do: {:empty_script}

  def extract_from_script(<<@zero::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Constants.Zero{}, remaining}
  end

  def extract_from_script(<<@one::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Constants.One{}, remaining}
  end

  def extract_from_script(<<@two::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Constants.Two{}, remaining}
  end

  def extract_from_script(<<@verify::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %FlowControl.Verify{}, remaining}
  end

  def extract_from_script(<<@return::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %FlowControl.Return{}, remaining}
  end

  def extract_from_script(<<@dup::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Stack.Dup{}, remaining}
  end

  def extract_from_script(<<@equal::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %BitwiseLogic.Equal{}, remaining}
  end

  def extract_from_script(<<@equal_verify::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %BitwiseLogic.EqualVerify{}, remaining}
  end

  def extract_from_script(<<@hash160::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Crypto.Hash160{}, remaining}
  end

  def extract_from_script(<<@check_sig::8, remaining::bitstring>>, whole_script) do
    {:opcode, %Crypto.CheckSig{script: whole_script}, remaining}
  end

  def extract_from_script(<<@check_sig_verify::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Crypto.CheckSigVerify{}, remaining}
  end

  def extract_from_script(<<@check_multi_sig::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Crypto.CheckMultiSig{}, remaining}
  end

  def extract_from_script(<<@check_multi_sig_verify::8, remaining::bitstring>>, _whole_script) do
    {:opcode, %Crypto.CheckMultiSigVerify{}, remaining}
  end

  def extract_from_script(<<unknown_opcode::8, remaining::bitstring>> = script, _whole_script) do
    case unknown_opcode do
      opcode when opcode in 0x01..0x4B -> extract_and_return_data(script)
      _ -> {:error, "trying to extract an unknown upcode: #{unknown_opcode}", remaining}
    end
  end

  defp extract_and_return_data(script) do
    %CompactInteger{value: data_length, remaining: remaining} =
      CompactInteger.extract_from(script)

    data_length = data_length * @byte

    <<data::bitstring-size(data_length), remaining::bitstring>> = remaining

    {:data, data, remaining}
  end
end