Scanner Module

July 2, 2025 ยท View on GitHub

The Scanner module is the core component for decoding and parsing various types of Bitcoin and Lightning Network invoices and addresses.

Features

  • Bitcoin Address Support
    • Decodes multiple address formats (P2PKH, P2SH, P2WPKH, P2WSH, P2TR)
    • Processes BIP21 Bitcoin payment URIs
    • Network support for Mainnet, Testnet, Regtest, and Signet
  • Lightning Network Features
    • Decodes BOLT-11 Lightning invoices
    • Supports Lightning Addresses
    • Handles multiple LNURL types:
      • LNURL-pay
      • LNURL-withdraw
      • LNURL-auth
      • LNURL-channel
    • Node connection string parsing
  • Pubky authentication string handling
  • Gift code decoding (format: bitkit://gift--)

Usage Examples

iOS (Swift)

import BitkitCore

func decodeInvoice() async {
    do {
        let result = try await Scanner.decode("lnbc500n1ps...")
        switch result {
        case .lightning(let invoice):
            print("Lightning Invoice:")
            print("Amount: \(invoice.amountSatoshis) sats")
            print("Payment Hash: \(invoice.paymentHash.hex)")
            print("Timestamp: \(invoice.timestampSeconds)")
            print("Expiry: \(invoice.expirySeconds) seconds")
            print("Is Expired: \(invoice.isExpired)")
            if let desc = invoice.description {
                print("Description: \(desc)")
            }
            print("Network: \(invoice.networkType)")
            if let nodeId = invoice.payeeNodeId {
                print("Node ID: \(nodeId.hex)")
            }
            
        case .onChain(let invoice):
            print("On-chain Invoice:")
            print("Address: \(invoice.address)")
            print("Amount: \(invoice.amountSatoshis) sats")
            if let label = invoice.label {
                print("Label: \(label)")
            }
            if let message = invoice.message {
                print("Message: \(message)")
            }
            if let params = invoice.params {
                print("Parameters:")
                params.forEach { key, value in
                    print("\t\(key): \(value)")
                }
            }
            
        case .lnurlPay(let data):
            print("LNURL-pay:")
            print("URI: \(data.uri)")
            print("Callback: \(data.callback)")
            print("Min Sendable: \(data.minSendable) sats")
            print("Max Sendable: \(data.maxSendable) sats")
            print("Metadata: \(data.metadataStr)")
            if let commentLength = data.commentAllowed {
                print("Comment allowed (max length): \(commentLength)")
            }
            print("Allows Nostr: \(data.allowsNostr)")
            if let pubkey = data.nostrPubkey {
                print("Nostr Pubkey: \(pubkey.hex)")
            }
            
        case .lnurlWithdraw(let data):
            print("LNURL-withdraw:")
            print("URI: \(data.uri)")
            print("Callback: \(data.callback)")
            print("K1: \(data.k1)")
            print("Description: \(data.defaultDescription)")
            print("Max Withdrawable: \(data.maxWithdrawable) sats")
            if let min = data.minWithdrawable {
                print("Min Withdrawable: \(min) sats")
            }
            print("Tag: \(data.tag)")
            
        case .lnurlAuth(let data):
            print("LNURL-auth:")
            print("URI: \(data.uri)")
            print("Tag: \(data.tag)")
            print("K1: \(data.k1)")
            
        case .lnurlChannel(let data):
            print("LNURL-channel:")
            print("URI: \(data.uri)")
            print("Callback: \(data.callback)")
            print("K1: \(data.k1)")
            print("Tag: \(data.tag)")
            
        case .lnurlAddress(let data):
            print("Lightning Address:")
            print("URI: \(data.uri)")
            print("Username: \(data.username)")
            print("Domain: \(data.domain)")
            
        case .nodeId(let url, let network):
            print("Node Connection:")
            print("URL: \(url)")
            print("Network: \(network)")
            
        case .gift(let code, let amount):
            print("Gift Code:")
            print("Code: \(code)")
            print("Amount: \(amount) sats")
            
        case .pubkyAuth(let auth):
            print("pubky Auth:")
            print("Data: \(auth.data)")
        }
    } catch let error as DecodingError {
        switch error {
        case .invalidFormat:
            print("Invalid invoice format")
        case .invalidNetwork:
            print("Invalid network type")
        case .invalidAmount:
            print("Invalid amount")
        case .invalidLNURLPayAmount(let amount, let min, let max):
            print("Invalid LNURL pay amount: \(amount) sats (must be between \(min) and \(max) sats)")
        case .invalidTimestamp:
            print("Invalid timestamp")
        case .invalidChecksum:
            print("Invalid checksum")
        case .invalidResponse:
            print("Invalid response")
        case .unsupportedType:
            print("Unsupported invoice type")
        case .invalidAddress:
            print("Invalid address")
        case .requestFailed:
            print("LNURL request failed")
        case .clientCreationFailed:
            print("Client creation failed")
        case .invoiceCreationFailed(let message):
            print("Invoice creation failed: \(message)")
        }
    }
}

Android (Kotlin)

import com.synonym.bitkitcore.*

suspend fun decodeInvoice() {
    try {
        when (val result = Scanner.decode("lnbc500n1ps...")) {
            is Scanner.Lightning -> with(result.invoice) {
                println("Lightning Invoice:")
                println("Amount: $amountSatoshis sats")
                println("Payment Hash: ${paymentHash.joinToString("") { "%02x".format(it) }}")
                println("Timestamp: $timestampSeconds")
                println("Expiry: $expirySeconds seconds")
                println("Is Expired: $isExpired")
                description?.let { println("Description: $it") }
                println("Network: $networkType")
                payeeNodeId?.let { 
                    println("Node ID: ${it.joinToString("") { b -> "%02x".format(b) }}")
                }
            }
            
            is Scanner.OnChain -> with(result.invoice) {
                println("On-chain Invoice:")
                println("Address: $address")
                println("Amount: $amountSatoshis sats")
                label?.let { println("Label: $it") }
                message?.let { println("Message: $it") }
                params?.forEach { (key, value) ->
                    println("\t$key: $value")
                }
            }
            
            is Scanner.LnurlPay -> with(result.data) {
                println("LNURL-pay:")
                println("URI: $uri")
                println("Callback: $callback")
                println("Min Sendable: $minSendable sats")
                println("Max Sendable: $maxSendable sats")
                println("Metadata: $metadataStr")
                commentAllowed?.let { println("Comment allowed (max length): $it") }
                println("Allows Nostr: $allowsNostr")
                nostrPubkey?.let { 
                    println("Nostr Pubkey: ${it.joinToString("") { b -> "%02x".format(b) }}")
                }
            }
            
            is Scanner.LnurlWithdraw -> with(result.data) {
                println("LNURL-withdraw:")
                println("URI: $uri")
                println("Callback: $callback")
                println("K1: $k1")
                println("Description: $defaultDescription")
                println("Max Withdrawable: $maxWithdrawable sats")
                minWithdrawable?.let { println("Min Withdrawable: $it sats") }
                println("Tag: $tag")
            }
            
            is Scanner.LnurlAuth -> with(result.data) {
                println("LNURL-auth:")
                println("URI: $uri")
                println("Tag: $tag")
                println("K1: $k1")
            }
            
            is Scanner.LnurlChannel -> with(result.data) {
                println("LNURL-channel:")
                println("URI: $uri")
                println("Callback: $callback")
                println("K1: $k1")
                println("Tag: $tag")
            }
            
            is Scanner.LnurlAddress -> with(result.data) {
                println("Lightning Address:")
                println("URI: $uri")
                println("Username: $username")
                println("Domain: $domain")
            }
            
            is Scanner.NodeId -> {
                println("Node Connection:")
                println("URL: ${result.url}")
                println("Network: ${result.network}")
            }
            
            is Scanner.Gift -> {
                println("Gift Code:")
                println("Code: ${result.code}")
                println("Amount: ${result.amount} sats")
            }
            
            is Scanner.PubkyAuth -> {
                println("pubky Auth:")
                println("Data: ${result.auth.data}")
            }
        }
    } catch (e: DecodingError) {
        when (e) {
            is DecodingError.InvalidFormat -> println("Invalid invoice format")
            is DecodingError.InvalidNetwork -> println("Invalid network type")
            is DecodingError.InvalidAmount -> println("Invalid amount")
            is DecodingError.InvalidLNURLPayAmount -> println(
                "Invalid LNURL pay amount: ${e.amount_satoshis} sats " +
                "(must be between ${e.min} and ${e.max} sats)"
            )
            is DecodingError.InvalidTimestamp -> println("Invalid timestamp")
            is DecodingError.InvalidChecksum -> println("Invalid checksum")
            is DecodingError.InvalidResponse -> println("Invalid response")
            is DecodingError.UnsupportedType -> println("Unsupported invoice type")
            is DecodingError.InvalidAddress -> println("Invalid address")
            is DecodingError.RequestFailed -> println("LNURL request failed")
            is DecodingError.ClientCreationFailed -> println("Client creation failed")
            is DecodingError.InvoiceCreationFailed -> println("Invoice creation failed: ${e.error_message}")
        }
    }
}

Python

from bitkitcore import Scanner, DecodingError

try:
  result = await Scanner.decode("lnbc500n1ps...")
  if isinstance(result, Scanner.Lightning):
    print("Lightning Invoice:")
    print(f"Amount: {result.invoice.amount_satoshis} sats")
    print(f"Payment Hash: {result.invoice.payment_hash.hex()}")
    print(f"Timestamp: {result.invoice.timestamp_seconds}")
    print(f"Expiry: {result.invoice.expiry_seconds} seconds")
    print(f"Is Expired: {result.invoice.is_expired}")
    if result.invoice.description:
      print(f"Description: {result.invoice.description}")
    print(f"Network: {result.invoice.network_type}")
    if result.invoice.payee_node_id:
      print(f"Node ID: {result.invoice.payee_node_id.hex()}")

  elif isinstance(result, Scanner.OnChain):
    print("On-chain Invoice:")
    print(f"Address: {result.invoice.address}")
    print(f"Amount: {result.invoice.amount_satoshis} sats")
    if result.invoice.label:
      print(f"Label: {result.invoice.label}")
    if result.invoice.message:
      print(f"Message: {result.invoice.message}")
    if result.invoice.params:
      print("Parameters:")
      for key, value in result.invoice.params.items():
        print(f"\t{key}: {value}")

  elif isinstance(result, Scanner.LnurlPay):
    print("LNURL-pay:")
    print(f"URI: {result.data.uri}")
    print(f"Callback: {result.data.callback}")
    print(f"Min Sendable: {result.data.min_sendable} sats")
    print(f"Max Sendable: {result.data.max_sendable} sats")
    print(f"Metadata: {result.data.metadata_str}")
    if result.data.comment_allowed:
      print(f"Comment allowed (max length): {result.data.comment_allowed}")
    print(f"Allows Nostr: {result.data.allows_nostr}")
    if result.data.nostr_pubkey:
      print(f"Nostr Pubkey: {result.data.nostr_pubkey.hex()}")

  elif isinstance(result, Scanner.LnurlWithdraw):
    print("LNURL-withdraw:")
    print(f"URI: {result.data.uri}")
    print(f"Callback: {result.data.callback}")
    print(f"K1: {result.data.k1}")
    print(f"Description: {result.data.default_description}")
    print(f"Max Withdrawable: {result.data.max_withdrawable} sats")
    if result.data.min_withdrawable:
      print(f"Min Withdrawable: {result.data.min_withdrawable} sats")
    print(f"Tag: {result.data.tag}")

  elif isinstance(result, Scanner.LnurlAuth):
    print("LNURL-auth:")
    print(f"URI: {result.data.uri}")
    print(f"Tag: {result.data.tag}")
    print(f"K1: {result.data.k1}")

  elif isinstance(result, Scanner.LnurlChannel):
    print("LNURL-channel:")
    print(f"URI: {result.data.uri}")
    print(f"Callback: {result.data.callback}")
    print(f"K1: {result.data.k1}")
    print(f"Tag: {result.data.tag}")

  elif isinstance(result, Scanner.LnurlAddress):
    print("Lightning Address:")
    print(f"URI: {result.data.uri}")
    print(f"Username: {result.data.username}")
    print(f"Domain: {result.data.domain}")

  elif isinstance(result, Scanner.NodeId):
    print("Node Connection:")
    print(f"URL: {result.url}")
    print(f"Network: {result.network}")

  elif isinstance(result, Scanner.Gift):
    print("Gift Code:")
    print(f"Code: {result.code}")
    print(f"Amount: {result.amount} sats")

  elif isinstance(result, Scanner.PubkyAuth):
    print("pubky Auth:")
    print(f"Data: {result.auth.data}")

except DecodingError as e:
  if isinstance(e, DecodingError.InvalidFormat):
    print("Invalid invoice format")
  elif isinstance(e, DecodingError.InvalidNetwork):
    print("Invalid network type")
  elif isinstance(e, DecodingError.InvalidAmount):
    print("Invalid amount")
  elif isinstance(e, DecodingError.InvalidLNURLPayAmount):
    print(f"Invalid LNURL pay amount: {e.amount_satoshis} sats " +
          f"(must be between {e.min} and {e.max} sats)")
  elif isinstance(e, DecodingError.InvalidTimestamp):
    print("Invalid timestamp")
  elif isinstance(e, DecodingError.InvalidChecksum):
    print("Invalid checksum")
  elif isinstance(e, DecodingError.InvalidResponse):
    print("Invalid response")
  elif isinstance(e, DecodingError.UnsupportedType):
    print("Unsupported invoice type")
  elif isinstance(e, DecodingError.InvalidAddress):
    print("Invalid address")
  elif isinstance(e, DecodingError.RequestFailed):
    print("LNURL request failed")
  elif isinstance(e, DecodingError.ClientCreationFailed):
    print("Client creation failed")
  elif isinstance(e, DecodingError.InvoiceCreationFailed):
    print(f"Invoice creation failed: {e.error_message}")

Supported Types

The Scanner can decode:

  • Lightning Network BOLT-11 invoices
  • Bitcoin addresses (P2PKH, P2SH, P2WPKH, P2WSH, P2TR)
  • LNURL-pay requests
  • LNURL-withdraw requests
  • LNURL-auth requests
  • LNURL-channel requests
  • Lightning Addresses
  • BIP21 Bitcoin URIs
  • Pubky authentication strings
  • Gift codes (format: bitkit://gift--)

Error Handling

The module uses the following error types:

  • InvalidFormat: The input string format is invalid
  • InvalidNetwork: The network type is invalid or mismatched
  • InvalidAmount: The amount specified is invalid
  • InvalidTimestamp: The timestamp is invalid
  • InvalidChecksum: The checksum verification failed
  • InvalidResponse: Received an invalid response
  • UnsupportedType: The invoice type is not supported
  • InvalidAddress: The address format is invalid
  • RequestFailed: The LNURL request failed
  • ClientCreationFailed: Failed to create the client
  • InvoiceCreationFailed: Failed to create the invoice