feat: zap (#107)

This commit is contained in:
Cody Tseng
2025-03-01 23:52:05 +08:00
committed by GitHub
parent 407a6fb802
commit 249593d547
72 changed files with 2582 additions and 818 deletions

View File

@@ -1,3 +1,7 @@
export function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
}
export function isEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

View File

@@ -3,11 +3,13 @@ import client from '@/services/client.service'
import { TImageInfo, TRelayList } from '@/types'
import { LRUCache } from 'lru-cache'
import { Event, kinds, nip19 } from 'nostr-tools'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey } from './pubkey'
import { extractImageInfoFromTag, isReplyETag, isRootETag, tagNameEquals } from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
const EVENT_EMBEDDED_EVENT_IDS_CACHE = new LRUCache<string, string[]>({ max: 10000 })
const EVENT_IS_REPLY_NOTE_CACHE = new LRUCache<string, boolean>({ max: 10000 })
export function isNsfwEvent(event: Event) {
return event.tags.some(
@@ -19,15 +21,23 @@ export function isNsfwEvent(event: Event) {
export function isReplyNoteEvent(event: Event) {
if (event.kind !== kinds.ShortTextNote) return false
const cache = EVENT_IS_REPLY_NOTE_CACHE.get(event.id)
if (cache !== undefined) return cache
const mentionsEventIds: string[] = []
for (const [tagName, eventId, , marker] of event.tags) {
if (tagName !== 'e' || !eventId) continue
mentionsEventIds.push(eventId)
if (['root', 'reply'].includes(marker)) return true
if (['root', 'reply'].includes(marker)) {
EVENT_IS_REPLY_NOTE_CACHE.set(event.id, true)
return true
}
}
const embeddedEventIds = extractEmbeddedEventIds(event)
return mentionsEventIds.some((id) => !embeddedEventIds.includes(id))
const result = mentionsEventIds.some((id) => !embeddedEventIds.includes(id))
EVENT_IS_REPLY_NOTE_CACHE.set(event.id, result)
return result
}
export function isCommentEvent(event: Event) {
@@ -159,6 +169,9 @@ export function getProfileFromProfileEvent(event: Event) {
nip05: profileObj.nip05,
about: profileObj.about,
website: profileObj.website ? normalizeHttpUrl(profileObj.website) : undefined,
lud06: profileObj.lud06,
lud16: profileObj.lud16,
lightningAddress: getLightningAddressFromProfile(profileObj),
created_at: event.created_at
}
} catch (err) {
@@ -363,6 +376,68 @@ export function extractEmbeddedNotesFromContent(content: string) {
return { embeddedNotes, contentWithoutEmbeddedNotes: c }
}
export function extractZapInfoFromReceipt(receiptEvent: Event) {
if (receiptEvent.kind !== kinds.Zap) return null
let senderPubkey: string | undefined
let recipientPubkey: string | undefined
let eventId: string | undefined
let invoice: string | undefined
let amount: number | undefined
let comment: string | undefined
let description: string | undefined
let preimage: string | undefined
try {
receiptEvent.tags.forEach(([tagName, tagValue]) => {
switch (tagName) {
case 'P':
senderPubkey = tagValue
break
case 'p':
recipientPubkey = tagValue
break
case 'e':
eventId = tagValue
break
case 'bolt11':
invoice = tagValue
break
case 'description':
description = tagValue
break
case 'preimage':
preimage = tagValue
break
}
})
if (!recipientPubkey || !invoice) return null
amount = invoice ? getAmountFromInvoice(invoice) : 0
if (description) {
try {
const zapRequest = JSON.parse(description)
comment = zapRequest.content
if (!senderPubkey) {
senderPubkey = zapRequest.pubkey
}
} catch {
// ignore
}
}
return {
senderPubkey,
recipientPubkey,
eventId,
invoice,
amount,
comment,
preimage
}
} catch {
return null
}
}
export function extractEmbeddedEventIds(event: Event) {
const cache = EVENT_EMBEDDED_EVENT_IDS_CACHE.get(event.id)
if (cache) return cache

32
src/lib/lightning.ts Normal file
View File

@@ -0,0 +1,32 @@
import { TProfile } from '@/types'
import { Invoice } from '@getalby/lightning-tools'
import { isEmail } from './common'
export function getAmountFromInvoice(invoice: string): number {
const _invoice = new Invoice({ pr: invoice }) // TODO: need to validate
return _invoice.satoshi
}
export function formatAmount(amount: number) {
if (amount < 1000) return amount
if (amount < 1000000) return `${Math.round(amount / 100) / 10}k`
return `${Math.round(amount / 100000) / 10}M`
}
export function getLightningAddressFromProfile(profile: TProfile) {
// Some clients have incorrectly filled in the positions for lud06 and lud16
const { lud16: a, lud06: b } = profile
let lud16: string | undefined
let lud06: string | undefined
if (a && isEmail(a)) {
lud16 = a
} else if (b && isEmail(b)) {
lud16 = b
} else if (b && b.startsWith('lnurl')) {
lud06 = b
} else if (a && a.startsWith('lnurl')) {
lud06 = a
}
return lud16 || lud06 || undefined
}

View File

@@ -39,6 +39,7 @@ export const toRelaySettings = (tag?: 'mailbox' | 'relay-sets') => {
return '/relay-settings' + (tag ? '#' + tag : '')
}
export const toSettings = () => '/settings'
export const toWallet = () => '/wallet'
export const toProfileEditor = () => '/profile-editor'
export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}`
export const toMuteList = () => '/mutes'