Refactor code to use solana wallet adapters
This commit is contained in:
parent
8b220a0dee
commit
9995e76e87
122
package-lock.json
generated
122
package-lock.json
generated
@ -13,8 +13,10 @@
|
||||
"@solana/spl-token": "^0.4.13",
|
||||
"@solana/wallet-adapter-backpack": "^0.1.9",
|
||||
"@solana/wallet-adapter-base": "^0.9.18",
|
||||
"@solana/wallet-adapter-phantom": "^0.9.28",
|
||||
"@solana/wallet-adapter-react": "^0.15.20",
|
||||
"@solana/wallet-adapter-react-ui": "^0.9.18",
|
||||
"@solana/wallet-adapter-solflare": "^0.6.32",
|
||||
"@solana/web3.js": "^1.98.2",
|
||||
"axios": "^1.6.8",
|
||||
"big.js": "^6.2.2",
|
||||
@ -3470,6 +3472,21 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@solana/wallet-adapter-phantom": {
|
||||
"version": "0.9.28",
|
||||
"resolved": "https://registry.npmjs.org/@solana/wallet-adapter-phantom/-/wallet-adapter-phantom-0.9.28.tgz",
|
||||
"integrity": "sha512-g/hcuWwWjzo5l8I4vor9htniVhLxd/GhoVK52WSd0hy8IZ8/FBnV3u8ABVTheLqO13d0IVy+xTxoVBbDaMjLog==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@solana/wallet-adapter-base": "^0.9.27"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "^1.98.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@solana/wallet-adapter-react": {
|
||||
"version": "0.15.39",
|
||||
"resolved": "https://registry.npmjs.org/@solana/wallet-adapter-react/-/wallet-adapter-react-0.15.39.tgz",
|
||||
@ -3507,6 +3524,25 @@
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@solana/wallet-adapter-solflare": {
|
||||
"version": "0.6.32",
|
||||
"resolved": "https://registry.npmjs.org/@solana/wallet-adapter-solflare/-/wallet-adapter-solflare-0.6.32.tgz",
|
||||
"integrity": "sha512-FIqNyooif3yjPnw2gPNBZnsG6X9JYSrwCf1Oa0NN4/VxQcPjzGqvc+Tq1+js/nBOHju5roToeMFTbwNTdEOuZw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@solana/wallet-adapter-base": "^0.9.27",
|
||||
"@solana/wallet-standard-chains": "^1.1.1",
|
||||
"@solflare-wallet/metamask-sdk": "^1.0.3",
|
||||
"@solflare-wallet/sdk": "^1.4.2",
|
||||
"@wallet-standard/wallet": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "^1.98.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@solana/wallet-standard": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@solana/wallet-standard/-/wallet-standard-1.1.4.tgz",
|
||||
@ -3778,6 +3814,92 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@solflare-wallet/metamask-sdk": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@solflare-wallet/metamask-sdk/-/metamask-sdk-1.0.3.tgz",
|
||||
"integrity": "sha512-os5Px5PTMYKGS5tzOoyjDxtOtj0jZKnbI1Uwt8+Jsw1HHIA+Ib2UACCGNhQ/un2f8sIbTfLD1WuucNMOy8KZpQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@solana/wallet-standard-features": "^1.1.0",
|
||||
"@wallet-standard/base": "^1.0.1",
|
||||
"bs58": "^5.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@solflare-wallet/metamask-sdk/node_modules/base-x": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
|
||||
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@solflare-wallet/metamask-sdk/node_modules/bs58": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@solflare-wallet/metamask-sdk/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@solflare-wallet/sdk": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@solflare-wallet/sdk/-/sdk-1.4.2.tgz",
|
||||
"integrity": "sha512-jrseNWipwl9xXZgrzwZF3hhL0eIVxuEtoZOSLmuPuef7FgHjstuTtNJAeT4icA7pzdDV4hZvu54pI2r2f7SmrQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bs58": "^5.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@solflare-wallet/sdk/node_modules/base-x": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
|
||||
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@solflare-wallet/sdk/node_modules/bs58": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@solflare-wallet/sdk/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
|
||||
@ -12,11 +12,13 @@
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@cosmjs/stargate": "^0.32.3",
|
||||
"@solana/spl-token": "^0.4.13",
|
||||
"@solana/web3.js": "^1.98.2",
|
||||
"@solana/wallet-adapter-backpack": "^0.1.9",
|
||||
"@solana/wallet-adapter-base": "^0.9.18",
|
||||
"@solana/wallet-adapter-phantom": "^0.9.28",
|
||||
"@solana/wallet-adapter-react": "^0.15.20",
|
||||
"@solana/wallet-adapter-react-ui": "^0.9.18",
|
||||
"@solana/wallet-adapter-solflare": "^0.6.32",
|
||||
"@solana/web3.js": "^1.98.2",
|
||||
"axios": "^1.6.8",
|
||||
"big.js": "^6.2.2",
|
||||
"bn.js": "^5.2.2",
|
||||
|
||||
@ -11,6 +11,8 @@ import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
||||
import { transferLNTTokens } from '@/services/laconic-transfer';
|
||||
import { getRegistry, getRegistryConfig } from '@/config';
|
||||
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
||||
import { PaymentMethod, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments';
|
||||
import { getRecipientAddress } from '@/services/solana';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
@ -143,12 +145,13 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// First check if the request body is valid JSON
|
||||
let url, txHash, senderPublicKey;
|
||||
let url, txHash, senderPublicKey, paymentMethod;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
url = body.url;
|
||||
txHash = body.txHash;
|
||||
paymentMethod = body.paymentMethod as PaymentMethod;
|
||||
|
||||
const tx = await connection.getParsedTransaction(txHash, 'confirmed');
|
||||
if (!tx) {
|
||||
@ -165,10 +168,18 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
senderPublicKey = signerKeys[0];
|
||||
|
||||
if (!url || !txHash) {
|
||||
if (!url || !txHash || !paymentMethod) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Missing required fields: url and txHash are required'
|
||||
message: 'Missing required fields: url, txHash, and paymentMethod are required'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate payment method
|
||||
if (paymentMethod !== 'sol' && paymentMethod !== 'spl-token') {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Invalid payment method. Must be "sol" or "spl-token".'
|
||||
}, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
@ -180,36 +191,58 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify Solana payment
|
||||
console.log('Step 0: Verifying Solana token payment...');
|
||||
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
||||
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||
// Verify Solana payment based on method
|
||||
console.log(`Step 0: Verifying Solana ${paymentMethod} payment...`);
|
||||
|
||||
// Calculate expected token amount based on current price
|
||||
let expectedTokenAmount: number;
|
||||
try {
|
||||
const { requiredAmountInBaseUnits } = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||
expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
||||
} catch (error) {
|
||||
console.error('Error calculating token amount:', error);
|
||||
let expectedAmount: BN;
|
||||
let expectedRecipientAddress: string;
|
||||
|
||||
if (paymentMethod === 'sol') {
|
||||
expectedAmount = new BN(SOL_PAYMENT_AMOUNT_LAMPORTS);
|
||||
expectedRecipientAddress = getRecipientAddress('sol');
|
||||
} else if (paymentMethod === 'spl-token') {
|
||||
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
||||
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||
|
||||
// Calculate expected token amount based on current price
|
||||
let expectedTokenAmount: number;
|
||||
try {
|
||||
const { requiredAmountInBaseUnits } = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||
expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
||||
} catch (error) {
|
||||
console.error('Error calculating token amount:', error);
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Unable to verify payment due to price calculation error'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
expectedAmount = new BN(expectedTokenAmount);
|
||||
expectedRecipientAddress = getRecipientAddress('spl-token');
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Unable to verify payment due to price calculation error'
|
||||
}, { status: 500 });
|
||||
message: 'Unsupported payment method'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const tokenAmount = new BN(expectedTokenAmount);
|
||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(connection, txHash, tokenAmount);
|
||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(
|
||||
connection,
|
||||
txHash,
|
||||
expectedAmount,
|
||||
paymentMethod,
|
||||
expectedRecipientAddress
|
||||
);
|
||||
|
||||
if (!solanaPaymentResult.valid) {
|
||||
console.error('Solana token payment verification failed:', solanaPaymentResult.reason);
|
||||
console.error(`Solana ${paymentMethod} payment verification failed:`, solanaPaymentResult.reason);
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: `Payment verification failed: ${solanaPaymentResult.reason}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('Solana token payment verified successfully:', {
|
||||
console.log(`Solana ${paymentMethod} payment verified successfully:`, {
|
||||
amount: solanaPaymentResult.amount,
|
||||
sender: solanaPaymentResult.sender
|
||||
});
|
||||
|
||||
115
src/app/page.tsx
115
src/app/page.tsx
@ -1,27 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
||||
|
||||
import URLForm from '@/components/URLForm';
|
||||
import StatusDisplay from '@/components/StatusDisplay';
|
||||
import { createApplicationDeploymentRequest } from '@/services/registry';
|
||||
import { SolanaWalletState } from '@/types';
|
||||
import { PaymentMethod } from '@/constants/payments';
|
||||
import { useWallet } from '@solana/wallet-adapter-react';
|
||||
|
||||
// Dynamically import components to avoid SSR issues with browser APIs
|
||||
const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false });
|
||||
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||
|
||||
export default function Home() {
|
||||
const { wallet, connected } = useWallet()
|
||||
const [showWalletConnection, setShowWalletConnection] = useState(false);
|
||||
const [solanaWalletState, setSolanaWalletState] = useState<SolanaWalletState>({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
walletType: null
|
||||
});
|
||||
const { wallet, connected, publicKey } = useWallet();
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle');
|
||||
@ -32,34 +25,15 @@ export default function Home() {
|
||||
const [dns, setDns] = useState<string | null>(null);
|
||||
const [appName, setAppName] = useState<string | null>(null);
|
||||
const [repoUrl, setRepoUrl] = useState<string | null>(null);
|
||||
const [commitHash, setCommitHash] = useState<string | null>(null);
|
||||
const [shortCommitHash, setShortCommitHash] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wallet || !connected || !wallet.adapter.publicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSolanaWalletState({
|
||||
connected,
|
||||
publicKey: wallet.adapter.publicKey.toString(),
|
||||
walletType: 'backpack'
|
||||
})
|
||||
|
||||
}, [connected, wallet])
|
||||
|
||||
const handleConnectWallet = () => {
|
||||
setShowWalletConnection(true);
|
||||
};
|
||||
|
||||
const handleUrlSubmit = (submittedUrl: string) => {
|
||||
setUrl(submittedUrl);
|
||||
setShowPaymentModal(true);
|
||||
};
|
||||
|
||||
const handlePaymentComplete = useCallback(async (hash: string) => {
|
||||
if (!solanaWalletState.publicKey || !url) {
|
||||
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
|
||||
if (!publicKey || !url) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -69,7 +43,7 @@ export default function Home() {
|
||||
|
||||
try {
|
||||
// Create the Laconic Registry record (payment verification is done in the API)
|
||||
const result = await createApplicationDeploymentRequest(url, hash);
|
||||
const result = await createApplicationDeploymentRequest(url, hash, paymentMethod);
|
||||
|
||||
if (result.status === 'success') {
|
||||
setRecordId(result.id);
|
||||
@ -88,12 +62,6 @@ export default function Home() {
|
||||
if (result.repoUrl) {
|
||||
setRepoUrl(result.repoUrl);
|
||||
}
|
||||
if (result.commitHash) {
|
||||
setCommitHash(result.commitHash);
|
||||
}
|
||||
if (result.shortCommitHash) {
|
||||
setShortCommitHash(result.shortCommitHash);
|
||||
}
|
||||
setStatus('success');
|
||||
} else {
|
||||
setStatus('error');
|
||||
@ -103,7 +71,7 @@ export default function Home() {
|
||||
setStatus('error');
|
||||
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
||||
}
|
||||
}, [solanaWalletState, url])
|
||||
}, [publicKey, url])
|
||||
|
||||
const handleClosePaymentModal = () => {
|
||||
setShowPaymentModal(false);
|
||||
@ -123,37 +91,47 @@ export default function Home() {
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
|
||||
Connect Your Wallet
|
||||
</h2>
|
||||
{!showWalletConnection ? (
|
||||
<div className="text-center">
|
||||
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Payment method: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana)
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
onClick={handleConnectWallet}
|
||||
className="px-6 py-2 rounded-lg font-medium transition-colors"
|
||||
style={{
|
||||
background: 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
border: 'none'
|
||||
}}
|
||||
>
|
||||
Connect Solana Wallet
|
||||
</button>
|
||||
|
||||
<div className="mt-4">
|
||||
<WalletMultiButton />
|
||||
<div className="text-center">
|
||||
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Payment methods: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
SOL or {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana)
|
||||
</span>
|
||||
</p>
|
||||
{connected && publicKey ? (
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<div className="flex items-center">
|
||||
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
|
||||
<p className="font-medium" style={{ color: 'var(--success)' }}>
|
||||
Connected ({wallet?.adapter.name})
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<p className="text-sm font-mono break-all text-center">{publicKey.toBase58()}</p>
|
||||
</div>
|
||||
<WalletMultiButton
|
||||
className="!px-4 !py-2 !rounded-md !text-sm !transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--muted)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--input-border)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SolanaConnect onConnect={(walletState) => setSolanaWalletState(walletState)} />
|
||||
)}
|
||||
) : (
|
||||
<WalletMultiButton
|
||||
className="!px-6 !py-3 !rounded-md !w-full !transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 p-6 rounded-lg" style={{
|
||||
background: 'var(--muted-light)',
|
||||
borderLeft: '4px solid var(--primary)',
|
||||
opacity: solanaWalletState.connected ? '1' : '0.6'
|
||||
opacity: connected ? '1' : '0.6'
|
||||
}}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
@ -162,7 +140,7 @@ export default function Home() {
|
||||
</h2>
|
||||
<URLForm
|
||||
onSubmit={handleUrlSubmit}
|
||||
disabled={!solanaWalletState.connected || status === 'creating'}
|
||||
disabled={!connected || status === 'creating'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -182,21 +160,18 @@ export default function Home() {
|
||||
dns={dns || undefined}
|
||||
appName={appName || undefined}
|
||||
repoUrl={repoUrl || undefined}
|
||||
commitHash={commitHash || undefined}
|
||||
shortCommitHash={shortCommitHash || undefined}
|
||||
error={error || undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPaymentModal && url && solanaWalletState.connected && (
|
||||
{showPaymentModal && url && connected && publicKey && (
|
||||
<PaymentModal
|
||||
isOpen={showPaymentModal}
|
||||
onClose={handleClosePaymentModal}
|
||||
url={url}
|
||||
onPaymentComplete={handlePaymentComplete}
|
||||
walletState={solanaWalletState}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import BN from 'bn.js';
|
||||
import assert from 'assert';
|
||||
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
||||
import { useWallet } from '@solana/wallet-adapter-react';
|
||||
|
||||
import { sendSolanaTokenPayment } from '@/services/solana';
|
||||
import { sendSolanaPayment, getRecipientAddress } from '@/services/solana';
|
||||
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
||||
import { PaymentModalProps } from '@/types';
|
||||
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
|
||||
import { PaymentModalProps, PaymentRequest } from '@/types';
|
||||
import { PaymentMethod, PAYMENT_METHOD_LABELS, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
@ -19,34 +19,36 @@ export default function PaymentModal({
|
||||
onClose,
|
||||
url,
|
||||
onPaymentComplete,
|
||||
walletState,
|
||||
}: PaymentModalProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
|
||||
const [tokenAmount, setTokenAmount] = useState<number>(0);
|
||||
const [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback
|
||||
const [loadingPrice, setLoadingPrice] = useState(true);
|
||||
const [loadingPrice, setLoadingPrice] = useState(false);
|
||||
|
||||
const { wallet } = useWallet()
|
||||
const {connection: backConn} = useConnection();
|
||||
const { wallet, publicKey } = useWallet();
|
||||
|
||||
const connection = useMemo(() => new Connection(SOLANA_RPC_URL), [])
|
||||
const directConnection = useMemo(() => new Connection(SOLANA_RPC_URL), []);
|
||||
|
||||
// Get configuration from environment variables
|
||||
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
||||
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||
const recipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
||||
const tokenSymbol = process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'TOKEN';
|
||||
|
||||
// Fetch token amount based on USD price
|
||||
// Fetch token amount based on USD price when SPL token method is selected
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen || selectedPaymentMethod !== 'spl-token') {
|
||||
setLoadingPrice(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchTokenAmount = async () => {
|
||||
setLoadingPrice(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress)
|
||||
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||
setTokenAmount(requiredAmountInBaseUnits);
|
||||
setTokenDecimals(decimals);
|
||||
} catch (error) {
|
||||
@ -58,11 +60,41 @@ export default function PaymentModal({
|
||||
};
|
||||
|
||||
fetchTokenAmount();
|
||||
}, [isOpen, targetUsdAmount, mintAddress]);
|
||||
}, [isOpen, selectedPaymentMethod, targetUsdAmount, mintAddress]);
|
||||
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedPaymentMethod(null);
|
||||
setError('');
|
||||
setTokenAmount(0);
|
||||
setLoadingPrice(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handlePaymentMethodChange = (method: PaymentMethod) => {
|
||||
setSelectedPaymentMethod(method);
|
||||
setError('');
|
||||
|
||||
// Set tokenAmount for SOL payments to maintain consistency
|
||||
if (method === 'sol') {
|
||||
setTokenAmount(SOL_PAYMENT_AMOUNT_LAMPORTS);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayment = useCallback(async () => {
|
||||
if (tokenAmount === 0 || loadingPrice) {
|
||||
setError('Token amount not calculated yet. Please wait.');
|
||||
if (!selectedPaymentMethod) {
|
||||
setError('Please select a payment method.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenAmount === 0 || (selectedPaymentMethod === 'spl-token' && loadingPrice)) {
|
||||
setError('Payment amount not ready. Please wait.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wallet?.adapter) {
|
||||
setError('Wallet not connected.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -70,21 +102,44 @@ export default function PaymentModal({
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const tokenAmountBN = new BN(tokenAmount);
|
||||
const paymentRequest: PaymentRequest = {
|
||||
paymentMethod: selectedPaymentMethod,
|
||||
amount: tokenAmount,
|
||||
recipientAddress: getRecipientAddress(selectedPaymentMethod)
|
||||
};
|
||||
|
||||
const result = await sendSolanaTokenPayment(wallet!.adapter, backConn, connection, walletState.publicKey!, tokenAmountBN, walletState.walletType!);
|
||||
const result = await sendSolanaPayment(
|
||||
wallet.adapter,
|
||||
directConnection,
|
||||
publicKey!.toBase58(),
|
||||
paymentRequest
|
||||
);
|
||||
|
||||
if (result.success && result.transactionSignature) {
|
||||
onPaymentComplete(result.transactionSignature);
|
||||
onPaymentComplete(result.transactionSignature, selectedPaymentMethod);
|
||||
} else {
|
||||
setError(result.error || `${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment failed. Please try again.`);
|
||||
setError(result.error || 'Payment failed. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [connection, walletState, tokenAmount, loadingPrice, onPaymentComplete, backConn, wallet]);
|
||||
}, [selectedPaymentMethod, tokenAmount, loadingPrice, wallet, directConnection, publicKey, onPaymentComplete]);
|
||||
|
||||
const getPaymentAmountDisplay = () => {
|
||||
if (!selectedPaymentMethod) return '';
|
||||
|
||||
switch (selectedPaymentMethod) {
|
||||
case 'sol':
|
||||
return `${SOL_PAYMENT_AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL`;
|
||||
case 'spl-token':
|
||||
if (loadingPrice) return 'Loading...';
|
||||
return tokenAmount > 0 ? `${(tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6)} ${tokenSymbol}` : '0';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@ -94,7 +149,7 @@ export default function PaymentModal({
|
||||
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
|
||||
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
Complete {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment
|
||||
Complete Payment
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -106,76 +161,111 @@ export default function PaymentModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method Selection */}
|
||||
<div>
|
||||
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
|
||||
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||
<code className="text-sm font-mono break-all block">{recipientAddress}</code>
|
||||
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--foreground)' }}>
|
||||
Select Payment Method *
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(PAYMENT_METHOD_LABELS).map(([method, label]) => (
|
||||
<label key={method} className="flex items-center p-3 rounded-md cursor-pointer border"
|
||||
style={{
|
||||
borderColor: selectedPaymentMethod === method ? 'var(--primary)' : 'var(--input-border)',
|
||||
backgroundColor: selectedPaymentMethod === method ? 'var(--primary-light)' : 'var(--card-bg)'
|
||||
}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="paymentMethod"
|
||||
value={method}
|
||||
checked={selectedPaymentMethod === method}
|
||||
onChange={() => handlePaymentMethodChange(method as PaymentMethod)}
|
||||
className="mr-3"
|
||||
/>
|
||||
<span style={{ color: 'var(--foreground)' }}>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
|
||||
Payment Amount
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={`$${targetUsdAmount}`}
|
||||
disabled={true}
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: '0.7'
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<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)' }}>USD</span>
|
||||
{/* Payment Details - Only show when method is selected */}
|
||||
{selectedPaymentMethod && (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
|
||||
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||
<code className="text-sm font-mono break-all block">{getRecipientAddress(selectedPaymentMethod)}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{loadingPrice ? (
|
||||
<div className="w-full p-3 rounded-md flex items-center" style={{
|
||||
background: 'var(--muted-light)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--muted)'
|
||||
}}>
|
||||
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Fetching token amount...
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
|
||||
Payment Amount
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={targetUsdAmount}
|
||||
disabled={true}
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: '0.7'
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<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)' }}>USD</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={tokenAmount > 0 ? (tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6) : '0'}
|
||||
disabled={true}
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: '0.7'
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{!loadingPrice && (
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL}</span>
|
||||
|
||||
<div className="relative">
|
||||
{(selectedPaymentMethod === 'spl-token' && loadingPrice) ? (
|
||||
<div className="w-full p-3 rounded-md flex items-center" style={{
|
||||
background: 'var(--muted-light)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--muted)'
|
||||
}}>
|
||||
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Fetching token amount...
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={getPaymentAmountDisplay()}
|
||||
disabled={true}
|
||||
className="w-full p-3 pr-20 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: '0.7'
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{!(selectedPaymentMethod === '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)' }}>
|
||||
{selectedPaymentMethod === 'sol' ? 'SOL' : tokenSymbol}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedPaymentMethod === 'spl-token' && (
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
||||
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
||||
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS}`}>Jupiter</a>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
|
||||
@ -201,11 +291,11 @@ export default function PaymentModal({
|
||||
onClick={handlePayment}
|
||||
className="px-5 py-2 rounded-md flex items-center transition-colors"
|
||||
style={{
|
||||
backgroundColor: (loading || loadingPrice) ? 'var(--muted)' : 'var(--primary)',
|
||||
backgroundColor: (loading || loadingPrice || !selectedPaymentMethod) ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
opacity: (loading || loadingPrice) ? '0.8' : '1'
|
||||
opacity: (loading || loadingPrice || !selectedPaymentMethod) ? '0.8' : '1'
|
||||
}}
|
||||
disabled={loading || loadingPrice}
|
||||
disabled={loading || loadingPrice || !selectedPaymentMethod}
|
||||
>
|
||||
{(loading || loadingPrice) && (
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
@ -213,10 +303,13 @@ export default function PaymentModal({
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{loadingPrice ? 'Loading Price...' : loading ? 'Processing...' : 'Pay with Solana Wallet'}
|
||||
{!selectedPaymentMethod ? 'Select Payment Method' :
|
||||
loadingPrice ? 'Loading Price...' :
|
||||
loading ? 'Processing...' :
|
||||
'Pay with Solana Wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { connectSolanaWallet, disconnectSolanaWallet, checkSolanaWalletConnection } from '@/services/solana';
|
||||
import { SolanaWalletType, SolanaWalletState } from '@/types';
|
||||
|
||||
interface SolanaConnectProps {
|
||||
onConnect: (walletState: SolanaWalletState) => void;
|
||||
}
|
||||
|
||||
export default function SolanaConnect({ onConnect }: SolanaConnectProps) {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [walletState, setWalletState] = useState<SolanaWalletState>({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
walletType: null
|
||||
});
|
||||
|
||||
const handleConnect = async (walletType: SolanaWalletType) => {
|
||||
setConnecting(true);
|
||||
try {
|
||||
const newWalletState = await connectSolanaWallet(walletType);
|
||||
setWalletState(newWalletState);
|
||||
onConnect(newWalletState);
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Solana wallet:', error);
|
||||
alert(error instanceof Error ? error.message : 'Failed to connect wallet');
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
if (walletState.walletType) {
|
||||
try {
|
||||
await disconnectSolanaWallet(walletState.walletType);
|
||||
const disconnectedState = {
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
walletType: null
|
||||
};
|
||||
setWalletState(disconnectedState);
|
||||
onConnect(disconnectedState);
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect wallet:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check for auto-connection on page load
|
||||
const checkConnection = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Check Phantom
|
||||
if (window.phantom?.solana && checkSolanaWalletConnection('phantom')) {
|
||||
handleConnect('phantom');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check Solflare
|
||||
if (window.solflare && checkSolanaWalletConnection('solflare')) {
|
||||
handleConnect('solflare');
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkConnection();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
||||
{walletState.connected ? (
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
|
||||
<p className="font-medium" style={{ color: 'var(--success)' }}>
|
||||
Connected ({walletState.walletType})
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full p-3 rounded-md mb-3" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<p className="text-sm font-mono break-all text-center">{walletState.publicKey}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
className="px-4 py-2 rounded-md text-sm transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--muted)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--input-border)'
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-3">
|
||||
<button
|
||||
onClick={() => handleConnect('phantom')}
|
||||
disabled={connecting || !window.phantom?.solana}
|
||||
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{connecting && (
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{connecting ? 'Connecting...' : 'Connect Phantom Wallet'}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleConnect('solflare')}
|
||||
disabled={connecting || !window.solflare}
|
||||
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{connecting && (
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{connecting ? 'Connecting...' : 'Connect Solflare Wallet'}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!window.phantom?.solana && !window.solflare && (
|
||||
<p className="text-sm text-center" style={{ color: 'var(--muted)' }}>
|
||||
Please install Phantom or Solflare wallet extension
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -9,8 +9,6 @@ interface StatusDisplayProps {
|
||||
dns?: string;
|
||||
appName?: string;
|
||||
repoUrl?: string;
|
||||
commitHash?: string;
|
||||
shortCommitHash?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -23,8 +21,6 @@ export default function StatusDisplay({
|
||||
dns,
|
||||
appName,
|
||||
repoUrl,
|
||||
commitHash,
|
||||
shortCommitHash,
|
||||
error,
|
||||
}: StatusDisplayProps) {
|
||||
// Get domain suffix from environment variable
|
||||
|
||||
@ -4,6 +4,8 @@ import { useMemo } from 'react';
|
||||
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
|
||||
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
|
||||
import { BackpackWalletAdapter } from '@solana/wallet-adapter-backpack';
|
||||
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom';
|
||||
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare';
|
||||
|
||||
// Default styles that can be overridden by your app
|
||||
import '@solana/wallet-adapter-react-ui/styles.css';
|
||||
@ -23,7 +25,11 @@ export default function WalletProviders({ children }: WalletProvidersProps) {
|
||||
}, []);
|
||||
|
||||
const wallets = useMemo(
|
||||
() => [new BackpackWalletAdapter()],
|
||||
() => [
|
||||
new PhantomWalletAdapter(),
|
||||
new SolflareWalletAdapter(),
|
||||
new BackpackWalletAdapter()
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
16
src/constants/payments.ts
Normal file
16
src/constants/payments.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// Payment configuration constants
|
||||
|
||||
// SOL payment amount in lamports (1 SOL = 1,000,000,000 lamports)
|
||||
export const SOL_PAYMENT_AMOUNT_LAMPORTS = 10000000; // 0.01 SOL
|
||||
|
||||
// Payment method types
|
||||
export type PaymentMethod = 'sol' | 'spl-token';
|
||||
|
||||
// Payment method labels for UI
|
||||
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
|
||||
'sol': 'SOL (Native)',
|
||||
'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;
|
||||
@ -1,8 +1,10 @@
|
||||
import { CreateRecordResponse } from '../types';
|
||||
import { PaymentMethod } from '../constants/payments';
|
||||
|
||||
export const createApplicationDeploymentRequest = async (
|
||||
url: string,
|
||||
txHash: string,
|
||||
paymentMethod: PaymentMethod,
|
||||
): Promise<CreateRecordResponse> => {
|
||||
try {
|
||||
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment`);
|
||||
@ -13,7 +15,7 @@ export const createApplicationDeploymentRequest = async (
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ url, txHash }),
|
||||
body: JSON.stringify({ url, txHash, paymentMethod }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
@ -5,11 +5,13 @@ import {
|
||||
TOKEN_PROGRAM_ID,
|
||||
createTransferInstruction,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
getAssociatedTokenAddressSync
|
||||
} from '@solana/spl-token';
|
||||
import { Adapter } from '@solana/wallet-adapter-base';
|
||||
import { WalletAdapter } from '@solana/wallet-adapter-base';
|
||||
|
||||
import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types';
|
||||
import { SolanaPaymentResult, PaymentRequest } from '../types';
|
||||
import { PaymentMethod, SOL_PAYMENT_AMOUNT_LAMPORTS } 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_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
|
||||
@ -17,59 +19,7 @@ assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_REC
|
||||
const TOKEN_MINT = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
|
||||
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
||||
|
||||
export const connectSolanaWallet = async (walletType: SolanaWalletType): Promise<SolanaWalletState> => {
|
||||
try {
|
||||
if (walletType === 'phantom') {
|
||||
if (!window.phantom?.solana) {
|
||||
throw new Error('Phantom wallet not found. Please install Phantom browser extension.');
|
||||
}
|
||||
const response = await window.phantom.solana.connect();
|
||||
return {
|
||||
connected: true,
|
||||
publicKey: response.publicKey.toString(),
|
||||
walletType
|
||||
};
|
||||
} else if (walletType === 'solflare') {
|
||||
if (!window.solflare) {
|
||||
throw new Error('Solflare wallet not found. Please install Solflare browser extension.');
|
||||
}
|
||||
await window.solflare.connect();
|
||||
const publicKey = window.solflare.publicKey?.toString();
|
||||
if (!publicKey) {
|
||||
throw new Error('Failed to get public key from Solflare wallet');
|
||||
}
|
||||
return {
|
||||
connected: true,
|
||||
publicKey,
|
||||
walletType
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported wallet type: ${walletType}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Solana wallet:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const disconnectSolanaWallet = async (walletType: SolanaWalletType): Promise<void> => {
|
||||
try {
|
||||
let wallet = null;
|
||||
|
||||
if (walletType === 'phantom') {
|
||||
wallet = window.phantom?.solana;
|
||||
} else if (walletType === 'solflare') {
|
||||
wallet = window.solflare;
|
||||
}
|
||||
|
||||
if (wallet && wallet.disconnect) {
|
||||
await wallet.disconnect();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect Solana wallet:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to find associated token address
|
||||
async function findAssociatedTokenAddress(
|
||||
walletAddress: PublicKey,
|
||||
tokenMintAddress: PublicKey
|
||||
@ -84,41 +34,89 @@ async function findAssociatedTokenAddress(
|
||||
)[0];
|
||||
}
|
||||
|
||||
interface WalletAdapter {
|
||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
||||
}
|
||||
|
||||
export async function sendSolanaTokenPayment(
|
||||
backWallet: WalletAdapter | Adapter,
|
||||
backConnection: Connection,
|
||||
// Send SOL (native token) payment
|
||||
export async function sendSolPayment(
|
||||
wallet: WalletAdapter,
|
||||
connection: Connection,
|
||||
walletPublicKey: string,
|
||||
tokenAmount: BN,
|
||||
walletType: SolanaWalletType
|
||||
walletPublicKey: string
|
||||
): Promise<SolanaPaymentResult> {
|
||||
try {
|
||||
let wallet: WalletAdapter | Adapter | null = null;
|
||||
|
||||
if (walletType === 'phantom') {
|
||||
wallet = window.phantom?.solana || null;
|
||||
} else if (walletType === 'solflare') {
|
||||
wallet = window.solflare || null;
|
||||
} else if (walletType === 'backpack') {
|
||||
wallet = backWallet
|
||||
if (!wallet.connected || !wallet.publicKey) {
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error(`${walletType} wallet not found`);
|
||||
const senderPublicKey = new PublicKey(walletPublicKey);
|
||||
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||
|
||||
console.log('Processing SOL payment:', {
|
||||
sender: senderPublicKey.toBase58(),
|
||||
receiver: receiverPublicKey.toBase58(),
|
||||
amount: SOL_PAYMENT_AMOUNT_LAMPORTS
|
||||
});
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: senderPublicKey,
|
||||
toPubkey: receiverPublicKey,
|
||||
lamports: SOL_PAYMENT_AMOUNT_LAMPORTS,
|
||||
})
|
||||
);
|
||||
|
||||
// Set transaction details
|
||||
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||
transaction.feePayer = senderPublicKey;
|
||||
|
||||
console.log('Sending SOL transaction...');
|
||||
const signature = await wallet.sendTransaction(transaction, connection);
|
||||
console.log('SOL transaction sent:', signature);
|
||||
|
||||
// Confirm transaction
|
||||
const confirmation = await connection.confirmTransaction({
|
||||
signature,
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||
}, 'confirmed');
|
||||
|
||||
if (confirmation.value.err) {
|
||||
console.error('Transaction error:', confirmation.value.err);
|
||||
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionSignature: signature
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('SOL payment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'SOL payment failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Send SPL token payment
|
||||
export async function sendSplTokenPayment(
|
||||
wallet: WalletAdapter,
|
||||
connection: Connection,
|
||||
walletPublicKey: string,
|
||||
tokenAmount: BN
|
||||
): Promise<SolanaPaymentResult> {
|
||||
try {
|
||||
if (!wallet.connected || !wallet.publicKey) {
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
const senderPublicKey = new PublicKey(walletPublicKey);
|
||||
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
||||
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||
|
||||
console.log('Processing payment with keys:', {
|
||||
console.log('Processing SPL token payment:', {
|
||||
sender: senderPublicKey.toBase58(),
|
||||
mint: mintPublicKey.toBase58(),
|
||||
receiver: receiverPublicKey.toBase58(),
|
||||
amount: tokenAmount.toString()
|
||||
});
|
||||
|
||||
const senderATA = await findAssociatedTokenAddress(
|
||||
@ -187,27 +185,9 @@ export async function sendSolanaTokenPayment(
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||
transaction.feePayer = senderPublicKey;
|
||||
|
||||
let signature;
|
||||
console.log('Sending transaction...');
|
||||
if (isWalletAdapter(wallet)) {
|
||||
const result = await wallet.signAndSendTransaction(transaction);
|
||||
signature = result.signature;
|
||||
console.log('Transaction sent:', signature);
|
||||
} else {
|
||||
|
||||
console.log("IN BACK TXXXXIRIFPOIEPODPOJOPIJOIJIO")
|
||||
const backTx = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: wallet.publicKey!,
|
||||
toPubkey: receiverPublicKey,
|
||||
// Note you can't send below minimum required for rent exemption to a random
|
||||
// account so use something above that value
|
||||
lamports: 206274714,
|
||||
})
|
||||
);
|
||||
signature = await wallet.sendTransaction(backTx, backConnection)
|
||||
console.log('BACKKKKK, Transaction sent:', signature);
|
||||
}
|
||||
console.log('Sending SPL token transaction...');
|
||||
const signature = await wallet.sendTransaction(transaction, connection);
|
||||
console.log('SPL token transaction sent:', signature);
|
||||
|
||||
// Confirm transaction
|
||||
const confirmation = await connection.confirmTransaction({
|
||||
@ -225,6 +205,36 @@ export async function sendSolanaTokenPayment(
|
||||
success: true,
|
||||
transactionSignature: signature
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('SPL token payment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'SPL token payment failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Main payment function that routes to the appropriate payment method
|
||||
export async function sendSolanaPayment(
|
||||
wallet: WalletAdapter,
|
||||
connection: Connection,
|
||||
walletPublicKey: string,
|
||||
paymentRequest: PaymentRequest
|
||||
): Promise<SolanaPaymentResult> {
|
||||
try {
|
||||
if (!wallet.connected || !wallet.publicKey) {
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
switch (paymentRequest.paymentMethod) {
|
||||
case 'sol':
|
||||
return await sendSolPayment(wallet, connection, walletPublicKey);
|
||||
case 'spl-token':
|
||||
const tokenAmount = new BN(paymentRequest.amount);
|
||||
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||
default:
|
||||
throw new Error(`Unsupported payment method: ${paymentRequest.paymentMethod}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
return {
|
||||
@ -232,26 +242,25 @@ export async function sendSolanaTokenPayment(
|
||||
error: error instanceof Error ? error.message : 'Payment failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to check wallet connection status
|
||||
export const checkSolanaWalletConnection = (walletType: SolanaWalletType): boolean => {
|
||||
try {
|
||||
if (walletType === 'phantom') {
|
||||
return window.phantom?.solana?.isConnected || false;
|
||||
} else if (walletType === 'solflare') {
|
||||
return window.solflare?.isConnected || false;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function isWalletAdapter(obj: any): obj is WalletAdapter {
|
||||
return (
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
typeof obj.signAndSendTransaction === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to get recipient address based on payment method
|
||||
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
|
||||
switch (paymentMethod) {
|
||||
case 'sol':
|
||||
// For SOL payments, use the direct recipient address
|
||||
return PAYMENT_RECEIVER_ADDRESS;
|
||||
case 'spl-token':
|
||||
// For SPL token payments, use the associated token account
|
||||
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
||||
const associatedTokenAddress = getAssociatedTokenAddressSync(
|
||||
mintPublicKey,
|
||||
receiverPublicKey,
|
||||
true // Allow off-curve addresses
|
||||
);
|
||||
return associatedTokenAddress.toBase58();
|
||||
default:
|
||||
throw new Error(`Unsupported payment method: ${paymentMethod}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,4 @@
|
||||
import { Transaction } from "@solana/web3.js";
|
||||
|
||||
// extend the global Window interface to include Solana wallets
|
||||
declare global {
|
||||
interface Window {
|
||||
phantom?: {
|
||||
solana?: {
|
||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
||||
connect(): Promise<{ publicKey: { toString(): string } }>;
|
||||
disconnect(): Promise<void>;
|
||||
isConnected: boolean;
|
||||
};
|
||||
};
|
||||
solflare?: {
|
||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
||||
connect(): Promise<{ publicKey: { toString(): string } }>;
|
||||
disconnect(): Promise<void>;
|
||||
isConnected: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
import { PaymentMethod } from '../constants/payments';
|
||||
|
||||
export interface RegistryConfig {
|
||||
chainId: string;
|
||||
@ -45,7 +25,6 @@ export interface CreateRecordResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type SolanaWalletType = 'phantom' | 'solflare' | 'backpack';
|
||||
|
||||
export interface SolanaPaymentResult {
|
||||
success: boolean;
|
||||
@ -57,14 +36,14 @@ export interface PaymentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
url: string;
|
||||
onPaymentComplete: (txHash: string) => void;
|
||||
walletState: SolanaWalletState;
|
||||
onPaymentComplete: (txHash: string, paymentMethod: PaymentMethod) => void;
|
||||
}
|
||||
|
||||
export interface SolanaWalletState {
|
||||
connected: boolean;
|
||||
publicKey: string | null;
|
||||
walletType: SolanaWalletType | null;
|
||||
|
||||
export interface PaymentRequest {
|
||||
paymentMethod: PaymentMethod;
|
||||
amount: number; // in base units (lamports for SOL, token base units for SPL)
|
||||
recipientAddress: string;
|
||||
}
|
||||
|
||||
export interface LaconicTransferResult {
|
||||
|
||||
@ -1,32 +1,76 @@
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { Connection, PublicKey } from '@solana/web3.js';
|
||||
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
import { PaymentMethod } from '../constants/payments';
|
||||
|
||||
// Simplified transaction info extraction following reference implementation
|
||||
const extractTxInfo = async (connection: Connection, transactionSignature: string): Promise<{ authority: string; amount: string }> => {
|
||||
// Extract transaction info for SOL transfers
|
||||
const extractSolTransferInfo = 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 system program transfer instruction
|
||||
const transferInstruction = result.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr
|
||||
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
||||
);
|
||||
|
||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||
throw new Error('Transfer instruction not found');
|
||||
throw new Error('SOL transfer instruction not found');
|
||||
}
|
||||
|
||||
const { info: { lamports, authority } } = transferInstruction.parsed;
|
||||
return { authority, amount: lamports };
|
||||
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 (
|
||||
connection: Connection,
|
||||
transactionSignature: string,
|
||||
tokenAmount: BN
|
||||
expectedAmount: BN,
|
||||
paymentMethod: PaymentMethod,
|
||||
expectedRecipientAddress: string
|
||||
): Promise<{
|
||||
valid: boolean,
|
||||
reason?: string,
|
||||
@ -39,7 +83,7 @@ export const verifyUnusedSolanaPayment = async (
|
||||
// Fetch transaction details
|
||||
const transactionResult = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
|
||||
console.dir(transactionResult, {depth: null})
|
||||
console.dir(transactionResult, {depth: null});
|
||||
|
||||
if (!transactionResult) {
|
||||
return {
|
||||
@ -76,45 +120,61 @@ export const verifyUnusedSolanaPayment = async (
|
||||
};
|
||||
}
|
||||
|
||||
// Extract transaction info using simplified approach
|
||||
const { amount, authority } = await extractTxInfo(connection, transactionSignature);
|
||||
// Extract transaction info based on payment method
|
||||
let amount: string;
|
||||
let authority: string;
|
||||
let destination: string;
|
||||
|
||||
// Verify amount using BN comparison like in reference
|
||||
const transactionAmount = new BN(amount);
|
||||
if (transactionAmount.lt(tokenAmount)) {
|
||||
if (paymentMethod === 'sol') {
|
||||
const transferInfo = await extractSolTransferInfo(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: `Payment amount (${amount}) is less than required (${tokenAmount.toString()})`
|
||||
reason: `Unsupported payment method: ${paymentMethod}`
|
||||
};
|
||||
}
|
||||
|
||||
const expectedTokenAccount = getAssociatedTokenAddressSync(
|
||||
new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!),
|
||||
new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS!),
|
||||
// Allow recipient account address to be off the curve (i.e now having a private key)
|
||||
true
|
||||
);
|
||||
|
||||
// Verify recipient address by checking the transaction instructions
|
||||
let foundValidTransfer = false;
|
||||
|
||||
for (const instruction of transactionResult.transaction.message.instructions) {
|
||||
if ('parsed' in instruction ) {
|
||||
const parsed = instruction.parsed;
|
||||
if (parsed.type === 'transferChecked' || parsed.type === 'transfer') {
|
||||
// Verify amount and recipient's associated token address
|
||||
if (parsed.info.destination === process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS ) {
|
||||
foundValidTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundValidTransfer) {
|
||||
// Verify amount using BN comparison
|
||||
const transactionAmount = new BN(amount);
|
||||
if (transactionAmount.lt(expectedAmount)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Valid Solana token transfer not found in transaction'
|
||||
reason: `Payment amount (${amount}) is less than required (${expectedAmount.toString()})`
|
||||
};
|
||||
}
|
||||
|
||||
// Verify recipient address
|
||||
let validRecipient = false;
|
||||
|
||||
if (paymentMethod === 'sol') {
|
||||
// For SOL 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 {
|
||||
valid: false,
|
||||
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user