story-research-zapwall/lib/zapVerification.ts
2025-12-23 02:20:57 +01:00

118 lines
3.4 KiB
TypeScript

import { Event, validateEvent, verifySignature } from 'nostr-tools'
/**
* Service for verifying zap receipts and their signatures
*/
export class ZapVerificationService {
/**
* Verify a zap receipt signature
*/
verifyZapReceiptSignature(event: Event): boolean {
try {
return validateEvent(event) && verifySignature(event)
} catch (error) {
console.error('Error verifying zap receipt signature:', error)
return false
}
}
/**
* Check if a zap receipt is valid for a specific article and user
*/
verifyZapReceiptForArticle(
zapReceipt: Event,
articleId: string,
articlePubkey: string,
_userPubkey: string,
expectedAmount: number
): boolean {
if (!this.verifyZapReceiptSignature(zapReceipt)) {
console.warn('Zap receipt signature verification failed')
return false
}
return (
this.isRecipientValid(zapReceipt, articlePubkey) &&
this.isArticleReferenced(zapReceipt, articleId) &&
this.isAmountValid(zapReceipt, expectedAmount) &&
this.isZapKind(zapReceipt)
)
}
/**
* Extract payment information from a zap receipt
*/
extractPaymentInfo(zapReceipt: Event): {
amount: number // in satoshis
recipient: string
articleId: string | null
payer: string
} | null {
try {
const amountTag = zapReceipt.tags.find((tag) => tag[0] === 'amount')
const recipientTag = zapReceipt.tags.find((tag) => tag[0] === 'p')
const eventTag = zapReceipt.tags.find((tag) => tag[0] === 'e')
if (!amountTag || !recipientTag) {
return null
}
const amountInMillisats = parseInt(amountTag[1] ?? '0')
const amountInSats = Math.floor(amountInMillisats / 1000)
return {
amount: amountInSats,
recipient: recipientTag?.[1] ?? '',
articleId: eventTag?.[1] ?? null,
payer: zapReceipt.pubkey,
}
} catch (error) {
console.error('Error extracting payment info:', error)
return null
}
}
private isRecipientValid(zapReceipt: Event, articlePubkey: string): boolean {
const recipient = zapReceipt.tags.find((tag) => tag[0] === 'p')?.[1]
if (recipient !== articlePubkey) {
console.warn('Zap receipt recipient does not match article author')
return false
}
return true
}
private isArticleReferenced(zapReceipt: Event, articleId: string): boolean {
const eventIdTag = zapReceipt.tags.find((tag) => tag[0] === 'e')?.[1]
if (eventIdTag !== articleId) {
console.warn('Zap receipt does not reference the correct article')
return false
}
return true
}
private isAmountValid(zapReceipt: Event, expectedAmount: number): boolean {
const amountTag = zapReceipt.tags.find((tag) => tag[0] === 'amount')?.[1]
if (!amountTag) {
console.warn('Zap receipt does not contain amount tag')
return false
}
const amountInMillisats = parseInt(amountTag ?? '0')
const expectedAmountInMillisats = expectedAmount * 1000
if (amountInMillisats < expectedAmountInMillisats) {
console.warn(`Zap amount ${amountInMillisats} is less than expected ${expectedAmountInMillisats}`)
return false
}
return true
}
private isZapKind(zapReceipt: Event): boolean {
if (zapReceipt.kind !== 9735) {
console.warn('Event is not a zap receipt (kind 9735)')
return false
}
return true
}
}
export const zapVerificationService = new ZapVerificationService()