diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..13f6214
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index e69de29..18b0d89 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,161 @@
+
+
BIP39
+
TypeScript library and CLI for generating, validating, and converting BIP39 English mnemonics
+

+

+

+

+
+
+
+## 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).
diff --git a/package.json b/package.json
index 3450d11..d0e1fa2 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/DESIGN.md b/src/DESIGN.md
index 06ff8db..ae836f7 100644
--- a/src/DESIGN.md
+++ b/src/DESIGN.md
@@ -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.
diff --git a/src/cli/DESIGN.md b/src/cli/DESIGN.md
new file mode 100644
index 0000000..df8992f
--- /dev/null
+++ b/src/cli/DESIGN.md
@@ -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.
diff --git a/src/cli/args.ts b/src/cli/args.ts
new file mode 100644
index 0000000..5491212
--- /dev/null
+++ b/src/cli/args.ts
@@ -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([
+ "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,
+ },
+ };
+};
diff --git a/src/cli/commands/entropyToMnemonic.ts b/src/cli/commands/entropyToMnemonic.ts
new file mode 100644
index 0000000..99551bd
--- /dev/null
+++ b/src/cli/commands/entropyToMnemonic.ts
@@ -0,0 +1,4 @@
+import { entropyToMnemonic } from "../../bip39/entropyToMnemonic.js";
+
+export const entropyToMnemonicCommand = (entropy: Uint8Array): string =>
+ entropyToMnemonic(entropy);
diff --git a/src/cli/commands/generateEntropy.ts b/src/cli/commands/generateEntropy.ts
new file mode 100644
index 0000000..4539d74
--- /dev/null
+++ b/src/cli/commands/generateEntropy.ts
@@ -0,0 +1,4 @@
+import { generateEntropy } from "../../entropy/entropyGenerator.js";
+
+export const generateEntropyCommand = (bytes: number): Uint8Array =>
+ generateEntropy(bytes);
diff --git a/src/cli/commands/generateMnemonic.ts b/src/cli/commands/generateMnemonic.ts
new file mode 100644
index 0000000..cab4409
--- /dev/null
+++ b/src/cli/commands/generateMnemonic.ts
@@ -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));
+};
diff --git a/src/cli/commands/generateMnemonicWithWordlist.ts b/src/cli/commands/generateMnemonicWithWordlist.ts
new file mode 100644
index 0000000..a714a2d
--- /dev/null
+++ b/src/cli/commands/generateMnemonicWithWordlist.ts
@@ -0,0 +1,15 @@
+import { loadEnglishWordlist } from "../../bip39/englishWordlist.js";
+import { generateMnemonicCommand } from "./generateMnemonic.js";
+
+export type MnemonicWithWordlist = {
+ mnemonic: string;
+ wordlist: string[];
+};
+
+export const generateMnemonicWithWordlistCommand = (
+ words: number,
+): MnemonicWithWordlist => {
+ const mnemonic = generateMnemonicCommand(words);
+ const { words: wordlist } = loadEnglishWordlist();
+ return { mnemonic, wordlist };
+};
diff --git a/src/cli/commands/mnemonicToEntropy.ts b/src/cli/commands/mnemonicToEntropy.ts
new file mode 100644
index 0000000..d0449fb
--- /dev/null
+++ b/src/cli/commands/mnemonicToEntropy.ts
@@ -0,0 +1,10 @@
+import { mnemonicToEntropy } from "../../bip39/mnemonicToEntropy.js";
+import { normalizeMnemonicInput } from "../../normalize/normalizeMnemonicInput.js";
+
+export const mnemonicToEntropyCommand = (
+ input: string,
+ strict: boolean,
+): Uint8Array => {
+ const normalized = strict ? input : normalizeMnemonicInput(input);
+ return mnemonicToEntropy(normalized);
+};
diff --git a/src/cli/commands/mnemonicToSeed.ts b/src/cli/commands/mnemonicToSeed.ts
new file mode 100644
index 0000000..9fe4a15
--- /dev/null
+++ b/src/cli/commands/mnemonicToSeed.ts
@@ -0,0 +1,11 @@
+import { mnemonicToSeed } from "../../bip39/mnemonicToSeed.js";
+import { normalizeMnemonicInput } from "../../normalize/normalizeMnemonicInput.js";
+
+export const mnemonicToSeedCommand = (
+ input: string,
+ strict: boolean,
+ passphrase: string,
+): Uint8Array => {
+ const normalized = strict ? input : normalizeMnemonicInput(input);
+ return mnemonicToSeed(normalized, passphrase);
+};
diff --git a/src/cli/commands/validate.ts b/src/cli/commands/validate.ts
new file mode 100644
index 0000000..c710f88
--- /dev/null
+++ b/src/cli/commands/validate.ts
@@ -0,0 +1,31 @@
+import { validateMnemonic } from "../../bip39/validateMnemonic.js";
+import { ErrorCode } from "../../errors/errorCodes.js";
+import { normalizeMnemonicInput } from "../../normalize/normalizeMnemonicInput.js";
+
+export type ValidateCommandResult =
+ | {
+ ok: true;
+ normalized: string;
+ }
+ | {
+ ok: false;
+ errorCode: ErrorCode;
+ };
+
+export const validateCommand = (
+ input: string,
+ strict: boolean,
+): ValidateCommandResult => {
+ const normalized = strict ? input : normalizeMnemonicInput(input);
+ const result = validateMnemonic(normalized);
+ if (!result.ok) {
+ return {
+ ok: false,
+ errorCode: result.error_code ?? ErrorCode.ERR_INVALID_MNEMONIC_FORMAT,
+ };
+ }
+ return {
+ ok: true,
+ normalized: result.normalized_mnemonic ?? normalized,
+ };
+};
diff --git a/src/cli/hex.ts b/src/cli/hex.ts
new file mode 100644
index 0000000..b58e5ad
--- /dev/null
+++ b/src/cli/hex.ts
@@ -0,0 +1,22 @@
+const isHex = (value: string): boolean => /^[0-9a-fA-F]+$/u.test(value);
+
+export const hexToBytes = (hex: string): Uint8Array => {
+ const trimmed = hex.trim();
+ if (trimmed.length === 0) {
+ throw new Error("Hex input is empty");
+ }
+ if (trimmed.length % 2 !== 0) {
+ throw new Error("Hex input must have even length");
+ }
+ if (!isHex(trimmed)) {
+ throw new Error("Hex input contains non-hex characters");
+ }
+ const bytes = new Uint8Array(trimmed.length / 2);
+ for (let index = 0; index < trimmed.length; index += 2) {
+ bytes[index / 2] = Number.parseInt(trimmed.slice(index, index + 2), 16);
+ }
+ return bytes;
+};
+
+export const bytesToHex = (bytes: Uint8Array): string =>
+ Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
diff --git a/src/cli/index.ts b/src/cli/index.ts
new file mode 100644
index 0000000..b2ca743
--- /dev/null
+++ b/src/cli/index.ts
@@ -0,0 +1,25 @@
+#!/usr/bin/env node
+import { runCli } from "./runCli.js";
+
+const readStdin = async (): Promise => {
+ if (process.stdin.isTTY) {
+ return null;
+ }
+ const chunks: Buffer[] = [];
+ for await (const chunk of process.stdin) {
+ chunks.push(Buffer.from(chunk));
+ }
+ return Buffer.concat(chunks).toString("utf8");
+};
+
+const exitCode = await runCli(process.argv.slice(2), {
+ readStdin,
+ writeStdout: (text) => {
+ process.stdout.write(text);
+ },
+ writeStderr: (text) => {
+ process.stderr.write(text);
+ },
+});
+
+process.exitCode = exitCode;
diff --git a/src/cli/runCli.ts b/src/cli/runCli.ts
new file mode 100644
index 0000000..21b0b90
--- /dev/null
+++ b/src/cli/runCli.ts
@@ -0,0 +1,210 @@
+import { ENTROPY_BYTES, WORD_COUNTS } from "../constants/bip39.js";
+import type { ErrorCode } from "../errors/errorCodes.js";
+import { ERROR_MESSAGES } from "../integration/errorMessages.js";
+import { parseArgs } from "./args.js";
+import { entropyToMnemonicCommand } from "./commands/entropyToMnemonic.js";
+import { generateEntropyCommand } from "./commands/generateEntropy.js";
+import { generateMnemonicCommand } from "./commands/generateMnemonic.js";
+import { generateMnemonicWithWordlistCommand } from "./commands/generateMnemonicWithWordlist.js";
+import { mnemonicToEntropyCommand } from "./commands/mnemonicToEntropy.js";
+import { mnemonicToSeedCommand } from "./commands/mnemonicToSeed.js";
+import { validateCommand } from "./commands/validate.js";
+import { bytesToHex, hexToBytes } from "./hex.js";
+
+export type CliIO = {
+ readStdin: () => Promise;
+ writeStdout: (text: string) => void;
+ writeStderr: (text: string) => void;
+};
+
+type UsageError = {
+ message: string;
+};
+
+const usage = `Usage: bip39 [options] [input]
+
+Commands:
+ validate [MNEMONIC] [--strict]
+ entropy-to-mnemonic [HEX]
+ mnemonic-to-entropy [MNEMONIC] [--strict]
+ mnemonic-to-seed [MNEMONIC] [--strict] [--passphrase ]
+ generate-entropy [--bytes <16|20|24|28|32>]
+ generate-mnemonic [--words <12|15|18|21|24>]
+ generate-mnemonic-with-wordlist [--words <12|15|18|21|24>]
+
+Options:
+ --help Show help
+ --strict Disable input normalization
+ --passphrase Passphrase for mnemonic-to-seed
+ --bytes Entropy bytes for generate-entropy
+ --words Word count for generate-mnemonic
+`;
+
+const isErrorWithCode = (value: unknown): value is { code: ErrorCode } =>
+ !!value && typeof value === "object" && "code" in value;
+
+const formatError = (code: ErrorCode): string => {
+ const message = ERROR_MESSAGES[code] ?? "Unknown error";
+ return `error_code: ${code}\nmessage: ${message}\n`;
+};
+
+const isValidEntropyBytes = (bytes: number): boolean =>
+ (ENTROPY_BYTES as readonly number[]).includes(bytes);
+
+const isValidWordCount = (count: number): boolean =>
+ (WORD_COUNTS as readonly number[]).includes(count);
+
+const getInputFromArgsOrStdin = async (
+ positionals: string[],
+ io: CliIO,
+ joiner: string,
+): Promise => {
+ if (positionals.length > 0) {
+ return positionals.join(joiner);
+ }
+ return io.readStdin();
+};
+
+const getSingleInputOrStdin = async (
+ positionals: string[],
+ io: CliIO,
+): Promise => {
+ if (positionals.length > 1) {
+ return { message: "Too many arguments" };
+ }
+ const input =
+ positionals.length === 1 ? positionals[0] : await io.readStdin();
+ if (input === null) {
+ return { message: "Missing input" };
+ }
+ return input;
+};
+
+export const runCli = async (argv: string[], io: CliIO): Promise => {
+ const parsed = parseArgs(argv);
+ if (!parsed.ok) {
+ io.writeStderr(`${parsed.error}\n`);
+ io.writeStderr(usage);
+ return 2;
+ }
+
+ const { command, flags, positionals } = parsed.value;
+ if (flags.help) {
+ io.writeStdout(usage);
+ return 0;
+ }
+ if (!command) {
+ io.writeStderr("Missing command\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+
+ try {
+ switch (command) {
+ case "validate": {
+ const input = await getInputFromArgsOrStdin(positionals, io, " ");
+ if (input === null) {
+ io.writeStderr("Missing input\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ const result = validateCommand(input, flags.strict);
+ if (!result.ok) {
+ io.writeStderr(formatError(result.errorCode));
+ return 1;
+ }
+ io.writeStdout(`valid\nnormalized: ${result.normalized}\n`);
+ return 0;
+ }
+ case "entropy-to-mnemonic": {
+ const input = await getSingleInputOrStdin(positionals, io);
+ if (typeof input !== "string") {
+ io.writeStderr(`${input.message}\n`);
+ io.writeStderr(usage);
+ return 2;
+ }
+ let bytes: Uint8Array;
+ try {
+ bytes = hexToBytes(input);
+ } catch (error) {
+ io.writeStderr(`Invalid hex: ${(error as Error).message}\n`);
+ return 2;
+ }
+ const mnemonic = entropyToMnemonicCommand(bytes);
+ io.writeStdout(`${mnemonic}\n`);
+ return 0;
+ }
+ case "mnemonic-to-entropy": {
+ const input = await getInputFromArgsOrStdin(positionals, io, " ");
+ if (input === null) {
+ io.writeStderr("Missing input\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ const entropy = mnemonicToEntropyCommand(input, flags.strict);
+ io.writeStdout(`${bytesToHex(entropy)}\n`);
+ return 0;
+ }
+ case "mnemonic-to-seed": {
+ const input = await getInputFromArgsOrStdin(positionals, io, " ");
+ if (input === null) {
+ io.writeStderr("Missing input\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ const passphrase = flags.passphrase ?? "";
+ const seed = mnemonicToSeedCommand(input, flags.strict, passphrase);
+ io.writeStdout(`${bytesToHex(seed)}\n`);
+ return 0;
+ }
+ case "generate-entropy": {
+ const bytes = flags.bytes ?? 16;
+ if (!isValidEntropyBytes(bytes)) {
+ io.writeStderr("Invalid --bytes value\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ const entropy = generateEntropyCommand(bytes);
+ io.writeStdout(`${bytesToHex(entropy)}\n`);
+ return 0;
+ }
+ case "generate-mnemonic": {
+ const words = flags.words ?? 12;
+ if (!isValidWordCount(words)) {
+ io.writeStderr("Invalid --words value\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ const mnemonic = generateMnemonicCommand(words);
+ io.writeStdout(`${mnemonic}\n`);
+ return 0;
+ }
+ case "generate-mnemonic-with-wordlist": {
+ const words = flags.words ?? 12;
+ if (!isValidWordCount(words)) {
+ io.writeStderr("Invalid --words value\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ const result = generateMnemonicWithWordlistCommand(words);
+ io.writeStdout(`mnemonic: ${result.mnemonic}\n`);
+ io.writeStdout("wordlist:\n");
+ for (const word of result.wordlist) {
+ io.writeStdout(`${word}\n`);
+ }
+ return 0;
+ }
+ default:
+ io.writeStderr("Unknown command\n");
+ io.writeStderr(usage);
+ return 2;
+ }
+ } catch (error) {
+ if (isErrorWithCode(error)) {
+ io.writeStderr(formatError(error.code));
+ return 1;
+ }
+ io.writeStderr(`Unexpected error: ${(error as Error).message}\n`);
+ return 3;
+ }
+};
diff --git a/src/index.ts b/src/index.ts
index 95b7030..f2a59f5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,6 +7,8 @@ export * from "./constants/bip39.js";
export * from "./crypto/crypto.js";
export * from "./entropy/entropyGenerator.js";
export * from "./errors/errorCodes.js";
+export * from "./integration/errorMessages.js";
+export * from "./integration/externalIntegration.js";
export * from "./normalize/normalizeMnemonicInput.js";
export * from "./parser/strictMnemonic.js";
export * from "./types/validationResult.js";
diff --git a/src/integration/DESIGN.md b/src/integration/DESIGN.md
new file mode 100644
index 0000000..f06d337
--- /dev/null
+++ b/src/integration/DESIGN.md
@@ -0,0 +1,5 @@
+# integration design
+
+- Purpose: Connect core BIP39 APIs to external layers like UI or BIP32.
+- Scope: Normalize UI input, orchestrate validation/derivation, and map error codes to messages.
+- Output: JavaScript is emitted to `dist/`; keep this directory TypeScript-only.
diff --git a/src/integration/errorMessages.ts b/src/integration/errorMessages.ts
new file mode 100644
index 0000000..8118d16
--- /dev/null
+++ b/src/integration/errorMessages.ts
@@ -0,0 +1,12 @@
+import { ErrorCode } from "../errors/errorCodes.js";
+
+export const ERROR_MESSAGES: Record = {
+ [ErrorCode.ERR_ENTROPY_LENGTH]:
+ "Entropy length must be 16/20/24/28/32 bytes.",
+ [ErrorCode.ERR_INVALID_MNEMONIC_FORMAT]: "Mnemonic format is invalid.",
+ [ErrorCode.ERR_INVALID_WORD_COUNT]:
+ "Mnemonic word count must be 12/15/18/21/24.",
+ [ErrorCode.ERR_WORD_NOT_IN_LIST]: "Mnemonic contains an unknown word.",
+ [ErrorCode.ERR_CHECKSUM_MISMATCH]: "Mnemonic checksum does not match.",
+ [ErrorCode.ERR_PBKDF2_FAILURE]: "Failed to derive seed (PBKDF2 error).",
+};
diff --git a/src/integration/externalIntegration.ts b/src/integration/externalIntegration.ts
new file mode 100644
index 0000000..1f3ab18
--- /dev/null
+++ b/src/integration/externalIntegration.ts
@@ -0,0 +1,96 @@
+import { mnemonicToSeed } from "../bip39/mnemonicToSeed.js";
+import { validateMnemonic } from "../bip39/validateMnemonic.js";
+import { ErrorCode } from "../errors/errorCodes.js";
+import { normalizeMnemonicInput } from "../normalize/normalizeMnemonicInput.js";
+import type { ValidationResult } from "../types/validationResult.js";
+import { ERROR_MESSAGES } from "./errorMessages.js";
+
+export type SeedDerivationResult =
+ | {
+ ok: true;
+ seed: Uint8Array;
+ normalized_mnemonic: string;
+ }
+ | {
+ ok: false;
+ error_code: ErrorCode;
+ normalized_mnemonic: string | null;
+ };
+
+export type Bip32Adapter = {
+ fromSeed: (seed: Uint8Array) => T;
+};
+
+export type Bip32DerivationResult =
+ | {
+ ok: true;
+ seed: Uint8Array;
+ root: T;
+ normalized_mnemonic: string;
+ }
+ | {
+ ok: false;
+ error_code: ErrorCode;
+ normalized_mnemonic: string | null;
+ };
+
+const isErrorWithCode = (value: unknown): value is { code: ErrorCode } =>
+ !!value && typeof value === "object" && "code" in value;
+
+export const validateMnemonicForUi = (input: string): ValidationResult => {
+ const normalized = normalizeMnemonicInput(input);
+ return validateMnemonic(normalized);
+};
+
+export const deriveSeedForUi = (
+ input: string,
+ passphrase = "",
+): SeedDerivationResult => {
+ const normalized = normalizeMnemonicInput(input);
+ const validation = validateMnemonic(normalized);
+ if (!validation.ok) {
+ return {
+ ok: false,
+ error_code:
+ validation.error_code ?? ErrorCode.ERR_INVALID_MNEMONIC_FORMAT,
+ normalized_mnemonic: validation.normalized_mnemonic,
+ };
+ }
+ try {
+ const seed = mnemonicToSeed(normalized, passphrase);
+ return {
+ ok: true,
+ seed,
+ normalized_mnemonic: normalized,
+ };
+ } catch (error) {
+ if (isErrorWithCode(error)) {
+ return {
+ ok: false,
+ error_code: error.code,
+ normalized_mnemonic: normalized,
+ };
+ }
+ throw error;
+ }
+};
+
+export const deriveBip32RootFromMnemonic = (
+ input: string,
+ passphrase: string,
+ adapter: Bip32Adapter,
+): Bip32DerivationResult => {
+ const seedResult = deriveSeedForUi(input, passphrase);
+ if (!seedResult.ok) {
+ return seedResult;
+ }
+ return {
+ ok: true,
+ seed: seedResult.seed,
+ root: adapter.fromSeed(seedResult.seed),
+ normalized_mnemonic: seedResult.normalized_mnemonic,
+ };
+};
+
+export const errorCodeToMessage = (code: ErrorCode): string =>
+ ERROR_MESSAGES[code];
diff --git a/test/acceptance.spec.ts b/test/acceptance.spec.ts
index 6d82d37..d68f305 100644
--- a/test/acceptance.spec.ts
+++ b/test/acceptance.spec.ts
@@ -65,7 +65,7 @@ test("roundtrip covers all allowed entropy lengths", () => {
test("failure cases from appendix C are enforced", () => {
assert.throws(
() => entropyToMnemonic(new Uint8Array(15)),
- (error) =>
+ (error: unknown) =>
error instanceof EntropyLengthError &&
error.code === ErrorCode.ERR_ENTROPY_LENGTH,
);
@@ -75,7 +75,7 @@ test("failure cases from appendix C are enforced", () => {
mnemonicToEntropy(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
),
- (error) =>
+ (error: unknown) =>
error instanceof InvalidWordCountError &&
error.code === ErrorCode.ERR_INVALID_WORD_COUNT,
);
@@ -85,7 +85,7 @@ test("failure cases from appendix C are enforced", () => {
mnemonicToEntropy(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
),
- (error) =>
+ (error: unknown) =>
error instanceof ChecksumMismatchError &&
error.code === ErrorCode.ERR_CHECKSUM_MISMATCH,
);
@@ -95,7 +95,7 @@ test("failure cases from appendix C are enforced", () => {
mnemonicToEntropy(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon typo",
),
- (error) =>
+ (error: unknown) =>
error instanceof WordNotInListError &&
error.code === ErrorCode.ERR_WORD_NOT_IN_LIST,
);
@@ -105,14 +105,14 @@ test("failure cases from appendix C are enforced", () => {
mnemonicToEntropy(
"abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
),
- (error) =>
+ (error: unknown) =>
error instanceof InvalidMnemonicFormatError &&
error.code === ErrorCode.ERR_INVALID_MNEMONIC_FORMAT,
);
assert.throws(
() => mnemonicToSeed(["abandon", "", "abandon"]),
- (error) =>
+ (error: unknown) =>
error instanceof InvalidMnemonicSeedFormatError &&
error.code === ErrorCode.ERR_INVALID_MNEMONIC_FORMAT,
);
diff --git a/test/cli/args.spec.ts b/test/cli/args.spec.ts
new file mode 100644
index 0000000..f531dc7
--- /dev/null
+++ b/test/cli/args.spec.ts
@@ -0,0 +1,73 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { parseArgs } from "../../src/cli/args.ts";
+
+test("parseArgs parses command, positionals, and strict flag", () => {
+ const result = parseArgs(["validate", "abandon", "about", "--strict"]);
+ assert.equal(result.ok, true);
+ if (!result.ok) return;
+ assert.equal(result.value.command, "validate");
+ assert.deepEqual(result.value.positionals, ["abandon", "about"]);
+ assert.equal(result.value.flags.strict, true);
+});
+
+test("parseArgs parses passphrase", () => {
+ const result = parseArgs([
+ "mnemonic-to-seed",
+ "abandon",
+ "--passphrase",
+ "TREZOR",
+ ]);
+ assert.equal(result.ok, true);
+ if (!result.ok) return;
+ assert.equal(result.value.flags.passphrase, "TREZOR");
+});
+
+test("parseArgs rejects unknown options", () => {
+ const result = parseArgs(["validate", "--nope"]);
+ assert.equal(result.ok, false);
+});
+
+test("parseArgs rejects missing option values", () => {
+ const result = parseArgs(["mnemonic-to-seed", "--passphrase"]);
+ assert.equal(result.ok, false);
+});
+
+test("parseArgs handles --help without command", () => {
+ const result = parseArgs(["--help"]);
+ assert.equal(result.ok, true);
+ if (!result.ok) return;
+ assert.equal(result.value.flags.help, true);
+ assert.equal(result.value.command, null);
+});
+
+test("parseArgs rejects missing command", () => {
+ const result = parseArgs([]);
+ assert.equal(result.ok, false);
+});
+
+test("parseArgs parses numeric options", () => {
+ const result = parseArgs([
+ "generate-entropy",
+ "--bytes",
+ "24",
+ "--words",
+ "12",
+ ]);
+ assert.equal(result.ok, true);
+ if (!result.ok) return;
+ assert.equal(result.value.flags.bytes, 24);
+ assert.equal(result.value.flags.words, 12);
+});
+
+test("parseArgs accepts generate-mnemonic-with-wordlist command", () => {
+ const result = parseArgs([
+ "generate-mnemonic-with-wordlist",
+ "--words",
+ "12",
+ ]);
+ assert.equal(result.ok, true);
+ if (!result.ok) return;
+ assert.equal(result.value.command, "generate-mnemonic-with-wordlist");
+});
diff --git a/test/cli/commands.spec.ts b/test/cli/commands.spec.ts
new file mode 100644
index 0000000..5718926
--- /dev/null
+++ b/test/cli/commands.spec.ts
@@ -0,0 +1,65 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { entropyToMnemonicCommand } from "../../src/cli/commands/entropyToMnemonic.ts";
+import { generateEntropyCommand } from "../../src/cli/commands/generateEntropy.ts";
+import { generateMnemonicCommand } from "../../src/cli/commands/generateMnemonic.ts";
+import { generateMnemonicWithWordlistCommand } from "../../src/cli/commands/generateMnemonicWithWordlist.ts";
+import { mnemonicToEntropyCommand } from "../../src/cli/commands/mnemonicToEntropy.ts";
+import { mnemonicToSeedCommand } from "../../src/cli/commands/mnemonicToSeed.ts";
+import { validateCommand } from "../../src/cli/commands/validate.ts";
+
+const ENTROPY_HEX = "00000000000000000000000000000000";
+const MNEMONIC =
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+const SEED_HEX =
+ "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553" +
+ "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04";
+
+const hexToBytes = (hex: string): Uint8Array =>
+ Uint8Array.from(hex.match(/.{2}/gu) ?? [], (pair) =>
+ Number.parseInt(pair, 16),
+ );
+
+const bytesToHex = (bytes: Uint8Array): string =>
+ Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
+
+test("entropyToMnemonicCommand matches vector", () => {
+ const mnemonic = entropyToMnemonicCommand(hexToBytes(ENTROPY_HEX));
+ assert.equal(mnemonic, MNEMONIC);
+});
+
+test("mnemonicToEntropyCommand matches vector", () => {
+ const entropy = mnemonicToEntropyCommand(MNEMONIC, false);
+ assert.equal(bytesToHex(entropy), ENTROPY_HEX);
+});
+
+test("mnemonicToSeedCommand matches vector", () => {
+ const seed = mnemonicToSeedCommand(MNEMONIC, false, "TREZOR");
+ assert.equal(bytesToHex(seed), SEED_HEX);
+});
+
+test("validateCommand returns normalized mnemonic", () => {
+ const result = validateCommand(MNEMONIC, true);
+ assert.equal(result.ok, true);
+ if (!result.ok) return;
+ assert.equal(result.normalized, MNEMONIC);
+});
+
+test("generateEntropyCommand returns requested length", () => {
+ const entropy = generateEntropyCommand(16);
+ assert.equal(entropy.length, 16);
+});
+
+test("generateMnemonicCommand returns requested word count", () => {
+ const mnemonic = generateMnemonicCommand(12);
+ assert.equal(mnemonic.split(" ").length, 12);
+});
+
+test("generateMnemonicWithWordlistCommand returns mnemonic and wordlist", () => {
+ const result = generateMnemonicWithWordlistCommand(12);
+ assert.equal(result.mnemonic.split(" ").length, 12);
+ assert.equal(result.wordlist.length, 2048);
+ assert.equal(result.wordlist[0], "abandon");
+ assert.equal(result.wordlist[result.wordlist.length - 1], "zoo");
+});
diff --git a/test/cli/hex.spec.ts b/test/cli/hex.spec.ts
new file mode 100644
index 0000000..8b65a10
--- /dev/null
+++ b/test/cli/hex.spec.ts
@@ -0,0 +1,26 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { bytesToHex, hexToBytes } from "../../src/cli/hex.ts";
+
+test("hexToBytes decodes valid hex", () => {
+ const bytes = hexToBytes("0001ff");
+ assert.deepEqual(Array.from(bytes), [0, 1, 255]);
+});
+
+test("hexToBytes rejects odd-length hex", () => {
+ assert.throws(() => hexToBytes("0"));
+});
+
+test("hexToBytes rejects non-hex characters", () => {
+ assert.throws(() => hexToBytes("zz"));
+});
+
+test("hexToBytes rejects empty input", () => {
+ assert.throws(() => hexToBytes(""));
+});
+
+test("bytesToHex encodes bytes as lowercase hex", () => {
+ const hex = bytesToHex(Uint8Array.from([0, 15, 16, 255]));
+ assert.equal(hex, "000f10ff");
+});
diff --git a/test/cli/integration.spec.ts b/test/cli/integration.spec.ts
new file mode 100644
index 0000000..c14c208
--- /dev/null
+++ b/test/cli/integration.spec.ts
@@ -0,0 +1,17 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import test from "node:test";
+
+const MNEMONIC =
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+
+test("cli validate runs via node with tsx", () => {
+ const result = spawnSync(
+ process.execPath,
+ ["--import", "tsx", "src/cli/index.ts", "validate", MNEMONIC],
+ { encoding: "utf8" },
+ );
+
+ assert.equal(result.status, 0);
+ assert.match(result.stdout, /^valid\nnormalized: /u);
+});
diff --git a/test/cli/run.spec.ts b/test/cli/run.spec.ts
new file mode 100644
index 0000000..5785bfe
--- /dev/null
+++ b/test/cli/run.spec.ts
@@ -0,0 +1,109 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import type { CliIO } from "../../src/cli/runCli.ts";
+import { runCli } from "../../src/cli/runCli.ts";
+
+const ENTROPY_HEX = "00000000000000000000000000000000";
+const MNEMONIC =
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+const SEED_HEX =
+ "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553" +
+ "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04";
+
+const createIo = (stdin: string | null = null) => {
+ const stdout: string[] = [];
+ const stderr: string[] = [];
+ const io: CliIO = {
+ readStdin: async () => stdin,
+ writeStdout: (text) => {
+ stdout.push(text);
+ },
+ writeStderr: (text) => {
+ stderr.push(text);
+ },
+ };
+ return { io, stdout, stderr };
+};
+
+test("runCli validate succeeds with args", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(["validate", MNEMONIC], io);
+ assert.equal(exitCode, 0);
+ const output = stdout.join("");
+ assert.match(output, /^valid\nnormalized: /u);
+});
+
+test("runCli validate reads from stdin", async () => {
+ const { io, stdout } = createIo(MNEMONIC);
+ const exitCode = await runCli(["validate"], io);
+ assert.equal(exitCode, 0);
+ assert.match(stdout.join(""), /^valid\nnormalized: /u);
+});
+
+test("runCli entropy-to-mnemonic outputs mnemonic", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(["entropy-to-mnemonic", ENTROPY_HEX], io);
+ assert.equal(exitCode, 0);
+ assert.equal(stdout.join("").trim(), MNEMONIC);
+});
+
+test("runCli entropy-to-mnemonic rejects invalid hex", async () => {
+ const { io, stderr } = createIo();
+ const exitCode = await runCli(["entropy-to-mnemonic", "0"], io);
+ assert.equal(exitCode, 2);
+ assert.match(stderr.join(""), /Invalid hex/u);
+});
+
+test("runCli mnemonic-to-entropy outputs hex", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(["mnemonic-to-entropy", MNEMONIC], io);
+ assert.equal(exitCode, 0);
+ assert.equal(stdout.join("").trim(), ENTROPY_HEX);
+});
+
+test("runCli mnemonic-to-seed outputs seed hex", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(
+ ["mnemonic-to-seed", MNEMONIC, "--passphrase", "TREZOR"],
+ io,
+ );
+ assert.equal(exitCode, 0);
+ assert.equal(stdout.join("").trim(), SEED_HEX);
+});
+
+test("runCli generate-entropy outputs hex of default length", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(["generate-entropy"], io);
+ assert.equal(exitCode, 0);
+ const hex = stdout.join("").trim();
+ assert.equal(hex.length, 32);
+ assert.match(hex, /^[0-9a-f]+$/u);
+});
+
+test("runCli generate-mnemonic outputs requested word count", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(["generate-mnemonic", "--words", "12"], io);
+ assert.equal(exitCode, 0);
+ const words = stdout.join("").trim().split(" ");
+ assert.equal(words.length, 12);
+});
+
+test("runCli generate-mnemonic-with-wordlist outputs mnemonic and wordlist", async () => {
+ const { io, stdout } = createIo();
+ const exitCode = await runCli(
+ ["generate-mnemonic-with-wordlist", "--words", "12"],
+ io,
+ );
+ assert.equal(exitCode, 0);
+ const lines = stdout.join("").trimEnd().split("\n");
+ assert.ok(lines.length >= 2050);
+ assert.match(lines[0], /^mnemonic: /u);
+ const mnemonic = lines[0].replace(/^mnemonic: /u, "");
+ assert.equal(mnemonic.split(" ").length, 12);
+ assert.equal(lines[1], "wordlist:");
+ const wordlist = lines.slice(2);
+ assert.equal(wordlist.length, 2048);
+ assert.equal(wordlist[0], "abandon");
+ assert.equal(wordlist[wordlist.length - 1], "zoo");
+});
diff --git a/test/integration.spec.ts b/test/integration.spec.ts
new file mode 100644
index 0000000..aa20302
--- /dev/null
+++ b/test/integration.spec.ts
@@ -0,0 +1,58 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { mnemonicToSeed } from "../src/bip39/mnemonicToSeed.ts";
+import { ErrorCode } from "../src/errors/errorCodes.ts";
+import {
+ deriveBip32RootFromMnemonic,
+ deriveSeedForUi,
+ errorCodeToMessage,
+ validateMnemonicForUi,
+} from "../src/integration/externalIntegration.ts";
+
+const messyMnemonic =
+ " ABANDON abandon\tabandon\nABANDON abandon abandon abandon abandon abandon abandon abandon about ";
+const normalizedMnemonic =
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+
+test("validateMnemonicForUi normalizes before validation", () => {
+ const result = validateMnemonicForUi(messyMnemonic);
+ assert.equal(result.ok, true);
+ assert.equal(result.normalized_mnemonic, normalizedMnemonic);
+});
+
+test("deriveSeedForUi returns seed for valid input", () => {
+ const expected = mnemonicToSeed(normalizedMnemonic, "TREZOR");
+ const result = deriveSeedForUi(messyMnemonic, "TREZOR");
+ assert.equal(result.ok, true);
+ if (result.ok) {
+ assert.deepEqual(Array.from(result.seed), Array.from(expected));
+ assert.equal(result.normalized_mnemonic, normalizedMnemonic);
+ }
+});
+
+test("deriveSeedForUi returns error for invalid input", () => {
+ const result = deriveSeedForUi("abandon abandon", "");
+ assert.equal(result.ok, false);
+ if (!result.ok) {
+ assert.equal(result.error_code, ErrorCode.ERR_INVALID_WORD_COUNT);
+ }
+});
+
+test("deriveBip32RootFromMnemonic uses adapter", () => {
+ const result = deriveBip32RootFromMnemonic(messyMnemonic, "", {
+ fromSeed: (seed) => ({ length: seed.length, first: seed[0] }),
+ });
+ assert.equal(result.ok, true);
+ if (result.ok) {
+ assert.equal(result.root.length, 64);
+ }
+});
+
+test("errorCodeToMessage covers all error codes", () => {
+ for (const code of Object.values(ErrorCode)) {
+ const message = errorCodeToMessage(code);
+ assert.equal(typeof message, "string");
+ assert.notEqual(message.length, 0);
+ }
+});
diff --git a/tsconfig.build.json b/tsconfig.build.json
new file mode 100644
index 0000000..8d5de7e
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "noEmit": false,
+ "allowImportingTsExtensions": false,
+ "rewriteRelativeImportExtensions": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["test/**/*.ts"]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 121c84f..53e7973 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,13 +3,13 @@
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
- "rootDir": "src",
- "outDir": "dist",
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
"types": ["node"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
- "include": ["src/**/*.ts"]
+ "include": ["src/**/*.ts", "test/**/*.ts"]
}
diff --git a/tsconfig.test.json b/tsconfig.test.json
index 15fa963..811685a 100644
--- a/tsconfig.test.json
+++ b/tsconfig.test.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"noEmit": true,
"types": ["node"],
- "allowImportingTsExtensions": true
+ "allowImportingTsExtensions": true,
+ "rootDir": "."
},
- "include": ["test/**/*.ts"]
+ "include": ["test/**/*.ts", "src/**/*.ts"]
}