diff --git a/lib/cryptopunk/derivation/path.ex b/lib/cryptopunk/derivation/path.ex index 09a21a0..964ec3f 100644 --- a/lib/cryptopunk/derivation/path.ex +++ b/lib/cryptopunk/derivation/path.ex @@ -45,8 +45,7 @@ defmodule Cryptopunk.Derivation.Path do @spec parse(String.t()) :: {:error, any()} | {:ok, t()} def parse(string_path) do string_path - |> String.split("/") - |> Enum.map(&String.trim/1) + |> split_string_path() |> do_parse() end @@ -69,16 +68,25 @@ defmodule Cryptopunk.Derivation.Path do ]} end + @spec parse_incomplete_path(String.t()) :: {:ok, raw_path()} | {:error, any()} + def parse_incomplete_path(string_path) do + with [string_type | levels] <- split_string_path(string_path), + {:ok, type} <- parse_type(string_type), + {:ok, parsed_levels} <- parse_levels(levels) do + {:ok, {type, parsed_levels}} + end + end + @spec two_power_31() :: non_neg_integer() def two_power_31, do: @two_power_31 defp do_parse([type, purpose, coin_type, account, change, address_index]) do with {:ok, type} <- parse_type(type), - {:ok, purpose} <- parse_int(purpose, type: :purpose, hardened: true), - {:ok, coin_type} <- parse_int(coin_type, type: :coin_type, hardened: true), - {:ok, account} <- parse_int(account, type: :account, hardened: true), - {:ok, change} <- parse_int(change, type: :change), - {:ok, address_index} <- parse_int(address_index, type: :address_index) do + {:ok, purpose} <- parse_level(purpose, type: :purpose, hardened: true), + {:ok, coin_type} <- parse_level(coin_type, type: :coin_type, hardened: true), + {:ok, account} <- parse_level(account, type: :account, hardened: true), + {:ok, change} <- parse_level(change, type: :change), + {:ok, address_index} <- parse_level(address_index, type: :address_index) do params = [ type: type, purpose: purpose, @@ -102,17 +110,44 @@ defmodule Cryptopunk.Derivation.Path do end end - defp parse_int(int, type: type, hardened: true) do + defp parse_level(int, type: type, hardened: true) do case Integer.parse(int) do {num, "'"} -> {:ok, num} _ -> {:error, {:invalid_level, type}} end end - defp parse_int(int, type: type) do + defp parse_level(int, type: type) do case Integer.parse(int) do {num, ""} -> {:ok, num} _ -> {:error, {:invalid_level, type}} end end + + defp parse_level_with_type(int) do + case Integer.parse(int) do + {num, ""} -> {:ok, num} + {num, "'"} -> {:ok, num + @two_power_31} + result -> {:error, {:invalid_path_level, result}} + end + end + + defp split_string_path(string_path) do + string_path + |> String.split("/") + |> Enum.map(&String.trim/1) + end + + defp parse_levels(levels, acc \\ []) + + defp parse_levels([], acc) do + {:ok, Enum.reverse(acc)} + end + + defp parse_levels([level | tail], acc) do + case parse_level_with_type(level) do + {:ok, parsed_level} -> parse_levels(tail, [parsed_level | acc]) + error -> error + end + end end diff --git a/test/cryptopunk/derivation/path_test.exs b/test/cryptopunk/derivation/path_test.exs index 2015f85..62acb1d 100644 --- a/test/cryptopunk/derivation/path_test.exs +++ b/test/cryptopunk/derivation/path_test.exs @@ -44,4 +44,42 @@ defmodule Cryptopunk.Derivation.PathTest do Path.to_raw_path(path) end end + + describe "parse_incomplete_path/1" do + test "parsed an incomplete path" do + path = "M/100/99'/10'/9/1000'" + + assert {:ok, + { + :public, + [ + 100, + Path.two_power_31() + 99, + Path.two_power_31() + 10, + 9, + Path.two_power_31() + 1000 + ] + }} == + Path.parse_incomplete_path(path) + end + + test "fails to parth path if type is invalid" do + path = "T/99/11" + + assert {:error, {:invalid_level, :type}} == + Path.parse_incomplete_path(path) + end + + test "parse incomplete regular paths" do + paths = [ + {"M/84'/0'/0'", {:public, [2_147_483_732, 2_147_483_648, 2_147_483_648]}}, + {"M/49'/0'/0'", {:public, [2_147_483_697, 2_147_483_648, 2_147_483_648]}}, + {"M/44'/0'/0'", {:public, [2_147_483_692, 2_147_483_648, 2_147_483_648]}} + ] + + Enum.each(paths, fn {str_path, expected} -> + assert {:ok, expected} == Path.parse_incomplete_path(str_path) + end) + end + end end