Refactor native token flow

This commit is contained in:
Shreerang Kale 2025-07-24 12:43:44 +05:30
parent f73b1ce211
commit d37c9f642a
12 changed files with 142 additions and 186 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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