├── .gitignore ├── entrypoint.sh ├── tsconfig.json ├── tsup.config.ts ├── Dockerfile ├── README.md ├── LICENSE ├── package.json └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node lib/index.mjs $@ 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "resolveJsonModule": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup' 2 | 3 | export const tsup: Options = { 4 | splitting: false, 5 | clean: true, 6 | format: ['esm'], 7 | minify: true, 8 | bundle: true, 9 | noExternal: ['fuzzy', '@scure/bip39'], 10 | entry: ['src/**/*.ts'], 11 | target: 'es2020', 12 | outDir: 'lib' 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.14.0@sha256:0d8bf0e743a752d8d01e9ff8aba21ac15a0ad1a3d2a2b8df90764d427618c791 2 | 3 | WORKDIR /opt/nip06-cli 4 | 5 | COPY package.json package-lock.json ./ 6 | RUN npm ci 7 | 8 | COPY tsconfig.json tsup.config.ts ./ 9 | COPY ./src/ ./src/ 10 | 11 | RUN npm run build && rm -rf src 12 | 13 | COPY entrypoint.sh ./ 14 | 15 | ENTRYPOINT [ "sh", "./entrypoint.sh" ] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nip06-cli 2 | 3 | NodeJS CLI to generate or restore Nostr [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md) 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i -g nip06-cli 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | $ nip06-cli [options] [command] 15 | 16 | NodeJS CLI to generate or restore Nostr NIP-06 17 | 18 | Options: 19 | -v, --version output the version number 20 | -h, --help display help for command 21 | 22 | Commands: 23 | random Generate a random mnemonic 24 | restore Restore an existing mnemonic 25 | help [command] display help for command 26 | ``` 27 | 28 | ## Docker 29 | 30 | ``` 31 | docker run --rm -it jaonoctus/nip06-cli [options] [command] 32 | ``` 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 jaonoctus 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nip06-cli", 3 | "version": "1.1.1", 4 | "description": "NIP06 CLI", 5 | "author": { 6 | "email": "jaonoctus@protonmail.com", 7 | "name": "jaonoctus", 8 | "url": "https://twitter.com/jaonoctus" 9 | }, 10 | "homepage": "https://github.com/jaonoctus/nip06-cli", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/jaonoctus/nip06-cli.git" 14 | }, 15 | "keywords": [ 16 | "cli", 17 | "nip06", 18 | "nostr", 19 | "bip39", 20 | "bip32", 21 | "bech32" 22 | ], 23 | "bin": { 24 | "nip06-cli": "./lib/index.mjs" 25 | }, 26 | "files": [ 27 | "lib" 28 | ], 29 | "license": "MIT", 30 | "scripts": { 31 | "build": "tsup", 32 | "dev": "tsx src/index.ts" 33 | }, 34 | "devDependencies": { 35 | "@types/inquirer": "9.0.7", 36 | "@types/inquirer-autocomplete-prompt": "3.0.3", 37 | "@types/node": "20.12.11", 38 | "tsup": "8.0.2", 39 | "tsx": "4.10.0", 40 | "typescript": "5.4.5" 41 | }, 42 | "dependencies": { 43 | "@scure/bip39": "1.3.0", 44 | "chalk": "5.3.0", 45 | "commander": "12.0.0", 46 | "fuzzy": "0.1.3", 47 | "inquirer": "9.2.20", 48 | "inquirer-autocomplete-prompt": "3.0.1", 49 | "nip06": "1.0.3" 50 | }, 51 | "engines": { 52 | "node": ">=16.4.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander' 3 | import inquirer from 'inquirer' 4 | import chalk from 'chalk' 5 | import inquirerPrompt from 'inquirer-autocomplete-prompt' 6 | import { 7 | generateSeedWords, 8 | privateKeyFromSeedWords, 9 | getPublicKey, 10 | getBech32PrivateKey, 11 | getBech32PublicKey, 12 | validateWords 13 | } from 'nip06' 14 | import * as fuzzy from 'fuzzy' 15 | import { version } from '../package.json' 16 | import { wordlist } from '@scure/bip39/wordlists/english' 17 | 18 | inquirer.registerPrompt('autocomplete', inquirerPrompt) 19 | 20 | const program = new Command() 21 | 22 | program 23 | .name('nip06-cli') 24 | .description('NodeJS CLI to generate or restore Nostr NIP-06') 25 | .version(version, '-v, --version') 26 | 27 | program 28 | .command('random') 29 | .description('Generate a random mnemonic') 30 | .action(async () => { 31 | const { passphrase } = await askPassphrase() 32 | const { mnemonic } = generateSeedWords() 33 | outputKeys({ mnemonic, passphrase }) 34 | }) 35 | 36 | program 37 | .command('restore') 38 | .description('Restore an existing mnemonic') 39 | .option('-m, --mnemonic ') 40 | .option('-p, --passphrase ', '') 41 | .action(async (cmd?: { mnemonic?: string, passphrase: string }) => { 42 | let passphrase = '' 43 | let mnemonic 44 | 45 | if (!cmd?.mnemonic) { 46 | let words = [] 47 | const { wordsCount } = await inquirer.prompt([ 48 | { 49 | type: 'list', 50 | name: 'wordsCount', 51 | message: 'Mnemonic size:', 52 | choices: [12, 24], 53 | default: 12 54 | }, 55 | ]) 56 | 57 | for(let i = 0; i < wordsCount; i++) { 58 | const { word } = await askWord(i) 59 | words.push(word) 60 | } 61 | 62 | mnemonic = words.join(' '); 63 | 64 | ({ passphrase } = await askPassphrase()) 65 | } else { 66 | mnemonic = cmd.mnemonic 67 | passphrase = cmd.passphrase 68 | } 69 | 70 | const { isMnemonicValid } = validateWords({ mnemonic }) 71 | 72 | if (!isMnemonicValid) { 73 | console.log(chalk.bold(chalk.red('[ERROR] INVALID MNEMONIC'))) 74 | console.log(chalk.yellow('>'), 'mnemonic:', chalk.red(mnemonic)) 75 | return 76 | } 77 | 78 | outputKeys({ mnemonic, passphrase }) 79 | }) 80 | 81 | program.parse(process.argv) 82 | 83 | async function askWord(index: number) { 84 | const { word } = await inquirer.prompt([ 85 | { 86 | type: 'autocomplete', 87 | name: 'word', 88 | message: `Word [${index + 1}]:`, 89 | source: searchWord, 90 | default: '' 91 | }, 92 | ]) 93 | 94 | return { word } 95 | } 96 | 97 | function searchWord(_answersSoFar: any, input = '') { 98 | return new Promise((resolve) => { 99 | const wordsStartsWithInput = wordlist.filter((word) => word.startsWith(input)) 100 | 101 | if (Array.isArray(wordsStartsWithInput) && wordsStartsWithInput.length > 0) { 102 | resolve(wordsStartsWithInput) 103 | } 104 | 105 | resolve(fuzzy.filter(input, wordlist).map((el) => el.original)) 106 | }) 107 | } 108 | 109 | function outputKeys({ mnemonic, passphrase }: { mnemonic: string, passphrase?: string }) { 110 | const { privateKey } = privateKeyFromSeedWords({ mnemonic, passphrase }) 111 | const { publicKey } = getPublicKey({ privateKey }) 112 | const { bech32PrivateKey } = getBech32PrivateKey({ privateKey }) 113 | const { bech32PublicKey } = getBech32PublicKey({ publicKey }) 114 | 115 | console.log(chalk.gray('mnemonic:'), chalk.bgCyan(mnemonic)) 116 | console.log(chalk.gray('hex private key:'), chalk.cyan(privateKey)) 117 | console.log(chalk.gray('hex public key:'), chalk.cyan(publicKey)) 118 | console.log(chalk.gray('bech32 private key:'), chalk.cyan(bech32PrivateKey)) 119 | console.log(chalk.gray('bech32 public key:'), chalk.cyan(bech32PublicKey)) 120 | } 121 | 122 | async function askPassphrase() { 123 | let passphrase = '' 124 | let passphraseConfirmation = '' 125 | 126 | const { usePassphrase } = await inquirer.prompt([ 127 | { 128 | type: 'confirm', 129 | name: 'usePassphrase', 130 | message: 'Do you want to use a passphrase?', 131 | default: false 132 | } 133 | ]) 134 | 135 | if (usePassphrase) { 136 | ({ passphrase, passphraseConfirmation } = await inquirer.prompt([ 137 | { 138 | type: 'password', 139 | name: 'passphrase', 140 | message: 'Passphrase:' 141 | }, 142 | { 143 | type: 'password', 144 | name: 'passphraseConfirmation', 145 | message: 'Type the passphrase again:' 146 | } 147 | ])) 148 | 149 | if (passphrase !== passphraseConfirmation) { 150 | console.log(chalk.bold(chalk.red('[ERROR] PASSPHRASES DOES NOT MATCH.'))) 151 | return process.exit(1) 152 | } 153 | } 154 | 155 | return { 156 | passphrase 157 | } 158 | } 159 | --------------------------------------------------------------------------------