import * as lightningPayReq from "bolt11" ; import moment from "moment" ; import url from "url" ; import { networks , address } from "bitcoinjs-lib" ; import { getDescription , getDestination , getUsername } from "./bolt11" ; import type { INetwork } from "../types/network" ; import * as parsing from "./parsing" ; export type IPaymentType = "lightning" | "onchain" | "username" | "lnurl" | undefined ; export interface IValidPaymentReponse { valid : boolean ; errorMessage ? : string | undefined ; invoice ? : string | undefined ; address ? : string | undefined ; lnurl ? : string | undefined ; amount ? : number | undefined ; amountless ? : boolean | undefined ; memo ? : lightningPayReq . TagData | string | undefined ; paymentType ? : IPaymentType ; sameNode ? : boolean | undefined ; username ? : string | undefined ; } const mappingToBitcoinJs = ( input ) => { switch ( input ) { case "mainnet" : return networks . bitcoin ; case "testnet" : return networks . testnet ; case "regtest" : return networks . regtest ; } } ; export const lightningInvoiceHasExpired = ( payReq ) : boolean => { return payReq ?. timeExpireDate < moment ( ) . unix ( ) ; } ; const reAmount = /^(([\d.]+)(X(\d+))?|x([\da-f]*)(\.([\da-f]*))?(X([\da-f]+))?)$/i ; function parseAmount ( txt ) { const m = txt . match ( reAmount ) ; return Math . round ( m [ 5 ] ? ( parseInt ( m [ 5 ] , 16 ) + ( m [ 7 ] ? parseInt ( m [ 7 ] , 16 ) * Math . pow ( 16 , - m [ 7 ] . length ) : 0 ) ) * ( m [ 9 ] ? Math . pow ( 16 , parseInt ( m [ 9 ] , 16 ) ) : 0x10000 ) : m [ 2 ] * ( m [ 4 ] ? Math . pow ( 10 , m [ 4 ] ) : 1e8 ) ) ; } export const validPayment = ( input , network , myPubKey , username ) : IValidPaymentReponse => { if ( ! input ) { return { valid : false } ; } let [ protocol , data ] = input . split ( ":" ) ; let paymentType ; let lnurl ; if ( protocol . toLowerCase ( ) === "bitcoin" ) { paymentType = "onchain" ; } else if ( protocol . toLowerCase ( ) === "lightning" ) { paymentType = "lightning" ; data = data . toLowerCase ( ) ; } else if ( ( protocol && protocol . toLowerCase ( ) . startsWith ( "lnurl" ) ) || ( data && data . toLowerCase ( ) . startsWith ( "lnurl" ) ) ) { paymentType = "lnurl" ; lnurl = protocol || data ; } else if ( protocol . toLowerCase ( ) . startsWith ( "ln" ) ) { paymentType = "lightning" ; if ( network === "testnet" && protocol . toLowerCase ( ) . startsWith ( "lnbc" ) ) { return { valid : false , paymentType , errorMessage : "This is a mainnet invoice. The wallet is on testnet" , } ; } if ( network === "mainnet" && protocol . toLowerCase ( ) . startsWith ( "lntb" ) ) { return { valid : false , paymentType , errorMessage : "This is a testnet invoice. The wallet is on mainnet" , } ; } data = protocol . toLowerCase ( ) ; } else if ( protocol . toLowerCase ( ) === "https" ) { const domain = "//ln.bitcoinbeach.com/" ; if ( data . startsWith ( domain ) ) { return { valid : true , paymentType : "username" , username : data . substring ( domain . length ) , } ; } } else { data = protocol ; } if ( paymentType === "onchain" || paymentType === undefined ) { try { const decodedData = url . parse ( data , true ) ; let path = decodedData . pathname ; const lowerCasePath = path . toLowerCase ( ) ; if ( lowerCasePath . startsWith ( "bc1" ) || lowerCasePath . startsWith ( "tb1" ) || lowerCasePath . startsWith ( "bcrt1" ) ) { path = lowerCasePath ; } let amount ; try { amount = parseAmount ( decodedData . query . amount ) ; } catch ( err ) { console . log ( `can't decode amount ${ err }` ) ; } address . toOutputScript ( path , mappingToBitcoinJs ( network ) ) ; paymentType = "onchain" ; return { valid : true , paymentType , address : path , amount , amountless : ! amount , } ; } catch ( e ) { console . warn ( `issue with payment ${ e }` ) ; return { valid : false } ; } } else if ( paymentType === "lightning" ) { let payReq ; try { payReq = lightningPayReq . decode ( data ) ; } catch ( err ) { console . log ( err ) ; return { valid : false } ; } const sameNode = myPubKey === getDestination ( payReq ) ; if ( sameNode && username === getUsername ( payReq ) ) { return { valid : false , paymentType , errorMessage : "invoice needs to be for a different user" , } ; } let amount ; let amountless ; if ( payReq . satoshis || payReq . millisatoshis ) { amount = payReq . satoshis ?? Number ( payReq . millisatoshis ) / 1000 ; amountless = false ; } else { amount = 0 ; amountless = true ; } if ( parsing . lightningInvoiceHasExpired ( payReq ) ) { return { valid : false , errorMessage : "invoice has expired" , paymentType } ; } const memo = getDescription ( payReq ) ; return { valid : true , invoice : data , amount , amountless , memo , paymentType , sameNode , } ; } else if ( paymentType === "lnurl" ) { return { valid : true , lnurl , amountless : false , } ; } else { return { valid : false , errorMessage : "We are unable to detect an invoice or payment address" , } ; } } ;