diff --git a/.env.example b/.env.example index 132ac94..15ccd7f 100644 --- a/.env.example +++ b/.env.example @@ -8,8 +8,8 @@ NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFEL # Multisig address NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR -NEXT_PUBLIC_SOLANA_TOKEN_NAME=GOR Token -NEXT_PUBLIC_MIN_SOLANA_PAYMENT_AMOUNT=400000000 +NEXT_PUBLIC_MIN_SOLANA_PAYMENT_AMOUNT=400000000 # Approx. 5 USD +NEXT_PUBLIC_MIN # UI Configuration (optional) NEXT_PUBLIC_DOMAIN_SUFFIX=apps.vaasl.io @@ -24,10 +24,7 @@ REGISTRY_RPC_ENDPOINT= REGISTRY_BOND_ID= REGISTRY_AUTHORITY= REGISTRY_USER_KEY= -REGISTRY_GAS=900000 -REGISTRY_FEES=900000alnt REGISTRY_GAS_PRICE=0.001 # Application Configuration DEPLOYER_LRN= -LACONIC_TRANSFER_AMOUNT=1000000alnt diff --git a/README.md b/README.md index 2aff3dc..ea62391 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ Client-side (must be prefixed with NEXT_PUBLIC_): - `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_MIN_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") diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index 690d58b..c42525f 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -1,10 +1,11 @@ import BN from 'bn.js'; import { NextRequest, NextResponse } from 'next/server'; -import { Registry } from '@cerc-io/registry-sdk'; -import { GasPrice } from '@cosmjs/stargate'; import axios from 'axios'; + +import { GasPrice } from '@cosmjs/stargate'; import { verifyUnusedSolanaPayment } from '@/utils/solanaVerify'; import { transferLNTTokens } from '@/services/laconicTransfer'; +import { getRegistry, getRegistryConfig } from '@/config'; // Use CAIP convention for chain ID: namespace + reference const SOLANA_CHAIN_ID = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; // Solana mainnet @@ -126,11 +127,13 @@ export const registryTransactionWithRetry = async ( export async function POST(request: NextRequest) { try { // First check if the request body is valid JSON - let url, txHash; + let url, txHash, senderPublicKey; + try { const body = await request.json(); url = body.url; txHash = body.txHash; + senderPublicKey = body.senderPublicKey; if (!url || !txHash) { return NextResponse.json({ @@ -147,7 +150,7 @@ export async function POST(request: NextRequest) { // Verify Solana payment console.log('Step 0: Verifying Solana token payment...'); - const paymentAmount = parseInt(process.env.NEXT_PUBLIC_MIN_SOLANA_PAYMENT_AMOUNT || '50'); + const paymentAmount = parseInt(process.env.NEXT_PUBLIC_MIN_SOLANA_PAYMENT_AMOUNT || '400000000'); const tokenAmount = new BN(paymentAmount); const solanaPaymentResult = await verifyUnusedSolanaPayment(txHash, tokenAmount); @@ -255,19 +258,7 @@ export async function POST(request: NextRequest) { console.log(`Final DNS name with salt: ${cleanDnsName}`); // Set up Registry config - const config = { - chainId: process.env.REGISTRY_CHAIN_ID!, - rpcEndpoint: process.env.REGISTRY_RPC_ENDPOINT!, - gqlEndpoint: process.env.REGISTRY_GQL_ENDPOINT!, - bondId: process.env.REGISTRY_BOND_ID!, - authority: process.env.REGISTRY_AUTHORITY!, - privateKey: process.env.REGISTRY_USER_KEY!, - fee: { - gas: process.env.REGISTRY_GAS || '900000', - fees: process.env.REGISTRY_FEES || '900000alnt', - gasPrice: '0.001alnt', // Hardcoded valid gas price string with denomination - }, - }; + const config = getRegistryConfig() console.log('Registry config:', { ...config, @@ -277,14 +268,10 @@ export async function POST(request: NextRequest) { const deployerLrn = process.env.DEPLOYER_LRN!; // Create Registry client instance - const gasPrice = GasPrice.fromString('0.001alnt'); + const gasPrice = GasPrice.fromString(config.fee.gasPrice); console.log('Using manual gas price:', gasPrice); - const registry = new Registry( - config.gqlEndpoint, - config.rpcEndpoint, - { chainId: config.chainId, gasPrice } - ); + const registry = getRegistry() // Create LRN for the application with commit hash and salt // We already have the salt from earlier, so we use it directly @@ -305,12 +292,6 @@ export async function POST(request: NextRequest) { app_version: '0.0.1' }; - // Create fee for transaction directly - const fee = { - amount: [{ denom: 'alnt', amount: process.env.REGISTRY_FEES?.replace('alnt', '') || '900000' }], - gas: process.env.REGISTRY_GAS || '900000', - }; - console.log('Application record data:', applicationRecord); // Publish the application record @@ -324,7 +305,6 @@ export async function POST(request: NextRequest) { bondId: config.bondId, }, config.privateKey, - fee ) ) as { id?: string }; @@ -356,7 +336,6 @@ export async function POST(request: NextRequest) { lrn }, config.privateKey, - fee ) ); console.log(`Set name mapping: ${lrn} -> ${applicationRecordId}`); @@ -369,7 +348,6 @@ export async function POST(request: NextRequest) { lrn: `${lrn}@${fullHash}` }, config.privateKey, - fee ) ); console.log(`Set name mapping: ${lrn}@${fullHash} -> ${applicationRecordId}`); @@ -403,8 +381,7 @@ export async function POST(request: NextRequest) { external_payment: { chain_id: SOLANA_CHAIN_ID, tx_hash: txHash, - // TODO: Take pubkey from user and add it - // pubkey: '' + pubkey: senderPublicKey } }, payment: finalTxHash, @@ -423,7 +400,6 @@ export async function POST(request: NextRequest) { bondId: config.bondId, }, config.privateKey, - fee ) ) as { id?: string }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 1cf7ee7..74d34e4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import dynamic from 'next/dynamic'; import URLForm from '@/components/URLForm'; @@ -33,17 +33,6 @@ export default function Home() { const [shortCommitHash, setShortCommitHash] = useState(null); const [error, setError] = useState(null); - const handleSolanaConnect = (walletState: SolanaWalletState) => { - setSolanaWalletState(walletState); - // Store wallet info globally for PaymentModal access (simplified approach) - if (typeof window !== 'undefined') { - (window as any).solanaWalletInfo = { - publicKey: walletState.publicKey, - walletType: walletState.walletType - }; - } - }; - const handleConnectWallet = () => { setShowWalletConnection(true); }; @@ -53,50 +42,51 @@ export default function Home() { setShowPaymentModal(true); }; - const handlePaymentComplete = async (hash: string) => { + const handlePaymentComplete = useCallback(async (hash: string) => { + if (!solanaWalletState.publicKey || !url) { + return + } + 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); - - if (result.status === 'success') { - setRecordId(result.id); - if (result.applicationRecordId) { - setAppRecordId(result.applicationRecordId); - } - if (result.lrn) { - setLrn(result.lrn); - } - if (result.dns) { - setDns(result.dns); - } - if (result.appName) { - setAppName(result.appName); - } - if (result.repoUrl) { - setRepoUrl(result.repoUrl); - } - if (result.commitHash) { - setCommitHash(result.commitHash); - } - if (result.shortCommitHash) { - setShortCommitHash(result.shortCommitHash); - } - setStatus('success'); - } else { - setStatus('error'); - setError(result.message || 'Failed to create record in Laconic Registry'); + const result = await createApplicationDeploymentRequest(url, hash, solanaWalletState.publicKey); + if (result.status === 'success') { + setRecordId(result.id); + if (result.applicationRecordId) { + setAppRecordId(result.applicationRecordId); } + if (result.lrn) { + setLrn(result.lrn); + } + if (result.dns) { + setDns(result.dns); + } + if (result.appName) { + setAppName(result.appName); + } + if (result.repoUrl) { + setRepoUrl(result.repoUrl); + } + if (result.commitHash) { + setCommitHash(result.commitHash); + } + if (result.shortCommitHash) { + setShortCommitHash(result.shortCommitHash); + } + setStatus('success'); + } else { + setStatus('error'); + setError(result.message || 'Failed to create record in Laconic Registry'); } } catch (error) { setStatus('error'); setError(error instanceof Error ? error.message : 'An unknown error occurred'); } - }; + }, [solanaWalletState, url]) const handleClosePaymentModal = () => { setShowPaymentModal(false); @@ -136,10 +126,9 @@ export default function Home() { ) : ( - + setSolanaWalletState(walletState)} /> )} -
)} diff --git a/src/components/PaymentModal.tsx b/src/components/PaymentModal.tsx index 102297a..f4ea945 100644 --- a/src/components/PaymentModal.tsx +++ b/src/components/PaymentModal.tsx @@ -11,12 +11,14 @@ export default function PaymentModal({ onClose, url, onPaymentComplete, + walletState, }: PaymentModalProps) { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); // Get configuration from environment variables directly - const amount = parseInt(process.env.NEXT_PUBLIC_MIN_SOLANA_PAYMENT_AMOUNT || '50'); + const amount = parseInt(process.env.NEXT_PUBLIC_MIN_SOLANA_PAYMENT_AMOUNT || '400000000'); + const recipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS; const handlePayment = async () => { @@ -24,16 +26,8 @@ export default function PaymentModal({ setError(''); try { - // 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 tokenAmount = new BN(amount); - const result = await sendSolanaTokenPayment(walletInfo.publicKey, tokenAmount, walletInfo.walletType); + const result = await sendSolanaTokenPayment(walletState.publicKey!, tokenAmount, walletState.walletType!); if (result.success && result.transactionSignature) { onPaymentComplete(result.transactionSignature); @@ -82,7 +76,7 @@ export default function PaymentModal({ { + if (!registryInstance) { + const config = getRegistryConfig(); + const gasPrice = GasPrice.fromString(config.fee.gasPrice + ALNT_DENOM); + + registryInstance = new Registry( + config.gqlEndpoint, + config.rpcEndpoint, + { chainId: config.chainId, gasPrice } + ); + } + return registryInstance; +}; + export const getRegistryConfig = (): RegistryConfig => { // Validate required environment variables const requiredEnvVars = [ @@ -25,27 +44,7 @@ export const getRegistryConfig = (): RegistryConfig => { authority: process.env.REGISTRY_AUTHORITY!, privateKey: process.env.REGISTRY_USER_KEY!, fee: { - gas: process.env.REGISTRY_GAS || '900000', - fees: process.env.REGISTRY_FEES || '900000alnt', - gasPrice: process.env.REGISTRY_GAS_PRICE || '0.025', + gasPrice: process.env.REGISTRY_GAS_PRICE || '0.001', }, }; }; - -export const getLaconicTransferConfig = () => { - const requiredEnvVars = [ - 'REGISTRY_USER_KEY', // Same account as the registry user - 'LACONIC_TRANSFER_AMOUNT' - ]; - - for (const envVar of requiredEnvVars) { - if (!process.env[envVar]) { - throw new Error(`Missing environment variable: ${envVar}`); - } - } - - return { - prefilledPrivateKey: process.env.REGISTRY_USER_KEY!, // Use the same key as registry operations - transferAmount: process.env.LACONIC_TRANSFER_AMOUNT! - }; -}; diff --git a/src/services/laconicTransfer.ts b/src/services/laconicTransfer.ts index 3173d6d..369bca1 100644 --- a/src/services/laconicTransfer.ts +++ b/src/services/laconicTransfer.ts @@ -1,28 +1,13 @@ -import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, parseGasAndFees, Registry } from '@cerc-io/registry-sdk'; -import { DeliverTxResponse, GasPrice } from '@cosmjs/stargate'; -import { getRegistryConfig, getLaconicTransferConfig } from '../config'; -import { LaconicTransferResult } from '../types'; +import { Account, DENOM as ALNT_DENOM } from '@cerc-io/registry-sdk'; +import { DeliverTxResponse } from '@cosmjs/stargate'; + import { registryTransactionWithRetry } from '@/app/api/registry/route'; - -let registryInstance: Registry | null = null; - -const getRegistry = (): Registry => { - if (!registryInstance) { - const config = getRegistryConfig(); - const gasPrice = GasPrice.fromString(config.fee.gasPrice + 'alnt'); - - registryInstance = new Registry( - config.gqlEndpoint, - config.rpcEndpoint, - { chainId: config.chainId, gasPrice } - ); - } - return registryInstance; -}; +import { getRegistry, getRegistryConfig } from '../config'; +import { LaconicTransferResult } from '../types'; export const transferLNTTokens = async (): Promise => { try { - const transferConfig = getLaconicTransferConfig(); + const registryConfig = getRegistryConfig(); const registry = getRegistry(); console.log('Resolving deployer LRN to get payment address...'); @@ -56,15 +41,18 @@ export const transferLNTTokens = async (): Promise => { // Find the paymentAddress attribute const paymentAddress = deployerRecord.attributes.paymentAddress + const deployerMinPayment = deployerRecord.attributes.minimumPayment console.log('Found payment address:', paymentAddress); + console.log('Found minimum payment:', deployerMinPayment); - console.log('Initiating LNT transfer from prefilled account to payment address...'); + console.log('Initiating LNT transfer...'); // Send tokens from prefilled account to payment address const transferResult = await sendTokensToAccount( + registryConfig.privateKey, paymentAddress, - transferConfig.transferAmount + deployerMinPayment ); console.log('LNT transfer result:', transferResult); @@ -89,24 +77,9 @@ export const transferLNTTokens = async (): Promise => { } }; -// Helper function to validate transfer configuration -export const validateLaconicTransferConfig = (): { valid: boolean; error?: string } => { - try { - getLaconicTransferConfig(); - return { valid: true }; - } catch (error) { - return { - valid: false, - error: error instanceof Error ? error.message : 'Invalid Laconic transfer configuration' - }; - } -}; - -const getAccount = async (): Promise => { - const registryConfig = getRegistryConfig(); - +const getAccount = async (accountPrivateKey: string): Promise => { const account = new Account( - Buffer.from(registryConfig.privateKey, 'hex'), + Buffer.from(accountPrivateKey, 'hex'), ); await account.init(); @@ -115,23 +88,15 @@ const getAccount = async (): Promise => { const sendTokensToAccount = async ( + senderPrivateKey: string, receiverAddress: string, amount: string, ): Promise => { - const registryConfig = getRegistryConfig(); + const registry = getRegistry(); - const registry = new Registry( - registryConfig.gqlEndpoint, - registryConfig.rpcEndpoint, - { chainId: registryConfig.chainId }, - ); - - const fee = parseGasAndFees( - registryConfig.fee.gas, - registryConfig.fee.fees, - ); - const account = await getAccount(); + const account = await getAccount(senderPrivateKey); const laconicClient = await registry.getLaconicClient(account); + const txResponse: DeliverTxResponse = await registryTransactionWithRetry( () => laconicClient.sendTokens( @@ -139,11 +104,11 @@ const sendTokensToAccount = async ( receiverAddress, [ { - denom: 'alnt', + denom: ALNT_DENOM, amount, }, ], - fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER, + "auto", ), ); diff --git a/src/services/registry.ts b/src/services/registry.ts index 72b2da9..ff5cb68 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -2,7 +2,8 @@ import { CreateRecordResponse } from '../types'; export const createApplicationDeploymentRequest = async ( url: string, - txHash: string + txHash: string, + senderPublicKey: string, ): Promise => { try { console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment`); @@ -13,7 +14,7 @@ export const createApplicationDeploymentRequest = async ( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ url, txHash }), + body: JSON.stringify({ url, txHash, senderPublicKey }), }); const result = await response.json(); diff --git a/src/types/index.ts b/src/types/index.ts index f2287d0..240b7c3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -26,8 +26,6 @@ export interface RegistryConfig { authority: string; privateKey: string; fee: { - gas: string; - fees: string; gasPrice: string; }; } @@ -58,6 +56,7 @@ export interface PaymentModalProps { onClose: () => void; url: string; onPaymentComplete: (txHash: string) => void; + walletState: SolanaWalletState; } export interface SolanaWalletState { diff --git a/src/utils/solanaVerify.ts b/src/utils/solanaVerify.ts index 370fbd9..3242204 100644 --- a/src/utils/solanaVerify.ts +++ b/src/utils/solanaVerify.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import BN from 'bn.js'; + import { Connection } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; @@ -29,18 +30,6 @@ const extractTxInfo = async (transactionSignature: string): Promise<{ authority: return { authority, amount }; }; - -// Helper to track used signatures (simple in-memory store for now) -const usedSignatures = new Set(); - -export const isSignatureUsed = (transactionSignature: string): boolean => { - return usedSignatures.has(transactionSignature); -}; - -export const markSignatureAsUsed = (transactionSignature: string): void => { - usedSignatures.add(transactionSignature); -}; - export const verifyUnusedSolanaPayment = async ( transactionSignature: string, tokenAmount: BN @@ -51,13 +40,7 @@ export const verifyUnusedSolanaPayment = async ( sender?: string }> => { try { - // Check if signature is already used - if (isSignatureUsed(transactionSignature)) { - return { - valid: false, - reason: 'Transaction signature has already been used' - }; - } + // TODO: Check if provided signature is already used // Fetch transaction details const transactionResult = await connection.getParsedTransaction(transactionSignature, 'confirmed'); @@ -133,9 +116,6 @@ export const verifyUnusedSolanaPayment = async ( }; } - // Mark signature as used - markSignatureAsUsed(transactionSignature); - return { valid: true, amount,