try
This commit is contained in:
parent
429bb62cf1
commit
15187227e9
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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)) {
|
||||
|
18
src/main.ts
18
src/main.ts
@ -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
366
src/paymentProcessor.ts
Normal 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 };
|
Loading…
Reference in New Issue
Block a user