Skip to content

Commit

Permalink
Add support for Solana addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
lishawnl committed Apr 13, 2024
1 parent cd86643 commit f9b1d15
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 24 deletions.
69 changes: 46 additions & 23 deletions lib/block_keys/ckd.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,29 @@ defmodule BlockKeys.CKD do
iex> BlockKeys.derive("xprv9s21ZrQH143K3BwM39ubv3fkaHxCN6M4roETEg68Jviq9AnbRjmqVAF4qJHkoLqgSv2bNqYTnRNY9yBQhjNYceZ1NxiDe8WcNJAeWetCvfR", "m/44'/0'/0'")
"xprv9yAYtNSBnu2ojv5BR1b8T39t8oPnbzG8H8CbEHnhBhoXWf441nRA3zDW7PFBL4wkz7CNqtbhr4YVnLuSquiR1QPJgk72jVN8uZ4S2UkuLVk"
"""
def derive(<<"xpub", _rest::binary>>, <<"m/", _path::binary>>),
def derive(key, path, opts \\ [])

def derive(<<"xpub", _rest::binary>>, <<"m/", _path::binary>>, _opts),
do: {:error, "Cannot derive private key from public key"}

def derive(<<"tpub", _rest::binary>>, <<"m/", _path::binary>>),
def derive(<<"tpub", _rest::binary>>, <<"m/", _path::binary>>, _opts),
do: {:error, "Cannot derive private key from public key"}

def derive(<<"xprv", _rest::binary>> = extended_key, <<"M/", path::binary>>) do
def derive(<<"xprv", _rest::binary>> = extended_key, <<"M/", path::binary>>, opts) do
path
|> String.split("/")
|> _derive(extended_key)
|> master_public_key()
|> master_public_key(opts)
end

def derive(<<"tprv", _rest::binary>> = extended_key, <<"M/", path::binary>>) do
def derive(<<"tprv", _rest::binary>> = extended_key, <<"M/", path::binary>>, opts) do
path
|> String.split("/")
|> _derive(extended_key)
|> master_public_key()
|> master_public_key(opts)
end

def derive(extended_key, path) do
def derive(extended_key, path, _opts) do
path
|> String.replace(~r/m\/|M\//, "")
|> String.split("/")
Expand Down Expand Up @@ -116,35 +118,42 @@ defmodule BlockKeys.CKD do
)
end

def master_public_key(<<"xpub", _rest::binary>>),
def master_public_key(key, opts \\ [])

def master_public_key(<<"xpub", _rest::binary>>, _opts),
do: {:error, "Cannot derive master public key from another extended public key"}

def master_public_key(<<"tpub", _rest::binary>>),
def master_public_key(<<"tpub", _rest::binary>>, _opts),
do: {:error, "Cannot derive master public key from another extended public key"}

def master_public_key(key) do
def master_public_key(key, opts) do
decoded_key = Encoding.decode_extended_key(key)

data =
decoded_key
|> slice_prefix()
|> put_uncompressed_parent_pub(%{index: decoded_key.index})
|> put_compressed_parent_pub()
|> put_parent_pub(%{index: decoded_key.index}, opts)

network =
{network, prefix} =
case key do
"xprv" <> _ -> :mainnet
"tprv" <> _ -> :testnet
"xprv" <> _ -> {:mainnet, "xpub"}
"tprv" <> _ -> {:testnet, "tpub"}
end

Encoding.encode_extended_key(
Encoding.public_version_number(network),
decoded_key.depth,
decoded_key.fingerprint,
decoded_key.index,
decoded_key.chain_code,
data.parent_pub_key
)
encoded_public_key =
Encoding.encode_extended_key(
Encoding.public_version_number(network),
decoded_key.depth,
decoded_key.fingerprint,
decoded_key.index,
decoded_key.chain_code,
data.parent_pub_key
)

case opts[:network] do
:solana -> prefix <> encoded_public_key
_ -> encoded_public_key
end
end

defp parse_index(index) do
Expand Down Expand Up @@ -197,6 +206,20 @@ defmodule BlockKeys.CKD do
|> Map.merge(%{decoded_key: decoded_key, version_number: version_number})
end

defp put_parent_pub(%{parent_priv_key: parent_priv_key} = data, index, opts) do
case opts[:network] do
:solana ->
data
|> Map.merge(%{parent_pub_key: Ed25519.derive_public_key(parent_priv_key)})
|> Map.merge(index)

_ ->
data
|> put_uncompressed_parent_pub(index)
|> put_compressed_parent_pub()
end
end

defp put_uncompressed_parent_pub(%{parent_priv_key: parent_priv_key} = data, index) do
data
|> Map.merge(%{parent_pub_key_uncompressed: Crypto.public_key(parent_priv_key)})
Expand Down
53 changes: 53 additions & 0 deletions lib/block_keys/solana/address.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule BlockKeys.Solana.Address do
@moduledoc """
Converts a public extended key into a Solana Address
"""

alias BlockKeys.Base58

def from_xpub(xpub) do
xpub
|> maybe_decode()
|> Base58.encode()
end

def valid_address?(address) when byte_size(address) in 32..44 do
public_key = address |> BlockKeys.Base58.decode() |> :binary.encode_unsigned()
Ed25519.on_curve?(public_key)
end

def valid_address?(_address), do: false

defp maybe_decode(<<"xpub", encoded_key::binary>> = _xpub) do
encoded_key
|> decode_extended_key()
|> Map.fetch!(:key)
end

defp maybe_decode(key), do: key

defp decode_extended_key(key) do
decoded_key =
Base58.decode(key)
|> :binary.encode_unsigned()

<<
version_number::binary-4,
depth::binary-1,
fingerprint::binary-4,
index::binary-4,
chain_code::binary-32,
key::binary-32,
_checksum::binary-4
>> = decoded_key

%{
version_number: version_number,
depth: depth,
fingerprint: fingerprint,
index: index,
chain_code: chain_code,
key: key
}
end
end
13 changes: 13 additions & 0 deletions lib/block_keys/solana/solana.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule BlockKeys.Solana do
@moduledoc """
Helper module to derive and convert to a Solana Address
"""

alias BlockKeys.Solana.Address
alias BlockKeys.CKD

def address(key, path) do
CKD.derive(key, path, network: :solana)
|> Address.from_xpub()
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ defmodule BlockKeys.MixProject do
{:ex_doc, "~> 0.19", only: :dev, runtime: false},
{:ex_keccak, "~> 0.7.3"},
{:ex_secp256k1, "~> 0.7.2"},
{:excoveralls, "~> 0.10", only: :test}
{:excoveralls, "~> 0.10", only: :test},
# Solana keys algorithm
{:ed25519, "~> 1.3"}
]
end
end
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
%{
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
"ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"},
"ex_keccak": {:hex, :ex_keccak, "0.7.3", "33298f97159f6b0acd28f6e96ce5ea975a0f4a19f85fe615b4f4579b88b24d06", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4c5e6d9d5f77b64ab48769a0166a9814180d40ced68ed74ce60a5174ab55b3fc"},
"ex_secp256k1": {:hex, :ex_secp256k1, "0.7.2", "33398c172813b90fab9ab75c12b98d16cfab472c6dcbde832b13c45ce1c01947", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f3b1bf56e6992e28b9d86e3bf741a4aca3e641052eb47d13ae4f5f4d4944bdaf"},
Expand Down
28 changes: 28 additions & 0 deletions test/block_keys/ckd_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ defmodule CKDTest do
assert CKD.derive(xpub, "M/0/0") == CKD.derive(xprv, "M/44'/0'/0'/0/0")
assert CKD.derive(xpub, "M/0/1") == CKD.derive(xprv, "M/44'/0'/0'/0/1")
end

test "derives xpub from master with network keyword" do
path = "M/44'/0'/0'"

xprv =
"xprv9s21ZrQH143K4RdNK1f51Rdeu4XRG8q2cgzeh7ejtzgYpdZcHpNb1MJ2DdBa4iX6NVoZZajsC4gr26mLFaHGBrrtvGkxwhGh6ng8HVZRSeV"

assert CKD.derive(xprv, path, network: :solana) ==
"xpubDeb7pPtgAGEq2eo7ZHPNUvR7xsF4nhK5dBRqA1KD9jZSkSouoZQj6XJ2BVMAMkjHyPeUtUv46Ku4WCWns9uZnUc9BbV5WvFaWNeXbn15bKXNzr"
end
end

describe "master_keys/1" do
Expand Down Expand Up @@ -170,6 +180,24 @@ defmodule CKDTest do
assert testnet_public_key ==
"tpubD6NzVbkrYhZ4XEp55bZ1JFNwu7uUPpqcTaFJSb5nDa2yQq5NKwWNHnrrTrGkK1HxcfswjNaMY1fYx23rohEt6PwKqX8HAeFHTb8oYhXsaYi"
end

test "returns proper public key with network keyword" do
mainnet_private_key =
"xprv9tyUQV64JT5qs3RSTJkXCWKMyUgoQp7F3hA1xzG6ZGu6u6Q9VMNjGr67Lctvy5P8oyaYAL9CAWrUE9i6GoNMKUga5biW6Hx4tws2six3b9c"

mainnet_public_key = CKD.master_public_key(mainnet_private_key, network: :solana)

assert mainnet_public_key ==
"xpubDeb7pNy9ZrJfKYBgSVfGXtVfeJWFPmkZoxhMFwAf1YC34mH74pyFja1T1pu1xHiWSBoCMyKa41TTvd821mnV5D1rom5JzN6KPPiqnaoK6L6Jwn"

testnet_private_key =
"tprv8ZgxMBicQKsPdmnHBwtQtqiqL6PYEVehtGeXA53UoJEaaLpbhYgn7JEzHhuXusKgYiNyZnC71oS5D7s1CDVmsMpoRxfM5e3TZfG9LAbmyuc"

testnet_public_key = CKD.master_public_key(testnet_private_key, network: :solana)

assert testnet_public_key ==
"tpubCk2VbTbSG6A2iAeAru4sKG5Xt2u6eBdMhy9kNt2t5SPqJoTGxF4i5kANbAV4WoQ8Fi7m5oQVMd3TD3novoKLtquy8XKmbe45pMm8WbSRpKgddt"
end
end

describe "child_key_public/2" do
Expand Down
27 changes: 27 additions & 0 deletions test/block_keys/solana/address_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule SolanaAddressTest do
use ExUnit.Case, async: true

alias BlockKeys.Solana.Address
alias BlockKeys.CKD

test "address from mnemonic" do
root_key =
BlockKeys.from_mnemonic(
"nurse grid sister metal flock choice system control about mountain sister rapid hundred render shed chicken print cover tape sister zero bronze tattoo stairs"
)

assert root_key ==
"xprv9s21ZrQH143K35qGjQ6GG1wGHFZP7uCZA1WBdUJA8vBZqESQXQGA4A9d4eve5JqWB5m8YTMcNe8cc7c3FVzDGNcmiabi9WQycbFeEvvJF2D"

assert CKD.derive(root_key, "M/44'/501'/0'/0/0", network: :solana)
|> Address.from_xpub() ==
"4U76rEGDx595M46rWgoA7LwtA821BWCU9CkwG8zbJ6xa"
end

test "check if address is valid" do
valid_address = "4U76rEGDx595M46rWgoA7LwtA821BWCU9CkwG8zbJ6xa"
assert Address.valid_address?(valid_address) == true
invalid_address = "ABCDEFG1234567"
assert Address.valid_address?(invalid_address) == false
end
end

0 comments on commit f9b1d15

Please sign in to comment.