Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 BIP39 contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
161 changes: 161 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<div align="center">
<h1>BIP39</h1>
<p><strong>TypeScript library and CLI for generating, validating, and converting BIP39 English mnemonics</strong></p>
<img alt="License" src="https://img.shields.io/badge/license-MIT-blue.svg" />
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5.9.3-3178C6?logo=typescript&logoColor=white" />
<img alt="Node" src="https://img.shields.io/badge/node-%3E%3D24-339933?logo=node.js&logoColor=white" />
<img alt="pnpm" src="https://img.shields.io/badge/pnpm-10.30.0-F69220?logo=pnpm&logoColor=white" />
</div>


## Project Overview

This repository provides the core BIP39 workflows for the English wordlist profile:

- Convert entropy to a mnemonic sentence
- Convert a mnemonic sentence back to entropy
- Derive a 64-byte seed from a mnemonic and passphrase
- Validate mnemonic structure, word membership, and checksum
- Use the same behavior from both a TypeScript API and a CLI

It is backed by pinned specification assets in `assets/`, including the English wordlist and official test vectors.

## Warnings

- This project targets the English BIP39 profile only.
- The CLI and exported APIs distinguish between strict parsing and compatibility normalization; do not assume all inputs are auto-corrected.
- Generated mnemonics and derived seeds are sensitive secrets. Never commit them, log them, or share them in screenshots.
- This repository implements BIP39 behavior only. It does not implement BIP32, derivation paths, addresses, or wallet UX.

## Quick Start

### 1. Install dependencies

```bash
pnpm install
```

### 2. Build the project

```bash
pnpm build
```

### 3. Show CLI help

```bash
node dist/cli/index.js --help
```

### 4. Try the CLI

Generate a 12-word mnemonic:

```bash
node dist/cli/index.js generate-mnemonic --words 12
```

Convert entropy hex to a mnemonic:

```bash
node dist/cli/index.js entropy-to-mnemonic 00000000000000000000000000000000
```

Validate a mnemonic:

```bash
node dist/cli/index.js validate "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
```

Derive a seed:

```bash
node dist/cli/index.js mnemonic-to-seed "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" --passphrase TREZOR
```

### 5. Use the library API

```ts
import {
entropyToMnemonic,
generateEntropy,
mnemonicToEntropy,
mnemonicToSeed,
validateMnemonic,
} from "./dist/index.js";

const entropy = generateEntropy(16);
const mnemonic = entropyToMnemonic(entropy);
const roundTripEntropy = mnemonicToEntropy(mnemonic);
const seed = mnemonicToSeed(mnemonic, "TREZOR");
const validation = validateMnemonic(mnemonic);

console.log({
mnemonic,
roundTripEntropyLength: roundTripEntropy.length,
seedLength: seed.length,
validation,
});
```

## Development Setup

1. Ensure `Node.js >= 24` is installed.
2. Ensure `pnpm 10.30.0` or a compatible `pnpm` version is available.
3. Clone the repository.
4. Install dependencies with `pnpm install`.
5. Run `pnpm test` to execute the test suite.
6. Run `pnpm typecheck` to verify TypeScript types.
7. Run `pnpm lint` to check formatting and static issues.
8. Run `pnpm build` to emit JavaScript into `dist/`.

## Output Location

- Compiled JavaScript is emitted to `dist/`.
- The CLI entry point is emitted to `dist/cli/index.js`.
- Source files remain in `src/`.
- Tests live in `test/` and are not emitted by the build.

## Configuration File Setup

No runtime configuration file is required.

The main repository-level configuration files are:

- `package.json` for scripts and package metadata
- `tsconfig.json` and `tsconfig.build.json` for TypeScript compilation
- `biome.json` for linting and formatting

## Environment Variables

No environment variables are required for standard development, build, test, or CLI usage.

## Directory Structure

```text
.
├── assets/ # Pinned specification assets and test vectors
├── src/ # TypeScript source code
│ ├── bip39/ # Core entropy/mnemonic/seed workflows
│ ├── bits/ # Bit conversion helpers
│ ├── cli/ # Command-line interface
│ ├── constants/ # Fixed BIP39 constants and mappings
│ ├── crypto/ # SHA-256 and PBKDF2 wrappers
│ ├── entropy/ # Secure entropy generation
│ ├── errors/ # Standard error codes
│ ├── integration/ # Error messaging and integration adapters
│ ├── normalize/ # Compatibility input normalization
│ ├── parser/ # Strict mnemonic parsing rules
│ ├── types/ # Shared DTOs and result types
│ └── index.ts # Public export surface
├── test/ # Unit and integration tests
├── biome.json # Lint/format configuration
├── package.json # Scripts and package metadata
├── README.md # Project overview and usage
├── tsconfig.build.json # Build-time TypeScript configuration
└── tsconfig.json # Base TypeScript configuration
```

## License

