Secure, audited & 0-dep implementation of base encoding algorithms
—
Bitcoin BIP 173/350 compliant bech32 and bech32m encodings designed for Bitcoin addresses and Lightning Network invoices. These encodings work with 5-bit words and provide strong error detection with typed prefix support.
BIP 173 bech32 implementation for Bitcoin segwit v0 addresses.
/**
* bech32 from BIP 173 for Bitcoin segwit v0 addresses
* Operates on 5-bit words with strong error detection
* For high-level Bitcoin operations, use scure-btc-signer instead
*/
const bech32: Bech32;
interface Bech32 {
encode<Prefix extends string>(
prefix: Prefix,
words: number[] | Uint8Array,
limit?: number | false
): `${Lowercase<Prefix>}1${string}`;
decode<Prefix extends string>(
str: `${Prefix}1${string}`,
limit?: number | false
): Bech32Decoded<Prefix>;
encodeFromBytes(prefix: string, bytes: Uint8Array): string;
decodeToBytes(str: string): Bech32DecodedWithArray;
decodeUnsafe(str: string, limit?: number | false): void | Bech32Decoded<string>;
fromWords(to: number[]): Uint8Array;
fromWordsUnsafe(to: number[]): void | Uint8Array;
toWords(from: Uint8Array): number[];
}BIP 350 bech32m implementation that fixes bech32 length extension weaknesses.
/**
* bech32m from BIP 350 for Bitcoin segwit v1+ addresses
* Mitigates length extension attacks found in original bech32
* Same interface as bech32 but different checksum constant
*/
const bech32m: Bech32;Type definitions for bech32 decoding results.
interface Bech32Decoded<Prefix extends string = string> {
prefix: Prefix;
words: number[];
}
interface Bech32DecodedWithArray<Prefix extends string = string> {
prefix: Prefix;
words: number[];
bytes: Uint8Array;
}import { bech32, bech32m } from "@scure/base";
// Encode from bytes (for simple data)
const data = new Uint8Array([0x12, 0x34, 0x56, 0x78]);
const encoded = bech32.encodeFromBytes("test", data);
console.log(encoded); // "test1zg69v0y73adq4p"
// Decode back to bytes
const decoded = bech32.decodeToBytes(encoded);
console.log(decoded.prefix); // "test"
console.log(decoded.bytes); // Uint8Array([0x12, 0x34, 0x56, 0x78])
console.log(decoded.words); // 5-bit word representationimport { bech32 } from "@scure/base";
// Convert bytes to 5-bit words
const bytes = new Uint8Array([0xff, 0xee, 0xdd]);
const words = bech32.toWords(bytes);
console.log(words); // Array of 5-bit values
// Encode with words directly
const encoded = bech32.encode("bc", words);
// Decode to get words back
const decoded = bech32.decode(encoded);
console.log(decoded.prefix); // "bc"
console.log(decoded.words); // Original 5-bit words
// Convert words back to bytes
const originalBytes = bech32.fromWords(decoded.words);import { bech32, bech32m } from "@scure/base";
// Example: Parsing Bitcoin bech32 address (simplified)
function parseBitcoinAddress(address: string) {
try {
const decoded = bech32.decode(address);
if (decoded.prefix !== "bc" && decoded.prefix !== "tb") {
throw new Error("Invalid Bitcoin address prefix");
}
// BIP-141: First word is version, rest is witness program
const [version, ...dataWords] = decoded.words;
const program = bech32.fromWords(dataWords);
return {
network: decoded.prefix === "bc" ? "mainnet" : "testnet",
version,
program
};
} catch (error) {
// Try bech32m for v1+ addresses
const decoded = bech32m.decode(address);
const [version, ...dataWords] = decoded.words;
const program = bech32m.fromWords(dataWords);
return {
network: decoded.prefix === "bc" ? "mainnet" : "testnet",
version,
program
};
}
}
// Example usage
const address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
const parsed = parseBitcoinAddress(address);
console.log(parsed);import { bech32 } from "@scure/base";
// Example: Basic Lightning invoice parsing (simplified)
function parseLightningInvoice(invoice: string) {
// Lightning invoices use bech32 with "ln" + network prefix
const decoded = bech32.decode(invoice.toLowerCase());
// Extract network from prefix (lnbc = mainnet, lntb = testnet, etc.)
const network = decoded.prefix.startsWith("lnbc") ? "mainnet" :
decoded.prefix.startsWith("lntb") ? "testnet" : "unknown";
// Lightning invoices encode amount in prefix after "ln"
const amountMatch = decoded.prefix.match(/^ln[a-z]+(\d+)([munp]?)$/);
let amount = null;
if (amountMatch) {
const [, value, unit] = amountMatch;
const multiplier = unit === "m" ? 0.001 : unit === "u" ? 0.000001 :
unit === "n" ? 0.000000001 : unit === "p" ? 0.000000000001 : 1;
amount = parseInt(value) * multiplier;
}
return {
network,
amount,
words: decoded.words,
// In practice, you'd parse the data field for payment details
};
}import { bech32 } from "@scure/base";
// Use decodeUnsafe for validation without throwing
const maybeDecoded = bech32.decodeUnsafe("potentially_invalid_string");
if (maybeDecoded) {
console.log("Valid bech32:", maybeDecoded);
} else {
console.log("Invalid bech32 string");
}
// Use fromWordsUnsafe for word conversion without throwing
const words = [31, 15, 20, 8]; // Some 5-bit words
const maybeBytes = bech32.fromWordsUnsafe(words);
if (maybeBytes) {
console.log("Valid conversion:", maybeBytes);
} else {
console.log("Invalid word sequence");
}import { bech32 } from "@scure/base";
// Default limit is 90 characters
const shortData = new Uint8Array(10);
const encoded = bech32.encodeFromBytes("test", shortData); // OK
// Disable length limit
const longData = new Uint8Array(100);
const encodedLong = bech32.encodeFromBytes("test", longData, false); // OK
// Custom limit
try {
const encoded = bech32.encode("test", [1, 2, 3], 10); // Very short limit
} catch (error) {
console.log(error.message); // Length exceeds limit
}import { bech32 } from "@scure/base";
// TypeScript enforces correct prefix format
type BitcoinPrefix = "bc" | "tb";
function encodeBitcoinAddress<T extends BitcoinPrefix>(
prefix: T,
program: Uint8Array
): `${T}1${string}` {
return bech32.encodeFromBytes(prefix, program);
}
const mainnetAddr = encodeBitcoinAddress("bc", new Uint8Array(20));
// Return type is `bc1${string}`
function decodeBitcoinAddress<T extends BitcoinPrefix>(
address: `${T}1${string}`
) {
return bech32.decodeToBytes(address);
}import { bech32, bech32m } from "@scure/base";
try {
// Invalid checksum
bech32.decode("bc1invalid_checksum");
} catch (error) {
console.log(error.message); // "Invalid checksum in bc1invalid_checksum: expected ..."
}
try {
// Invalid prefix
bech32.decode("invalidformat");
} catch (error) {
console.log(error.message); // 'Letter "1" must be present between prefix and data only'
}
try {
// Invalid characters
bech32.decode("bc1invalid#characters1234567");
} catch (error) {
console.log(error.message); // "Unknown letter: #"
}
try {
// Convert invalid 5-bit words to bytes
bech32.fromWords([32]); // Invalid: 5-bit values must be 0-31
} catch (error) {
console.log(error.message); // "Invalid word value"
}Both have identical APIs but use different checksum constants internally for security reasons.
Install with Tessl CLI
npx tessl i tessl/npm-scure--base