Refactor code to use solana wallet adapters

This commit is contained in:
Shreerang Kale 2025-07-23 16:41:43 +05:30
parent 8b220a0dee
commit 9995e76e87
13 changed files with 665 additions and 520 deletions

122
package-lock.json generated
View File

@ -13,8 +13,10 @@
"@solana/spl-token": "^0.4.13",
"@solana/wallet-adapter-backpack": "^0.1.9",
"@solana/wallet-adapter-base": "^0.9.18",
"@solana/wallet-adapter-phantom": "^0.9.28",
"@solana/wallet-adapter-react": "^0.15.20",
"@solana/wallet-adapter-react-ui": "^0.9.18",
"@solana/wallet-adapter-solflare": "^0.6.32",
"@solana/web3.js": "^1.98.2",
"axios": "^1.6.8",
"big.js": "^6.2.2",
@ -3470,6 +3472,21 @@
"react": "*"
}
},
"node_modules/@solana/wallet-adapter-phantom": {
"version": "0.9.28",
"resolved": "https://registry.npmjs.org/@solana/wallet-adapter-phantom/-/wallet-adapter-phantom-0.9.28.tgz",
"integrity": "sha512-g/hcuWwWjzo5l8I4vor9htniVhLxd/GhoVK52WSd0hy8IZ8/FBnV3u8ABVTheLqO13d0IVy+xTxoVBbDaMjLog==",
"license": "Apache-2.0",
"dependencies": {
"@solana/wallet-adapter-base": "^0.9.27"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@solana/web3.js": "^1.98.0"
}
},
"node_modules/@solana/wallet-adapter-react": {
"version": "0.15.39",
"resolved": "https://registry.npmjs.org/@solana/wallet-adapter-react/-/wallet-adapter-react-0.15.39.tgz",
@ -3507,6 +3524,25 @@
"react-dom": "*"
}
},
"node_modules/@solana/wallet-adapter-solflare": {
"version": "0.6.32",
"resolved": "https://registry.npmjs.org/@solana/wallet-adapter-solflare/-/wallet-adapter-solflare-0.6.32.tgz",
"integrity": "sha512-FIqNyooif3yjPnw2gPNBZnsG6X9JYSrwCf1Oa0NN4/VxQcPjzGqvc+Tq1+js/nBOHju5roToeMFTbwNTdEOuZw==",
"license": "Apache-2.0",
"dependencies": {
"@solana/wallet-adapter-base": "^0.9.27",
"@solana/wallet-standard-chains": "^1.1.1",
"@solflare-wallet/metamask-sdk": "^1.0.3",
"@solflare-wallet/sdk": "^1.4.2",
"@wallet-standard/wallet": "^1.1.0"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@solana/web3.js": "^1.98.0"
}
},
"node_modules/@solana/wallet-standard": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@solana/wallet-standard/-/wallet-standard-1.1.4.tgz",
@ -3778,6 +3814,92 @@
"node": ">=20"
}
},
"node_modules/@solflare-wallet/metamask-sdk": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@solflare-wallet/metamask-sdk/-/metamask-sdk-1.0.3.tgz",
"integrity": "sha512-os5Px5PTMYKGS5tzOoyjDxtOtj0jZKnbI1Uwt8+Jsw1HHIA+Ib2UACCGNhQ/un2f8sIbTfLD1WuucNMOy8KZpQ==",
"license": "Apache-2.0",
"dependencies": {
"@solana/wallet-standard-features": "^1.1.0",
"@wallet-standard/base": "^1.0.1",
"bs58": "^5.0.0",
"eventemitter3": "^5.0.1",
"uuid": "^9.0.0"
},
"peerDependencies": {
"@solana/web3.js": "*"
}
},
"node_modules/@solflare-wallet/metamask-sdk/node_modules/base-x": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
"license": "MIT"
},
"node_modules/@solflare-wallet/metamask-sdk/node_modules/bs58": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"license": "MIT",
"dependencies": {
"base-x": "^4.0.0"
}
},
"node_modules/@solflare-wallet/metamask-sdk/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@solflare-wallet/sdk": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@solflare-wallet/sdk/-/sdk-1.4.2.tgz",
"integrity": "sha512-jrseNWipwl9xXZgrzwZF3hhL0eIVxuEtoZOSLmuPuef7FgHjstuTtNJAeT4icA7pzdDV4hZvu54pI2r2f7SmrQ==",
"license": "Apache-2.0",
"dependencies": {
"bs58": "^5.0.0",
"eventemitter3": "^5.0.1",
"uuid": "^9.0.0"
},
"peerDependencies": {
"@solana/web3.js": "*"
}
},
"node_modules/@solflare-wallet/sdk/node_modules/base-x": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
"license": "MIT"
},
"node_modules/@solflare-wallet/sdk/node_modules/bs58": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"license": "MIT",
"dependencies": {
"base-x": "^4.0.0"
}
},
"node_modules/@solflare-wallet/sdk/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",

View File

@ -12,11 +12,13 @@
"@cerc-io/registry-sdk": "^0.2.11",
"@cosmjs/stargate": "^0.32.3",
"@solana/spl-token": "^0.4.13",
"@solana/web3.js": "^1.98.2",
"@solana/wallet-adapter-backpack": "^0.1.9",
"@solana/wallet-adapter-base": "^0.9.18",
"@solana/wallet-adapter-phantom": "^0.9.28",
"@solana/wallet-adapter-react": "^0.15.20",
"@solana/wallet-adapter-react-ui": "^0.9.18",
"@solana/wallet-adapter-solflare": "^0.6.32",
"@solana/web3.js": "^1.98.2",
"axios": "^1.6.8",
"big.js": "^6.2.2",
"bn.js": "^5.2.2",

