Remove code for ATOM token payments

This commit is contained in:
Shreerang Kale 2025-07-18 15:31:04 +05:30
parent 982232545a
commit 4c291dc307
16 changed files with 275 additions and 851 deletions

View File

@ -1,16 +1,14 @@
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
# ATOM Payment Configuration
NEXT_PUBLIC_RECIPIENT_ADDRESS=cosmos1yourrealaddress
NEXT_PUBLIC_COSMOS_RPC_URL=https://rpc.cosmos.network
NEXT_PUBLIC_COSMOS_API_URL=https://api.cosmos.network
NEXT_PUBLIC_COSMOS_CHAIN_ID=cosmoshub-4
# Solana/GOR Payment Configuration
# Solana Token Payment Configuration
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
NEXT_PUBLIC_SOLANA_WEBSOCKET_URL=wss://skilled-prettiest-seed.solana-mainnet.quiknode.pro/
NEXT_PUBLIC_GOR_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS=
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
NEXT_PUBLIC_SOLANA_TOKEN_NAME=GOR Token
NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS=6
NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT=50
# UI Configuration (optional)
NEXT_PUBLIC_DOMAIN_SUFFIX=
@ -30,11 +28,9 @@ REGISTRY_FEES=900000alnt
REGISTRY_GAS_PRICE=0.001
# Application Configuration
APP_NAME=atom-deploy
APP_NAME=gor-deploy
DEPLOYER_LRN=
# LNT Transfer Configuration (required for both ATOM and GOR flows)
# LNT Transfer Configuration (required for Solana flow)
# Note: REGISTRY_USER_KEY is used as the prefilled account for LNT transfers
# TODO: Use deployer lrn to determine the address
LACONIC_SERVICE_PROVIDER_ADDRESS=
LACONIC_TRANSFER_AMOUNT=1000000alnt

View File

@ -1,13 +1,15 @@
# ATOM Deploy - Laconic Registry
# GOR Deploy - Laconic Registry
A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using Keplr wallet) and paste a URL. The transaction hash and URL are used to create records in the Laconic Registry.
A simple Next.js frontend that allows users to pay in GOR tokens (configurable Solana SPL tokens) using Solana wallets and paste a URL. The transaction hash and URL are used to create records in the Laconic Registry.
## Features
- Keplr wallet integration for ATOM payments
- Solana wallet integration (Phantom & Solflare) for GOR token payments
- Configurable Solana SPL token support (defaults to GOR)
- URL validation and submission
- Transaction verification
- Solana transaction verification with replay protection
- Laconic Registry record creation using official `@cerc-io/registry-sdk`
- LNT token transfer integration for registry payments
- Automatic salt addition to DNS names to prevent collisions
- Error handling and validation throughout the application flow
@ -15,7 +17,7 @@ A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using
- Node.js 18.x or later
- npm or yarn
- Keplr wallet browser extension
- Solana wallet browser extension (Phantom or Solflare)
- Access to a Laconic Registry node
## Environment Variables
@ -29,10 +31,14 @@ cp .env.local.example .env.local
Required environment variables:
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 (used by Keplr for transactions)
- `NEXT_PUBLIC_COSMOS_API_URL` - The REST API URL for the Cosmos blockchain (used for transaction queries)
- `NEXT_PUBLIC_COSMOS_CHAIN_ID` - The chain ID for Keplr wallet (e.g., cosmoshub-4)
- `NEXT_PUBLIC_SOLANA_RPC_URL` - The RPC URL for the Solana blockchain
- `NEXT_PUBLIC_SOLANA_WEBSOCKET_URL` - The WebSocket URL for Solana (optional)
- `NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS` - The mint address of the SPL token to accept
- `NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS` - The Solana address that will receive token payments
- `NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL` - The token symbol to display (e.g., "GOR")
- `NEXT_PUBLIC_SOLANA_TOKEN_NAME` - The full token name (e.g., "GOR Token")
- `NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS` - The number of decimals for the token (e.g., 6)
- `NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT` - The fixed payment amount required (e.g., 50)
- `NEXT_PUBLIC_DOMAIN_SUFFIX` - Optional suffix to append to DNS names in the UI (e.g. ".example.com")
- `NEXT_PUBLIC_EXAMPLE_URL` - Example URL to pre-fill in the URL form (e.g. "https://github.com/cerc-io/laconic-registry-cli")
@ -42,9 +48,11 @@ Server-side:
- `REGISTRY_RPC_ENDPOINT` - The RPC endpoint for the Laconic Registry
- `REGISTRY_BOND_ID` - The bond ID to use for Laconic Registry records
- `REGISTRY_AUTHORITY` - The authority for Laconic Registry LRNs
- `REGISTRY_USER_KEY` - The private key for Laconic Registry transactions
- `APP_NAME` - The name of the application (used in record creation)
- `REGISTRY_USER_KEY` - The private key for Laconic Registry transactions (also used for LNT transfers)
- `APP_NAME` - The name of the application (used in record creation, defaults to "gor-deploy")
- `DEPLOYER_LRN` - The LRN of the deployer
- `LACONIC_SERVICE_PROVIDER_ADDRESS` - The Laconic address to receive LNT transfers
- `LACONIC_TRANSFER_AMOUNT` - The amount of LNT to transfer (e.g., "1000000alnt")
## Installation
@ -74,24 +82,25 @@ npm start
## How It Works
1. User connects their Keplr wallet to the application
1. User connects their Solana wallet (Phantom or Solflare) to the application
2. User enters a URL they want to deploy to the Laconic Registry
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 server generates a unique DNS name by adding a random salt to prevent name collisions
7. Two records are created in the Laconic Registry:
3. User completes payment in GOR tokens (or configured SPL token) to a specified Solana address
4. The application verifies the Solana transaction with replay protection
5. After payment verification, the server transfers LNT tokens from a prefilled account to the service provider
6. The application calls a server-side API route which creates records in the Laconic Registry using the LNT transfer hash
7. The server generates a unique DNS name by adding a random salt to prevent name collisions
8. Two records are created in the Laconic Registry:
- An ApplicationRecord containing metadata about the URL
- An ApplicationDeploymentRequest linking the URL, DNS, and payment transaction
- An ApplicationDeploymentRequest linking the URL, DNS, and payment details with external_payment metadata
### Architecture
This application uses a hybrid client/server approach:
- Client-side: Handles the user interface, Keplr wallet integration, and transaction verification
- Server-side: Next.js API route handles the communication with the Laconic Registry
- Client-side: Handles the user interface, Solana wallet integration, and transaction verification
- Server-side: Next.js API route handles LNT transfers and communication with the Laconic Registry
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience.
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience. The dual-payment system (Solana → LNT → Registry) enables cross-chain payment acceptance.
### Resource Name Formats
@ -115,7 +124,7 @@ 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`
For example: `lrn://gor/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.
@ -123,7 +132,7 @@ Including the commit hash and salt in the LRN ensures that each application reco
This application was built with reference to:
- `snowballtools-base/packages/backend/src/registry.ts`
- `hosted-frontends/deploy-atom.sh`
- Original `hosted-frontends/deploy-atom.sh` (adapted for Solana/GOR)
## Deployment to Production
@ -158,24 +167,32 @@ 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
docker build -t gor-deploy .
docker run -p 3000:3000 --env-file .env.production gor-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.
- Ensure that your Solana RPC endpoint supports CORS for client-side requests.
- Solana wallet integrations require HTTPS in production environments.
## Troubleshooting
### Keplr Wallet Issues
### Solana 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.
- **Wallet not detecting**: Install the Phantom or Solflare browser extension and refresh the page.
- **Connection issues**: Ensure the wallet is unlocked and try refreshing the page.
- **Transaction failures**: Check that you have sufficient SOL for transaction fees and enough tokens for the payment.
### Token Configuration
- **Wrong token**: Verify the `NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS` matches your desired SPL token.
- **Incorrect decimals**: Ensure `NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS` matches the token's actual decimal count.
- **Payment amount**: Adjust `NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT` to the desired payment amount.
### 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 and COSMOS_API_URL are accessible and return correct transaction data.
- **LNT transfer errors**: Ensure your REGISTRY_USER_KEY has sufficient LNT balance and the LACONIC_SERVICE_PROVIDER_ADDRESS is valid.
- **Transaction verification errors**: Ensure your SOLANA_RPC_URL is accessible and returns correct transaction data.

