├── .nvmrc ├── .eslintignore ├── packages ├── xpub-lib │ ├── .vscode │ │ └── settings.json │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ │ ├── constants.js │ │ ├── index.js │ │ ├── utils.test.js │ │ ├── types.js │ │ ├── conversion.js │ │ ├── purpose.js │ │ ├── conversion.test.js │ │ ├── utils.js │ │ ├── metadata.test.js │ │ ├── metadata.js │ │ ├── paths.js │ │ ├── validation.js │ │ ├── derivation.js │ │ ├── paths.test.js │ │ ├── validation.test.js │ │ └── derivation.test.js │ ├── package.json │ ├── README.md │ └── test │ │ └── fixtures.js └── xpub-cli │ ├── babel.config.js │ ├── __tests__ │ └── xpub-cli.test.js │ ├── package.json │ ├── README.md │ └── src │ └── xpub.js ├── package.json ├── .prettierrc ├── .vscode └── settings.json ├── .github └── workflows │ ├── ci.yml │ ├── publish.yaml │ └── dependency-review.yml ├── LICENSE ├── .eslintrc ├── .gitignore └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.12.2 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/ 2 | .cache/ 3 | node_modules/ 4 | lib/ 5 | -------------------------------------------------------------------------------- /packages/xpub-lib/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /packages/xpub-cli/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-env"], 3 | } 4 | -------------------------------------------------------------------------------- /packages/xpub-lib/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-env"], 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": false, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /packages/xpub-lib/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | testEnvironment: "node", 5 | transformIgnorePatterns: ['/node_modules/(?!(valibot)/)'], 6 | } 7 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module defines string constants for derivation paths. 3 | * 4 | * @module constants 5 | */ 6 | 7 | const SEPARATOR = "/" 8 | const APOSTROPHE = "'" 9 | const COIN_PREFIX = "m" 10 | 11 | export { SEPARATOR, APOSTROPHE, COIN_PREFIX } 12 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/index.js: -------------------------------------------------------------------------------- 1 | export { Network, networkLabel } from "@caravan/bitcoin" 2 | 3 | export * from "./conversion" 4 | export * from "./derivation" 5 | export * from "./metadata" 6 | export * from "./paths" 7 | export * from "./purpose" 8 | export * from "./types" 9 | export * from "./utils" 10 | export * from "./validation" 11 | -------------------------------------------------------------------------------- /packages/xpub-cli/__tests__/xpub-cli.test.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const xpubCli = require("..") 4 | 5 | // For a comprehensive test suite please refer to the @swan-bitcoin/xpub-lib 6 | // https://github.com/swan-bitcoin/xpub-tool/tree/master/packages/xpub-lib/src 7 | describe("@swan-bitcoin/xpub-cli", () => { 8 | it("needs tests") 9 | }) 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "BECH", 4 | "BIPs", 5 | "Gigi", 6 | "SAMOURAI", 7 | "SEGWIT", 8 | "TPUBS", 9 | "WPKH", 10 | "XPUBS", 11 | "bitcoinjs", 12 | "chaincode", 13 | "pubkey", 14 | "tpub", 15 | "upub", 16 | "vpub", 17 | "xpub", 18 | "ypub", 19 | "zpub" 20 | ], 21 | "cSpell.ignorePaths": ["fixtures.js"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/utils.test.js: -------------------------------------------------------------------------------- 1 | import { KEY } from "../test/fixtures" 2 | import { segment } from "./utils" 3 | 4 | describe("segment", () => { 5 | test("address segmentation", () => { 6 | // bc1qdx0pd4h65d7mekkhk7n6jwzfwgqath7s0e368g 7 | // -> bc1qdx 0pd4h65d7mekkhk7n6jwzfwgqath7s 0e368g 8 | const result = ["bc1qdx", "0pd4h65d7mekkhk7n6jwzfwgqath7s", "0e368g"] 9 | expect(segment(KEY.MAIN.BECH32)[0].length).toBe(6) 10 | expect(segment(KEY.MAIN.BECH32)[2].length).toBe(6) 11 | expect(segment(KEY.MAIN.BECH32)).toStrictEqual(result) 12 | }) 13 | }) -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: Build & Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Setup Node 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version-file: '.nvmrc' 15 | registry-url: 'https://npm.pkg.github.com' 16 | scope: '@swan-bitcoin' 17 | - name: Install dependencies 18 | run: yarn install --frozen-lockfile 19 | - name: Build 20 | run: yarn workspaces run compile 21 | - name: Run tests 22 | run: yarn workspaces run test 23 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exports constants related to extended public key types. 3 | * 4 | * @module types 5 | */ 6 | 7 | /** 8 | * Extended public key type, which is also the prefix of the extended public 9 | * key in question. 10 | * 11 | * @constant 12 | * @enum {string} 13 | */ 14 | const TYPE = { 15 | /** xpub... (BIP 44, P2PKH, mainnet) */ 16 | XPUB: "xpub", 17 | 18 | /** ypub... (BIP 49, P2WPKH-P2SH, mainnet) */ 19 | YPUB: "ypub", 20 | 21 | /** zpub... (BIP 84, P2WPKH, mainnet) */ 22 | ZPUB: "zpub", 23 | 24 | /** tpub... (BIP 44, P2PKH, testnet) */ 25 | TPUB: "tpub", 26 | 27 | /** upub... (BIP 49, P2WPKH-P2SH, testnet) */ 28 | UPUB: "upub", 29 | 30 | /** vpub... (BIP 84, P2WPKH, testnet) */ 31 | VPUB: "vpub", 32 | } 33 | 34 | export { TYPE } 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to Github Packages 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | environment: publish 9 | permissions: 10 | contents: read 11 | packages: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version-file: '.nvmrc' 17 | registry-url: 'https://npm.pkg.github.com' 18 | scope: '@swan-bitcoin' 19 | - run: yarn install --frozen-lockfile 20 | - run: yarn workspaces run compile 21 | # publish xpub-lib 22 | - run: yarn publish packages/xpub-lib --access public 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | # publish xpub-cli 26 | - run: yarn publish packages/xpub-cli --access public 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /packages/xpub-lib/src/conversion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module defines helper functions for extended public key conversion. 3 | * 4 | * @module conversion 5 | */ 6 | 7 | import { Network, convertExtendedPublicKey } from "@caravan/bitcoin" 8 | import { TYPE } from "./types" 9 | 10 | /** 11 | * Convert any extended public key (ypub, zpub, etc.) to the XPUB format 12 | * defined in BIP44. Resulting key will be an xpub (mainnet) or a tpub 13 | * (testnet). 14 | * 15 | * @param {string} extPubKey - the extended public key to convert 16 | * @param {string} network - the network to convert to (MAINNET or TESTNET) 17 | * 18 | * @returns {(string|object)} converted extended public key or error object 19 | * with the failed key and error message 20 | */ 21 | function convertToXPUB(extPubKey, network) { 22 | const targetPrefix = network === Network.MAINNET ? TYPE.XPUB : TYPE.TPUB 23 | return convertExtendedPublicKey(extPubKey, targetPrefix) 24 | } 25 | 26 | export { convertToXPUB } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Swan Bitcoin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "jest": true, 7 | "node": true 8 | }, 9 | "extends": [ 10 | "airbnb", 11 | "plugin:react/recommended", 12 | "plugin:jsx-a11y/strict", 13 | "plugin:prettier/recommended", 14 | "prettier/react" 15 | ], 16 | "parser": "babel-eslint", 17 | "parserOptions": { 18 | "ecmaVersion": 6, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "jsx": true, 22 | "modules": true, 23 | "experimentalObjectRestSpread": true 24 | } 25 | }, 26 | "plugins": ["react", "react-hooks", "jsx-a11y", "prettier"], 27 | "rules": { 28 | "import/prefer-default-export": "off", 29 | "react/jsx-filename-extension": [ 30 | 1, 31 | { 32 | "extensions": [".js", ".jsx"] 33 | } 34 | ], 35 | "react-hooks/rules-of-hooks": "error", 36 | "react-hooks/exhaustive-deps": "warn", 37 | "no-console": "off", 38 | "react/prop-types": 0, 39 | "prettier/prettier": ["error"] 40 | }, 41 | "globals": { 42 | "window": true, 43 | "document": true, 44 | "localStorage": true, 45 | "FormData": true, 46 | "FileReader": true, 47 | "Blob": true, 48 | "navigator": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # compiled output 72 | packages/xpub-lib/lib 73 | packages/xpub-cli/lib 74 | 75 | # jsdoc output 76 | out 77 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/purpose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exports purpose constants as defined in BIPs 44/49/84/86 and 3 | * human-readable account-type names. 4 | * 5 | * @module purpose 6 | */ 7 | 8 | /** 9 | * Derivation purpose as defined in BIP 44. 10 | * 11 | * @constant 12 | * @enum {string} 13 | */ 14 | const Purpose = { 15 | /** BIP 44: Pay To Pubkey Hash (addresses starting with 1) */ 16 | P2PKH: "44", // 1... 17 | /** BIP 49: Pay To Witness Pubkey Hash nested in Pay To Script Hash (addresses starting with 3) */ 18 | P2SH: "49", // 3... 19 | /** BIP 84: Pay To Witness Pubkey Hash (addresses starting with bc1q) */ 20 | P2WPKH: "84", // bc1q... 21 | /** BIP 86: Pay-to-Taproot (P2TR) (addresses starting with bc1p) */ 22 | P2TR: "86", // bc1p... 23 | } 24 | 25 | /** 26 | * Human-readable account names 27 | * 28 | * @constant 29 | * @enum {string} 30 | */ 31 | const AccountTypeName = { 32 | /** "Legacy" (BIP 44, addresses starting with 1) */ 33 | [Purpose.P2PKH]: "Legacy", 34 | /** "SegWit" (BIP 49, addresses starting with 3) */ 35 | [Purpose.P2SH]: "SegWit", 36 | /** "Native SegWit" (BIP 84, addresses starting with bc1q) */ 37 | [Purpose.P2WPKH]: "Native SegWit", // bc1q addresses 38 | /** BIP 86: Pay-to-Taproot (P2TR) (addresses starting with bc1p) */ 39 | [Purpose.P2TR]: "Pay-to-Taproot (P2TR)", // bc1p addresses 40 | } 41 | 42 | export { Purpose, AccountTypeName } 43 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/conversion.test.js: -------------------------------------------------------------------------------- 1 | import { Network } from "@caravan/bitcoin" 2 | import { KEY } from "../test/fixtures" 3 | 4 | import { convertToXPUB } from "./conversion" 5 | 6 | describe("convertToXPUB", () => { 7 | test("no conversion if none is required", () => { 8 | expect(convertToXPUB(KEY.MAIN.XPUB, Network.MAINNET)).toBe(KEY.MAIN.XPUB) 9 | expect(convertToXPUB(KEY.TEST.TPUB, Network.TESTNET)).toBe(KEY.TEST.TPUB) 10 | }) 11 | test("conversion of mainnet keys", () => { 12 | expect(convertToXPUB(KEY.MAIN.YPUB, Network.MAINNET)).toBe(KEY.MAIN.XPUB) 13 | expect(convertToXPUB(KEY.MAIN.ZPUB, Network.MAINNET)).toBe(KEY.MAIN.XPUB) 14 | }) 15 | test("conversion of testnet keys", () => { 16 | expect(convertToXPUB(KEY.TEST.UPUB, Network.TESTNET)).toBe(KEY.TEST.TPUB) 17 | expect(convertToXPUB(KEY.TEST.VPUB, Network.TESTNET)).toBe(KEY.TEST.TPUB) 18 | }) 19 | test("conversion of mainnet keys to testnet keys", () => { 20 | expect(convertToXPUB(KEY.MAIN.XPUB, Network.TESTNET)).toBe(KEY.TEST.TPUB) 21 | expect(convertToXPUB(KEY.MAIN.YPUB, Network.TESTNET)).toBe(KEY.TEST.TPUB) 22 | expect(convertToXPUB(KEY.MAIN.ZPUB, Network.TESTNET)).toBe(KEY.TEST.TPUB) 23 | }) 24 | test("conversion of testnet keys to mainnet keys", () => { 25 | expect(convertToXPUB(KEY.TEST.TPUB, Network.MAINNET)).toBe(KEY.MAIN.XPUB) 26 | expect(convertToXPUB(KEY.TEST.UPUB, Network.MAINNET)).toBe(KEY.MAIN.XPUB) 27 | expect(convertToXPUB(KEY.TEST.VPUB, Network.MAINNET)).toBe(KEY.MAIN.XPUB) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/xpub-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swan-bitcoin/xpub-cli", 3 | "version": "0.3.0", 4 | "description": "Command-line wrapper around @swan-bitcoin/xpub-lib", 5 | "keywords": [ 6 | "bitcoin", 7 | "cli", 8 | "xpub", 9 | "ypub", 10 | "zpub", 11 | "address", 12 | "derivation", 13 | "bip32", 14 | "bip44", 15 | "bip49", 16 | "bip84", 17 | "p2pkh", 18 | "p2sh", 19 | "p2wpkh", 20 | "bech32", 21 | "segwit" 22 | ], 23 | "author": "Gigi ", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/swan-bitcoin/xpub-tool.git" 28 | }, 29 | "homepage": "https://github.com/swan-bitcoin/xpub-tool", 30 | "bugs": { 31 | "url": "https://github.com/swan-bitcoin/xpub-tool/issues" 32 | }, 33 | "main": "lib/xpub.js", 34 | "bin": { 35 | "xpub": "lib/xpub.js" 36 | }, 37 | "directories": { 38 | "lib": "lib", 39 | "test": "__tests__" 40 | }, 41 | "files": [ 42 | "lib" 43 | ], 44 | "publishConfig": { 45 | "access": "public" 46 | }, 47 | "scripts": { 48 | "babel-version": "babel --version", 49 | "compile": "babel -d lib/ src/", 50 | "compile:watch": "babel --watch -d lib/ src/", 51 | "pretest": "yarn run compile", 52 | "prepublish": "yarn run compile", 53 | "test": "echo \"Warning: no test specified\"" 54 | }, 55 | "dependencies": { 56 | "@swan-bitcoin/xpub-lib": "^0.3.0", 57 | "commander": "^6.1.0", 58 | "tiny-secp256k1": "^2.2.3" 59 | }, 60 | "devDependencies": { 61 | "@babel/cli": "^7.11.6", 62 | "@babel/core": "^7.11.6", 63 | "@babel/plugin-transform-modules-commonjs": "^7.10.4", 64 | "@babel/preset-env": "^7.11.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exports various utility functions. 3 | * 4 | * @module utils 5 | */ 6 | 7 | import { APOSTROPHE } from "./constants" 8 | 9 | /** 10 | * Masks an extended public key by showing only the first and last couple 11 | * characters. 12 | * 13 | * @param {string} key - the (extended public) key to mask 14 | * @param {number} [pre=15] - number of characters to show in the beginning 15 | * @param {number} [post=15] - number of characters to show in the end 16 | * @param {string} [placeholder="[...]"] - string used for masking 17 | * 18 | * @returns {string} the masked address 19 | */ 20 | function maskKey(key, pre = 15, post = 15, placeholder = "[...]") { 21 | const beginning = key.substr(0, pre) 22 | const ending = key.substr(key.length - post, key.length) 23 | return beginning + placeholder + ending 24 | } 25 | 26 | /** 27 | * Hardens a path segment as described in BIP32. 28 | * 29 | * @param {string} pathSegment - the path segment to harden 30 | */ 31 | function harden(pathSegment) { 32 | return pathSegment + APOSTROPHE 33 | } 34 | 35 | /** 36 | * Splits a given bitcoin address into three segments for easier readability. 37 | * Per default, the first and last segment is 6 characters long. 38 | * 39 | * @param {string} address - the given bitcoin address 40 | * @param {number} [pre=6] - length of the first segment 41 | * @param {number} [post=6] - length of the last segment 42 | * 43 | * @returns {string[]} array of address segments 44 | */ 45 | function segment(address, pre = 6, post = 6) { 46 | const beginning = address.substr(0, pre) 47 | const middle = address.substr(pre, address.length - (pre + post)) 48 | const end = address.substr(address.length - post, address.length) 49 | return [beginning, middle, end] 50 | } 51 | 52 | export { maskKey, harden, segment } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swan's Address Derivation Tool 2 | 3 | A small JavaScript library and accompanying cli tool that derives bitcoin addresses from extended public keys. Built upon [caravan](https://github.com/caravan-bitcoin/caravan) and [bitcoinjs-lib](https://github.com/bitcoinjs/bitcoinjs-lib). 4 | 5 | For more details refer to the individual packages: 6 | 7 | - [`xpub-lib`](https://github.com/swan-bitcoin/xpub-tool/tree/master/packages/xpub-lib) - address derivation and validation library 8 | - [`xpub-cli`](https://github.com/swan-bitcoin/xpub-tool/tree/master/packages/xpub-cli) - command-line interface 9 | 10 | ## Relevant BIPs and Educational Resources 11 | 12 | - [BIP 32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) - Hierarchical Deterministic Wallets 13 | - [BIP 44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) - Multi-Account Hierarchy for Deterministic Wallets 14 | - [BIP 49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) - Derivation scheme for P2WPKH-nested-in-P2SH based accounts 15 | - [BIP 84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) - Derivation scheme for P2WPKH based accounts 16 | - [BIP 86](https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki) - Key Derivation for Single Key P2TR Outputs 17 | 18 | For a detailed explanation on derivation paths refer to [learn me a bitcoin](https://learnmeabitcoin.com/technical/derivation-paths). 19 | 20 | ## Publishing 21 | 22 | The `xpub-lib` and `xpub-cli` can be published to GitHub Packages by: 23 | 1. Incrementing the version field within the package.json file within each package you want to publish. 24 | 2. Creating a new tag with SemVer sytax (i.e. v0.0.1). Needs to match `v*`. 25 | 3. Creating a new GitHub release using this tag. This will start the publish workflow. 26 | 27 | The workflow will need to be approved by another user with write access before its executed. If published successfully, the packages will be published [here](https://github.com/orgs/swan-bitcoin/packages). 28 | 29 | ## License: [MIT](./LICENSE.md) 30 | -------------------------------------------------------------------------------- /packages/xpub-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swan-bitcoin/xpub-lib", 3 | "version": "0.3.0", 4 | "description": "A JavaScript library for bitcoin address derivation from extended public keys, built upon bitcoinjs-lib and Unchained's bitcoin utilities. Supports P2PKH, P2SH, and P2WPKH (bech32) addresses as defined in BIP44, BIP49, and BIP84.", 5 | "author": "Pablof7z ", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/swan-bitcoin/xpub-tool.git" 11 | }, 12 | "homepage": "https://github.com/swan-bitcoin/xpub-tool", 13 | "bugs": { 14 | "url": "https://github.com/swan-bitcoin/xpub-tool/issues" 15 | }, 16 | "keywords": [ 17 | "bitcoin", 18 | "xpub", 19 | "address", 20 | "derivation", 21 | "bip32", 22 | "bip44", 23 | "bip49", 24 | "bip84", 25 | "bip86", 26 | "p2pkh", 27 | "p2sh", 28 | "p2wpkh", 29 | "p2tr", 30 | "bech32" 31 | ], 32 | "files": [ 33 | "lib/**" 34 | ], 35 | "dependencies": { 36 | "@caravan/bitcoin": "^0.3.1", 37 | "bitcoinjs-lib": "^6.1.7" 38 | }, 39 | "scripts": { 40 | "babel-version": "babel --version", 41 | "compile": "babel -d lib/ src/", 42 | "compile:watch": "babel --watch -d lib/ src/", 43 | "pretest": "yarn run compile", 44 | "prepublish": "yarn run compile", 45 | "test": "jest lib", 46 | "test:watch": "jest --watch src", 47 | "build:docs": "jsdoc *", 48 | "watch:doc-src": "yarn run nodemon --exec 'yarn build:docs' --watch src", 49 | "watch:doc-output": "yarn run livereload out", 50 | "watch:docs": "yarn watch:doc-output & yarn watch:doc-src" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "^7.11.6", 54 | "@babel/core": "^7.11.6", 55 | "@babel/plugin-transform-modules-commonjs": "^7.10.4", 56 | "@babel/preset-env": "^7.11.5", 57 | "babel-jest": "^26.3.0", 58 | "jest": "^26.4.2", 59 | "livereload": "^0.9.1", 60 | "nodemon": "^2.0.4", 61 | "tiny-secp256k1": "^2.2.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/metadata.test.js: -------------------------------------------------------------------------------- 1 | import { Network } from "@caravan/bitcoin" 2 | import { KEY, ACCOUNT21 } from "../test/fixtures" 3 | import { 4 | getPurposeFromExtPubKey, 5 | getNetworkFromExtPubKey, 6 | getExtPubKeyMetadata, 7 | getAccountFromExtPubKey, 8 | } from "./metadata" 9 | 10 | describe("getNetworkFromExtPubKey", () => { 11 | test("xpub/ypub/zpub should be mainnet", () => { 12 | expect(getNetworkFromExtPubKey(KEY.MAIN.XPUB)).toBe(Network.MAINNET) 13 | expect(getNetworkFromExtPubKey(KEY.MAIN.YPUB)).toBe(Network.MAINNET) 14 | expect(getNetworkFromExtPubKey(KEY.MAIN.ZPUB)).toBe(Network.MAINNET) 15 | }) 16 | test("tpu/upub/vpub should be mainnet", () => { 17 | expect(getNetworkFromExtPubKey(KEY.TEST.TPUB)).toBe(Network.TESTNET) 18 | expect(getNetworkFromExtPubKey(KEY.TEST.UPUB)).toBe(Network.TESTNET) 19 | expect(getNetworkFromExtPubKey(KEY.TEST.VPUB)).toBe(Network.TESTNET) 20 | }) 21 | test("invalid key should be undefined", () => { 22 | expect(getNetworkFromExtPubKey("abc")).toBeFalsy() 23 | }) 24 | }) 25 | 26 | describe("getPurposeFromExtPubKey", () => { 27 | test("xpub/tpub should be of type P2PKH (BIP 44)", () => { 28 | expect(getPurposeFromExtPubKey(KEY.MAIN.XPUB)).toBe("44") 29 | expect(getPurposeFromExtPubKey(KEY.TEST.TPUB)).toBe("44") 30 | }) 31 | test("ypub/upub should be of type P2SH (BIP 49)", () => { 32 | expect(getPurposeFromExtPubKey(KEY.MAIN.YPUB)).toBe("49") 33 | expect(getPurposeFromExtPubKey(KEY.TEST.UPUB)).toBe("49") 34 | }) 35 | test("zpub/vpub should be of type P2WPKH (BIP 84)", () => { 36 | expect(getPurposeFromExtPubKey(KEY.MAIN.ZPUB)).toBe("84") 37 | expect(getPurposeFromExtPubKey(KEY.TEST.VPUB)).toBe("84") 38 | }) 39 | }) 40 | 41 | describe("getExtPubKeyMetadata", () => { 42 | test("xpub metadata", () => { 43 | expect(getExtPubKeyMetadata(KEY.MAIN.XPUB)).toStrictEqual({ 44 | chaincode: 45 | "2d6929b63bd13b5f21af8470535baf7ca10924cf21c88fd96f735d65cd0a6cfc", 46 | depth: 4, 47 | index: 0, 48 | network: "mainnet", 49 | parentFingerprint: 3131820507, 50 | pubkey: 51 | "02dd9d5ff10088b43146268c361911d10bb730904bf3a5291402d63c04f66ed2a2", 52 | type: "44", 53 | version: "0488b21e", 54 | }) 55 | expect(getExtPubKeyMetadata(KEY.MAIN.YPUB)).toStrictEqual({ 56 | chaincode: 57 | "2d6929b63bd13b5f21af8470535baf7ca10924cf21c88fd96f735d65cd0a6cfc", 58 | depth: 4, 59 | index: 0, 60 | network: "mainnet", 61 | parentFingerprint: 3131820507, 62 | pubkey: 63 | "02dd9d5ff10088b43146268c361911d10bb730904bf3a5291402d63c04f66ed2a2", 64 | type: "49", 65 | version: "049d7cb2", 66 | }) 67 | expect(getExtPubKeyMetadata(KEY.MAIN.ZPUB)).toStrictEqual({ 68 | chaincode: 69 | "2d6929b63bd13b5f21af8470535baf7ca10924cf21c88fd96f735d65cd0a6cfc", 70 | depth: 4, 71 | index: 0, 72 | network: "mainnet", 73 | parentFingerprint: 3131820507, 74 | pubkey: 75 | "02dd9d5ff10088b43146268c361911d10bb730904bf3a5291402d63c04f66ed2a2", 76 | type: "84", 77 | version: "04b24746", 78 | }) 79 | }) 80 | }) 81 | 82 | describe("getAccountFromExtPubKey", () => { 83 | test("Main xpub/tpub should be at index 0", () => { 84 | expect(getAccountFromExtPubKey(KEY.MAIN.XPUB)).toBe(0) 85 | expect(getAccountFromExtPubKey(KEY.TEST.TPUB)).toBe(0) 86 | }) 87 | test("Account 21 xpub/tpub should be at index 21", () => { 88 | expect(getAccountFromExtPubKey(ACCOUNT21.MAIN.XPUB)).toBe(21) 89 | expect(getAccountFromExtPubKey(ACCOUNT21.TEST.TPUB)).toBe(21) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/metadata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module retrieves metadata from extended public keys. 3 | * 4 | * @module metadata 5 | */ 6 | 7 | import { Network, ExtendedPublicKey } from "@caravan/bitcoin" 8 | import { Purpose } from "./purpose" 9 | import { TYPE } from "./types" 10 | 11 | /** 12 | * Get network type from extended public key format. 13 | * XPUB/YPUB/ZPUB = mainnet, 14 | * TPUB/UPUB/VPUB = testnet. 15 | * 16 | * @param {string} extPubKey - an extended public key 17 | * 18 | * @returns {NETWORK} the associated network. 19 | */ 20 | function getNetworkFromExtPubKey(extPubKey) { 21 | const prefix = extPubKey.slice(0, 4) 22 | switch (prefix) { 23 | case TYPE.XPUB: 24 | case TYPE.YPUB: 25 | case TYPE.ZPUB: 26 | return Network.MAINNET 27 | case TYPE.TPUB: 28 | case TYPE.UPUB: 29 | case TYPE.VPUB: 30 | return Network.TESTNET 31 | default: 32 | return undefined 33 | } 34 | } 35 | 36 | /** 37 | * Get purpose from an extended public key, dependent on key type 38 | * (xpub/ypub/zpub, or tpub/upub/vpub). 39 | * 40 | * @param {string} extPubKey - an extended public key 41 | * 42 | * @returns {module:purpose~Purpose} the purpose (address type) of the key 43 | */ 44 | function getPurposeFromExtPubKey(extPubKey) { 45 | const prefix = extPubKey.slice(0, 4) 46 | switch (prefix) { 47 | case TYPE.XPUB: 48 | case TYPE.TPUB: 49 | return Purpose.P2PKH 50 | case TYPE.YPUB: 51 | case TYPE.UPUB: 52 | return Purpose.P2SH 53 | case TYPE.ZPUB: 54 | case TYPE.VPUB: 55 | return Purpose.P2WPKH 56 | default: 57 | return undefined 58 | } 59 | } 60 | 61 | /** 62 | * Extended public key metadata. 63 | * 64 | * @typedef {object} metadata 65 | * @property {module:type~TYPE} type - the extended public key type (xpub/ypub/zpub/tpub/upub/vpub) 66 | * @property {number} index - the key index 67 | * @property {number} depth - the depth of the derivation path 68 | * @property {string} pubkey - the corresponding (non-extended) public key 69 | * @property {string} chaincode - the chaincode 70 | * @property {string} parentFingerprint - the fingerprint of the parent key 71 | * @property {NETWORK} network - the associated network (TESTNET or MAINNET) 72 | */ 73 | 74 | /** 75 | * Retrieves metadata from a given extended public key. 76 | * 77 | * @param {string} extPubKey - an extended public key 78 | * 79 | * @returns {Metadata} a {@link module:metadata~metadata} object 80 | */ 81 | function getExtPubKeyMetadata(extPubKey) { 82 | try { 83 | const xpubObj = ExtendedPublicKey.fromBase58(extPubKey) 84 | 85 | return { 86 | type: getPurposeFromExtPubKey(extPubKey), 87 | index: xpubObj.index, 88 | depth: xpubObj.depth, 89 | pubkey: xpubObj.pubkey, 90 | chaincode: xpubObj.chaincode, 91 | parentFingerprint: xpubObj.parentFingerprint, 92 | network: getNetworkFromExtPubKey(extPubKey), 93 | version: xpubObj.version, 94 | } 95 | } catch (error) { 96 | return {} 97 | } 98 | } 99 | 100 | /** 101 | * Retrieves the (account) index from a given extended public key. 102 | * 103 | * @param {string} extPubKey - an extended public key 104 | * 105 | * @returns {Account} the associated account index 106 | */ 107 | function getAccountFromExtPubKey(extPubKey) { 108 | const rawAccountNumber = getExtPubKeyMetadata(extPubKey).index 109 | if (rawAccountNumber > 2147483647) { 110 | return rawAccountNumber - 2147483648 111 | } 112 | return rawAccountNumber 113 | } 114 | 115 | export { 116 | getPurposeFromExtPubKey, 117 | getExtPubKeyMetadata, 118 | getNetworkFromExtPubKey, 119 | getAccountFromExtPubKey, 120 | } 121 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/paths.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module defines functions to construct valid BIP32 derivation paths. 3 | * 4 | * @module paths 5 | */ 6 | 7 | import { Network } from "@caravan/bitcoin" 8 | import { harden } from "./utils" 9 | import { isValidIndex, isValidChainIndex } from "./validation" 10 | import { AccountTypeName } from "./purpose" 11 | import { SEPARATOR, APOSTROPHE, COIN_PREFIX } from "./constants" 12 | import { getAccountFromExtPubKey } from "./metadata" 13 | 14 | /** 15 | * Construct a partial key derivation path from a given `change` and `keyIndex`. 16 | * 17 | * @param {number} [change=0] - the unhardened chain index 18 | * @param {number} [keyIndex=0] - the unhardened key index 19 | * 20 | * @returns {string} a partial derivation path 21 | */ 22 | function partialKeyDerivationPath({ change = 0, keyIndex = 0 }) { 23 | if (isValidChainIndex(change) && isValidIndex(keyIndex)) { 24 | return [change, keyIndex].join(SEPARATOR) 25 | } 26 | return undefined 27 | } 28 | 29 | /** 30 | * Construct an account derivation path given a `purpose` and an `accountNumber`. 31 | * 32 | * @param {string} [coinPrefix=COIN_PREFIX] - the coin prefix, defaulting to "m" for bitcoin 33 | * @param {module:purpose~Purpose} purpose - the derivation purpose 34 | * @param {NETWORK} [network=Network.TESTNET] - the target network (TESTNET or MAINNET) 35 | * @param {number} accountNumber - the account number, starting with 0 36 | * 37 | * @returns {string} the account derivation path, e.g. "m/44'/0'/3'" 38 | */ 39 | function accountDerivationPath({ 40 | coinPrefix = COIN_PREFIX, 41 | purpose, 42 | network = Network.TESTNET, 43 | accountNumber, 44 | }) { 45 | return [ 46 | coinPrefix, 47 | harden(purpose), 48 | harden(network === Network.MAINNET ? 0 : 1), 49 | harden(accountNumber), 50 | ].join(SEPARATOR) 51 | } 52 | 53 | /** 54 | * Construct a full derivation path as defined by BIP44 given an xpub, `purpose`, 55 | * `change`, and `keyIndex`. 56 | * 57 | * @param {string} convertedExtPubKey - a BIP44 extended public key 58 | * @param {string} [coinPrefix=COIN_PREFIX] - the coin prefix, defaulting to "m" for bitcoin 59 | * @param {module:purpose~Purpose} purpose - derivation purpose 60 | * @param {NETWORK} [network=Network.TESTNET] - target network (TESTNET or MAINNET) 61 | * @param {number} [change=0] - change (0 = external chain, 1 = internal chain / change) 62 | * @param {number} keyIndex - the key index, i.e. the number of the key that 63 | * should be derived from the extended public key 64 | * 65 | * @returns {string} the full derivation path, e.g. "m/44'/0'/3'/0/1" 66 | */ 67 | function fullDerivationPath({ 68 | convertedExtPubKey, 69 | coinPrefix = COIN_PREFIX, 70 | purpose, 71 | network = Network.TESTNET, 72 | change = 0, 73 | keyIndex, 74 | }) { 75 | return [ 76 | accountDerivationPath({ 77 | purpose, 78 | accountNumber: getAccountFromExtPubKey(convertedExtPubKey), 79 | network, 80 | coinPrefix, 81 | }), 82 | change, 83 | keyIndex, 84 | ].join(SEPARATOR) 85 | } 86 | 87 | /** 88 | * Return a human-readable string for a BIP32 derivation path. 89 | * 90 | * @param {string} bip32Path - a BIP32 derivation path 91 | * @param {string} [accountString="Account"] - the string to display before the 92 | * account number 93 | * 94 | * @example 95 | * humanReadableDerivationPath("m/49'/0'/2'/0/1") 96 | * // --> "Account #3 (SegWit)" 97 | * 98 | * @returns {string} a human readable derivation path 99 | */ 100 | function humanReadableDerivationPath({ bip32Path, accountString = "Account" }) { 101 | const pathSegments = bip32Path.split(SEPARATOR) 102 | const purpose = pathSegments[1].replace(APOSTROPHE, "") 103 | const account = Number(pathSegments[3].replace(APOSTROPHE, "")) + 1 104 | return `${accountString} #${account} (${AccountTypeName[purpose]})` 105 | } 106 | 107 | export { 108 | APOSTROPHE, 109 | accountDerivationPath, 110 | fullDerivationPath, 111 | partialKeyDerivationPath, 112 | humanReadableDerivationPath, 113 | } 114 | -------------------------------------------------------------------------------- /packages/xpub-cli/README.md: -------------------------------------------------------------------------------- 1 | # Swan's Address Derivation CLI 2 | 3 | A small command-line tool to derive and validate bitcoin addresses from 4 | extended public keys. Supports xpub, ypub, and zpub extended public keys and 5 | their testnet equivalents. Support for legacy, SegWit, and native SegWit 6 | (bech32) addresses. Uses [@swan-bitcoin/xpub-lib](https://www.npmjs.com/package/@swan-bitcoin/xpub-lib) under the hood. 7 | 8 | ## Basic Example 9 | 10 | ``` 11 | $ xpub derive xpub6CQtk4bkfG1d4UTWNBwmWGP95gjvTvEKZhm74CxLfbd4XqXY5wkyaUvLoWyy6Le24VxCqg2nASLu2xhNaDh5FhFDf8ndUUgbm8q1VDqCipy 12 | 13 | bc1qcksx27qlksr2cy3pnwdw0mnm94c5cm0vz3jh6e 14 | ``` 15 | 16 | Address derivation defaults to native SegWit (bech32) addresses, i.e. the 17 | derivation purpose is set to `84` (p2wpkh) by default. Set the `--purpose` 18 | accordingly if you want to derive legacy or wrapped SegWit addresses. 19 | 20 | The sub-commands `xpub derive` and `xpub validate` are explained in more detail below. 21 | 22 | ## `derive`: Address Derivation 23 | 24 | The `xpub derive` command requires an extended public key (`extPubKey`) as an input. 25 | 26 | ``` 27 | Usage: xpub derive [options] [extPubKey] 28 | ``` 29 | 30 | Use `--testnet` to derive from testnet extended public keys (tpub, upub, vpub) and generate testnet addresses. 31 | 32 | ``` 33 | $ xpub derive -t tpubDCZv1xNTnmwmXe3BBMyXekiVreY853jFeC8k9AaEAqCDYi1ZTSTLH3uQonwCTRk9jL1SFu1cLNbDY76YtcDR8n2inSMwBEAdZs37EpYS9px 34 | 35 | tb1qynjqnqvuwqys8l0jkuzmjuntj6ar4cyaeqwwk3 36 | ``` 37 | 38 | ### Options 39 | 40 | Run `xpub derive --help` to see all options. 41 | 42 | - `-p, --purpose ` - derivation purpose which dictates the address type. Can be 'p2pkh', 'p2sh', or 'p2wpkh' (default: "p2wpkh") 43 | - `-n, --addressCount ` - number of addresses to generate (default: 1) 44 | - `-c, --accountNumber ` - the account number as defined in BIP 44 (default: 0) 45 | - `-i, --keyIndex ` - index of the address to generate (ignored if `addressCount` is set) (default: 0) 46 | - `-t, --testnet` - use testnet 47 | 48 | ### Advanced Example 49 | 50 | The following example derives the first three SegWit (p2sh) addresses of account 5 for the extended public key `ypub6XF...4wa`. Note that account numbers start at 0, so the account with number `5` will be shown as "Account Nr. 6" (or similar) in most wallets. 51 | 52 | ``` 53 | $ xpub derive ypub6XFA3jGfowZ6umedCYjPiMUeFetNQYDpUpHKqbrE3bzwawLmLbvYCYaUpiwZ6FHwU951b9dLd6hSvFJwHv763vvpXUV44PW62rtesm5g4wa -n3 -c5 --purpose p2sh 54 | 55 | [ 56 | { 57 | path: "m/49'/0'/5'/0/0", 58 | address: '3PEpUeFZUWJPrbdKGzaNeEpekSPpSbVSzL' 59 | }, 60 | { 61 | path: "m/49'/0'/5'/0/1", 62 | address: '3AedcVmzeoUF4tHkzDHM6wp7WLoo668KwT' 63 | }, 64 | { 65 | path: "m/49'/0'/5'/0/2", 66 | address: '3BpPnS79WUzRQMG2DCNUKHgSKCaNoAMPCu' 67 | } 68 | ] 69 | ``` 70 | 71 | ## `validate`: Validation 72 | 73 | The `xpub validate` command takes an encoded bitcoin address or an extended public key as an input. 74 | 75 | ``` 76 | Usage: xpub validate [options] [encoded] 77 | ``` 78 | 79 | ``` 80 | $ xpub validate xpub6CCHViYn5VzKSmKD9cK9LBDPz9wBLV7owXJcNDioETNvhqhVtj3ABnVUERN9aV1RGTX9YpyPHnC4Ekzjnr7TZthsJRBiXA4QCeXNHEwxLab 81 | ``` 82 | 83 | `validate` will terminate without error if the extended public key or address is 84 | valid. If invalid, `validate` will fail with exit code 1. 85 | 86 | Use `--verbose` to generate output and `--testnet` to validate testnet keys and addresses. 87 | 88 | ``` 89 | $ xpub validate --testnet tb1qynjqnqvuwqys8l0jkuzmjuntj6ar4cyaeqwwk3 --verbose 90 | 91 | valid tb1qynjqnqvuwqys8l0jkuzmjuntj6ar4cyaeqwwk3 92 | ``` 93 | 94 | The `validate` command doesn't discriminate between extended public keys and addresses. You need to pass `--check-address` or `--check-ext` to do a validation that is exclusive. 95 | 96 | ``` 97 | $ xpub validate --check-ext --testnet tb1qynjqnqvuwqys8l0jkuzmjuntj6ar4cyaeqwwk3 --verbose 98 | 99 | invalid extPubKey tb1qynjqnqvuwqys8l0jkuzmjuntj6ar4cyaeqwwk3 100 | ``` 101 | 102 | ### Options 103 | 104 | Run `xpub validate --help` to see all options. 105 | 106 | - `-a, --check-address` - check bitcoin address for validity 107 | - `-x, --check-ext` - check extended public key for validity 108 | - `-t, --testnet` - use testnet 109 | - `-v, --verbose` - verbose output 110 | 111 | ## License: [MIT](./LICENSE.md) 112 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable 6 | # packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 10 | name: 'Dependency review' 11 | on: 12 | pull_request: 13 | branches: [ "master"] 14 | merge_group: 15 | branches: [ "master"] 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | # If using a dependency submission action in this workflow this permission will need to be set to: 22 | # 23 | # permissions: 24 | # contents: write 25 | # 26 | # https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api 27 | 28 | jobs: 29 | dependency-review: 30 | if: github.event_name == 'pull_request' 31 | permissions: 32 | contents: read 33 | pull-requests: write # Only needed for comment-summary-in-pr 34 | runs-on: ${{ vars.ACTIVE_RUNNER_LABEL }} 35 | steps: 36 | - name: 'Check if dependency files changed' 37 | id: should_run 38 | env: 39 | GH_TOKEN: ${{ github.token }} 40 | run: | 41 | # Check if dependency files changed using GitHub API 42 | CHANGED_FILES=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --jq '.[].filename') 43 | if echo "$CHANGED_FILES" | grep -qE '^(package\.json|yarn\.lock)$'; then 44 | echo "Dependency files changed - will run review" 45 | echo "run=true" >> $GITHUB_OUTPUT 46 | else 47 | echo "No dependency changes - skipping review" 48 | echo "run=false" >> $GITHUB_OUTPUT 49 | fi 50 | - name: 'Checkout repository' 51 | if: steps.should_run.outputs.run == 'true' 52 | uses: swan-bitcoin/actions/actions/checkout@master 53 | with: 54 | fetch-depth: 1 55 | fetch-tags: false 56 | - name: 'Dependency Review' 57 | if: steps.should_run.outputs.run == 'true' 58 | uses: swan-bitcoin/actions/actions/dependency-review-action@master 59 | # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. 60 | with: 61 | comment-summary-in-pr: on-failure 62 | license-check: false 63 | fail-on-severity: high 64 | # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later 65 | # retry-on-snapshot-warnings: true 66 | 67 | analyze-dependency-changes: 68 | if: github.event_name == 'pull_request' 69 | permissions: 70 | contents: read 71 | pull-requests: read 72 | runs-on: ${{ vars.ACTIVE_RUNNER_LABEL }} 73 | steps: 74 | - name: 'Check if dependency files changed' 75 | id: should_run 76 | env: 77 | GH_TOKEN: ${{ github.token }} 78 | run: | 79 | # Check if dependency files changed using GitHub API 80 | CHANGED_FILES=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --jq '.[].filename') 81 | if echo "$CHANGED_FILES" | grep -qE '^(package\.json|yarn\.lock)$'; then 82 | echo "Dependency files changed - will run analysis" 83 | echo "run=true" >> $GITHUB_OUTPUT 84 | else 85 | echo "No dependency changes - skipping analysis" 86 | echo "run=false" >> $GITHUB_OUTPUT 87 | fi 88 | - name: 'Checkout repository' 89 | if: steps.should_run.outputs.run == 'true' 90 | uses: swan-bitcoin/actions/actions/checkout@master 91 | with: 92 | fetch-depth: 1 93 | fetch-tags: false 94 | - name: 'Analyze Dependency Changes' 95 | if: steps.should_run.outputs.run == 'true' 96 | uses: swan-bitcoin/actions/swan/analyze-dependencies@master 97 | with: 98 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 99 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module contains methods to validate extended public keys and BIP32 3 | * derivation path segments. 4 | * 5 | * @module validation 6 | */ 7 | 8 | import { 9 | validateExtendedPublicKey, 10 | validateAddress, 11 | validateBIP32Index, 12 | } from "@caravan/bitcoin" 13 | import { getNetworkFromExtPubKey } from "./metadata" 14 | import { convertToXPUB } from "./conversion" 15 | import { Purpose } from "./purpose" 16 | import { harden } from "./utils" 17 | import { APOSTROPHE, COIN_PREFIX } from "./constants" 18 | 19 | /** 20 | * Returns true if the given `extPubKey` matches the given `network`, false otherwise. 21 | * 22 | * @param {string} extPubKey - the extended public key 23 | * @param {NETWORK} network - the network to check against 24 | * 25 | * @returns {boolean} 26 | */ 27 | function isNetworkMatch(extPubKey, network) { 28 | return extPubKey && getNetworkFromExtPubKey(extPubKey) === network 29 | } 30 | 31 | /** 32 | * Returns true if the given `extPubKey` is valid for the given `network`, 33 | * false otherwise. 34 | * 35 | * @param {string} extPubKey - the extended public key 36 | * @param {NETWORK} network - the network to check against 37 | * 38 | * @returns {boolean} 39 | */ 40 | function isValidExtPubKey(extPubKey, network) { 41 | if (!isNetworkMatch(extPubKey, network)) { 42 | return false 43 | } 44 | try { 45 | const convertedExtPubKey = convertToXPUB(extPubKey, network) 46 | // validateExtendedPublicKey expects "xpub..." or "tpub..." 47 | return validateExtendedPublicKey(convertedExtPubKey, network) === "" 48 | } catch (error) { 49 | return false 50 | } 51 | } 52 | 53 | /** 54 | * Returns true if the given bitcoin `address` is valid for the given 55 | * `network`, false otherwise. 56 | * 57 | * @param {string} address - the given bitcoin address 58 | * @param {NETWORK} network - the network to check against 59 | * 60 | * @returns {boolean} 61 | */ 62 | function isValidAddress(address, network) { 63 | return validateAddress(address, network) === "" 64 | } 65 | 66 | /** 67 | * Returns true if the given derivation purpose is valid, false otherwise. 68 | * 69 | * @param {module:purpose~Purpose} purpose - the derivation purpose 70 | * 71 | * @returns {boolean} 72 | */ 73 | function isValidPurpose(purpose) { 74 | switch (purpose) { 75 | case Purpose.P2PKH: 76 | case Purpose.P2SH: 77 | case Purpose.P2WPKH: 78 | case Purpose.P2TR: 79 | return true 80 | default: 81 | return false 82 | } 83 | } 84 | 85 | /** 86 | * Returns true if the given index is valid, false otherwise. 87 | * 88 | * @param {number} index - the BIP32 index to check 89 | * 90 | * @returns {boolean} 91 | */ 92 | function isValidIndex(index) { 93 | const indexString = harden(String(index)) 94 | try { 95 | return validateBIP32Index(indexString, { mode: "hardened" }) === "" 96 | } catch (error) { 97 | return false 98 | } 99 | } 100 | 101 | /** 102 | * Returns true if the given chain index is valid, false otherwise. 103 | * 104 | * @param {number} index - the chain (internal / external) index to check 105 | * 106 | * @returns {boolean} 107 | */ 108 | function isValidChainIndex(index) { 109 | const indexString = String(index) 110 | const validChains = ["0", "1"] 111 | if (validChains.indexOf(indexString) === -1) { 112 | return false 113 | } 114 | return true 115 | } 116 | 117 | /** 118 | * Returns true if the path segment is hardened, false otherwise. 119 | * 120 | * @param {string} segment - the path segment to check 121 | * 122 | * @returns {boolean} 123 | */ 124 | function isHardened(segment) { 125 | return segment.includes(APOSTROPHE) 126 | } 127 | 128 | /** 129 | * Returns true if the given BIP32 path segment is valid, false otherwise. 130 | * 131 | * @param {string} segment - the path segment to check 132 | * 133 | * @returns {boolean} 134 | */ 135 | function isValidPathSegment(segment) { 136 | let unhardened = segment 137 | if (isHardened(segment)) { 138 | unhardened = segment.slice(0, -1) 139 | } 140 | 141 | switch (unhardened) { 142 | case COIN_PREFIX: 143 | return true 144 | case Purpose.P2PKH: 145 | case Purpose.P2SH: 146 | case Purpose.P2WPKH: 147 | case Purpose.P2TR: 148 | return true 149 | default: 150 | return isValidIndex(unhardened) 151 | } 152 | } 153 | 154 | export { 155 | isNetworkMatch, 156 | isValidExtPubKey, 157 | isValidAddress, 158 | isValidPurpose, 159 | isValidIndex, 160 | isValidChainIndex, 161 | isValidPathSegment, 162 | } 163 | -------------------------------------------------------------------------------- /packages/xpub-cli/src/xpub.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { 4 | Network, 5 | initEccLib, 6 | isValidAddress, 7 | isValidExtPubKey, 8 | addressFromExtPubKey, 9 | addressesFromExtPubKey, 10 | Purpose, 11 | } = require("@swan-bitcoin/xpub-lib") 12 | const ecc = require('tiny-secp256k1') 13 | const { version } = require("../package.json") 14 | 15 | function parsePurpose(purpose) { 16 | switch (purpose.toLowerCase()) { 17 | case "p2pkh": { 18 | return Purpose.P2PKH 19 | } 20 | case "p2sh": { 21 | return Purpose.P2SH 22 | } 23 | case "p2wpkh": { 24 | return Purpose.P2WPKH 25 | } 26 | case "p2tr": { 27 | return Purpose.P2TR 28 | } 29 | default: { 30 | return undefined 31 | } 32 | } 33 | } 34 | 35 | function printAddress(address, verbose = false) { 36 | if (verbose) { 37 | console.log(address) 38 | } else { 39 | console.log(address.address) 40 | } 41 | } 42 | 43 | const { program } = require("commander") 44 | 45 | program.version(`${version}`) 46 | 47 | program 48 | .command("derive [extPubKey]") 49 | .description("derive address(es) from an extended public key") 50 | .option( 51 | "-p, --purpose ", 52 | "derivation purpose which dictates the address type ['p2pkh', 'p2sh', 'p2wpkh', 'p2tr']", 53 | "p2wpkh" 54 | ) // use `choices` once this feature is released: https://github.com/tj/commander.js/issues/518 55 | .option( 56 | "-n, --addressCount ", 57 | "number of addresses to generate", 58 | 1 59 | ) 60 | .option( 61 | "-c, --change ", 62 | "the change index to use (0 = external aka receive, 1 = internal aka change / other)", 63 | 0 64 | ) 65 | .option( 66 | "-i, --keyIndex ", 67 | "index of the address to generate (ignored if `addressCount` is set)", 68 | 0 69 | ) 70 | .option("-t, --testnet", "use TESTNET") 71 | .option("-v, --verbose", "verbose output") 72 | .action((extPubKey, cmdObj) => { 73 | if (!extPubKey) { 74 | cmdObj.help() 75 | } 76 | 77 | initEccLib(ecc) 78 | 79 | const network = cmdObj.testnet ? Network.TESTNET : Network.MAINNET 80 | if (!isValidExtPubKey(extPubKey, network)) { 81 | console.error(`error: invalid extended public key '${extPubKey}'`) 82 | process.exitCode = 1 83 | return 84 | } 85 | 86 | const purpose = cmdObj.purpose 87 | ? parsePurpose(cmdObj.purpose) 88 | : Purpose.P2WPKH // default to P2WPKH 89 | const change = cmdObj.change ? cmdObj.change : 0 // default to external chain 90 | const keyIndex = cmdObj.keyIndex ? cmdObj.keyIndex : 0 // default to first index 91 | 92 | if (cmdObj.addressCount > 1) { 93 | // Multiple addresses 94 | const { addressCount } = cmdObj 95 | const addresses = addressesFromExtPubKey({ 96 | extPubKey, 97 | addressCount, 98 | change, 99 | purpose, 100 | network, 101 | }) 102 | addresses.forEach(address => { 103 | printAddress(address, cmdObj.verbose) 104 | }) 105 | } else { 106 | // Single address 107 | const address = addressFromExtPubKey({ 108 | extPubKey, 109 | change, 110 | keyIndex, 111 | purpose, 112 | network, 113 | }) 114 | printAddress(address, cmdObj.verbose) 115 | } 116 | }) 117 | 118 | program 119 | .command("validate [encoded]") 120 | .description( 121 | "check an encoded bitcoin address or extended public key for validity" 122 | ) 123 | .option("-a, --check-address", "check bitcoin address for validity") 124 | .option("-x, --check-ext", "check extended public key for validity") 125 | .option("-t, --testnet", "use TESTNET") 126 | .option("-v, --verbose", "verbose output") 127 | .action((encoded, cmdObj) => { 128 | if (!encoded) { 129 | cmdObj.help() 130 | } 131 | 132 | const network = cmdObj.testnet ? Network.TESTNET : Network.MAINNET 133 | let isValid = false 134 | let type = "" 135 | if (cmdObj.checkAddress) { 136 | isValid = isValidAddress(encoded, network) 137 | type = "address" 138 | } else if (cmdObj.checkExt) { 139 | isValid = isValidExtPubKey(encoded, network) 140 | type = "extPubKey" 141 | } else { 142 | isValid = 143 | isValidExtPubKey(encoded, network) || isValidAddress(encoded, network) 144 | } 145 | 146 | if (cmdObj.verbose) { 147 | console.log(`${isValid ? "valid" : "invalid"} ${type} ${encoded}`) 148 | } 149 | process.exitCode = isValid ? 0 : 1 150 | }) 151 | 152 | program.parse(process.argv) 153 | -------------------------------------------------------------------------------- /packages/xpub-lib/README.md: -------------------------------------------------------------------------------- 1 | # Swan's Address Derivation Library 2 | 3 | A small JavaScript library that derives bitcoin addresses from extended public keys. Built upon [caravan](https://github.com/caravan-bitcoin/caravan) and [bitcoinjs-lib](https://github.com/bitcoinjs/bitcoinjs-lib). 4 | 5 | The library supports derivation from `xpub`s, `zpub`s, and `ypub`s, as well as legacy, SegWit, native SegWit (bech32) and Taproot (P2TR) address formats. Both Bitcoin mainnet and testnet are supported. If no network is specified the library defaults to testnet. 6 | 7 | | BIP | Derivation Path | Address Type | Address Format | Address Name | 8 | | --- | ------------------- | ------------ | -------------- | ---------------------- | 9 | | 44 | `m/44'/0'/0'` | P2PKH | `1...` | Legacy | 10 | | 49 | `m/49'/0'/0'` | P2WPKH-P2SH | `3...` | SegWit (Nested SegWit) | 11 | | 84 | `m/84'/0'/0'` | P2WPKH | `bc1q...` | Bech32 (Native SegWit) | 12 | | 86 | `m/86'/0'/0'` | P2TR | `bc1p...` | Taproot | 13 | 14 | Note that the different extended public key formats (i.e. xpub, ypub, zpub) are interchangeable and 15 | not bound to address formats. Every address type can be generated from every 16 | extended public key. 17 | 18 | The testnet equivalents are extended public keys starting with `tpub`, `upub`, and `vpub`. 19 | 20 | ## Example Usage 21 | 22 | ``` 23 | yarn add @swan-bitcoin/xpub-lib 24 | ``` 25 | 26 | Use `addressFromExtPubKey` to derive a single addresses. The following example 27 | derives the first address of the first account from an `xpub` on mainnet. If no purpose is given, it will default to P2WPKH (Native SegWit). 28 | 29 | ``` 30 | const key = "xpub6EuV33a2DXxAhoJTRTnr8qnysu81AA4YHpLY6o8NiGkEJ8KADJ35T64eJsStWsmRf1xXkEANVjXFXnaUKbRtFwuSPCLfDdZwYNZToh4LBCd" 31 | 32 | addressFromExtPubKey({ extPubKey: key, network: "mainnet" }) 33 | 34 | // { 35 | // path: "m/84'/0'/0'/0/0", 36 | // address: 'bc1qdx0pd4h65d7mekkhk7n6jwzfwgqath7s0e368g' 37 | // } 38 | ``` 39 | 40 | For taproot addresses, you must first initialize an instance of an ECC library implementing the secp256k1 elliptic curve interface, such as [tiny-secp256k1](https://www.npmjs.com/package/tiny-secp256k1). 41 | 42 | ``` 43 | const ecc = require('tiny-secp256k1') 44 | initEccLib(ecc) 45 | 46 | const key = "xpub6EuV33a2DXxAhoJTRTnr8qnysu81AA4YHpLY6o8NiGkEJ8KADJ35T64eJsStWsmRf1xXkEANVjXFXnaUKbRtFwuSPCLfDdZwYNZToh4LBCd" 47 | 48 | addressFromExtPubKey({ extPubKey: key, network: "mainnet", purpose: Purpose.P2TR }) 49 | 50 | // { 51 | // path: "m/86'/0'/0'/0/0", 52 | // address: 'bc1ptpvckxtuurh4t26yls5s6t5j9hyy2fh945zfpad44ngxdxqm0s2qhk3ljc' 53 | // } 54 | ``` 55 | 56 | Use `addressesFromExtPubKey` to derive multiple addresses. The following 57 | example derives the first three addresses of the first account from a `vpub` 58 | extended public key on testnet. 59 | 60 | ``` 61 | const key = "vpub5bExRiEBvAsD1CvDkkDbifbyXxq7Gv5YTbJ6Y1LbxFzUBvghhyhxCxkNGTXiX4TaqjivFGyFaQp9mDMLtCbrfUYEeWwp3ovxzvSB2XY87ph" 62 | 63 | addressesFromExtPubKey({ 64 | extPubKey: key, 65 | addressCount: 3, 66 | }) 67 | 68 | // [ 69 | // { 70 | // path: "m/84'/1'/0'/0/0", 71 | // address: 'tb1qdx0pd4h65d7mekkhk7n6jwzfwgqath7s9l2fum' 72 | // }, 73 | // { 74 | // path: "m/84'/1'/0'/0/1", 75 | // address: 'tb1q5tc3z8c4hs4x0p3vu88zk26anecge77g33ggk6' 76 | // }, 77 | // { 78 | // path: "m/84'/1'/0'/0/2", 79 | // address: 'tb1q3qu2fng7zw36cvzyaqec5nptp6cmnep0lf3323' 80 | // } 81 | // ] 82 | ``` 83 | 84 | To derive wrapped SegWit addresses 85 | (starting with `3...`) specify the appropriate purpose with `purpose: Purpose.P2SH`. 86 | 87 | For more examples refer to the tests of this library or the implementation of the CLI tool. 88 | 89 | ## Relevant BIPs and Educational Resources 90 | 91 | - [BIP 32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) - Hierarchical Deterministic Wallets 92 | - [BIP 44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) - Multi-Account Hierarchy for Deterministic Wallets 93 | - [BIP 49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) - Derivation scheme for P2WPKH-nested-in-P2SH based accounts 94 | - [BIP 84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) - Derivation scheme for P2WPKH based accounts 95 | - [BIP 86](https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki) - Key Derivation for Single Key P2TR Outputs 96 | 97 | For a detailed explanation on derivation paths refer to [learn me a bitcoin](https://learnmeabitcoin.com/technical/derivation-paths). 98 | 99 | ## License: [MIT](./LICENSE.md) 100 | -------------------------------------------------------------------------------- /packages/xpub-lib/test/fixtures.js: -------------------------------------------------------------------------------- 1 | // Same extended public key, different formats. Both testnet and mainnet. 2 | // Includes first valid address (legacy, segwit, bech32, taproot) 3 | const KEY = { 4 | MAIN: { 5 | XPUB: 6 | "xpub6EuV33a2DXxAhoJTRTnr8qnysu81AA4YHpLY6o8NiGkEJ8KADJ35T64eJsStWsmRf1xXkEANVjXFXnaUKbRtFwuSPCLfDdZwYNZToh4LBCd", 7 | YPUB: 8 | "ypub6ZjkLiEwNDVeZ6VaFpaULvtV3sGT6n43CvrktC2G6H87ME8PTxCe59inL5QUWnRM4f5LVhkvxPsoR5C33Hqu4Bb3FY35oYPRp6d7CCfcqmo", 9 | ZPUB: 10 | "zpub6ta1eNurWu38QPgh6BN6Z1yzDqQu3Q3Y83Nyfav9UHVzQKwcicNChDNvMHN4Wh5GUJC9FBMVR4EMJMobkzFurRGe7sjWPTCv5pgkaqEA6or", 11 | LEGACY: "1AdTLNfqiQtQ7yRNoZDEFTE9kSri2jrRVD", 12 | SEGWIT: "3JDVonJcuQ7yQQQJh1tFLV74uRZUP6LgvF", 13 | BECH32: "bc1qdx0pd4h65d7mekkhk7n6jwzfwgqath7s0e368g", 14 | TAPROOT: "bc1ptpvckxtuurh4t26yls5s6t5j9hyy2fh945zfpad44ngxdxqm0s2qhk3ljc", 15 | CHANGE: { 16 | LEGACY: "13AGMJSeF7HXzyKWtingr4ZSz14REnuRXh", 17 | SEGWIT: "3EdfREdHwcZ6AFmaL6mDkmvLw2qfb31ori", 18 | BECH32: "bc1qz7cv7zmexlsn0k6ny6sc0m9v8zwcrnlv5ndyqr", 19 | TAPROOT: "bc1pej4d3srkrhwm4k0n5twxf3lvmk3c4ff728n6kt5dcqy8tvtk6axsv4780t", 20 | }, 21 | }, 22 | TEST: { 23 | TPUB: 24 | "tpubDFH7ZHPhvoucybVJsemwLm8MD2Df9YZbqSvqTMBX4BW83QysHWtAbXjUYuXg3NifSvVSMogF2qMJDy55iTH89PMjSo5xuAEB8L9sZdEkW4B", 25 | UPUB: 26 | "upub5GQh83ZGmVKj9uj6vPRyWaWUMzgfLJ63YUmskcSiaFcb8psUTKYPau6EFFa8X9ofS6c7VoNh7kTbsvjnAWBqsErdnBFPTu7UjCNXdwwAxf1", 27 | VPUB: 28 | "vpub5bExRiEBvAsD1CvDkkDbifbyXxq7Gv5YTbJ6Y1LbxFzUBvghhyhxCxkNGTXiX4TaqjivFGyFaQp9mDMLtCbrfUYEeWwp3ovxzvSB2XY87ph", 29 | LEGACY: "mq9QdRkpXSKeu5tzX8Bc5NSUcSTQxzpa8G", 30 | SEGWIT: "2N9mhsXEeWrdKcC2rN9W7xS6L7mme9kJrVe", 31 | BECH32: "tb1qdx0pd4h65d7mekkhk7n6jwzfwgqath7s9l2fum", 32 | TAPROOT: "tb1ptpvckxtuurh4t26yls5s6t5j9hyy2fh945zfpad44ngxdxqm0s2qq78sgh", 33 | CHANGE: { 34 | LEGACY: "mhgDeMXd48inn5o8cHm4fymmqzf88LnBbC", 35 | SEGWIT: "2N6BsUyZKZ54SN3Q81EP6Niuc9P3qPjbT9n", 36 | BECH32: "tb1qz7cv7zmexlsn0k6ny6sc0m9v8zwcrnlv74khms", 37 | TAPROOT: "tb1pej4d3srkrhwm4k0n5twxf3lvmk3c4ff728n6kt5dcqy8tvtk6axsmagg4y", 38 | }, 39 | }, 40 | } 41 | 42 | const ACCOUNT21 = { 43 | MAIN: { 44 | XPUB: 45 | "xpub6CiTznFtgYouh6RvuJY66FtUxFeqqxGoTz9y1x7d56kMSSH1NAxtCpJtVYqxoe7ADm5fYAjyxedVc3CNZS9F5rXMVXiYDYTYXWFu3aHuHYP", 46 | }, 47 | TEST: { 48 | TPUB: 49 | "tpubDDmMSyuXefoaNf46Lo2jKhRnsfEZFwQTvNeiGjv2z4g2egg5y7xGofZqMy2p6LcqS3eXYDkuEGf2MaQUGMLVXeDtVP7djqiyAMpXXzBaaEx", 50 | }, 51 | } 52 | 53 | const KEYS = { 54 | MAIN: { 55 | XPUB: [ 56 | "xpub6CCHViYn5VzKSmKD9cK9LBDPz9wBLV7owXJcNDioETNvhqhVtj3ABnVUERN9aV1RGTX9YpyPHnC4Ekzjnr7TZthsJRBiXA4QCeXNHEwxLab", 57 | "xpub6D7NqpxWckGwCHhpXoL4pH38m5xVty62KY2wUh6JoyDCofwHciDRoQ3xm7WAg2ffpHaC6X4bEociYq81niyNUGhCxEs6fDFAd1LPbEmzcAm", 58 | "xpub6BfKpqjTwvH21wJGWEfxLppb8sU7C6FJge2kWb9315oP4ZVqCXG29cdUtkyu7YQhHyfA5nt63nzcNZHYmqXYHDxYo8mm1Xq1dAC7YtodwUR", 59 | ], 60 | YPUB: [ 61 | "ypub6ZjkLiEwNDVeZ6VaFpaULvtV3sGT6n43CvrktC2G6H87ME8PTxCe59inL5QUWnRM4f5LVhkvxPsoR5C33Hqu4Bb3FY35oYPRp6d7CCfcqmo", 62 | "ypub6TgEG2U9Fvznqe16tYu5vbaEnPXpFUue1MVN2wXDH21fk9LXEgVGLL93t6dz6vA6Kg2R7ywzoZv9vPe6DUXud9n47T7CKW6xjXrX87MhYLU", 63 | "ypub6YdKsNXoig5FEiM57Qrr7GzePXGBYZQJX4NRhkgoWej1qbtAEZeLTdU3MABFhJAyDCrjnTWL8wZDbc2hzPAAYrzEaMa6W4W4BCnhpg5B7aW", 64 | ], 65 | ZPUB: [ 66 | "zpub6ssizzcfQnWpVVeQJ7zy5tXEWA5gqRo3cFLn1cM8z8tdqoTQPDoKayrj9zF4mraaNUTeHA6cSyb5qL93QdMFrzruEdGYjRC1hKEyghMhwZZ", 67 | "zpub6sL1LBEvyMhmE4HZ4nr3RbsuqVBqTsLiGLf1ogsTCdzVJ43ngxyrKrYMoDiTT7ka6MxYRUacQzgqCL6bdCHCjkSTmhi8aN1uom3xdkzXktK", 68 | "zpub6tHb7NmWm4nPu18fQ4Tk1xS4etNryiKVqti3A83i5mzcqZjFnMrX95hnYj6yj6zkpzRmkbNhvZE5vzhyiEstYLdG8SVZnbLrf341WDzvQWt", 69 | ], 70 | }, 71 | TEST: { 72 | TPUB: [ 73 | "tpubDCZv1xNTnmwmXe3BBMyXekiVreY853jFeC8k9AaEAqCDYi1ZTSTLH3uQonwCTRk9jL1SFu1cLNbDY76YtcDR8n2inSMwBEAdZs37EpYS9px", 74 | "tpubDEQApNpryrEgXYPwsqmQWoeirc5nt4yawFcjaSg5ENHBoEgfLnHuwzVehZG4Nn2qbfLjotwuZFrGkNHyt9EWWkfoVfaPWWsAsV9VopxgUYr", 75 | "tpubD9RP6BdSN6wdxmjNwVKu7NDFQNjwxSAq32Se4NM5AJGKaYpr4YHVSrq5BEbZH1pbqq17zXGQhXLAvvfVfWQESenVgHjmYgcjLQMpJfz4HoY", 76 | ], 77 | UPUB: [ 78 | "upub5GGT3WZcGGFQZycqExe4iTxx8iL2BqXcJTXGTCALaoRvDjPD889bHZPXoRz3z9EGnx3GzCu7WuAPDnNUronFVDZjWsGJSUWpf63qVJqmGt1", 79 | "upub5ExvzmRuiP6T4XWG6qMi6NjQbMxnqmJewxwgHHTrkekfCfzCHvtWjLDWhhcXfsT7EnFiFGurxJJ7eNokXcZb2y6khVDEeiQJHbxBzKJ72AH", 80 | "upub5FNubXv4KESULE6ya9A42MzGcwkX2GneMy3rmUVEfLKe2veFd4y4f5WG5WjJp72eV162EGoLE8fsiYcQiSjEK8gYanA5sTEphsbXPvdizn4", 81 | ], 82 | VPUB: [ 83 | "vpub5bTmE9K4QmkbLUnWm6pmKgRDLkckrJprBBUx49PwEEAqgb3ehJo45FamVZ481S3dvhaRbDnUrFxqDC61yLTGSEcHyvAA365DmsjpADBAqrB", 84 | "vpub5Ybrievs5pSmu6utBXJGqxLWZ9cNcL4qkPZAZd7vbM68tUKh7wewzRDTTEKtoqC25VvWEZbrTRuhhcx9eN7kYCxann64AzrbFtTG2Vq9zF5", 85 | "vpub5abgwCpBesPT6h2Z1Qg3GbemVfRnquT3ogegq97tPTfiaqALZJdfSEnB1cwWxY2C2Z3MsARyDLX2t7GqbZHterjT8P874KwMC4W89mm2Q4U", 86 | ], 87 | }, 88 | } 89 | 90 | const WASABI = { 91 | XPUB: 92 | "xpub6CQtk4bkfG1d4UTWNBwmWGP95gjvTvEKZhm74CxLfbd4XqXY5wkyaUvLoWyy6Le24VxCqg2nASLu2xhNaDh5FhFDf8ndUUgbm8q1VDqCipy", 93 | YPUB: 94 | "ypub6XFA3jGfowZ6umedCYjPiMUeFetNQYDpUpHKqbrE3bzwawLmLbvYCYaUpiwZ6FHwU951b9dLd6hSvFJwHv763vvpXUV44PW62rtesm5g4wa", 95 | ZPUB: 96 | "zpub6r5RMPwaxd6am4qk2uX1vSa9Rd2pMADKPvoYczk7RcNpe39zbG66pcEcqvu969wrsnBpLdDu5m3zoXvW1cX6rAcRPpBUeJKaJaxJGLBWaLe", 97 | ADDRESSES: [ 98 | "bc1qcksx27qlksr2cy3pnwdw0mnm94c5cm0vz3jh6e", 99 | "bc1qw0c77zue3xduyh4jef3r3jhfpx30jxc7s5z7lv", 100 | "bc1ql4l5m2wnlcwl28rsu0k8k5rx7yjg9fkr2vld8p", 101 | ], 102 | } 103 | 104 | const SAMOURAI = { 105 | ZPUB: 106 | "zpub6rk5rRte9pPyKTNuP2iKak9ZSEqvsXMP48TQoP23vjVDLeywBwJKcCzj1avQEybYVD1A9uTDmou8F5hcL6KFataVGjyzZxwYyDLqBEv9H8R", 107 | ADDRESSES: [ 108 | "bc1qg7v2efej3lqmj828lcgfnedptrdncjv4mgpyfd", 109 | "bc1qjvpph2k4h3rvfdwrlczgsrs0ku6ymzq9z5ct2v", 110 | "bc1qtnew2mxs90w53qwta7wqhk89hruka6mqsrnkr8", 111 | ], 112 | } 113 | 114 | export { KEY, ACCOUNT21, KEYS, WASABI, SAMOURAI } 115 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/derivation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module defines functions for address derivation. 3 | * 4 | * @module derivation 5 | */ 6 | 7 | import * as bitcoin from "bitcoinjs-lib" 8 | import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371'; 9 | import { deriveChildPublicKey, networkData, Network } from "@caravan/bitcoin" 10 | import { fullDerivationPath, partialKeyDerivationPath } from "./paths" 11 | import { 12 | isValidExtPubKey, 13 | isValidIndex, 14 | isValidPurpose, 15 | isValidChainIndex, 16 | } from "./validation" 17 | import { convertToXPUB } from "./conversion" 18 | import { Purpose } from "./purpose" 19 | 20 | /** 21 | * Default network to use for address derivation. 22 | * 23 | * @constant 24 | * @type {string} 25 | * @default Network.TESTNET 26 | * */ 27 | const DEFAULT_NETWORK = Network.TESTNET 28 | /** 29 | * Default purpose to use for address derivation. 30 | * 31 | * @constant 32 | * @type {string} 33 | * @default Purpose.P2WPKH 34 | * */ 35 | const DEFAULT_PURPOSE = Purpose.P2WPKH 36 | 37 | /** 38 | * The secp256k1 interface to use for Taproot address derivation. 39 | */ 40 | let eccInstance = null; 41 | 42 | /** 43 | * Initialize the ECC library for Taproot address derivation. 44 | * 45 | * @param {object} eccLib - The secp256k1 interface to use. 46 | */ 47 | function initEccLib(eccLib) { 48 | eccInstance = eccLib; 49 | bitcoin.initEccLib(eccInstance); 50 | } 51 | 52 | /** 53 | * Derive a single address from a public key. 54 | * 55 | * @param {module:purpose~Purpose} purpose - the purpose dictates the derived 56 | * address type (P2PKH = 1address, P2SH = 3address, P2WPKH = bc1qaddress, P2TR = bc1paddress) 57 | * @param {Buffer} pubkey - the Buffer representation public key to derive from 58 | * @param {NETWORK} network - the network to use (MAINNET or TESTNET) 59 | * 60 | * @returns {object|undefined} derived address 61 | */ 62 | function deriveAddress({ purpose, pubkey, network }) { 63 | switch (purpose) { 64 | case Purpose.P2PKH: { 65 | const { address: oneAddress } = bitcoin.payments.p2pkh({ 66 | pubkey, 67 | network: networkData(network), 68 | }) 69 | return oneAddress 70 | } 71 | case Purpose.P2SH: { 72 | const { address: threeAddress } = bitcoin.payments.p2sh({ 73 | redeem: bitcoin.payments.p2wpkh({ 74 | pubkey, 75 | network: networkData(network), 76 | }), 77 | }) 78 | return threeAddress 79 | } 80 | case Purpose.P2WPKH: { 81 | const { address: bc1qAddress } = bitcoin.payments.p2wpkh({ 82 | pubkey, 83 | network: networkData(network), 84 | }) 85 | return bc1qAddress 86 | } 87 | case Purpose.P2TR: { 88 | if (!eccInstance) { 89 | throw new Error("An instance of an ECC library implementing the secp256k1 curve must be initialized to generate taproot addresses. You must first call initEccLib()."); 90 | } 91 | const xOnlyPubkey = toXOnly(pubkey) 92 | const { address: bc1pAddress } = bitcoin.payments.p2tr({ 93 | internalPubkey: xOnlyPubkey, 94 | network: networkData(network), 95 | }) 96 | return bc1pAddress 97 | } 98 | default: 99 | return undefined 100 | } 101 | } 102 | /** 103 | * Derive a single address from a given extended public key. Address type is 104 | * defined by the `purpose` parameter. 105 | * 106 | * @param {string} extPubKey - the extended public key 107 | * @param {number} [change=0] - change (0 = external chain, 1 = internal chain / change) 108 | * @param {number} [keyIndex=0] - the unhardened key index 109 | * @param {module:purpose~Purpose} [purpose=DEFAULT_PURPOSE] - the derivation purpose 110 | * @param {NETWORK} [network=DEFAULT_NETWORK] - the target network (TESTNET or MAINNET) 111 | * 112 | * @returns {object|undefined} derived address 113 | */ 114 | function addressFromExtPubKey({ 115 | extPubKey, 116 | change = 0, 117 | keyIndex = 0, 118 | purpose = DEFAULT_PURPOSE, 119 | network = DEFAULT_NETWORK, 120 | }) { 121 | if ( 122 | !isValidChainIndex(change) || 123 | !isValidIndex(keyIndex) || 124 | !isValidPurpose(purpose) || 125 | !isValidExtPubKey(extPubKey, network) 126 | ) { 127 | return undefined 128 | } 129 | const partialPath = partialKeyDerivationPath({ change, keyIndex }) 130 | const convertedExtPubKey = convertToXPUB(extPubKey, network) 131 | const fullPath = fullDerivationPath({ 132 | convertedExtPubKey, 133 | purpose, 134 | change, 135 | keyIndex, 136 | network, 137 | }) 138 | const childPubKey = deriveChildPublicKey( 139 | convertedExtPubKey, 140 | partialPath, 141 | network 142 | ) 143 | const pubkey = Buffer.from(childPubKey, "hex") 144 | 145 | return { 146 | path: fullPath, 147 | address: deriveAddress({ purpose, pubkey, network }), 148 | } 149 | } 150 | 151 | /** 152 | * Derive multiple addresses from a given extended public key. 153 | * See {@link module:derivation~addressFromExtPubKey}. 154 | * 155 | * @param {string} extPubKey - the extended public key 156 | * @param {string} addressCount - number of key indices to derive 157 | * @param {number} [addressStartIndex=0] - start key index to derive from 158 | * @param {number} [change=0] - change (0 = external chain, 1 = internal chain / change) 159 | * @param {module:purpose~Purpose} [purpose=DEFAULT_PURPOSE] - the derivation purpose 160 | * @param {NETWORK} [network=DEFAULT_NETWORK] - the target network (TESTNET or MAINNET) 161 | * 162 | * @returns {object[]} array of derived addresses 163 | */ 164 | function addressesFromExtPubKey({ 165 | extPubKey, 166 | addressCount, 167 | addressStartIndex = 0, 168 | change = 0, 169 | purpose = DEFAULT_PURPOSE, 170 | network = DEFAULT_NETWORK, 171 | }) { 172 | const addresses = [] 173 | 174 | for ( 175 | let keyIndex = addressStartIndex; 176 | keyIndex < addressStartIndex + addressCount; 177 | keyIndex += 1 178 | ) { 179 | const { path, address } = addressFromExtPubKey({ 180 | extPubKey, 181 | change, 182 | keyIndex, 183 | purpose, 184 | network, 185 | }) 186 | 187 | addresses.push({ path, address }) 188 | } 189 | 190 | return addresses 191 | } 192 | 193 | export { addressFromExtPubKey, addressesFromExtPubKey, initEccLib } 194 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/paths.test.js: -------------------------------------------------------------------------------- 1 | import { Network } from "@caravan/bitcoin" 2 | import { 3 | accountDerivationPath, 4 | fullDerivationPath, 5 | partialKeyDerivationPath, 6 | humanReadableDerivationPath, 7 | } from "./paths" 8 | import { KEY, ACCOUNT21 } from "../test/fixtures" 9 | import { Purpose } from "./purpose" 10 | 11 | describe("humanReadableDerivationPath", () => { 12 | test("Legacy", () => { 13 | expect(humanReadableDerivationPath({ bip32Path: "m/44'/0'/0'/0/1" })).toBe( 14 | "Account #1 (Legacy)" 15 | ) 16 | expect(humanReadableDerivationPath({ bip32Path: "m/44'/0'/1'/0/1" })).toBe( 17 | "Account #2 (Legacy)" 18 | ) 19 | expect(humanReadableDerivationPath({ bip32Path: "m/44'/0'/2'/0/1" })).toBe( 20 | "Account #3 (Legacy)" 21 | ) 22 | }) 23 | test("SegWit", () => { 24 | expect(humanReadableDerivationPath({ bip32Path: "m/49'/0'/0'/0/1" })).toBe( 25 | "Account #1 (SegWit)" 26 | ) 27 | expect(humanReadableDerivationPath({ bip32Path: "m/49'/0'/1'/0/1" })).toBe( 28 | "Account #2 (SegWit)" 29 | ) 30 | expect(humanReadableDerivationPath({ bip32Path: "m/49'/0'/2'/0/1" })).toBe( 31 | "Account #3 (SegWit)" 32 | ) 33 | }) 34 | test("Native SegWit", () => { 35 | expect(humanReadableDerivationPath({ bip32Path: "m/84'/0'/0'/0/1" })).toBe( 36 | "Account #1 (Native SegWit)" 37 | ) 38 | expect(humanReadableDerivationPath({ bip32Path: "m/84'/0'/1'/0/1" })).toBe( 39 | "Account #2 (Native SegWit)" 40 | ) 41 | expect(humanReadableDerivationPath({ bip32Path: "m/84'/0'/2'/0/1" })).toBe( 42 | "Account #3 (Native SegWit)" 43 | ) 44 | }) 45 | test("empty path", () => { 46 | expect(() => { 47 | humanReadableDerivationPath({ bip32Path: "" }) 48 | }).toThrow() 49 | }) 50 | }) 51 | 52 | describe("accountDerivationPath", () => { 53 | test("testnet derivation paths", () => { 54 | expect( 55 | accountDerivationPath({ purpose: Purpose.P2PKH, accountNumber: 0 }) 56 | ).toBe("m/44'/1'/0'") 57 | expect( 58 | accountDerivationPath({ purpose: Purpose.P2SH, accountNumber: 0 }) 59 | ).toBe("m/49'/1'/0'") 60 | expect( 61 | accountDerivationPath({ purpose: Purpose.P2WPKH, accountNumber: 0 }) 62 | ).toBe("m/84'/1'/0'") 63 | expect( 64 | accountDerivationPath({ purpose: Purpose.P2TR, accountNumber: 0 }) 65 | ).toBe("m/86'/1'/0'") 66 | expect( 67 | accountDerivationPath({ purpose: Purpose.P2PKH, accountNumber: 1337 }) 68 | ).toBe("m/44'/1'/1337'") 69 | }) 70 | test("mainnet derivation paths", () => { 71 | expect( 72 | accountDerivationPath({ 73 | purpose: Purpose.P2PKH, 74 | accountNumber: 0, 75 | network: Network.MAINNET, 76 | }) 77 | ).toBe("m/44'/0'/0'") 78 | expect( 79 | accountDerivationPath({ 80 | purpose: Purpose.P2SH, 81 | accountNumber: 0, 82 | network: Network.MAINNET, 83 | }) 84 | ).toBe("m/49'/0'/0'") 85 | expect( 86 | accountDerivationPath({ 87 | purpose: Purpose.P2WPKH, 88 | accountNumber: 0, 89 | network: Network.MAINNET, 90 | }) 91 | ).toBe("m/84'/0'/0'") 92 | expect( 93 | accountDerivationPath({ 94 | purpose: Purpose.P2TR, 95 | accountNumber: 0, 96 | network: Network.MAINNET, 97 | }) 98 | ).toBe("m/86'/0'/0'") 99 | expect( 100 | accountDerivationPath({ 101 | purpose: Purpose.P2WPKH, 102 | accountNumber: 21, 103 | network: Network.MAINNET, 104 | }) 105 | ).toBe("m/84'/0'/21'") 106 | }) 107 | }) 108 | 109 | describe("fullDerivationPath", () => { 110 | test("full testnet derivation paths", () => { 111 | expect( 112 | fullDerivationPath({ 113 | convertedExtPubKey: KEY.TEST.TPUB, 114 | purpose: Purpose.P2PKH, 115 | keyIndex: 0, 116 | }) 117 | ).toBe("m/44'/1'/0'/0/0") 118 | expect( 119 | fullDerivationPath({ 120 | convertedExtPubKey: ACCOUNT21.TEST.TPUB, 121 | purpose: Purpose.P2PKH, 122 | keyIndex: 1337, 123 | }) 124 | ).toBe("m/44'/1'/21'/0/1337") 125 | expect( 126 | fullDerivationPath({ 127 | convertedExtPubKey: KEY.TEST.TPUB, 128 | purpose: Purpose.P2PKH, 129 | change: 1, 130 | keyIndex: 0, 131 | }) 132 | ).toBe("m/44'/1'/0'/1/0") 133 | }) 134 | test("full mainnet derivation paths", () => { 135 | expect( 136 | fullDerivationPath({ 137 | convertedExtPubKey: KEY.MAIN.XPUB, 138 | purpose: Purpose.P2PKH, 139 | network: Network.MAINNET, 140 | keyIndex: 0, 141 | }) 142 | ).toBe("m/44'/0'/0'/0/0") 143 | expect( 144 | fullDerivationPath({ 145 | convertedExtPubKey: ACCOUNT21.MAIN.XPUB, 146 | purpose: Purpose.P2PKH, 147 | network: Network.MAINNET, 148 | keyIndex: 1337, 149 | }) 150 | ).toBe("m/44'/0'/21'/0/1337") 151 | expect( 152 | fullDerivationPath({ 153 | convertedExtPubKey: KEY.MAIN.XPUB, 154 | purpose: Purpose.P2PKH, 155 | network: Network.MAINNET, 156 | change: 1, 157 | keyIndex: 0, 158 | }) 159 | ).toBe("m/44'/0'/0'/1/0") 160 | }) 161 | }) 162 | 163 | describe("partialKeyDerivationPath", () => { 164 | test("partial key derivation paths", () => { 165 | expect( 166 | partialKeyDerivationPath({ 167 | change: 0, 168 | keyIndex: 0, 169 | }) 170 | ).toBe("0/0") 171 | expect( 172 | partialKeyDerivationPath({ 173 | change: 1, 174 | keyIndex: 0, 175 | }) 176 | ).toBe("1/0") 177 | }) 178 | test("valid change index", () => { 179 | expect( 180 | partialKeyDerivationPath({ 181 | change: 0, 182 | }) 183 | ).toBeTruthy() 184 | expect( 185 | partialKeyDerivationPath({ 186 | change: 1, 187 | }) 188 | ).toBeTruthy() 189 | }) 190 | test("invalid change index", () => { 191 | expect( 192 | partialKeyDerivationPath({ 193 | change: 21, 194 | }) 195 | ).toBeFalsy() 196 | expect( 197 | partialKeyDerivationPath({ 198 | change: 1337, 199 | }) 200 | ).toBeFalsy() 201 | expect( 202 | partialKeyDerivationPath({ 203 | change: 2147483647, 204 | }) 205 | ).toBeFalsy() 206 | expect( 207 | partialKeyDerivationPath({ 208 | change: -1, 209 | }) 210 | ).toBeFalsy() 211 | expect( 212 | partialKeyDerivationPath({ 213 | change: 2147483648, 214 | }) 215 | ).toBeFalsy() 216 | }) 217 | test("invalid key index", () => { 218 | expect( 219 | partialKeyDerivationPath({ 220 | keyIndex: -1, 221 | }) 222 | ).toBeFalsy() 223 | }) 224 | }) 225 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/validation.test.js: -------------------------------------------------------------------------------- 1 | import { Network } from "@caravan/bitcoin" 2 | import { KEY, KEYS } from "../test/fixtures" 3 | import { 4 | isValidExtPubKey, 5 | isValidAddress, 6 | isValidIndex, 7 | isValidChainIndex, 8 | isValidPathSegment, 9 | } from "./validation" 10 | 11 | describe("isValidIndex", () => { 12 | test("valid indices", () => { 13 | expect(isValidIndex("0")).toBeTruthy() 14 | expect(isValidIndex("1")).toBeTruthy() 15 | expect(isValidIndex("21")).toBeTruthy() 16 | expect(isValidIndex("1337")).toBeTruthy() 17 | expect(isValidIndex("2147483647")).toBeTruthy() 18 | 19 | expect(isValidIndex(0)).toBeTruthy() 20 | expect(isValidIndex(1)).toBeTruthy() 21 | expect(isValidIndex(21)).toBeTruthy() 22 | expect(isValidIndex(1337)).toBeTruthy() 23 | expect(isValidIndex(2147483647)).toBeTruthy() 24 | }) 25 | test("invalid indices", () => { 26 | expect(isValidIndex("-1")).toBeFalsy() 27 | expect(isValidIndex("2147483648")).toBeFalsy() 28 | expect(isValidIndex("a")).toBeFalsy() 29 | expect(isValidIndex("/")).toBeFalsy() 30 | 31 | expect(isValidIndex(-1)).toBeFalsy() 32 | expect(isValidIndex(2147483648)).toBeFalsy() 33 | }) 34 | }) 35 | 36 | describe("isValidChainIndex", () => { 37 | test("valid chain indices", () => { 38 | expect(isValidChainIndex("0")).toBeTruthy() 39 | expect(isValidChainIndex("1")).toBeTruthy() 40 | 41 | expect(isValidChainIndex(0)).toBeTruthy() 42 | expect(isValidChainIndex(1)).toBeTruthy() 43 | }) 44 | test("invalid chain indices", () => { 45 | expect(isValidChainIndex("21")).toBeFalsy() 46 | expect(isValidChainIndex("1337")).toBeFalsy() 47 | expect(isValidChainIndex("2147483647")).toBeFalsy() 48 | expect(isValidChainIndex("-1")).toBeFalsy() 49 | expect(isValidChainIndex("2147483648")).toBeFalsy() 50 | expect(isValidChainIndex("a")).toBeFalsy() 51 | expect(isValidChainIndex("/")).toBeFalsy() 52 | 53 | expect(isValidChainIndex(21)).toBeFalsy() 54 | expect(isValidChainIndex(1337)).toBeFalsy() 55 | expect(isValidChainIndex(2147483647)).toBeFalsy() 56 | expect(isValidChainIndex(-1)).toBeFalsy() 57 | expect(isValidChainIndex(2147483648)).toBeFalsy() 58 | }) 59 | }) 60 | 61 | describe("isValidPathSegment", () => { 62 | test("valid path segments", () => { 63 | // hardened 64 | expect(isValidPathSegment("m'")).toBeTruthy() 65 | expect(isValidPathSegment("0'")).toBeTruthy() 66 | expect(isValidPathSegment("1'")).toBeTruthy() 67 | expect(isValidPathSegment("44'")).toBeTruthy() 68 | expect(isValidPathSegment("49'")).toBeTruthy() 69 | expect(isValidPathSegment("84'")).toBeTruthy() 70 | expect(isValidPathSegment("86'")).toBeTruthy() 71 | 72 | // not hardened 73 | expect(isValidPathSegment("m")).toBeTruthy() 74 | expect(isValidPathSegment("0")).toBeTruthy() 75 | expect(isValidPathSegment("1")).toBeTruthy() 76 | expect(isValidPathSegment("44")).toBeTruthy() 77 | expect(isValidPathSegment("49")).toBeTruthy() 78 | expect(isValidPathSegment("84")).toBeTruthy() 79 | expect(isValidPathSegment("86")).toBeTruthy() 80 | }) 81 | }) 82 | 83 | describe("isValidExtPubKey", () => { 84 | test("invalid keys are invalid on mainnet", () => { 85 | expect(isValidExtPubKey("", Network.MAINNET)).toBeFalsy() 86 | expect(isValidExtPubKey("xpub...", Network.MAINNET)).toBeFalsy() 87 | expect(isValidExtPubKey("ypub...", Network.MAINNET)).toBeFalsy() 88 | expect(isValidExtPubKey("zpub...", Network.MAINNET)).toBeFalsy() 89 | expect(isValidExtPubKey("tpub...", Network.TESTNET)).toBeFalsy() 90 | expect(isValidExtPubKey("upub...", Network.TESTNET)).toBeFalsy() 91 | expect(isValidExtPubKey("vpub...", Network.TESTNET)).toBeFalsy() 92 | expect( 93 | isValidExtPubKey( 94 | "ExtPubKey6D7NqpxWckGwCHhpXoL4pH38m5xVty62KY2wUh6JoyDCofwHciDRoQ3xm7WAg2ffpHaC6X4bEociYq81niyNUGhCxEs6fDFAd1LPbEmzcAm", 95 | Network.MAINNET 96 | ) 97 | ).toBeFalsy() 98 | expect( 99 | isValidExtPubKey( 100 | "xpub6BfKpqjTwvH21wJGWEfxLppb8sU7C6FJge2kWb9315oP4ZVqCXG29cdUtkyu7YQhHyfA5nt63nzcNZHYmqXYHDxYo8mm1Xq1dAC7YtodwUr", 101 | Network.MAINNET 102 | ) 103 | ).toBeFalsy() 104 | }) 105 | test("mainnet keys (extPubKey/ypub/zpub) are invalid on testnet", () => { 106 | expect(isValidExtPubKey(KEYS.MAIN.XPUB[0], Network.TESTNET)).toBeFalsy() 107 | expect(isValidExtPubKey(KEYS.MAIN.YPUB[0], Network.TESTNET)).toBeFalsy() 108 | expect(isValidExtPubKey(KEYS.MAIN.ZPUB[0], Network.TESTNET)).toBeFalsy() 109 | }) 110 | test("testnet keys (tpub/upub/vpub) are invalid on mainnet", () => { 111 | expect(isValidExtPubKey(KEYS.TEST.TPUB[0], Network.MAINNET)).toBeFalsy() 112 | expect(isValidExtPubKey(KEYS.TEST.UPUB[0], Network.MAINNET)).toBeFalsy() 113 | expect(isValidExtPubKey(KEYS.TEST.VPUB[0], Network.MAINNET)).toBeFalsy() 114 | }) 115 | 116 | // BIP 32 and BIP 44: XPUB & TPUB 117 | test("mainnet xpub is valid", () => { 118 | expect(isValidExtPubKey(KEYS.MAIN.XPUB[0], Network.MAINNET)).toBeTruthy() 119 | expect(isValidExtPubKey(KEYS.MAIN.XPUB[1], Network.MAINNET)).toBeTruthy() 120 | expect(isValidExtPubKey(KEYS.MAIN.XPUB[2], Network.MAINNET)).toBeTruthy() 121 | }) 122 | test("testnet xpub (tpub) is valid", () => { 123 | expect(isValidExtPubKey(KEYS.TEST.TPUB[0], Network.TESTNET)).toBeTruthy() 124 | expect(isValidExtPubKey(KEYS.TEST.TPUB[1], Network.TESTNET)).toBeTruthy() 125 | expect(isValidExtPubKey(KEYS.TEST.TPUB[2], Network.TESTNET)).toBeTruthy() 126 | }) 127 | 128 | // BIP 49: YPUB & UPUB 129 | test("mainnet ypub is valid", () => { 130 | expect(isValidExtPubKey(KEYS.MAIN.YPUB[0], Network.MAINNET)).toBeTruthy() 131 | expect(isValidExtPubKey(KEYS.MAIN.YPUB[1], Network.MAINNET)).toBeTruthy() 132 | expect(isValidExtPubKey(KEYS.MAIN.YPUB[2], Network.MAINNET)).toBeTruthy() 133 | }) 134 | test("testnet ypub (upub) is valid", () => { 135 | expect(isValidExtPubKey(KEYS.TEST.UPUB[0], Network.TESTNET)).toBeTruthy() 136 | expect(isValidExtPubKey(KEYS.TEST.UPUB[1], Network.TESTNET)).toBeTruthy() 137 | expect(isValidExtPubKey(KEYS.TEST.UPUB[2], Network.TESTNET)).toBeTruthy() 138 | }) 139 | 140 | // BIP 84: ZPUB & VPUB 141 | test("mainnet zpub is valid", () => { 142 | expect(isValidExtPubKey(KEYS.MAIN.ZPUB[0], Network.MAINNET)).toBeTruthy() 143 | expect(isValidExtPubKey(KEYS.MAIN.ZPUB[1], Network.MAINNET)).toBeTruthy() 144 | expect(isValidExtPubKey(KEYS.MAIN.ZPUB[2], Network.MAINNET)).toBeTruthy() 145 | }) 146 | test("testnet zpub (vpub) is valid", () => { 147 | expect(isValidExtPubKey(KEYS.TEST.VPUB[0], Network.TESTNET)).toBeTruthy() 148 | expect(isValidExtPubKey(KEYS.TEST.VPUB[1], Network.TESTNET)).toBeTruthy() 149 | expect(isValidExtPubKey(KEYS.TEST.VPUB[2], Network.TESTNET)).toBeTruthy() 150 | }) 151 | }) 152 | 153 | describe("isValidAddress", () => { 154 | // MAINNET 155 | test("valid legacy (P2PKH) address on mainnet", () => { 156 | expect(isValidAddress(KEY.MAIN.LEGACY, Network.MAINNET)).toBeTruthy() 157 | }) 158 | test("valid segwit (P2SH) address on mainnet", () => { 159 | expect(isValidAddress(KEY.MAIN.SEGWIT, Network.MAINNET)).toBeTruthy() 160 | }) 161 | test("valid bech32 v0 (P2WPKH) address on mainnet", () => { 162 | expect(isValidAddress(KEY.MAIN.BECH32, Network.MAINNET)).toBeTruthy() 163 | }) 164 | test("valid bech32 v1 (P2TR) address on mainnet", () => { 165 | expect(isValidAddress(KEY.MAIN.TAPROOT, Network.MAINNET)).toBeTruthy() 166 | }) 167 | 168 | // TESTNET 169 | test("valid legacy (P2PKH) address on testnet", () => { 170 | expect(isValidAddress(KEY.TEST.LEGACY, Network.TESTNET)).toBeTruthy() 171 | }) 172 | test("valid segwit (P2SH) address on testnet", () => { 173 | expect(isValidAddress(KEY.TEST.SEGWIT, Network.TESTNET)).toBeTruthy() 174 | }) 175 | test("valid bech32 v0 (P2WPKH) address on testnet", () => { 176 | expect(isValidAddress(KEY.TEST.BECH32, Network.TESTNET)).toBeTruthy() 177 | }) 178 | test("valid bech32 v1 (P2TR) address on testnet", () => { 179 | expect(isValidAddress(KEY.TEST.TAPROOT, Network.TESTNET)).toBeTruthy() 180 | }) 181 | 182 | // INVALID: NETWORK MISMATCH 183 | test("invalid legacy (P2PKH) address (wrong network)", () => { 184 | expect(isValidAddress(KEY.TEST.LEGACY, Network.MAINNET)).toBeFalsy() 185 | expect(isValidAddress(KEY.MAIN.LEGACY, Network.TESTNET)).toBeFalsy() 186 | }) 187 | test("invalid segwit (P2SH) address (wrong network)", () => { 188 | expect(isValidAddress(KEY.TEST.SEGWIT, Network.MAINNET)).toBeFalsy() 189 | expect(isValidAddress(KEY.MAIN.SEGWIT, Network.TESTNET)).toBeFalsy() 190 | }) 191 | test("invalid bech32 (P2WPKH) address (wrong network)", () => { 192 | expect(isValidAddress(KEY.TEST.BECH32, Network.MAINNET)).toBeFalsy() 193 | expect(isValidAddress(KEY.MAIN.BECH32, Network.TESTNET)).toBeFalsy() 194 | }) 195 | 196 | // INVALID: BAD ADDRESS 197 | test("invalid addresses on mainnet", () => { 198 | expect(isValidAddress("", Network.MAINNET)).toBeFalsy() 199 | expect(isValidAddress(" ", Network.MAINNET)).toBeFalsy() 200 | expect(isValidAddress("1AdT...", Network.MAINNET)).toBeFalsy() 201 | expect(isValidAddress("3JDv...", Network.MAINNET)).toBeFalsy() 202 | expect(isValidAddress("bc1q...", Network.MAINNET)).toBeFalsy() 203 | expect(isValidAddress("bc1p...", Network.MAINNET)).toBeFalsy() 204 | expect( 205 | isValidAddress("1AdTLNfqiQtQ7yRNoZDEFTE9kSri2jrRVd", Network.MAINNET) 206 | ).toBeFalsy() 207 | expect( 208 | isValidAddress("3JDvonJcuQ7yQQQJh1tFLV74uRZUP6LgvF", Network.MAINNET) 209 | ).toBeFalsy() 210 | expect( 211 | isValidAddress( 212 | "bc1qdX0pd4h65d7mekkhk7n6jwzfwgqath7s0e368g", 213 | Network.MAINNET 214 | ) 215 | ).toBeFalsy() 216 | expect( 217 | isValidAddress(KEY.MAIN.TAPROOT.concat("abcd"), Network.MAINNET) 218 | ).toBeFalsy() 219 | expect( 220 | isValidAddress( 221 | KEY.MAIN.TAPROOT.substring(0, KEY.TEST.TAPROOT.length - 1), 222 | Network.MAINNET 223 | ) 224 | ).toBeFalsy() 225 | expect(isValidAddress(KEY.TEST.TAPROOT, Network.MAINNET)).toBeFalsy() 226 | }) 227 | test("invalid addresses on TESTNET", () => { 228 | expect(isValidAddress("", Network.TESTNET)).toBeFalsy() 229 | expect(isValidAddress(" ", Network.TESTNET)).toBeFalsy() 230 | expect(isValidAddress("1AdT...", Network.TESTNET)).toBeFalsy() 231 | expect(isValidAddress("3JDv...", Network.TESTNET)).toBeFalsy() 232 | expect(isValidAddress("bc1q...", Network.TESTNET)).toBeFalsy() 233 | expect(isValidAddress("tb1p...", Network.TESTNET)).toBeFalsy() 234 | expect( 235 | isValidAddress("mq9QdRkpXSKeu5tzX8Bc5NSucSTQxzpa8G", Network.TESTNET) 236 | ).toBeFalsy() 237 | expect( 238 | isValidAddress("2N9mhsXEeWrdKcC3rN9W7xS6L7mme9kJrVe", Network.TESTNET) 239 | ).toBeFalsy() 240 | expect( 241 | isValidAddress( 242 | "tb1qdx0pd4h65d7mekkhk7n6Jwzfwgqath7s9l2fum", 243 | Network.TESTNET 244 | ) 245 | ).toBeFalsy() 246 | expect( 247 | isValidAddress(KEY.TEST.TAPROOT.concat("abcd"), Network.TESTNET) 248 | ).toBeFalsy() 249 | expect( 250 | isValidAddress( 251 | KEY.TEST.TAPROOT.substring(0, KEY.TEST.TAPROOT.length - 1), 252 | Network.TESTNET 253 | ) 254 | ).toBeFalsy() 255 | expect(isValidAddress(KEY.MAIN.TAPROOT, Network.TESTNET)).toBeFalsy() 256 | }) 257 | }) 258 | -------------------------------------------------------------------------------- /packages/xpub-lib/src/derivation.test.js: -------------------------------------------------------------------------------- 1 | import { Network } from "@caravan/bitcoin" 2 | import { KEY, KEYS, WASABI, SAMOURAI } from "../test/fixtures" 3 | import { Purpose } from "./purpose" 4 | import { addressFromExtPubKey, addressesFromExtPubKey, initEccLib } from "./derivation" 5 | import * as ecc from "tiny-secp256k1" 6 | 7 | beforeAll(() => { 8 | initEccLib(ecc) 9 | }) 10 | 11 | describe("addressFromExtPubKey() with invalid xpubs", () => { 12 | test("address generation from invalid xpub fails", () => { 13 | expect(addressFromExtPubKey({ extPubKey: "" })).toBeFalsy() 14 | expect(addressFromExtPubKey({ extPubKey: "xpub123" })).toBeFalsy() 15 | }) 16 | test("address generation with invalid parameters fails", () => { 17 | expect( 18 | addressFromExtPubKey({ extPubKey: KEY.TEST.TPUB, keyIndex: -1 }) 19 | ).toBeFalsy() 20 | expect( 21 | addressFromExtPubKey({ 22 | extPubKey: KEY.TEST.TPUB, 23 | network: Network.MAINNET, 24 | }) 25 | ).toBeFalsy() 26 | expect( 27 | addressFromExtPubKey({ extPubKey: KEY.TEST.TPUB, purpose: "99" }) 28 | ).toBeFalsy() 29 | }) 30 | }) 31 | 32 | describe("addressFromExtPubKey(MAINNET)", () => { 33 | // BIP 44 34 | test("P2PKH address generation from xpub", () => { 35 | expect( 36 | addressFromExtPubKey({ 37 | extPubKey: KEY.MAIN.XPUB, 38 | keyIndex: 0, 39 | purpose: Purpose.P2PKH, 40 | network: Network.MAINNET, 41 | }).address 42 | ).toBe(KEY.MAIN.LEGACY) 43 | }) 44 | test("P2PKH address generation from ypub", () => { 45 | expect( 46 | addressFromExtPubKey({ 47 | extPubKey: KEY.MAIN.YPUB, 48 | keyIndex: 0, 49 | purpose: Purpose.P2PKH, 50 | network: Network.MAINNET, 51 | }).address 52 | ).toBe(KEY.MAIN.LEGACY) 53 | }) 54 | test("P2PKH address generation from zpub", () => { 55 | expect( 56 | addressFromExtPubKey({ 57 | extPubKey: KEY.MAIN.ZPUB, 58 | keyIndex: 0, 59 | purpose: Purpose.P2PKH, 60 | network: Network.MAINNET, 61 | }).address 62 | ).toBe(KEY.MAIN.LEGACY) 63 | }) 64 | test("P2PKH change address generation from xpub", () => { 65 | expect( 66 | addressFromExtPubKey({ 67 | extPubKey: KEY.MAIN.XPUB, 68 | change: 1, 69 | keyIndex: 0, 70 | purpose: Purpose.P2PKH, 71 | network: Network.MAINNET, 72 | }).address 73 | ).toBe(KEY.MAIN.CHANGE.LEGACY) 74 | }) 75 | test("P2PKH change address generation from ypub", () => { 76 | expect( 77 | addressFromExtPubKey({ 78 | extPubKey: KEY.MAIN.YPUB, 79 | change: 1, 80 | keyIndex: 0, 81 | purpose: Purpose.P2PKH, 82 | network: Network.MAINNET, 83 | }).address 84 | ).toBe(KEY.MAIN.CHANGE.LEGACY) 85 | }) 86 | test("P2PKH change address generation from zpub", () => { 87 | expect( 88 | addressFromExtPubKey({ 89 | extPubKey: KEY.MAIN.ZPUB, 90 | change: 1, 91 | keyIndex: 0, 92 | purpose: Purpose.P2PKH, 93 | network: Network.MAINNET, 94 | }).address 95 | ).toBe(KEY.MAIN.CHANGE.LEGACY) 96 | }) 97 | 98 | // BIP 49 99 | test("P2SH address generation from xpub", () => { 100 | expect( 101 | addressFromExtPubKey({ 102 | extPubKey: KEY.MAIN.XPUB, 103 | keyIndex: 0, 104 | purpose: Purpose.P2SH, 105 | network: Network.MAINNET, 106 | }).address 107 | ).toBe(KEY.MAIN.SEGWIT) 108 | }) 109 | test("P2SH address generation from ypub", () => { 110 | expect( 111 | addressFromExtPubKey({ 112 | extPubKey: KEY.MAIN.YPUB, 113 | keyIndex: 0, 114 | purpose: Purpose.P2SH, 115 | network: Network.MAINNET, 116 | }).address 117 | ).toBe(KEY.MAIN.SEGWIT) 118 | }) 119 | test("P2SH address generation from zpub", () => { 120 | expect( 121 | addressFromExtPubKey({ 122 | extPubKey: KEY.MAIN.ZPUB, 123 | keyIndex: 0, 124 | purpose: Purpose.P2SH, 125 | network: Network.MAINNET, 126 | }).address 127 | ).toBe(KEY.MAIN.SEGWIT) 128 | }) 129 | test("P2SH change address generation from xpub", () => { 130 | expect( 131 | addressFromExtPubKey({ 132 | extPubKey: KEY.MAIN.XPUB, 133 | change: 1, 134 | keyIndex: 0, 135 | purpose: Purpose.P2SH, 136 | network: Network.MAINNET, 137 | }).address 138 | ).toBe(KEY.MAIN.CHANGE.SEGWIT) 139 | }) 140 | test("P2SH change address generation from ypub", () => { 141 | expect( 142 | addressFromExtPubKey({ 143 | extPubKey: KEY.MAIN.YPUB, 144 | change: 1, 145 | keyIndex: 0, 146 | purpose: Purpose.P2SH, 147 | network: Network.MAINNET, 148 | }).address 149 | ).toBe(KEY.MAIN.CHANGE.SEGWIT) 150 | }) 151 | test("P2SH change address generation from zpub", () => { 152 | expect( 153 | addressFromExtPubKey({ 154 | extPubKey: KEY.MAIN.ZPUB, 155 | change: 1, 156 | keyIndex: 0, 157 | purpose: Purpose.P2SH, 158 | network: Network.MAINNET, 159 | }).address 160 | ).toBe(KEY.MAIN.CHANGE.SEGWIT) 161 | }) 162 | 163 | // BIP 84 164 | test("P2WPKH address generation from xpub", () => { 165 | expect( 166 | addressFromExtPubKey({ 167 | extPubKey: KEY.MAIN.XPUB, 168 | keyIndex: 0, 169 | purpose: Purpose.P2WPKH, 170 | network: Network.MAINNET, 171 | }).address 172 | ).toBe(KEY.MAIN.BECH32) 173 | }) 174 | test("P2WPKH address generation from ypub", () => { 175 | expect( 176 | addressFromExtPubKey({ 177 | extPubKey: KEY.MAIN.YPUB, 178 | keyIndex: 0, 179 | purpose: Purpose.P2WPKH, 180 | network: Network.MAINNET, 181 | }).address 182 | ).toBe(KEY.MAIN.BECH32) 183 | }) 184 | test("P2WPKH address generation from zpub", () => { 185 | expect( 186 | addressFromExtPubKey({ 187 | extPubKey: KEY.MAIN.ZPUB, 188 | keyIndex: 0, 189 | purpose: Purpose.P2WPKH, 190 | network: Network.MAINNET, 191 | }).address 192 | ).toBe(KEY.MAIN.BECH32) 193 | }) 194 | test("P2WPKH change address generation from xpub", () => { 195 | expect( 196 | addressFromExtPubKey({ 197 | extPubKey: KEY.MAIN.XPUB, 198 | change: 1, 199 | keyIndex: 0, 200 | purpose: Purpose.P2WPKH, 201 | network: Network.MAINNET, 202 | }).address 203 | ).toBe(KEY.MAIN.CHANGE.BECH32) 204 | }) 205 | test("P2WPKH change address generation from ypub", () => { 206 | expect( 207 | addressFromExtPubKey({ 208 | extPubKey: KEY.MAIN.YPUB, 209 | change: 1, 210 | keyIndex: 0, 211 | purpose: Purpose.P2WPKH, 212 | network: Network.MAINNET, 213 | }).address 214 | ).toBe(KEY.MAIN.CHANGE.BECH32) 215 | }) 216 | test("P2WPKH change address generation from zpub", () => { 217 | expect( 218 | addressFromExtPubKey({ 219 | extPubKey: KEY.MAIN.ZPUB, 220 | change: 1, 221 | keyIndex: 0, 222 | purpose: Purpose.P2WPKH, 223 | network: Network.MAINNET, 224 | }).address 225 | ).toBe(KEY.MAIN.CHANGE.BECH32) 226 | }) 227 | // BIP 86 228 | test("P2TR address generation from xpub", () => { 229 | expect( 230 | addressFromExtPubKey({ 231 | extPubKey: KEY.MAIN.XPUB, 232 | purpose: Purpose.P2TR, 233 | network: Network.MAINNET, 234 | }).address 235 | ).toBe(KEY.MAIN.TAPROOT) 236 | }) 237 | test("P2TR change address generation from xpub", () => { 238 | expect( 239 | addressFromExtPubKey({ 240 | extPubKey: KEY.MAIN.XPUB, 241 | change: 1, 242 | purpose: Purpose.P2TR, 243 | network: Network.MAINNET, 244 | }).address 245 | ).toBe(KEY.MAIN.CHANGE.TAPROOT) 246 | }) 247 | }) 248 | 249 | describe("addressFromExtPubKey(TESTNET)", () => { 250 | // BIP 44 251 | test("P2PKH address generation from tpub", () => { 252 | expect( 253 | addressFromExtPubKey({ 254 | extPubKey: KEY.TEST.TPUB, 255 | purpose: Purpose.P2PKH, 256 | network: Network.TESTNET, 257 | }).address 258 | ).toBe(KEY.TEST.LEGACY) 259 | }) 260 | test("P2PKH address generation from upub", () => { 261 | expect( 262 | addressFromExtPubKey({ 263 | extPubKey: KEY.TEST.UPUB, 264 | purpose: Purpose.P2PKH, 265 | network: Network.TESTNET, 266 | }).address 267 | ).toBe(KEY.TEST.LEGACY) 268 | }) 269 | test("P2PKH address generation from vpub", () => { 270 | expect( 271 | addressFromExtPubKey({ 272 | extPubKey: KEY.TEST.VPUB, 273 | purpose: Purpose.P2PKH, 274 | network: Network.TESTNET, 275 | }).address 276 | ).toBe(KEY.TEST.LEGACY) 277 | }) 278 | test("P2PKH change address generation from tpub", () => { 279 | expect( 280 | addressFromExtPubKey({ 281 | extPubKey: KEY.TEST.TPUB, 282 | change: 1, 283 | purpose: Purpose.P2PKH, 284 | network: Network.TESTNET, 285 | }).address 286 | ).toBe(KEY.TEST.CHANGE.LEGACY) 287 | }) 288 | test("P2PKH change address generation from upub", () => { 289 | expect( 290 | addressFromExtPubKey({ 291 | extPubKey: KEY.TEST.UPUB, 292 | change: 1, 293 | purpose: Purpose.P2PKH, 294 | network: Network.TESTNET, 295 | }).address 296 | ).toBe(KEY.TEST.CHANGE.LEGACY) 297 | }) 298 | test("P2PKH change address generation from vpub", () => { 299 | expect( 300 | addressFromExtPubKey({ 301 | extPubKey: KEY.TEST.VPUB, 302 | change: 1, 303 | purpose: Purpose.P2PKH, 304 | network: Network.TESTNET, 305 | }).address 306 | ).toBe(KEY.TEST.CHANGE.LEGACY) 307 | }) 308 | 309 | // BIP 49 310 | test("P2SH address generation from tpub", () => { 311 | expect( 312 | addressFromExtPubKey({ 313 | extPubKey: KEY.TEST.TPUB, 314 | purpose: Purpose.P2SH, 315 | network: Network.TESTNET, 316 | }).address 317 | ).toBe(KEY.TEST.SEGWIT) 318 | }) 319 | test("P2SH address generation from upub", () => { 320 | expect( 321 | addressFromExtPubKey({ 322 | extPubKey: KEY.TEST.UPUB, 323 | purpose: Purpose.P2SH, 324 | network: Network.TESTNET, 325 | }).address 326 | ).toBe(KEY.TEST.SEGWIT) 327 | }) 328 | test("P2SH address generation from vpub", () => { 329 | expect( 330 | addressFromExtPubKey({ 331 | extPubKey: KEY.TEST.VPUB, 332 | purpose: Purpose.P2SH, 333 | network: Network.TESTNET, 334 | }).address 335 | ).toBe(KEY.TEST.SEGWIT) 336 | }) 337 | test("P2SH change address generation from tpub", () => { 338 | expect( 339 | addressFromExtPubKey({ 340 | extPubKey: KEY.TEST.TPUB, 341 | change: 1, 342 | purpose: Purpose.P2SH, 343 | network: Network.TESTNET, 344 | }).address 345 | ).toBe(KEY.TEST.CHANGE.SEGWIT) 346 | }) 347 | test("P2SH change address generation from upub", () => { 348 | expect( 349 | addressFromExtPubKey({ 350 | extPubKey: KEY.TEST.UPUB, 351 | change: 1, 352 | purpose: Purpose.P2SH, 353 | network: Network.TESTNET, 354 | }).address 355 | ).toBe(KEY.TEST.CHANGE.SEGWIT) 356 | }) 357 | test("P2SH change address generation from vpub", () => { 358 | expect( 359 | addressFromExtPubKey({ 360 | extPubKey: KEY.TEST.VPUB, 361 | change: 1, 362 | purpose: Purpose.P2SH, 363 | network: Network.TESTNET, 364 | }).address 365 | ).toBe(KEY.TEST.CHANGE.SEGWIT) 366 | }) 367 | 368 | // BIP 84 369 | test("P2WPKH address generation from tpub", () => { 370 | expect( 371 | addressFromExtPubKey({ 372 | extPubKey: KEY.TEST.TPUB, 373 | purpose: Purpose.P2WPKH, 374 | network: Network.TESTNET, 375 | }).address 376 | ).toBe(KEY.TEST.BECH32) 377 | }) 378 | test("P2WPKH address generation from upub", () => { 379 | expect( 380 | addressFromExtPubKey({ 381 | extPubKey: KEY.TEST.UPUB, 382 | purpose: Purpose.P2WPKH, 383 | network: Network.TESTNET, 384 | }).address 385 | ).toBe(KEY.TEST.BECH32) 386 | }) 387 | test("P2WPKH address generation from vpub", () => { 388 | expect( 389 | addressFromExtPubKey({ 390 | extPubKey: KEY.TEST.VPUB, 391 | purpose: Purpose.P2WPKH, 392 | network: Network.TESTNET, 393 | }).address 394 | ).toBe(KEY.TEST.BECH32) 395 | }) 396 | test("P2WPKH change address generation from tpub", () => { 397 | expect( 398 | addressFromExtPubKey({ 399 | extPubKey: KEY.TEST.TPUB, 400 | change: 1, 401 | purpose: Purpose.P2WPKH, 402 | network: Network.TESTNET, 403 | }).address 404 | ).toBe(KEY.TEST.CHANGE.BECH32) 405 | }) 406 | test("P2WPKH change address generation from upub", () => { 407 | expect( 408 | addressFromExtPubKey({ 409 | extPubKey: KEY.TEST.UPUB, 410 | change: 1, 411 | purpose: Purpose.P2WPKH, 412 | network: Network.TESTNET, 413 | }).address 414 | ).toBe(KEY.TEST.CHANGE.BECH32) 415 | }) 416 | test("P2WPKH change address generation from vpub", () => { 417 | expect( 418 | addressFromExtPubKey({ 419 | extPubKey: KEY.TEST.VPUB, 420 | change: 1, 421 | purpose: Purpose.P2WPKH, 422 | network: Network.TESTNET, 423 | }).address 424 | ).toBe(KEY.TEST.CHANGE.BECH32) 425 | }) 426 | 427 | // BIP 86 428 | test("P2TR address generation from tpub", () => { 429 | expect( 430 | addressFromExtPubKey({ 431 | extPubKey: KEY.TEST.TPUB, 432 | purpose: Purpose.P2TR, 433 | network: Network.TESTNET, 434 | }).address 435 | ).toBe(KEY.TEST.TAPROOT) 436 | }) 437 | test("P2TR change address generation from tpub", () => { 438 | expect( 439 | addressFromExtPubKey({ 440 | extPubKey: KEY.TEST.TPUB, 441 | change: 1, 442 | purpose: Purpose.P2TR, 443 | network: Network.TESTNET, 444 | }).address 445 | ).toBe(KEY.TEST.CHANGE.TAPROOT) 446 | }) 447 | 448 | }) 449 | 450 | describe("addressFromExtPubKey", () => { 451 | test("default address generation from tpub (default = testnet)", () => { 452 | expect(addressFromExtPubKey({ extPubKey: KEY.TEST.TPUB }).address).toBe( 453 | KEY.TEST.BECH32 454 | ) 455 | }) 456 | test("default address generation from xpub on mainnet", () => { 457 | expect( 458 | addressFromExtPubKey({ 459 | extPubKey: KEY.MAIN.XPUB, 460 | network: Network.MAINNET, // or "mainnet" 461 | }).address 462 | ).toBe(KEY.MAIN.BECH32) 463 | }) 464 | 465 | test("forbid testnet address generation from mainnet key", () => { 466 | expect( 467 | addressFromExtPubKey({ 468 | extPubKey: KEYS.MAIN.XPUB[0], 469 | network: Network.TESTNET, 470 | }) 471 | ).toBeFalsy() 472 | expect( 473 | addressFromExtPubKey({ 474 | extPubKey: KEYS.MAIN.YPUB[0], 475 | network: Network.TESTNET, 476 | }) 477 | ).toBeFalsy() 478 | expect( 479 | addressFromExtPubKey({ 480 | extPubKey: KEYS.MAIN.ZPUB[0], 481 | network: Network.TESTNET, 482 | }) 483 | ).toBeFalsy() 484 | }) 485 | test("forbid mainnet address generation from testnet key", () => { 486 | expect( 487 | addressFromExtPubKey({ 488 | extPubKey: KEYS.TEST.TPUB[0], 489 | network: Network.MAINNET, 490 | }) 491 | ).toBeFalsy() 492 | expect( 493 | addressFromExtPubKey({ 494 | extPubKey: KEYS.TEST.UPUB[0], 495 | network: Network.MAINNET, 496 | }) 497 | ).toBeFalsy() 498 | expect( 499 | addressFromExtPubKey({ 500 | extPubKey: KEYS.TEST.VPUB[0], 501 | network: Network.MAINNET, 502 | }) 503 | ).toBeFalsy() 504 | }) 505 | }) 506 | 507 | describe("addressesFromExtPubKey", () => { 508 | test("default address generation from vpub on testnet", () => { 509 | expect( 510 | addressesFromExtPubKey({ 511 | extPubKey: KEY.TEST.VPUB, 512 | addressCount: 3, 513 | }).length 514 | ).toEqual(3) 515 | }) 516 | test("offset address generation from vpub on testnet", () => { 517 | const expected = [KEYS.TEST.VPUB[1], KEYS.TEST.VPUB[2]] 518 | const addresses = addressesFromExtPubKey({ 519 | extPubKey: KEY.TEST.VPUB, 520 | addressCount: 2, 521 | addressStartIndex: 1, 522 | }) 523 | expect(addresses.length).toBe(expected.length) 524 | }) 525 | test("default address generation from xpub on mainnet", () => { 526 | expect( 527 | addressesFromExtPubKey({ 528 | extPubKey: WASABI.XPUB, 529 | addressCount: 3, 530 | network: Network.MAINNET, 531 | }).map((obj) => obj.address) 532 | ).toStrictEqual(WASABI.ADDRESSES) 533 | }) 534 | test("default address generation from ypub on mainnet", () => { 535 | expect( 536 | addressesFromExtPubKey({ 537 | extPubKey: WASABI.YPUB, 538 | addressCount: 3, 539 | network: Network.MAINNET, 540 | }).map((obj) => obj.address) 541 | ).toStrictEqual(WASABI.ADDRESSES) 542 | }) 543 | test("default address generation from zpub on mainnet", () => { 544 | expect( 545 | addressesFromExtPubKey({ 546 | extPubKey: WASABI.ZPUB, 547 | addressCount: 3, 548 | network: Network.MAINNET, 549 | }).map((obj) => obj.address) 550 | ).toStrictEqual(WASABI.ADDRESSES) 551 | expect( 552 | addressesFromExtPubKey({ 553 | extPubKey: SAMOURAI.ZPUB, 554 | addressCount: 3, 555 | network: Network.MAINNET, 556 | }).map((obj) => obj.address) 557 | ).toStrictEqual(SAMOURAI.ADDRESSES) 558 | expect( 559 | addressesFromExtPubKey({ 560 | extPubKey: SAMOURAI.ZPUB, 561 | addressCount: 20, // generate 20 addresses 562 | network: Network.MAINNET, 563 | }).map((obj) => obj.address)[19] // pick nr. 20 and compare 564 | ).toEqual("bc1qrkv7s6enp5n7nnz97g2em2q4jefcmt9208syg0") 565 | }) 566 | test("offset address generation from zpub on mainnet", () => { 567 | // Wasabi 568 | const expectedWasabi = [WASABI.ADDRESSES[1], WASABI.ADDRESSES[2]] 569 | const addressesWasabi = addressesFromExtPubKey({ 570 | extPubKey: WASABI.ZPUB, 571 | addressStartIndex: 1, 572 | addressCount: 2, 573 | network: Network.MAINNET, 574 | }) 575 | expect(addressesWasabi.length).toBe(expectedWasabi.length) 576 | expect(addressesWasabi.map((obj) => obj.address)).toEqual(expectedWasabi) 577 | 578 | // Samourai 579 | const expectedSamourai = [ 580 | "bc1qmderpmzcft4csyq0dnned2sw69np6nljes4we3", 581 | "bc1q0lw3ae3uujqcyk3wd40acf0q0wyzza9tysucwg", 582 | "bc1qqnmqt9zkawf8x9j3dl9pqlhlw5gzcuj04ujjw3", 583 | ] 584 | const addressesSamourai = addressesFromExtPubKey({ 585 | extPubKey: SAMOURAI.ZPUB, 586 | addressStartIndex: 16, 587 | addressCount: 3, 588 | network: Network.MAINNET, 589 | }) 590 | expect(addressesSamourai.length).toBe(expectedSamourai.length) 591 | expect(addressesSamourai.map((obj) => obj.address)).toEqual( 592 | expectedSamourai 593 | ) 594 | }) 595 | }) 596 | --------------------------------------------------------------------------------