From f9b1d150a88a03eb7ddf1bd428ebec1fe90a08d4 Mon Sep 17 00:00:00 2001 From: lishawnl Date: Fri, 12 Apr 2024 14:55:46 -0400 Subject: [PATCH 1/3] Add support for Solana addresses --- lib/block_keys/ckd.ex | 69 ++++++++++++++++--------- lib/block_keys/solana/address.ex | 53 +++++++++++++++++++ lib/block_keys/solana/solana.ex | 13 +++++ mix.exs | 4 +- mix.lock | 1 + test/block_keys/ckd_test.exs | 28 ++++++++++ test/block_keys/solana/address_test.exs | 27 ++++++++++ 7 files changed, 171 insertions(+), 24 deletions(-) create mode 100644 lib/block_keys/solana/address.ex create mode 100644 lib/block_keys/solana/solana.ex create mode 100644 test/block_keys/solana/address_test.exs diff --git a/lib/block_keys/ckd.ex b/lib/block_keys/ckd.ex index 6daaf59..295fba2 100644 --- a/lib/block_keys/ckd.ex +++ b/lib/block_keys/ckd.ex @@ -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("/") @@ -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 @@ -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)}) diff --git a/lib/block_keys/solana/address.ex b/lib/block_keys/solana/address.ex new file mode 100644 index 0000000..8c00aa1 --- /dev/null +++ b/lib/block_keys/solana/address.ex @@ -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 diff --git a/lib/block_keys/solana/solana.ex b/lib/block_keys/solana/solana.ex new file mode 100644 index 0000000..9d8c6d0 --- /dev/null +++ b/lib/block_keys/solana/solana.ex @@ -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 diff --git a/mix.exs b/mix.exs index 9db8511..c8ccba1 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index a739b24..a1db8fa 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/block_keys/ckd_test.exs b/test/block_keys/ckd_test.exs index 725cdc2..add0904 100644 --- a/test/block_keys/ckd_test.exs +++ b/test/block_keys/ckd_test.exs @@ -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 @@ -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 diff --git a/test/block_keys/solana/address_test.exs b/test/block_keys/solana/address_test.exs new file mode 100644 index 0000000..bc4fdbe --- /dev/null +++ b/test/block_keys/solana/address_test.exs @@ -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 From 077fd16c49faff0c0abb884a140f96584e13e6b3 Mon Sep 17 00:00:00 2001 From: lishawnl Date: Fri, 12 Apr 2024 21:46:08 -0400 Subject: [PATCH 2/3] Update doc and upgrade version --- CHANGELOG.md | 4 ++++ README.md | 18 ++++++++++++++++-- lib/block_keys/solana/address.ex | 1 + mix.exs | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95850ff..cce42dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.0.2 + + * Adds support for Solana addresses + ## v1.0.1 * Brings in Ethereum EIP-55 checksum addresses, credit goes to @wchenNL diff --git a/README.md b/README.md index de73b3e..9ad7429 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.com/AgileAlpha/block_keys.svg?branch=master)](https://travis-ci.com/AgileAlpha/block_keys) BlockKeys is an Elixir implementation of BIP44 Multi-Account Hierarchy for Deterministic Wallets. -Currently it supports Bitcoin and Ethereum but will be extended to support a [large number of coin types](https://github.com/satoshilabs/slips/blob/master/slip-0044.md) of coins in the future. +Currently it supports Bitcoin, Ethereum and Solana, but will be extended to support a [large number of coin types](https://github.com/satoshilabs/slips/blob/master/slip-0044.md) of coins in the future. For low level details check the [Wiki](https://github.com/AgileAlpha/block_keys/wiki). @@ -14,7 +14,7 @@ Add the dependency to your `mix.exs`: ``` def deps do [ - {:block_keys, "~> 1.0.1"} + {:block_keys, "~> 1.0.2"} ] end ``` @@ -100,6 +100,13 @@ path = "M/60'/0'/0'" xpub = BlockKeys.CKD.derive(root_key, path) ``` +### Solana + +``` +path = "M/501'/0'/0'" +xpub = BlockKeys.CKD.derive(root_key, path, network: :solana) +``` + ## Generating addresses from Master Public Key Generally you would export the master public key and keep it on your live server so that you can generate addresses for payments or deposits. @@ -120,6 +127,13 @@ path = "M/0/0" address = BlockKeys.Ethereum.address(xpub, path) ``` +### Solana + +``` +path = "M/0/0" +address = BlockKeys.Solana.address(xpub, path) +``` + ## Path and derivations You will notice that we used different paths for generating the master private key vs addresses. This is because our initial derivation path includes some hardened paths in order to prevent any downstream generation of addresses for specific paths. For example, the coin code path is hardened in order to prevent anyone from generating a tree of addresses for a different coin given our master public key. diff --git a/lib/block_keys/solana/address.ex b/lib/block_keys/solana/address.ex index 8c00aa1..92c61aa 100644 --- a/lib/block_keys/solana/address.ex +++ b/lib/block_keys/solana/address.ex @@ -11,6 +11,7 @@ defmodule BlockKeys.Solana.Address do |> Base58.encode() end + # https://solanacookbook.com/references/keypairs-and-wallets.html#how-to-check-if-a-public-key-has-an-associated-private-key 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) diff --git a/mix.exs b/mix.exs index c8ccba1..20b8f29 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule BlockKeys.MixProject do def project do [ app: :block_keys, - version: "1.0.1", + version: "1.0.2", elixir: "~> 1.7", description: description(), start_permanent: Mix.env() == :prod, From f05467b0d3ed9dece50c3abc9d854d7622d6cfd8 Mon Sep 17 00:00:00 2001 From: lishawnl Date: Mon, 15 Apr 2024 16:41:28 -0400 Subject: [PATCH 3/3] Change keyword network to curve --- README.md | 2 +- lib/block_keys/ckd.ex | 8 ++++---- lib/block_keys/solana/solana.ex | 2 +- test/block_keys/ckd_test.exs | 6 +++--- test/block_keys/solana/address_test.exs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9ad7429..0f0ab32 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ xpub = BlockKeys.CKD.derive(root_key, path) ``` path = "M/501'/0'/0'" -xpub = BlockKeys.CKD.derive(root_key, path, network: :solana) +xpub = BlockKeys.CKD.derive(root_key, path, curve: :ed25519) ``` ## Generating addresses from Master Public Key diff --git a/lib/block_keys/ckd.ex b/lib/block_keys/ckd.ex index 295fba2..bc499fc 100644 --- a/lib/block_keys/ckd.ex +++ b/lib/block_keys/ckd.ex @@ -150,8 +150,8 @@ defmodule BlockKeys.CKD do data.parent_pub_key ) - case opts[:network] do - :solana -> prefix <> encoded_public_key + case opts[:curve] do + :ed25519 -> prefix <> encoded_public_key _ -> encoded_public_key end end @@ -207,8 +207,8 @@ defmodule BlockKeys.CKD do end defp put_parent_pub(%{parent_priv_key: parent_priv_key} = data, index, opts) do - case opts[:network] do - :solana -> + case opts[:curve] do + :ed25519 -> data |> Map.merge(%{parent_pub_key: Ed25519.derive_public_key(parent_priv_key)}) |> Map.merge(index) diff --git a/lib/block_keys/solana/solana.ex b/lib/block_keys/solana/solana.ex index 9d8c6d0..1e559cd 100644 --- a/lib/block_keys/solana/solana.ex +++ b/lib/block_keys/solana/solana.ex @@ -7,7 +7,7 @@ defmodule BlockKeys.Solana do alias BlockKeys.CKD def address(key, path) do - CKD.derive(key, path, network: :solana) + CKD.derive(key, path, curve: :ed25519) |> Address.from_xpub() end end diff --git a/test/block_keys/ckd_test.exs b/test/block_keys/ckd_test.exs index add0904..80e5165 100644 --- a/test/block_keys/ckd_test.exs +++ b/test/block_keys/ckd_test.exs @@ -86,7 +86,7 @@ defmodule CKDTest do xprv = "xprv9s21ZrQH143K4RdNK1f51Rdeu4XRG8q2cgzeh7ejtzgYpdZcHpNb1MJ2DdBa4iX6NVoZZajsC4gr26mLFaHGBrrtvGkxwhGh6ng8HVZRSeV" - assert CKD.derive(xprv, path, network: :solana) == + assert CKD.derive(xprv, path, curve: :ed25519) == "xpubDeb7pPtgAGEq2eo7ZHPNUvR7xsF4nhK5dBRqA1KD9jZSkSouoZQj6XJ2BVMAMkjHyPeUtUv46Ku4WCWns9uZnUc9BbV5WvFaWNeXbn15bKXNzr" end end @@ -185,7 +185,7 @@ defmodule CKDTest do mainnet_private_key = "xprv9tyUQV64JT5qs3RSTJkXCWKMyUgoQp7F3hA1xzG6ZGu6u6Q9VMNjGr67Lctvy5P8oyaYAL9CAWrUE9i6GoNMKUga5biW6Hx4tws2six3b9c" - mainnet_public_key = CKD.master_public_key(mainnet_private_key, network: :solana) + mainnet_public_key = CKD.master_public_key(mainnet_private_key, curve: :ed25519) assert mainnet_public_key == "xpubDeb7pNy9ZrJfKYBgSVfGXtVfeJWFPmkZoxhMFwAf1YC34mH74pyFja1T1pu1xHiWSBoCMyKa41TTvd821mnV5D1rom5JzN6KPPiqnaoK6L6Jwn" @@ -193,7 +193,7 @@ defmodule CKDTest do testnet_private_key = "tprv8ZgxMBicQKsPdmnHBwtQtqiqL6PYEVehtGeXA53UoJEaaLpbhYgn7JEzHhuXusKgYiNyZnC71oS5D7s1CDVmsMpoRxfM5e3TZfG9LAbmyuc" - testnet_public_key = CKD.master_public_key(testnet_private_key, network: :solana) + testnet_public_key = CKD.master_public_key(testnet_private_key, curve: :ed25519) assert testnet_public_key == "tpubCk2VbTbSG6A2iAeAru4sKG5Xt2u6eBdMhy9kNt2t5SPqJoTGxF4i5kANbAV4WoQ8Fi7m5oQVMd3TD3novoKLtquy8XKmbe45pMm8WbSRpKgddt" diff --git a/test/block_keys/solana/address_test.exs b/test/block_keys/solana/address_test.exs index bc4fdbe..442c889 100644 --- a/test/block_keys/solana/address_test.exs +++ b/test/block_keys/solana/address_test.exs @@ -13,7 +13,7 @@ defmodule SolanaAddressTest do assert root_key == "xprv9s21ZrQH143K35qGjQ6GG1wGHFZP7uCZA1WBdUJA8vBZqESQXQGA4A9d4eve5JqWB5m8YTMcNe8cc7c3FVzDGNcmiabi9WQycbFeEvvJF2D" - assert CKD.derive(root_key, "M/44'/501'/0'/0/0", network: :solana) + assert CKD.derive(root_key, "M/44'/501'/0'/0/0", curve: :ed25519) |> Address.from_xpub() == "4U76rEGDx595M46rWgoA7LwtA821BWCU9CkwG8zbJ6xa" end