View File

@ -1,5 +1,5 @@
{
"name": "atom-deploy",
"name": "gor-deploy",
"version": "0.1.0",
"private": true,
"scripts": {
@ -11,11 +11,9 @@
"dependencies": {
"@cerc-io/registry-sdk": "^0.2.11",
"@cosmjs/stargate": "^0.32.3",
"@keplr-wallet/types": "^0.12.71",
"@solana/spl-token": "^0.4.13",
"@solana/web3.js": "^1.98.2",
"axios": "^1.6.8",
"bn.js": "^5.2.2",
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { Account, Registry, parseGasAndFees } from '@cerc-io/registry-sdk';
import { Registry } from '@cerc-io/registry-sdk';
import { GasPrice } from '@cosmjs/stargate';
import axios from 'axios';
import { verifyUnusedSolanaPayment } from '@/utils/solanaVerify';
@ -8,114 +8,6 @@ import { transferLNTTokens } from '@/services/laconicTransfer';
// 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 instanceof Error ? error.message : 'Unknown error'}`
};
}
};
// Extract repo name from URL
const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, provider: string } => {
@ -230,12 +122,11 @@ const registryTransactionWithRetry = async (
export async function POST(request: NextRequest) {
try {
// First check if the request body is valid JSON
let url, txHash, paymentType;
let url, txHash;
try {
const body = await request.json();
url = body.url;
txHash = body.txHash;
paymentType = body.paymentType || 'ATOM'; // Default to ATOM for backward compatibility
if (!url || !txHash) {
return NextResponse.json({
@ -243,13 +134,6 @@ export async function POST(request: NextRequest) {
message: 'Missing required fields: url and txHash are required'
}, { status: 400 });
}
if (!['ATOM', 'GOR'].includes(paymentType)) {
return NextResponse.json({
status: 'error',
message: 'Invalid payment type. Must be ATOM or GOR'
}, { status: 400 });
}
} catch (error) {
return NextResponse.json({
status: 'error',
@ -257,42 +141,24 @@ export async function POST(request: NextRequest) {
}, { status: 400 });
}
// Verify payment based on type
if (paymentType === 'ATOM') {
console.log('Step 0: Verifying ATOM payment...');
const paymentVerificationResult = await verifyAtomPayment(txHash);
// Verify Solana payment
console.log('Step 0: Verifying Solana token payment...');
const solanaPaymentResult = await verifyUnusedSolanaPayment(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
});
} else if (paymentType === 'GOR') {
console.log('Step 0: Verifying GOR payment...');
const gorPaymentResult = await verifyUnusedSolanaPayment(txHash);
if (!gorPaymentResult.valid) {
console.error('GOR payment verification failed:', gorPaymentResult.reason);
return NextResponse.json({
status: 'error',
message: `Payment verification failed: ${gorPaymentResult.reason}`
}, { status: 400 });
}
console.log('GOR payment verified successfully:', {
amount: gorPaymentResult.amount,
sender: gorPaymentResult.sender
});
if (!solanaPaymentResult.valid) {
console.error('Solana token payment verification failed:', solanaPaymentResult.reason);
return NextResponse.json({
status: 'error',
message: `Payment verification failed: ${solanaPaymentResult.reason}`
}, { status: 400 });
}
// For both payment types, perform LNT transfer after payment verification
console.log('Solana token payment verified successfully:', {
amount: solanaPaymentResult.amount,
sender: solanaPaymentResult.sender
});
// Perform LNT transfer after payment verification
console.log('Step 0.5: Performing LNT transfer from prefilled account to service provider...');
const lntTransferResult = await transferLNTTokens();
@ -307,8 +173,8 @@ export async function POST(request: NextRequest) {
console.log('LNT transfer completed:', lntTransferResult.transactionHash);
const finalTxHash = lntTransferResult.transactionHash!; // Use LNT transfer hash for registry
// Validate required environment variables based on payment type
const baseRequiredEnvVars = [
// Validate required environment variables for GOR/Solana payments
const requiredEnvVars = [
'REGISTRY_CHAIN_ID',
'REGISTRY_GQL_ENDPOINT',
'REGISTRY_RPC_ENDPOINT',
@ -316,29 +182,15 @@ export async function POST(request: NextRequest) {
'REGISTRY_AUTHORITY',
'REGISTRY_USER_KEY', // This is the same as the prefilled account for LNT transfers
'DEPLOYER_LRN',
// LNT transfer variables now required for both payment types
// LNT transfer variables
'LACONIC_SERVICE_PROVIDER_ADDRESS',
'LACONIC_TRANSFER_AMOUNT'
];
const atomRequiredEnvVars = [
'NEXT_PUBLIC_RECIPIENT_ADDRESS',
'NEXT_PUBLIC_COSMOS_API_URL'
];
const gorRequiredEnvVars = [
'LACONIC_TRANSFER_AMOUNT',
// Solana/GOR specific variables
'NEXT_PUBLIC_SOLANA_RPC_URL',
'NEXT_PUBLIC_GOR_MINT_ADDRESS',
'NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS'
'NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS',
'NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS'
];
let requiredEnvVars = [...baseRequiredEnvVars];
if (paymentType === 'ATOM') {
requiredEnvVars = [...requiredEnvVars, ...atomRequiredEnvVars];
} else if (paymentType === 'GOR') {
requiredEnvVars = [...requiredEnvVars, ...gorRequiredEnvVars];
}
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
console.error(`Missing environment variable: ${envVar}`);
@ -540,9 +392,16 @@ export async function POST(request: NextRequest) {
}
},
meta: {
note: `Added via ATOM-Deploy @ ${timestamp}`,
note: `Added via GOR-Deploy @ ${timestamp}`,
repository: repoUrl,
repository_ref: fullHash,
external_payment: {
// Use CAIP convention for chain ID: namespace + reference
chain_id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet
tx_hash: txHash,
// TODO: Take pubkey from user and add it
// pubkey: ''
}
},
payment: finalTxHash,
};

View File

@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
@ -14,8 +15,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Deploy Frontends using ATOM and Laconic",
description: "Deploy URLs to Laconic Registry using ATOM payments",
title: "Deploy Frontends using GOR and Laconic",
description: "Deploy URLs to Laconic Registry using GOR payments",
};
export default function RootLayout({

View File

@ -1,19 +1,19 @@
'use client';
import { useState } from 'react';
// Dynamically import components to avoid SSR issues with browser APIs
const KeplrConnect = dynamic(() => import('@/components/KeplrConnect'), { ssr: false });
const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false });
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
import dynamic from 'next/dynamic';
import URLForm from '@/components/URLForm';
import StatusDisplay from '@/components/StatusDisplay';
import { createApplicationDeploymentRequest } from '@/services/registry';
import { PaymentType, SolanaWalletState } from '@/types';
import dynamic from 'next/dynamic';
import { SolanaWalletState } from '@/types';
// Dynamically import components to avoid SSR issues with browser APIs
const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false });
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
export default function Home() {
const [paymentType, setPaymentType] = useState<PaymentType>('ATOM');
const [walletAddress, setWalletAddress] = useState<string | null>(null);
const [showWalletConnection, setShowWalletConnection] = useState(false);
const [solanaWalletState, setSolanaWalletState] = useState<SolanaWalletState>({
connected: false,
publicKey: null,
@ -33,10 +33,6 @@ export default function Home() {
const [shortCommitHash, setShortCommitHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const handleConnect = (address: string) => {
setWalletAddress(address);
};
const handleSolanaConnect = (walletState: SolanaWalletState) => {
setSolanaWalletState(walletState);
// Store wallet info globally for PaymentModal access (simplified approach)
@ -48,14 +44,8 @@ export default function Home() {
}
};
const handlePaymentTypeChange = (type: PaymentType) => {
setPaymentType(type);
// Reset wallet states when switching payment types
if (type === 'ATOM') {
setSolanaWalletState({ connected: false, publicKey: null, walletType: null });
} else {
setWalletAddress(null);
}
const handleConnectWallet = () => {
setShowWalletConnection(true);
};
const handleUrlSubmit = (submittedUrl: string) => {
@ -67,12 +57,12 @@ export default function Home() {
setTxHash(hash);
setShowPaymentModal(false);
setStatus('creating');
try {
// Create the Laconic Registry record (payment verification is done in the API)
if (url) {
const result = await createApplicationDeploymentRequest(url, hash, paymentType);
const result = await createApplicationDeploymentRequest(url, hash);
if (result.status === 'success') {
setRecordId(result.id);
if (result.applicationRecordId) {
@ -114,85 +104,69 @@ export default function Home() {
return (
<main className="min-h-screen flex flex-col items-center justify-center p-6">
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
Deploy Frontends with ATOM/GOR + Laconic
Deploy Frontends with GOR + Laconic
</h1>
{/* Payment Type Selection */}
<div className="mb-6 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
Select Payment Method
</h2>
<div className="flex space-x-4">
<label className="flex items-center cursor-pointer">
<input
type="radio"
name="paymentType"
value="ATOM"
checked={paymentType === 'ATOM'}
onChange={(e) => handlePaymentTypeChange(e.target.value as PaymentType)}
className="mr-2"
/>
<span style={{ color: 'var(--foreground)' }}>ATOM (Cosmos)</span>
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
name="paymentType"
value="GOR"
checked={paymentType === 'GOR'}
onChange={(e) => handlePaymentTypeChange(e.target.value as PaymentType)}
className="mr-2"
/>
<span style={{ color: 'var(--foreground)' }}>GOR (Solana)</span>
</label>
</div>
</div>
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
Connect Your Wallet
</h2>
{paymentType === 'ATOM' ? (
<KeplrConnect onConnect={handleConnect} />
{!showWalletConnection ? (
<div className="text-center">
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
Payment method: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
GOR (Solana)
</span>
</p>
<button
onClick={handleConnectWallet}
className="px-6 py-2 rounded-lg font-medium transition-colors"
style={{
background: 'var(--primary)',
color: 'var(--primary-foreground)',
border: 'none'
}}
>
Connect Solana Wallet
</button>
</div>
) : (
<SolanaConnect onConnect={handleSolanaConnect} />
)}
</div>
<div className="mb-8 p-6 rounded-lg" style={{
background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)',
opacity: (paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) ? '1' : '0.6'
<div className="mb-8 p-6 rounded-lg" style={{
background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)',
opacity: solanaWalletState.connected ? '1' : '0.6'
}}>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
Enter URL to Deploy
</h2>
<URLForm
onSubmit={handleUrlSubmit}
disabled={!(paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) || status === 'creating'}
<URLForm
onSubmit={handleUrlSubmit}
disabled={!solanaWalletState.connected || status === 'creating'}
/>
</div>
{status !== 'idle' && (
<div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>4</span>
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
Deployment Status
</h2>
<StatusDisplay
status={status}
txHash={txHash || undefined}
recordId={recordId || undefined}
<StatusDisplay
status={status}
txHash={txHash || undefined}
recordId={recordId || undefined}
appRecordId={appRecordId || undefined}
lrn={lrn || undefined}
dns={dns || undefined}
@ -200,18 +174,17 @@ export default function Home() {
repoUrl={repoUrl || undefined}
commitHash={commitHash || undefined}
shortCommitHash={shortCommitHash || undefined}
error={error || undefined}
error={error || undefined}
/>
</div>
)}
</div>
{showPaymentModal && url && (paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) && (
<PaymentModal
{showPaymentModal && url && solanaWalletState.connected && (
<PaymentModal
isOpen={showPaymentModal}
onClose={handleClosePaymentModal}
url={url}
paymentType={paymentType}
onPaymentComplete={handlePaymentComplete}
/>
)}

View File

@ -1,74 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { connectKeplr } from '@/services/keplr';
interface KeplrConnectProps {
onConnect: (address: string) => void;
}
export default function KeplrConnect({ onConnect }: KeplrConnectProps) {
const [connecting, setConnecting] = useState(false);
const [address, setAddress] = useState<string | null>(null);
const handleConnect = async () => {
setConnecting(true);
try {
const userAddress = await connectKeplr();
if (userAddress) {
setAddress(userAddress);
onConnect(userAddress);
}
} catch (error) {
console.error('Failed to connect to Keplr:', error);
} finally {
setConnecting(false);
}
};
useEffect(() => {
// Check if Keplr is available
if (typeof window !== 'undefined' && window.keplr) {
// Auto-connect on page load
handleConnect();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="flex flex-col items-center p-4 rounded-lg">
{address ? (
<div className="flex flex-col items-center w-full">
<div className="flex items-center mb-2">
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
<p className="font-medium" style={{ color: 'var(--success)' }}>Connected</p>
</div>
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
<p className="text-sm font-mono break-all text-center">{address}</p>
</div>
</div>
) : (
<button
onClick={handleConnect}
disabled={connecting}
className="px-6 py-3 rounded-md w-full sm:w-auto transition-colors"
style={{
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)',
opacity: connecting ? '0.8' : '1',
}}
>
<div className="flex items-center justify-center">
{connecting && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{connecting ? 'Connecting...' : 'Connect Keplr Wallet'}
</div>
</button>
)}
</div>
);
}

View File

@ -1,76 +1,44 @@
'use client';
import { useState } from 'react';
import { sendAtomPayment } from '@/services/keplr';
import { sendGorPayment } from '@/services/solana';
import { sendSolanaTokenPayment } from '@/services/solana';
import { PaymentModalProps } from '@/types';
import { getSolanaConfig, GOR_PAYMENT_AMOUNT } from '@/config';
import { getSolanaConfig } from '@/config';
export default function PaymentModal({
isOpen,
onClose,
url,
paymentType,
onPaymentComplete,
}: PaymentModalProps) {
const [amount, setAmount] = useState(paymentType === 'ATOM' ? '0.01' : GOR_PAYMENT_AMOUNT.toString());
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// Get recipient addresses from environment variables
const atomRecipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
const gorRecipientAddress = paymentType === 'GOR' ?
(process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS || 'solana_recipient_address') : '';
const recipientAddress = paymentType === 'ATOM' ? atomRecipientAddress : gorRecipientAddress;
// Validate amount on change
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setAmount(value);
// Clear error when user types
if (error) {
setError('');
}
};
// Get configuration
const solanaConfig = getSolanaConfig();
const amount = solanaConfig.paymentAmount;
const recipientAddress = solanaConfig.tokenRecipientAddress;
const handlePayment = async () => {
// Validate amount before sending
const parsedAmount = parseFloat(amount);
if (isNaN(parsedAmount) || parsedAmount <= 0) {
setError('Please enter a valid positive amount');
return;
}
setLoading(true);
setError('');
try {
if (paymentType === 'ATOM') {
const result = await sendAtomPayment(recipientAddress, amount);
if (result.status === 'success' && result.hash) {
onPaymentComplete(result.hash);
} else {
setError(result.message || 'ATOM payment failed. Please try again.');
}
} else if (paymentType === 'GOR') {
// For GOR payments, we need wallet info from parent component
// This is a simplified approach - in a real implementation, you'd pass wallet state
const walletInfo = (window as any).solanaWalletInfo;
if (!walletInfo || !walletInfo.publicKey || !walletInfo.walletType) {
setError('Solana wallet not connected. Please connect your wallet first.');
return;
}
const result = await sendGorPayment(walletInfo.publicKey, walletInfo.walletType);
if (result.success && result.transactionSignature) {
onPaymentComplete(result.transactionSignature);
} else {
setError(result.error || 'GOR payment failed. Please try again.');
}
// For Solana payments, we need wallet info from parent component
// This is a simplified approach - in a real implementation, you'd pass wallet state
const walletInfo = (window as any).solanaWalletInfo;
if (!walletInfo || !walletInfo.publicKey || !walletInfo.walletType) {
setError('Solana wallet not connected. Please connect your wallet first.');
return;
}
const result = await sendSolanaTokenPayment(walletInfo.publicKey, walletInfo.walletType);
if (result.success && result.transactionSignature) {
onPaymentComplete(result.transactionSignature);
} else {
setError(result.error || 'GOR payment failed. Please try again.');
}
} catch (error) {
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
@ -83,14 +51,14 @@ export default function PaymentModal({
return (
<div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}>
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
Complete {paymentType} Payment
Complete GOR Payment
</h2>
</div>
<div className="p-6 space-y-6">
<div>
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>URL to be deployed:</p>
@ -98,58 +66,54 @@ export default function PaymentModal({
<code className="text-sm font-mono">{url}</code>
</div>
</div>
<div>
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
<code className="text-sm font-mono break-all block">{recipientAddress}</code>
</div>
</div>
<div>
<label htmlFor="amount" className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
Amount ({paymentType})
Amount (GOR)
</label>
<div className="relative">
<input
id="amount"
type="number"
min={paymentType === 'ATOM' ? "0.01" : "1"}
step={paymentType === 'ATOM' ? "0.01" : "1"}
value={amount}
onChange={handleAmountChange}
disabled={paymentType === 'GOR'} // Fixed amount for GOR
disabled={true} // Fixed amount for Solana tokens
className="w-full p-3 pr-12 rounded-md"
style={{
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: paymentType === 'GOR' ? '0.7' : '1'
opacity: '0.7'
}}
readOnly
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>{paymentType}</span>
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>GOR</span>
</div>
</div>
{paymentType === 'GOR' && (
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Fixed amount required for deployment
</p>
)}
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Fixed amount required for deployment
</p>
</div>
{error && (
<div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
{error}
</div>
)}
</div>
<div className="p-6 flex justify-end space-x-4 border-t" style={{ borderColor: 'var(--card-border)' }}>
<button
onClick={onClose}
className="px-4 py-2 rounded-md transition-colors"
style={{
style={{
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: loading ? '0.5' : '1'
@ -174,7 +138,7 @@ export default function PaymentModal({
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{loading ? 'Processing...' : `Pay with ${paymentType === 'ATOM' ? 'Keplr' : 'Solana Wallet'}`}
{loading ? 'Processing...' : 'Pay with Solana Wallet'}
</button>
</div>
</div>

View File

@ -125,7 +125,7 @@ export default function StatusDisplay({
</div>
)}
{txHash && <InfoItem label="ATOM Payment Transaction Hash" value={txHash} />}
{txHash && <InfoItem label="GOR Payment Transaction Hash" value={txHash} />}
{appRecordId && <InfoItem label="Laconic Application Record ID" value={appRecordId} />}
{recordId && <InfoItem label="Laconic Deployment Request Record ID" value={recordId} />}
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}

View File

@ -40,35 +40,35 @@ export const getDeployerLrn = (): string => {
};
export const getAppName = (): string => {
return process.env.APP_NAME || 'atom-deploy';
return process.env.APP_NAME || 'gor-deploy';
};
export const COSMOS_DENOM = 'uatom';
// Solana/GOR Token Configuration
export const GOR_PAYMENT_AMOUNT = 50; // 50 GOR tokens
export const GOR_TOKEN_DECIMALS = 9; // Standard SPL token decimals
// Solana Token Configuration (configurable)
export const getSolanaConfig = () => {
const requiredEnvVars = [
'NEXT_PUBLIC_SOLANA_RPC_URL',
'NEXT_PUBLIC_GOR_MINT_ADDRESS',
'NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS'
];
// Direct validation without loop to avoid the strange undefined behavior
const rpcUrl = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
const tokenMintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
const tokenRecipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing environment variable: ${envVar}`);
}
if (!rpcUrl) {
throw new Error(`Missing environment variable: NEXT_PUBLIC_SOLANA_RPC_URL`);
}
if (!tokenMintAddress) {
throw new Error(`Missing environment variable: NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS`);
}
if (!tokenRecipientAddress) {
throw new Error(`Missing environment variable: NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS`);
}
return {
rpcUrl: process.env.NEXT_PUBLIC_SOLANA_RPC_URL!,
rpcUrl,
websocketUrl: process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL,
gorMintAddress: process.env.NEXT_PUBLIC_GOR_MINT_ADDRESS!,
gorRecipientAddress: process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS!,
paymentAmount: GOR_PAYMENT_AMOUNT,
tokenDecimals: GOR_TOKEN_DECIMALS
tokenMintAddress,
tokenRecipientAddress,
tokenSymbol: process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'TOKEN',
tokenName: process.env.NEXT_PUBLIC_SOLANA_TOKEN_NAME || 'Solana Token',
paymentAmount: parseInt(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT || '50'),
tokenDecimals: parseInt(process.env.NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS || '9')
};
};
@ -92,6 +92,3 @@ export const getLaconicTransferConfig = () => {
};
};

View File

@ -1,260 +0,0 @@
import { SigningStargateClient } from '@cosmjs/stargate';
import { TransactionResponse } from '../types';
import { COSMOS_DENOM } from '../config';
export const connectKeplr = async (): Promise<string | null> => {
if (!window.keplr) {
alert('Keplr wallet extension is not installed!');
return null;
}
try {
const chainId = process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4';
// Try to suggest chain if custom network
if (chainId !== 'cosmoshub-4') {
try {
// Check if we need to suggest the chain to Keplr
await window.keplr.getKey(chainId).catch(async () => {
// Chain needs to be suggested
if (process.env.NEXT_PUBLIC_COSMOS_RPC_URL) {
await window.keplr.experimentalSuggestChain({
chainId: chainId,
chainName: chainId,
rpc: process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
rest: process.env.NEXT_PUBLIC_COSMOS_REST_URL || process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
bip44: {
coinType: 118,
},
bech32Config: {
bech32PrefixAccAddr: "cosmos",
bech32PrefixAccPub: "cosmospub",
bech32PrefixValAddr: "cosmosvaloper",
bech32PrefixValPub: "cosmosvaloperpub",
bech32PrefixConsAddr: "cosmosvalcons",
bech32PrefixConsPub: "cosmosvalconspub",
},
currencies: [
{
coinDenom: "ATOM",
coinMinimalDenom: "uatom",
coinDecimals: 6,
},
],
feeCurrencies: [
{
coinDenom: "ATOM",
coinMinimalDenom: "uatom",
coinDecimals: 6,
},
],
stakeCurrency: {
coinDenom: "ATOM",
coinMinimalDenom: "uatom",
coinDecimals: 6,
},
gasPriceStep: {
low: 0.01,
average: 0.025,
high: 0.04,
},
});
}
});
} catch (suggestError) {
console.warn("Failed to suggest chain to Keplr:", suggestError);
// Continue anyway, as enable might still work
}
}
// Enable Keplr for the specified chain
await window.keplr.enable(chainId);
const offlineSigner = window.keplr.getOfflineSigner(chainId);
// Get the user's account
const accounts = await offlineSigner.getAccounts();
if (!accounts || accounts.length === 0) {
console.error('No accounts found in Keplr wallet');
return null;
}
return accounts[0].address;
} catch (error) {
console.error('Failed to connect to Keplr wallet:', error);
return null;
}
};
export const sendAtomPayment = async (
recipientAddress: string,
amount: string
): Promise<TransactionResponse> => {
try {
if (!window.keplr) {
return {
hash: '',
status: 'error',
message: 'Keplr wallet extension is not installed!'
};
}
// Validate recipient address is a valid cosmos address
if (!recipientAddress || !recipientAddress.startsWith('cosmos1')) {
return {
hash: '',
status: 'error',
message: 'Invalid recipient address. Must be a valid Cosmos address starting with cosmos1'
};
}
// Validate amount is a positive number
const parsedAmount = parseFloat(amount);
if (isNaN(parsedAmount) || parsedAmount <= 0) {
return {
hash: '',
status: 'error',
message: 'Invalid amount. Must be a positive number'
};
}
// Get the chain ID from environment variables or use default
const chainId = process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4';
// Enable the chain in Keplr, following same logic as connectKeplr
if (chainId !== 'cosmoshub-4') {
try {
// Check if we need to suggest the chain to Keplr
await window.keplr.getKey(chainId).catch(async () => {
// Chain needs to be suggested
if (process.env.NEXT_PUBLIC_COSMOS_RPC_URL) {
await window.keplr.experimentalSuggestChain({
chainId: chainId,
chainName: chainId,
rpc: process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
rest: process.env.NEXT_PUBLIC_COSMOS_REST_URL || process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
bip44: { coinType: 118 },
bech32Config: {
bech32PrefixAccAddr: "cosmos",
bech32PrefixAccPub: "cosmospub",
bech32PrefixValAddr: "cosmosvaloper",
bech32PrefixValPub: "cosmosvaloperpub",
bech32PrefixConsAddr: "cosmosvalcons",
bech32PrefixConsPub: "cosmosvalconspub",
},
currencies: [{ coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 }],
feeCurrencies: [{ coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 }],
stakeCurrency: { coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 },
gasPriceStep: { low: 0.01, average: 0.025, high: 0.04 },
});
}
});
} catch (suggestError) {
console.warn("Failed to suggest chain to Keplr:", suggestError);
// Continue anyway, as enable might still work
}
}
// Enable the chain in Keplr
await window.keplr.enable(chainId);
const offlineSigner = window.keplr.getOfflineSigner(chainId);
// Create the Stargate client
const rpcEndpoint = process.env.NEXT_PUBLIC_COSMOS_RPC_URL;
if (!rpcEndpoint) {
return {
hash: '',
status: 'error',
message: 'NEXT_PUBLIC_COSMOS_RPC_URL environment variable is not set'
};
}
const client = await SigningStargateClient.connectWithSigner(
rpcEndpoint,
offlineSigner
);
// Get the user's account
const accounts = await offlineSigner.getAccounts();
if (!accounts || accounts.length === 0) {
return {
hash: '',
status: 'error',
message: 'No accounts found in Keplr wallet'
};
}
const sender = accounts[0].address;
// Convert amount to microdenom (e.g., ATOM to uatom)
const microAmount = convertToMicroDenom(amount);
// Send the transaction
const result = await client.sendTokens(
sender,
recipientAddress,
[{ denom: COSMOS_DENOM, amount: microAmount }],
{
amount: [{ denom: COSMOS_DENOM, amount: '5000' }],
gas: '200000',
}
);
if (!result || !result.transactionHash) {
return {
hash: '',
status: 'error',
message: 'Transaction did not return a valid hash'
};
}
return {
hash: result.transactionHash,
status: 'success',
};
} catch (error) {
console.error('Failed to send ATOM payment:', error);
// Provide more descriptive error messages for common errors
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('insufficient funds')) {
return {
hash: '',
status: 'error',
message: 'Insufficient funds in your Keplr wallet to complete this transaction'
};
} else if (errorMessage.includes('rejected')) {
return {
hash: '',
status: 'error',
message: 'Transaction was rejected in the Keplr wallet'
};
} else if (errorMessage.includes('timeout')) {
return {
hash: '',
status: 'error',
message: 'Transaction timed out. Please try again'
};
}
return {
hash: '',
status: 'error',
message: error.message
};
}
return {
hash: '',
status: 'error',
message: 'Unknown error occurred while sending payment'
};
}
};
// Helper function to convert from ATOM to uatom (1 ATOM = 1,000,000 uatom)
export const convertToMicroDenom = (amount: string): string => {
const parsedAmount = parseFloat(amount);
if (isNaN(parsedAmount)) {
throw new Error('Invalid amount');
}
return Math.floor(parsedAmount * 1_000_000).toString();
};

View File

@ -25,7 +25,51 @@ export const transferLNTTokens = async (): Promise<LaconicTransferResult> => {
const transferConfig = getLaconicTransferConfig();
const registry = getRegistry();
console.log('Initiating LNT transfer from prefilled account to service provider...');
console.log('Resolving deployer LRN to get payment address...');
// Resolve the deployer LRN to get the payment address
const deployerLrn = process.env.DEPLOYER_LRN;
if (!deployerLrn) {
return {
success: false,
error: 'DEPLOYER_LRN environment variable is required'
};
}
const resolveResult = await registry.resolveNames([deployerLrn]);
console.log('Resolve result:', resolveResult);
if (!resolveResult || resolveResult.length === 0) {
return {
success: false,
error: `Failed to resolve deployer LRN: ${deployerLrn}`
};
}
const deployerRecord = resolveResult[0];
if (!deployerRecord.attributes) {
return {
success: false,
error: 'Deployer record has no attributes'
};
}
// Find the paymentAddress attribute
const paymentAddressAttr = deployerRecord.attributes.find(
(attr: any) => attr.key === 'paymentAddress'
);
if (!paymentAddressAttr || !paymentAddressAttr.value?.string) {
return {
success: false,
error: 'paymentAddress attribute not found in deployer record'
};
}
const paymentAddress = paymentAddressAttr.value.string;
console.log('Found payment address:', paymentAddress);
console.log('Initiating LNT transfer from prefilled account to payment address...');
// Create fee for transaction
const fee = {
@ -33,10 +77,10 @@ export const transferLNTTokens = async (): Promise<LaconicTransferResult> => {
gas: registryConfig.fee.gas || '900000',
};
// Send tokens from prefilled account to service provider
// Send tokens from prefilled account to payment address
const transferResult = await registry.sendCoins(
{
destinationAddress: transferConfig.serviceProviderAddress,
destinationAddress: paymentAddress,
amount: transferConfig.transferAmount,
denom: 'alnt'
},

View File

@ -1,13 +1,11 @@
import axios from 'axios';
import { CreateRecordResponse, PaymentType } from '../types';
import { CreateRecordResponse } from '../types';
export const createApplicationDeploymentRequest = async (
url: string,
txHash: string,
paymentType: PaymentType = 'ATOM'
txHash: string
): Promise<CreateRecordResponse> => {
try {
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${paymentType} payment`);
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using GOR payment`);
// Call our serverless API endpoint to handle the registry interaction
const response = await fetch('/api/registry', {
@ -15,7 +13,7 @@ export const createApplicationDeploymentRequest = async (
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, txHash, paymentType }),
body: JSON.stringify({ url, txHash }),
});
const result = await response.json();
@ -51,63 +49,3 @@ export const createApplicationDeploymentRequest = async (
}
};
export const verifyTransaction = async (txHash: string): Promise<boolean> => {
try {
// Use the public Cosmos API URL for verification queries
const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL;
if (!apiEndpoint) {
console.error('NEXT_PUBLIC_COSMOS_API_URL environment variable not set');
return false;
}
// Use Axios to directly query the Cosmos transaction via REST API
const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`);
// Check if transaction exists and was successful
// The Cosmos API returns a tx_response object with a code field - 0 means success
if (response.data &&
response.data.tx_response &&
response.data.tx_response.code === 0) {
return true;
}
// Also check for successful transactions with code === undefined (some nodes report it this way)
if (response.data &&
response.data.tx_response &&
response.data.tx_response.code === undefined &&
response.data.tx_response.height) {
return true;
}
// Also fallback to checking if the transaction has a height (was included in a block)
if (response.data &&
response.data.tx_response &&
response.data.tx_response.height &&
!response.data.tx_response.code) {
return true;
}
return false;
} catch (error) {
console.error('Failed to verify transaction:', error);
// If the API call fails, try checking a public explorer API as fallback
try {
// Try a different URL format that some RPC nodes might use
const rpcEndpoint = process.env.NEXT_PUBLIC_COSMOS_RPC_URL;
const fallbackResponse = await axios.get(`${rpcEndpoint}/tx?hash=0x${txHash}`);
if (fallbackResponse.data &&
fallbackResponse.data.result &&
(fallbackResponse.data.result.height ||
(fallbackResponse.data.result.tx_result &&
fallbackResponse.data.result.tx_result.code === 0))) {
return true;
}
} catch (fallbackError) {
console.error('Fallback verification also failed:', fallbackError);
}
return false;
}
};

View File

@ -3,10 +3,8 @@ import {
TOKEN_PROGRAM_ID,
createTransferInstruction,
createAssociatedTokenAccountInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID,
getAssociatedTokenAddress
} from '@solana/spl-token';
import BN from 'bn.js';
import { getSolanaConfig } from '../config';
import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types';
@ -79,7 +77,7 @@ export const disconnectSolanaWallet = async (walletType: SolanaWalletType): Prom
}
};
export const sendGorPayment = async (
export const sendSolanaTokenPayment = async (
walletPublicKey: string,
walletType: SolanaWalletType
): Promise<SolanaPaymentResult> => {
@ -102,14 +100,15 @@ export const sendGorPayment = async (
const connection = getConnection();
const senderPublicKey = new PublicKey(walletPublicKey);
const mintPublicKey = new PublicKey(config.gorMintAddress);
const receiverPublicKey = new PublicKey(config.gorRecipientAddress);
const mintPublicKey = new PublicKey(config.tokenMintAddress);
const receiverPublicKey = new PublicKey(config.tokenRecipientAddress);
console.log('Processing GOR payment with keys:', {
console.log(`Processing ${config.tokenSymbol} payment with keys:`, {
sender: senderPublicKey.toBase58(),
mint: mintPublicKey.toBase58(),
receiver: receiverPublicKey.toBase58(),
amount: config.paymentAmount
amount: config.paymentAmount,
token: config.tokenSymbol
});
// Get associated token addresses
@ -180,7 +179,7 @@ export const sendGorPayment = async (
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.feePayer = senderPublicKey;
console.log('Sending GOR payment transaction...');
console.log(`Sending ${config.tokenSymbol} payment transaction...`);
const { signature } = await wallet.signAndSendTransaction(transaction);
console.log('Transaction sent:', signature);
@ -201,7 +200,7 @@ export const sendGorPayment = async (
transactionSignature: signature
};
} catch (error) {
console.error('GOR payment error:', error);
console.error('Solana token payment error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Payment failed'

View File

@ -1,8 +1,6 @@
import { Window as KeplrWindow } from "@keplr-wallet/types";
// extend the global Window interface to include Keplr and Solana wallets
// extend the global Window interface to include Solana wallets
declare global {
interface Window extends KeplrWindow {
interface Window {
phantom?: {
solana?: {
signAndSendTransaction(transaction: any): Promise<{ signature: string }>;
@ -34,27 +32,6 @@ export interface RegistryConfig {
};
}
export interface TransactionResponse {
hash: string;
status: 'success' | 'error';
message?: string;
}
export interface LaconicRecordData {
type: string;
version: string;
name: string;
application: string;
deployer: string;
dns: string;
meta: {
note: string;
repository: string;
repository_ref: string;
};
payment: string;
}
export interface CreateRecordResponse {
id: string;
applicationRecordId?: string;
@ -68,9 +45,6 @@ export interface CreateRecordResponse {
message?: string;
}
// Payment types
export type PaymentType = 'ATOM' | 'GOR';
export type SolanaWalletType = 'phantom' | 'solflare';
export interface SolanaPaymentResult {
@ -83,7 +57,6 @@ export interface PaymentModalProps {
isOpen: boolean;
onClose: () => void;
url: string;
paymentType: PaymentType;
onPaymentComplete: (txHash: string) => void;
}

View File

@ -115,9 +115,8 @@ export const verifySolanaPayment = async (
if (parsed.type === 'transferChecked' || parsed.type === 'transfer') {
const destination = parsed.info.destination;
// We need to verify this transfer was to our expected recipient
// For now, we'll check if the amount matches and trust the verification
if (parsed.info.amount === amount) {
// Verify both amount and destination address
if (parsed.info.amount === amount && destination === config.tokenRecipientAddress) {
foundValidTransfer = true;
break;
}
@ -128,7 +127,7 @@ export const verifySolanaPayment = async (
if (!foundValidTransfer) {
return {
valid: false,
reason: 'Valid GOR transfer not found in transaction'
reason: 'Valid Solana token transfer not found in transaction'
};
}