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_)
# 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_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf
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 { transferLNTTokens } from '@/services/laconic-transfer';
import { getRegistry, getRegistryConfig } from '@/config';
import { getRequiredNativeGorInfo, getRequiredTokenInfo } from '@/services/jupiter-price';
import { PaymentMethod } from '@/constants/payments';
import { getRecipientAddress } from '@/services/solana';
import { getRequiredTokenInfo } from '@/services/jupiter-price';
import { WRAPPED_SOL_MINT_ADDRESS } from '@/constants/payments';
import { PaymentMethod } from '@/types';
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');
@ -141,7 +141,7 @@ export const registryTransactionWithRetry = async (
// Helper function to get the appropriate connection based on payment method
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(SOLANA_RPC_URL);
@ -185,7 +185,7 @@ export async function POST(request: NextRequest) {
}
// Validate payment method
if (paymentMethod !== 'nat-gor' && paymentMethod !== 'spl-token') {
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".'
@ -205,21 +205,18 @@ export async function POST(request: NextRequest) {
// Calculate expected token amount based on current price
let expectedRecipientAddress: string;
let requiredAmountInBaseUnits: number;
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
try {
if (paymentMethod === 'nat-gor') {
const requiredNativeGorInfo = await getRequiredNativeGorInfo(targetUsdAmount);
if (paymentMethod === PaymentMethod.NAT_GOR) {
const requiredNativeGorInfo = await getRequiredTokenInfo(targetUsdAmount, WRAPPED_SOL_MINT_ADDRESS);
requiredAmountInBaseUnits = requiredNativeGorInfo.requiredAmountInBaseUnits;
expectedRecipientAddress = getRecipientAddress('nat-gor');
} else if (paymentMethod === 'spl-token') {
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
const requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
expectedRecipientAddress = getRecipientAddress('spl-token');
} else {
return NextResponse.json({
status: 'error',
@ -241,7 +238,6 @@ export async function POST(request: NextRequest) {
txHash,
new BN(expectedTokenAmount),
paymentMethod,
expectedRecipientAddress
);
if (!solanaPaymentResult.valid) {

View File

@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useRef } from 'react';
import dynamic from 'next/dynamic';
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 StatusDisplay from '@/components/StatusDisplay';
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 { PaymentMethod } from '@/types';
// Dynamically import components to avoid SSR issues with browser APIs
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
@ -51,13 +52,24 @@ export default function Home() {
warnOnIncorrectChain();
}, [wallet]);
// Track previous payment method to detect switches
const previousPaymentMethodRef = useRef<PaymentMethod | null>(null);
useEffect(() => {
if (selectedPaymentMethod === null) {
return;
}
disconnect();
}, [selectedPaymentMethod, disconnect]);
// 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();
}
previousPaymentMethodRef.current = selectedPaymentMethod;
}, [selectedPaymentMethod, connected, disconnect]);
const handleUrlSubmit = (submittedUrl: string) => {
setUrl(submittedUrl);
@ -71,7 +83,7 @@ export default function Home() {
const walletName = wallet.adapter.name.toLowerCase();
const isBackpack = walletName.includes('backpack');
if (selectedPaymentMethod === 'nat-gor') {
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
return isBackpack; // Only Backpack for native GOR
} else {
return !isBackpack; // Only non-Backpack wallets for SPL tokens
@ -140,13 +152,13 @@ export default function Home() {
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
onClick={() => setSelectedPaymentMethod('nat-gor')}
onClick={() => setSelectedPaymentMethod(PaymentMethod.NAT_GOR)}
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={{
backgroundColor: selectedPaymentMethod === 'nat-gor' ? 'var(--accent)' : 'var(--card-bg)',
borderColor: selectedPaymentMethod === 'nat-gor' ? 'var(--primary)' : 'var(--card-border)'
backgroundColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--accent)' : 'var(--card-bg)',
borderColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--primary)' : 'var(--card-border)'
}}
>
<div className="text-left">
@ -157,13 +169,13 @@ export default function Home() {
</div>
</button>
<button
onClick={() => setSelectedPaymentMethod('spl-token')}
onClick={() => setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN)}
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={{
backgroundColor: selectedPaymentMethod === 'spl-token' ? 'var(--accent)' : 'var(--card-bg)',
borderColor: selectedPaymentMethod === 'spl-token' ? 'var(--primary)' : 'var(--card-border)'
backgroundColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--accent)' : 'var(--card-bg)',
borderColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--primary)' : 'var(--card-border)'
}}
>
<div className="text-left">
@ -236,7 +248,7 @@ export default function Home() {
) : (
<div>
<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'
: '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 { 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 { getRequiredTokenInfo, getRequiredNativeGorInfo } from '@/services/jupiter-price';
import { PaymentModalProps, PaymentRequest } from '@/types';
import { PAYMENT_METHOD_LABELS } from '@/constants/payments';
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
import { PaymentMethod, PaymentModalProps, PaymentRequest } from '@/types';
import { PAYMENT_METHOD_LABELS, WRAPPED_SOL_MINT_ADDRESS } from '@/constants/payments';
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');
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
export default function PaymentModal({
@ -26,6 +24,8 @@ export default function PaymentModal({
}: PaymentModalProps) {
const { selectedPaymentMethod: paymentMethod } = usePaymentMethod();
const { connection } = useConnection();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [tokenAmount, setTokenAmount] = useState<number>(0);
@ -34,7 +34,8 @@ export default function PaymentModal({
const { wallet, publicKey } = useWallet();
const solanaConnection = useMemo(() => new Connection(SOLANA_RPC_URL), []);
const solanaConnection = connection;
const gorbaganaConnection = useMemo(() =>
GORBAGANA_RPC_URL ? new Connection(GORBAGANA_RPC_URL) : solanaConnection,
[solanaConnection]
@ -57,17 +58,20 @@ export default function PaymentModal({
setError('');
try {
if (paymentMethod === 'nat-gor') {
let requiredTokenInfo: RequiredTokenInfo
if (paymentMethod === PaymentMethod.NAT_GOR) {
// Fetch native GOR amount using wrapped SOL price
const {requiredAmountInBaseUnits, decimals} = await getRequiredNativeGorInfo(targetUsdAmount);
setTokenAmount(requiredAmountInBaseUnits);
setTokenDecimals(decimals);
} else if (paymentMethod === 'spl-token') {
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, WRAPPED_SOL_MINT_ADDRESS);
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
// Fetch SPL token amount using token mint price
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
setTokenAmount(requiredAmountInBaseUnits);
setTokenDecimals(decimals);
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
} else {
setError('Invalid payment method');
return;
}
setTokenAmount(requiredTokenInfo.requiredAmountInBaseUnits);
setTokenDecimals(requiredTokenInfo.decimals);
} catch (error) {
console.error('Error fetching payment amount:', error);
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
const connectionToUse = paymentMethod === 'nat-gor' ? gorbaganaConnection : solanaConnection;
const connectionToUse = paymentMethod === PaymentMethod.NAT_GOR ? gorbaganaConnection : solanaConnection;
const result = await sendSolanaPayment(
wallet.adapter,
@ -138,9 +142,9 @@ export default function PaymentModal({
if (loadingPrice) return 'Loading...';
switch (paymentMethod) {
case 'nat-gor':
case PaymentMethod.NAT_GOR:
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';
default:
return '';
@ -239,16 +243,16 @@ export default function PaymentModal({
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">
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>
{paymentMethod === 'nat-gor' ? 'GOR (native)' : tokenSymbol}
{paymentMethod === PaymentMethod.NAT_GOR ? 'GOR (native)' : tokenSymbol}
</span>
</div>
)}
</div>
</div>
{paymentMethod === 'spl-token' && (
{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>

View File

@ -10,11 +10,11 @@ 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;
@ -50,7 +50,7 @@ export default function WalletProviders({ children }: WalletProvidersProps) {
return allWallets.filter(wallet => {
const isBackpack = wallet.name.toLowerCase().includes('backpack');
if (selectedPaymentMethod === 'nat-gor') {
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
return isBackpack; // Only Backpack for native GOR
} else {
return !isBackpack; // Only non-Backpack wallets for SPL tokens

View File

@ -1,13 +1,12 @@
// Payment configuration constants
// Payment method types
export type PaymentMethod = 'nat-gor' | 'spl-token';
import { PaymentMethod } from "@/types";
// Payment method labels for UI
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
'nat-gor': 'GOR (native)',
'spl-token': process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'SPL Token'
[PaymentMethod.NAT_GOR]: 'GOR (native)',
[PaymentMethod.SPL_TOKEN]: process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'SPL Token'
};
// Default payment method (none selected initially)
export const DEFAULT_PAYMENT_METHOD: PaymentMethod | null = null;
export const WRAPPED_SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';

View File

@ -2,7 +2,7 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { PaymentMethod } from '@/constants/payments';
import { PaymentMethod } from '@/types';
interface PaymentMethodContextType {
selectedPaymentMethod: PaymentMethod | null;

View File

@ -12,19 +12,17 @@ interface TokenPriceInfo {
decimals: number;
}
interface RequiredTokenInfo {
export interface RequiredTokenInfo {
requiredAmountInBaseUnits: number;
decimals: number;
}
const WRAPPED_SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
/**
* Fetches token price from Jupiter aggregator API
* @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}`);
@ -63,28 +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};
}
/**
* 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
};
return {requiredAmountInBaseUnits, decimals: priceInfo.decimals};
}

View File

@ -1,5 +1,5 @@
import { CreateRecordResponse } from '../types';
import { PaymentMethod } from '../constants/payments';
import { PaymentMethod } from '../types';
export const createApplicationDeploymentRequest = async (
url: string,

View File

@ -10,8 +10,7 @@ import {
} from '@solana/spl-token';
import { WalletAdapter } from '@solana/wallet-adapter-base';
import { SolanaPaymentResult, PaymentRequest } from '../types';
import { PaymentMethod } from '../constants/payments';
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');
@ -230,9 +229,9 @@ export async function sendSolanaPayment(
const tokenAmount = new BN(paymentRequest.amount);
switch (paymentRequest.paymentMethod) {
case 'nat-gor':
case PaymentMethod.NAT_GOR:
return await sendNativeGorPayment(wallet, connection, walletPublicKey, tokenAmount);
case 'spl-token':
case PaymentMethod.SPL_TOKEN:
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
default:
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
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
switch (paymentMethod) {
case 'nat-gor':
case PaymentMethod.NAT_GOR:
// For native GOR payments, use the direct recipient address
return PAYMENT_RECEIVER_ADDRESS;
case 'spl-token':
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);

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 {
chainId: string;
rpcEndpoint: string;

View File

@ -1,14 +1,16 @@
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';
import { PaymentMethod } from '../constants/payments';
import { getRecipientAddress } from '@/services/solana';
import { PaymentMethod } from '../types';
// Extract transaction info for native GOR transfers
const extractNativeGorTransferInfo = async (
const extractTxInfo = async (
connection: Connection,
transactionSignature: string
transactionSignature: string,
paymentMethod: PaymentMethod
): Promise<{ authority: string; amount: string; destination: string }> => {
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
@ -16,55 +18,52 @@ const extractNativeGorTransferInfo = async (
throw new Error('Transaction not found');
}
// Look for system program transfer instruction
const transferInstruction = result.transaction.message.instructions.find(
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
);
let transferInstruction: ParsedInstruction | PartiallyDecodedInstruction | undefined;
if (!transferInstruction || !('parsed' in transferInstruction)) {
throw new Error('Native GOR 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: { lamports, source, destination } } = transferInstruction.parsed;
return { authority: source, amount: lamports.toString(), destination };
};
// Extract transaction info for SPL token transfers
const extractSplTokenTransferInfo = async (
connection: Connection,
transactionSignature: string
): Promise<{ authority: string; amount: string; destination: string }> => {
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
if (!result) {
throw new Error('Transaction not found');
}
// Look for token transfer instruction using TOKEN_PROGRAM_ID
const transferInstruction = result.transaction.message.instructions.find(
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
);
if (!transferInstruction || !('parsed' in transferInstruction)) {
throw new Error('SPL token transfer instruction not found');
}
const parsed = transferInstruction.parsed;
// Handle both transferChecked and transfer types
if (parsed.type === 'transferChecked') {
const { info: { tokenAmount, authority, destination } } = parsed;
return {
authority,
amount: tokenAmount.amount,
destination
};
} else if (parsed.type === 'transfer') {
const { info: { amount, authority, destination } } = parsed;
return { authority, amount, destination };
}
throw new Error('Unsupported token transfer type');
};
export const verifyUnusedSolanaPayment = async (
@ -72,7 +71,6 @@ export const verifyUnusedSolanaPayment = async (
transactionSignature: string,
expectedAmount: BN,
paymentMethod: PaymentMethod,
expectedRecipientAddress: string
): Promise<{
valid: boolean,
reason?: string,
@ -121,26 +119,10 @@ export const verifyUnusedSolanaPayment = async (
}
// Extract transaction info based on payment method
let amount: string;
let authority: string;
let destination: string;
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}`
};
}
const transferInfo = await extractTxInfo(connection, transactionSignature, paymentMethod);
const amount = transferInfo.amount;
const authority = transferInfo.authority;
const destination = transferInfo.destination;
// Verify amount using BN comparison
const transactionAmount = new BN(amount);
@ -152,26 +134,9 @@ export const verifyUnusedSolanaPayment = async (
}
// Verify recipient address
let validRecipient = false;
const expectedRecipientAddress = getRecipientAddress(paymentMethod);
if (paymentMethod === 'nat-gor') {
// 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) {
if (destination !== expectedRecipientAddress) {
return {
valid: false,
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
@ -190,4 +155,4 @@ export const verifyUnusedSolanaPayment = async (
reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
};
};