lib/key/hd/derivation_path/parser.ex

defmodule BitcoinLib.Key.HD.DerivationPath.Parser do
  @moduledoc """
  Single purpose module that aims to simplify DerivationPath by isolating string parsing business
  logic
  """

  alias BitcoinLib.Key.HD.DerivationPath
  alias BitcoinLib.Key.HD.DerivationPath.Parser.{Type, Purpose, CoinType, Account, Change, Index}

  @doc """
  Single purpose function that's being called by DerivationPath.parse/1, returning a DerivationPath
  out of a string if the tuple starts by :ok

  ## Examples
      iex> "m/44'/0'/0'/1/0"
      ...> |> BitcoinLib.Key.HD.DerivationPath.Parser.parse_valid_derivation_path
      {
        :ok,
        %BitcoinLib.Key.HD.DerivationPath{
          type: :private,
          purpose: :bip44,
          coin_type: :bitcoin,
          account: 0,
          change: :change_chain,
          address_index: 0
        }
      }
  """
  @spec parse_valid_derivation_path(binary()) ::
          {:ok, %DerivationPath{}} | {:error, binary()}
  def parse_valid_derivation_path(derivation_path) do
    with {:ok, derivation_path} <- validate(derivation_path),
         tokens <- split_path(derivation_path),
         {:ok, type, tokens} <- Type.extract(tokens),
         {:ok, purpose, tokens} <- Purpose.extract(tokens),
         {:ok, coin_type, tokens} <- CoinType.extract(tokens),
         {:ok, account, tokens} <- Account.extract(tokens),
         {:ok, change, tokens} <- Change.extract(tokens),
         {:ok, index} <- Index.extract(tokens) do
      {
        :ok,
        %DerivationPath{
          type: type,
          purpose: purpose,
          coin_type: coin_type,
          account: account,
          change: change,
          address_index: index
        }
      }
    else
      {:error, message} -> {:error, message}
    end
  end

  defp validate(derivation_path) do
    trimmed_path =
      derivation_path
      |> String.replace(" ", "")

    case Regex.match?(~r/^(m|M)((\/(\d+\'?)*){0,5})$/, trimmed_path) do
      true -> {:ok, trimmed_path}
      false -> {:error, "Invalid derivation path"}
    end
  end

  defp split_path(derivation_path) do
    String.split(derivation_path, "/")
  end
end