View File

@ -11,6 +11,8 @@ import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
import { transferLNTTokens } from '@/services/laconic-transfer';
import { getRegistry, getRegistryConfig } from '@/config';
import { getRequiredTokenInfo } from '@/services/jupiter-price';
import { PaymentMethod, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments';
import { getRecipientAddress } from '@/services/solana';
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
@ -143,12 +145,13 @@ export async function POST(request: NextRequest) {
}
// First check if the request body is valid JSON
let url, txHash, senderPublicKey;
let url, txHash, senderPublicKey, paymentMethod;
try {
const body = await request.json();
url = body.url;
txHash = body.txHash;
paymentMethod = body.paymentMethod as PaymentMethod;
const tx = await connection.getParsedTransaction(txHash, 'confirmed');
if (!tx) {
@ -165,10 +168,18 @@ export async function POST(request: NextRequest) {
senderPublicKey = signerKeys[0];
if (!url || !txHash) {
if (!url || !txHash || !paymentMethod) {
return NextResponse.json({
status: 'error',
message: 'Missing required fields: url and txHash are required'
message: 'Missing required fields: url, txHash, and paymentMethod are required'
}, { status: 400 });
}
// Validate payment method
if (paymentMethod !== 'sol' && paymentMethod !== 'spl-token') {
return NextResponse.json({
status: 'error',
message: 'Invalid payment method. Must be "sol" or "spl-token".'
}, { status: 400 });
}
} catch (error) {
@ -180,36 +191,58 @@ export async function POST(request: NextRequest) {
}, { status: 400 });
}
// Verify Solana payment
console.log('Step 0: Verifying Solana token payment...');
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
// Verify Solana payment based on method
console.log(`Step 0: Verifying Solana ${paymentMethod} payment...`);
// Calculate expected token amount based on current price
let expectedTokenAmount: number;
try {
const { requiredAmountInBaseUnits } = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
} catch (error) {
console.error('Error calculating token amount:', error);
let expectedAmount: BN;
let expectedRecipientAddress: string;
if (paymentMethod === 'sol') {
expectedAmount = new BN(SOL_PAYMENT_AMOUNT_LAMPORTS);
expectedRecipientAddress = getRecipientAddress('sol');
} else if (paymentMethod === 'spl-token') {
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
// Calculate expected token amount based on current price
let expectedTokenAmount: number;
try {
const { requiredAmountInBaseUnits } = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
} catch (error) {
console.error('Error calculating token amount:', error);
return NextResponse.json({
status: 'error',
message: 'Unable to verify payment due to price calculation error'
}, { status: 500 });
}
expectedAmount = new BN(expectedTokenAmount);
expectedRecipientAddress = getRecipientAddress('spl-token');
} else {
return NextResponse.json({
status: 'error',
message: 'Unable to verify payment due to price calculation error'
}, { status: 500 });
message: 'Unsupported payment method'
}, { status: 400 });
}
const tokenAmount = new BN(expectedTokenAmount);
const solanaPaymentResult = await verifyUnusedSolanaPayment(connection, txHash, tokenAmount);
const solanaPaymentResult = await verifyUnusedSolanaPayment(
connection,
txHash,
expectedAmount,
paymentMethod,
expectedRecipientAddress
);
if (!solanaPaymentResult.valid) {
console.error('Solana token payment verification failed:', solanaPaymentResult.reason);
console.error(`Solana ${paymentMethod} payment verification failed:`, solanaPaymentResult.reason);
return NextResponse.json({
status: 'error',
message: `Payment verification failed: ${solanaPaymentResult.reason}`
}, { status: 400 });
}
console.log('Solana token payment verified successfully:', {
console.log(`Solana ${paymentMethod} payment verified successfully:`, {
amount: solanaPaymentResult.amount,
sender: solanaPaymentResult.sender
});

View File

@ -1,27 +1,20 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import dynamic from 'next/dynamic';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import URLForm from '@/components/URLForm';
import StatusDisplay from '@/components/StatusDisplay';
import { createApplicationDeploymentRequest } from '@/services/registry';
import { SolanaWalletState } from '@/types';
import { PaymentMethod } from '@/constants/payments';
import { useWallet } from '@solana/wallet-adapter-react';
// 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 { wallet, connected } = useWallet()
const [showWalletConnection, setShowWalletConnection] = useState(false);
const [solanaWalletState, setSolanaWalletState] = useState<SolanaWalletState>({
connected: false,
publicKey: null,
walletType: null
});
const { wallet, connected, publicKey } = useWallet();
const [url, setUrl] = useState<string | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle');
@ -32,34 +25,15 @@ export default function Home() {
const [dns, setDns] = useState<string | null>(null);
const [appName, setAppName] = useState<string | null>(null);
const [repoUrl, setRepoUrl] = useState<string | null>(null);
const [commitHash, setCommitHash] = useState<string | null>(null);
const [shortCommitHash, setShortCommitHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!wallet || !connected || !wallet.adapter.publicKey) {
return;
}
setSolanaWalletState({
connected,
publicKey: wallet.adapter.publicKey.toString(),
walletType: 'backpack'
})
}, [connected, wallet])
const handleConnectWallet = () => {
setShowWalletConnection(true);
};
const handleUrlSubmit = (submittedUrl: string) => {
setUrl(submittedUrl);
setShowPaymentModal(true);
};
const handlePaymentComplete = useCallback(async (hash: string) => {
if (!solanaWalletState.publicKey || !url) {
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
if (!publicKey || !url) {
return
}
@ -69,7 +43,7 @@ export default function Home() {
try {
// Create the Laconic Registry record (payment verification is done in the API)
const result = await createApplicationDeploymentRequest(url, hash);
const result = await createApplicationDeploymentRequest(url, hash, paymentMethod);
if (result.status === 'success') {
setRecordId(result.id);
@ -88,12 +62,6 @@ export default function Home() {
if (result.repoUrl) {
setRepoUrl(result.repoUrl);
}
if (result.commitHash) {
setCommitHash(result.commitHash);
}
if (result.shortCommitHash) {
setShortCommitHash(result.shortCommitHash);
}
setStatus('success');
} else {
setStatus('error');
@ -103,7 +71,7 @@ export default function Home() {
setStatus('error');
setError(error instanceof Error ? error.message : 'An unknown error occurred');
}
}, [solanaWalletState, url])
}, [publicKey, url])
const handleClosePaymentModal = () => {
setShowPaymentModal(false);
@ -123,37 +91,47 @@ export default function Home() {
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
Connect Your Wallet
</h2>
{!showWalletConnection ? (
<div className="text-center">
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
Payment method: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (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 className="mt-4">
<WalletMultiButton />
<div className="text-center">
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
Payment methods: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
SOL or {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana)
</span>
</p>
{connected && publicKey ? (
<div className="flex flex-col items-center space-y-3">
<div className="flex items-center">
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
<p className="font-medium" style={{ color: 'var(--success)' }}>
Connected ({wallet?.adapter.name})
</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">{publicKey.toBase58()}</p>
</div>
<WalletMultiButton
className="!px-4 !py-2 !rounded-md !text-sm !transition-colors"
style={{
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
border: '1px solid var(--input-border)'
}}
/>
</div>
</div>
) : (
<SolanaConnect onConnect={(walletState) => setSolanaWalletState(walletState)} />
)}
) : (
<WalletMultiButton
className="!px-6 !py-3 !rounded-md !w-full !transition-colors"
style={{
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
}}
/>
)}
</div>
</div>
<div className="mb-8 p-6 rounded-lg" style={{
background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)',
opacity: solanaWalletState.connected ? '1' : '0.6'
opacity: 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"
@ -162,7 +140,7 @@ export default function Home() {
</h2>
<URLForm
onSubmit={handleUrlSubmit}
disabled={!solanaWalletState.connected || status === 'creating'}
disabled={!connected || status === 'creating'}
/>
</div>
@ -182,21 +160,18 @@ export default function Home() {
dns={dns || undefined}
appName={appName || undefined}
repoUrl={repoUrl || undefined}
commitHash={commitHash || undefined}
shortCommitHash={shortCommitHash || undefined}
error={error || undefined}
/>
</div>
)}
</div>
{showPaymentModal && url && solanaWalletState.connected && (
{showPaymentModal && url && connected && publicKey && (
<PaymentModal
isOpen={showPaymentModal}
onClose={handleClosePaymentModal}
url={url}
onPaymentComplete={handlePaymentComplete}
walletState={solanaWalletState}
/>
)}
</main>

View File

@ -1,15 +1,15 @@
'use client';
import { useCallback, useMemo, useState, useEffect } from 'react';
import BN from 'bn.js';
import assert from 'assert';
import { Connection } from '@solana/web3.js';
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useWallet } from '@solana/wallet-adapter-react';
import { sendSolanaTokenPayment } from '@/services/solana';
import { sendSolanaPayment, getRecipientAddress } from '@/services/solana';
import { getRequiredTokenInfo } from '@/services/jupiter-price';
import { PaymentModalProps } from '@/types';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PaymentModalProps, PaymentRequest } from '@/types';
import { PaymentMethod, PAYMENT_METHOD_LABELS, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments';
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
@ -19,34 +19,36 @@ export default function PaymentModal({
onClose,
url,
onPaymentComplete,
walletState,
}: PaymentModalProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
const [tokenAmount, setTokenAmount] = useState<number>(0);
const [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback
const [loadingPrice, setLoadingPrice] = useState(true);
const [loadingPrice, setLoadingPrice] = useState(false);
const { wallet } = useWallet()
const {connection: backConn} = useConnection();
const { wallet, publicKey } = useWallet();
const connection = useMemo(() => new Connection(SOLANA_RPC_URL), [])
const directConnection = useMemo(() => new Connection(SOLANA_RPC_URL), []);
// Get configuration from environment variables
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
const recipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
const tokenSymbol = process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'TOKEN';
// Fetch token amount based on USD price
// Fetch token amount based on USD price when SPL token method is selected
useEffect(() => {
if (!isOpen) return;
if (!isOpen || selectedPaymentMethod !== 'spl-token') {
setLoadingPrice(false);
return;
}
const fetchTokenAmount = async () => {
setLoadingPrice(true);
setError('');
try {
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress)
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
setTokenAmount(requiredAmountInBaseUnits);
setTokenDecimals(decimals);
} catch (error) {
@ -58,11 +60,41 @@ export default function PaymentModal({
};
fetchTokenAmount();
}, [isOpen, targetUsdAmount, mintAddress]);
}, [isOpen, selectedPaymentMethod, targetUsdAmount, mintAddress]);
// Reset state when modal opens
useEffect(() => {
if (isOpen) {
setSelectedPaymentMethod(null);
setError('');
setTokenAmount(0);
setLoadingPrice(false);
}
}, [isOpen]);
const handlePaymentMethodChange = (method: PaymentMethod) => {
setSelectedPaymentMethod(method);
setError('');
// Set tokenAmount for SOL payments to maintain consistency
if (method === 'sol') {
setTokenAmount(SOL_PAYMENT_AMOUNT_LAMPORTS);
}
};
const handlePayment = useCallback(async () => {
if (tokenAmount === 0 || loadingPrice) {
setError('Token amount not calculated yet. Please wait.');
if (!selectedPaymentMethod) {
setError('Please select a payment method.');
return;
}
if (tokenAmount === 0 || (selectedPaymentMethod === 'spl-token' && loadingPrice)) {
setError('Payment amount not ready. Please wait.');
return;
}
if (!wallet?.adapter) {
setError('Wallet not connected.');
return;
}
@ -70,21 +102,44 @@ export default function PaymentModal({
setError('');
try {
const tokenAmountBN = new BN(tokenAmount);
const paymentRequest: PaymentRequest = {
paymentMethod: selectedPaymentMethod,
amount: tokenAmount,
recipientAddress: getRecipientAddress(selectedPaymentMethod)
};
const result = await sendSolanaTokenPayment(wallet!.adapter, backConn, connection, walletState.publicKey!, tokenAmountBN, walletState.walletType!);
const result = await sendSolanaPayment(
wallet.adapter,
directConnection,
publicKey!.toBase58(),
paymentRequest
);
if (result.success && result.transactionSignature) {
onPaymentComplete(result.transactionSignature);
onPaymentComplete(result.transactionSignature, selectedPaymentMethod);
} else {
setError(result.error || `${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment failed. Please try again.`);
setError(result.error || 'Payment failed. Please try again.');
}
} catch (error) {
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
} finally {
setLoading(false);
}
}, [connection, walletState, tokenAmount, loadingPrice, onPaymentComplete, backConn, wallet]);
}, [selectedPaymentMethod, tokenAmount, loadingPrice, wallet, directConnection, publicKey, onPaymentComplete]);
const getPaymentAmountDisplay = () => {
if (!selectedPaymentMethod) return '';
switch (selectedPaymentMethod) {
case 'sol':
return `${SOL_PAYMENT_AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL`;
case 'spl-token':
if (loadingPrice) return 'Loading...';
return tokenAmount > 0 ? `${(tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6)} ${tokenSymbol}` : '0';
default:
return '';
}
};
if (!isOpen) return null;
@ -94,7 +149,7 @@ export default function PaymentModal({
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 {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment
Complete Payment
</h2>
</div>
@ -106,76 +161,111 @@ export default function PaymentModal({
</div>
</div>
{/* Payment Method Selection */}
<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>
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--foreground)' }}>
Select Payment Method *
</label>
<div className="space-y-2">
{Object.entries(PAYMENT_METHOD_LABELS).map(([method, label]) => (
<label key={method} className="flex items-center p-3 rounded-md cursor-pointer border"
style={{
borderColor: selectedPaymentMethod === method ? 'var(--primary)' : 'var(--input-border)',
backgroundColor: selectedPaymentMethod === method ? 'var(--primary-light)' : 'var(--card-bg)'
}}>
<input
type="radio"
name="paymentMethod"
value={method}
checked={selectedPaymentMethod === method}
onChange={() => handlePaymentMethodChange(method as PaymentMethod)}
className="mr-3"
/>
<span style={{ color: 'var(--foreground)' }}>{label}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
Payment Amount
</label>
<div className="space-y-3">
<div className="relative">
<input
type="text"
value={`$${targetUsdAmount}`}
disabled={true}
className="w-full p-3 pr-12 rounded-md"
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
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)' }}>USD</span>
{/* Payment Details - Only show when method is selected */}
{selectedPaymentMethod && (
<>
<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">{getRecipientAddress(selectedPaymentMethod)}</code>
</div>
</div>
<div className="relative">
{loadingPrice ? (
<div className="w-full p-3 rounded-md flex items-center" style={{
background: 'var(--muted-light)',
border: '1px solid var(--input-border)',
color: 'var(--muted)'
}}>
<svg className="animate-spin h-4 w-4 mr-2" 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>
Fetching token amount...
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
Payment Amount
</label>
<div className="space-y-3">
<div className="relative">
<input
type="text"
value={targetUsdAmount}
disabled={true}
className="w-full p-3 pr-12 rounded-md"
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
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)' }}>USD</span>
</div>
</div>
) : (
<input
type="text"
value={tokenAmount > 0 ? (tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6) : '0'}
disabled={true}
className="w-full p-3 pr-12 rounded-md"
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: '0.7'
}}
readOnly
/>
)}
{!loadingPrice && (
<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)' }}>{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL}</span>
<div className="relative">
{(selectedPaymentMethod === 'spl-token' && loadingPrice) ? (
<div className="w-full p-3 rounded-md flex items-center" style={{
background: 'var(--muted-light)',
border: '1px solid var(--input-border)',
color: 'var(--muted)'
}}>
<svg className="animate-spin h-4 w-4 mr-2" 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>
Fetching token amount...
</div>
) : (
<input
type="text"
value={getPaymentAmountDisplay()}
disabled={true}
className="w-full p-3 pr-20 rounded-md"
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: '0.7'
}}
readOnly
/>
)}
{!(selectedPaymentMethod === 'spl-token' && loadingPrice) && (
<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)' }}>
{selectedPaymentMethod === 'sol' ? 'SOL' : tokenSymbol}
</span>
</div>
)}
</div>
</div>
{selectedPaymentMethod === 'spl-token' && (
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
</p>
)}
</div>
</div>
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS}`}>Jupiter</a>
</p>
</div>
</>
)}
{error && (
<div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
@ -201,11 +291,11 @@ export default function PaymentModal({
onClick={handlePayment}
className="px-5 py-2 rounded-md flex items-center transition-colors"
style={{
backgroundColor: (loading || loadingPrice) ? 'var(--muted)' : 'var(--primary)',
backgroundColor: (loading || loadingPrice || !selectedPaymentMethod) ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)',
opacity: (loading || loadingPrice) ? '0.8' : '1'
opacity: (loading || loadingPrice || !selectedPaymentMethod) ? '0.8' : '1'
}}
disabled={loading || loadingPrice}
disabled={loading || loadingPrice || !selectedPaymentMethod}
>
{(loading || loadingPrice) && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@ -213,10 +303,13 @@ 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>
)}
{loadingPrice ? 'Loading Price...' : loading ? 'Processing...' : 'Pay with Solana Wallet'}
{!selectedPaymentMethod ? 'Select Payment Method' :
loadingPrice ? 'Loading Price...' :
loading ? 'Processing...' :
'Pay with Solana Wallet'}
</button>
</div>
</div>
</div>
);
}
}

View File

@ -1,148 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { connectSolanaWallet, disconnectSolanaWallet, checkSolanaWalletConnection } from '@/services/solana';
import { SolanaWalletType, SolanaWalletState } from '@/types';
interface SolanaConnectProps {
onConnect: (walletState: SolanaWalletState) => void;
}
export default function SolanaConnect({ onConnect }: SolanaConnectProps) {
const [connecting, setConnecting] = useState(false);
const [walletState, setWalletState] = useState<SolanaWalletState>({
connected: false,
publicKey: null,
walletType: null
});
const handleConnect = async (walletType: SolanaWalletType) => {
setConnecting(true);
try {
const newWalletState = await connectSolanaWallet(walletType);
setWalletState(newWalletState);
onConnect(newWalletState);
} catch (error) {
console.error('Failed to connect to Solana wallet:', error);
alert(error instanceof Error ? error.message : 'Failed to connect wallet');
} finally {
setConnecting(false);
}
};
const handleDisconnect = async () => {
if (walletState.walletType) {
try {
await disconnectSolanaWallet(walletState.walletType);
const disconnectedState = {
connected: false,
publicKey: null,
walletType: null
};
setWalletState(disconnectedState);
onConnect(disconnectedState);
} catch (error) {
console.error('Failed to disconnect wallet:', error);
}
}
};
useEffect(() => {
// Check for auto-connection on page load
const checkConnection = () => {
if (typeof window !== 'undefined') {
// Check Phantom
if (window.phantom?.solana && checkSolanaWalletConnection('phantom')) {
handleConnect('phantom');
return;
}
// Check Solflare
if (window.solflare && checkSolanaWalletConnection('solflare')) {
handleConnect('solflare');
return;
}
}
};
checkConnection();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="flex flex-col items-center p-4 rounded-lg">
{walletState.connected ? (
<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 ({walletState.walletType})
</p>
</div>
<div className="w-full p-3 rounded-md mb-3" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
<p className="text-sm font-mono break-all text-center">{walletState.publicKey}</p>
</div>
<button
onClick={handleDisconnect}
className="px-4 py-2 rounded-md text-sm transition-colors"
style={{
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
border: '1px solid var(--input-border)'
}}
>
Disconnect
</button>
</div>
) : (
<div className="w-full space-y-3">
<button
onClick={() => handleConnect('phantom')}
disabled={connecting || !window.phantom?.solana}
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
style={{
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)',
}}
>
<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 Phantom Wallet'}
</div>
</button>
<button
onClick={() => handleConnect('solflare')}
disabled={connecting || !window.solflare}
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
style={{
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)',
}}
>
<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 Solflare Wallet'}
</div>
</button>
{!window.phantom?.solana && !window.solflare && (
<p className="text-sm text-center" style={{ color: 'var(--muted)' }}>
Please install Phantom or Solflare wallet extension
</p>
)}
</div>
)}
</div>
);
}

View File

@ -9,8 +9,6 @@ interface StatusDisplayProps {
dns?: string;
appName?: string;
repoUrl?: string;
commitHash?: string;
shortCommitHash?: string;
error?: string;
}
@ -23,8 +21,6 @@ export default function StatusDisplay({
dns,
appName,
repoUrl,
commitHash,
shortCommitHash,
error,
}: StatusDisplayProps) {
// Get domain suffix from environment variable

View File

@ -4,6 +4,8 @@ import { useMemo } from 'react';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { BackpackWalletAdapter } from '@solana/wallet-adapter-backpack';
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom';
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare';
// Default styles that can be overridden by your app
import '@solana/wallet-adapter-react-ui/styles.css';
@ -23,7 +25,11 @@ export default function WalletProviders({ children }: WalletProvidersProps) {
}, []);
const wallets = useMemo(
() => [new BackpackWalletAdapter()],
() => [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
new BackpackWalletAdapter()
],
[]
);

16
src/constants/payments.ts Normal file
View File

@ -0,0 +1,16 @@
// Payment configuration constants
// SOL payment amount in lamports (1 SOL = 1,000,000,000 lamports)
export const SOL_PAYMENT_AMOUNT_LAMPORTS = 10000000; // 0.01 SOL
// Payment method types
export type PaymentMethod = 'sol' | 'spl-token';
// Payment method labels for UI
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
'sol': 'SOL (Native)',
'spl-token': process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'SPL Token'
};
// Default payment method (none selected initially)
export const DEFAULT_PAYMENT_METHOD: PaymentMethod | null = null;

View File

@ -1,8 +1,10 @@
import { CreateRecordResponse } from '../types';
import { PaymentMethod } from '../constants/payments';
export const createApplicationDeploymentRequest = async (
url: string,
txHash: string,
paymentMethod: PaymentMethod,
): 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 +15,7 @@ export const createApplicationDeploymentRequest = async (
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, txHash }),
body: JSON.stringify({ url, txHash, paymentMethod }),
});
const result = await response.json();

View File

@ -5,11 +5,13 @@ import {
TOKEN_PROGRAM_ID,
createTransferInstruction,
createAssociatedTokenAccountInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID
ASSOCIATED_TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync
} from '@solana/spl-token';
import { Adapter } from '@solana/wallet-adapter-base';
import { WalletAdapter } from '@solana/wallet-adapter-base';
import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types';
import { SolanaPaymentResult, PaymentRequest } from '../types';
import { PaymentMethod, SOL_PAYMENT_AMOUNT_LAMPORTS } from '../constants/payments';
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS, 'SOLANA_TOKEN_MINT_ADDRESS is required');
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
@ -17,59 +19,7 @@ assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_REC
const TOKEN_MINT = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
export const connectSolanaWallet = async (walletType: SolanaWalletType): Promise<SolanaWalletState> => {
try {
if (walletType === 'phantom') {
if (!window.phantom?.solana) {
throw new Error('Phantom wallet not found. Please install Phantom browser extension.');
}
const response = await window.phantom.solana.connect();
return {
connected: true,
publicKey: response.publicKey.toString(),
walletType
};
} else if (walletType === 'solflare') {
if (!window.solflare) {
throw new Error('Solflare wallet not found. Please install Solflare browser extension.');
}
await window.solflare.connect();
const publicKey = window.solflare.publicKey?.toString();
if (!publicKey) {
throw new Error('Failed to get public key from Solflare wallet');
}
return {
connected: true,
publicKey,
walletType
};
}
throw new Error(`Unsupported wallet type: ${walletType}`);
} catch (error) {
console.error('Failed to connect to Solana wallet:', error);
throw error;
}
};
export const disconnectSolanaWallet = async (walletType: SolanaWalletType): Promise<void> => {
try {
let wallet = null;
if (walletType === 'phantom') {
wallet = window.phantom?.solana;
} else if (walletType === 'solflare') {
wallet = window.solflare;
}
if (wallet && wallet.disconnect) {
await wallet.disconnect();
}
} catch (error) {
console.error('Failed to disconnect Solana wallet:', error);
}
};
// Helper function to find associated token address
async function findAssociatedTokenAddress(
walletAddress: PublicKey,
tokenMintAddress: PublicKey
@ -84,41 +34,89 @@ async function findAssociatedTokenAddress(
)[0];
}
interface WalletAdapter {
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
}
export async function sendSolanaTokenPayment(
backWallet: WalletAdapter | Adapter,
backConnection: Connection,
// Send SOL (native token) payment
export async function sendSolPayment(
wallet: WalletAdapter,
connection: Connection,
walletPublicKey: string,
tokenAmount: BN,
walletType: SolanaWalletType
walletPublicKey: string
): Promise<SolanaPaymentResult> {
try {
let wallet: WalletAdapter | Adapter | null = null;
if (walletType === 'phantom') {
wallet = window.phantom?.solana || null;
} else if (walletType === 'solflare') {
wallet = window.solflare || null;
} else if (walletType === 'backpack') {
wallet = backWallet
if (!wallet.connected || !wallet.publicKey) {
throw new Error('Wallet not connected');
}
if (!wallet) {
throw new Error(`${walletType} wallet not found`);
const senderPublicKey = new PublicKey(walletPublicKey);
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
console.log('Processing SOL payment:', {
sender: senderPublicKey.toBase58(),
receiver: receiverPublicKey.toBase58(),
amount: SOL_PAYMENT_AMOUNT_LAMPORTS
});
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: senderPublicKey,
toPubkey: receiverPublicKey,
lamports: SOL_PAYMENT_AMOUNT_LAMPORTS,
})
);
// Set transaction details
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.feePayer = senderPublicKey;
console.log('Sending SOL transaction...');
const signature = await wallet.sendTransaction(transaction, connection);
console.log('SOL transaction sent:', signature);
// Confirm transaction
const confirmation = await connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
}, 'confirmed');
if (confirmation.value.err) {
console.error('Transaction error:', confirmation.value.err);
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
}
return {
success: true,
transactionSignature: signature
};
} catch (error) {
console.error('SOL payment error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'SOL payment failed'
};
}
}
// Send SPL token payment
export async function sendSplTokenPayment(
wallet: WalletAdapter,
connection: Connection,
walletPublicKey: string,
tokenAmount: BN
): Promise<SolanaPaymentResult> {
try {
if (!wallet.connected || !wallet.publicKey) {
throw new Error('Wallet not connected');
}
const senderPublicKey = new PublicKey(walletPublicKey);
const mintPublicKey = new PublicKey(TOKEN_MINT);
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
console.log('Processing payment with keys:', {
console.log('Processing SPL token payment:', {
sender: senderPublicKey.toBase58(),
mint: mintPublicKey.toBase58(),
receiver: receiverPublicKey.toBase58(),
amount: tokenAmount.toString()
});
const senderATA = await findAssociatedTokenAddress(
@ -187,27 +185,9 @@ export async function sendSolanaTokenPayment(
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.feePayer = senderPublicKey;
let signature;
console.log('Sending transaction...');
if (isWalletAdapter(wallet)) {
const result = await wallet.signAndSendTransaction(transaction);
signature = result.signature;
console.log('Transaction sent:', signature);
} else {
console.log("IN BACK TXXXXIRIFPOIEPODPOJOPIJOIJIO")
const backTx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: wallet.publicKey!,
toPubkey: receiverPublicKey,
// Note you can't send below minimum required for rent exemption to a random
// account so use something above that value
lamports: 206274714,
})
);
signature = await wallet.sendTransaction(backTx, backConnection)
console.log('BACKKKKK, Transaction sent:', signature);
}
console.log('Sending SPL token transaction...');
const signature = await wallet.sendTransaction(transaction, connection);
console.log('SPL token transaction sent:', signature);
// Confirm transaction
const confirmation = await connection.confirmTransaction({
@ -225,6 +205,36 @@ export async function sendSolanaTokenPayment(
success: true,
transactionSignature: signature
};
} catch (error) {
console.error('SPL token payment error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'SPL token payment failed'
};
}
}
// Main payment function that routes to the appropriate payment method
export async function sendSolanaPayment(
wallet: WalletAdapter,
connection: Connection,
walletPublicKey: string,
paymentRequest: PaymentRequest
): Promise<SolanaPaymentResult> {
try {
if (!wallet.connected || !wallet.publicKey) {
throw new Error('Wallet not connected');
}
switch (paymentRequest.paymentMethod) {
case 'sol':
return await sendSolPayment(wallet, connection, walletPublicKey);
case 'spl-token':
const tokenAmount = new BN(paymentRequest.amount);
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
default:
throw new Error(`Unsupported payment method: ${paymentRequest.paymentMethod}`);
}
} catch (error) {
console.error('Payment error:', error);
return {
@ -232,26 +242,25 @@ export async function sendSolanaTokenPayment(
error: error instanceof Error ? error.message : 'Payment failed'
};
}
};
// Helper function to check wallet connection status
export const checkSolanaWalletConnection = (walletType: SolanaWalletType): boolean => {
try {
if (walletType === 'phantom') {
return window.phantom?.solana?.isConnected || false;
} else if (walletType === 'solflare') {
return window.solflare?.isConnected || false;
}
return false;
} catch {
return false;
}
};
function isWalletAdapter(obj: any): obj is WalletAdapter {
return (
obj !== null &&
typeof obj === 'object' &&
typeof obj.signAndSendTransaction === 'function'
);
}
// Helper function to get recipient address based on payment method
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
switch (paymentMethod) {
case 'sol':
// For SOL payments, use the direct recipient address
return PAYMENT_RECEIVER_ADDRESS;
case 'spl-token':
// For SPL token payments, use the associated token account
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
const mintPublicKey = new PublicKey(TOKEN_MINT);
const associatedTokenAddress = getAssociatedTokenAddressSync(
mintPublicKey,
receiverPublicKey,
true // Allow off-curve addresses
);
return associatedTokenAddress.toBase58();
default:
throw new Error(`Unsupported payment method: ${paymentMethod}`);
}
}

View File

@ -1,24 +1,4 @@
import { Transaction } from "@solana/web3.js";
// extend the global Window interface to include Solana wallets
declare global {
interface Window {
phantom?: {
solana?: {
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
connect(): Promise<{ publicKey: { toString(): string } }>;
disconnect(): Promise<void>;
isConnected: boolean;
};
};
solflare?: {
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
connect(): Promise<{ publicKey: { toString(): string } }>;
disconnect(): Promise<void>;
isConnected: boolean;
};
}
}
import { PaymentMethod } from '../constants/payments';
export interface RegistryConfig {
chainId: string;
@ -45,7 +25,6 @@ export interface CreateRecordResponse {
message?: string;
}
export type SolanaWalletType = 'phantom' | 'solflare' | 'backpack';
export interface SolanaPaymentResult {
success: boolean;
@ -57,14 +36,14 @@ export interface PaymentModalProps {
isOpen: boolean;
onClose: () => void;
url: string;
onPaymentComplete: (txHash: string) => void;
walletState: SolanaWalletState;
onPaymentComplete: (txHash: string, paymentMethod: PaymentMethod) => void;
}
export interface SolanaWalletState {
connected: boolean;
publicKey: string | null;
walletType: SolanaWalletType | null;
export interface PaymentRequest {
paymentMethod: PaymentMethod;
amount: number; // in base units (lamports for SOL, token base units for SPL)
recipientAddress: string;
}
export interface LaconicTransferResult {

View File

@ -1,32 +1,76 @@
import BN from 'bn.js';
import { Connection, PublicKey } from '@solana/web3.js';
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { PaymentMethod } from '../constants/payments';
// Simplified transaction info extraction following reference implementation
const extractTxInfo = async (connection: Connection, transactionSignature: string): Promise<{ authority: string; amount: string }> => {
// Extract transaction info for SOL transfers
const extractSolTransferInfo = async (
connection: Connection,
transactionSignature: string
): Promise<{ authority: string; amount: string; destination: string }> => {
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
if (!result) {
throw new Error('Transaction not found');
}
// Look for system program transfer instruction
const transferInstruction = result.transaction.message.instructions.find(
(instr) => 'parsed' in instr
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
);
if (!transferInstruction || !('parsed' in transferInstruction)) {
throw new Error('Transfer instruction not found');
throw new Error('SOL transfer instruction not found');
}
const { info: { lamports, authority } } = transferInstruction.parsed;
return { authority, amount: lamports };
const { info: { lamports, source, destination } } = transferInstruction.parsed;
return { authority: source, amount: lamports.toString(), destination };
};
// Extract transaction info for SPL token transfers
const extractSplTokenTransferInfo = async (
connection: Connection,
transactionSignature: string
): Promise<{ authority: string; amount: string; destination: string }> => {
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
if (!result) {
throw new Error('Transaction not found');
}
// Look for token transfer instruction using TOKEN_PROGRAM_ID
const transferInstruction = result.transaction.message.instructions.find(
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
);
if (!transferInstruction || !('parsed' in transferInstruction)) {
throw new Error('SPL token transfer instruction not found');
}
const parsed = transferInstruction.parsed;
// Handle both transferChecked and transfer types
if (parsed.type === 'transferChecked') {
const { info: { tokenAmount, authority, destination } } = parsed;
return {
authority,
amount: tokenAmount.amount,
destination
};
} else if (parsed.type === 'transfer') {
const { info: { amount, authority, destination } } = parsed;
return { authority, amount, destination };
}
throw new Error('Unsupported token transfer type');
};
export const verifyUnusedSolanaPayment = async (
connection: Connection,
transactionSignature: string,
tokenAmount: BN
expectedAmount: BN,
paymentMethod: PaymentMethod,
expectedRecipientAddress: string
): Promise<{
valid: boolean,
reason?: string,
@ -39,7 +83,7 @@ export const verifyUnusedSolanaPayment = async (
// Fetch transaction details
const transactionResult = await connection.getParsedTransaction(transactionSignature, 'confirmed');
console.dir(transactionResult, {depth: null})
console.dir(transactionResult, {depth: null});
if (!transactionResult) {
return {
@ -76,45 +120,61 @@ export const verifyUnusedSolanaPayment = async (
};
}
// Extract transaction info using simplified approach
const { amount, authority } = await extractTxInfo(connection, transactionSignature);
// Extract transaction info based on payment method
let amount: string;
let authority: string;
let destination: string;
// Verify amount using BN comparison like in reference
const transactionAmount = new BN(amount);
if (transactionAmount.lt(tokenAmount)) {
if (paymentMethod === 'sol') {
const transferInfo = await extractSolTransferInfo(connection, transactionSignature);
amount = transferInfo.amount;
authority = transferInfo.authority;
destination = transferInfo.destination;
} else if (paymentMethod === 'spl-token') {
const transferInfo = await extractSplTokenTransferInfo(connection, transactionSignature);
amount = transferInfo.amount;
authority = transferInfo.authority;
destination = transferInfo.destination;
} else {
return {
valid: false,
reason: `Payment amount (${amount}) is less than required (${tokenAmount.toString()})`
reason: `Unsupported payment method: ${paymentMethod}`
};
}
const expectedTokenAccount = getAssociatedTokenAddressSync(
new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!),
new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS!),
// Allow recipient account address to be off the curve (i.e now having a private key)
true
);
// Verify recipient address by checking the transaction instructions
let foundValidTransfer = false;
for (const instruction of transactionResult.transaction.message.instructions) {
if ('parsed' in instruction ) {
const parsed = instruction.parsed;
if (parsed.type === 'transferChecked' || parsed.type === 'transfer') {
// Verify amount and recipient's associated token address
if (parsed.info.destination === process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS ) {
foundValidTransfer = true;
break;
}
}
}
}
if (!foundValidTransfer) {
// Verify amount using BN comparison
const transactionAmount = new BN(amount);
if (transactionAmount.lt(expectedAmount)) {
return {
valid: false,
reason: 'Valid Solana token transfer not found in transaction'
reason: `Payment amount (${amount}) is less than required (${expectedAmount.toString()})`
};
}
// Verify recipient address
let validRecipient = false;
if (paymentMethod === 'sol') {
// For SOL payments, check direct recipient address
validRecipient = destination === expectedRecipientAddress;
} else if (paymentMethod === 'spl-token') {
// For SPL token payments, destination should be the associated token account
// but we also need to check if it matches the expected recipient
const recipientPublicKey = new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS!);
const mintPublicKey = new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!);
const expectedTokenAccount = getAssociatedTokenAddressSync(
mintPublicKey,
recipientPublicKey,
true // Allow off-curve addresses
);
validRecipient = destination === expectedTokenAccount.toBase58();
}
if (!validRecipient) {
return {
valid: false,
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
};
}