From 9995e76e8733d611edbbc8f7ae651c35a0ff6bd6 Mon Sep 17 00:00:00 2001 From: Shreerang Kale Date: Wed, 23 Jul 2025 16:41:43 +0530 Subject: [PATCH] Refactor code to use solana wallet adapters --- package-lock.json | 122 +++++++++++++ package.json | 4 +- src/app/api/registry/route.ts | 73 +++++--- src/app/page.tsx | 115 +++++-------- src/components/PaymentModal.tsx | 267 +++++++++++++++++++---------- src/components/SolanaConnect.tsx | 148 ---------------- src/components/StatusDisplay.tsx | 4 - src/components/WalletProviders.tsx | 8 +- src/constants/payments.ts | 16 ++ src/services/registry.ts | 4 +- src/services/solana.ts | 249 ++++++++++++++------------- src/types/index.ts | 35 +--- src/utils/solana-verify.ts | 140 ++++++++++----- 13 files changed, 665 insertions(+), 520 deletions(-) delete mode 100644 src/components/SolanaConnect.tsx create mode 100644 src/constants/payments.ts diff --git a/package-lock.json b/package-lock.json index e17fab2..dad3076 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4b3497a..3cb2e26 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index 0cda0b6..5d82733 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -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 }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 1a87aaa..bc066f5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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({ - connected: false, - publicKey: null, - walletType: null - }); + const { wallet, connected, publicKey } = useWallet(); const [url, setUrl] = useState(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(null); const [appName, setAppName] = useState(null); const [repoUrl, setRepoUrl] = useState(null); - const [commitHash, setCommitHash] = useState(null); - const [shortCommitHash, setShortCommitHash] = useState(null); const [error, setError] = useState(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 Connect Your Wallet - {!showWalletConnection ? ( -
-

- Payment method: - {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana) - -

- - -
- +
+

+ Payment methods: + SOL or {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana) + +

+ {connected && publicKey ? ( +
+
+ +

+ Connected ({wallet?.adapter.name}) +

+
+
+

{publicKey.toBase58()}

+
+
-
- ) : ( - setSolanaWalletState(walletState)} /> - )} + ) : ( + + )} +

@@ -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} /> )} - {showPaymentModal && url && solanaWalletState.connected && ( + {showPaymentModal && url && connected && publicKey && ( )} diff --git a/src/components/PaymentModal.tsx b/src/components/PaymentModal.tsx index 031b751..bd1ee27 100644 --- a/src/components/PaymentModal.tsx +++ b/src/components/PaymentModal.tsx @@ -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(null); const [tokenAmount, setTokenAmount] = useState(0); const [tokenDecimals, setTokenDecimals] = useState(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)' }}>

- Complete {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment + Complete Payment

@@ -106,76 +161,111 @@ export default function PaymentModal({ + {/* Payment Method Selection */}
-

Recipient Address:

-
- {recipientAddress} + +
+ {Object.entries(PAYMENT_METHOD_LABELS).map(([method, label]) => ( + + ))}
-
- -
-
- -
- USD + {/* Payment Details - Only show when method is selected */} + {selectedPaymentMethod && ( + <> +
+

Recipient Address:

+
+ {getRecipientAddress(selectedPaymentMethod)}
-
- {loadingPrice ? ( -
- - - - - Fetching token amount... +
+ +
+
+ +
+ USD +
- ) : ( - 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 && ( -
- {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + +
+ {(selectedPaymentMethod === 'spl-token' && loadingPrice) ? ( +
+ + + + + Fetching token amount... +
+ ) : ( + + )} + {!(selectedPaymentMethod === 'spl-token' && loadingPrice) && ( +
+ + {selectedPaymentMethod === 'sol' ? 'SOL' : tokenSymbol} + +
+ )}
+
+ {selectedPaymentMethod === 'spl-token' && ( +

+ Token information fetched from Jupiter +

)}
-
-

- Token information fetched from Jupiter -

-
+ + )} {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) && ( @@ -213,10 +303,13 @@ export default function PaymentModal({ )} - {loadingPrice ? 'Loading Price...' : loading ? 'Processing...' : 'Pay with Solana Wallet'} + {!selectedPaymentMethod ? 'Select Payment Method' : + loadingPrice ? 'Loading Price...' : + loading ? 'Processing...' : + 'Pay with Solana Wallet'}
); -} +} \ No newline at end of file diff --git a/src/components/SolanaConnect.tsx b/src/components/SolanaConnect.tsx deleted file mode 100644 index fbd93f2..0000000 --- a/src/components/SolanaConnect.tsx +++ /dev/null @@ -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({ - 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 ( -
- {walletState.connected ? ( -
-
- -

- Connected ({walletState.walletType}) -

-
-
-

{walletState.publicKey}

-
- -
- ) : ( -
- - - - - {!window.phantom?.solana && !window.solflare && ( -

- Please install Phantom or Solflare wallet extension -

- )} -
- )} -
- ); -} \ No newline at end of file diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx index 715d628..8c518c7 100644 --- a/src/components/StatusDisplay.tsx +++ b/src/components/StatusDisplay.tsx @@ -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 diff --git a/src/components/WalletProviders.tsx b/src/components/WalletProviders.tsx index 52e9dec..162d435 100644 --- a/src/components/WalletProviders.tsx +++ b/src/components/WalletProviders.tsx @@ -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() + ], [] ); diff --git a/src/constants/payments.ts b/src/constants/payments.ts new file mode 100644 index 0000000..d1b6cf1 --- /dev/null +++ b/src/constants/payments.ts @@ -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 = { + '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; diff --git a/src/services/registry.ts b/src/services/registry.ts index 3206ce9..c019d70 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -1,8 +1,10 @@ import { CreateRecordResponse } from '../types'; +import { PaymentMethod } from '../constants/payments'; export const createApplicationDeploymentRequest = async ( url: string, txHash: string, + paymentMethod: PaymentMethod, ): Promise => { 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(); diff --git a/src/services/solana.ts b/src/services/solana.ts index 54abbad..8657017 100644 --- a/src/services/solana.ts +++ b/src/services/solana.ts @@ -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 => { - 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 => { - 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 { 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 { + 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 { + 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}`); + } } diff --git a/src/types/index.ts b/src/types/index.ts index feb214b..bcda4dd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; - isConnected: boolean; - }; - }; - solflare?: { - signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>; - connect(): Promise<{ publicKey: { toString(): string } }>; - disconnect(): Promise; - 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 { diff --git a/src/utils/solana-verify.ts b/src/utils/solana-verify.ts index a7d8b8a..98d6e19 100644 --- a/src/utils/solana-verify.ts +++ b/src/utils/solana-verify.ts @@ -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}` }; }