diff --git a/README.md b/README.md index c798099..ca9988a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using - URL validation and submission - Transaction verification - Laconic Registry record creation using official `@cerc-io/registry-sdk` +- Automatic salt addition to DNS names to prevent collisions +- Error handling and validation throughout the application flow ## Prerequisites @@ -30,6 +32,7 @@ Client-side (must be prefixed with NEXT_PUBLIC_): - `NEXT_PUBLIC_RECIPIENT_ADDRESS` - The Cosmos address that will receive ATOM payments - `NEXT_PUBLIC_COSMOS_RPC_URL` - The RPC URL for the Cosmos blockchain - `NEXT_PUBLIC_COSMOS_CHAIN_ID` - The chain ID for Keplr wallet (e.g., cosmoshub-4) +- `NEXT_PUBLIC_DOMAIN_SUFFIX` - Optional suffix to append to DNS names in the UI (e.g. ".example.com") Server-side: - `REGISTRY_CHAIN_ID` - The chain ID for the Laconic Registry @@ -74,7 +77,10 @@ npm start 3. User completes payment in ATOM to a specified address 4. The application verifies the transaction using the Cosmos RPC 5. The application calls a server-side API route which creates records in the Laconic Registry -6. The record includes the URL, transaction hash, and other metadata +6. The server generates a unique DNS name by adding a random salt to prevent name collisions +7. Two records are created in the Laconic Registry: + - An ApplicationRecord containing metadata about the URL + - An ApplicationDeploymentRequest linking the URL, DNS, and payment transaction ### Architecture @@ -85,12 +91,89 @@ This application uses a hybrid client/server approach: This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience. +### Resource Name Formats + +#### DNS Name Format + +The DNS names are generated with the following format: +``` +{sanitized-url-name}-{short-commit-hash}-{random-salt}{domain-suffix} +``` + +For example: +- Basic DNS: `github-abc123-xyz789` +- With domain suffix: `github-abc123-xyz789.example.com` + +The random salt ensures that each deployment request has a unique DNS name, even if the same URL is deployed multiple times or by different users. The optional domain suffix allows the application to display a full domain name to users. + +#### Laconic Resource Name (LRN) Format + +The Laconic Resource Names (LRNs) are generated with the following format: +``` +lrn://{authority}/applications/{app-name}-{short-commit-hash}-{random-salt} +``` + +For example: `lrn://atom/applications/github-abc123-xyz789` + +Including the commit hash and salt in the LRN ensures that each application record has a unique identifier, consistently matching the DNS naming pattern. + ## Reference Files This application was built with reference to: - `snowballtools-base/packages/backend/src/registry.ts` - `hosted-frontends/deploy-atom.sh` +## Deployment to Production + +To deploy this application to production, follow these steps: + +1. Clone the repository +2. Install dependencies: `npm install` +3. Create a production build: `npm run build` +4. Set up all required environment variables in your production environment +5. Start the production server: `npm start` + +For containerized deployments, you can use the following Dockerfile: + +```dockerfile +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +EXPOSE 3000 + +ENV NODE_ENV=production + +CMD ["npm", "start"] +``` + +Build and run the Docker container: + +```bash +docker build -t atom-deploy . +docker run -p 3000:3000 --env-file .env.production atom-deploy +``` + ## Known Issues - You may see a deprecated Buffer() warning during build. This comes from dependencies in the registry-sdk. This doesn't affect functionality. +- If using a custom Cosmos chain, ensure that your RPC endpoint supports CORS for client-side requests. +- The Keplr wallet integration requires HTTPS in production environments. + +## Troubleshooting + +### Keplr Wallet Issues + +- **Keplr not detecting**: Install the Keplr browser extension and refresh the page. +- **Chain not found in Keplr**: The application will attempt to suggest the chain to Keplr, but if that fails, you may need to manually add the chain in your Keplr wallet settings. + +### Laconic Registry Issues + +- **Failed to create record**: Check that your REGISTRY_USER_KEY and REGISTRY_BOND_ID are correctly set. +- **Transaction verification errors**: Ensure your COSMOS_RPC_URL is accessible and returns correct transaction data. diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index 802d6e9..df9c33c 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -118,7 +118,25 @@ const registryTransactionWithRetry = async ( export async function POST(request: NextRequest) { try { - const { url, txHash } = await request.json(); + // First check if the request body is valid JSON + let url, txHash; + try { + const body = await request.json(); + url = body.url; + txHash = body.txHash; + + if (!url || !txHash) { + return NextResponse.json({ + status: 'error', + message: 'Missing required fields: url and txHash are required' + }, { status: 400 }); + } + } catch (error) { + return NextResponse.json({ + status: 'error', + message: 'Invalid JSON in request body' + }, { status: 400 }); + } // Validate required environment variables const requiredEnvVars = [ @@ -133,9 +151,10 @@ export async function POST(request: NextRequest) { for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { + console.error(`Missing environment variable: ${envVar}`); return NextResponse.json({ status: 'error', - message: `Missing environment variable: ${envVar}` + message: `Server configuration error: Missing environment variable: ${envVar}` }, { status: 500 }); } } @@ -155,9 +174,17 @@ export async function POST(request: NextRequest) { // Sanitize the app name to ensure it's DNS-compatible (only alphanumeric and dashes) const sanitizedAppName = appName.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase(); - // Create DNS name in format: app_name-shortcommithash - const dnsName = `${sanitizedAppName}-${shortHash}`; - console.log(`DNS name: ${dnsName} (sanitized from: ${appName})`); + // Generate a random salt (6 alphanumeric characters) to prevent name collisions + const generateSalt = (): string => { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + return Array.from({ length: 6 }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join(''); + }; + const salt = generateSalt(); + console.log(`Generated salt: ${salt}`); + + // Create DNS name in format: app_name-shortcommithash-salt + const dnsName = `${sanitizedAppName}-${shortHash}-${salt}`; + console.log(`DNS name with salt: ${dnsName} (sanitized from: ${appName})`); // Ensure the DNS name doesn't have consecutive dashes or start/end with a dash let cleanDnsName = dnsName @@ -166,10 +193,10 @@ export async function POST(request: NextRequest) { // Ensure DNS name is valid (63 chars max per label, all lowercase, starts with a letter) if (cleanDnsName.length > 63) { - // If too long, truncate but keep the commit hash part - const hashPart = `-${shortHash}`; - const maxAppNameLength = 63 - hashPart.length; - cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + hashPart; + // If too long, truncate but preserve both the commit hash and salt parts + const suffixPart = `-${shortHash}-${salt}`; + const maxAppNameLength = 63 - suffixPart.length; + cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + suffixPart; } // If the DNS name ended up empty (unlikely) or doesn't start with a letter (possible), @@ -178,7 +205,7 @@ export async function POST(request: NextRequest) { cleanDnsName = `app-${cleanDnsName}`; } - console.log(`Final DNS name: ${cleanDnsName}`); + console.log(`Final DNS name with salt: ${cleanDnsName}`); // Set up Registry config const config = { @@ -212,8 +239,9 @@ export async function POST(request: NextRequest) { { chainId: config.chainId, gasPrice } ); - // Create LRN for the application - const lrn = `lrn://${config.authority}/applications/${appName}`; + // Create LRN for the application with commit hash and salt + // We already have the salt from earlier, so we use it directly + const lrn = `lrn://${config.authority}/applications/${appName}-${shortHash}-${salt}`; // Get current timestamp for the meta note const timestamp = new Date().toUTCString(); @@ -222,7 +250,7 @@ export async function POST(request: NextRequest) { console.log('Step 1: Publishing ApplicationRecord...'); const applicationRecord = { type: 'ApplicationRecord', - name: appName, + name: `${appName}-${shortHash}-${salt}`, // Include commit hash and salt in the record name version: '1.0.0', app_type: 'webapp', repository: [repoUrl], @@ -232,8 +260,8 @@ export async function POST(request: NextRequest) { // Create fee for transaction directly const fee = { - amount: [{ denom: 'alnt', amount: '900000' }], - gas: '900000', + amount: [{ denom: 'alnt', amount: process.env.REGISTRY_FEES?.replace('alnt', '') || '900000' }], + gas: process.env.REGISTRY_GAS || '900000', }; console.log('Application record data:', applicationRecord); @@ -312,8 +340,8 @@ export async function POST(request: NextRequest) { const deploymentRequestData = { type: 'ApplicationDeploymentRequest', version: '1.0.0', - name: appName, - application: lrn, + name: `${appName}-${shortHash}-${salt}`, // Update name to match application record + application: lrn, // LRN already includes commit hash and salt deployer: deployerLrn, dns: cleanDnsName, config: { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1b9742b..42953a5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,7 +14,7 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "ATOM Deploy - Laconic Registry", + title: "Deploy Frontends using ATOM and Laconic", description: "Deploy URLs to Laconic Registry using ATOM payments", }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 20a60ec..e27e255 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -99,7 +99,7 @@ export default function Home() {
Repository: {repoUrl} - {shortCommitHash && @ {shortCommitHash}} - {(!shortCommitHash && commitHash) && @ {commitHash.substring(0, 7)}}
)}