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_)
|
||||
|
||||
# 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
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CreateRecordResponse } from '../types';
|
||||
import { PaymentMethod } from '../constants/payments';
|
||||
import { PaymentMethod } from '../types';
|
||||
|
||||
export const createApplicationDeploymentRequest = async (
|
||||
url: string,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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}`
|
||||
|
Loading…
Reference in New Issue
Block a user