story-research-zapwall/lib/zapVerification.ts
2025-12-22 09:48:57 +01:00

111 lines
3.3 KiB
TypeScript

import { Event, verifyEvent, getPublicKey } from 'nostr-tools'
import type { Article } from '@/types/nostr'
/**
* Service for verifying zap receipts and their signatures
*/
export class ZapVerificationService {
/**
* Verify a zap receipt signature
*/
verifyZapReceiptSignature(event: Event): boolean {
try {
return verifyEvent(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 {
// Verify signature first
if (!this.verifyZapReceiptSignature(zapReceipt)) {
console.warn('Zap receipt signature verification failed')
return false
}
// Verify the zap receipt is from the expected user
// In zap receipts, the 'p' tag should contain the recipient (article author)
// and the event pubkey should be from the payer or zap service
const recipientTag = zapReceipt.tags.find((tag) => tag[0] === 'p')
if (!recipientTag || recipientTag[1] !== articlePubkey) {
console.warn('Zap receipt recipient does not match article author')
return false
}
// Verify the article ID is referenced
const eventTag = zapReceipt.tags.find((tag) => tag[0] === 'e')
if (!eventTag || eventTag[1] !== articleId) {
console.warn('Zap receipt does not reference the correct article')
return false
}
// Verify the amount (in millisats, so we need to check if it's >= expectedAmount * 1000)
const amountTag = zapReceipt.tags.find((tag) => tag[0] === 'amount')
if (amountTag) {
const amountInMillisats = parseInt(amountTag[1] || '0')
const expectedAmountInMillisats = expectedAmount * 1000
if (amountInMillisats < expectedAmountInMillisats) {
console.warn(`Zap amount ${amountInMillisats} is less than expected ${expectedAmountInMillisats}`)
return false
}
} else {
console.warn('Zap receipt does not contain amount tag')
return false
}
// Verify it's a zap receipt (kind 9735)
if (zapReceipt.kind !== 9735) {
console.warn('Event is not a zap receipt (kind 9735)')
return false
}
return true
}
/**
* 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 ? eventTag[1] : null,
payer: zapReceipt.pubkey,
}
} catch (error) {
console.error('Error extracting payment info:', error)
return null
}
}
}
export const zapVerificationService = new ZapVerificationService()