From 50b1877e8c8e54eaaa51ec1fa6b33544a8c9de5b Mon Sep 17 00:00:00 2001 From: zramsay Date: Wed, 30 Apr 2025 11:08:00 -0400 Subject: [PATCH 1/3] accept atom payments --- README.md | 40 ++++++++++- package.json | 1 + scripts/run.sh | 2 + src/atomPayments.ts | 159 ++++++++++++++++++++++++++++++++++++++++++++ src/config.ts | 4 ++ src/deployments.ts | 79 +++++++++++++++++++++- src/main.ts | 42 +++++++++++- 7 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 src/atomPayments.ts diff --git a/README.md b/README.md index 054c398..20a7677 100644 --- a/README.md +++ b/README.md @@ -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 ``` 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 diff --git a/package.json b/package.json index c02f216..40feac5 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@cerc-io/laconic-sdk": "^0.1.15", "@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", diff --git a/scripts/run.sh b/scripts/run.sh index 2de8f82..1d6389a 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -168,6 +168,8 @@ while true; do --ip "${DEPLOYMENT_IP}" \ --lrn "$LRN" \ --min-required-payment ${MIN_REQUIRED_PAYMENT:-0} \ + --atom-payment-address "${ATOM_PAYMENT_ADDRESS:-}" \ + --min-atom-payment ${MIN_ATOM_PAYMENT:-1} \ --config-upload-dir "$UPLOAD_DIRECTORY" \ --private-key-file "$OPENPGP_PRIVATE_KEY_FILE" \ --private-key-passphrase "$OPENPGP_PASSPHRASE" \ diff --git a/src/atomPayments.ts b/src/atomPayments.ts new file mode 100644 index 0000000..90cc395 --- /dev/null +++ b/src/atomPayments.ts @@ -0,0 +1,159 @@ +import axios from 'axios'; +import { Config } from './config.js'; + +// Store verified transaction hashes to prevent double-spending +const verifiedTransactions = new Map(); + +// 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 +): Promise<{ + valid: boolean, + reason?: string, + amount?: number, + 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, + 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}` + }; + } + + // Extract the payment details + const tx = response.data.tx; + let foundValidPayment = false; + let paymentAmount = 0; + 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') { // uatom is the micro ATOM denomination (1 ATOM = 1,000,000 uatom) + paymentAmount = parseInt(coin.amount) / 1000000; // Convert from uatom to ATOM + if (paymentAmount >= requiredAmount) { + foundValidPayment = true; + } + break; + } + } + } + } + } + + if (!foundValidPayment) { + return { + valid: false, + reason: `Payment amount (${paymentAmount} ATOM) is less than required (${requiredAmount} ATOM) or not sent to the correct address` + }; + } + + // Cache the verification result + verifiedTransactions.set(txHash, { + timestamp: Date.now(), + amount: paymentAmount, + sender, + used: markAsUsed + }); + + return { + valid: true, + amount: paymentAmount, + sender, + alreadyUsed: markAsUsed + }; + } catch (error) { + console.error('Error verifying ATOM payment:', error); + return { + valid: false, + reason: `Failed to verify transaction: ${error.message || 'Unknown error'}` + }; + } +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 76ba6f7..419d22a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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: parseInt(process.env.MIN_ATOM_PAYMENT || '1'), + COSMOS_RPC_ENDPOINT: process.env.COSMOS_RPC_ENDPOINT || '', }; export const getRegistry = (): Registry => { diff --git a/src/deployments.ts b/src/deployments.ts index b30f630..531abf1 100644 --- a/src/deployments.ts +++ b/src/deployments.ts @@ -1,8 +1,9 @@ -import {getRegistry} from './config.js'; +import {getRegistry, Config} from './config.js'; import {Registry} from '@cerc-io/laconic-sdk'; import stringify from 'json-stable-stringify'; import {createHash} from 'crypto'; +import {verifyAtomPayment} from './atomPayments.js'; function generateHostnameForApp(app) { const lastPart = app.attributes.name.split('/').pop(); @@ -25,10 +26,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; } } @@ -107,12 +110,56 @@ 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} ATOM`); + 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; + } + } + async deploymentRequestStatus(requestId?: string) { const requests: any[] = []; const deployments: any[] = []; const removalRequests = await this.queryRecords({ type: 'ApplicationDeploymentRemovalRequest', }); + + // Get all WebappDeployer records to check for atom payment configuration + const deployers = await this.queryRecords({ + type: 'WebappDeployer', + }); if (requestId) { const request = await this.getRecordById(requestId); @@ -133,6 +180,16 @@ 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(); + 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(); for (const d of deployments) { @@ -160,6 +217,26 @@ 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; + } + } + } + } if (deploymentsByRequest.has(r.id)) { const deployment = deploymentsByRequest.get(r.id); diff --git a/src/main.ts b/src/main.ts index dc53b2e..f9ab2fd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,8 @@ 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'; const app = express(); app.use(express.json()); @@ -112,6 +113,45 @@ app.post('/upload/config', configUploadParser, async (req, res) => { } }); +// API endpoint to verify ATOM payments +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; + const result = await verifyAtomPayment(txHash, requiredAmount, Boolean(markAsUsed)); + + 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 { -- 2.45.2 From a8be189229cae87c28c0f59158884301cf2cf024 Mon Sep 17 00:00:00 2001 From: zramsay Date: Thu, 1 May 2025 07:51:35 -0400 Subject: [PATCH 2/3] use local laconic-so for testing --- Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0547004..583be00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,12 @@ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg - apt update && apt install -y nodejs # laconic-so -RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/latest/laconic-so && \ - chmod +x ./laconic-so && \ - mv ./laconic-so /usr/bin/laconic-so +#RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/latest/laconic-so && \ +# 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/ && \ -- 2.45.2 From 429bb62cf17ca104621551f324ff29d7f5376fa6 Mon Sep 17 00:00:00 2001 From: zramsay Date: Fri, 2 May 2025 14:14:09 -0400 Subject: [PATCH 3/3] uatom. string --- src/atomPayments.ts | 23 ++++++++++++++--------- src/config.ts | 2 +- src/deployments.ts | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/atomPayments.ts b/src/atomPayments.ts index 90cc395..c6dd356 100644 --- a/src/atomPayments.ts +++ b/src/atomPayments.ts @@ -33,7 +33,7 @@ export async function verifyAtomPayment( ): Promise<{ valid: boolean, reason?: string, - amount?: number, + amount?: string, sender?: string, alreadyUsed?: boolean }> { @@ -72,7 +72,7 @@ export async function verifyAtomPayment( // Return cached verification result return { valid: true, - amount: txData.amount, + amount: `${txData.amount}uatom`, sender: txData.sender, alreadyUsed: txData.used }; @@ -103,7 +103,7 @@ export async function verifyAtomPayment( // Extract the payment details const tx = response.data.tx; let foundValidPayment = false; - let paymentAmount = 0; + let paymentAmountUAtom = ''; let sender = ''; // Get the sender address from the first signer @@ -116,9 +116,14 @@ export async function verifyAtomPayment( 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') { // uatom is the micro ATOM denomination (1 ATOM = 1,000,000 uatom) - paymentAmount = parseInt(coin.amount) / 1000000; // Convert from uatom to ATOM - if (paymentAmount >= requiredAmount) { + 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; @@ -131,21 +136,21 @@ export async function verifyAtomPayment( if (!foundValidPayment) { return { valid: false, - reason: `Payment amount (${paymentAmount} ATOM) is less than required (${requiredAmount} ATOM) or not sent to the correct address` + 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: paymentAmount, + amount: parseInt(paymentAmountUAtom), sender, used: markAsUsed }); return { valid: true, - amount: paymentAmount, + amount: `${paymentAmountUAtom}uatom`, sender, alreadyUsed: markAsUsed }; diff --git a/src/config.ts b/src/config.ts index 419d22a..8edbed8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -24,7 +24,7 @@ export const Config = { 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: parseInt(process.env.MIN_ATOM_PAYMENT || '1'), + MIN_ATOM_PAYMENT: process.env.MIN_ATOM_PAYMENT || '1000000uatom', COSMOS_RPC_ENDPOINT: process.env.COSMOS_RPC_ENDPOINT || '', }; diff --git a/src/deployments.ts b/src/deployments.ts index 531abf1..0437e6a 100644 --- a/src/deployments.ts +++ b/src/deployments.ts @@ -137,7 +137,7 @@ export class RegHelper { const verification = await verifyAtomPayment(request.attributes.payment, minAtomPayment); if (verification.valid) { - console.log(`ATOM payment verified for request ${request.id} - Amount: ${verification.amount} ATOM`); + 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}`); -- 2.45.2