lib/key/hd/derivation_path.ex

defmodule BitcoinLib.Key.HD.DerivationPath do
  @moduledoc """
  Can parse derivation paths string format into a native format

  m / purpose' / coin_type' / account' / change / address_index

  Inspired by

    https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
    https://learnmeabitcoin.com/technical/derivation-paths
  """

  @enforce_keys [:type]
  defstruct [:type, :purpose, :coin_type, :account, :change, :address_index]

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

  @hardened :math.pow(2, 31) |> trunc

  @doc """
  Transforms a derivation path string into an elixir structure

  ## Examples
      iex> "m / 44' / 1' / 2' / 1 / 4"
      ...> |> BitcoinLib.Key.HD.DerivationPath.parse()
      { :ok,
        %BitcoinLib.Key.HD.DerivationPath{
          type: :private,
          purpose: :bip44,
          coin_type: :bitcoin_testnet,
          account: 2,
          change: :change_chain,
          address_index: 4
        }
      }
  """
  @spec parse(binary()) :: {:ok, %BitcoinLib.Key.HD.DerivationPath{}}
  def parse(derivation_path) do
    derivation_path
    |> Parser.parse_valid_derivation_path()
  end

  @doc """
  Retruns a %DerivationPath from a set of parameters, with these values potentially missing:
  coin_type, account, change, address_index

  ## Examples
      iex> BitcoinLib.Key.HD.DerivationPath.from_values("M", 0x80000054, 0x80000000, 0x80000000, 0, 0)
      %BitcoinLib.Key.HD.DerivationPath{
        type: :public,
        purpose: :bip84,
        coin_type: :bitcoin,
        account: 0,
        change: :receiving_chain,
        address_index: 0
      }
  """
  @spec from_values(
          binary(),
          integer(),
          integer() | nil,
          integer() | nil,
          integer() | nil,
          integer() | nil
        ) ::
          %DerivationPath{}
  def from_values(
        type,
        purpose,
        coin_type \\ nil,
        account \\ nil,
        change \\ nil,
        address_index \\ nil
      ) do
    %DerivationPath{
      type: parse_type(type),
      purpose: parse_purpose(purpose),
      coin_type: parse_coin_type(coin_type),
      account: convert_hardened_account(account),
      change: parse_change(change),
      address_index: address_index
    }
  end

  @doc """
  Turns a list of path values into a %DerivationPath{}

  ## Examples
      iex> ["m", 0x80000054, 0x80000000, 0x80000005]
      ...> |> BitcoinLib.Key.HD.DerivationPath.from_list
      %BitcoinLib.Key.HD.DerivationPath{
        type: :private,
        purpose: :bip84,
        coin_type: :bitcoin,
        account: 5
      }
  """
  @spec from_list(list()) :: %DerivationPath{}
  def from_list(values_list) do
    %PathValues{
      type: type,
      purpose: purpose,
      coin_type: coin_type,
      account: account,
      change: change,
      address_index: address_index
    } = PathValues.from_list(values_list)

    from_values(type, purpose, coin_type, account, change, address_index)
  end

  defp parse_type(type) do
    Type.get_atom(type)
  end

  defp parse_purpose(purpose) when is_integer(purpose) do
    Purpose.get_atom(purpose - @hardened)
  end

  defp parse_coin_type(nil), do: nil

  defp parse_coin_type(coin_type) do
    CoinType.get_atom(coin_type - @hardened)
  end

  defp convert_hardened_account(nil), do: nil

  defp convert_hardened_account(hardened_account) when is_integer(hardened_account) do
    hardened_account - @hardened
  end

  defp parse_change(nil), do: nil

  defp parse_change(change_chain) do
    Change.get_atom(change_chain)
  end
end