import { Registry, Account, parseGasAndFees, DEFAULT_GAS_ESTIMATION_MULTIPLIER } from '@cerc-io/registry-sdk'; import { DeliverTxResponse } from '@cosmjs/stargate'; import { Config } from './config.js'; import { verifyAtomPayment } from './atomPayments.js'; import { RegHelper } from './deployments.js'; import yaml from 'js-yaml'; import fs from 'fs'; interface ChainConfig { name: string; rpcUrl: string; chainId: string; minAmount: string; paymentAddress: string; } interface VerificationResult { valid: boolean; reason?: string; amount?: string; sender?: string; chainUsed?: string; } class PaymentProcessor { private regHelper: RegHelper; private processedHashes: Set = new Set(); private checkInterval: number = 30000; // 30 seconds private lastProcessedTime: Date = new Date(); constructor() { this.regHelper = new RegHelper(); // Only process requests from the last 10 minutes on startup this.lastProcessedTime = new Date(Date.now() - 10 * 60 * 1000); } /** * Start the payment processor service */ start(): void { console.log('Payment processor started'); this.initializeChains(); this.processPayments(); } /** * Main processing loop */ private async processPayments(): Promise { try { await this.processNewRequests(); } catch (error) { console.error('Error in payment processing:', error); } // Schedule next run setTimeout(() => this.processPayments(), this.checkInterval); } /** * Process new ApplicationDeploymentRequest records (only recent ones) */ private async processNewRequests(): Promise { try { // Query for all ApplicationDeploymentRequest records const allRequests = await this.regHelper.queryRecords({ type: 'ApplicationDeploymentRequest', }); // Filter to only process recent requests (created after lastProcessedTime) const recentRequests = allRequests.filter(request => { const createTime = new Date(request.createTime); return createTime > this.lastProcessedTime; }); console.log(`Found ${allRequests.length} total deployment requests, processing ${recentRequests.length} recent ones`); for (const request of recentRequests) { await this.processRequest(request); } // Update lastProcessedTime to now this.lastProcessedTime = new Date(); } catch (error) { console.error('Error querying deployment requests:', error); } } /** * Process individual deployment request */ private async processRequest(request: any): Promise { const requestId = request.id; const paymentHash = request.attributes.payment; // Skip if no payment hash if (!paymentHash) { return; } // Skip if already processed if (this.processedHashes.has(paymentHash)) { return; } // Skip if request already has a deployment (already successfully processed) const existingDeployments = await this.regHelper.queryRecords({ type: 'ApplicationDeploymentRecord', request: requestId }); if (existingDeployments.length > 0) { console.log(`Skipping request ${requestId} - already has deployment`); this.processedHashes.add(paymentHash); return; } console.log(`Processing request ${requestId} with payment hash ${paymentHash}`); try { // First check if this is an LNT payment const lntVerification = await this.verifyLNTPayment(paymentHash, request); if (lntVerification.valid) { console.log(`LNT payment verified for request ${requestId}`); this.processedHashes.add(paymentHash); return; } // If LNT verification failed, try ATOM verification const atomVerification = await this.verifyATOMPayment(paymentHash, request); if (atomVerification.valid) { console.log(`ATOM payment verified for request ${requestId}, converting to LNT`); // Send LNT from deployer account to deployer account await this.sendLNTToDeployer(request); this.processedHashes.add(paymentHash); return; } // If both failed, log and continue (don't mark as processed to allow retries) console.log(`Payment verification failed for request ${requestId}: LNT: ${lntVerification.reason}, ATOM: ${atomVerification.reason}`); } catch (error) { console.error(`Error processing request ${requestId}:`, error); } } /** * Verify LNT payment - delegated to stack-orchestrator * Since stack-orchestrator already handles LNT verification efficiently, * we only need to handle ATOM verification here */ private async verifyLNTPayment(_txHash: string, _request: any): Promise { try { // LNT verification is handled by stack-orchestrator // We assume if it's not an ATOM payment, stack-orchestrator will handle it return { valid: false, reason: 'LNT verification delegated to stack-orchestrator' }; } catch (error) { return { valid: false, reason: `LNT verification error: ${error.message}` }; } } /** * Verify ATOM payment with 5-minute time window */ private async verifyATOMPayment(txHash: string, request: any): Promise { try { // Skip if ATOM payment not configured if (!Config.ATOM_PAYMENT_ADDRESS || !Config.COSMOS_RPC_ENDPOINT) { return { valid: false, reason: 'ATOM payment verification not configured' }; } // Get the deployer record to check ATOM payment config const deployer = await this.regHelper.getRecord(request.attributes.deployer); if (!deployer) { return { valid: false, reason: 'Deployer record not found' }; } const atomPaymentAddress = deployer.attributes.atomPaymentAddress || Config.ATOM_PAYMENT_ADDRESS; const minAtomPayment = deployer.attributes.minimumAtomPayment || Config.MIN_ATOM_PAYMENT; if (!atomPaymentAddress) { return { valid: false, reason: 'ATOM payment address not configured for this deployer' }; } // Use existing ATOM verification with 5-minute time window const result = await verifyAtomPayment(txHash, minAtomPayment, false); if (result.valid) { return { valid: true, amount: result.amount, sender: result.sender, chainUsed: 'ATOM' }; } else { return { valid: false, reason: result.reason || 'ATOM payment verification failed' }; } } catch (error) { return { valid: false, reason: `ATOM verification error: ${error.message}` }; } } /** * Send LNT from deployer account to deployer account to satisfy stack-orchestrator */ private async sendLNTToDeployer(request: any): Promise { try { // Get the deployer record const deployer = await this.regHelper.getRecord(request.attributes.deployer); if (!deployer) { throw new Error('Deployer record not found'); } const paymentAddress = deployer.attributes.paymentAddress; const minPayment = deployer.attributes.minimumPayment || '0alnt'; // Extract amount number from string like "100alnt" const amount = minPayment.replace(/[^0-9]/g, '') || '0'; console.log(`Sending ${minPayment} LNT from deployer to deployer (${paymentAddress}) for request ${request.id}`); // Send LNT tokens using proper Registry SDK methods const result = await this.sendLNTTokens(paymentAddress, amount); console.log(`LNT transfer successful: ${result.transactionHash}`); } catch (error) { console.error(`Error sending LNT for request ${request.id}:`, error); throw error; } } /** * Send LNT tokens using Registry SDK */ private async sendLNTTokens(toAddress: string, amount: string): Promise { // Load laconic config to get registry details and private key const configData = yaml.load(fs.readFileSync(Config.LACONIC_CONFIG, 'utf8')) as any; const registryConfig = configData.services?.registry; if (!registryConfig) { throw new Error('Registry configuration not found'); } // Get private key from environment or config const privateKey = process.env.REGISTRY_USER_KEY || registryConfig.privateKey; if (!privateKey) { throw new Error('Registry private key not configured'); } // Create account const account = new Account(Buffer.from(privateKey, 'hex')); await account.init(); // Create registry instance const registry = new Registry( registryConfig.gqlEndpoint, registryConfig.rpcEndpoint, { chainId: registryConfig.chainId } ); // Get laconic client const laconicClient = await registry.getLaconicClient(account); // Calculate fees const fee = registryConfig.gas && registryConfig.fees ? parseGasAndFees(registryConfig.gas, registryConfig.fees) : DEFAULT_GAS_ESTIMATION_MULTIPLIER; // Send tokens from deployer account to deployer account const txResponse: DeliverTxResponse = await laconicClient.sendTokens( account.address, // from (deployer account) toAddress, // to (same deployer account) [{ denom: 'alnt', // LNT token denomination amount // amount as string }], fee ); return txResponse; } /** * Get chain configurations for future extensibility */ private getChainConfigs(): ChainConfig[] { const configs: ChainConfig[] = []; // ATOM configuration if (Config.ATOM_PAYMENT_ADDRESS && Config.COSMOS_RPC_ENDPOINT) { configs.push({ name: 'ATOM', rpcUrl: Config.COSMOS_RPC_ENDPOINT, chainId: 'cosmoshub-4', // Could be made configurable minAmount: Config.MIN_ATOM_PAYMENT, paymentAddress: Config.ATOM_PAYMENT_ADDRESS, }); } // Future chains can be added here // if (Config.OSMO_PAYMENT_ADDRESS && Config.OSMO_RPC_ENDPOINT) { // configs.push({ // name: 'OSMO', // rpcUrl: Config.OSMO_RPC_ENDPOINT, // chainId: 'osmosis-1', // minAmount: Config.MIN_OSMO_PAYMENT, // paymentAddress: Config.OSMO_PAYMENT_ADDRESS, // }); // } console.log(`Configured ${configs.length} payment chains`); return configs; } /** * Initialize chain configurations on startup */ private initializeChains(): void { const configs = this.getChainConfigs(); console.log(`Payment processor initialized with ${configs.length} supported chains`); } } export { PaymentProcessor };