story-research-zapwall/lib/paymentNotes.ts
2026-01-10 10:50:47 +01:00

484 lines
14 KiB
TypeScript

import { nostrService } from './nostr'
import { buildTags } from './nostrTagSystem'
import { PLATFORM_SERVICE } from './platformConfig'
import { generatePurchaseHashId, generateReviewTipHashId, generateSponsoringHashId } from './hashIdGenerator'
import { buildObjectId } from './urlGenerator'
import type { Event, EventTemplate } from 'nostr-tools'
import { finalizeEvent } from 'nostr-tools'
import { hexToBytes } from 'nostr-tools/utils'
import type { Purchase, ReviewTip, Sponsoring } from '@/types/nostr'
import { writeOrchestrator } from './writeOrchestrator'
import { getPublishRelays } from './relaySelection'
/**
* Publish an explicit payment note (kind 1) for a purchase
* This note is published in addition to the zap receipt (kind 9735)
*/
export async function publishPurchaseNote(params: {
articleId: string
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
zapReceiptId?: string
category?: 'science-fiction' | 'scientific-research'
seriesId?: string
payerPrivateKey: string
}): Promise<Event | null> {
const category = mapPaymentCategory(params.category)
const payload = await buildPurchaseNotePayload({ ...params, category })
return publishPaymentNoteToRelays({
payerPrivateKey: params.payerPrivateKey,
objectType: 'purchase',
hash: payload.hashId,
eventTemplate: payload.eventTemplate,
parsed: payload.parsedPurchase,
version: 0,
hidden: false,
index: 0,
})
}
/**
* Publish an explicit payment note (kind 1) for a review tip
* This note is published in addition to the zap receipt (kind 9735)
*/
export async function publishReviewTipNote(params: {
articleId: string
reviewId: string
authorPubkey: string
reviewerPubkey: string
payerPubkey: string
amount: number
paymentHash: string
zapReceiptId?: string
category?: 'science-fiction' | 'scientific-research'
seriesId?: string
text?: string
payerPrivateKey: string
}): Promise<Event | null> {
const category = mapPaymentCategory(params.category)
const payload = await buildReviewTipNotePayload({ ...params, category })
return publishPaymentNoteToRelays({
payerPrivateKey: params.payerPrivateKey,
objectType: 'review_tip',
hash: payload.hashId,
eventTemplate: payload.eventTemplate,
parsed: payload.parsedReviewTip,
version: 0,
hidden: false,
index: 0,
})
}
/**
* Publish an explicit payment note (kind 1) for a sponsoring
* This note is published in addition to the zap receipt (kind 9735) if applicable
*/
export async function publishSponsoringNote(params: {
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
category?: 'science-fiction' | 'scientific-research'
seriesId?: string
articleId?: string
text?: string
transactionId?: string // Bitcoin transaction ID for mainnet payments
payerPrivateKey: string
}): Promise<Event | null> {
const category = mapPaymentCategory(params.category)
const payload = await buildSponsoringNotePayload({ ...params, category })
return publishPaymentNoteToRelays({
payerPrivateKey: params.payerPrivateKey,
objectType: 'sponsoring',
hash: payload.hashId,
eventTemplate: payload.eventTemplate,
parsed: payload.parsedSponsoring,
version: 0,
hidden: false,
index: 0,
})
}
function mapPaymentCategory(
category: 'science-fiction' | 'scientific-research' | undefined
): 'sciencefiction' | 'research' {
if (category === 'scientific-research') {
return 'research'
}
return 'sciencefiction'
}
async function buildPurchaseNotePayload(params: {
articleId: string
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
zapReceiptId?: string
category: 'sciencefiction' | 'research'
seriesId?: string
}): Promise<{ hashId: string; eventTemplate: EventTemplate; parsedPurchase: Purchase }> {
const purchaseData = {
payerPubkey: params.payerPubkey,
articleId: params.articleId,
authorPubkey: params.authorPubkey,
amount: params.amount,
paymentHash: params.paymentHash,
...(params.seriesId ? { seriesId: params.seriesId } : {}),
}
const hashId = await generatePurchaseHashId(purchaseData)
const id = buildObjectId(hashId, 0, 0)
const tags = buildPurchaseNoteTags({ ...params, hashId })
tags.push(['json', JSON.stringify({ type: 'purchase', id, hash: hashId, version: 0, index: 0, ...purchaseData })])
const parsedPurchase = buildParsedPurchase({ ...params, id, hashId })
return { hashId, eventTemplate: buildPaymentNoteTemplate(tags, `Purchase confirmed: ${params.amount} sats for article ${params.articleId}`), parsedPurchase }
}
function buildPurchaseNoteTags(params: {
articleId: string
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
zapReceiptId?: string
category: 'sciencefiction' | 'research'
seriesId?: string
hashId: string
}): string[][] {
return buildTags({
type: 'payment',
category: params.category,
id: params.hashId,
service: PLATFORM_SERVICE,
version: 0,
hidden: false,
payment: true,
paymentType: 'purchase',
amount: params.amount,
payerPubkey: params.payerPubkey,
recipientPubkey: params.authorPubkey,
paymentHash: params.paymentHash,
articleId: params.articleId,
...(params.zapReceiptId ? { zapReceiptId: params.zapReceiptId } : {}),
...(params.seriesId ? { seriesId: params.seriesId } : {}),
})
}
function buildParsedPurchase(params: {
articleId: string
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
id: string
hashId: string
seriesId?: string
}): Purchase {
return {
id: params.id,
hash: params.hashId,
version: 0,
index: 0,
payerPubkey: params.payerPubkey,
articleId: params.articleId,
authorPubkey: params.authorPubkey,
amount: params.amount,
paymentHash: params.paymentHash,
createdAt: Math.floor(Date.now() / 1000),
...(params.seriesId ? { seriesId: params.seriesId } : {}),
kindType: 'purchase',
}
}
async function buildReviewTipNotePayload(params: {
articleId: string
reviewId: string
authorPubkey: string
reviewerPubkey: string
payerPubkey: string
amount: number
paymentHash: string
zapReceiptId?: string
category: 'sciencefiction' | 'research'
seriesId?: string
text?: string
}): Promise<{ hashId: string; eventTemplate: EventTemplate; parsedReviewTip: ReviewTip }> {
const tipData = {
payerPubkey: params.payerPubkey,
articleId: params.articleId,
reviewId: params.reviewId,
reviewerPubkey: params.reviewerPubkey,
authorPubkey: params.authorPubkey,
amount: params.amount,
paymentHash: params.paymentHash,
...(params.seriesId ? { seriesId: params.seriesId } : {}),
...(params.text ? { text: params.text } : {}),
}
const hashId = await generateReviewTipHashId(tipData)
const id = buildObjectId(hashId, 0, 0)
const tags = buildReviewTipNoteTags({ ...params, hashId })
tags.push(['json', JSON.stringify({ type: 'review_tip', id, hash: hashId, version: 0, index: 0, ...tipData })])
const parsedReviewTip = buildParsedReviewTip({ ...params, id, hashId })
return { hashId, eventTemplate: buildPaymentNoteTemplate(tags, buildReviewTipNoteContent(params)), parsedReviewTip }
}
function buildReviewTipNoteTags(params: {
articleId: string
reviewId: string
authorPubkey: string
reviewerPubkey: string
payerPubkey: string
amount: number
paymentHash: string
zapReceiptId?: string
category: 'sciencefiction' | 'research'
seriesId?: string
text?: string
hashId: string
}): string[][] {
return buildTags({
type: 'payment',
category: params.category,
id: params.hashId,
service: PLATFORM_SERVICE,
version: 0,
hidden: false,
payment: true,
paymentType: 'review_tip',
amount: params.amount,
payerPubkey: params.payerPubkey,
recipientPubkey: params.reviewerPubkey,
paymentHash: params.paymentHash,
articleId: params.articleId,
reviewId: params.reviewId,
...(params.zapReceiptId ? { zapReceiptId: params.zapReceiptId } : {}),
...(params.seriesId ? { seriesId: params.seriesId } : {}),
...(params.text ? { text: params.text } : {}),
})
}
function buildReviewTipNoteContent(params: { amount: number; reviewId: string; text?: string }): string {
const prefix = `Review tip confirmed: ${params.amount} sats for review ${params.reviewId}`
return params.text ? `${prefix}\n\n${params.text}` : prefix
}
function buildParsedReviewTip(params: {
articleId: string
reviewId: string
authorPubkey: string
reviewerPubkey: string
payerPubkey: string
amount: number
paymentHash: string
seriesId?: string
text?: string
id: string
hashId: string
}): ReviewTip {
return {
id: params.id,
hash: params.hashId,
version: 0,
index: 0,
payerPubkey: params.payerPubkey,
articleId: params.articleId,
reviewId: params.reviewId,
reviewerPubkey: params.reviewerPubkey,
authorPubkey: params.authorPubkey,
amount: params.amount,
paymentHash: params.paymentHash,
createdAt: Math.floor(Date.now() / 1000),
...(params.seriesId ? { seriesId: params.seriesId } : {}),
...(params.text ? { text: params.text } : {}),
kindType: 'review_tip',
}
}
function buildPaymentNoteTemplate(tags: string[][], content: string): EventTemplate {
return {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags,
content,
}
}
async function buildSponsoringNotePayload(params: {
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
category: 'sciencefiction' | 'research'
seriesId?: string
articleId?: string
text?: string
transactionId?: string
}): Promise<{
hashId: string
eventTemplate: EventTemplate
parsedSponsoring: Sponsoring
}> {
const sponsoringData = buildSponsoringHashInput(params)
const hashId = await generateSponsoringHashId(sponsoringData)
const id = buildObjectId(hashId, 0, 0)
const tags = buildSponsoringNoteTags({ ...params, hashId })
tags.push(['json', buildSponsoringPaymentJson({ ...params, sponsoringData, id, hashId })])
const parsedSponsoring = buildParsedSponsoring({ ...params, id, hashId })
const eventTemplate = buildSponsoringEventTemplate({ tags, content: buildSponsoringNoteContent(params) })
return { hashId, eventTemplate, parsedSponsoring }
}
function buildSponsoringHashInput(params: {
payerPubkey: string
authorPubkey: string
amount: number
paymentHash: string
seriesId?: string
articleId?: string
}): Parameters<typeof generateSponsoringHashId>[0] {
return {
payerPubkey: params.payerPubkey,
authorPubkey: params.authorPubkey,
amount: params.amount,
paymentHash: params.paymentHash,
...(params.seriesId ? { seriesId: params.seriesId } : {}),
...(params.articleId ? { articleId: params.articleId } : {}),
}
}
function buildParsedSponsoring(params: {
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
seriesId?: string
articleId?: string
text?: string
id: string
hashId: string
}): Sponsoring {
return {
id: params.id,
hash: params.hashId,
version: 0,
index: 0,
payerPubkey: params.payerPubkey,
authorPubkey: params.authorPubkey,
amount: params.amount,
paymentHash: params.paymentHash,
createdAt: Math.floor(Date.now() / 1000),
...(params.seriesId ? { seriesId: params.seriesId } : {}),
...(params.articleId ? { articleId: params.articleId } : {}),
...(params.text ? { text: params.text } : {}),
kindType: 'sponsoring',
}
}
function buildSponsoringEventTemplate(params: { tags: string[][]; content: string }): EventTemplate {
return { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: params.tags, content: params.content }
}
function buildSponsoringNoteContent(params: {
authorPubkey: string
amount: number
text?: string
}): string {
const prefix = `Sponsoring confirmed: ${params.amount} sats for author ${params.authorPubkey.substring(0, 16)}...`
return params.text ? `${prefix}\n\n${params.text}` : prefix
}
function buildSponsoringNoteTags(params: {
authorPubkey: string
payerPubkey: string
amount: number
paymentHash: string
category: 'sciencefiction' | 'research'
seriesId?: string
articleId?: string
text?: string
transactionId?: string
hashId: string
}): string[][] {
const tags = buildTags({
type: 'payment',
category: params.category,
id: params.hashId,
service: PLATFORM_SERVICE,
version: 0,
hidden: false,
payment: true,
paymentType: 'sponsoring',
amount: params.amount,
payerPubkey: params.payerPubkey,
recipientPubkey: params.authorPubkey,
paymentHash: params.paymentHash,
...(params.seriesId ? { seriesId: params.seriesId } : {}),
...(params.articleId ? { articleId: params.articleId } : {}),
...(params.text ? { text: params.text } : {}),
})
if (params.transactionId) {
tags.push(['transaction_id', params.transactionId])
}
return tags
}
function buildSponsoringPaymentJson(params: {
id: string
hashId: string
sponsoringData: Record<string, unknown>
text?: string
transactionId?: string
}): string {
return JSON.stringify({
type: 'sponsoring',
id: params.id,
hash: params.hashId,
version: 0,
index: 0,
...params.sponsoringData,
...(params.text ? { text: params.text } : {}),
...(params.transactionId ? { transactionId: params.transactionId } : {}),
})
}
async function publishPaymentNoteToRelays(params: {
payerPrivateKey: string
objectType: 'purchase' | 'review_tip' | 'sponsoring'
hash: string
eventTemplate: EventTemplate
parsed: Purchase | ReviewTip | Sponsoring
version: number
hidden: boolean
index: number
}): Promise<Event | null> {
nostrService.setPrivateKey(params.payerPrivateKey)
writeOrchestrator.setPrivateKey(params.payerPrivateKey)
const secretKey = hexToBytes(params.payerPrivateKey)
const event = finalizeEvent(params.eventTemplate, secretKey)
const relays = await getPublishRelays()
const result = await writeOrchestrator.writeAndPublish(
{
objectType: params.objectType,
hash: params.hash,
event,
parsed: params.parsed,
version: params.version,
hidden: params.hidden,
index: params.index,
},
relays
)
if (!result.success) {
return null
}
return event
}