Remove usage of gas fees config and set solana pubkey in deployment request

This commit is contained in:
Shreerang Kale 2025-07-21 11:19:16 +05:30
parent 77981f5bfb
commit 75989d9f34
10 changed files with 99 additions and 199 deletions

View File

@ -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

View File

@ -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")

View File

@ -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 };

View File

@ -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<string | null>(null);
const [error, setError] = useState<string | null>(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() {
</button>
</div>
) : (
<SolanaConnect onConnect={handleSolanaConnect} />
<SolanaConnect onConnect={(walletState) => setSolanaWalletState(walletState)} />
)}
</div>
<div className="mb-8 p-6 rounded-lg" style={{
background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)',
@ -186,6 +175,7 @@ export default function Home() {
onClose={handleClosePaymentModal}
url={url}
onPaymentComplete={handlePaymentComplete}
walletState={solanaWalletState}
/>
)}
</main>

View File

@ -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({
<input
id="amount"
type="number"
value={amount}
value={amount / 1e6} // Token decimals for GOR token
disabled={true} // Fixed amount for Solana tokens
className="w-full p-3 pr-12 rounded-md"
style={{

View File

@ -1,5 +1,24 @@
import { Registry, DENOM as ALNT_DENOM } from '@cerc-io/registry-sdk';
import { GasPrice } from '@cosmjs/stargate';
import { RegistryConfig } from '../types';
let registryInstance: Registry | null = null;
export const getRegistry = (): Registry => {
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!
};
};

View File

@ -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<LaconicTransferResult> => {
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<LaconicTransferResult> => {
// 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<LaconicTransferResult> => {
}
};
// 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<Account> => {
const registryConfig = getRegistryConfig();
const getAccount = async (accountPrivateKey: string): Promise<Account> => {
const account = new Account(
Buffer.from(registryConfig.privateKey, 'hex'),
Buffer.from(accountPrivateKey, 'hex'),
);
await account.init();
@ -115,23 +88,15 @@ const getAccount = async (): Promise<Account> => {
const sendTokensToAccount = async (
senderPrivateKey: string,
receiverAddress: string,
amount: string,
): Promise<DeliverTxResponse> => {
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",
),
);

View File

@ -2,7 +2,8 @@ import { CreateRecordResponse } from '../types';
export const createApplicationDeploymentRequest = async (
url: string,
txHash: string
txHash: string,
senderPublicKey: string,
): Promise<CreateRecordResponse> => {
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();

View File

@ -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 {

View File

@ -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<string>();
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,