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

202 lines
6.1 KiB
TypeScript

import { calculateArticleSplit, calculateReviewSplit } from './platformCommissions'
/**
* Automatic transfer service
* Handles automatic forwarding of author/reviewer portions after payment
*
* Since WebLN doesn't support BOLT12 with split, the platform receives the full amount
* and must automatically forward the author/reviewer portion.
*
* This service tracks transfers and ensures they are executed correctly.
*/
export interface TransferResult {
success: boolean
transferInvoiceId?: string
error?: string
amount: number
recipient: string
}
export class AutomaticTransferService {
/**
* Transfer author portion after article payment
* Creates a Lightning invoice from the platform to the author
*/
private logTransferRequired(params: {
type: 'article' | 'review'
id: string
pubkey: string
amount: number
recipient: string
platformCommission: number
}): void {
const logData = {
[params.type === 'article' ? 'articleId' : 'reviewId']: params.id,
[params.type === 'article' ? 'articlePubkey' : 'reviewerPubkey']: params.pubkey,
amount: params.amount,
recipient: params.recipient,
platformCommission: params.platformCommission,
timestamp: new Date().toISOString(),
}
console.warn(`Automatic transfer required${params.type === 'review' ? ' for review' : ''}`, logData)
}
private buildTransferError(error: unknown, recipient: string, amount: number = 0): TransferResult {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
error: errorMessage,
amount,
recipient,
}
}
async transferAuthorPortion(
authorLightningAddress: string,
articleId: string,
articlePubkey: string,
paymentAmount: number
): Promise<TransferResult> {
return this.transferPortion({
type: 'article',
id: articleId,
pubkey: articlePubkey,
recipient: authorLightningAddress,
paymentAmount,
computeSplit: calculateArticleSplit,
getRecipientAmount: (split) => split.author,
missingRecipientError: 'Author Lightning address not available',
errorLogMessage: 'Error transferring author portion',
})
}
/**
* Transfer reviewer portion after review reward payment
*/
async transferReviewerPortion(
reviewerLightningAddress: string,
reviewId: string,
reviewerPubkey: string,
paymentAmount: number
): Promise<TransferResult> {
return this.transferPortion({
type: 'review',
id: reviewId,
pubkey: reviewerPubkey,
recipient: reviewerLightningAddress,
paymentAmount,
computeSplit: calculateReviewSplit,
getRecipientAmount: (split) => split.reviewer,
missingRecipientError: 'Reviewer Lightning address not available',
errorLogMessage: 'Error transferring reviewer portion',
})
}
private async transferPortion<TSplit extends { platform: number }>(params: {
type: 'article' | 'review'
id: string
pubkey: string
recipient: string
paymentAmount: number
computeSplit: (amount: number) => TSplit
getRecipientAmount: (split: TSplit) => number
missingRecipientError: string
errorLogMessage: string
}): Promise<TransferResult> {
try {
const split = params.computeSplit(params.paymentAmount)
const recipientAmount = params.getRecipientAmount(split)
if (!params.recipient) {
return this.buildMissingRecipientResult({
error: params.missingRecipientError,
recipient: params.recipient,
amount: recipientAmount,
})
}
this.logAndTrackTransferRequirement({
type: params.type,
id: params.id,
pubkey: params.pubkey,
recipient: params.recipient,
amount: recipientAmount,
platformCommission: split.platform,
})
return { success: true, amount: recipientAmount, recipient: params.recipient }
} catch (error) {
this.logTransferError({ message: params.errorLogMessage, id: params.id, pubkey: params.pubkey, error })
return this.buildTransferError(error, params.recipient)
}
}
private buildMissingRecipientResult(params: { error: string; recipient: string; amount: number }): TransferResult {
return { success: false, error: params.error, amount: params.amount, recipient: params.recipient }
}
private logAndTrackTransferRequirement(params: {
type: 'article' | 'review'
id: string
pubkey: string
recipient: string
amount: number
platformCommission: number
}): void {
this.logTransferRequired({
type: params.type,
id: params.id,
pubkey: params.pubkey,
amount: params.amount,
recipient: params.recipient,
platformCommission: params.platformCommission,
})
this.trackTransferRequirement({
type: params.type,
id: params.id,
recipientPubkey: params.pubkey,
amount: params.amount,
recipientAddress: params.recipient,
})
}
private logTransferError(params: { message: string; id: string; pubkey: string; error: unknown }): void {
console.error(params.message, {
id: params.id,
pubkey: params.pubkey,
error: params.error instanceof Error ? params.error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
}
/**
* Track transfer requirement for later processing
* In production, this would be stored in a database or queue
*/
private trackTransferRequirement(
params: {
type: 'article' | 'review'
id: string
recipientPubkey: string
amount: number
recipientAddress: string
}
): void {
// In production, this would:
// 1. Store in a database/queue for processing
// 2. Trigger automatic transfer via platform's Lightning node
// 3. Update tracking when transfer is complete
console.warn('Transfer requirement tracked', {
type: params.type,
id: params.id,
recipientPubkey: params.recipientPubkey,
amount: params.amount,
recipientAddress: params.recipientAddress,
timestamp: new Date().toISOString(),
})
}
}
export const automaticTransferService = new AutomaticTransferService()