Refactor native token flow
This commit is contained in:
parent
f73b1ce211
commit
d37c9f642a
@ -1,6 +1,7 @@
|
|||||||
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
|
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
|
||||||
|
|
||||||
# Solana Payment Configuration
|
# Solana Payment Configuration
|
||||||
|
# TODO: Use different RPC URL or use browser wallet
|
||||||
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
||||||
NEXT_PUBLIC_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf
|
NEXT_PUBLIC_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
|
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
|
||||||
|
@ -10,9 +10,9 @@ import { DENOM as ALNT_DENOM } from '@cerc-io/registry-sdk';
|
|||||||
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
||||||
import { transferLNTTokens } from '@/services/laconic-transfer';
|
import { transferLNTTokens } from '@/services/laconic-transfer';
|
||||||
import { getRegistry, getRegistryConfig } from '@/config';
|
import { getRegistry, getRegistryConfig } from '@/config';
|
||||||
import { getRequiredNativeGorInfo, getRequiredTokenInfo } from '@/services/jupiter-price';
|
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
||||||
import { PaymentMethod } from '@/constants/payments';
|
import { WRAPPED_SOL_MINT_ADDRESS } from '@/constants/payments';
|
||||||
import { getRecipientAddress } from '@/services/solana';
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||||
assert(process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required');
|
assert(process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required');
|
||||||
@ -141,7 +141,7 @@ export const registryTransactionWithRetry = async (
|
|||||||
|
|
||||||
// Helper function to get the appropriate connection based on payment method
|
// Helper function to get the appropriate connection based on payment method
|
||||||
const getConnection = (paymentMethod: PaymentMethod): Connection => {
|
const getConnection = (paymentMethod: PaymentMethod): Connection => {
|
||||||
if (paymentMethod === 'nat-gor' && GORBAGANA_RPC_URL) {
|
if (paymentMethod === PaymentMethod.NAT_GOR && GORBAGANA_RPC_URL) {
|
||||||
return new Connection(GORBAGANA_RPC_URL);
|
return new Connection(GORBAGANA_RPC_URL);
|
||||||
}
|
}
|
||||||
return new Connection(SOLANA_RPC_URL);
|
return new Connection(SOLANA_RPC_URL);
|
||||||
@ -185,7 +185,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate payment method
|
// Validate payment method
|
||||||
if (paymentMethod !== 'nat-gor' && paymentMethod !== 'spl-token') {
|
if (paymentMethod !== PaymentMethod.NAT_GOR && paymentMethod !== PaymentMethod.SPL_TOKEN) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Invalid payment method. Must be "nat-gor" or "spl-token".'
|
message: 'Invalid payment method. Must be "nat-gor" or "spl-token".'
|
||||||
@ -205,21 +205,18 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
|
|
||||||
// Calculate expected token amount based on current price
|
// Calculate expected token amount based on current price
|
||||||
let expectedRecipientAddress: string;
|
|
||||||
let requiredAmountInBaseUnits: number;
|
let requiredAmountInBaseUnits: number;
|
||||||
|
|
||||||
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
||||||
|
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (paymentMethod === 'nat-gor') {
|
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
const requiredNativeGorInfo = await getRequiredNativeGorInfo(targetUsdAmount);
|
const requiredNativeGorInfo = await getRequiredTokenInfo(targetUsdAmount, WRAPPED_SOL_MINT_ADDRESS);
|
||||||
requiredAmountInBaseUnits = requiredNativeGorInfo.requiredAmountInBaseUnits;
|
requiredAmountInBaseUnits = requiredNativeGorInfo.requiredAmountInBaseUnits;
|
||||||
expectedRecipientAddress = getRecipientAddress('nat-gor');
|
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||||
} else if (paymentMethod === 'spl-token') {
|
|
||||||
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
|
||||||
const requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
const requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||||
requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
|
requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
|
||||||
expectedRecipientAddress = getRecipientAddress('spl-token');
|
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@ -241,7 +238,6 @@ export async function POST(request: NextRequest) {
|
|||||||
txHash,
|
txHash,
|
||||||
new BN(expectedTokenAmount),
|
new BN(expectedTokenAmount),
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
expectedRecipientAddress
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!solanaPaymentResult.valid) {
|
if (!solanaPaymentResult.valid) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
||||||
@ -10,8 +10,9 @@ import { BackpackWalletName } from '@solana/wallet-adapter-backpack';
|
|||||||
import URLForm from '@/components/URLForm';
|
import URLForm from '@/components/URLForm';
|
||||||
import StatusDisplay from '@/components/StatusDisplay';
|
import StatusDisplay from '@/components/StatusDisplay';
|
||||||
import { createApplicationDeploymentRequest } from '@/services/registry';
|
import { createApplicationDeploymentRequest } from '@/services/registry';
|
||||||
import { PaymentMethod, PAYMENT_METHOD_LABELS } from '@/constants/payments';
|
import { PAYMENT_METHOD_LABELS } from '@/constants/payments';
|
||||||
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
// Dynamically import components to avoid SSR issues with browser APIs
|
// Dynamically import components to avoid SSR issues with browser APIs
|
||||||
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||||
@ -51,13 +52,24 @@ export default function Home() {
|
|||||||
warnOnIncorrectChain();
|
warnOnIncorrectChain();
|
||||||
}, [wallet]);
|
}, [wallet]);
|
||||||
|
|
||||||
|
// Track previous payment method to detect switches
|
||||||
|
const previousPaymentMethodRef = useRef<PaymentMethod | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPaymentMethod === null) {
|
if (selectedPaymentMethod === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only disconnect if switching between different payment methods while connected
|
||||||
|
if (previousPaymentMethodRef.current !== null &&
|
||||||
|
previousPaymentMethodRef.current !== selectedPaymentMethod &&
|
||||||
|
connected) {
|
||||||
|
console.log("DISCONNECT TRIGGERED - Payment method switched while connected")
|
||||||
disconnect();
|
disconnect();
|
||||||
}, [selectedPaymentMethod, disconnect]);
|
}
|
||||||
|
|
||||||
|
previousPaymentMethodRef.current = selectedPaymentMethod;
|
||||||
|
}, [selectedPaymentMethod, connected, disconnect]);
|
||||||
|
|
||||||
const handleUrlSubmit = (submittedUrl: string) => {
|
const handleUrlSubmit = (submittedUrl: string) => {
|
||||||
setUrl(submittedUrl);
|
setUrl(submittedUrl);
|
||||||
@ -71,7 +83,7 @@ export default function Home() {
|
|||||||
const walletName = wallet.adapter.name.toLowerCase();
|
const walletName = wallet.adapter.name.toLowerCase();
|
||||||
const isBackpack = walletName.includes('backpack');
|
const isBackpack = walletName.includes('backpack');
|
||||||
|
|
||||||
if (selectedPaymentMethod === 'nat-gor') {
|
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
return isBackpack; // Only Backpack for native GOR
|
return isBackpack; // Only Backpack for native GOR
|
||||||
} else {
|
} else {
|
||||||
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||||
@ -140,13 +152,13 @@ export default function Home() {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedPaymentMethod('nat-gor')}
|
onClick={() => setSelectedPaymentMethod(PaymentMethod.NAT_GOR)}
|
||||||
className={`p-4 rounded-lg border-2 transition-colors ${
|
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||||
selectedPaymentMethod === 'nat-gor' ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedPaymentMethod === 'nat-gor' ? 'var(--accent)' : 'var(--card-bg)',
|
backgroundColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--accent)' : 'var(--card-bg)',
|
||||||
borderColor: selectedPaymentMethod === 'nat-gor' ? 'var(--primary)' : 'var(--card-border)'
|
borderColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--primary)' : 'var(--card-border)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
@ -157,13 +169,13 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedPaymentMethod('spl-token')}
|
onClick={() => setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN)}
|
||||||
className={`p-4 rounded-lg border-2 transition-colors ${
|
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||||
selectedPaymentMethod === 'spl-token' ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedPaymentMethod === 'spl-token' ? 'var(--accent)' : 'var(--card-bg)',
|
backgroundColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--accent)' : 'var(--card-bg)',
|
||||||
borderColor: selectedPaymentMethod === 'spl-token' ? 'var(--primary)' : 'var(--card-border)'
|
borderColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--primary)' : 'var(--card-border)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
@ -236,7 +248,7 @@ export default function Home() {
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-4 text-sm" style={{ color: 'var(--muted-foreground)' }}>
|
<p className="mb-4 text-sm" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
{selectedPaymentMethod === 'nat-gor'
|
{selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||||
? 'Only Backpack wallet supports native GOR payments'
|
? 'Only Backpack wallet supports native GOR payments'
|
||||||
: 'Phantom and Solflare wallets support SPL token payments'
|
: 'Phantom and Solflare wallets support SPL token payments'
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,16 @@ import { useCallback, useMemo, useState, useEffect } from 'react';
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
||||||
import { useWallet } from '@solana/wallet-adapter-react';
|
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
|
||||||
|
|
||||||
import { sendSolanaPayment, getRecipientAddress } from '@/services/solana';
|
import { sendSolanaPayment, getRecipientAddress } from '@/services/solana';
|
||||||
import { getRequiredTokenInfo, getRequiredNativeGorInfo } from '@/services/jupiter-price';
|
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||||
import { PaymentModalProps, PaymentRequest } from '@/types';
|
import { PaymentMethod, PaymentModalProps, PaymentRequest } from '@/types';
|
||||||
import { PAYMENT_METHOD_LABELS } from '@/constants/payments';
|
import { PAYMENT_METHOD_LABELS, WRAPPED_SOL_MINT_ADDRESS } from '@/constants/payments';
|
||||||
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
|
||||||
assert(process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required');
|
assert(process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required');
|
||||||
|
|
||||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
|
||||||
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
||||||
|
|
||||||
export default function PaymentModal({
|
export default function PaymentModal({
|
||||||
@ -26,6 +24,8 @@ export default function PaymentModal({
|
|||||||
}: PaymentModalProps) {
|
}: PaymentModalProps) {
|
||||||
const { selectedPaymentMethod: paymentMethod } = usePaymentMethod();
|
const { selectedPaymentMethod: paymentMethod } = usePaymentMethod();
|
||||||
|
|
||||||
|
const { connection } = useConnection();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [tokenAmount, setTokenAmount] = useState<number>(0);
|
const [tokenAmount, setTokenAmount] = useState<number>(0);
|
||||||
@ -34,7 +34,8 @@ export default function PaymentModal({
|
|||||||
|
|
||||||
const { wallet, publicKey } = useWallet();
|
const { wallet, publicKey } = useWallet();
|
||||||
|
|
||||||
const solanaConnection = useMemo(() => new Connection(SOLANA_RPC_URL), []);
|
const solanaConnection = connection;
|
||||||
|
|
||||||
const gorbaganaConnection = useMemo(() =>
|
const gorbaganaConnection = useMemo(() =>
|
||||||
GORBAGANA_RPC_URL ? new Connection(GORBAGANA_RPC_URL) : solanaConnection,
|
GORBAGANA_RPC_URL ? new Connection(GORBAGANA_RPC_URL) : solanaConnection,
|
||||||
[solanaConnection]
|
[solanaConnection]
|
||||||
@ -57,17 +58,20 @@ export default function PaymentModal({
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (paymentMethod === 'nat-gor') {
|
let requiredTokenInfo: RequiredTokenInfo
|
||||||
|
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
// Fetch native GOR amount using wrapped SOL price
|
// Fetch native GOR amount using wrapped SOL price
|
||||||
const {requiredAmountInBaseUnits, decimals} = await getRequiredNativeGorInfo(targetUsdAmount);
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, WRAPPED_SOL_MINT_ADDRESS);
|
||||||
setTokenAmount(requiredAmountInBaseUnits);
|
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||||
setTokenDecimals(decimals);
|
|
||||||
} else if (paymentMethod === 'spl-token') {
|
|
||||||
// Fetch SPL token amount using token mint price
|
// Fetch SPL token amount using token mint price
|
||||||
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||||
setTokenAmount(requiredAmountInBaseUnits);
|
} else {
|
||||||
setTokenDecimals(decimals);
|
setError('Invalid payment method');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTokenAmount(requiredTokenInfo.requiredAmountInBaseUnits);
|
||||||
|
setTokenDecimals(requiredTokenInfo.decimals);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching payment amount:', error);
|
console.error('Error fetching payment amount:', error);
|
||||||
setError('Unable to fetch current payment amount. Please try again.');
|
setError('Unable to fetch current payment amount. Please try again.');
|
||||||
@ -113,7 +117,7 @@ export default function PaymentModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Use different RPC connection based on payment method
|
// Use different RPC connection based on payment method
|
||||||
const connectionToUse = paymentMethod === 'nat-gor' ? gorbaganaConnection : solanaConnection;
|
const connectionToUse = paymentMethod === PaymentMethod.NAT_GOR ? gorbaganaConnection : solanaConnection;
|
||||||
|
|
||||||
const result = await sendSolanaPayment(
|
const result = await sendSolanaPayment(
|
||||||
wallet.adapter,
|
wallet.adapter,
|
||||||
@ -138,9 +142,9 @@ export default function PaymentModal({
|
|||||||
if (loadingPrice) return 'Loading...';
|
if (loadingPrice) return 'Loading...';
|
||||||
|
|
||||||
switch (paymentMethod) {
|
switch (paymentMethod) {
|
||||||
case 'nat-gor':
|
case PaymentMethod.NAT_GOR:
|
||||||
return tokenAmount > 0 ? `${(tokenAmount / LAMPORTS_PER_SOL).toFixed(6)} GOR` : '0';
|
return tokenAmount > 0 ? `${(tokenAmount / LAMPORTS_PER_SOL).toFixed(6)} GOR` : '0';
|
||||||
case 'spl-token':
|
case PaymentMethod.SPL_TOKEN:
|
||||||
return tokenAmount > 0 ? `${(tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6)} ${tokenSymbol}` : '0';
|
return tokenAmount > 0 ? `${(tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6)} ${tokenSymbol}` : '0';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
@ -239,16 +243,16 @@ export default function PaymentModal({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!(paymentMethod === 'spl-token' && loadingPrice) && (
|
{!(paymentMethod === PaymentMethod.SPL_TOKEN && loadingPrice) && (
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
<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)' }}>
|
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>
|
||||||
{paymentMethod === 'nat-gor' ? 'GOR (native)' : tokenSymbol}
|
{paymentMethod === PaymentMethod.NAT_GOR ? 'GOR (native)' : tokenSymbol}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{paymentMethod === 'spl-token' && (
|
{paymentMethod === PaymentMethod.SPL_TOKEN && (
|
||||||
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
<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>
|
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -10,11 +10,11 @@ import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom';
|
|||||||
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare';
|
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare';
|
||||||
|
|
||||||
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
// Default styles that can be overridden by your app
|
// Default styles that can be overridden by your app
|
||||||
import '@solana/wallet-adapter-react-ui/styles.css';
|
import '@solana/wallet-adapter-react-ui/styles.css';
|
||||||
|
|
||||||
|
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export default function WalletProviders({ children }: WalletProvidersProps) {
|
|||||||
return allWallets.filter(wallet => {
|
return allWallets.filter(wallet => {
|
||||||
const isBackpack = wallet.name.toLowerCase().includes('backpack');
|
const isBackpack = wallet.name.toLowerCase().includes('backpack');
|
||||||
|
|
||||||
if (selectedPaymentMethod === 'nat-gor') {
|
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
return isBackpack; // Only Backpack for native GOR
|
return isBackpack; // Only Backpack for native GOR
|
||||||
} else {
|
} else {
|
||||||
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Payment configuration constants
|
import { PaymentMethod } from "@/types";
|
||||||
|
|
||||||
// Payment method types
|
|
||||||
export type PaymentMethod = 'nat-gor' | 'spl-token';
|
|
||||||
|
|
||||||
// Payment method labels for UI
|
// Payment method labels for UI
|
||||||
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
|
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
|
||||||
'nat-gor': 'GOR (native)',
|
[PaymentMethod.NAT_GOR]: 'GOR (native)',
|
||||||
'spl-token': process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'SPL Token'
|
[PaymentMethod.SPL_TOKEN]: process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'SPL Token'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default payment method (none selected initially)
|
// Default payment method (none selected initially)
|
||||||
export const DEFAULT_PAYMENT_METHOD: PaymentMethod | null = null;
|
export const DEFAULT_PAYMENT_METHOD: PaymentMethod | null = null;
|
||||||
|
|
||||||
|
export const WRAPPED_SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
import { PaymentMethod } from '@/constants/payments';
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
interface PaymentMethodContextType {
|
interface PaymentMethodContextType {
|
||||||
selectedPaymentMethod: PaymentMethod | null;
|
selectedPaymentMethod: PaymentMethod | null;
|
||||||
|
@ -12,19 +12,17 @@ interface TokenPriceInfo {
|
|||||||
decimals: number;
|
decimals: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequiredTokenInfo {
|
export interface RequiredTokenInfo {
|
||||||
requiredAmountInBaseUnits: number;
|
requiredAmountInBaseUnits: number;
|
||||||
decimals: number;
|
decimals: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WRAPPED_SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches token price from Jupiter aggregator API
|
* Fetches token price from Jupiter aggregator API
|
||||||
* @param mintAddress The Solana token mint address
|
* @param mintAddress The Solana token mint address
|
||||||
* @returns Token price information including USD price and decimals
|
* @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 {
|
try {
|
||||||
const response = await fetch(`https://lite-api.jup.ag/price/v3?ids=${mintAddress}`);
|
const response = await fetch(`https://lite-api.jup.ag/price/v3?ids=${mintAddress}`);
|
||||||
|
|
||||||
@ -63,28 +61,7 @@ export async function getRequiredTokenInfo(targetUsdAmount: number, mintAddress:
|
|||||||
const tokenAmount = targetUsdAmount / priceInfo.usdPrice;
|
const tokenAmount = targetUsdAmount / priceInfo.usdPrice;
|
||||||
|
|
||||||
// Convert to smallest units (considering decimals)
|
// 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};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the native GOR amount needed for a given USD price
|
|
||||||
* Uses wrapped SOL price since native GOR = SOL
|
|
||||||
* @param targetUsdAmount The target amount in USD
|
|
||||||
* @returns The GOR amount in lamports needed and decimals (always 9)
|
|
||||||
*/
|
|
||||||
export async function getRequiredNativeGorInfo(targetUsdAmount: number): Promise<RequiredTokenInfo> {
|
|
||||||
const priceInfo = await getTokenInfo(WRAPPED_SOL_MINT_ADDRESS);
|
|
||||||
|
|
||||||
// Calculate GOR amount needed (same as SOL)
|
|
||||||
const gorAmount = targetUsdAmount / priceInfo.usdPrice;
|
|
||||||
|
|
||||||
// Convert to lamports
|
|
||||||
const amountInLamports = Math.round(gorAmount * Math.pow(10, 9));
|
|
||||||
|
|
||||||
return {
|
|
||||||
requiredAmountInBaseUnits: amountInLamports,
|
|
||||||
decimals: 9
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CreateRecordResponse } from '../types';
|
import { CreateRecordResponse } from '../types';
|
||||||
import { PaymentMethod } from '../constants/payments';
|
import { PaymentMethod } from '../types';
|
||||||
|
|
||||||
export const createApplicationDeploymentRequest = async (
|
export const createApplicationDeploymentRequest = async (
|
||||||
url: string,
|
url: string,
|
||||||
|
@ -10,8 +10,7 @@ import {
|
|||||||
} from '@solana/spl-token';
|
} from '@solana/spl-token';
|
||||||
import { WalletAdapter } from '@solana/wallet-adapter-base';
|
import { WalletAdapter } from '@solana/wallet-adapter-base';
|
||||||
|
|
||||||
import { SolanaPaymentResult, PaymentRequest } from '../types';
|
import { SolanaPaymentResult, PaymentRequest, PaymentMethod } from '../types';
|
||||||
import { PaymentMethod } 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_MINT_ADDRESS, 'SOLANA_TOKEN_MINT_ADDRESS is required');
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
|
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
|
||||||
@ -230,9 +229,9 @@ export async function sendSolanaPayment(
|
|||||||
const tokenAmount = new BN(paymentRequest.amount);
|
const tokenAmount = new BN(paymentRequest.amount);
|
||||||
|
|
||||||
switch (paymentRequest.paymentMethod) {
|
switch (paymentRequest.paymentMethod) {
|
||||||
case 'nat-gor':
|
case PaymentMethod.NAT_GOR:
|
||||||
return await sendNativeGorPayment(wallet, connection, walletPublicKey, tokenAmount);
|
return await sendNativeGorPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||||
case 'spl-token':
|
case PaymentMethod.SPL_TOKEN:
|
||||||
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
|
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported payment method: ${paymentRequest.paymentMethod}`);
|
throw new Error(`Unsupported payment method: ${paymentRequest.paymentMethod}`);
|
||||||
@ -249,10 +248,10 @@ export async function sendSolanaPayment(
|
|||||||
// Helper function to get recipient address based on payment method
|
// Helper function to get recipient address based on payment method
|
||||||
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
|
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
|
||||||
switch (paymentMethod) {
|
switch (paymentMethod) {
|
||||||
case 'nat-gor':
|
case PaymentMethod.NAT_GOR:
|
||||||
// For native GOR payments, use the direct recipient address
|
// For native GOR payments, use the direct recipient address
|
||||||
return PAYMENT_RECEIVER_ADDRESS;
|
return PAYMENT_RECEIVER_ADDRESS;
|
||||||
case 'spl-token':
|
case PaymentMethod.SPL_TOKEN:
|
||||||
// For SPL token payments, use the associated token account
|
// For SPL token payments, use the associated token account
|
||||||
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||||
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { PaymentMethod } from '../constants/payments';
|
// Payment method types
|
||||||
|
export enum PaymentMethod {
|
||||||
|
NAT_GOR = 'nat-gor',
|
||||||
|
SPL_TOKEN = 'spl-token'
|
||||||
|
}
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
chainId: string;
|
chainId: string;
|
||||||
rpcEndpoint: string;
|
rpcEndpoint: string;
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
|
|
||||||
import { Connection, PublicKey } from '@solana/web3.js';
|
import { Connection, ParsedInstruction, PartiallyDecodedInstruction } from '@solana/web3.js';
|
||||||
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
|
||||||
import { PaymentMethod } from '../constants/payments';
|
import { getRecipientAddress } from '@/services/solana';
|
||||||
|
import { PaymentMethod } from '../types';
|
||||||
|
|
||||||
// Extract transaction info for native GOR transfers
|
// Extract transaction info for native GOR transfers
|
||||||
const extractNativeGorTransferInfo = async (
|
const extractTxInfo = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
transactionSignature: string
|
transactionSignature: string,
|
||||||
|
paymentMethod: PaymentMethod
|
||||||
): Promise<{ authority: string; amount: string; destination: string }> => {
|
): Promise<{ authority: string; amount: string; destination: string }> => {
|
||||||
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||||
|
|
||||||
@ -16,8 +18,12 @@ const extractNativeGorTransferInfo = async (
|
|||||||
throw new Error('Transaction not found');
|
throw new Error('Transaction not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transferInstruction: ParsedInstruction | PartiallyDecodedInstruction | undefined;
|
||||||
|
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentMethod.NAT_GOR:
|
||||||
// Look for system program transfer instruction
|
// Look for system program transfer instruction
|
||||||
const transferInstruction = result.transaction.message.instructions.find(
|
transferInstruction = result.transaction.message.instructions.find(
|
||||||
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -27,21 +33,10 @@ const extractNativeGorTransferInfo = async (
|
|||||||
|
|
||||||
const { info: { lamports, source, destination } } = transferInstruction.parsed;
|
const { info: { lamports, source, destination } } = transferInstruction.parsed;
|
||||||
return { authority: source, amount: lamports.toString(), destination };
|
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
case PaymentMethod.SPL_TOKEN:
|
||||||
// Look for token transfer instruction using TOKEN_PROGRAM_ID
|
// Look for token transfer instruction using TOKEN_PROGRAM_ID
|
||||||
const transferInstruction = result.transaction.message.instructions.find(
|
transferInstruction = result.transaction.message.instructions.find(
|
||||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,6 +60,10 @@ const extractSplTokenTransferInfo = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Unsupported token transfer type');
|
throw new Error('Unsupported token transfer type');
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid payment method');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyUnusedSolanaPayment = async (
|
export const verifyUnusedSolanaPayment = async (
|
||||||
@ -72,7 +71,6 @@ export const verifyUnusedSolanaPayment = async (
|
|||||||
transactionSignature: string,
|
transactionSignature: string,
|
||||||
expectedAmount: BN,
|
expectedAmount: BN,
|
||||||
paymentMethod: PaymentMethod,
|
paymentMethod: PaymentMethod,
|
||||||
expectedRecipientAddress: string
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
valid: boolean,
|
valid: boolean,
|
||||||
reason?: string,
|
reason?: string,
|
||||||
@ -121,26 +119,10 @@ export const verifyUnusedSolanaPayment = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract transaction info based on payment method
|
// Extract transaction info based on payment method
|
||||||
let amount: string;
|
const transferInfo = await extractTxInfo(connection, transactionSignature, paymentMethod);
|
||||||
let authority: string;
|
const amount = transferInfo.amount;
|
||||||
let destination: string;
|
const authority = transferInfo.authority;
|
||||||
|
const destination = transferInfo.destination;
|
||||||
if (paymentMethod === 'nat-gor') {
|
|
||||||
const transferInfo = await extractNativeGorTransferInfo(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: `Unsupported payment method: ${paymentMethod}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify amount using BN comparison
|
// Verify amount using BN comparison
|
||||||
const transactionAmount = new BN(amount);
|
const transactionAmount = new BN(amount);
|
||||||
@ -152,26 +134,9 @@ export const verifyUnusedSolanaPayment = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify recipient address
|
// Verify recipient address
|
||||||
let validRecipient = false;
|
const expectedRecipientAddress = getRecipientAddress(paymentMethod);
|
||||||
|
|
||||||
if (paymentMethod === 'nat-gor') {
|
if (destination !== expectedRecipientAddress) {
|
||||||
// For native GOR 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 {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
||||||
|
Loading…
Reference in New Issue
Block a user