lib/bsv/contract/helpers.ex

defmodule BSV.Contract.Helpers do
  @moduledoc """
  Base helper module containing helper functions for use in `BSV.Contract`
  modules.

  Using `BSV.Contract.Helpers` will import itself and all related helper modules
  into your context.

      use BSV.Contract.Helpers

  Alternative, helper modules can be imported individually.

      import BSV.Contract.Helpers
      import BSV.Contract.OpCodeHelpers
      import BSV.Contract.VarIntHelpers
  """
  alias BSV.{Contract, PrivKey, Sig, UTXO}
  import BSV.Contract.OpCodeHelpers

  defmacro __using__(_) do
    quote do
      import BSV.Contract.Helpers
      import BSV.Contract.OpCodeHelpers
      import BSV.Contract.VarIntHelpers
    end
  end

  @doc """
  Assuming the top stack element is an unsigned integer, casts it to a
  `BSV.ScriptNum.t()` encoded number.
  """
  @spec decode_uint(Contract.t(), atom()) :: Contract.t()
  def decode_uint(contract, endianess \\ :little)
  def decode_uint(%Contract{} = contract, endianess)
    when endianess in [:le, :little]
  do
    contract
    |> push(<<0>>)
    |> op_cat()
    |> op_bin2num()
  end

  # TODO encode big endian decoding
  def decode_uint(%Contract{} = _contract, endianess)
    when endianess in [:be, :big],
    do: raise "Big endian decoding not implemented yet"

  @doc """
  Iterates over the given enumerable, invoking the `handle_each` function on
  each.

  ## Example

      contract
      |> each(["foo", "bar", "baz"], fn el, c ->
        c
        |> push(el)
        |> op_cat()
      end)
  """
  @spec each(
    Contract.t(),
    Enum.t(),
    (Enum.element(), Contract.t() -> Contract.t())
  ) :: Contract.t()
  def each(%Contract{} = contract, enum, handle_each)
    when is_function(handle_each),
    do: Enum.reduce(enum, contract, handle_each)

  @doc """
  Pushes the given data onto the script. If a list of data elements is given,
  each will be pushed to the script as seperate pushdata elements.
  """
  @spec push(
      Contract.t(),
      atom() | binary() | integer() |
      list(atom() | binary() | integer())
    ) ::Contract.t()
  def push(%Contract{} = contract, data) when is_list(data),
    do: each(contract, data, &push(&2, &1))

  def push(%Contract{} = contract, data),
    do: Contract.script_push(contract, data)

  @doc """
  Iterates the given number of times, invoking the `handle_each` function on
  each iteration.

  ## Example

      contract
      |> repeat(5, fn _i, c ->
        c
        |> op_5()
        |> op_add()
      end)
  """
  @spec repeat(
    Contract.t(),
    non_neg_integer(),
    (non_neg_integer(), Contract.t() -> Contract.t())
  ) :: Contract.t()
  def repeat(%Contract{} = contract, loops, handle_each)
    when is_integer(loops) and loops > 0
    and is_function(handle_each),
    do: Enum.reduce(0..loops-1, contract, handle_each)

  @doc """
  Reverses the top item on the stack.

  This helper function pushes op codes on to the script that will reverse a
  binary of the given length.
  """
  @spec reverse(Contract.t(), integer()) :: Contract.t()
  def reverse(%Contract{} = contract, length)
    when is_integer(length) and length > 1
  do
    contract
    |> repeat(length-1, fn _i, contract ->
      contract
      |> op_1()
      |> op_split()
    end)
    |> repeat(length-1, fn _i, contract ->
      contract
      |> op_swap()
      |> op_cat()
    end)
  end

  @doc """
  Signs the transaction [`context`](`t:BSV.Contract.ctx/0`) and pushes the
  signature onto the script.

  A list of private keys can be given, in which case each is used to sign and
  multiple signatures are added.

  If no context is available in the [`contract`](`t:BSV.Contract.t/0`), then
  71 bytes of zeros are pushed onto the script for each private key.
  """
  @spec sig(Contract.t(), PrivKey.t() | list(PrivKey.t())) :: Contract.t()
  def sig(%Contract{} = contract, privkey) when is_list(privkey),
    do: each(contract, privkey, &sig(&2, &1))

  def sig(
    %Contract{ctx: {tx, index}, opts: opts, subject: %UTXO{txout: txout}} = contract,
    %PrivKey{} = privkey
  ) do
    signature = Sig.sign(tx, index, txout, privkey, opts)
    Contract.script_push(contract, signature)
  end

  def sig(%Contract{ctx: nil} = contract, %PrivKey{} = _privkey),
    do: Contract.script_push(contract, <<0::568>>)

  @doc """
  Extracts the bytes from top item on the stack, starting on the given `start`
  index for `length` bytes. The stack item is replaced with the sliced value.

  Binaries are zero indexed. If `start` is a negative integer, then the start
  index is counted from the end.
  """
  @spec slice(Contract.t(), integer(), non_neg_integer()) :: Contract.t()
  def slice(%Contract{} = contract, start, length) when start < 0 do
    contract
    |> op_size()
    |> push(start * -1)
    |> op_sub()
    |> op_split()
    |> op_nip()
    |> slice(0, length)
  end

  def slice(%Contract{} = contract, start, length) when start > 0 do
    contract
    |> trim(start)
    |> slice(0, length)
  end

  def slice(%Contract{} = contract, 0, length) do
    contract
    |> push(length)
    |> op_split()
    |> op_drop()
  end

  @doc """
  Trims the given number of leading or trailing bytes from the top item on the
  stack. The stack item is replaced with the trimmed value.

  When the given `length` is a positive integer, leading bytes are trimmed. When
  a negative integer is given, trailing bytes are trimmed.
  """
  @spec trim(Contract.t(), integer()) :: Contract.t()
  def trim(%Contract{} = contract, length) when length > 0 do
    contract
    |> push(length)
    |> op_split()
    |> op_nip()
  end

  def trim(%Contract{} = contract, length) when length < 0 do
    contract
    |> op_size()
    |> push(length * -1)
    |> op_sub()
    |> op_split()
    |> op_drop()
  end

  def trim(%Contract{} = contract, 0), do: contract

end