350 lines
11 KiB
TypeScript
350 lines
11 KiB
TypeScript
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<string> = 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<VerificationResult> {
|
|
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<VerificationResult> {
|
|
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<void> {
|
|
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<DeliverTxResponse> {
|
|
// 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 }; |