Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions lib/cryptopunk/derivation/path.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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
38 changes: 38 additions & 0 deletions test/cryptopunk/derivation/path_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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