111 lines
3.3 KiB
TypeScript
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()
|