UNPKG

svm-pay

Version:

A payment solution for SVM networks (Solana, Sonic SVM, Eclipse, s00n)

475 lines 17.4 kB
"use strict"; /** * SVM-Pay Transaction Request Handler * * This file implements the handler for transaction requests in the SVM-Pay protocol * with dynamic payment status handling by ID and type. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionRequestHandler = void 0; const types_1 = require("./types"); /** * Simple async mutex implementation for concurrency safety */ class AsyncMutex { constructor() { this.locked = false; this.waitQueue = []; } async acquire() { return new Promise((resolve) => { if (!this.locked) { this.locked = true; resolve(); } else { this.waitQueue.push(resolve); } }); } release() { if (this.waitQueue.length > 0) { const next = this.waitQueue.shift(); if (next) next(); } else { this.locked = false; } } async withLock(fn) { await this.acquire(); try { return await fn(); } finally { this.release(); } } } /** * In-memory payment store implementation * * ⚠️ CONCURRENCY & SCALING LIMITATIONS: * This in-memory store includes basic concurrency safety via async mutex but is still * NOT suitable for production use beyond demos due to: * * 1. LIMITED CONCURRENCY: Basic mutex prevents corruption but reduces throughput * 2. NO PERSISTENCE: Data is lost when the process restarts * 3. NO SCALING: Limited by single-process memory constraints * 4. NO DISTRIBUTION: Cannot share data across multiple instances * * FOR PRODUCTION USE, REPLACE WITH: * - Database-backed store (PostgreSQL, MongoDB, etc.) * - Distributed cache with persistence (Redis with AOF/RDB) * - Transaction-safe storage with proper locking mechanisms * - Microservice architecture with dedicated payment storage service */ class MemoryPaymentStore { constructor() { this.payments = new Map(); this.mutex = new AsyncMutex(); } async save(record) { await this.mutex.withLock(async () => { this.payments.set(record.id, { ...record }); }); } async load(id) { return this.mutex.withLock(async () => { const record = this.payments.get(id); return record ? { ...record } : null; }); } async loadByType(type) { return this.mutex.withLock(async () => { return Array.from(this.payments.values()) .filter(record => record.request.type === type) .map(record => ({ ...record })); }); } async update(id, updates) { return this.mutex.withLock(async () => { const existing = this.payments.get(id); if (!existing) { throw new Error(`Payment record not found: ${id}`); } const updated = { ...existing, ...updates, updatedAt: Date.now() }; this.payments.set(id, updated); return { ...updated }; }); } async delete(id) { return this.mutex.withLock(async () => { return this.payments.delete(id); }); } } /** * Payment ID parser for extracting type and network information */ class PaymentIdParser { /** * Parse payment ID to extract metadata with robust validation */ static parse(paymentId) { if (!paymentId || typeof paymentId !== 'string') { throw new Error('Invalid payment ID: must be a non-empty string'); } // Test against expected format: {type}_{network}_{timestamp}_{random} const match = this.ID_PATTERN.exec(paymentId); if (match) { const [, typeStr, networkStr, timestampStr] = match; // Validate type const type = typeStr.toUpperCase(); if (!Object.values(types_1.RequestType).includes(type)) { console.warn(`Warning: Unknown request type '${typeStr}' in payment ID ${paymentId}`); } // Validate network const network = networkStr.toUpperCase(); if (!Object.values(types_1.SVMNetwork).includes(network)) { console.warn(`Warning: Unknown network '${networkStr}' in payment ID ${paymentId}`); } // Validate timestamp const timestamp = parseInt(timestampStr, 10); if (isNaN(timestamp) || timestamp <= 0) { throw new Error(`Invalid timestamp in payment ID: ${paymentId}`); } return { id: paymentId, type, network, timestamp }; } // Log warning for malformed IDs but don't throw to maintain backward compatibility console.warn(`Warning: Payment ID '${paymentId}' does not match expected format. Using as simple ID.`); console.warn('Expected format: {type}_{network}_{timestamp}_{random}'); // Fallback for simple IDs return { id: paymentId }; } /** * Generate a structured payment ID with validation */ static generate(type, network) { if (!type || !network) { throw new Error('Type and network are required to generate payment ID'); } if (!Object.values(types_1.RequestType).includes(type)) { throw new Error(`Invalid request type: ${type}`); } if (!Object.values(types_1.SVMNetwork).includes(network)) { throw new Error(`Invalid network: ${network}`); } const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `${type.toLowerCase()}_${network.toLowerCase()}_${timestamp}_${random}`; } /** * Validate payment ID format without parsing */ static isValid(paymentId) { if (!paymentId || typeof paymentId !== 'string') { return false; } return this.ID_PATTERN.test(paymentId); } } // Regex pattern for valid payment ID format PaymentIdParser.ID_PATTERN = /^([a-zA-Z]+)_([a-zA-Z0-9]+)_(\d+)_([a-zA-Z0-9]+)$/; /** * Handler for transaction requests with dynamic status checking */ class TransactionRequestHandler { /** * Create a new TransactionRequestHandler * * @param networkAdapters Map of network adapters for each supported network * @param paymentStore Optional custom payment store (defaults to in-memory) */ constructor(networkAdapters, paymentStore) { this.networkAdapters = networkAdapters; this.paymentStore = paymentStore || new MemoryPaymentStore(); // Warn about using in-memory store for anything beyond demos if (!paymentStore) { console.warn('\n⚠️ SCALING WARNING: Using in-memory payment store (demo only)'); console.warn(' For production use, provide a persistent PaymentStore implementation'); console.warn(' that supports concurrency, persistence, and proper scaling.\n'); } } /** * Process a transaction request * * @param request The transaction request to process * @returns A payment record for the processed request */ async processRequest(request) { // Get the network adapter for this request const adapter = this.networkAdapters.get(request.network); if (!adapter) { throw new Error(`No adapter available for network: ${request.network}`); } // Generate a structured ID for this payment const id = PaymentIdParser.generate(request.type, request.network); // Create a payment record const record = { id, request, status: types_1.PaymentStatus.CREATED, createdAt: Date.now(), updatedAt: Date.now() }; try { // Save initial record await this.paymentStore.save(record); // Fetch the transaction from the provided link const _transaction = await adapter.fetchTransaction(request); // Update record with pending status const updatedRecord = await this.paymentStore.update(id, { status: types_1.PaymentStatus.PENDING }); return updatedRecord; } catch (error) { // Update record with error const errorRecord = await this.paymentStore.update(id, { status: types_1.PaymentStatus.FAILED, error: error.message }); return errorRecord; } } /** * Submit a signed transaction for a transaction request * * @param paymentId The ID of the payment record * @param transaction The transaction to submit * @param signature The signature for the transaction * @returns The updated payment record */ async submitTransaction(paymentId, transaction, signature) { // Load the payment record const record = await this.paymentStore.load(paymentId); if (!record) { throw new Error(`Payment record not found: ${paymentId}`); } try { // Get the network adapter for this request const adapter = this.networkAdapters.get(record.request.network); if (!adapter) { throw new Error(`No adapter available for network: ${record.request.network}`); } // Submit the transaction const txSignature = await adapter.submitTransaction(transaction, signature); // Update the payment record const updatedRecord = await this.paymentStore.update(paymentId, { status: types_1.PaymentStatus.CONFIRMED, signature: txSignature }); return updatedRecord; } catch (error) { // Update record with error const errorRecord = await this.paymentStore.update(paymentId, { status: types_1.PaymentStatus.FAILED, error: error.message }); return errorRecord; } } /** * Check the status of a payment dynamically by ID * * @param paymentId The ID of the payment to check * @returns The updated payment record */ async checkStatus(paymentId) { // Load the payment record const record = await this.paymentStore.load(paymentId); if (!record) { throw new Error(`Payment record not found: ${paymentId}`); } // Parse payment ID to determine type and handle accordingly const parsedId = PaymentIdParser.parse(paymentId); if (parsedId.type) { return this.checkStatusByType(paymentId, parsedId.type); } // Fallback to generic status check return this.checkGenericStatus(record); } /** * Check status for multiple payment types dynamically * * @param paymentId The ID of the payment to check * @param requestType The type of request (transfer, transaction, etc.) * @returns The updated payment record */ async checkStatusByType(paymentId, requestType) { const record = await this.paymentStore.load(paymentId); if (!record) { throw new Error(`Payment record not found: ${paymentId}`); } // Handle different request types with specialized logic switch (requestType) { case types_1.RequestType.TRANSFER: return this.checkTransferStatus(record); case types_1.RequestType.TRANSACTION: return this.checkTransactionStatus(record); default: throw new Error(`Unsupported request type: ${requestType}`); } } /** * Check status specifically for transfer requests * * @param record The payment record to check * @returns The updated payment record */ async checkTransferStatus(record) { if (record.request.type !== types_1.RequestType.TRANSFER) { throw new Error('Invalid request type for transfer status check'); } // Transfer-specific validation const transferRequest = record.request; // Validate transfer amount const amount = parseFloat(transferRequest.amount); if (amount <= 0) { return this.paymentStore.update(record.id, { status: types_1.PaymentStatus.FAILED, error: 'Invalid transfer amount' }); } return this.checkGenericStatus(record); } /** * Check status specifically for transaction requests * * @param record The payment record to check * @returns The updated payment record */ async checkTransactionStatus(record) { if (record.request.type !== types_1.RequestType.TRANSACTION) { throw new Error('Invalid request type for transaction status check'); } // Transaction-specific validation const transactionRequest = record.request; // Validate transaction link if (!transactionRequest.link || !transactionRequest.link.startsWith('http')) { return this.paymentStore.update(record.id, { status: types_1.PaymentStatus.FAILED, error: 'Invalid transaction link' }); } return this.checkGenericStatus(record); } /** * Generic status check using network adapter * * @param record The payment record to check * @returns The updated payment record */ async checkGenericStatus(record) { try { // Get the network adapter for this request const adapter = this.networkAdapters.get(record.request.network); if (!adapter) { throw new Error(`No adapter available for network: ${record.request.network}`); } // Only check if we have a signature if (!record.signature) { return record; // No signature to check yet } // Check the transaction status const status = await adapter.checkTransactionStatus(record.signature); // Update the payment record return this.paymentStore.update(record.id, { status }); } catch (error) { // Update record with error return this.paymentStore.update(record.id, { status: types_1.PaymentStatus.FAILED, error: error.message }); } } /** * Get all payments by type * * @param requestType The type of requests to retrieve * @returns Array of payment records */ async getPaymentsByType(requestType) { return this.paymentStore.loadByType(requestType); } /** * Get payment record by ID * * @param paymentId The ID of the payment to retrieve * @returns The payment record or null if not found */ async getPayment(paymentId) { return this.paymentStore.load(paymentId); } /** * Delete a payment record * * @param paymentId The ID of the payment to delete * @returns True if deleted, false if not found */ async deletePayment(paymentId) { return this.paymentStore.delete(paymentId); } /** * Bulk status check for multiple payments * * @param paymentIds Array of payment IDs to check * @returns Array of updated payment records */ async bulkCheckStatus(paymentIds) { const results = []; for (const paymentId of paymentIds) { try { const updatedRecord = await this.checkStatus(paymentId); results.push(updatedRecord); } catch (error) { // Log error but continue with other payments console.error(`Error checking status for payment ${paymentId}:`, error); } } return results; } /** * Get payment statistics * * @returns Statistics about payments */ async getPaymentStats() { const allTransfers = await this.paymentStore.loadByType(types_1.RequestType.TRANSFER); const allTransactions = await this.paymentStore.loadByType(types_1.RequestType.TRANSACTION); const allPayments = [...allTransfers, ...allTransactions]; const stats = { total: allPayments.length, byStatus: {}, byType: {}, byNetwork: {} }; // Initialize counters Object.values(types_1.PaymentStatus).forEach(status => stats.byStatus[status] = 0); Object.values(types_1.RequestType).forEach(type => stats.byType[type] = 0); Object.values(types_1.SVMNetwork).forEach(network => stats.byNetwork[network] = 0); // Count occurrences allPayments.forEach(payment => { stats.byStatus[payment.status]++; stats.byType[payment.request.type]++; stats.byNetwork[payment.request.network]++; }); return stats; } } exports.TransactionRequestHandler = TransactionRequestHandler; //# sourceMappingURL=transaction-handler.js.map