├── .github ├── actions-rs │ └── grcov.yml └── workflows │ ├── publish.yaml │ └── test.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.adoc ├── crates.md └── src ├── errors.rs ├── lib.rs ├── path_account.rs ├── path_custom.rs ├── path_short.rs ├── path_standard.rs ├── path_value.rs ├── purpose.rs └── traits.rs /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | llvm: true 2 | output-type: lcov 3 | ignore: 4 | - "target/*" 5 | - "/usr/share/rust/.cargo/*" 6 | - "/.cargo/*" 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [created] 5 | 6 | jobs: 7 | 8 | publish-crates: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout the code 12 | uses: actions/checkout@v4 13 | 14 | - name: Install Rust 15 | uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | override: true 20 | 21 | - name: Publish to Crates 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: publish 25 | args: --token ${{ secrets.CRATES_TOKEN }} 26 | env: 27 | RUST_BACKTRACE: "1" 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - ci/* 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | tests: 14 | name: Test ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: ["windows-latest", "macos-latest", "ubuntu-latest"] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout the code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: stable 28 | override: true 29 | 30 | - name: Build 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: build 34 | args: --all --release 35 | env: 36 | RUST_BACKTRACE: "1" 37 | 38 | - name: Test 39 | uses: actions-rs/cargo@v1 40 | with: 41 | command: test 42 | args: --all-features --release 43 | env: 44 | RUST_BACKTRACE: "1" 45 | 46 | coverage: 47 | name: Coverage Report 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout the code 51 | uses: actions/checkout@v4 52 | 53 | - name: Install Rust 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | toolchain: nightly 57 | override: true 58 | 59 | - name: Setup Tarpaulin 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: install 63 | args: cargo-tarpaulin 64 | 65 | - name: Test 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: tarpaulin 69 | args: --no-fail-fast --all-features --out xml 70 | 71 | - name: Upload to Coveralls 72 | uses: coverallsapp/github-action@v2 73 | with: 74 | github-token: ${{ secrets.GITHUB_TOKEN }} 75 | file: cobertura.xml 76 | 77 | - name: Upload to Codecov 78 | uses: codecov/codecov-action@v1 79 | with: 80 | token: ${{ secrets.CODECOV_TOKEN }} 81 | files: cobertura.xml 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.iml 4 | *.html 5 | *.profraw 6 | cobertura.xml -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "arrayvec" 16 | version = "0.7.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 19 | 20 | [[package]] 21 | name = "base58ck" 22 | version = "0.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" 25 | dependencies = [ 26 | "bitcoin-internals", 27 | "bitcoin_hashes", 28 | ] 29 | 30 | [[package]] 31 | name = "bech32" 32 | version = "0.11.0" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" 35 | 36 | [[package]] 37 | name = "bitcoin" 38 | version = "0.32.5" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" 41 | dependencies = [ 42 | "base58ck", 43 | "bech32", 44 | "bitcoin-internals", 45 | "bitcoin-io", 46 | "bitcoin-units", 47 | "bitcoin_hashes", 48 | "hex-conservative", 49 | "hex_lit", 50 | "secp256k1", 51 | ] 52 | 53 | [[package]] 54 | name = "bitcoin-internals" 55 | version = "0.3.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" 58 | 59 | [[package]] 60 | name = "bitcoin-io" 61 | version = "0.1.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" 64 | 65 | [[package]] 66 | name = "bitcoin-units" 67 | version = "0.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" 70 | dependencies = [ 71 | "bitcoin-internals", 72 | ] 73 | 74 | [[package]] 75 | name = "bitcoin_hashes" 76 | version = "0.14.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" 79 | dependencies = [ 80 | "bitcoin-io", 81 | "hex-conservative", 82 | ] 83 | 84 | [[package]] 85 | name = "bitflags" 86 | version = "2.9.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 89 | 90 | [[package]] 91 | name = "byteorder" 92 | version = "1.3.4" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 95 | 96 | [[package]] 97 | name = "cc" 98 | version = "1.0.41" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "8dae9c4b8fedcae85592ba623c4fd08cfdab3e3b72d6df780c6ead964a69bfff" 101 | 102 | [[package]] 103 | name = "cfg-if" 104 | version = "0.1.10" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 107 | 108 | [[package]] 109 | name = "cfg-if" 110 | version = "1.0.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 113 | 114 | [[package]] 115 | name = "env_logger" 116 | version = "0.8.4" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 119 | dependencies = [ 120 | "log", 121 | "regex", 122 | ] 123 | 124 | [[package]] 125 | name = "getrandom" 126 | version = "0.2.15" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 129 | dependencies = [ 130 | "cfg-if 1.0.0", 131 | "libc", 132 | "wasi 0.11.0+wasi-snapshot-preview1", 133 | ] 134 | 135 | [[package]] 136 | name = "getrandom" 137 | version = "0.3.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 140 | dependencies = [ 141 | "cfg-if 1.0.0", 142 | "libc", 143 | "r-efi", 144 | "wasi 0.14.2+wasi-0.2.4", 145 | ] 146 | 147 | [[package]] 148 | name = "hdpath" 149 | version = "0.7.0" 150 | dependencies = [ 151 | "bitcoin", 152 | "byteorder", 153 | "quickcheck", 154 | "rand 0.9.0", 155 | ] 156 | 157 | [[package]] 158 | name = "hex-conservative" 159 | version = "0.2.1" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" 162 | dependencies = [ 163 | "arrayvec", 164 | ] 165 | 166 | [[package]] 167 | name = "hex_lit" 168 | version = "0.1.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" 171 | 172 | [[package]] 173 | name = "lazy_static" 174 | version = "1.4.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 177 | 178 | [[package]] 179 | name = "libc" 180 | version = "0.2.171" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 183 | 184 | [[package]] 185 | name = "log" 186 | version = "0.4.8" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 189 | dependencies = [ 190 | "cfg-if 0.1.10", 191 | ] 192 | 193 | [[package]] 194 | name = "memchr" 195 | version = "2.3.3" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 198 | 199 | [[package]] 200 | name = "ppv-lite86" 201 | version = "0.2.21" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 204 | dependencies = [ 205 | "zerocopy", 206 | ] 207 | 208 | [[package]] 209 | name = "proc-macro2" 210 | version = "1.0.94" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 213 | dependencies = [ 214 | "unicode-ident", 215 | ] 216 | 217 | [[package]] 218 | name = "quickcheck" 219 | version = "1.0.3" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" 222 | dependencies = [ 223 | "env_logger", 224 | "log", 225 | "rand 0.8.5", 226 | ] 227 | 228 | [[package]] 229 | name = "quote" 230 | version = "1.0.40" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 233 | dependencies = [ 234 | "proc-macro2", 235 | ] 236 | 237 | [[package]] 238 | name = "r-efi" 239 | version = "5.2.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 242 | 243 | [[package]] 244 | name = "rand" 245 | version = "0.8.5" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 248 | dependencies = [ 249 | "rand_core 0.6.4", 250 | ] 251 | 252 | [[package]] 253 | name = "rand" 254 | version = "0.9.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 257 | dependencies = [ 258 | "rand_chacha", 259 | "rand_core 0.9.3", 260 | "zerocopy", 261 | ] 262 | 263 | [[package]] 264 | name = "rand_chacha" 265 | version = "0.9.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 268 | dependencies = [ 269 | "ppv-lite86", 270 | "rand_core 0.9.3", 271 | ] 272 | 273 | [[package]] 274 | name = "rand_core" 275 | version = "0.6.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 278 | dependencies = [ 279 | "getrandom 0.2.15", 280 | ] 281 | 282 | [[package]] 283 | name = "rand_core" 284 | version = "0.9.3" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 287 | dependencies = [ 288 | "getrandom 0.3.2", 289 | ] 290 | 291 | [[package]] 292 | name = "regex" 293 | version = "1.3.9" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 296 | dependencies = [ 297 | "aho-corasick", 298 | "memchr", 299 | "regex-syntax", 300 | "thread_local", 301 | ] 302 | 303 | [[package]] 304 | name = "regex-syntax" 305 | version = "0.6.18" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 308 | 309 | [[package]] 310 | name = "secp256k1" 311 | version = "0.29.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" 314 | dependencies = [ 315 | "bitcoin_hashes", 316 | "secp256k1-sys", 317 | ] 318 | 319 | [[package]] 320 | name = "secp256k1-sys" 321 | version = "0.10.1" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" 324 | dependencies = [ 325 | "cc", 326 | ] 327 | 328 | [[package]] 329 | name = "syn" 330 | version = "2.0.100" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 333 | dependencies = [ 334 | "proc-macro2", 335 | "quote", 336 | "unicode-ident", 337 | ] 338 | 339 | [[package]] 340 | name = "thread_local" 341 | version = "1.0.1" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 344 | dependencies = [ 345 | "lazy_static", 346 | ] 347 | 348 | [[package]] 349 | name = "unicode-ident" 350 | version = "1.0.18" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 353 | 354 | [[package]] 355 | name = "wasi" 356 | version = "0.11.0+wasi-snapshot-preview1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 359 | 360 | [[package]] 361 | name = "wasi" 362 | version = "0.14.2+wasi-0.2.4" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 365 | dependencies = [ 366 | "wit-bindgen-rt", 367 | ] 368 | 369 | [[package]] 370 | name = "wit-bindgen-rt" 371 | version = "0.39.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 374 | dependencies = [ 375 | "bitflags", 376 | ] 377 | 378 | [[package]] 379 | name = "zerocopy" 380 | version = "0.8.23" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" 383 | dependencies = [ 384 | "zerocopy-derive", 385 | ] 386 | 387 | [[package]] 388 | name = "zerocopy-derive" 389 | version = "0.8.23" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" 392 | dependencies = [ 393 | "proc-macro2", 394 | "quote", 395 | "syn", 396 | ] 397 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hdpath" 3 | description = "Hierarchical Deterministic Path (BIP32, BIP43, BIP44, BIP49, BIP84)" 4 | version = "0.7.0" 5 | authors = ["Igor Artamonov "] 6 | edition = "2018" 7 | readme = "crates.md" 8 | license = "Apache-2.0" 9 | repository = "https://github.com/emeraldpay/hdpath-rs" 10 | documentation = "https://docs.rs/hdpath" 11 | 12 | [lib] 13 | name = "hdpath" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | byteorder= "1.3.4" 18 | bitcoin = { version = "0.32", optional = true } 19 | 20 | [dev-dependencies] 21 | rand = "0.9" 22 | quickcheck = "1.0" 23 | 24 | [features] 25 | default = [] 26 | with-bitcoin = ["bitcoin"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = HD Path for Rust 2 | :lib-version: 0.5.0 3 | 4 | image:https://github.com/emeraldpay/hdpath-rs/workflows/Test/badge.svg["Test"] 5 | image:https://coveralls.io/repos/github/emeraldpay/hdpath-rs/badge.svg["Coveralls"] 6 | image:https://codecov.io/gh/emeraldpay/hdpath-rs/branch/master/graph/badge.svg[Codecov,link=https://codecov.io/gh/emeraldpay/hdpath-rs] 7 | image:https://img.shields.io/crates/v/hdpath.svg?style=flat-square["Crates",link="https://crates.io/crates/hdpath"] 8 | image:https://img.shields.io/badge/License-Apache%202.0-blue.svg["License"] 9 | 10 | 11 | Rust crate that implements common structures and utilities to operate HD Path defined by Bitcoin's BIP-32 standard. 12 | 13 | The main specification for the Hierarchical Deterministic Wallets is https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki[BIP-32], 14 | and HD Path is a part of it which specifies the format for the hierarchy path. 15 | 16 | The crate doesn't try to implement Key Derivation specification, but instead implements all common 17 | functionality for creating, parsing and displaying an HD Path, especially standard paths defined 18 | by BIP-44 and related. 19 | 20 | The common structure, defined by BIP-43, is `m/purpose'/coin_type'/account'/change/address_index`, for example `m/44'/0'/0'/0/0` 21 | 22 | All supported standards: 23 | 24 | - https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki[BIP-32] 25 | - https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki[BIP-43] 26 | - https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki[BIP-44] 27 | - https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki[BIP-49] 28 | - https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki[BIP-84] 29 | 30 | == Use 31 | 32 | .Cargo.toml: 33 | [source,toml,subs="attributes"] 34 | ---- 35 | [dependencies] 36 | hdpath = "{lib-version}" 37 | ---- 38 | 39 | === Examples 40 | 41 | .Basic usage 42 | [source, rust] 43 | ---- 44 | use hdpath::StandardHDPath; 45 | use std::str::FromStr; 46 | 47 | let hd_path = StandardHDPath::from_str("m/44'/0'/0'/0/0").unwrap(); 48 | //prints "m/44'/0'/0'/0/0" 49 | println!("{:?}", hd_path); 50 | 51 | //prints "0", which is account id 52 | println!("{:?}", hd_path.account()); 53 | 54 | //prints: "purpose: Pubkey, coin: 0, account: 0, change: 0, index: 0" 55 | println!("purpose: {:?}, coin: {}, account: {}, change: {}, index: {}", 56 | hd_path.purpose(), 57 | hd_path.coin_type(), 58 | hd_path.account(), 59 | hd_path.change(), 60 | hd_path.index() 61 | ); 62 | ---- 63 | 64 | .Create from values 65 | [source, rust] 66 | ---- 67 | use hdpath::{StandardHDPath, Purpose}; 68 | 69 | let hd_path = StandardHDPath::new(Purpose::Witness, 0, 1, 0, 101); 70 | //prints "m/84'/0'/1'/0/101" 71 | println!("{:?}", hd_path); 72 | ---- 73 | 74 | .Create account and derive addresses 75 | [source, rust] 76 | ---- 77 | use hdpath::{AccountHDPath, StandardHDPath, Purpose}; 78 | use std::convert::TryFrom; 79 | 80 | let hd_account = AccountHDPath::new(Purpose::Witness, 0, 1); 81 | // prints "m/44'/0'/1'/x/x" 82 | println!("{:?}", hd_account); 83 | 84 | // get actual address on the account path. Returns StandardHDPath 85 | let hd_path = hd_account.address_at(0, 7); 86 | 87 | //prints: "m/44'/0'/1'/0/7" 88 | println!("{:?}", hd_path); 89 | ---- 90 | 91 | Please note that values for HD Path are limited to `2^31-1` because the highest bit is reserved for marking a _hardened_ value. 92 | Therefore, if you're getting individual values from some user input, you should verify the value before passing to `::new`. 93 | Otherwise, the constructor may fail with _panic_ if an invalid value is passed. 94 | 95 | .Verify before create 96 | [source, rust] 97 | ---- 98 | use hdpath::{StandardHDPath, PathValue, Purpose}; 99 | 100 | fn user_path(index: u32) -> Result { 101 | let user_id = 1234 as u32; 102 | if PathValue::is_ok(index) { 103 | Ok(StandardHDPath::new(Purpose::Witness, 0, user_id, 0, index)) 104 | } else { 105 | Err(()) 106 | } 107 | } 108 | ---- 109 | 110 | === Use with bitcoin library 111 | 112 | Enable `with-bitcoin` feature, that provides extra methods for compatibility with bitcoin lib. 113 | It includes conversion into `Vec` and `DerivationPath`. 114 | 115 | [source,toml,subs="attributes"] 116 | ---- 117 | hdpath = { version = "{lib-version}", features = ["with-bitcoin"] } 118 | ---- 119 | 120 | .Convert to DerivationPath 121 | [source,rust] 122 | ---- 123 | use hdpath::{StandardHDPath}; 124 | use secp256k1::Secp256k1; 125 | use bitcoin::{ 126 | network::constants::Network, 127 | util::bip32::{ExtendedPrivKey, DerivationPath} 128 | }; 129 | 130 | fn get_pk(seed: &[u8], hd_path: &StandardHDPath) -> ExtendedPrivKey { 131 | let secp = Secp256k1::new(); 132 | ExtendedPrivKey::new_master(Network::Bitcoin, seed) 133 | // we convert HD Path to bitcoin lib format (DerivationPath) 134 | .and_then(|k| k.derive_priv(&secp, &DerivationPath::from(hd_path))) 135 | .unwrap() 136 | } 137 | 138 | ---- 139 | 140 | == License 141 | 142 | Copyright 2020 EmeraldPay, Inc 143 | 144 | Licensed under the Apache License, Version 2.0 (the "License"); 145 | you may not use this file except in compliance with the License. 146 | You may obtain a copy of the License at 147 | 148 | http://www.apache.org/licenses/LICENSE-2.0 149 | 150 | Unless required by applicable law or agreed to in writing, software 151 | distributed under the License is distributed on an "AS IS" BASIS, 152 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 153 | See the License for the specific language governing permissions and 154 | limitations under the License. -------------------------------------------------------------------------------- /crates.md: -------------------------------------------------------------------------------- 1 | Common structures and utilities to operate HD Path defined by Bitcoin's BIP-32 standard. 2 | 3 | The main specification for the Hierarchical Deterministic Wallets is [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), 4 | and HD Path is a part of it which specifies the format for the hierarchy path. 5 | 6 | The crate doesn't try to implement Key Derivation specification, but instead implements all common 7 | functionality for creating, parsing and displaying an HD Path, especially standard paths defined 8 | by BIP-44 and related. 9 | 10 | The common structure, defined by BIP-43, is `m/purpose'/coin_type'/account'/change/address_index`, for example `m/44'/0'/0'/0/0` 11 | 12 | All supported standards: 13 | 14 | - [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 15 | - [BIP-43](https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki) 16 | - [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) 17 | - [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) 18 | - [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) 19 | 20 | # Examples 21 | 22 | ## Basic usage 23 | ```rust 24 | use hdpath::StandardHDPath; 25 | use std::str::FromStr; 26 | 27 | let hd_path = StandardHDPath::from_str("m/44'/0'/0'/0/0").unwrap(); 28 | // prints "m/44'/0'/0'/0/0" 29 | println!("{:?}", hd_path); 30 | 31 | // prints "0", which is account id 32 | println!("{:?}", hd_path.account()); 33 | 34 | // prints: "purpose: Pubkey, coin: 0, account: 0, change: 0, index: 0" 35 | println!("purpose: {:?}, coin: {}, account: {}, change: {}, index: {}", 36 | hd_path.purpose(), 37 | hd_path.coin_type(), 38 | hd_path.account(), 39 | hd_path.change(), 40 | hd_path.index()) 41 | ``` 42 | 43 | ## Create from values 44 | ```rust 45 | use hdpath::{StandardHDPath, Purpose}; 46 | 47 | let hd_path = StandardHDPath::new(Purpose::Witness, 0, 1, 0, 101); 48 | // prints "m/84'/0'/1'/0/101" 49 | println!("{:?}", hd_path); 50 | ``` 51 | 52 | ## Create account and derive addresses 53 | ```rust 54 | use hdpath::{AccountHDPath, StandardHDPath, Purpose}; 55 | 56 | let hd_account = AccountHDPath::new(Purpose::Witness, 0, 1); 57 | // prints "m/44'/0'/1'/x/x" 58 | println!("{:?}", hd_account); 59 | 60 | // get actual address on the account path. Returns StandardHDPath 61 | let hd_path = hd_account.address_at(0, 7); 62 | 63 | //prints: "m/44'/0'/1'/0/7" 64 | println!("{:?}", hd_path); 65 | ``` 66 | 67 | ## Verify before create 68 | 69 | Please note that values for HD Path are limited to `2^31-1` because the highest bit is reserved 70 | for marking a _hardened_ value. Therefore, if you're getting individual values from some user 71 | input, you should verify the value before passing to `::new`. Otherwise the constructor may 72 | fail with _panic_ if an invalid value was passed. 73 | 74 | ```rust 75 | use hdpath::{StandardHDPath, PathValue, Purpose}; 76 | 77 | fn user_path(index: u32) -> Result { 78 | let user_id = 1234 as u32; 79 | if PathValue::is_ok(index) { 80 | Ok(StandardHDPath::new(Purpose::Witness, 0, user_id, 0, index)) 81 | } else { 82 | Err(()) 83 | } 84 | } 85 | ``` 86 | 87 | ## How to use with bitcoin library 88 | 89 | Enable `with-bitcoin` feature, that provides extra methods for compatibility with bitcoin lib. 90 | It includes conversion into `Vec` and `DerivationPath`. 91 | 92 | ```toml 93 | hdpath = { version = "0.5.0", features = ["with-bitcoin"] } 94 | ``` 95 | 96 | Convert to DerivationPath when needed 97 | 98 | ```rust 99 | use hdpath::{StandardHDPath}; 100 | use secp256k1::Secp256k1; 101 | use bitcoin::{ 102 | network::constants::Network, 103 | util::bip32::{ExtendedPrivKey, DerivationPath} 104 | }; 105 | 106 | fn get_pk(seed: &[u8], hd_path: &StandardHDPath) -> ExtendedPrivKey { 107 | let secp = Secp256k1::new(); 108 | ExtendedPrivKey::new_master(Network::Bitcoin, seed) 109 | // we convert HD Path to bitcoin lib format (DerivationPath) 110 | .and_then(|k| k.derive_priv(&secp, &DerivationPath::from(hd_path))) 111 | .unwrap() 112 | } 113 | ``` -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 4 | pub enum Error { 5 | HighBitIsSet, 6 | InvalidLength(usize), 7 | InvalidPurpose(u32), 8 | InvalidStructure, 9 | InvalidFormat 10 | } 11 | 12 | impl Display for Error { 13 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 14 | match self { 15 | Error::HighBitIsSet => write!(f, "High bit is set"), 16 | Error::InvalidLength(len) => write!(f, "Invalid length: {}", len), 17 | Error::InvalidPurpose(purpose) => write!(f, "Invalid purpose: {}", purpose), 18 | Error::InvalidStructure => write!(f, "Invalid structure"), 19 | Error::InvalidFormat => write!(f, "Invalid format") 20 | } 21 | } 22 | } 23 | 24 | impl std::error::Error for Error {} -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Common structures and utilities to operate HD Path defined by Bitcoin's BIP-32 standard. 2 | //! 3 | //! The main specification for the Hierarchical Deterministic Wallets is [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), 4 | //! and HD Path is a part of it which specifies the format for the hierarchy path. 5 | //! 6 | //! The crate doesn't try to implement Key Derivation specification, but instead implements all common 7 | //! functionality for creating, parsing and displaying an HD Path, especially standard paths defined 8 | //! by BIP-44 and related. 9 | //! 10 | //! The common structure, defined by BIP-43, is `m/purpose'/coin_type'/account'/change/address_index`, for example `m/44'/0'/0'/0/0` 11 | //! 12 | //! All supported standards: 13 | //! - [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 14 | //! - [BIP-43](https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki) 15 | //! - [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) 16 | //! - [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) 17 | //! - [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) 18 | //! 19 | //! Base traits is [HDPath](trait.HDPath.html), with few specific implementations and general [`CustomHDPath`](struct.CustomHDPath.html) 20 | //! 21 | //! # Examples 22 | //! 23 | //! ## Basic usage 24 | //! ``` 25 | //! use hdpath::StandardHDPath; 26 | //! # use std::str::FromStr; 27 | //! 28 | //! let hdpath = StandardHDPath::from_str("m/44'/0'/0'/0/0").unwrap(); 29 | //! //prints "m/44'/0'/0'/0/0" 30 | //! println!("{:?}", hdpath); 31 | //! 32 | //! //prints "0", which is account id 33 | //! println!("{:?}", hdpath.account()); 34 | //! 35 | //! //prints: "purpose: Pubkey, coin: 0, account: 0, change: 0, index: 0" 36 | //! println!("purpose: {:?}, coin: {}, account: {}, change: {}, index: {}", 37 | //! hdpath.purpose(), 38 | //! hdpath.coin_type(), 39 | //! hdpath.account(), 40 | //! hdpath.change(), 41 | //! hdpath.index()) 42 | //! ``` 43 | //! 44 | //! ## Create from values 45 | //! ``` 46 | //! use hdpath::{StandardHDPath, Purpose}; 47 | //! 48 | //! let hdpath = StandardHDPath::new(Purpose::Witness, 0, 1, 0, 101); 49 | //! //prints "m/84'/0'/1'/0/101" 50 | //! println!("{:?}", hdpath); 51 | //! ``` 52 | //! 53 | //! ## Create account and derive addresses 54 | //! ``` 55 | //! use hdpath::{AccountHDPath, StandardHDPath, Purpose}; 56 | //! 57 | //! let hd_account = AccountHDPath::new(Purpose::Witness, 0, 1); 58 | //! // prints "m/44'/0'/1'/x/x" 59 | //! println!("{:?}", hd_account); 60 | //! 61 | //! // get actual address on the account path. Returns StandardHDPath 62 | //! let hd_path = hd_account.address_at(0, 7); 63 | //! 64 | //! //prints: "m/44'/0'/1'/0/7" 65 | //! println!("{:?}", hd_path); 66 | //! ``` 67 | //! 68 | //! ## Verify before create 69 | //! 70 | //! Please note that values for HD Path are limited to `2^31-1` because the highest bit is reserved 71 | //! for marking a _hardened_ value. Therefore, if you're getting individual values from some user 72 | //! input, you should verify the value before passing to `::new`. Otherwise the constructor may 73 | //! fail with _panic_ if an invalid value was passed. 74 | //! 75 | //! ``` 76 | //! use hdpath::{StandardHDPath, PathValue, Purpose}; 77 | //! 78 | //! fn user_path(index: u32) -> Result { 79 | //! let user_id = 1234 as u32; 80 | //! if PathValue::is_ok(index) { 81 | //! Ok(StandardHDPath::new(Purpose::Witness, 0, user_id, 0, index)) 82 | //! } else { 83 | //! Err(()) 84 | //! } 85 | //! } 86 | //! ``` 87 | //! 88 | extern crate byteorder; 89 | #[cfg(feature = "with-bitcoin")] 90 | extern crate bitcoin; 91 | 92 | mod errors; 93 | mod traits; 94 | mod path_account; 95 | mod path_custom; 96 | mod path_short; 97 | mod path_standard; 98 | mod path_value; 99 | mod purpose; 100 | 101 | pub use errors::Error; 102 | pub use traits::HDPath; 103 | pub use path_account::AccountHDPath; 104 | pub use path_custom::CustomHDPath; 105 | pub use path_standard::StandardHDPath; 106 | pub use path_value::{PathValue}; 107 | pub use purpose::Purpose; 108 | -------------------------------------------------------------------------------- /src/path_account.rs: -------------------------------------------------------------------------------- 1 | use crate::{Purpose, CustomHDPath, Error, PathValue, StandardHDPath}; 2 | use std::convert::TryFrom; 3 | #[cfg(feature = "with-bitcoin")] 4 | use bitcoin::bip32::{ChildNumber, DerivationPath}; 5 | use std::str::FromStr; 6 | use crate::traits::HDPath; 7 | 8 | 9 | /// Account-only HD Path for [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki), 10 | /// [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki), [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) 11 | /// and similar. 12 | /// 13 | /// It's not supposed to be used to derive actual addresses, but only to build other path based on this 14 | /// 15 | /// Represents `m/purpose'/coin_type'/account'/x/x`, like `m/44'/0'/0'/x/x`. 16 | /// 17 | /// # Create new 18 | /// ``` 19 | /// use hdpath::{AccountHDPath, Purpose}; 20 | /// 21 | /// //creates path m/84'/0'/0' 22 | /// let hd_account = AccountHDPath::new(Purpose::Witness, 0, 0); 23 | /// ``` 24 | /// # Parse string 25 | /// ``` 26 | /// use hdpath::{AccountHDPath}; 27 | /// # use std::str::FromStr; 28 | /// 29 | /// //creates path m/84'/0'/0' 30 | /// let hd_account = AccountHDPath::from_str("m/84'/0'/0'").unwrap(); 31 | /// ``` 32 | /// 33 | /// Internal type and index can be explicitly market as unused (which is the default format for converting it into a string). 34 | /// 35 | /// ``` 36 | /// use hdpath::{AccountHDPath}; 37 | /// # use std::str::FromStr; 38 | /// 39 | /// //creates path m/84'/0'/0' 40 | /// let hd_account = AccountHDPath::from_str("m/84'/0'/0'/x/x").unwrap(); 41 | /// ``` 42 | /// 43 | /// # Create actial path 44 | /// ``` 45 | /// use hdpath::{AccountHDPath, Purpose, StandardHDPath}; 46 | /// # use std::str::FromStr; 47 | /// 48 | /// let hd_account = AccountHDPath::from_str("m/84'/0'/0'").unwrap(); 49 | /// // gives hd path m/84'/0'/0'/0/4 50 | /// let hd_path: StandardHDPath = hd_account.address_at(0, 4).unwrap(); 51 | /// ``` 52 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 53 | pub struct AccountHDPath { 54 | purpose: Purpose, 55 | coin_type: u32, 56 | account: u32, 57 | } 58 | 59 | impl AccountHDPath { 60 | 61 | pub fn new(purpose: Purpose, coin_type: u32, account: u32) -> AccountHDPath { 62 | match Self::try_new(purpose, coin_type, account) { 63 | Ok(path) => path, 64 | Err(err) => panic!("Invalid {}: {}", err.0, err.1) 65 | } 66 | } 67 | 68 | pub fn try_new(purpose: Purpose, coin_type: u32, account: u32) -> Result { 69 | if let Purpose::Custom(n) = purpose { 70 | if !PathValue::is_ok(n) { 71 | return Err(("purpose".to_string(), n)); 72 | } 73 | } 74 | if !PathValue::is_ok(coin_type) { 75 | return Err(("coin_type".to_string(), coin_type)); 76 | } 77 | if !PathValue::is_ok(account) { 78 | return Err(("account".to_string(), account)); 79 | } 80 | Ok(AccountHDPath { 81 | purpose, 82 | coin_type, 83 | account, 84 | }) 85 | } 86 | 87 | /// Derive path to an address withing this account path 88 | /// ``` 89 | /// # use hdpath::{AccountHDPath, Purpose, StandardHDPath}; 90 | /// # use std::convert::TryFrom; 91 | /// let hd_account = AccountHDPath::try_from("m/84'/0'/0'").unwrap(); 92 | /// // gives hd path m/84'/0'/0'/0/4 93 | /// let hd_path: StandardHDPath = hd_account.address_at(0, 4).unwrap(); 94 | /// ``` 95 | /// 96 | /// Return error `(field_name, invalid_value)` if the field has an incorrect value. 97 | /// It may happed if change or index are in _hardened_ space. 98 | pub fn address_at(&self, change: u32, index: u32) -> Result { 99 | StandardHDPath::try_new( 100 | self.purpose.clone(), 101 | self.coin_type, 102 | self.account, 103 | change, 104 | index 105 | ) 106 | } 107 | 108 | pub fn purpose(&self) -> &Purpose { 109 | &self.purpose 110 | } 111 | 112 | pub fn coin_type(&self) -> u32 { 113 | self.coin_type 114 | } 115 | 116 | pub fn account(&self) -> u32 { 117 | self.account 118 | } 119 | } 120 | 121 | impl HDPath for AccountHDPath { 122 | fn len(&self) -> u8 { 123 | 3 124 | } 125 | 126 | fn get(&self, pos: u8) -> Option { 127 | match pos { 128 | 0 => Some(self.purpose.as_value()), 129 | 1 => Some(PathValue::Hardened(self.coin_type)), 130 | 2 => Some(PathValue::Hardened(self.account)), 131 | _ => None 132 | } 133 | } 134 | } 135 | 136 | impl From<&StandardHDPath> for AccountHDPath { 137 | fn from(value: &StandardHDPath) -> Self { 138 | AccountHDPath::new( 139 | value.purpose().clone(), 140 | value.coin_type(), 141 | value.account(), 142 | ) 143 | } 144 | } 145 | 146 | impl From for AccountHDPath { 147 | fn from(value: StandardHDPath) -> Self { 148 | AccountHDPath::new( 149 | value.purpose().clone(), 150 | value.coin_type(), 151 | value.account(), 152 | ) 153 | } 154 | } 155 | 156 | impl TryFrom for AccountHDPath { 157 | type Error = Error; 158 | 159 | fn try_from(value: CustomHDPath) -> Result { 160 | if value.0.len() < 3 { 161 | return Err(Error::InvalidLength(value.0.len())) 162 | } 163 | if let Some(PathValue::Hardened(p)) = value.0.get(0) { 164 | let purpose = Purpose::try_from(*p)?; 165 | if let Some(PathValue::Hardened(coin_type)) = value.0.get(1) { 166 | if let Some(PathValue::Hardened(account)) = value.0.get(2) { 167 | return Ok(AccountHDPath { 168 | purpose, 169 | coin_type: *coin_type, 170 | account: *account, 171 | }) 172 | } 173 | } 174 | Err(Error::InvalidStructure) 175 | } else { 176 | Err(Error::InvalidStructure) 177 | } 178 | } 179 | } 180 | 181 | impl TryFrom<&str> for AccountHDPath 182 | { 183 | type Error = Error; 184 | 185 | fn try_from(value: &str) -> Result { 186 | AccountHDPath::from_str(value) 187 | } 188 | } 189 | 190 | impl FromStr for AccountHDPath { 191 | type Err = Error; 192 | 193 | fn from_str(s: &str) -> Result { 194 | let clean = if s.ends_with("/x/x") { 195 | &s[0..s.len() - 4] 196 | } else { 197 | s 198 | }; 199 | let value = CustomHDPath::from_str(clean)?; 200 | AccountHDPath::try_from(value) 201 | } 202 | } 203 | 204 | impl std::fmt::Display for AccountHDPath { 205 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 206 | write!(f, "m/{}'/{}'/{}'/x/x", 207 | self.purpose.as_value().as_number(), 208 | self.coin_type, 209 | self.account, 210 | ) 211 | } 212 | } 213 | 214 | #[cfg(feature = "with-bitcoin")] 215 | impl std::convert::From<&AccountHDPath> for Vec { 216 | fn from(value: &AccountHDPath) -> Self { 217 | let result = [ 218 | ChildNumber::from_hardened_idx(value.purpose.as_value().as_number()) 219 | .expect("Purpose is not Hardened"), 220 | ChildNumber::from_hardened_idx(value.coin_type) 221 | .expect("Coin Type is not Hardened"), 222 | ChildNumber::from_hardened_idx(value.account) 223 | .expect("Account is not Hardened"), 224 | ]; 225 | return result.to_vec(); 226 | } 227 | } 228 | 229 | #[cfg(feature = "with-bitcoin")] 230 | impl std::convert::From for Vec { 231 | fn from(value: AccountHDPath) -> Self { 232 | Vec::::from(&value) 233 | } 234 | } 235 | 236 | #[cfg(feature = "with-bitcoin")] 237 | impl std::convert::From for DerivationPath { 238 | fn from(value: AccountHDPath) -> Self { 239 | DerivationPath::from(Vec::::from(&value)) 240 | } 241 | } 242 | 243 | #[cfg(feature = "with-bitcoin")] 244 | impl std::convert::From<&AccountHDPath> for DerivationPath { 245 | fn from(value: &AccountHDPath) -> Self { 246 | DerivationPath::from(Vec::::from(value)) 247 | } 248 | } 249 | 250 | #[cfg(test)] 251 | mod tests { 252 | use super::*; 253 | use std::convert::TryFrom; 254 | 255 | #[test] 256 | fn create_try_from_string() { 257 | let hd_account = AccountHDPath::try_from("m/84'/0'/5'"); 258 | assert!(hd_account.is_ok()); 259 | let hd_account = hd_account.unwrap(); 260 | assert_eq!(Purpose::Witness, hd_account.purpose); 261 | assert_eq!(0, hd_account.coin_type); 262 | assert_eq!(5, hd_account.account); 263 | } 264 | 265 | #[test] 266 | fn create_from_string() { 267 | let hd_account = AccountHDPath::from_str("m/84'/0'/5'"); 268 | assert!(hd_account.is_ok()); 269 | let hd_account = hd_account.unwrap(); 270 | assert_eq!(Purpose::Witness, hd_account.purpose); 271 | assert_eq!(0, hd_account.coin_type); 272 | assert_eq!(5, hd_account.account); 273 | } 274 | 275 | #[test] 276 | fn create_from_acc_string() { 277 | let hd_account = AccountHDPath::from_str("m/84'/0'/5'/x/x"); 278 | assert!(hd_account.is_ok()); 279 | let hd_account = hd_account.unwrap(); 280 | assert_eq!(Purpose::Witness, hd_account.purpose); 281 | assert_eq!(0, hd_account.coin_type); 282 | assert_eq!(5, hd_account.account); 283 | } 284 | 285 | #[test] 286 | fn create_from_string_sh() { 287 | let hd_account = AccountHDPath::try_from("m/49'/0'/5'"); 288 | assert!(hd_account.is_ok()); 289 | let hd_account = hd_account.unwrap(); 290 | assert_eq!(Purpose::ScriptHash, hd_account.purpose); 291 | assert_eq!(0, hd_account.coin_type()); 292 | assert_eq!(5, hd_account.account()); 293 | } 294 | 295 | #[test] 296 | fn create_from_string_pubkey() { 297 | let hd_account = AccountHDPath::try_from("m/44'/0'/5'"); 298 | assert!(hd_account.is_ok()); 299 | let hd_account = hd_account.unwrap(); 300 | assert_eq!(Purpose::Pubkey, hd_account.purpose); 301 | assert_eq!(0, hd_account.coin_type); 302 | assert_eq!(5, hd_account.account); 303 | } 304 | 305 | #[test] 306 | fn create_from_string_custom() { 307 | let hd_account = AccountHDPath::try_from("m/218'/0'/5'"); 308 | assert!(hd_account.is_ok()); 309 | let hd_account = hd_account.unwrap(); 310 | assert_eq!(Purpose::Custom(218), hd_account.purpose); 311 | assert_eq!(0, hd_account.coin_type()); 312 | assert_eq!(5, hd_account.account()); 313 | } 314 | 315 | #[test] 316 | fn create_from_full_string() { 317 | let hd_account = AccountHDPath::try_from("m/84'/0'/5'/0/101"); 318 | assert!(hd_account.is_ok()); 319 | let hd_account = hd_account.unwrap(); 320 | assert_eq!(Purpose::Witness, hd_account.purpose); 321 | assert_eq!(0, hd_account.coin_type()); 322 | assert_eq!(5, hd_account.account()); 323 | } 324 | 325 | #[test] 326 | fn to_string() { 327 | let hd_account = AccountHDPath::try_from("m/84'/0'/5'/0/101").unwrap(); 328 | assert_eq!("m/84'/0'/5'/x/x", hd_account.to_string()); 329 | } 330 | 331 | #[test] 332 | fn create_change_address() { 333 | let hd_account = AccountHDPath::try_from("m/84'/0'/0'").unwrap(); 334 | let hd_path = hd_account.address_at(1, 3).expect("address create"); 335 | assert_eq!( 336 | StandardHDPath::try_from("m/84'/0'/0'/1/3").unwrap(), 337 | hd_path 338 | ); 339 | } 340 | 341 | #[test] 342 | fn create_receive_address() { 343 | let hd_account = AccountHDPath::try_from("m/84'/0'/0'").unwrap(); 344 | let hd_path = hd_account.address_at(0, 15).expect("address create"); 345 | assert_eq!( 346 | StandardHDPath::try_from("m/84'/0'/0'/0/15").unwrap(), 347 | hd_path 348 | ); 349 | } 350 | 351 | #[test] 352 | fn convert_from_full() { 353 | let hd_path = StandardHDPath::from_str("m/84'/0'/0'/0/15").unwrap(); 354 | let hd_account = AccountHDPath::from(&hd_path); 355 | assert_eq!(AccountHDPath::from_str("m/84'/0'/0'").unwrap(), hd_account); 356 | 357 | let hd_path = StandardHDPath::from_str("m/84'/0'/3'/0/0").unwrap(); 358 | let hd_account = AccountHDPath::from(&hd_path); 359 | assert_eq!(AccountHDPath::from_str("m/84'/0'/3'").unwrap(), hd_account); 360 | 361 | let hd_path = StandardHDPath::from_str("m/44'/1'/1'/0/0").unwrap(); 362 | let hd_account = AccountHDPath::from(&hd_path); 363 | assert_eq!(AccountHDPath::from_str("m/44'/1'/1'").unwrap(), hd_account); 364 | } 365 | } 366 | 367 | #[cfg(all(test, feature = "with-bitcoin"))] 368 | mod tests_with_bitcoin { 369 | use super::*; 370 | use std::convert::TryFrom; 371 | use bitcoin::bip32::ChildNumber; 372 | 373 | #[test] 374 | pub fn convert_to_childnumbers() { 375 | let hdpath = AccountHDPath::try_from("m/44'/60'/2'/0/3581").unwrap(); 376 | let children: Vec = hdpath.into(); 377 | assert_eq!(children.len(), 3); 378 | assert_eq!(children[0], ChildNumber::from_hardened_idx(44).unwrap()); 379 | assert_eq!(children[1], ChildNumber::from_hardened_idx(60).unwrap()); 380 | assert_eq!(children[2], ChildNumber::from_hardened_idx(2).unwrap()); 381 | } 382 | 383 | } -------------------------------------------------------------------------------- /src/path_custom.rs: -------------------------------------------------------------------------------- 1 | use crate::{PathValue, Error}; 2 | use std::convert::TryFrom; 3 | #[cfg(feature = "with-bitcoin")] 4 | use bitcoin::bip32::{ChildNumber, DerivationPath}; 5 | use std::str::FromStr; 6 | use crate::traits::HDPath; 7 | 8 | /// A custom HD Path, that can be any length and contain any Hardened and non-Hardened values in 9 | /// any order. Direct implementation for [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#The_default_wallet_layout) 10 | /// 11 | /// If you need just standard type of HD Path like `m/44'/0'/0'/0/0` use [`StandardHDPath`](struct.StandardHDPath.html) instead. 12 | /// 13 | /// # Usage 14 | /// 15 | /// ## Parse string 16 | /// ``` 17 | /// use hdpath::CustomHDPath; 18 | /// # use std::convert::TryFrom; 19 | /// 20 | /// let hdpath = CustomHDPath::try_from("m/1'/2'/3/4/5'/6'/7").unwrap(); 21 | /// let hdpath = CustomHDPath::try_from("m/44'/0'/1'/0/0").unwrap(); 22 | /// //also support uppercase notation 23 | /// let hdpath = CustomHDPath::try_from("M/44H/0H/1H/0/0").unwrap(); 24 | /// ``` 25 | /// ## Direct create 26 | /// ``` 27 | /// use hdpath::{CustomHDPath, PathValue}; 28 | /// 29 | /// let hdpath = CustomHDPath::try_new(vec![ 30 | /// PathValue::hardened(44), PathValue::hardened(0), PathValue::hardened(1), 31 | /// PathValue::normal(0), PathValue::normal(0) 32 | /// ]).unwrap(); 33 | /// ``` 34 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 35 | pub struct CustomHDPath(pub Vec); 36 | 37 | impl CustomHDPath { 38 | 39 | /// Create a new HD Path. 40 | /// 41 | /// Returns error only if provided vector is too large, i.e. more than 255 elements (since BIP-32 42 | /// says about ability to encode depth in a single byte). 43 | pub fn try_new(values: Vec) -> Result { 44 | if values.len() > 0xff { 45 | Err(Error::InvalidLength(values.len())) 46 | } else { 47 | Ok(CustomHDPath(values)) 48 | } 49 | } 50 | } 51 | 52 | impl HDPath for CustomHDPath { 53 | fn len(&self) -> u8 { 54 | self.0.len() as u8 55 | } 56 | 57 | fn get(&self, pos: u8) -> Option { 58 | self.0.get(pos as usize).map(|a| a.clone()) 59 | } 60 | } 61 | 62 | impl TryFrom<&str> for CustomHDPath { 63 | type Error = Error; 64 | 65 | fn try_from(value: &str) -> Result { 66 | CustomHDPath::from_str(value) 67 | } 68 | } 69 | 70 | impl std::convert::From<&dyn HDPath> for CustomHDPath { 71 | fn from(value: &dyn HDPath) -> Self { 72 | let mut path = Vec::with_capacity(value.len() as usize); 73 | for i in 0..value.len() { 74 | path.push(value.get(i).expect("no-path-element")); 75 | } 76 | CustomHDPath(path) 77 | } 78 | } 79 | 80 | impl std::fmt::Display for CustomHDPath { 81 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 82 | write!(f, "m")?; 83 | for pv in self.0.iter() { 84 | write!(f, "/{}", pv)?; 85 | } 86 | Ok(()) 87 | } 88 | } 89 | 90 | impl FromStr for CustomHDPath { 91 | type Err = Error; 92 | 93 | fn from_str(value: &str) -> Result { 94 | const STATE_EXPECT_NUM: usize = 0; 95 | const STATE_READING_NUM: usize = 1; 96 | const STATE_READ_MARKER: usize = 2; 97 | 98 | let chars = value.as_bytes(); 99 | if chars.len() < 2 { 100 | return Err(Error::InvalidFormat) 101 | } 102 | if chars[0] != 'm' as u8 && chars[0] != 'M' as u8 { 103 | return Err(Error::InvalidFormat) 104 | } 105 | if chars[1] != '/' as u8 { 106 | return Err(Error::InvalidFormat) 107 | } 108 | let mut keys: Vec = Vec::new(); 109 | let mut pos = 2; 110 | let mut num: u32 = 0; 111 | let mut state = STATE_EXPECT_NUM; 112 | while chars.len() > pos { 113 | match chars[pos] { 114 | 39 | 72 => { // (') apostrophe or H 115 | if state != STATE_READING_NUM { 116 | return Err(Error::InvalidFormat) 117 | } 118 | if !PathValue::is_ok(num) { 119 | return Err(Error::InvalidFormat) 120 | } 121 | keys.push(PathValue::hardened(num)); 122 | state = STATE_READ_MARKER; 123 | num = 0; 124 | }, 125 | 47 => { // slash 126 | if state == STATE_READING_NUM { 127 | if !PathValue::is_ok(num) { 128 | return Err(Error::InvalidFormat) 129 | } 130 | keys.push(PathValue::normal(num)); 131 | } else if state != STATE_READ_MARKER { 132 | return Err(Error::InvalidFormat) 133 | } 134 | state = STATE_EXPECT_NUM; 135 | num = 0; 136 | }, 137 | 48..=57 => { //number 138 | if state == STATE_EXPECT_NUM { 139 | state = STATE_READING_NUM 140 | } else if state != STATE_READING_NUM { 141 | return Err(Error::InvalidFormat) 142 | } 143 | num = num * 10 + (chars[pos] - 48) as u32; 144 | }, 145 | _ => { 146 | return Err(Error::InvalidFormat) 147 | } 148 | } 149 | pos += 1; 150 | if chars.len() == pos && state == 1 { 151 | if !PathValue::is_ok(num) { 152 | return Err(Error::InvalidFormat) 153 | } 154 | keys.push(PathValue::normal(num)); 155 | } 156 | } 157 | if state == STATE_EXPECT_NUM { 158 | //finished with slash 159 | Err(Error::InvalidFormat) 160 | } else if keys.is_empty() { 161 | Err(Error::InvalidStructure) 162 | } else { 163 | Ok(CustomHDPath(keys)) 164 | } 165 | } 166 | } 167 | 168 | #[cfg(feature = "with-bitcoin")] 169 | impl std::convert::From<&CustomHDPath> for Vec { 170 | fn from(value: &CustomHDPath) -> Self { 171 | let mut result: Vec = Vec::with_capacity(value.0.len()); 172 | for item in value.0.iter() { 173 | result.push(ChildNumber::from(item.to_raw())) 174 | } 175 | return result; 176 | } 177 | } 178 | 179 | #[cfg(feature = "with-bitcoin")] 180 | impl std::convert::From for Vec { 181 | fn from(value: CustomHDPath) -> Self { 182 | Vec::::from(&value) 183 | } 184 | } 185 | 186 | #[cfg(feature = "with-bitcoin")] 187 | impl std::convert::From for DerivationPath { 188 | fn from(value: CustomHDPath) -> Self { 189 | DerivationPath::from(Vec::::from(&value)) 190 | } 191 | } 192 | 193 | #[cfg(feature = "with-bitcoin")] 194 | impl std::convert::From<&CustomHDPath> for DerivationPath { 195 | fn from(value: &CustomHDPath) -> Self { 196 | DerivationPath::from(Vec::::from(value)) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | use crate::StandardHDPath; 204 | 205 | #[test] 206 | pub fn to_string() { 207 | assert_eq!( 208 | CustomHDPath::try_from("m/44'/0'/0'/0/0").unwrap().to_string(), 209 | "m/44'/0'/0'/0/0".to_string() 210 | ); 211 | 212 | assert_eq!( 213 | CustomHDPath::try_from("m/84'/1'/2'/3/4").unwrap().to_string(), 214 | "m/84'/1'/2'/3/4".to_string() 215 | ); 216 | 217 | assert_eq!( 218 | CustomHDPath::try_from("m/1'").unwrap().to_string(), 219 | "m/1'".to_string() 220 | ); 221 | 222 | assert_eq!( 223 | CustomHDPath::try_from("m/44'/0'/1'/2/3/4'/5/67'/8'/910").unwrap().to_string(), 224 | "m/44'/0'/1'/2/3/4'/5/67'/8'/910".to_string() 225 | ); 226 | } 227 | 228 | #[test] 229 | pub fn try_from_common() { 230 | let act = CustomHDPath::try_from("m/44'/0'/0'/0/0").unwrap(); 231 | act.0[0].as_number(); 232 | assert_eq!(5, act.0.len()); 233 | assert_eq!(&PathValue::Hardened(44), act.0.get(0).unwrap()); 234 | assert_eq!(&PathValue::Hardened(0), act.0.get(1).unwrap()); 235 | assert_eq!(&PathValue::Hardened(0), act.0.get(2).unwrap()); 236 | assert_eq!(&PathValue::Normal(0), act.0.get(3).unwrap()); 237 | assert_eq!(&PathValue::Normal(0), act.0.get(4).unwrap()); 238 | } 239 | 240 | #[test] 241 | pub fn try_from_common_trait() { 242 | let source = StandardHDPath::from_str("m/84'/0'/1'/2/3").unwrap(); 243 | let act = CustomHDPath::from(source.to_trait()); 244 | assert_eq!( 245 | CustomHDPath::try_from("m/84'/0'/1'/2/3").unwrap(), act 246 | ); 247 | } 248 | 249 | #[test] 250 | pub fn try_from_bignum() { 251 | let act = CustomHDPath::try_from("m/44'/12'/345'/6789/101112").unwrap(); 252 | assert_eq!(5, act.0.len()); 253 | assert_eq!(&PathValue::Hardened(44), act.0.get(0).unwrap()); 254 | assert_eq!(&PathValue::Hardened(12), act.0.get(1).unwrap()); 255 | assert_eq!(&PathValue::Hardened(345), act.0.get(2).unwrap()); 256 | assert_eq!(&PathValue::Normal(6789), act.0.get(3).unwrap()); 257 | assert_eq!(&PathValue::Normal(101112), act.0.get(4).unwrap()); 258 | } 259 | 260 | #[test] 261 | pub fn try_from_long() { 262 | let act = CustomHDPath::try_from("m/44'/0'/1'/2/3/4'/5/67'/8'/910").unwrap(); 263 | assert_eq!(10, act.0.len()); 264 | assert_eq!(&PathValue::Hardened(44), act.0.get(0).unwrap()); 265 | assert_eq!(&PathValue::Hardened(0), act.0.get(1).unwrap()); 266 | assert_eq!(&PathValue::Hardened(1), act.0.get(2).unwrap()); 267 | assert_eq!(&PathValue::Normal(2), act.0.get(3).unwrap()); 268 | assert_eq!(&PathValue::Normal(3), act.0.get(4).unwrap()); 269 | assert_eq!(&PathValue::Hardened(4), act.0.get(5).unwrap()); 270 | assert_eq!(&PathValue::Normal(5), act.0.get(6).unwrap()); 271 | assert_eq!(&PathValue::Hardened(67), act.0.get(7).unwrap()); 272 | assert_eq!(&PathValue::Hardened(8), act.0.get(8).unwrap()); 273 | assert_eq!(&PathValue::Normal(910), act.0.get(9).unwrap()); 274 | } 275 | 276 | #[test] 277 | pub fn try_from_all_hardened() { 278 | let act = CustomHDPath::try_from("m/44'/0'/0'/0'/1'").unwrap(); 279 | assert_eq!(5, act.0.len()); 280 | assert_eq!(&PathValue::Hardened(44), act.0.get(0).unwrap()); 281 | assert_eq!(&PathValue::Hardened(0), act.0.get(1).unwrap()); 282 | assert_eq!(&PathValue::Hardened(0), act.0.get(2).unwrap()); 283 | assert_eq!(&PathValue::Hardened(0), act.0.get(3).unwrap()); 284 | assert_eq!(&PathValue::Hardened(1), act.0.get(4).unwrap()); 285 | } 286 | 287 | #[test] 288 | pub fn try_from_all_normal() { 289 | let act = CustomHDPath::try_from("m/44/0/0/0/1").unwrap(); 290 | assert_eq!(5, act.0.len()); 291 | assert_eq!(&PathValue::Normal(44), act.0.get(0).unwrap()); 292 | assert_eq!(&PathValue::Normal(0), act.0.get(1).unwrap()); 293 | assert_eq!(&PathValue::Normal(0), act.0.get(2).unwrap()); 294 | assert_eq!(&PathValue::Normal(0), act.0.get(3).unwrap()); 295 | assert_eq!(&PathValue::Normal(1), act.0.get(4).unwrap()); 296 | } 297 | 298 | #[test] 299 | pub fn try_from_other_format() { 300 | let act = CustomHDPath::try_from("M/44H/0H/0H/1/5").unwrap(); 301 | assert_eq!(5, act.0.len()); 302 | assert_eq!(&PathValue::Hardened(44), act.0.get(0).unwrap()); 303 | assert_eq!(&PathValue::Hardened(0), act.0.get(1).unwrap()); 304 | assert_eq!(&PathValue::Hardened(0), act.0.get(2).unwrap()); 305 | assert_eq!(&PathValue::Normal(1), act.0.get(3).unwrap()); 306 | assert_eq!(&PathValue::Normal(5), act.0.get(4).unwrap()); 307 | } 308 | 309 | #[test] 310 | pub fn error_on_invalid_path() { 311 | let paths = vec![ 312 | "", "1", "m44", 313 | "m/", "m/44/", "m/44/0/", "m/44''/0/0/0/1", "m/44/H0/0/0/1", 314 | ]; 315 | for p in paths { 316 | assert!(CustomHDPath::try_from(p).is_err(), "test: {}", p); 317 | } 318 | } 319 | 320 | #[test] 321 | pub fn fail_incorrect_hardened() { 322 | let custom = CustomHDPath::try_from("m/2147483692'/0'/0'/0/0"); 323 | assert!(custom.is_err()); 324 | } 325 | 326 | #[test] 327 | pub fn cannot_create_too_long() { 328 | let mut path = Vec::with_capacity(0xff + 1); 329 | for _i in 0..path.capacity() { 330 | path.push(PathValue::Normal(1)); 331 | } 332 | let custom = CustomHDPath::try_new(path); 333 | assert!(custom.is_err()); 334 | assert_eq!(Error::InvalidLength(256), custom.expect_err("not error")); 335 | } 336 | } 337 | 338 | #[cfg(all(test, feature = "with-bitcoin"))] 339 | mod tests_with_bitcoin { 340 | use super::*; 341 | use std::convert::TryFrom; 342 | use bitcoin::bip32::ChildNumber; 343 | 344 | #[test] 345 | pub fn convert_to_childnumbers() { 346 | let hdpath = CustomHDPath::try_from("m/44'/15'/2'/0/35/81/0").unwrap(); 347 | let childs: Vec = hdpath.into(); 348 | assert_eq!(childs.len(), 7); 349 | assert_eq!(childs[0], ChildNumber::from_hardened_idx(44).unwrap()); 350 | assert_eq!(childs[1], ChildNumber::from_hardened_idx(15).unwrap()); 351 | assert_eq!(childs[2], ChildNumber::from_hardened_idx(2).unwrap()); 352 | assert_eq!(childs[3], ChildNumber::from_normal_idx(0).unwrap()); 353 | assert_eq!(childs[4], ChildNumber::from_normal_idx(35).unwrap()); 354 | assert_eq!(childs[5], ChildNumber::from_normal_idx(81).unwrap()); 355 | assert_eq!(childs[6], ChildNumber::from_normal_idx(0).unwrap()); 356 | } 357 | 358 | } -------------------------------------------------------------------------------- /src/path_short.rs: -------------------------------------------------------------------------------- 1 | use crate::{Purpose, CustomHDPath, Error, PathValue}; 2 | use std::convert::TryFrom; 3 | #[cfg(feature = "with-bitcoin")] 4 | use bitcoin::bip32::{ChildNumber, DerivationPath}; 5 | use std::str::FromStr; 6 | use crate::traits::HDPath; 7 | use std::fmt; 8 | 9 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 10 | pub struct ShortHDPath { 11 | pub purpose: Purpose, 12 | pub coin_type: u32, 13 | pub account: u32, 14 | pub index: u32 15 | } 16 | 17 | impl HDPath for ShortHDPath { 18 | fn len(&self) -> u8 { 19 | 4 20 | } 21 | 22 | fn get(&self, pos: u8) -> Option { 23 | match pos { 24 | 0 => Some(self.purpose.as_value()), 25 | 1 => Some(PathValue::Hardened(self.coin_type)), 26 | 2 => Some(PathValue::Hardened(self.account)), 27 | 3 => Some(PathValue::Normal(self.index)), 28 | _ => None 29 | } 30 | } 31 | } 32 | 33 | impl TryFrom for ShortHDPath { 34 | type Error = Error; 35 | 36 | fn try_from(value: CustomHDPath) -> Result { 37 | if value.0.len() != 4 { 38 | return Err(Error::InvalidLength(value.0.len())) 39 | } 40 | if let Some(PathValue::Hardened(p)) = value.0.get(0) { 41 | let purpose = Purpose::try_from(*p)?; 42 | if let Some(PathValue::Hardened(coin_type)) = value.0.get(1) { 43 | if let Some(PathValue::Hardened(account)) = value.0.get(2) { 44 | if let Some(PathValue::Normal(index)) = value.0.get(3) { 45 | return Ok(ShortHDPath { 46 | purpose, 47 | coin_type: *coin_type, 48 | account: *account, 49 | index: *index 50 | }) 51 | } 52 | } 53 | } 54 | Err(Error::InvalidStructure) 55 | } else { 56 | Err(Error::InvalidStructure) 57 | } 58 | } 59 | } 60 | 61 | impl TryFrom<&str> for ShortHDPath 62 | { 63 | type Error = Error; 64 | 65 | fn try_from(value: &str) -> Result { 66 | ShortHDPath::from_str(value) 67 | } 68 | } 69 | 70 | impl FromStr for ShortHDPath { 71 | type Err = Error; 72 | 73 | fn from_str(s: &str) -> Result { 74 | let value = CustomHDPath::from_str(s)?; 75 | ShortHDPath::try_from(value) 76 | } 77 | } 78 | 79 | impl fmt::Display for ShortHDPath { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | write!(f, "m/{}'/{}'/{}'/{}", 82 | self.purpose.as_value().as_number(), 83 | self.coin_type, 84 | self.account, 85 | self.index 86 | ) 87 | } 88 | } 89 | 90 | #[cfg(feature = "with-bitcoin")] 91 | impl std::convert::From<&ShortHDPath> for Vec { 92 | fn from(value: &ShortHDPath) -> Self { 93 | let result = [ 94 | ChildNumber::from_hardened_idx(value.purpose.as_value().as_number()) 95 | .expect("Purpose is not Hardened"), 96 | ChildNumber::from_hardened_idx(value.coin_type) 97 | .expect("Coin Type is not Hardened"), 98 | ChildNumber::from_hardened_idx(value.account) 99 | .expect("Account is not Hardened"), 100 | ChildNumber::from_normal_idx(value.index) 101 | .expect("Index is Hardened"), 102 | ]; 103 | return result.to_vec(); 104 | } 105 | } 106 | 107 | #[cfg(feature = "with-bitcoin")] 108 | impl std::convert::From for Vec { 109 | fn from(value: ShortHDPath) -> Self { 110 | Vec::::from(&value) 111 | } 112 | } 113 | 114 | #[cfg(feature = "with-bitcoin")] 115 | impl std::convert::From for DerivationPath { 116 | fn from(value: ShortHDPath) -> Self { 117 | DerivationPath::from(Vec::::from(&value)) 118 | } 119 | } 120 | 121 | #[cfg(feature = "with-bitcoin")] 122 | impl std::convert::From<&ShortHDPath> for DerivationPath { 123 | fn from(value: &ShortHDPath) -> Self { 124 | DerivationPath::from(Vec::::from(value)) 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[test] 133 | pub fn to_string_short() { 134 | assert_eq!("m/44'/60'/0'/0", ShortHDPath { 135 | purpose: Purpose::Pubkey, 136 | coin_type: 60, 137 | account: 0, 138 | index: 0 139 | }.to_string()); 140 | assert_eq!("m/44'/61'/0'/0", ShortHDPath { 141 | purpose: Purpose::Pubkey, 142 | coin_type: 61, 143 | account: 0, 144 | index: 0 145 | }.to_string()); 146 | assert_eq!("m/101'/61'/0'/0", ShortHDPath { 147 | purpose: Purpose::Custom(101), 148 | coin_type: 61, 149 | account: 0, 150 | index: 0 151 | }.to_string()); 152 | } 153 | 154 | #[test] 155 | pub fn to_string_short_all() { 156 | let paths = vec![ 157 | "m/44'/0'/0'/0", 158 | "m/44'/60'/0'/1", 159 | "m/44'/60'/160720'/0", 160 | "m/44'/60'/160720'/101", 161 | ]; 162 | for p in paths { 163 | assert_eq!(p, ShortHDPath::try_from(p).unwrap().to_string()) 164 | } 165 | } 166 | } 167 | 168 | #[cfg(all(test, feature = "with-bitcoin"))] 169 | mod tests_with_bitcoin { 170 | use super::*; 171 | use std::convert::TryFrom; 172 | use bitcoin::bip32::ChildNumber; 173 | 174 | #[test] 175 | pub fn convert_to_childnumbers() { 176 | let hdpath = ShortHDPath::try_from("m/44'/60'/2'/100").unwrap(); 177 | let childs: Vec = hdpath.into(); 178 | assert_eq!(childs.len(), 4); 179 | assert_eq!(childs[0], ChildNumber::from_hardened_idx(44).unwrap()); 180 | assert_eq!(childs[1], ChildNumber::from_hardened_idx(60).unwrap()); 181 | assert_eq!(childs[2], ChildNumber::from_hardened_idx(2).unwrap()); 182 | assert_eq!(childs[3], ChildNumber::from_normal_idx(100).unwrap()); 183 | } 184 | 185 | } -------------------------------------------------------------------------------- /src/path_standard.rs: -------------------------------------------------------------------------------- 1 | use crate::{Purpose, PathValue, Error, CustomHDPath}; 2 | use std::convert::{TryFrom, TryInto}; 3 | #[cfg(feature = "with-bitcoin")] 4 | use bitcoin::bip32::{ChildNumber, DerivationPath}; 5 | use std::str::FromStr; 6 | use crate::traits::HDPath; 7 | use std::fmt; 8 | 9 | /// Standard HD Path for [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki), 10 | /// [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki), [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) 11 | /// and similar. For path as `m/purpose'/coin_type'/account'/change/address_index`, like `m/44'/0'/0'/0/0`. 12 | /// 13 | /// # Create new 14 | /// ``` 15 | /// use hdpath::{StandardHDPath, Purpose}; 16 | /// 17 | /// //creates path m/84'/0'/0'/0/0 18 | /// let hdpath = StandardHDPath::new(Purpose::Witness, 0, 0, 0, 0); 19 | /// ``` 20 | /// # Parse string 21 | /// ``` 22 | /// use hdpath::{StandardHDPath, Purpose}; 23 | /// # use std::str::FromStr; 24 | /// 25 | /// //creates path m/84'/0'/0'/0/0 26 | /// let hdpath = StandardHDPath::from_str("m/84'/0'/0'/0/0").unwrap(); 27 | /// ``` 28 | /// 29 | #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 30 | pub struct StandardHDPath { 31 | purpose: Purpose, 32 | coin_type: u32, 33 | account: u32, 34 | change: u32, 35 | index: u32 36 | } 37 | 38 | impl StandardHDPath { 39 | /// Create a standard HD Path. Panics if any of the values is incorrect 40 | ///``` 41 | ///use hdpath::{StandardHDPath, Purpose}; 42 | /// 43 | ///let hdpath = StandardHDPath::new(Purpose::Witness, 0, 2, 0, 0); 44 | ///``` 45 | pub fn new(purpose: Purpose, coin_type: u32, account: u32, change: u32, index: u32) -> StandardHDPath { 46 | match Self::try_new(purpose, coin_type, account, change, index) { 47 | Ok(path) => path, 48 | Err(err) => panic!("Invalid {}: {}", err.0, err.1) 49 | } 50 | } 51 | 52 | ///Try to create a standard HD Path. 53 | ///Return error `(field_name, invalid_value)` if a field has an incorrect value. 54 | ///``` 55 | ///use hdpath::{StandardHDPath, Purpose}; 56 | /// 57 | /// 58 | ///let index = 0x80000100; //received from unreliable source 59 | ///match StandardHDPath::try_new(Purpose::Witness, 0, 2, 0, index) { 60 | /// Ok(hdpath) => { } 61 | /// Err(err) => println!("Invalid value {} = {}", err.0, err.1) 62 | ///} 63 | ///``` 64 | pub fn try_new(purpose: Purpose, coin_type: u32, account: u32, change: u32, index: u32) -> Result { 65 | if let Purpose::Custom(n) = purpose { 66 | if !PathValue::is_ok(n) { 67 | return Err(("purpose".to_string(), n)); 68 | } 69 | } 70 | if !PathValue::is_ok(coin_type) { 71 | return Err(("coin_type".to_string(), coin_type)); 72 | } 73 | if !PathValue::is_ok(account) { 74 | return Err(("account".to_string(), account)); 75 | } 76 | if !PathValue::is_ok(change) { 77 | return Err(("change".to_string(), change)); 78 | } 79 | if !PathValue::is_ok(index) { 80 | return Err(("index".to_string(), index)); 81 | } 82 | Ok(StandardHDPath { 83 | purpose, 84 | coin_type, 85 | account, 86 | change, 87 | index, 88 | }) 89 | } 90 | 91 | pub fn purpose(&self) -> &Purpose { 92 | &self.purpose 93 | } 94 | 95 | pub fn coin_type(&self) -> u32 { 96 | self.coin_type 97 | } 98 | 99 | pub fn account(&self) -> u32 { 100 | self.account 101 | } 102 | 103 | pub fn change(&self) -> u32 { 104 | self.change 105 | } 106 | 107 | pub fn index(&self) -> u32 { 108 | self.index 109 | } 110 | 111 | /// Decode from bytes, where first byte is number of elements in path (always 5 for StandardHDPath) 112 | /// following by 4-byte BE values 113 | pub fn from_bytes(path: &[u8]) -> Result { 114 | if path.len() != 1 + 4 * 5 { 115 | return Err(Error::InvalidFormat); 116 | } 117 | if path[0] != 5u8 { 118 | return Err(Error::InvalidFormat); 119 | } 120 | 121 | let hdpath = StandardHDPath::try_new( 122 | Purpose::try_from(PathValue::from_raw(u32::from_be_bytes(path[1..5].try_into().unwrap())))?, 123 | PathValue::from_raw(u32::from_be_bytes(path[5..9].try_into().unwrap())).as_number(), 124 | PathValue::from_raw(u32::from_be_bytes(path[9..13].try_into().unwrap())).as_number(), 125 | PathValue::from_raw(u32::from_be_bytes(path[13..17].try_into().unwrap())).as_number(), 126 | PathValue::from_raw(u32::from_be_bytes(path[17..21].try_into().unwrap())).as_number(), 127 | ); 128 | hdpath.map_err(|_| Error::InvalidFormat) 129 | } 130 | } 131 | 132 | impl HDPath for StandardHDPath { 133 | fn len(&self) -> u8 { 134 | 5 135 | } 136 | 137 | fn get(&self, pos: u8) -> Option { 138 | match pos { 139 | 0 => Some(self.purpose.as_value()), 140 | 1 => Some(PathValue::Hardened(self.coin_type)), 141 | 2 => Some(PathValue::Hardened(self.account)), 142 | 3 => Some(PathValue::Normal(self.change)), 143 | 4 => Some(PathValue::Normal(self.index)), 144 | _ => None 145 | } 146 | } 147 | } 148 | 149 | impl Default for StandardHDPath { 150 | fn default() -> Self { 151 | StandardHDPath { 152 | purpose: Purpose::Pubkey, 153 | coin_type: 0, 154 | account: 0, 155 | change: 0, 156 | index: 0 157 | } 158 | } 159 | } 160 | 161 | impl TryFrom for StandardHDPath { 162 | type Error = Error; 163 | 164 | fn try_from(value: CustomHDPath) -> Result { 165 | if value.0.len() != 5 { 166 | return Err(Error::InvalidLength(value.0.len())) 167 | } 168 | if let Some(PathValue::Hardened(p)) = value.0.get(0) { 169 | let purpose = Purpose::try_from(*p)?; 170 | if let Some(PathValue::Hardened(coin_type)) = value.0.get(1) { 171 | if let Some(PathValue::Hardened(account)) = value.0.get(2) { 172 | if let Some(PathValue::Normal(change)) = value.0.get(3) { 173 | if let Some(PathValue::Normal(index)) = value.0.get(4) { 174 | return Ok(StandardHDPath::new( 175 | purpose, 176 | *coin_type, 177 | *account, 178 | *change, 179 | *index 180 | )) 181 | } 182 | } 183 | } 184 | } 185 | Err(Error::InvalidStructure) 186 | } else { 187 | Err(Error::InvalidStructure) 188 | } 189 | } 190 | } 191 | 192 | impl From for CustomHDPath { 193 | fn from(value: StandardHDPath) -> Self { 194 | CustomHDPath( 195 | vec![ 196 | value.purpose().as_value(), 197 | PathValue::Hardened(value.coin_type()), 198 | PathValue::Hardened(value.account()), 199 | PathValue::Normal(value.change()), 200 | PathValue::Normal(value.index()), 201 | ] 202 | ) 203 | } 204 | } 205 | 206 | impl TryFrom<&str> for StandardHDPath 207 | { 208 | type Error = Error; 209 | 210 | fn try_from(value: &str) -> Result { 211 | StandardHDPath::from_str(value) 212 | } 213 | } 214 | 215 | impl FromStr for StandardHDPath { 216 | type Err = Error; 217 | 218 | fn from_str(s: &str) -> Result { 219 | let value = CustomHDPath::from_str(s)?; 220 | StandardHDPath::try_from(value) 221 | } 222 | } 223 | 224 | impl fmt::Display for StandardHDPath { 225 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 226 | write!(f, "m/{}'/{}'/{}'/{}/{}", 227 | self.purpose().as_value().as_number(), 228 | self.coin_type(), 229 | self.account(), 230 | self.change(), 231 | self.index() 232 | ) 233 | } 234 | } 235 | 236 | #[cfg(feature = "with-bitcoin")] 237 | impl std::convert::From<&StandardHDPath> for Vec { 238 | fn from(value: &StandardHDPath) -> Self { 239 | let result = [ 240 | ChildNumber::from_hardened_idx(value.purpose().as_value().as_number()) 241 | .expect("Purpose is not Hardened"), 242 | ChildNumber::from_hardened_idx(value.coin_type()) 243 | .expect("Coin Type is not Hardened"), 244 | ChildNumber::from_hardened_idx(value.account()) 245 | .expect("Account is not Hardened"), 246 | ChildNumber::from_normal_idx(value.change()) 247 | .expect("Change is Hardened"), 248 | ChildNumber::from_normal_idx(value.index()) 249 | .expect("Index is Hardened"), 250 | ]; 251 | return result.to_vec(); 252 | } 253 | } 254 | 255 | #[cfg(feature = "with-bitcoin")] 256 | impl std::convert::From for Vec { 257 | fn from(value: StandardHDPath) -> Self { 258 | Vec::::from(&value) 259 | } 260 | } 261 | 262 | #[cfg(feature = "with-bitcoin")] 263 | impl std::convert::From for DerivationPath { 264 | fn from(value: StandardHDPath) -> Self { 265 | DerivationPath::from(Vec::::from(&value)) 266 | } 267 | } 268 | 269 | #[cfg(feature = "with-bitcoin")] 270 | impl std::convert::From<&StandardHDPath> for DerivationPath { 271 | fn from(value: &StandardHDPath) -> Self { 272 | DerivationPath::from(Vec::::from(value)) 273 | } 274 | } 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | use super::*; 279 | use std::convert::TryFrom; 280 | use rand::{Rng}; 281 | 282 | #[test] 283 | pub fn from_custom() { 284 | let act = StandardHDPath::try_from( 285 | CustomHDPath::try_new(vec![ 286 | PathValue::Hardened(49), PathValue::Hardened(0), PathValue::Hardened(1), 287 | PathValue::Normal(0), PathValue::Normal(5) 288 | ]).unwrap() 289 | ).unwrap(); 290 | assert_eq!( 291 | StandardHDPath::new(Purpose::ScriptHash, 0, 1, 0, 5), 292 | act 293 | ); 294 | 295 | let act = StandardHDPath::try_from( 296 | CustomHDPath::try_new(vec![ 297 | PathValue::Hardened(44), PathValue::Hardened(60), PathValue::Hardened(1), 298 | PathValue::Normal(0), PathValue::Normal(0) 299 | ]).unwrap() 300 | ).unwrap(); 301 | assert_eq!( 302 | StandardHDPath::new(Purpose::Pubkey, 60, 1, 0, 0), 303 | act 304 | ); 305 | } 306 | 307 | #[test] 308 | pub fn create_from_str() { 309 | let standard = StandardHDPath::from_str("m/49'/0'/1'/0/5").unwrap(); 310 | let act = CustomHDPath::from(standard); 311 | assert_eq!(5, act.0.len()); 312 | assert_eq!(&PathValue::Hardened(49), act.0.get(0).unwrap()); 313 | assert_eq!(&PathValue::Hardened(0), act.0.get(1).unwrap()); 314 | assert_eq!(&PathValue::Hardened(1), act.0.get(2).unwrap()); 315 | assert_eq!(&PathValue::Normal(0), act.0.get(3).unwrap()); 316 | assert_eq!(&PathValue::Normal(5), act.0.get(4).unwrap()); 317 | } 318 | 319 | #[test] 320 | pub fn from_standard_to_custom() { 321 | let standard = StandardHDPath::try_from("m/49'/0'/1'/0/5").unwrap(); 322 | let act = CustomHDPath::from(standard); 323 | assert_eq!(5, act.0.len()); 324 | assert_eq!(&PathValue::Hardened(49), act.0.get(0).unwrap()); 325 | assert_eq!(&PathValue::Hardened(0), act.0.get(1).unwrap()); 326 | assert_eq!(&PathValue::Hardened(1), act.0.get(2).unwrap()); 327 | assert_eq!(&PathValue::Normal(0), act.0.get(3).unwrap()); 328 | assert_eq!(&PathValue::Normal(5), act.0.get(4).unwrap()); 329 | } 330 | 331 | #[test] 332 | pub fn to_standard_path_with_custom_purpose() { 333 | let act = StandardHDPath::try_from("m/101'/0'/1'/0/5").unwrap(); 334 | assert_eq!( 335 | StandardHDPath::new(Purpose::Custom(101), 0, 1, 0, 5), 336 | act 337 | ); 338 | } 339 | 340 | #[test] 341 | pub fn err_to_standard_path_not_hardened() { 342 | let paths = vec![ 343 | "m/49/0'/1'/0/5", 344 | "m/49'/0/1'/0/5", 345 | "m/49'/0'/1/0/5", 346 | "m/49/0/1'/0/5", 347 | ]; 348 | for p in paths { 349 | let custom = CustomHDPath::try_from(p).expect(format!("failed for: {}", p).as_str()); 350 | assert!(StandardHDPath::try_from(custom).is_err(), "test: {}", p); 351 | } 352 | } 353 | 354 | #[test] 355 | pub fn to_string_standard_all() { 356 | let paths = vec![ 357 | "m/44'/0'/0'/0/0", 358 | "m/44'/60'/0'/0/1", 359 | "m/44'/60'/160720'/0/2", 360 | "m/49'/0'/0'/0/0", 361 | "m/49'/0'/1'/0/5", 362 | "m/84'/0'/0'/0/0", 363 | "m/84'/0'/0'/1/120", 364 | "m/101'/0'/0'/1/101", 365 | ]; 366 | for p in paths { 367 | assert_eq!(p, StandardHDPath::try_from(p).unwrap().to_string()) 368 | } 369 | } 370 | 371 | #[test] 372 | pub fn order() { 373 | let path1 = StandardHDPath::new(Purpose::Pubkey, 0, 0, 0, 0); 374 | let path2 = StandardHDPath::new(Purpose::Pubkey, 0, 0, 0, 1); 375 | let path3 = StandardHDPath::new(Purpose::Pubkey, 0, 0, 1, 1); 376 | let path4 = StandardHDPath::new(Purpose::Witness, 0, 2, 0, 100); 377 | let path5 = StandardHDPath::new(Purpose::Witness, 0, 3, 0, 0); 378 | 379 | assert!(path1 < path2); 380 | assert!(path1 < path3); 381 | assert!(path1 < path4); 382 | assert!(path1 < path5); 383 | 384 | assert!(path2 > path1); 385 | assert!(path2 < path3); 386 | assert!(path2 < path4); 387 | assert!(path2 < path5); 388 | 389 | assert!(path3 > path1); 390 | assert!(path3 > path2); 391 | assert!(path3 < path4); 392 | assert!(path3 < path5); 393 | 394 | assert!(path4 > path1); 395 | assert!(path4 > path2); 396 | assert!(path4 > path3); 397 | assert!(path4 < path5); 398 | 399 | assert!(path5 > path1); 400 | assert!(path5 > path2); 401 | assert!(path5 > path3); 402 | assert!(path5 > path4); 403 | } 404 | 405 | #[test] 406 | pub fn order_with_custom_purpose() { 407 | let path1 = StandardHDPath::new(Purpose::Pubkey, 0, 0, 0, 0); 408 | let path2 = StandardHDPath::new(Purpose::Custom(60), 0, 0, 0, 0); 409 | let path3 = StandardHDPath::new(Purpose::Witness, 0, 0, 0, 0); 410 | 411 | assert!(path1 < path2); 412 | assert!(path2 < path3); 413 | } 414 | 415 | #[test] 416 | #[should_panic] 417 | pub fn panic_to_create_invalid_coin() { 418 | StandardHDPath::new(Purpose::Pubkey, 0x80000000, 0, 0, 1); 419 | } 420 | 421 | #[test] 422 | #[should_panic] 423 | pub fn panic_to_create_invalid_account() { 424 | StandardHDPath::new(Purpose::Pubkey, 0, 0x80000000, 0, 1); 425 | } 426 | 427 | #[test] 428 | #[should_panic] 429 | pub fn panic_to_create_invalid_change() { 430 | StandardHDPath::new(Purpose::Pubkey, 0, 0, 0x80000000, 1); 431 | } 432 | 433 | #[test] 434 | #[should_panic] 435 | pub fn panic_to_create_invalid_index() { 436 | StandardHDPath::new(Purpose::Pubkey, 0, 0, 0, 0x80000000); 437 | } 438 | 439 | #[test] 440 | pub fn err_to_create_invalid_coin() { 441 | let act = StandardHDPath::try_new(Purpose::Pubkey, 2147483692, 0, 0, 1); 442 | assert_eq!(act, Err(("coin_type".to_string(), 2147483692))) 443 | } 444 | 445 | #[test] 446 | pub fn err_to_create_invalid_account() { 447 | let act = StandardHDPath::try_new(Purpose::Pubkey, 60, 2147483792, 0, 1); 448 | assert_eq!(act, Err(("account".to_string(), 2147483792))) 449 | } 450 | 451 | #[test] 452 | pub fn err_to_create_invalid_change() { 453 | let act = StandardHDPath::try_new(Purpose::Pubkey, 61, 0, 2147484692, 1); 454 | assert_eq!(act, Err(("change".to_string(), 2147484692))) 455 | } 456 | 457 | #[test] 458 | pub fn err_to_create_invalid_index() { 459 | let act = StandardHDPath::try_new(Purpose::Pubkey, 0, 0, 0, 2474893692); 460 | assert_eq!(act, Err(("index".to_string(), 2474893692))) 461 | } 462 | 463 | #[test] 464 | pub fn convert_to_bytes_base() { 465 | let exp: [u8; 21] = [ 466 | 5, 467 | 0x80, 0, 0, 44, 468 | 0x80, 0, 0, 0, 469 | 0x80, 0, 0, 0, 470 | 0, 0, 0, 0, 471 | 0, 0, 0, 0, 472 | ]; 473 | 474 | let parsed = StandardHDPath::try_from("m/44'/0'/0'/0/0").unwrap(); 475 | assert_eq!(parsed.to_bytes(), exp) 476 | } 477 | 478 | #[test] 479 | pub fn convert_from_bytes_base() { 480 | let data: [u8; 21] = [ 481 | 5, 482 | 0x80, 0, 0, 44, 483 | 0x80, 0, 0, 0, 484 | 0x80, 0, 0, 0, 485 | 0, 0, 0, 0, 486 | 0, 0, 0, 0, 487 | ]; 488 | 489 | assert_eq!(StandardHDPath::from_bytes(&data).unwrap(), 490 | StandardHDPath::try_from("m/44'/0'/0'/0/0").unwrap()) 491 | } 492 | 493 | #[test] 494 | pub fn convert_to_bytes_large_account() { 495 | let exp: [u8; 21] = [ 496 | 5, 497 | 0x80, 0, 0, 44, 498 | 0x80, 0, 0, 60, 499 | 0x80, 0x02, 0x73, 0xd0, 500 | 0, 0, 0, 0, 501 | 0, 0, 0, 0, 502 | ]; 503 | 504 | let parsed = StandardHDPath::try_from("m/44'/60'/160720'/0/0").unwrap(); 505 | assert_eq!(parsed.to_bytes(), exp) 506 | } 507 | 508 | #[test] 509 | pub fn convert_from_bytes_large_account() { 510 | let data: [u8; 21] = [ 511 | 5, 512 | 0x80, 0, 0, 44, 513 | 0x80, 0, 0, 60, 514 | 0x80, 0x02, 0x73, 0xd0, 515 | 0, 0, 0, 0, 516 | 0, 0, 0, 0, 517 | ]; 518 | 519 | assert_eq!(StandardHDPath::from_bytes(&data).unwrap(), 520 | StandardHDPath::try_from("m/44'/60'/160720'/0/0").unwrap()) 521 | } 522 | 523 | #[test] 524 | fn convert_to_bytes_witness() { 525 | let exp: [u8; 21] = [ 526 | 5, 527 | 0x80, 0, 0, 84, 528 | 0x80, 0, 0, 0, 529 | 0x80, 0, 0, 2, 530 | 0, 0, 0, 0, 531 | 0, 0, 0, 101, 532 | ]; 533 | 534 | let parsed = StandardHDPath::try_from("m/84'/0'/2'/0/101").unwrap(); 535 | assert_eq!(parsed.to_bytes(), exp) 536 | } 537 | 538 | #[test] 539 | pub fn convert_from_bytes_large_witness() { 540 | let data: [u8; 21] = [ 541 | 5, 542 | 0x80, 0, 0, 84, 543 | 0x80, 0, 0, 0, 544 | 0x80, 0, 0, 2, 545 | 0, 0, 0, 0, 546 | 0, 0, 0, 101, 547 | ]; 548 | 549 | assert_eq!(StandardHDPath::from_bytes(&data).unwrap(), 550 | StandardHDPath::try_from("m/84'/0'/2'/0/101").unwrap()) 551 | } 552 | 553 | #[test] 554 | fn convert_to_bytes_change() { 555 | let exp: [u8; 21] = [ 556 | 5, 557 | 0x80, 0, 0, 44, 558 | 0x80, 0, 0, 0, 559 | 0x80, 0, 0, 5, 560 | 0, 0, 0, 1, 561 | 0, 0, 0, 7, 562 | ]; 563 | 564 | let parsed = StandardHDPath::try_from("m/44'/0'/5'/1/7").unwrap(); 565 | assert_eq!(parsed.to_bytes(), exp) 566 | } 567 | 568 | #[test] 569 | pub fn convert_from_bytes_change() { 570 | let data: [u8; 21] = [ 571 | 5, 572 | 0x80, 0, 0, 44, 573 | 0x80, 0, 0, 0, 574 | 0x80, 0, 0, 5, 575 | 0, 0, 0, 1, 576 | 0, 0, 0, 7, 577 | ]; 578 | 579 | assert_eq!(StandardHDPath::from_bytes(&data).unwrap(), 580 | StandardHDPath::try_from("m/44'/0'/5'/1/7").unwrap()) 581 | } 582 | 583 | #[test] 584 | fn convert_to_bytes_index() { 585 | let exp: [u8; 21] = [ 586 | 5, 587 | 0x80, 0, 0, 44, 588 | 0x80, 0, 0, 60, 589 | 0x80, 0x02, 0x73, 0xd0, 590 | 0, 0, 0, 0, 591 | 0, 0, 0x02, 0x45, 592 | ]; 593 | 594 | let parsed = StandardHDPath::try_from("m/44'/60'/160720'/0/581").unwrap(); 595 | assert_eq!(parsed.to_bytes(), exp) 596 | } 597 | 598 | #[test] 599 | pub fn convert_from_bytes_index() { 600 | let data: [u8; 21] = [ 601 | 5, 602 | 0x80, 0, 0, 44, 603 | 0x80, 0, 0, 60, 604 | 0x80, 0x02, 0x73, 0xd0, 605 | 0, 0, 0, 0, 606 | 0, 0, 0x02, 0x45, 607 | ]; 608 | 609 | assert_eq!(StandardHDPath::from_bytes(&data).unwrap(), 610 | StandardHDPath::try_from("m/44'/60'/160720'/0/581").unwrap()) 611 | } 612 | 613 | #[test] 614 | pub fn cannot_convert_from_short_bytes() { 615 | let data: [u8; 17] = [ 616 | 5, 617 | 0x80, 0, 0, 44, 618 | 0x80, 0, 0, 60, 619 | 0x80, 0x02, 0x73, 0xd0, 620 | 0, 0, 0, 0, 621 | ]; 622 | 623 | assert!(StandardHDPath::from_bytes(&data).is_err()) 624 | } 625 | 626 | #[test] 627 | pub fn cannot_convert_from_invalid_prefix() { 628 | let data: [u8; 21] = [ 629 | 4, 630 | 0x80, 0, 0, 44, 631 | 0x80, 0, 0, 60, 632 | 0x80, 0x02, 0x73, 0xd0, 633 | 0, 0, 0, 0, 634 | 0, 0, 0, 0, 635 | ]; 636 | 637 | assert!(StandardHDPath::from_bytes(&data).is_err()) 638 | } 639 | 640 | #[test] 641 | pub fn test_random_conversion() { 642 | let range = |count: usize| { 643 | let mut rng = rand::rng(); 644 | let mut result: Vec = Vec::with_capacity(count); 645 | for _i in 0..count { 646 | result.push(rng.random_range(0u32..0x80000000u32)); 647 | } 648 | result 649 | }; 650 | 651 | for purpose in [Purpose::Pubkey, Purpose::ScriptHash, Purpose::Witness, Purpose::Custom(101), Purpose::Custom(0x11223344)].iter() { 652 | for coin_type in [0u32, 60, 61, 1001, 0x01234567].iter() { 653 | for account in range(100) { 654 | for change in 0..1 { 655 | for index in range(1000) { 656 | let orig = StandardHDPath::new( 657 | purpose.clone(), *coin_type, account, 658 | change, index 659 | ); 660 | let bytes = orig.to_bytes(); 661 | let parsed = StandardHDPath::from_bytes(&bytes).expect("Should parse"); 662 | assert_eq!( 663 | parsed, orig, 664 | "test m/{}'/{}'/{}'/{}/{}", purpose.as_value().as_number(), coin_type, account, change, index 665 | ) 666 | } 667 | } 668 | } 669 | 670 | } 671 | } 672 | } 673 | } 674 | 675 | #[cfg(all(test, feature = "with-bitcoin"))] 676 | mod tests_with_bitcoin { 677 | use super::*; 678 | use std::convert::TryFrom; 679 | use bitcoin::bip32::ChildNumber; 680 | 681 | #[test] 682 | pub fn convert_to_childnumbers() { 683 | let hdpath = StandardHDPath::try_from("m/44'/60'/2'/0/3581").unwrap(); 684 | let children: Vec = hdpath.into(); 685 | assert_eq!(children.len(), 5); 686 | assert_eq!(children[0], ChildNumber::from_hardened_idx(44).unwrap()); 687 | assert_eq!(children[1], ChildNumber::from_hardened_idx(60).unwrap()); 688 | assert_eq!(children[2], ChildNumber::from_hardened_idx(2).unwrap()); 689 | assert_eq!(children[3], ChildNumber::from_normal_idx(0).unwrap()); 690 | assert_eq!(children[4], ChildNumber::from_normal_idx(3581).unwrap()); 691 | } 692 | 693 | } -------------------------------------------------------------------------------- /src/path_value.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-bitcoin")] 2 | use bitcoin::bip32::ChildNumber; 3 | 4 | pub const FIRST_BIT: u32 = 0x80000000; 5 | 6 | #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] 7 | pub enum PathValue { 8 | Normal(u32), 9 | Hardened(u32), 10 | } 11 | 12 | impl PathValue { 13 | pub fn is_ok(value: u32) -> bool { 14 | value < FIRST_BIT 15 | } 16 | 17 | pub fn try_normal(value: u32) -> Result { 18 | if !PathValue::is_ok(value) { 19 | Err(()) 20 | } else { 21 | Ok(PathValue::Normal(value)) 22 | } 23 | } 24 | 25 | pub fn normal(value: u32) -> PathValue { 26 | if let Ok(result) = PathValue::try_normal(value) { 27 | result 28 | } else { 29 | panic!("Raw hardened value passed") 30 | } 31 | } 32 | 33 | pub fn try_hardened(value: u32) -> Result { 34 | if !PathValue::is_ok(value) { 35 | Err(()) 36 | } else { 37 | Ok(PathValue::Hardened(value)) 38 | } 39 | } 40 | 41 | pub fn hardened(value: u32) -> PathValue { 42 | if let Ok(result) = PathValue::try_hardened(value) { 43 | result 44 | } else { 45 | panic!("Raw hardened value passed") 46 | } 47 | } 48 | 49 | pub fn from_raw(value: u32) -> PathValue { 50 | if value >= FIRST_BIT { 51 | PathValue::Hardened(value - FIRST_BIT) 52 | } else { 53 | PathValue::Normal(value) 54 | } 55 | } 56 | 57 | pub fn as_number(&self) -> u32 { 58 | match &self { 59 | PathValue::Normal(n) => *n, 60 | PathValue::Hardened(n) => *n 61 | } 62 | } 63 | 64 | pub fn to_raw(&self) -> u32 { 65 | match &self { 66 | PathValue::Normal(n) => *n, 67 | PathValue::Hardened(n) => *n + FIRST_BIT 68 | } 69 | } 70 | } 71 | 72 | #[cfg(feature = "with-bitcoin")] 73 | impl From for ChildNumber { 74 | fn from(value: PathValue) -> Self { 75 | match value { 76 | PathValue::Hardened(i) => ChildNumber::from_hardened_idx(i).unwrap(), 77 | PathValue::Normal(i) => ChildNumber::from_normal_idx(i).unwrap(), 78 | } 79 | } 80 | } 81 | 82 | impl std::fmt::Display for PathValue { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | match self { 85 | PathValue::Normal(n) => write!(f, "{}", n), 86 | PathValue::Hardened(n) => write!(f, "{}'", n) 87 | } 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | #[cfg(feature = "with-bitcoin")] 95 | use bitcoin::bip32::ChildNumber; 96 | 97 | #[test] 98 | #[cfg(feature = "with-bitcoin")] 99 | fn convert_to_bitcoin() { 100 | let act: ChildNumber = PathValue::Normal(0).into(); 101 | assert_eq!(ChildNumber::from_normal_idx(0).unwrap(), act); 102 | 103 | let act: ChildNumber = PathValue::Normal(1).into(); 104 | assert_eq!(ChildNumber::from_normal_idx(1).unwrap(), act); 105 | 106 | let act: ChildNumber = PathValue::Normal(100).into(); 107 | assert_eq!(ChildNumber::from_normal_idx(100).unwrap(), act); 108 | 109 | let act: ChildNumber = PathValue::Hardened(0).into(); 110 | assert_eq!(ChildNumber::from_hardened_idx(0).unwrap(), act); 111 | 112 | let act: ChildNumber = PathValue::Hardened(1).into(); 113 | assert_eq!(ChildNumber::from_hardened_idx(1).unwrap(), act); 114 | 115 | let act: ChildNumber = PathValue::Hardened(11).into(); 116 | assert_eq!(ChildNumber::from_hardened_idx(11).unwrap(), act); 117 | } 118 | 119 | #[test] 120 | fn to_string_normal() { 121 | assert_eq!(PathValue::Normal(0).to_string(), "0"); 122 | assert_eq!(PathValue::Normal(11).to_string(), "11"); 123 | } 124 | 125 | #[test] 126 | fn to_string_hardened() { 127 | assert_eq!(PathValue::Hardened(0).to_string(), "0'"); 128 | assert_eq!(PathValue::Hardened(1).to_string(), "1'"); 129 | } 130 | 131 | #[test] 132 | fn display_normal() { 133 | assert_eq!(format!("{}", PathValue::Normal(0)), "0"); 134 | assert_eq!(format!("{}", PathValue::Normal(11)), "11"); 135 | } 136 | 137 | #[test] 138 | fn display_hardened() { 139 | assert_eq!(format!("{}", PathValue::Hardened(0)), "0'"); 140 | assert_eq!(format!("{}", PathValue::Hardened(11)), "11'"); 141 | } 142 | 143 | #[test] 144 | fn ok_for_small_values() { 145 | let values = vec![ 146 | 0u32, 1, 2, 3, 147 | 100, 1000, 10000, 148 | 0x80000000 - 1 149 | ]; 150 | for value in values { 151 | assert!(PathValue::is_ok(value), "value: {}", value); 152 | } 153 | } 154 | 155 | #[test] 156 | fn not_ok_for_large_values() { 157 | let values = vec![ 158 | 0x80000000, 0x80000001, 159 | 0xffffffff 160 | ]; 161 | for value in values { 162 | assert!(!PathValue::is_ok(value), "value: {}", value); 163 | } 164 | } 165 | 166 | #[test] 167 | fn create_normal() { 168 | assert_eq!(PathValue::Normal(0), PathValue::normal(0)); 169 | assert_eq!(PathValue::Normal(1), PathValue::normal(1)); 170 | assert_eq!(PathValue::Normal(101), PathValue::normal(101)); 171 | } 172 | 173 | #[test] 174 | fn create_hardened() { 175 | assert_eq!(PathValue::Hardened(0), PathValue::hardened(0)); 176 | assert_eq!(PathValue::Hardened(1), PathValue::hardened(1)); 177 | assert_eq!(PathValue::Hardened(101), PathValue::hardened(101)); 178 | } 179 | 180 | #[test] 181 | #[should_panic] 182 | fn panic_on_invalid_normal() { 183 | PathValue::normal(0x80000001); 184 | } 185 | 186 | #[test] 187 | #[should_panic] 188 | fn panic_on_invalid_hardened() { 189 | PathValue::hardened(0x80000001); 190 | } 191 | 192 | #[test] 193 | fn create_from_raw_normal() { 194 | assert_eq!(PathValue::Normal(0), PathValue::from_raw(0)); 195 | assert_eq!(PathValue::Normal(100), PathValue::from_raw(100)); 196 | assert_eq!(PathValue::Normal(0xffffff), PathValue::from_raw(0xffffff)); 197 | } 198 | 199 | #[test] 200 | fn create_from_raw_hardened() { 201 | assert_eq!(PathValue::hardened(0), PathValue::from_raw(0x80000000)); 202 | assert_eq!(PathValue::hardened(1), PathValue::from_raw(0x80000001)); 203 | assert_eq!(PathValue::hardened(44), PathValue::from_raw(0x8000002c)); 204 | } 205 | 206 | #[test] 207 | fn to_raw_normal() { 208 | assert_eq!(0, PathValue::Normal(0).to_raw()); 209 | assert_eq!(123, PathValue::Normal(123).to_raw()); 210 | } 211 | 212 | #[test] 213 | fn to_raw_hardened() { 214 | assert_eq!(0x80000000, PathValue::Hardened(0).to_raw()); 215 | assert_eq!(0x8000002c, PathValue::Hardened(44).to_raw()); 216 | } 217 | 218 | #[test] 219 | fn as_number_normal() { 220 | assert_eq!(0, PathValue::Normal(0).as_number()); 221 | assert_eq!(123, PathValue::Normal(123).as_number()); 222 | } 223 | 224 | #[test] 225 | fn as_number_hardened() { 226 | assert_eq!(0, PathValue::Hardened(0).as_number()); 227 | assert_eq!(123, PathValue::Hardened(123).as_number()); 228 | } 229 | } -------------------------------------------------------------------------------- /src/purpose.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use crate::{PathValue, Error}; 3 | use std::convert::TryFrom; 4 | #[cfg(feature = "with-bitcoin")] 5 | use bitcoin::bip32::{ChildNumber}; 6 | 7 | /// The purpose number, a first number in HD Path, which is supposed to be reference actual format. Supposed to be a hardened value 8 | /// See [BIP-43](https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki) 9 | #[derive(Debug, Clone, Eq, Hash)] 10 | pub enum Purpose { 11 | None, //0' 12 | Pubkey, //44' 13 | ScriptHash, //49' 14 | Witness, //84' 15 | Custom(u32) 16 | } 17 | 18 | impl PartialOrd for Purpose { 19 | fn partial_cmp(&self, other: &Self) -> Option { 20 | if self.as_value().to_raw() > other.as_value().to_raw() { 21 | Some(Ordering::Greater) 22 | } else if self.as_value().to_raw() == other.as_value().to_raw() { 23 | Some(Ordering::Equal) 24 | } else { 25 | Some(Ordering::Less) 26 | } 27 | } 28 | } 29 | 30 | impl Ord for Purpose { 31 | fn cmp(&self, other: &Self) -> Ordering { 32 | if self.as_value().to_raw() > other.as_value().to_raw() { 33 | Ordering::Greater 34 | } else if self.as_value().to_raw() == other.as_value().to_raw() { 35 | Ordering::Equal 36 | } else { 37 | Ordering::Less 38 | } 39 | } 40 | } 41 | 42 | impl PartialEq for Purpose { 43 | fn eq(&self, other: &Self) -> bool { 44 | self.as_value().to_raw() == other.as_value().to_raw() 45 | } 46 | } 47 | 48 | impl Purpose { 49 | pub fn as_value(&self) -> PathValue { 50 | let n = match self { 51 | Purpose::None => 0, 52 | Purpose::Pubkey => 44, 53 | Purpose::ScriptHash => 49, 54 | Purpose::Witness => 84, 55 | Purpose::Custom(n) => *n 56 | }; 57 | PathValue::Hardened(n) 58 | } 59 | } 60 | 61 | impl TryFrom for Purpose { 62 | type Error = Error; 63 | 64 | fn try_from(value: u32) -> Result { 65 | match value { 66 | 44 => Ok(Purpose::Pubkey), 67 | 49 => Ok(Purpose::ScriptHash), 68 | 84 => Ok(Purpose::Witness), 69 | n => if PathValue::is_ok(n) { 70 | Ok(Purpose::Custom(n)) 71 | } else { 72 | Err(Error::HighBitIsSet) 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl From for u32 { 79 | fn from(value: Purpose) -> Self { 80 | match value { 81 | Purpose::None => 0, 82 | Purpose::Pubkey => 44, 83 | Purpose::ScriptHash => 49, 84 | Purpose::Witness => 84, 85 | Purpose::Custom(n) => n.clone() 86 | } 87 | } 88 | } 89 | 90 | impl From<&Purpose> for u32 { 91 | fn from(value: &Purpose) -> Self { 92 | match value { 93 | Purpose::None => 0, 94 | Purpose::Pubkey => 44, 95 | Purpose::ScriptHash => 49, 96 | Purpose::Witness => 84, 97 | Purpose::Custom(n) => n.clone() 98 | } 99 | } 100 | } 101 | 102 | impl TryFrom for Purpose { 103 | type Error = Error; 104 | 105 | fn try_from(value: usize) -> Result { 106 | Purpose::try_from(value as u32) 107 | } 108 | } 109 | 110 | impl TryFrom for Purpose { 111 | type Error = Error; 112 | 113 | fn try_from(value: i32) -> Result { 114 | if value < 0 { 115 | return Err(Error::InvalidPurpose(0)) 116 | } 117 | Purpose::try_from(value as u32) 118 | } 119 | } 120 | 121 | impl TryFrom for Purpose { 122 | type Error = Error; 123 | 124 | fn try_from(value: PathValue) -> Result { 125 | Purpose::try_from(value.as_number()) 126 | } 127 | } 128 | 129 | #[cfg(feature = "with-bitcoin")] 130 | impl From for ChildNumber { 131 | fn from(value: Purpose) -> Self { 132 | ChildNumber::from_hardened_idx(value.into()).unwrap() 133 | } 134 | } 135 | 136 | #[cfg(feature = "with-bitcoin")] 137 | impl From<&Purpose> for ChildNumber { 138 | fn from(value: &Purpose) -> Self { 139 | ChildNumber::from_hardened_idx(value.into()).unwrap() 140 | } 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | use super::*; 146 | use std::convert::TryFrom; 147 | 148 | #[test] 149 | pub fn create_standard_purpose() { 150 | assert_eq!(Purpose::Pubkey, Purpose::try_from(44 as u32).unwrap()); 151 | assert_eq!(Purpose::Pubkey, Purpose::try_from(44 as usize).unwrap()); 152 | assert_eq!(Purpose::Pubkey, Purpose::try_from(44).unwrap()); 153 | 154 | assert_eq!(Purpose::ScriptHash, Purpose::try_from(49).unwrap()); 155 | assert_eq!(Purpose::Witness, Purpose::try_from(84).unwrap()); 156 | } 157 | 158 | #[test] 159 | pub fn create_custom_purpose() { 160 | assert_eq!(Purpose::Custom(101), Purpose::try_from(101).unwrap()); 161 | } 162 | 163 | #[test] 164 | pub fn compare() { 165 | assert!(Purpose::None < Purpose::Witness); 166 | assert!(Purpose::None < Purpose::Pubkey); 167 | assert!(Purpose::Pubkey < Purpose::Witness); 168 | assert!(Purpose::ScriptHash < Purpose::Witness); 169 | assert!(Purpose::Custom(0) < Purpose::Witness); 170 | assert!(Purpose::Custom(100) > Purpose::Witness); 171 | assert!(Purpose::Custom(50) > Purpose::Pubkey); 172 | } 173 | 174 | #[test] 175 | pub fn order() { 176 | let mut values = [ 177 | Purpose::Witness, Purpose::None, Purpose::Pubkey, Purpose::ScriptHash, Purpose::Pubkey, 178 | Purpose::Custom(44), Purpose::Custom(84), Purpose::Custom(50), Purpose::Custom(1000) 179 | ]; 180 | values.sort(); 181 | assert_eq!( 182 | [ 183 | Purpose::None, Purpose::Pubkey, Purpose::Pubkey, Purpose::Custom(44), Purpose::ScriptHash, 184 | Purpose::Custom(50), Purpose::Witness, 185 | Purpose::Custom(84), Purpose::Custom(1000) 186 | ], 187 | values 188 | ) 189 | } 190 | 191 | } -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::{PathValue, CustomHDPath}; 2 | use byteorder::{BigEndian, WriteBytesExt}; 3 | #[cfg(feature = "with-bitcoin")] 4 | use bitcoin::bip32::{ChildNumber, DerivationPath}; 5 | 6 | /// General trait for an HDPath. 7 | /// Common implementations are [`StandardHDPath`], [`AccountHDPath`] and [`CustomHDPath`] 8 | /// 9 | /// [`StandardHDPath`]: struct.StandardHDPath.html 10 | /// [`AccountHDPath`]: struct.AccountHDPath.html 11 | /// [`CustomHDPath`]: struct.CustomHDPath.html 12 | pub trait HDPath { 13 | 14 | /// Size of the HD Path 15 | fn len(&self) -> u8; 16 | 17 | /// Get element as the specified position. 18 | /// The implementation must return `Some` for all values up to `len()`. 19 | /// And return `None` if the position if out of bounds. 20 | /// 21 | /// See [`PathValue`](enum.PathValue.html) 22 | fn get(&self, pos: u8) -> Option; 23 | 24 | /// Encode as bytes, where first byte is number of elements in path (always 5 for StandardHDPath) 25 | /// following by 4-byte BE values 26 | fn to_bytes(&self) -> Vec { 27 | let len = self.len(); 28 | let mut buf = Vec::with_capacity(1 + 4 * (len as usize)); 29 | buf.push(len); 30 | for i in 0..len { 31 | buf.write_u32::(self.get(i) 32 | .expect(format!("No valut at {}", i).as_str()) 33 | .to_raw()).unwrap(); 34 | } 35 | buf 36 | } 37 | 38 | /// 39 | /// Get parent HD Path. 40 | /// Return `None` if the current path is empty (i.e. already at the top) 41 | fn parent(&self) -> Option { 42 | if self.len() == 0 { 43 | return None 44 | } 45 | let len = self.len(); 46 | let mut parent_hd_path = Vec::with_capacity(len as usize - 1); 47 | for i in 0..len - 1 { 48 | parent_hd_path.push(self.get(i).unwrap()); 49 | } 50 | let parent_hd_path = CustomHDPath::try_new(parent_hd_path) 51 | .expect("No parent HD Path"); 52 | Some(parent_hd_path) 53 | } 54 | 55 | /// 56 | /// Convert current to `CustomHDPath` structure 57 | fn as_custom(&self) -> CustomHDPath { 58 | let len = self.len(); 59 | let mut path = Vec::with_capacity(len as usize); 60 | for i in 0..len { 61 | path.push(self.get(i).unwrap()); 62 | } 63 | CustomHDPath::try_new(path).expect("Invalid HD Path") 64 | } 65 | 66 | /// 67 | /// Convert current to bitcoin lib type 68 | #[cfg(feature = "with-bitcoin")] 69 | fn as_bitcoin(&self) -> DerivationPath { 70 | let len = self.len(); 71 | let mut path = Vec::with_capacity(len as usize); 72 | for i in 0..len { 73 | path.push(ChildNumber::from(self.get(i).unwrap())); 74 | } 75 | DerivationPath::from(path) 76 | } 77 | } 78 | 79 | #[cfg(feature = "with-bitcoin")] 80 | impl std::convert::From<&dyn HDPath> for DerivationPath { 81 | fn from(value: &dyn HDPath) -> Self { 82 | let mut path = Vec::with_capacity(value.len() as usize); 83 | for i in 0..value.len() { 84 | path.push(ChildNumber::from(value.get(i).expect("no-path-element"))); 85 | } 86 | DerivationPath::from(path) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use crate::{StandardHDPath, AccountHDPath}; 94 | use std::str::FromStr; 95 | 96 | impl StandardHDPath { 97 | pub fn to_trait(&self) -> &dyn HDPath { 98 | self 99 | } 100 | } 101 | 102 | #[test] 103 | fn get_parent_from_std() { 104 | let act = StandardHDPath::from_str("m/44'/0'/1'/1/2").unwrap(); 105 | let parent = act.parent(); 106 | assert!(parent.is_some()); 107 | let parent = parent.unwrap(); 108 | assert_eq!( 109 | "m/44'/0'/1'/1", parent.to_string() 110 | ); 111 | } 112 | 113 | #[test] 114 | fn get_parent_twice() { 115 | let act = StandardHDPath::from_str("m/44'/0'/1'/1/2").unwrap(); 116 | let parent = act.parent().unwrap().parent(); 117 | assert!(parent.is_some()); 118 | let parent = parent.unwrap(); 119 | assert_eq!( 120 | "m/44'/0'/1'", parent.to_string() 121 | ); 122 | } 123 | 124 | #[test] 125 | fn get_parent_from_account() { 126 | let act = AccountHDPath::from_str("m/84'/0'/1'").unwrap(); 127 | let parent = act.parent(); 128 | assert!(parent.is_some()); 129 | let parent = parent.unwrap(); 130 | assert_eq!( 131 | "m/84'/0'", parent.to_string() 132 | ); 133 | } 134 | 135 | #[test] 136 | fn get_parent_from_custom() { 137 | let act = CustomHDPath::from_str("m/84'/0'/1'/0/16").unwrap(); 138 | let parent = act.parent(); 139 | assert!(parent.is_some()); 140 | let parent = parent.unwrap(); 141 | assert_eq!( 142 | "m/84'/0'/1'/0", parent.to_string() 143 | ); 144 | } 145 | 146 | #[test] 147 | fn convert_account_to_custom() { 148 | let src = AccountHDPath::from_str("m/84'/0'/1'").unwrap(); 149 | let act = src.as_custom(); 150 | assert_eq!(CustomHDPath::from_str("m/84'/0'/1'").unwrap(), act); 151 | } 152 | 153 | #[test] 154 | fn convert_standard_to_custom() { 155 | let src = StandardHDPath::from_str("m/84'/0'/1'/0/2").unwrap(); 156 | let act = src.as_custom(); 157 | assert_eq!(CustomHDPath::from_str("m/84'/0'/1'/0/2").unwrap(), act); 158 | } 159 | } 160 | 161 | #[cfg(all(test, feature = "with-bitcoin"))] 162 | mod tests_with_bitcoin { 163 | use crate::{StandardHDPath, HDPath}; 164 | use std::str::FromStr; 165 | use bitcoin::bip32::{DerivationPath}; 166 | 167 | #[test] 168 | fn convert_to_bitcoin() { 169 | let source = StandardHDPath::from_str("m/44'/0'/1'/1/2").unwrap(); 170 | let act = DerivationPath::from(source.to_trait()); 171 | assert_eq!( 172 | DerivationPath::from_str("m/44'/0'/1'/1/2").unwrap(), 173 | act 174 | ) 175 | } 176 | 177 | #[test] 178 | fn convert_to_bitcoin_directly() { 179 | let source = StandardHDPath::from_str("m/44'/0'/1'/1/2").unwrap(); 180 | let act = source.as_bitcoin(); 181 | assert_eq!( 182 | DerivationPath::from_str("m/44'/0'/1'/1/2").unwrap(), 183 | act 184 | ) 185 | } 186 | } --------------------------------------------------------------------------------