From 765fe3d49a82659efbba1eb665069f026800fbeb Mon Sep 17 00:00:00 2001 From: zramsay Date: Wed, 9 Jul 2025 07:04:28 -0400 Subject: [PATCH] work in progress --- src/app/api/registry/route.ts | 130 ++++++++++++++++++++++++++++++- src/app/page.tsx | 20 ++--- src/components/StatusDisplay.tsx | 11 ++- 3 files changed, 139 insertions(+), 22 deletions(-) diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index df9c33c..fb3c346 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -6,6 +6,115 @@ import axios from 'axios'; // Sleep helper function const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +// ATOM payment verification function +const verifyAtomPayment = async (txHash: string): Promise<{ + valid: boolean, + reason?: string, + amount?: string, + sender?: string +}> => { + try { + const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL; + const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS; + const minPaymentUAtom = '100000'; // 0.1 ATOM in uatom + + if (!apiEndpoint) { + return { + valid: false, + reason: 'ATOM API endpoint not configured' + }; + } + + if (!recipientAddress) { + return { + valid: false, + reason: 'ATOM recipient address not configured' + }; + } + + // Fetch transaction from the ATOM API endpoint + const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`); + + if (!response.data || !response.data.tx || !response.data.tx_response) { + return { + valid: false, + reason: 'Invalid transaction data from API 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 = 5 * 60 * 1000; // 5 minutes + + if (timeDiffMs > timeWindowMs) { + return { + valid: false, + reason: `Transaction is older than 5 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 === recipientAddress) { + for (const coin of msg.amount) { + if (coin.denom === 'uatom') { + // Get the amount in uatom + paymentAmountUAtom = coin.amount; + + if (parseInt(paymentAmountUAtom) >= parseInt(minPaymentUAtom)) { + foundValidPayment = true; + } + break; + } + } + } + } + } + + if (!foundValidPayment) { + return { + valid: false, + reason: `Payment amount (${paymentAmountUAtom || '0'}uatom) is less than required (${minPaymentUAtom}uatom) or not sent to the correct address (${recipientAddress})` + }; + } + + return { + valid: true, + amount: `${paymentAmountUAtom}uatom`, + sender + }; + } catch (error) { + console.error('Error verifying ATOM payment:', error); + return { + valid: false, + reason: `Failed to verify transaction: ${error.message || 'Unknown error'}` + }; + } +}; + // Extract repo name from URL const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, provider: string } => { try { @@ -138,6 +247,23 @@ export async function POST(request: NextRequest) { }, { status: 400 }); } + // First, verify the ATOM payment before doing anything else + console.log('Step 0: Verifying ATOM payment...'); + const paymentVerificationResult = await verifyAtomPayment(txHash); + + if (!paymentVerificationResult.valid) { + console.error('ATOM payment verification failed:', paymentVerificationResult.reason); + return NextResponse.json({ + status: 'error', + message: `Payment verification failed: ${paymentVerificationResult.reason}` + }, { status: 400 }); + } + + console.log('ATOM payment verified successfully:', { + amount: paymentVerificationResult.amount, + sender: paymentVerificationResult.sender + }); + // Validate required environment variables const requiredEnvVars = [ 'REGISTRY_CHAIN_ID', @@ -146,7 +272,9 @@ export async function POST(request: NextRequest) { 'REGISTRY_BOND_ID', 'REGISTRY_AUTHORITY', 'REGISTRY_USER_KEY', - 'DEPLOYER_LRN' + 'DEPLOYER_LRN', + 'NEXT_PUBLIC_RECIPIENT_ADDRESS', + 'NEXT_PUBLIC_COSMOS_API_URL' ]; for (const envVar of requiredEnvVars) { diff --git a/src/app/page.tsx b/src/app/page.tsx index e27e255..fb4b67d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,14 +7,14 @@ import URLForm from '@/components/URLForm'; // Dynamically import PaymentModal component to avoid SSR issues with browser APIs const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false }); import StatusDisplay from '@/components/StatusDisplay'; -import { verifyTransaction, createApplicationDeploymentRequest } from '@/services/registry'; +import { createApplicationDeploymentRequest } from '@/services/registry'; import dynamic from 'next/dynamic'; export default function Home() { const [walletAddress, setWalletAddress] = useState(null); const [url, setUrl] = useState(null); const [showPaymentModal, setShowPaymentModal] = useState(false); - const [status, setStatus] = useState<'idle' | 'verifying' | 'creating' | 'success' | 'error'>('idle'); + const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle'); const [txHash, setTxHash] = useState(null); const [recordId, setRecordId] = useState(null); const [appRecordId, setAppRecordId] = useState(null); @@ -38,20 +38,10 @@ export default function Home() { const handlePaymentComplete = async (hash: string) => { setTxHash(hash); setShowPaymentModal(false); - setStatus('verifying'); + setStatus('creating'); try { - // Verify the transaction - const isValid = await verifyTransaction(hash); - - if (!isValid) { - console.warn('Transaction verification via API failed, but will attempt to create registry record anyway as the transaction might still be valid'); - // We'll continue anyway, as the transaction might be valid but our verification failed - } - - // Create the Laconic Registry record - setStatus('creating'); - + // Create the Laconic Registry record (payment verification is done in the API) if (url) { const result = await createApplicationDeploymentRequest(url, hash); @@ -119,7 +109,7 @@ export default function Home() { diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx index 3056b7d..df34cc4 100644 --- a/src/components/StatusDisplay.tsx +++ b/src/components/StatusDisplay.tsx @@ -1,7 +1,7 @@ 'use client'; interface StatusDisplayProps { - status: 'idle' | 'verifying' | 'creating' | 'success' | 'error'; + status: 'idle' | 'creating' | 'success' | 'error'; txHash?: string; recordId?: string; appRecordId?: string; @@ -31,15 +31,14 @@ export default function StatusDisplay({ const domainSuffix = process.env.NEXT_PUBLIC_DOMAIN_SUFFIX || ''; if (status === 'idle') return null; - const StatusBadge = ({ type }: { type: 'verifying' | 'creating' | 'success' | 'error' }) => { + const StatusBadge = ({ type }: { type: 'creating' | 'success' | 'error' }) => { const getBadgeStyles = () => { switch (type) { - case 'verifying': case 'creating': return { bg: 'var(--warning-light)', color: 'var(--warning)', - text: type === 'verifying' ? 'Verifying' : 'Creating Record' + text: 'Creating Record' }; case 'success': return { @@ -80,7 +79,7 @@ export default function StatusDisplay({ return (
- {(status === 'verifying' || status === 'creating') && ( + {status === 'creating' && (
@@ -89,7 +88,7 @@ export default function StatusDisplay({ - {status === 'verifying' ? 'Verifying transaction...' : 'Creating Laconic Registry record...'} + Creating Laconic Registry record...