import type {Event} from 'nostr-tools' import {hexToBech32} from '@coracle.social/lib' import {Tags} from "./Tags" const DIVISORS = { m: BigInt(1e3), u: BigInt(1e6), n: BigInt(1e9), p: BigInt(1e12), } const MAX_MILLISATS = BigInt("2100000000000000000") const MILLISATS_PER_BTC = BigInt(1e11) export const hrpToMillisat = (hrpString: string) => { let divisor, value if (hrpString.slice(-1).match(/^[munp]$/)) { divisor = hrpString.slice(-1) value = hrpString.slice(0, -1) } else if (hrpString.slice(-1).match(/^[^munp0-9]$/)) { throw new Error("Not a valid multiplier for the amount") } else { value = hrpString } if (!value.match(/^\d+$/)) throw new Error("Not a valid human readable amount") const valueBN = BigInt(value) const millisatoshisBN = divisor ? (valueBN * MILLISATS_PER_BTC) / (DIVISORS as any)[divisor] : valueBN * MILLISATS_PER_BTC if ( (divisor === "p" && !(valueBN % BigInt(10) === BigInt(0))) || millisatoshisBN > MAX_MILLISATS ) { throw new Error("Amount is outside of valid range") } return millisatoshisBN } export const getInvoiceAmount = (bolt11: string) => { const hrp = bolt11.match(/lnbc(\d+\w)/) const bn = hrpToMillisat(hrp![1]) return Number(bn) } export const getLnUrl = (address: string) => { if (address.startsWith("lnurl1")) { return address } // If it's a regular url, just encode it if (address.includes("://")) { return hexToBech32("lnurl", address) } // Try to parse it as a lud16 address if (address.includes("@")) { const [name, domain] = address.split("@") if (domain && name) { return hexToBech32("lnurl", `https://${domain}/.well-known/lnurlp/${name}`) } } return null } export type Zapper = { lnurl: string pubkey: string, callback: string minSendable: number maxSendable: number nostrPubkey: string allowsNostr: boolean } export type Zap = { request: Event response: Event, invoiceAmount: number } export const zapFromEvent = (response: Event, zapper: Zapper) => { const responseMeta = Tags.fromEvent(response).asObject() let zap: Zap try { zap = { response, invoiceAmount: getInvoiceAmount(responseMeta.bolt11), request: JSON.parse(responseMeta.description), } } catch (e) { return null } // Don't count zaps that the user sent himself if (zap.request.pubkey === zapper.pubkey) { return null } const {amount, lnurl} = Tags.fromEvent(zap.request).asObject() // Verify that the zapper actually sent the requested amount (if it was supplied) if (amount && parseInt(amount) !== zap.invoiceAmount) { return null } // If the sending client provided an lnurl tag, verify that too if (lnurl && lnurl !== zapper.lnurl) { return null } // Verify that the request actually came from the recipient's zapper if (zap.response.pubkey !== zapper.nostrPubkey) { return null } return zap }