This commit is contained in:
zramsay 2025-06-26 10:45:32 -04:00
parent 429bb62cf1
commit 15187227e9
5 changed files with 408 additions and 75 deletions

View File

@ -29,7 +29,8 @@ setInterval(() => {
export async function verifyAtomPayment(
txHash: string,
requiredAmount = Config.MIN_ATOM_PAYMENT,
markAsUsed = false
markAsUsed = false,
timeWindowMinutes = 5
): Promise<{
valid: boolean,
reason?: string,
@ -100,6 +101,19 @@ export async function verifyAtomPayment(
};
}
// Check transaction timestamp (5-minute window)
const txTimestamp = new Date(txResponse.timestamp);
const now = new Date();
const timeDiffMs = now.getTime() - txTimestamp.getTime();
const timeWindowMs = timeWindowMinutes * 60 * 1000;
if (timeDiffMs > timeWindowMs) {
return {
valid: false,
reason: `Transaction is older than ${timeWindowMinutes} minutes (${Math.round(timeDiffMs / 60000)} minutes old)`
};
}
// Extract the payment details
const tx = response.data.tx;
let foundValidPayment = false;

View File

@ -1,7 +1,7 @@
import yaml from 'js-yaml';
import fs from 'fs';
import path from 'path';
import {Registry} from '@cerc-io/laconic-sdk';
import {Registry} from '@cerc-io/registry-sdk';
const loadConfigFile = (configFilePath: string): any => {
const resolvedFilePath = path.resolve(process.cwd(), configFilePath);

View File

@ -1,5 +1,5 @@
import {getRegistry, Config} from './config.js';
import {Registry} from '@cerc-io/laconic-sdk';
import {Registry} from '@cerc-io/registry-sdk';
import stringify from 'json-stable-stringify';
import {createHash} from 'crypto';
@ -110,44 +110,8 @@ export class RegHelper {
return [...records];
}
async verifyAtomPaymentForRequest(request: any, deployer: any) {
// Skip if no ATOM payment configuration
if (!Config.ATOM_PAYMENT_ADDRESS || !Config.COSMOS_RPC_ENDPOINT) {
console.log(`ATOM payment verification skipped: Missing configuration`);
return false;
}
// Skip if no payment transaction
if (!request.attributes.payment) {
console.log(`Request ${request.id}: No payment transaction hash provided`);
return false;
}
try {
// Check if deployer has atom payment config
const atomPaymentAddress = deployer.attributes.atomPaymentAddress || Config.ATOM_PAYMENT_ADDRESS;
const minAtomPayment = deployer.attributes.minimumAtomPayment || Config.MIN_ATOM_PAYMENT;
if (!atomPaymentAddress) {
console.log(`Request ${request.id}: No ATOM payment address configured`);
return false;
}
console.log(`Verifying ATOM payment for request ${request.id} - tx: ${request.attributes.payment}`);
const verification = await verifyAtomPayment(request.attributes.payment, minAtomPayment);
if (verification.valid) {
console.log(`ATOM payment verified for request ${request.id} - Amount: ${verification.amount}`);
return true;
} else {
console.log(`ATOM payment verification failed for request ${request.id}: ${verification.reason}`);
return false;
}
} catch (error) {
console.error(`Error verifying ATOM payment for request ${request.id}:`, error);
return false;
}
}
// ATOM payment verification moved to background payment processor
// This method is no longer used to avoid blocking the status API
async deploymentRequestStatus(requestId?: string) {
const requests: any[] = [];
@ -156,10 +120,8 @@ export class RegHelper {
type: 'ApplicationDeploymentRemovalRequest',
});
// Get all WebappDeployer records to check for atom payment configuration
const deployers = await this.queryRecords({
type: 'WebappDeployer',
});
// WebappDeployer records no longer needed for status display
// Payment verification is handled by background processor
if (requestId) {
const request = await this.getRecordById(requestId);
@ -180,16 +142,6 @@ export class RegHelper {
requests.sort((a, b) => a.createTime === b.createTime ? 0 : a.createTime > b.createTime ? 1 : -1,);
requests.reverse();
// Create a map of deployers by their LRN for quick lookup
const deployersByLRN = new Map<string, any>();
for (const deployer of deployers) {
if (deployer.names && deployer.names.length > 0) {
for (const name of deployer.names) {
deployersByLRN.set(name, deployer);
}
}
}
const deploymentsByRequest = new Map<string, any>();
for (const d of deployments) {
@ -218,24 +170,11 @@ export class RegHelper {
status.app = r.attributes.application;
const hostname = r.attributes.dns ?? generateHostnameForApp(app);
// Mark as ATOM payment verified if applicable
if (r.attributes.payment && !deploymentsByRequest.has(r.id) && !removalsByRequest.has(r.id)) {
// Find the deployer record for this request
const deployerLRN = r.attributes.deployer;
if (deployerLRN && deployersByLRN.has(deployerLRN)) {
const deployer = deployersByLRN.get(deployerLRN);
// Check if this is a valid ATOM payment
if (deployer.attributes.atomPaymentAddress || Config.ATOM_PAYMENT_ADDRESS) {
// Just verify but don't mark as used since this is just for status display
const isValidAtomPayment = await this.verifyAtomPaymentForRequest(r, deployer);
if (isValidAtomPayment) {
// If the payment is valid, mark it in the status
status.atomPaymentVerified = true;
}
}
}
// Payment verification is now handled by the background payment processor
// No longer blocking the status API with external verification calls
if (r.attributes.payment) {
// Just indicate that there is a payment hash, actual verification is done in background
status.atomPaymentVerified = false; // Will be updated by payment processor if needed
}
if (deploymentsByRequest.has(r.id)) {

View File

@ -7,6 +7,7 @@ import {Config} from './config.js';
import {RegHelper} from './deployments.js';
import {Uploader} from './upload.js';
import {verifyAtomPayment} from './atomPayments.js';
import {PaymentProcessor} from './paymentProcessor.js';
const app = express();
app.use(express.json());
@ -113,7 +114,7 @@ app.post('/upload/config', configUploadParser, async (req, res) => {
}
});
// API endpoint to verify ATOM payments
// API endpoint to verify ATOM payments (with timeout to prevent blocking)
app.post('/verify/atom-payment', async (req, res) => {
try {
const { txHash, minAmount, markAsUsed } = req.body;
@ -140,7 +141,15 @@ app.post('/verify/atom-payment', async (req, res) => {
}
const requiredAmount = minAmount || Config.MIN_ATOM_PAYMENT;
const result = await verifyAtomPayment(txHash, requiredAmount, Boolean(markAsUsed));
// Add timeout to prevent blocking
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Verification timeout')), 10000)
);
const verificationPromise = verifyAtomPayment(txHash, requiredAmount, Boolean(markAsUsed), 5);
const result = await Promise.race([verificationPromise, timeoutPromise]);
res.json(result);
} catch (e) {
@ -168,6 +177,11 @@ app.get('/log/:id', async (req, res) => {
}
});
// Start the payment processor service
const paymentProcessor = new PaymentProcessor();
paymentProcessor.start();
app.listen(Config.LISTEN_PORT, Config.LISTEN_ADDR, () => {
console.log(`listening on ${Config.LISTEN_ADDR}:${Config.LISTEN_PORT}`);
console.log('Payment processor service started');
});

366
src/paymentProcessor.ts Normal file
View File

@ -0,0 +1,366 @@
import { Registry, Account, parseGasAndFees, DEFAULT_GAS_ESTIMATION_MULTIPLIER } from '@cerc-io/registry-sdk';
import { DeliverTxResponse } from '@cosmjs/stargate';
import { getRegistry, 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
constructor() {
this.regHelper = new RegHelper();
}
/**
* Start the payment processor service
*/
start(): void {
console.log('Payment processor started');
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
*/
private async processNewRequests(): Promise<void> {
try {
// Query for all ApplicationDeploymentRequest records
const requests = await this.regHelper.queryRecords({
type: 'ApplicationDeploymentRequest',
});
console.log(`Found ${requests.length} deployment requests`);
for (const request of requests) {
await this.processRequest(request);
}
} 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;
}
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 using existing laconic registry
*/
private async verifyLNTPayment(txHash: string, request: any): Promise<VerificationResult> {
try {
// Use the registry from regHelper to get transaction
const registry = getRegistry();
const tx = await registry.getTx(txHash);
if (!tx) {
return {
valid: false,
reason: 'Transaction not found in laconic registry'
};
}
if (tx.code !== 0) {
return {
valid: false,
reason: `Transaction failed with code ${tx.code}: ${tx.log}`
};
}
// Get the deployer record to check payment address
const deployer = await this.regHelper.getRecord(request.attributes.deployer);
if (!deployer) {
return {
valid: false,
reason: 'Deployer record not found'
};
}
const paymentAddress = deployer.attributes.paymentAddress;
const minPayment = parseInt(deployer.attributes.minimumPayment?.replace(/[^0-9]/g, '') || '0');
if (tx.recipient !== paymentAddress) {
return {
valid: false,
reason: `Payment recipient ${tx.recipient} does not match deployer payment address ${paymentAddress}`
};
}
const payDenom = tx.amount.replace(/[0-9]/g, '');
if (payDenom !== 'alnt') {
return {
valid: false,
reason: `Payment denomination ${payDenom} is not 'alnt'`
};
}
const payAmount = parseInt(tx.amount.replace(/[^0-9]/g, ''));
if (payAmount < minPayment) {
return {
valid: false,
reason: `Payment amount ${payAmount} is less than minimum ${minPayment}`
};
}
return {
valid: true,
amount: tx.amount,
sender: tx.sender,
chainUsed: 'LNT'
};
} 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,
// });
// }
return configs;
}
}
export { PaymentProcessor };