CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-scure--base

Secure, audited & 0-dep implementation of base encoding algorithms

Pending
Overview
Eval results
Files

bech32-encodings.mddocs/

Bech32 Encodings

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.

Capabilities

Bech32 Standard

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[];
}

Bech32m

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;

Bech32 Type Definitions

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;
}

Usage Examples

Basic Encoding and Decoding

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 representation

Working with 5-bit Words

import { 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);

Bitcoin Address Example

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);

Lightning Invoice Example

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
  };
}

Safe Decoding

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");
}

String Length Limits

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
}

Advanced Features

Typed Prefix Support

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);
}

Error Handling

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"
}

When to Use Bech32 vs Bech32m

  • bech32: Use for Bitcoin segwit v0 addresses and Legacy Lightning invoices
  • bech32m: Use for Bitcoin segwit v1+ addresses (Taproot) and newer applications

Both have identical APIs but use different checksum constants internally for security reasons.

Install with Tessl CLI

npx tessl i tessl/npm-scure--base

docs

base-encodings.md

base58-encodings.md

bech32-encodings.md

index.md

utils.md

tile.json