MIT - see [LICENSE](LICENSE).
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"bip39": "dist/cli/index.js"
},
"scripts": {
"test": "node --test --import tsx \"test/**/*.test.ts\" \"test/**/*.spec.ts\" \"test/**/*.test.js\" \"test/**/*.spec.js\"",
"lint": "biome check .",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.json",
"build": "tsc -p tsconfig.build.json",
"ci": "pnpm lint && pnpm typecheck && pnpm build",
"format": "biome format --write ."
},
"keywords": [],
"author": "",
"license": "ISC",
"license": "MIT",
"type": "module",
"packageManager": "pnpm@10.30.0",
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions src/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
- `src/normalize/` provides a compatibility input adapter (trim, NFKD, lowercase).
- `src/parser/` implements strict mnemonic parsing contracts.
- `src/entropy/` generates entropy via secure randomness with injectable providers for tests.
- `src/cli/` provides a command-line interface wrapping the core APIs.
- `src/integration/` wires core APIs to external systems (UI/BIP32) and error messaging.
- `src/types/` defines shared DTOs such as `ValidationResult`.
- `src/index.ts` re-exports the public surface for these foundational modules.
8 changes: 8 additions & 0 deletions src/cli/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# cli design

- Purpose: Provide a human-friendly CLI wrapper for BIP39 core APIs.
- `runCli` handles argument parsing, input resolution (args/stdin), and exit codes.
- `commands/` contains small adapters that invoke core use cases.
- `hex.ts` handles hex encoding/decoding for byte outputs.
- The CLI defaults to normalized input, with `--strict` to disable normalization.
- Added `generate-mnemonic-with-wordlist` to emit a generated mnemonic plus the full English wordlist.
139 changes: 139 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
export type CliCommand =
| "validate"
| "entropy-to-mnemonic"
| "mnemonic-to-entropy"
| "mnemonic-to-seed"
| "generate-entropy"
| "generate-mnemonic"
| "generate-mnemonic-with-wordlist";

export type CliFlags = {
help: boolean;
strict: boolean;
passphrase: string | null;
bytes: number | null;
words: number | null;
};

export type ParsedArgs = {
command: CliCommand | null;
positionals: string[];
flags: CliFlags;
};

export type ParseResult =
| {
ok: true;
value: ParsedArgs;
}
| {
ok: false;
error: string;
};

const COMMANDS = new Set<CliCommand>([
"validate",
"entropy-to-mnemonic",
"mnemonic-to-entropy",
"mnemonic-to-seed",
"generate-entropy",
"generate-mnemonic",
"generate-mnemonic-with-wordlist",
]);

type ParseNumberResult =
| { ok: true; value: number }
| { ok: false; error: string };

const parseNumber = (value: string, label: string): ParseNumberResult => {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
return { ok: false, error: `Invalid ${label} value: ${value}` };
}
return { ok: true, value: parsed };
};

export const parseArgs = (argv: string[]): ParseResult => {
const flags: CliFlags = {
help: false,
strict: false,
passphrase: null,
bytes: null,
words: null,
};
let command: CliCommand | null = null;
const positionals: string[] = [];

for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (token.startsWith("--")) {
switch (token) {
case "--help":
flags.help = true;
break;
case "--strict":
flags.strict = true;
break;
case "--passphrase": {
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
return { ok: false, error: "Missing value for --passphrase" };
}
flags.passphrase = next;
index += 1;
break;
}
case "--bytes": {
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
return { ok: false, error: "Missing value for --bytes" };
}
const parsed = parseNumber(next, "bytes");
if (!parsed.ok) {
return parsed;
}
flags.bytes = parsed.value;
index += 1;
break;
}
case "--words": {
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
return { ok: false, error: "Missing value for --words" };
}
const parsed = parseNumber(next, "words");
if (!parsed.ok) {
return parsed;
}
flags.words = parsed.value;
index += 1;
break;
}
default:
return { ok: false, error: `Unknown option: ${token}` };
}
continue;
}
if (!command) {
if (!COMMANDS.has(token as CliCommand)) {
return { ok: false, error: `Unknown command: ${token}` };
}
command = token as CliCommand;
continue;
}
positionals.push(token);
}

if (!command && !flags.help) {
return { ok: false, error: "Missing command" };
}

return {
ok: true,
value: {
command,
positionals,
flags,
},
};
};
4 changes: 4 additions & 0 deletions src/cli/commands/entropyToMnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { entropyToMnemonic } from "../../bip39/entropyToMnemonic.js";

export const entropyToMnemonicCommand = (entropy: Uint8Array): string =>
entropyToMnemonic(entropy);
4 changes: 4 additions & 0 deletions src/cli/commands/generateEntropy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { generateEntropy } from "../../entropy/entropyGenerator.js";

export const generateEntropyCommand = (bytes: number): Uint8Array =>
generateEntropy(bytes);
13 changes: 13 additions & 0 deletions src/cli/commands/generateMnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { entropyToMnemonic } from "../../bip39/entropyToMnemonic.js";
import type { WordCount } from "../../constants/bip39.js";
import { entropyBitsForWordCount } from "../../constants/bip39.js";
import { generateEntropy } from "../../entropy/entropyGenerator.js";

export const generateMnemonicCommand = (words: number): string => {
const entropyBits = entropyBitsForWordCount(words as WordCount);
const bytes = entropyBits / 8;
if (!Number.isInteger(bytes)) {
throw new Error("Invalid word count for entropy bytes");
}
return entropyToMnemonic(generateEntropy(bytes));
};
Loading