accept atom payments #29

Closed
zramsay wants to merge 8 commits from zach/atom-payments into main
8 changed files with 671 additions and 14 deletions

View File

@ -15,6 +15,9 @@ RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/lat
chmod +x ./laconic-so && \
mv ./laconic-so /usr/bin/laconic-so
# for testing, put so in the root of this repo prior to running `build-containers`
#COPY laconic-so /usr/bin/laconic-so
# laconic-registry-cli
RUN npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/ && \
npm install -g @cerc-io/laconic-registry-cli && \

View File

@ -1,7 +1,7 @@
# webapp-deployment-status-api
This API provides status information about webapp deployment requests and a mechanism for upload encrypted configuration
files used by those requests.
files used by those requests. It supports payments in both Laconic testnet (alnt) and Cosmos mainnet (ATOM).
## Build and Run
@ -11,6 +11,22 @@ yarn build
yarn start
```
## API Endpoints
### Status Endpoints
- `GET /` - Get status of all deployment requests
- `GET /:id` - Get status of a specific deployment request
- `GET /:id/log` - Get logs for a specific deployment request
- `GET /log/:id` - Get logs for a specific deployment request (deprecated)
### Configuration Upload
- `POST /upload/config` - Upload encrypted configuration file
### ATOM Payment Verification
- `POST /verify/atom-payment` - Verify a Cosmos ATOM payment transaction
- Request body: `{ "txHash": "string", "minAmount": number (optional), "markAsUsed": boolean (optional) }`
- Response: `{ "valid": boolean, "reason": string (if invalid), "amount": number (if valid), "sender": string (if valid), "alreadyUsed": boolean (if transaction was already used) }`
## Configuration
### Keys
@ -44,6 +60,8 @@ record:
minimumPayment: 100alnt
paymentAddress: laconic1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry
publicKey: mQGNBGbJUk0BDAC3j3CiaVtoEf1jrgtsjJnTA5u1a3BExP72mv0eE8y84TgY5rVcf ...
atomPaymentAddress: cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry
minimumAtomPayment: 1
```
This record can most easily be created using `laconic-so publish-deployer-to-registry`.
@ -54,7 +72,9 @@ laconic-so publish-deployer-to-registry \
--api-url https://webapp-deployer-api.my.domain.com
--public-key-file webapp-deployer-api.my.domain.com.pgp.pub \
--lrn lrn://laconic/deployers/webapp-deployer-api.my.domain.com \
--min-required-payment 100
--min-required-payment 100 \
--atom-payment-address cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry \
--min-atom-payment 1
```
This will create the record in the proper format and assign its LRN.
@ -83,11 +103,13 @@ Users can now request deployment using the LRN of the deployer. This will allow
1. Discover the API URL for config uploads.
1. Obtain the public key for encrypting config.
1. See the minimum required payment.
1. See the minimum required payment (both alnt and ATOM if configured).
The request can be made using `laconic-so request-webapp-deployment`. This will handle encrypting and uploading the
config automatically, as well as making a payment (if necessary).
Payment can be made using either Laconic testnet (alnt) or Cosmos mainnet (ATOM). For Cosmos ATOM payments, you'll need to make the payment separately and provide the transaction hash.
```bash
laconic-so request-webapp-deployment \
--laconic-config ~/.laconic/registry.yml \
@ -95,6 +117,13 @@ laconic-so request-webapp-deployment \
--app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
--env-file hello.env \
--make-payment auto
# OR for Cosmos ATOM payment
laconic-so request-webapp-deployment \
--laconic-config ~/.laconic/registry.yml \
--deployer lrn://laconic/deployers/webapp-deployer-api.my.domain.com \
--app lrn://cerc-io/applications/webapp-hello-world@0.1.3 \
--env-file hello.env \
--use-payment <your_cosmos_transaction_hash>
```
Alternatively, users can also use a deployment auction they created instead of making the payment to any specific deployer directly:
@ -135,6 +164,11 @@ LACONIC_CONFIG="/etc/config/registry.yml"
LRN=lrn://laconic/deployers/webapp-deployer-api.my.domain.com
CHECK_INTERVAL=15
# Cosmos ATOM payment configuration
ATOM_PAYMENT_ADDRESS="cosmos1clpc8smrhx5k25zmk3vwna8kddxrsem7a1jlry"
MIN_ATOM_PAYMENT=1
COSMOS_RPC_ENDPOINT="https://cosmos-rpc.example.com"
AUCTION_CHECK_INTERVAL=10
HANDLE_AUCTION_REQUESTS=true
AUCTION_BID_AMOUNT=50000

View File

@ -36,8 +36,9 @@
"author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>",
"license": "Apache-2.0",
"dependencies": {
"@cerc-io/laconic-sdk": "^0.1.15",
"@cerc-io/registry-sdk": "^0.2.11",
"@openpgp/web-stream-tools": "^0.1.3",
"axios": "^1.6.7",
"body-parser": "^1.20.2",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",

178
src/atomPayments.ts Normal file
View File

@ -0,0 +1,178 @@
import axios from 'axios';
import { Config } from './config.js';
// Store verified transaction hashes to prevent double-spending
const verifiedTransactions = new Map<string, {
timestamp: number,
amount: number,
sender: string,
used: boolean
}>();
// Periodically clean up old verified transactions (older than 24 hours)
setInterval(() => {
const now = Date.now();
for (const [hash, data] of verifiedTransactions.entries()) {
if (now - data.timestamp > 24 * 60 * 60 * 1000) {
verifiedTransactions.delete(hash);
}
}
}, 60 * 60 * 1000); // Clean up every hour
/**
* Verifies a Cosmos ATOM payment transaction
* @param txHash The transaction hash to verify
* @param requiredAmount The minimum required amount of ATOM
* @param markAsUsed Set to true to mark this transaction as used (preventing double-spending)
* @returns An object with verification result and details
*/
export async function verifyAtomPayment(
txHash: string,
requiredAmount = Config.MIN_ATOM_PAYMENT,
markAsUsed = false,
timeWindowMinutes = 5
): Promise<{
valid: boolean,
reason?: string,
amount?: string,
sender?: string,
alreadyUsed?: boolean
}> {
if (!Config.ATOM_PAYMENT_ADDRESS) {
return {
valid: false,
reason: 'ATOM_PAYMENT_ADDRESS not configured'
};
}
if (!Config.COSMOS_RPC_ENDPOINT) {
return {
valid: false,
reason: 'COSMOS_RPC_ENDPOINT not configured'
};
}
// Check if we've already verified this transaction
if (verifiedTransactions.has(txHash)) {
const txData = verifiedTransactions.get(txHash);
// If the transaction is marked as used and we're not just checking
if (txData.used && !markAsUsed) {
return {
valid: false,
reason: 'Transaction has already been used for a deployment',
alreadyUsed: true
};
}
// If we're marking as used
if (markAsUsed) {
txData.used = true;
}
// Return cached verification result
return {
valid: true,
amount: `${txData.amount}uatom`,
sender: txData.sender,
alreadyUsed: txData.used
};
}
try {
// Fetch transaction from the Cosmos RPC endpoint
const response = await axios.get(
`${Config.COSMOS_RPC_ENDPOINT}/cosmos/tx/v1beta1/txs/${txHash}`
);
if (!response.data || !response.data.tx || !response.data.tx_response) {
return {
valid: false,
reason: 'Invalid transaction data from RPC endpoint'
};
}
// Check if transaction was successful
const txResponse = response.data.tx_response;
if (txResponse.code !== 0) {
return {
valid: false,
reason: `Transaction failed with code ${txResponse.code}: ${txResponse.raw_log}`
};
}
// 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;
let paymentAmountUAtom = '';
let sender = '';
// Get the sender address from the first signer
if (tx.auth_info && tx.auth_info.signer_infos && tx.auth_info.signer_infos.length > 0) {
sender = tx.auth_info.signer_infos[0].public_key.address || '';
}
// Find the send message in the transaction
for (const msg of tx.body.messages) {
if (msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') {
if (msg.to_address === Config.ATOM_PAYMENT_ADDRESS) {
for (const coin of msg.amount) {
if (coin.denom === 'uatom') {
// Get the amount in uatom
paymentAmountUAtom = coin.amount;
// Extract the required amount as an integer from the format like "10uatom"
const requiredAmountUAtom = parseInt(requiredAmount.replace(/[^0-9]/g, ''));
if (parseInt(paymentAmountUAtom) >= requiredAmountUAtom) {
foundValidPayment = true;
}
break;
}
}
}
}
}
if (!foundValidPayment) {
return {
valid: false,
reason: `Payment amount (${paymentAmountUAtom || '0'}uatom) is less than required (${requiredAmount}) or not sent to the correct address`
};
}
// Cache the verification result
verifiedTransactions.set(txHash, {
timestamp: Date.now(),
amount: parseInt(paymentAmountUAtom),
sender,
used: markAsUsed
});
return {
valid: true,
amount: `${paymentAmountUAtom}uatom`,
sender,
alreadyUsed: markAsUsed
};
} catch (error) {
console.error('Error verifying ATOM payment:', error);
return {
valid: false,
reason: `Failed to verify transaction: ${error.message || 'Unknown error'}`
};
}
}

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);
@ -22,6 +22,10 @@ export const Config = {
UPLOAD_MAX_SIZE: process.env.BUILD_LOGS || '1MB',
OPENPGP_PASSPHRASE: process.env.OPENPGP_PASSPHRASE,
OPENPGP_PRIVATE_KEY_FILE: process.env.OPENPGP_PRIVATE_KEY_FILE,
// Cosmos ATOM payment configuration
ATOM_PAYMENT_ADDRESS: process.env.ATOM_PAYMENT_ADDRESS || '',
MIN_ATOM_PAYMENT: process.env.MIN_ATOM_PAYMENT || '1000000uatom',
COSMOS_RPC_ENDPOINT: process.env.COSMOS_RPC_ENDPOINT || '',
};
export const getRegistry = (): Registry => {

View File

@ -1,5 +1,5 @@
import {getRegistry} 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';
@ -25,10 +25,12 @@ export class RequestStatus {
public url?: string;
public deployment?: string;
public lastUpdate?: string;
public atomPaymentVerified?: boolean;
constructor(public id: string, public createTime: string,) {
this.lastState = 'SUBMITTED';
this.lastUpdate = this.createTime;
this.atomPaymentVerified = false;
}
}
@ -70,14 +72,18 @@ export class RegHelper {
} else {
const records = await this.registry.getRecordsByIds([id]);
for (const r of records) {
this.cache.set(r.id, r);
if (Array.isArray(r.names)) {
for (const n of r.names) {
this.cache.set(n, r);
// Check if record is not null before accessing its properties
if (r && r.id) {
this.cache.set(r.id, r);
if (Array.isArray(r.names)) {
for (const n of r.names) {
this.cache.set(n, r);
}
}
}
}
return records[0];
// Return the first non-null record, or null if none found
return records.find(r => r !== null) || null;
}
}
@ -107,12 +113,18 @@ export class RegHelper {
return [...records];
}
// 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[] = [];
const deployments: any[] = [];
const removalRequests = await this.queryRecords({
type: 'ApplicationDeploymentRemovalRequest',
});
// WebappDeployer records no longer needed for status display
// Payment verification is handled by background processor
if (requestId) {
const request = await this.getRecordById(requestId);
@ -136,11 +148,13 @@ export class RegHelper {
const deploymentsByRequest = new Map<string, any>();
for (const d of deployments) {
deploymentsByRequest.set(d.attributes.request, d);
if (d && d.attributes?.request) {
deploymentsByRequest.set(d.attributes.request, d);
}
}
const removalsByRequest = new Map<string, any>();
for (const rr of removalRequests) {
if (rr.attributes.request) {
if (rr && rr.attributes?.request) {
removalsByRequest.set(rr.attributes.request, rr);
}
}
@ -149,9 +163,21 @@ export class RegHelper {
const ret = [];
for (const r of requests) {
// Skip null or invalid requests
if (!r || !r.id || !r.createTime) {
console.warn('Skipping invalid request:', r);
continue;
}
const status = new RequestStatus(r.id, r.createTime);
ret.push(status);
// Skip if application attribute is missing
if (!r.attributes?.application) {
status.lastState = 'ERROR';
continue;
}
const app = await this.getRecord(r.attributes.application);
if (!app) {
status.lastState = 'ERROR';
@ -160,6 +186,13 @@ export class RegHelper {
status.app = r.attributes.application;
const hostname = r.attributes.dns ?? generateHostnameForApp(app);
// 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)) {
const deployment = deploymentsByRequest.get(r.id);

View File

@ -5,7 +5,9 @@ import {existsSync, readdirSync, readFileSync} from 'fs';
import {Config} from './config.js';
import {RegHelper} from './deployments.js';
import { Uploader } from './upload.js';
import {Uploader} from './upload.js';
import {verifyAtomPayment} from './atomPayments.js';
import {PaymentProcessor} from './paymentProcessor.js';
const app = express();
app.use(express.json());
@ -112,6 +114,53 @@ app.post('/upload/config', configUploadParser, async (req, res) => {
}
});
// 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;
if (!txHash) {
return res.status(400).json({
valid: false,
reason: 'Transaction hash is required'
});
}
if (!Config.ATOM_PAYMENT_ADDRESS) {
return res.status(400).json({
valid: false,
reason: 'ATOM_PAYMENT_ADDRESS not configured'
});
}
if (!Config.COSMOS_RPC_ENDPOINT) {
return res.status(400).json({
valid: false,
reason: 'COSMOS_RPC_ENDPOINT not configured'
});
}
const requiredAmount = minAmount || Config.MIN_ATOM_PAYMENT;
// 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) {
console.error('Error verifying ATOM payment:', e);
res.status(500).json({
valid: false,
reason: `Server error: ${e.message || 'Unknown error'}`
});
}
});
// deprecated
app.get('/log/:id', async (req, res) => {
try {
@ -128,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');
});

350
src/paymentProcessor.ts Normal file
View File

@ -0,0 +1,350 @@
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 };