import { BufferReader } from "@node-lightning/bufio" ; import bech32 from "bech32" ; import { ADDRESS_VERSION } from "./address-version" ; import * as crypto from "./crypto" ; import { FIELD_TYPE } from "./field-type" ; import { hrpToPico } from "./hrp-pico" ; import { Invoice } from "./invoice" ; import { WordCursor } from "./word-cursor" ; param invoice return export function decode ( invoice ) { const { prefix , words } = bech32 . decode ( invoice , Number . MAX_SAFE_INTEGER ) ; const { network , picoBtc } = parsePrefix ( prefix ) ; const wordcursor = new WordCursor ( words ) ; const timestamp = wordcursor . readUIntBE ( 7 ) ; const fields = [ ] ; const unknownFields = [ ] ; while ( wordcursor . wordsRemaining > 104 ) { const type = wordcursor . readUIntBE ( 1 ) ; const len = wordcursor . readUIntBE ( 2 ) ; let value ; switch ( type ) { case 0 : continue ; case FIELD_TYPE . PAYMENT_HASH : value = wordcursor . readBytes ( len ) ; if ( len !== 52 ) { unknownFields . push ( { type , value } ) ; continue ; } break ; case FIELD_TYPE . ROUTE : { value = [ ] ; const bytes = wordcursor . readBytes ( len ) ; const bytecursor = new BufferReader ( bytes ) ; while ( ! bytecursor . eof ) { value . push ( { pubkey : bytecursor . readBytes ( 33 ) , short_channel_id : bytecursor . readBytes ( 8 ) , fee_base_msat : bytecursor . readUInt32BE ( ) , fee_proportional_millionths : bytecursor . readUInt32BE ( ) , cltv_expiry_delta : bytecursor . readUInt16BE ( ) , } ) ; } } break ; case FIELD_TYPE . EXPIRY : value = wordcursor . readUIntBE ( len ) ; break ; case FIELD_TYPE . FALLBACK_ADDRESS : { const version = wordcursor . readUIntBE ( 1 ) ; const address = wordcursor . readBytes ( len - 1 ) ; value = { version , address , } ; if ( version !== ADDRESS_VERSION . SEGWIT && version !== ADDRESS_VERSION . P2PKH && version !== ADDRESS_VERSION . P2SH ) { unknownFields . push ( { type , value } ) ; continue ; } } break ; case FIELD_TYPE . SHORT_DESC : value = wordcursor . readBytes ( len ) . toString ( "utf8" ) ; break ; case FIELD_TYPE . PAYEE_NODE : value = wordcursor . readBytes ( len ) ; if ( len !== 53 ) { unknownFields . push ( { type , value } ) ; continue ; } break ; case FIELD_TYPE . HASH_DESC : value = wordcursor . readBytes ( len ) ; if ( len !== 52 ) { unknownFields . push ( { type , value } ) ; continue ; } break ; case FIELD_TYPE . MIN_FINAL_CLTV_EXPIRY : value = wordcursor . readUIntBE ( len ) ; break ; default : value = wordcursor . readBytes ( len ) ; unknownFields . push ( { type , value } ) ; continue ; } fields . push ( { type , value } ) ; } const sigBytes = wordcursor . readBytes ( 103 ) ; const r = sigBytes . slice ( 0 , 32 ) ; const s = sigBytes . slice ( 32 ) ; const recoveryFlag = wordcursor . readUIntBE ( 1 ) ; wordcursor . position = 0 ; let preHashData = wordcursor . readBytes ( words . length - 104 , true ) ; preHashData = Buffer . concat ( [ Buffer . from ( prefix ) , preHashData ] ) ; const hashData = crypto . sha256 ( preHashData ) ; const payeeNodeField = fields . find ( p => p . type === FIELD_TYPE . PAYEE_NODE ) ; const pubkey = payeeNodeField ? payeeNodeField . value : crypto . ecdsaRecovery ( hashData , sigBytes , recoveryFlag ) ; if ( ! crypto . ecdsaVerify ( pubkey , hashData , sigBytes ) ) throw new Error ( "Signature invalid" ) ; const result = new Invoice ( ) ; result . _value = picoBtc ; result . network = network ; result . timestamp = timestamp ; result . fields = fields ; result . unknownFields = unknownFields ; result . signature = { r , s , recoveryFlag } ; result . pubkey = pubkey ; result . hashData = hashData ; result . usedSigRecovery = ! ! payeeNodeField ; return result ; } /**
 * Parses the prefix into network and value and then performs
 * validations on the values.
 *
 * This code is rough. Should refactor into two steps:
 * 1) tokenize
 * 2) parse tokens
 *
 * Value is returned as pico bitcoin.
 */ function parsePrefix ( prefix ) { if ( ! prefix . startsWith ( "ln" ) ) throw new Error ( "Invalid prefix" ) ; let network = "" ; let tempValue = "" ; let value ; let multiplier ; let hasNetwork = false ; let hasAmount = false ; for ( let i = 2 ; i < prefix . length ; i ++ ) { const charCode = prefix . charCodeAt ( i ) ; if ( ! hasNetwork ) { if ( charCode >= 97 && charCode <= 122 ) network += prefix [ i ] ; else hasNetwork = true ; } if ( hasNetwork && ! hasAmount ) { if ( charCode >= 48 && charCode <= 57 ) tempValue += prefix [ i ] ; else if ( tempValue ) hasAmount = true ; else throw new Error ( "Invalid amount" ) ; } if ( hasAmount ) { if ( charCode >= 97 && charCode <= 122 ) multiplier = prefix [ i ] ; else throw new Error ( "Invalid character" ) ; } } if ( tempValue === "" ) value = null ; else value = BigInt ( tempValue ) * hrpToPico ( multiplier ) ; if ( ! isValidNetwork ( network ) ) throw new Error ( "Invalid network" ) ; if ( ! isValidValue ( value ) ) throw new Error ( "Invalid amount" ) ; return { network , picoBtc : value , } ; } function isValidNetwork ( network ) { return network === "bc" || network === "tb" || network === "bcrt" || network === "sb" ; } function isValidValue ( value ) { return value === null || value > 0 ; }