Files
welshman/packages/util/Zaps.ts
T
2024-04-22 13:22:53 -07:00

127 lines
2.9 KiB
TypeScript

import type {Event} from 'nostr-tools'
import {hexToBech32} from '@welshman/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
}