Add option to accept payments in Gorbagana chain native tokens (#5)
Part of https://www.notion.so/Laconic-Mainnet-Plan-1eca6b22d47280569cd0d1e6d711d949 Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Reviewed-on: #5 Co-authored-by: shreerang <shreerang@noreply.git.vdb.to> Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
This commit was merged in pull request #5.
This commit is contained in:
@@ -10,10 +10,15 @@ import { DENOM as ALNT_DENOM } from '@cerc-io/registry-sdk';
|
||||
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
||||
import { transferLNTTokens } from '@/services/laconic-transfer';
|
||||
import { getRegistry, getRegistryConfig } from '@/config';
|
||||
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
||||
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||
import { IS_NAT_GOR_TRANSFER_ENABLED, SOLANA_GOR_MINT_ADDRESS } from '@/constants/payments';
|
||||
import { PaymentMethod } from '@/types';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
assert(!IS_NAT_GOR_TRANSFER_ENABLED || process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required when NAT GOR transfer is enabled');
|
||||
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
||||
|
||||
// Allow 20% slippage due to price fluctuations
|
||||
const ALLOWED_SLIPPAGE_FACTOR = 0.2
|
||||
@@ -134,21 +139,43 @@ export const registryTransactionWithRetry = async (
|
||||
throw lastError;
|
||||
};
|
||||
|
||||
let connection: Connection;
|
||||
let solanaConnection: Connection | null = null;
|
||||
let gorbaganaConnection: Connection | null = null;
|
||||
|
||||
// Helper function to get the appropriate connection based on payment method
|
||||
const getConnection = (paymentMethod: PaymentMethod): Connection => {
|
||||
switch (paymentMethod) {
|
||||
case PaymentMethod.NAT_GOR:
|
||||
if (!gorbaganaConnection) {
|
||||
gorbaganaConnection = new Connection(GORBAGANA_RPC_URL!);
|
||||
}
|
||||
|
||||
return gorbaganaConnection;
|
||||
case PaymentMethod.SPL_TOKEN:
|
||||
if (!solanaConnection) {
|
||||
solanaConnection = new Connection(SOLANA_RPC_URL);
|
||||
}
|
||||
|
||||
return solanaConnection;
|
||||
default:
|
||||
throw new Error("Invalid payment method");
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!connection) {
|
||||
connection = new Connection(SOLANA_RPC_URL);
|
||||
}
|
||||
|
||||
// First check if the request body is valid JSON
|
||||
let url, txHash, senderPublicKey;
|
||||
let url, txHash, senderPublicKey, paymentMethod;
|
||||
let connection: Connection;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
url = body.url;
|
||||
txHash = body.txHash;
|
||||
paymentMethod = body.paymentMethod as PaymentMethod;
|
||||
|
||||
// Get the appropriate connection based on payment method
|
||||
connection = getConnection(paymentMethod);
|
||||
|
||||
const tx = await connection.getParsedTransaction(txHash, 'confirmed');
|
||||
if (!tx) {
|
||||
@@ -160,15 +187,23 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const signerKeys = tx.transaction.message.accountKeys
|
||||
.filter(k => k.signer)
|
||||
.map(k => k.pubkey.toBase58());
|
||||
.filter(k => k.signer)
|
||||
.map(k => k.pubkey.toBase58());
|
||||
|
||||
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 !== PaymentMethod.NAT_GOR && paymentMethod !== PaymentMethod.SPL_TOKEN) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Invalid payment method. Must be "nat-gor" or "spl-token".'
|
||||
}, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -180,36 +215,54 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify Solana payment
|
||||
console.log('Step 0: Verifying Solana token payment...');
|
||||
// Verify Solana payment based on method
|
||||
console.log(`Step 0: Verifying Solana ${paymentMethod} payment...`);
|
||||
|
||||
|
||||
// Calculate expected token amount based on current price
|
||||
let requiredTokenInfo: RequiredTokenInfo;
|
||||
|
||||
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);
|
||||
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, SOLANA_GOR_MINT_ADDRESS);
|
||||
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Unsupported payment method'
|
||||
}, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating token amount:', error);
|
||||
console.error('Error calculating expected amount:', error);
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Unable to verify payment due to price calculation error'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
const tokenAmount = new BN(expectedTokenAmount);
|
||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(connection, txHash, tokenAmount);
|
||||
const requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
|
||||
const expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
||||
|
||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(
|
||||
connection,
|
||||
txHash,
|
||||
new BN(expectedTokenAmount),
|
||||
paymentMethod,
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
+8
-1
@@ -3,6 +3,9 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||
|
||||
import "./globals.css";
|
||||
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
|
||||
import WalletProviders from "../components/WalletProviders";
|
||||
import { PaymentMethodProvider } from "../contexts/PaymentMethodContext";
|
||||
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -30,7 +33,11 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ErrorBoundaryWrapper />
|
||||
{children}
|
||||
<PaymentMethodProvider>
|
||||
<WalletProviders>
|
||||
{children}
|
||||
</WalletProviders>
|
||||
</PaymentMethodProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+186
-54
@@ -1,24 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
||||
import { useWallet } from '@solana/wallet-adapter-react';
|
||||
import { BackpackWalletName } from '@solana/wallet-adapter-backpack';
|
||||
|
||||
import URLForm from '@/components/URLForm';
|
||||
import StatusDisplay from '@/components/StatusDisplay';
|
||||
import { createApplicationDeploymentRequest } from '@/services/registry';
|
||||
import { SolanaWalletState } from '@/types';
|
||||
import { IS_NAT_GOR_TRANSFER_ENABLED, PAYMENT_METHOD_LABELS } from '@/constants/payments';
|
||||
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||
import { PaymentMethod } from '@/types';
|
||||
|
||||
// Dynamically import components to avoid SSR issues with browser APIs
|
||||
const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false });
|
||||
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||
|
||||
const GORBAGANA_GENESIS_HASH = '533uBE9RRquhTBqEX58oV52FdTTsReMdAvaUvP6hNjsn';
|
||||
|
||||
export default function Home() {
|
||||
const [showWalletConnection, setShowWalletConnection] = useState(false);
|
||||
const [solanaWalletState, setSolanaWalletState] = useState<SolanaWalletState>({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
walletType: null
|
||||
});
|
||||
const { wallet, connected, publicKey, disconnect } = useWallet();
|
||||
|
||||
const { selectedPaymentMethod, setSelectedPaymentMethod } = usePaymentMethod();
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle');
|
||||
@@ -29,21 +33,70 @@ 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);
|
||||
const [incorrectChainWarining, setIncorrectChainWarining] = useState<string | null>(null);
|
||||
|
||||
const handleConnectWallet = () => {
|
||||
setShowWalletConnection(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!IS_NAT_GOR_TRANSFER_ENABLED) {
|
||||
setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN);
|
||||
}
|
||||
}, [setSelectedPaymentMethod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wallet || wallet.adapter.name !== BackpackWalletName || selectedPaymentMethod !== PaymentMethod.NAT_GOR) {
|
||||
return;
|
||||
}
|
||||
|
||||
const warnOnIncorrectChain = async () => {
|
||||
// @ts-expect-error: backpack exists on window object
|
||||
const genesisHash = await window.backpack.solana.connection.getGenesisHash();
|
||||
if (genesisHash !== GORBAGANA_GENESIS_HASH) {
|
||||
setIncorrectChainWarining("WARNING: Unsupported chain selected in wallet. Please switch to Gorbagana chain")
|
||||
}
|
||||
}
|
||||
|
||||
warnOnIncorrectChain();
|
||||
}, [wallet, selectedPaymentMethod]);
|
||||
|
||||
// Track previous payment method to detect switches
|
||||
const previousPaymentMethodRef = useRef<PaymentMethod | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPaymentMethod === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only disconnect if switching between different payment methods while connected
|
||||
if (previousPaymentMethodRef.current !== null &&
|
||||
previousPaymentMethodRef.current !== selectedPaymentMethod &&
|
||||
connected) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
previousPaymentMethodRef.current = selectedPaymentMethod;
|
||||
}, [selectedPaymentMethod, connected, disconnect]);
|
||||
|
||||
const handleUrlSubmit = (submittedUrl: string) => {
|
||||
setUrl(submittedUrl);
|
||||
setShowPaymentModal(true);
|
||||
};
|
||||
|
||||
const handlePaymentComplete = useCallback(async (hash: string) => {
|
||||
if (!solanaWalletState.publicKey || !url) {
|
||||
// Helper function to check if current wallet is compatible with selected payment method
|
||||
const isWalletCompatible = () => {
|
||||
if (!selectedPaymentMethod || !wallet) return false;
|
||||
|
||||
const walletName = wallet.adapter.name.toLowerCase();
|
||||
const isBackpack = walletName.includes('backpack');
|
||||
|
||||
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||
return isBackpack; // Only Backpack for native GOR
|
||||
} else {
|
||||
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
|
||||
if (!publicKey || !url) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,7 +106,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);
|
||||
@@ -72,12 +125,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');
|
||||
@@ -87,7 +134,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);
|
||||
@@ -96,61 +143,149 @@ export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
|
||||
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
||||
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
||||
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
|
||||
Deploy Frontends with {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + Laconic
|
||||
</h1>
|
||||
|
||||
{/* Step 1: Payment Method Selection */}
|
||||
{ IS_NAT_GOR_TRANSFER_ENABLED &&
|
||||
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
|
||||
Connect Your Wallet
|
||||
Choose Payment Method
|
||||
</h2>
|
||||
{!showWalletConnection ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => setSelectedPaymentMethod(PaymentMethod.NAT_GOR)}
|
||||
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||
selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--accent)' : 'var(--card-bg)',
|
||||
borderColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--primary)' : 'var(--card-border)'
|
||||
}}
|
||||
>
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold text-lg mb-2">GOR (native)</h3>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Compatible with: Backpack
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN)}
|
||||
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||
selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--accent)' : 'var(--card-bg)',
|
||||
borderColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--primary)' : 'var(--card-border)'
|
||||
}}
|
||||
>
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold text-lg mb-2">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Token</h3>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Compatible with: Phantom, Solflare
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* Step 2: Wallet Connection */}
|
||||
<div className="mb-10 p-6 rounded-lg" style={{
|
||||
background: 'var(--muted-light)',
|
||||
borderLeft: '4px solid var(--primary)',
|
||||
opacity: selectedPaymentMethod ? '1' : '0.6'
|
||||
}}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
Connect Compatible Wallet
|
||||
</h2>
|
||||
{!selectedPaymentMethod ? (
|
||||
<p className="text-center" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Please select a payment method first
|
||||
</p>
|
||||
) : (
|
||||
<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)
|
||||
Selected: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
{PAYMENT_METHOD_LABELS[selectedPaymentMethod]}
|
||||
</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>
|
||||
{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: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
|
||||
}}></span>
|
||||
<p className="font-medium" style={{
|
||||
color: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
|
||||
}}>
|
||||
{isWalletCompatible() ? 'Compatible' : 'Incompatible'} Wallet ({wallet?.adapter.name})
|
||||
</p>
|
||||
</div>
|
||||
{!isWalletCompatible() && (
|
||||
<p className="text-sm text-amber-400">
|
||||
This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments.
|
||||
Please select a different wallet or payment method.
|
||||
</p>
|
||||
)}
|
||||
{incorrectChainWarining && (
|
||||
<p className="text-sm text-amber-400">
|
||||
{incorrectChainWarining}
|
||||
</p>
|
||||
)}
|
||||
<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>
|
||||
<p className="mb-4 text-sm" style={{ color: 'var(--muted-foreground)' }}>
|
||||
{selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||
? 'Only Backpack wallet supports native GOR payments'
|
||||
: 'Phantom and Solflare wallets support SPL token payments'
|
||||
}
|
||||
</p>
|
||||
<WalletMultiButton
|
||||
className="!px-6 !py-3 !rounded-md !w-full !transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<SolanaConnect onConnect={(walletState) => setSolanaWalletState(walletState)} />
|
||||
)}
|
||||
</div>
|
||||
{/* Step 3: URL Input */}
|
||||
<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 && isWalletCompatible()) ? '1' : '0.6'
|
||||
}}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
|
||||
Enter URL to Deploy
|
||||
</h2>
|
||||
<URLForm
|
||||
onSubmit={handleUrlSubmit}
|
||||
disabled={!solanaWalletState.connected || status === 'creating'}
|
||||
disabled={!connected || !isWalletCompatible() || status === 'creating'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status !== 'idle' && (
|
||||
<div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
|
||||
Deployment Status
|
||||
</h2>
|
||||
<StatusDisplay
|
||||
@@ -162,21 +297,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 && selectedPaymentMethod && (
|
||||
<PaymentModal
|
||||
isOpen={showPaymentModal}
|
||||
onClose={handleClosePaymentModal}
|
||||
url={url}
|
||||
onPaymentComplete={handlePaymentComplete}
|
||||
walletState={solanaWalletState}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ErrorBoundary() {
|
||||
nextjs-portal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Specifically target the error popup button */
|
||||
[data-nextjs-dialog-overlay],
|
||||
[data-nextjs-toast],
|
||||
@@ -35,4 +35,4 @@ export default function ErrorBoundary() {
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ const ErrorBoundary = dynamic(() => import('./ErrorBoundary'), { ssr: false });
|
||||
|
||||
export default function ErrorBoundaryWrapper() {
|
||||
return <ErrorBoundary />;
|
||||
}
|
||||
}
|
||||
|
||||
+115
-40
@@ -1,64 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import BN from 'bn.js';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import assert from 'assert';
|
||||
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
|
||||
|
||||
import { sendSolanaTokenPayment } from '@/services/solana';
|
||||
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
||||
import { PaymentModalProps } from '@/types';
|
||||
import { sendSolanaPayment } from '@/services/solana';
|
||||
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||
import { PaymentMethod, PaymentModalProps, PaymentRequest } from '@/types';
|
||||
import { IS_NAT_GOR_TRANSFER_ENABLED, PAYMENT_METHOD_LABELS, SOLANA_GOR_MINT_ADDRESS } from '@/constants/payments';
|
||||
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
assert(!IS_NAT_GOR_TRANSFER_ENABLED || process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required when NAT GOR transfer is enabled');
|
||||
|
||||
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
||||
|
||||
export default function PaymentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
url,
|
||||
onPaymentComplete,
|
||||
walletState,
|
||||
}: PaymentModalProps) {
|
||||
const { selectedPaymentMethod: paymentMethod } = usePaymentMethod();
|
||||
|
||||
const { connection: solanaConnection } = useConnection();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
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 connection = useMemo(() => new Connection(SOLANA_RPC_URL), [])
|
||||
const { wallet, publicKey } = useWallet();
|
||||
|
||||
// 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;
|
||||
|
||||
// Fetch token amount based on USD price
|
||||
// Fetch payment amount based on USD price for both payment methods
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen || !paymentMethod) {
|
||||
setLoadingPrice(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchTokenAmount = async () => {
|
||||
const fetchPaymentAmount = async () => {
|
||||
setLoadingPrice(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress)
|
||||
setTokenAmount(requiredAmountInBaseUnits);
|
||||
setTokenDecimals(decimals);
|
||||
let requiredTokenInfo: RequiredTokenInfo
|
||||
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||
// Fetch native GOR amount using solana GOR token price
|
||||
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, SOLANA_GOR_MINT_ADDRESS);
|
||||
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||
// Fetch SPL token amount using token mint price
|
||||
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||
} else {
|
||||
setError('Invalid payment method');
|
||||
return;
|
||||
}
|
||||
|
||||
setTokenAmount(requiredTokenInfo.requiredAmountInBaseUnits);
|
||||
setTokenDecimals(requiredTokenInfo.decimals);
|
||||
} catch (error) {
|
||||
console.error('Error fetching token price:', error);
|
||||
setError('Unable to fetch current token price. Please try again.');
|
||||
console.error('Error fetching payment amount:', error);
|
||||
setError('Unable to fetch current payment amount. Please try again.');
|
||||
} finally {
|
||||
setLoadingPrice(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTokenAmount();
|
||||
}, [isOpen, targetUsdAmount, mintAddress]);
|
||||
fetchPaymentAmount();
|
||||
}, [isOpen, paymentMethod, targetUsdAmount, mintAddress]);
|
||||
|
||||
// Initialize state when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setError('');
|
||||
setTokenAmount(0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handlePayment = useCallback(async () => {
|
||||
if (!paymentMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenAmount === 0 || loadingPrice) {
|
||||
setError('Token amount not calculated yet. Please wait.');
|
||||
setError('Payment amount not ready. Please wait.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wallet || !wallet.adapter) {
|
||||
setError('Wallet not connected.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,31 +103,50 @@ export default function PaymentModal({
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const tokenAmountBN = new BN(tokenAmount);
|
||||
const paymentRequest: PaymentRequest = {
|
||||
paymentMethod: paymentMethod,
|
||||
amount: tokenAmount,
|
||||
};
|
||||
|
||||
const result = await sendSolanaTokenPayment(connection, walletState.publicKey!, tokenAmountBN, walletState.walletType!);
|
||||
// Use different RPC connection based on payment method
|
||||
const connectionToUse = paymentMethod === PaymentMethod.NAT_GOR ? new Connection(GORBAGANA_RPC_URL!) : solanaConnection;
|
||||
|
||||
const result = await sendSolanaPayment(
|
||||
wallet.adapter,
|
||||
connectionToUse,
|
||||
publicKey!.toBase58(),
|
||||
paymentRequest
|
||||
);
|
||||
|
||||
if (result.success && result.transactionSignature) {
|
||||
onPaymentComplete(result.transactionSignature);
|
||||
onPaymentComplete(result.transactionSignature, paymentMethod);
|
||||
} 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]);
|
||||
}, [paymentMethod, tokenAmount, loadingPrice, wallet, solanaConnection, publicKey, onPaymentComplete]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
const getPaymentAmountDisplay = () => {
|
||||
if (loadingPrice) {
|
||||
return 'Loading...'
|
||||
};
|
||||
|
||||
return tokenAmount > 0 ? `${(tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6)}` : '0';
|
||||
};
|
||||
|
||||
if (!isOpen || !paymentMethod) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}>
|
||||
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
|
||||
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
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>
|
||||
|
||||
@@ -102,10 +158,23 @@ export default function PaymentModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method Display */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--foreground)' }}>
|
||||
Payment Method
|
||||
</label>
|
||||
<div className="p-3 rounded-md" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||
<p className="text-sm font-semibold">
|
||||
{PAYMENT_METHOD_LABELS[paymentMethod]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Details */}
|
||||
<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>
|
||||
<code className="text-sm font-mono break-all block">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +186,7 @@ export default function PaymentModal({
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={`$${targetUsdAmount}`}
|
||||
value={targetUsdAmount}
|
||||
disabled={true}
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
@@ -149,9 +218,9 @@ export default function PaymentModal({
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={tokenAmount > 0 ? (tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6) : '0'}
|
||||
value={getPaymentAmountDisplay()}
|
||||
disabled={true}
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
className="w-full p-3 pr-20 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
@@ -161,16 +230,20 @@ export default function PaymentModal({
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{!loadingPrice && (
|
||||
{!(paymentMethod === PaymentMethod.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)' }}>{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL}</span>
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>
|
||||
{paymentMethod === PaymentMethod.NAT_GOR ? 'GOR (native)' : tokenSymbol}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
{paymentMethod === PaymentMethod.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>
|
||||
|
||||
{error && (
|
||||
@@ -209,7 +282,9 @@ 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'}
|
||||
{loadingPrice ? 'Loading Price...' :
|
||||
loading ? 'Processing...' :
|
||||
'Pay with Solana Wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
@@ -59,7 +55,7 @@ export default function StatusDisplay({
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold"
|
||||
style={{ backgroundColor: styles.bg, color: styles.color }}>
|
||||
style={{ backgroundColor: styles.bg, color: styles.color }}>
|
||||
{styles.text}
|
||||
</span>
|
||||
);
|
||||
@@ -131,9 +127,9 @@ export default function StatusDisplay({
|
||||
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
||||
{dns && (
|
||||
domainSuffix
|
||||
? <InfoItem label="Deployment URL (ready in 2-3 minutes)" value={`https://${dns}.${domainSuffix}`} />
|
||||
: <InfoItem label="Please set domain suffix" value="" />
|
||||
)}
|
||||
? <InfoItem label="Deployment URL (ready in 2-3 minutes)" value={`https://${dns}.${domainSuffix}`} />
|
||||
: <InfoItem label="Please set domain suffix" value="" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import assert from 'assert';
|
||||
|
||||
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';
|
||||
|
||||
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||
import { PaymentMethod } from '@/types';
|
||||
|
||||
// Default styles that can be overridden by your app
|
||||
import '@solana/wallet-adapter-react-ui/styles.css';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
|
||||
interface WalletProvidersProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function WalletProviders({ children }: WalletProvidersProps) {
|
||||
const { selectedPaymentMethod } = usePaymentMethod();
|
||||
|
||||
// Configure the Solana network endpoint
|
||||
const endpoint = useMemo(() => {
|
||||
return SOLANA_RPC_URL;
|
||||
}, []);
|
||||
|
||||
// All available wallet adapters
|
||||
const allWallets = useMemo(
|
||||
() => [
|
||||
new PhantomWalletAdapter(),
|
||||
new SolflareWalletAdapter(),
|
||||
new BackpackWalletAdapter()
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Filter wallets based on selected payment method
|
||||
const wallets = useMemo(() => {
|
||||
if (!selectedPaymentMethod) {
|
||||
// If no payment method selected, do not show any wallets
|
||||
return [];
|
||||
}
|
||||
|
||||
return allWallets.filter(wallet => {
|
||||
const isBackpack = wallet.name.toLowerCase().includes('backpack');
|
||||
|
||||
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||
return isBackpack; // Only Backpack for native GOR
|
||||
} else {
|
||||
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||
}
|
||||
});
|
||||
}, [allWallets, selectedPaymentMethod]);
|
||||
|
||||
return (
|
||||
<ConnectionProvider endpoint={endpoint}>
|
||||
<WalletProvider wallets={wallets} autoConnect>
|
||||
<WalletModalProvider>
|
||||
{children}
|
||||
</WalletModalProvider>
|
||||
</WalletProvider>
|
||||
</ConnectionProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PaymentMethod } from "@/types";
|
||||
|
||||
// Payment method labels for UI
|
||||
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
|
||||
[PaymentMethod.NAT_GOR]: 'GOR (native)',
|
||||
[PaymentMethod.SPL_TOKEN]: process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL!
|
||||
};
|
||||
|
||||
export const SOLANA_GOR_MINT_ADDRESS = '71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg';
|
||||
|
||||
export const IS_NAT_GOR_TRANSFER_ENABLED = process.env.NEXT_PUBLIC_ENABLE_NATIVE_GOR_TRANSFER === "true";
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
import { PaymentMethod } from '@/types';
|
||||
|
||||
interface PaymentMethodContextType {
|
||||
selectedPaymentMethod: PaymentMethod | null;
|
||||
setSelectedPaymentMethod: (method: PaymentMethod | null) => void;
|
||||
}
|
||||
|
||||
const PaymentMethodContext = createContext<PaymentMethodContextType | undefined>(undefined);
|
||||
|
||||
interface PaymentMethodProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function PaymentMethodProvider({ children }: PaymentMethodProviderProps) {
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
|
||||
|
||||
return (
|
||||
<PaymentMethodContext.Provider
|
||||
value={{
|
||||
selectedPaymentMethod,
|
||||
setSelectedPaymentMethod
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PaymentMethodContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePaymentMethod() {
|
||||
const context = useContext(PaymentMethodContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('usePaymentMethod must be used within a PaymentMethodProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ interface TokenPriceInfo {
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
interface RequiredTokenInfo {
|
||||
export interface RequiredTokenInfo {
|
||||
requiredAmountInBaseUnits: number;
|
||||
decimals: number;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ interface RequiredTokenInfo {
|
||||
* @param mintAddress The Solana token mint address
|
||||
* @returns Token price information including USD price and decimals
|
||||
*/
|
||||
export async function getTokenInfo(mintAddress: string): Promise<TokenPriceInfo> {
|
||||
async function getTokenInfo(mintAddress: string): Promise<TokenPriceInfo> {
|
||||
try {
|
||||
const response = await fetch(`https://lite-api.jup.ag/price/v3?ids=${mintAddress}`);
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function getRequiredTokenInfo(targetUsdAmount: number, mintAddress:
|
||||
const tokenAmount = targetUsdAmount / priceInfo.usdPrice;
|
||||
|
||||
// Convert to smallest units (considering decimals)
|
||||
const amountInSmallestUnits = Math.round(tokenAmount * Math.pow(10, priceInfo.decimals));
|
||||
const requiredAmountInBaseUnits = Math.round(tokenAmount * Math.pow(10, priceInfo.decimals));
|
||||
|
||||
return {requiredAmountInBaseUnits: amountInSmallestUnits, decimals: priceInfo.decimals};
|
||||
return {requiredAmountInBaseUnits, decimals: priceInfo.decimals};
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ export const transferLNTTokens = async (): Promise<LaconicTransferResult> => {
|
||||
|
||||
// Send tokens from prefilled account to payment address
|
||||
const transferResult = await sendTokensToAccount(
|
||||
registryConfig.privateKey,
|
||||
paymentAddress,
|
||||
deployerMinPayment
|
||||
registryConfig.privateKey,
|
||||
paymentAddress,
|
||||
deployerMinPayment
|
||||
);
|
||||
|
||||
console.log('LNT transfer result:', transferResult);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { CreateRecordResponse } from '../types';
|
||||
import { PaymentMethod } from '../types';
|
||||
|
||||
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();
|
||||
|
||||
+129
-88
@@ -1,14 +1,16 @@
|
||||
import assert from 'assert';
|
||||
import BN from 'bn.js';
|
||||
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
|
||||
import { Connection, PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
||||
import {
|
||||
TOKEN_PROGRAM_ID,
|
||||
createTransferInstruction,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
getAssociatedTokenAddressSync
|
||||
} from '@solana/spl-token';
|
||||
import { WalletAdapter } from '@solana/wallet-adapter-base';
|
||||
|
||||
import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types';
|
||||
import { SolanaPaymentResult, PaymentRequest, PaymentMethod } from '../types';
|
||||
|
||||
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');
|
||||
@@ -16,59 +18,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
|
||||
@@ -83,37 +33,90 @@ async function findAssociatedTokenAddress(
|
||||
)[0];
|
||||
}
|
||||
|
||||
interface WalletAdapter {
|
||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
||||
}
|
||||
|
||||
export async function sendSolanaTokenPayment(
|
||||
// Send native GOR payment
|
||||
export async function sendNativeGorPayment(
|
||||
wallet: WalletAdapter,
|
||||
connection: Connection,
|
||||
walletPublicKey: string,
|
||||
tokenAmount: BN,
|
||||
walletType: SolanaWalletType
|
||||
tokenAmount: BN
|
||||
): Promise<SolanaPaymentResult> {
|
||||
try {
|
||||
let wallet: WalletAdapter | null = null;
|
||||
|
||||
if (walletType === 'phantom') {
|
||||
wallet = window.phantom?.solana || null;
|
||||
} else if (walletType === 'solflare') {
|
||||
wallet = window.solflare || null;
|
||||
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 native GOR payment:', {
|
||||
sender: senderPublicKey.toBase58(),
|
||||
receiver: receiverPublicKey.toBase58(),
|
||||
amount: tokenAmount.toString()
|
||||
});
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: senderPublicKey,
|
||||
toPubkey: receiverPublicKey,
|
||||
lamports: BigInt(tokenAmount.toString()),
|
||||
})
|
||||
);
|
||||
|
||||
// Set transaction details
|
||||
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||
transaction.feePayer = senderPublicKey;
|
||||
|
||||
console.log('Sending native GOR transaction...');
|
||||
const signature = await wallet.sendTransaction(transaction, connection);
|
||||
console.log('Native GOR 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('Native GOR payment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Native GOR 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(
|
||||
@@ -182,9 +185,9 @@ export async function sendSolanaTokenPayment(
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||
transaction.feePayer = senderPublicKey;
|
||||
|
||||
console.log('Sending transaction...');
|
||||
const { signature } = await wallet.signAndSendTransaction(transaction);
|
||||
console.log('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({
|
||||
@@ -202,6 +205,37 @@ 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');
|
||||
}
|
||||
|
||||
const tokenAmount = new BN(paymentRequest.amount);
|
||||
|
||||
switch (paymentRequest.paymentMethod) {
|
||||
case PaymentMethod.NAT_GOR:
|
||||
return await sendNativeGorPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||
case PaymentMethod.SPL_TOKEN:
|
||||
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||
default:
|
||||
throw new Error(`Unsupported payment method: ${paymentRequest.paymentMethod}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
return {
|
||||
@@ -209,18 +243,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;
|
||||
// Helper function to get recipient address based on payment method
|
||||
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
|
||||
switch (paymentMethod) {
|
||||
case PaymentMethod.NAT_GOR:
|
||||
// For native GOR payments, use the direct recipient address
|
||||
return PAYMENT_RECEIVER_ADDRESS;
|
||||
case PaymentMethod.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}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+9
-28
@@ -1,25 +1,8 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
// Payment method types
|
||||
export enum PaymentMethod {
|
||||
NAT_GOR = 'nat-gor',
|
||||
SPL_TOKEN = 'spl-token'
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
chainId: string;
|
||||
rpcEndpoint: string;
|
||||
@@ -45,7 +28,6 @@ export interface CreateRecordResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type SolanaWalletType = 'phantom' | 'solflare';
|
||||
|
||||
export interface SolanaPaymentResult {
|
||||
success: boolean;
|
||||
@@ -57,14 +39,13 @@ 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 native GOR, token base units for SPL)
|
||||
}
|
||||
|
||||
export interface LaconicTransferResult {
|
||||
|
||||
+70
-43
@@ -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 { Connection, ParsedInstruction, PartiallyDecodedInstruction } from '@solana/web3.js';
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
|
||||
// Simplified transaction info extraction following reference implementation
|
||||
const extractTxInfo = async (connection: Connection, transactionSignature: string): Promise<{ authority: string; amount: string }> => {
|
||||
import { getRecipientAddress } from '@/services/solana';
|
||||
import { PaymentMethod } from '../types';
|
||||
|
||||
// Extract transaction info for native GOR transfers
|
||||
const extractTxInfo = async (
|
||||
connection: Connection,
|
||||
transactionSignature: string,
|
||||
paymentMethod: PaymentMethod
|
||||
): Promise<{ authority: string; amount: string; destination: string }> => {
|
||||
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
|
||||
const transferInstruction = result.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||
);
|
||||
let transferInstruction: ParsedInstruction | PartiallyDecodedInstruction | undefined;
|
||||
|
||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||
throw new Error('Transfer instruction not found');
|
||||
switch (paymentMethod) {
|
||||
case PaymentMethod.NAT_GOR:
|
||||
// Look for system program transfer instruction
|
||||
transferInstruction = result.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
||||
);
|
||||
|
||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||
throw new Error('Native GOR transfer instruction not found');
|
||||
}
|
||||
|
||||
const { info: { lamports, source, destination } } = transferInstruction.parsed;
|
||||
return { authority: source, amount: lamports.toString(), destination };
|
||||
|
||||
case PaymentMethod.SPL_TOKEN:
|
||||
// Look for token transfer instruction using TOKEN_PROGRAM_ID
|
||||
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');
|
||||
|
||||
default:
|
||||
throw new Error('Invalid payment method');
|
||||
}
|
||||
|
||||
const { info: { amount, authority } } = transferInstruction.parsed;
|
||||
return { authority, amount };
|
||||
};
|
||||
|
||||
export const verifyUnusedSolanaPayment = async (
|
||||
connection: Connection,
|
||||
transactionSignature: string,
|
||||
tokenAmount: BN
|
||||
expectedAmount: BN,
|
||||
paymentMethod: PaymentMethod,
|
||||
): Promise<{
|
||||
valid: boolean,
|
||||
reason?: string,
|
||||
@@ -74,45 +118,28 @@ export const verifyUnusedSolanaPayment = async (
|
||||
};
|
||||
}
|
||||
|
||||
// Extract transaction info using simplified approach
|
||||
const { amount, authority } = await extractTxInfo(connection, transactionSignature);
|
||||
// Extract transaction info based on payment method
|
||||
const transferInfo = await extractTxInfo(connection, transactionSignature, paymentMethod);
|
||||
const amount = transferInfo.amount;
|
||||
const authority = transferInfo.authority;
|
||||
const destination = transferInfo.destination;
|
||||
|
||||
// Verify amount using BN comparison like in reference
|
||||
// Verify amount using BN comparison
|
||||
const transactionAmount = new BN(amount);
|
||||
if (transactionAmount.lt(tokenAmount)) {
|
||||
if (transactionAmount.lt(expectedAmount)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Payment amount (${amount}) is less than required (${tokenAmount.toString()})`
|
||||
reason: `Payment amount (${amount}) is less than required (${expectedAmount.toString()})`
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
const expectedRecipientAddress = getRecipientAddress(paymentMethod);
|
||||
|
||||
// Verify recipient address by checking the transaction instructions
|
||||
let foundValidTransfer = false;
|
||||
|
||||
for (const instruction of transactionResult.transaction.message.instructions) {
|
||||
if ('parsed' in instruction && instruction.programId.equals(TOKEN_PROGRAM_ID)) {
|
||||
const parsed = instruction.parsed;
|
||||
if (parsed.type === 'transferChecked' || parsed.type === 'transfer') {
|
||||
// Verify amount and recipient's associated token address
|
||||
if (parsed.info.amount === amount && parsed.info.destination === expectedTokenAccount.toBase58() ) {
|
||||
foundValidTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundValidTransfer) {
|
||||
if (destination !== expectedRecipientAddress) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Valid Solana token transfer not found in transaction'
|
||||
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,4 +155,4 @@ export const verifyUnusedSolanaPayment = async (
|
||||
reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user