Files
securebit-chat/src/session/PayPerSessionManager.js
2025-08-11 20:52:14 -04:00

588 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class PayPerSessionManager {
constructor(config = {}) {
this.sessionPrices = {
free: { sats: 0, hours: 1/60, usd: 0.00 },
basic: { sats: 500, hours: 1, usd: 0.20 },
premium: { sats: 1000, hours: 4, usd: 0.40 },
extended: { sats: 2000, hours: 24, usd: 0.80 }
};
this.currentSession = null;
this.sessionTimer = null;
this.onSessionExpired = null;
this.staticLightningAddress = "dullpastry62@walletofsatoshi.com";
// Конфигурация для LNbits (ваши реальные данные)
this.verificationConfig = {
method: config.method || 'lnbits',
apiUrl: config.apiUrl || 'https://demo.lnbits.com',
apiKey: config.apiKey || '623515641d2e4ebcb1d5992d6d78419c', // Ваш Invoice/read ключ
walletId: config.walletId || 'bcd00f561c7b46b4a7b118f069e68997',
// Дополнительные настройки для демо
isDemo: true,
demoTimeout: 30000, // 30 секунд для демо
retryAttempts: 3
};
}
hasActiveSession() {
if (!this.currentSession) return false;
return Date.now() < this.currentSession.expiresAt;
}
createInvoice(sessionType) {
const pricing = this.sessionPrices[sessionType];
if (!pricing) throw new Error('Invalid session type');
return {
amount: pricing.sats,
memo: `LockBit.chat ${sessionType} session (${pricing.hours}h)`,
sessionType: sessionType,
timestamp: Date.now(),
paymentHash: Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0')).join(''),
lightningAddress: this.staticLightningAddress
};
}
// Создание реального Lightning инвойса через LNbits
async createLightningInvoice(sessionType) {
const pricing = this.sessionPrices[sessionType];
if (!pricing) throw new Error('Invalid session type');
try {
console.log(`Creating ${sessionType} invoice for ${pricing.sats} sats...`);
// Проверяем доступность API
const healthCheck = await fetch(`${this.verificationConfig.apiUrl}/api/v1/health`, {
method: 'GET',
headers: {
'X-Api-Key': this.verificationConfig.apiKey
}
});
if (!healthCheck.ok) {
throw new Error(`LNbits API недоступен: ${healthCheck.status}`);
}
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments`, {
method: 'POST',
headers: {
'X-Api-Key': this.verificationConfig.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
out: false, // incoming payment
amount: pricing.sats,
memo: `LockBit.chat ${sessionType} session (${pricing.hours}h)`,
unit: 'sat',
expiry: this.verificationConfig.isDemo ? 300 : 900 // 5 минут для демо, 15 для продакшена
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('LNbits API response:', errorText);
throw new Error(`LNbits API error ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('✅ Lightning invoice created successfully!', data);
return {
paymentRequest: data.bolt11 || data.payment_request, // BOLT11 invoice для QR кода
paymentHash: data.payment_hash,
checkingId: data.checking_id || data.payment_hash, // Для проверки статуса
amount: data.amount || pricing.sats,
sessionType: sessionType,
createdAt: Date.now(),
expiresAt: Date.now() + (this.verificationConfig.isDemo ? 5 * 60 * 1000 : 15 * 60 * 1000), // 5 минут для демо
description: data.description || data.memo || `LockBit.chat ${sessionType} session`,
lnurl: data.lnurl || null,
memo: data.memo || `LockBit.chat ${sessionType} session`,
bolt11: data.bolt11 || data.payment_request,
// Дополнительные поля для совместимости
payment_request: data.bolt11 || data.payment_request,
checking_id: data.checking_id || data.payment_hash
};
} catch (error) {
console.error('❌ Error creating Lightning invoice:', error);
// Для демо режима создаем фиктивный инвойс
if (this.verificationConfig.isDemo && error.message.includes('API')) {
console.log('🔄 Creating demo invoice for testing...');
return this.createDemoInvoice(sessionType);
}
throw error;
}
}
// Создание демо инвойса для тестирования
createDemoInvoice(sessionType) {
const pricing = this.sessionPrices[sessionType];
const demoHash = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0')).join('');
return {
paymentRequest: `lntb${pricing.sats}1p${demoHash}...`, // Фиктивный BOLT11
paymentHash: demoHash,
checkingId: demoHash,
amount: pricing.sats,
sessionType: sessionType,
createdAt: Date.now(),
expiresAt: Date.now() + (5 * 60 * 1000), // 5 минут
description: `LockBit.chat ${sessionType} session (DEMO)`,
isDemo: true
};
}
// Проверка статуса платежа через LNbits
async checkPaymentStatus(checkingId) {
try {
console.log(`🔍 Checking payment status for: ${checkingId}`);
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments/${checkingId}`, {
method: 'GET',
headers: {
'X-Api-Key': this.verificationConfig.apiKey,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('Payment status check failed:', errorText);
throw new Error(`Payment check failed: ${response.status} - ${errorText}`);
}
const data = await response.json();
console.log('📊 Payment status response:', data);
return {
paid: data.paid || false,
preimage: data.preimage || null,
details: data.details || {},
amount: data.amount || 0,
fee: data.fee || 0,
timestamp: data.timestamp || Date.now(),
bolt11: data.bolt11 || null
};
} catch (error) {
console.error('❌ Error checking payment status:', error);
// Для демо режима возвращаем фиктивный статус
if (this.verificationConfig.isDemo && error.message.includes('API')) {
console.log('🔄 Returning demo payment status...');
return {
paid: false,
preimage: null,
details: { demo: true },
amount: 0,
fee: 0,
timestamp: Date.now()
};
}
throw error;
}
}
// Метод 1: Верификация через LNbits API
async verifyPaymentLNbits(preimage, paymentHash) {
try {
console.log(`🔐 Verifying payment via LNbits: ${paymentHash}`);
if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) {
throw new Error('LNbits API configuration missing');
}
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments/${paymentHash}`, {
method: 'GET',
headers: {
'X-Api-Key': this.verificationConfig.apiKey,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('LNbits verification failed:', errorText);
throw new Error(`API request failed: ${response.status} - ${errorText}`);
}
const paymentData = await response.json();
console.log('📋 Payment verification data:', paymentData);
// Проверяем статус платежа
if (paymentData.paid && paymentData.preimage === preimage) {
console.log('✅ Payment verified successfully via LNbits');
return {
verified: true,
amount: paymentData.amount,
fee: paymentData.fee || 0,
timestamp: paymentData.timestamp || Date.now(),
method: 'lnbits'
};
}
console.log('❌ Payment verification failed: paid=', paymentData.paid, 'preimage match=', paymentData.preimage === preimage);
return {
verified: false,
reason: 'Payment not paid or preimage mismatch',
method: 'lnbits'
};
} catch (error) {
console.error('❌ LNbits payment verification failed:', error);
// Для демо режима возвращаем успешную верификацию
if (this.verificationConfig.isDemo && error.message.includes('API')) {
console.log('🔄 Demo payment verification successful');
return {
verified: true,
amount: 0,
fee: 0,
timestamp: Date.now(),
method: 'demo'
};
}
return {
verified: false,
reason: error.message,
method: 'lnbits'
};
}
}
// Метод 2: Верификация через LND REST API
async verifyPaymentLND(preimage, paymentHash) {
try {
if (!this.verificationConfig.nodeUrl || !this.verificationConfig.macaroon) {
throw new Error('LND configuration missing');
}
const response = await fetch(`${this.verificationConfig.nodeUrl}/v1/invoice/${paymentHash}`, {
method: 'GET',
headers: {
'Grpc-Metadata-macaroon': this.verificationConfig.macaroon,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`LND API request failed: ${response.status}`);
}
const invoiceData = await response.json();
// Проверяем, что инвойс оплачен и preimage совпадает
if (invoiceData.settled && invoiceData.r_preimage === preimage) {
return true;
}
return false;
} catch (error) {
console.error('LND payment verification failed:', error);
return false;
}
}
// Метод 3: Верификация через Core Lightning (CLN)
async verifyPaymentCLN(preimage, paymentHash) {
try {
if (!this.verificationConfig.nodeUrl) {
throw new Error('CLN configuration missing');
}
const response = await fetch(`${this.verificationConfig.nodeUrl}/v1/listinvoices`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
payment_hash: paymentHash
})
});
if (!response.ok) {
throw new Error(`CLN API request failed: ${response.status}`);
}
const data = await response.json();
if (data.invoices && data.invoices.length > 0) {
const invoice = data.invoices[0];
if (invoice.status === 'paid' && invoice.payment_preimage === preimage) {
return true;
}
}
return false;
} catch (error) {
console.error('CLN payment verification failed:', error);
return false;
}
}
// Метод 4: Верификация через Wallet of Satoshi API (если доступен)
async verifyPaymentWOS(preimage, paymentHash) {
try {
// Wallet of Satoshi обычно не предоставляет публичного API
// Этот метод для примера структуры
console.warn('Wallet of Satoshi API verification not implemented');
return false;
} catch (error) {
console.error('WOS payment verification failed:', error);
return false;
}
}
// Метод 5: Верификация через BTCPay Server
async verifyPaymentBTCPay(preimage, paymentHash) {
try {
if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) {
throw new Error('BTCPay Server configuration missing');
}
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/invoices/${paymentHash}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.verificationConfig.apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`BTCPay API request failed: ${response.status}`);
}
const invoiceData = await response.json();
if (invoiceData.status === 'Settled' && invoiceData.payment && invoiceData.payment.preimage === preimage) {
return true;
}
return false;
} catch (error) {
console.error('BTCPay payment verification failed:', error);
return false;
}
}
// Криптографическая верификация preimage
async verifyCryptographically(preimage, paymentHash) {
try {
// Преобразуем preimage в байты
const preimageBytes = new Uint8Array(preimage.match(/.{2}/g).map(byte => parseInt(byte, 16)));
// Вычисляем SHA256 от preimage
const hashBuffer = await crypto.subtle.digest('SHA-256', preimageBytes);
const computedHash = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0')).join('');
// Сравниваем с payment_hash
return computedHash === paymentHash;
} catch (error) {
console.error('Cryptographic verification failed:', error);
return false;
}
}
// Основной метод верификации платежа
async verifyPayment(preimage, paymentHash) {
console.log(`🔐 Verifying payment: preimage=${preimage}, hash=${paymentHash}`);
// Базовые проверки формата
if (!preimage || preimage.length !== 64) {
console.log('❌ Invalid preimage length');
return { verified: false, reason: 'Invalid preimage length' };
}
if (!/^[0-9a-fA-F]{64}$/.test(preimage)) {
console.log('❌ Invalid preimage format');
return { verified: false, reason: 'Invalid preimage format' };
}
// Для бесплатных сессий
if (preimage === '0'.repeat(64)) {
console.log('✅ Free session preimage accepted');
return { verified: true, method: 'free' };
}
// Проверяем, что preimage не является заглушкой
const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64)];
if (dummyPreimages.includes(preimage)) {
console.log('❌ Dummy preimage detected');
return { verified: false, reason: 'Dummy preimage detected' };
}
try {
// Сначала проверяем криптографически
const cryptoValid = await this.verifyCryptographically(preimage, paymentHash);
if (!cryptoValid) {
console.log('❌ Cryptographic verification failed');
return { verified: false, reason: 'Cryptographic verification failed' };
}
console.log('✅ Cryptographic verification passed');
// Затем проверяем через выбранный метод
switch (this.verificationConfig.method) {
case 'lnbits':
const lnbitsResult = await this.verifyPaymentLNbits(preimage, paymentHash);
return lnbitsResult.verified ? lnbitsResult : { verified: false, reason: 'LNbits verification failed' };
case 'lnd':
const lndResult = await this.verifyPaymentLND(preimage, paymentHash);
return lndResult ? { verified: true, method: 'lnd' } : { verified: false, reason: 'LND verification failed' };
case 'cln':
const clnResult = await this.verifyPaymentCLN(preimage, paymentHash);
return clnResult ? { verified: true, method: 'cln' } : { verified: false, reason: 'CLN verification failed' };
case 'btcpay':
const btcpayResult = await this.verifyPaymentBTCPay(preimage, paymentHash);
return btcpayResult ? { verified: true, method: 'btcpay' } : { verified: false, reason: 'BTCPay verification failed' };
case 'walletofsatoshi':
const wosResult = await this.verifyPaymentWOS(preimage, paymentHash);
return wosResult ? { verified: true, method: 'wos' } : { verified: false, reason: 'WOS verification failed' };
default:
console.warn('Unknown verification method, using crypto-only verification');
return { verified: cryptoValid, method: 'crypto-only' };
}
} catch (error) {
console.error('❌ Payment verification failed:', error);
return { verified: false, reason: error.message };
}
}
// Остальные методы остаются без изменений...
activateSession(sessionType, preimage) {
// Очистка предыдущей сессии
this.cleanup();
const pricing = this.sessionPrices[sessionType];
const now = Date.now();
const expiresAt = now + (pricing.hours * 60 * 60 * 1000);
this.currentSession = {
type: sessionType,
startTime: now,
expiresAt: expiresAt,
preimage: preimage
};
this.startSessionTimer();
return this.currentSession;
}
startSessionTimer() {
if (this.sessionTimer) {
clearInterval(this.sessionTimer);
}
this.sessionTimer = setInterval(() => {
if (!this.hasActiveSession()) {
this.expireSession();
}
}, 60000);
}
expireSession() {
if (this.sessionTimer) {
clearInterval(this.sessionTimer);
}
this.currentSession = null;
if (this.onSessionExpired) {
this.onSessionExpired();
}
}
getTimeLeft() {
if (!this.currentSession) return 0;
return Math.max(0, this.currentSession.expiresAt - Date.now());
}
forceUpdateTimer() {
if (this.currentSession) {
const timeLeft = this.getTimeLeft();
console.log('Timer updated:', timeLeft, 'ms left');
return timeLeft;
}
return 0;
}
cleanup() {
if (this.sessionTimer) {
clearInterval(this.sessionTimer);
}
this.currentSession = null;
}
resetSession() {
if (this.sessionTimer) {
clearInterval(this.sessionTimer);
}
this.currentSession = null;
console.log('Session reset due to failed verification');
}
canActivateSession() {
return !this.hasActiveSession() && !this.currentSession;
}
async safeActivateSession(sessionType, preimage, paymentHash) {
try {
console.log(`🚀 Activating session: ${sessionType} with preimage: ${preimage}`);
if (!sessionType || !preimage) {
console.warn('❌ Session activation failed: missing sessionType or preimage');
return { success: false, reason: 'Missing sessionType or preimage' };
}
if (!this.sessionPrices[sessionType]) {
console.warn('❌ Session activation failed: invalid session type');
return { success: false, reason: 'Invalid session type' };
}
// Верифицируем платеж
const verificationResult = await this.verifyPayment(preimage, paymentHash);
if (verificationResult.verified) {
this.activateSession(sessionType, preimage);
console.log(`✅ Session activated successfully: ${sessionType} via ${verificationResult.method}`);
return {
success: true,
sessionType: sessionType,
method: verificationResult.method,
details: verificationResult,
timeLeft: this.getTimeLeft()
};
} else {
console.log('❌ Payment verification failed:', verificationResult.reason);
return {
success: false,
reason: verificationResult.reason,
method: verificationResult.method
};
}
} catch (error) {
console.error('❌ Session activation failed:', error);
return {
success: false,
reason: error.message,
method: 'error'
};
}
}
}
export { PayPerSessionManager };