diff --git a/src/components/anchors/AnchorIntegration.jsx b/src/components/anchors/AnchorIntegration.jsx index 2fa9ffd..80f00c0 100644 --- a/src/components/anchors/AnchorIntegration.jsx +++ b/src/components/anchors/AnchorIntegration.jsx @@ -1,16 +1,12 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { TrendingUp, TrendingDown, - Clock, DollarSign, AlertTriangle, CheckCircle, ExternalLink, Info, - Star, - Shield, - Zap, ArrowUpDown, Wallet, CreditCard, @@ -21,6 +17,7 @@ import { } from 'lucide-react'; import anchorService from '../../lib/anchors.js'; import auditTrail from '../../lib/auditTrail.js'; +import { connectFreighter, signTransactionWithFreighter } from '../../lib/wallet/freighter.js'; const METHOD_ICONS = { bank_transfer: BanknoteIcon, @@ -48,6 +45,11 @@ export default function AnchorIntegration() { const [loading, setLoading] = useState(false); const [expandedAnchor, setExpandedAnchor] = useState(null); const [supportedAssets, setSupportedAssets] = useState([]); + const [authStatus, setAuthStatus] = useState('disconnected'); + const [authMessage, setAuthMessage] = useState('Not connected'); + const [authError, setAuthError] = useState(null); + const [anchorSession, setAnchorSession] = useState(null); + const [isAnchorAuthLoading, setIsAnchorAuthLoading] = useState(false); useEffect(() => { loadData(); @@ -57,7 +59,7 @@ export default function AnchorIntegration() { if (selectedAsset && amount && transactionType) { loadComparison(); } - }, [selectedAsset, amount, transactionType]); + }, [selectedAsset, amount, transactionType, loadComparison]); const loadData = async () => { try { @@ -76,7 +78,7 @@ export default function AnchorIntegration() { } }; - const loadComparison = async () => { + const loadComparison = useCallback(async () => { try { setLoading(true); const amountNum = parseFloat(amount); @@ -105,7 +107,7 @@ export default function AnchorIntegration() { } finally { setLoading(false); } - }; + }, [selectedAsset, amount, transactionType]); const handleAnchorSelect = (anchor) => { setSelectedAnchor(anchor); @@ -118,6 +120,110 @@ export default function AnchorIntegration() { }); }; + useEffect(() => { + const loadAnchorSession = async () => { + if (!selectedAnchor) { + setAnchorSession(null); + setAuthStatus('disconnected'); + setAuthMessage('Not connected'); + return; + } + + if (!anchorService.hasWebAuth(selectedAnchor.id)) { + setAnchorSession(null); + setAuthStatus('disconnected'); + setAuthMessage('Anchor SEP-10 not configured'); + return; + } + + try { + const session = await anchorService.getAnchorAuthSession(selectedAnchor.id); + if (session && session.token) { + setAnchorSession(session); + setAuthStatus('connected'); + setAuthMessage(`Connected as ${session.accountPublicKey}`); + } else { + setAnchorSession(null); + setAuthStatus('disconnected'); + setAuthMessage('Not connected'); + } + } catch (error) { + setAnchorSession(null); + setAuthStatus('error'); + setAuthMessage('Unable to load anchor auth session'); + auditTrail.logError(error, { operation: 'loadAnchorAuthSession', anchorId: selectedAnchor.id }); + } + }; + + loadAnchorSession(); + }, [selectedAnchor]); + + const handleConnectToAnchor = async () => { + if (!selectedAnchor) return; + + setAuthError(null); + setIsAnchorAuthLoading(true); + setAuthStatus('loading'); + + try { + const account = await connectFreighter(); + const challengeResponse = await anchorService.requestChallengeTransaction( + selectedAnchor.id, + account.publicKey, + account.network + ); + + const signedXdr = await signTransactionWithFreighter(challengeResponse.transaction, account.network); + const token = await anchorService.submitChallengeTransaction(selectedAnchor.id, signedXdr, account.network); + + await anchorService.saveAnchorAuthSession( + selectedAnchor.id, + token, + account.publicKey, + account.network, + selectedAnchor.homeDomain + ); + + const jwtPayload = anchorService.parseJwt(token); + const session = { + token, + accountPublicKey: account.publicKey, + network: account.network, + homeDomain: selectedAnchor.homeDomain, + tokenPayload: jwtPayload + }; + + setAnchorSession(session); + setAuthStatus('connected'); + setAuthMessage(`Connected as ${account.publicKey}`); + auditTrail.logUserAction('Authenticated with anchor via SEP-10', { + anchorId: selectedAnchor.id, + anchorName: selectedAnchor.name, + accountPublicKey: account.publicKey, + network: account.network, + homeDomain: selectedAnchor.homeDomain + }); + } catch (error) { + setAuthStatus('error'); + setAuthError(error.message || 'Anchor authentication failed'); + auditTrail.logError(error, { operation: 'anchorSep10Auth', anchorId: selectedAnchor.id, error: error?.message }); + } finally { + setIsAnchorAuthLoading(false); + } + }; + + const handleDisconnectAnchor = async () => { + if (!selectedAnchor) return; + await anchorService.clearAnchorAuthSession(selectedAnchor.id); + setAnchorSession(null); + setAuthStatus('disconnected'); + setAuthMessage('Not connected'); + auditTrail.logUserAction('Disconnected anchor SEP-10 session', { + anchorId: selectedAnchor.id, + anchorName: selectedAnchor.name + }); + }; + const generateInstructions = () => { if (!selectedAnchor) return null; @@ -378,6 +484,99 @@ export default function AnchorIntegration() { )} +
+
+
+
+ Anchor Authentication +
+
+ {anchorService.hasWebAuth(selectedAnchor.id) + ? 'This anchor supports SEP-10 web authentication. Connect with Freighter to request a signed challenge and receive a secure session token.' + : 'SEP-10 authentication is not available for this anchor.'} +
+
+ + {anchorService.hasWebAuth(selectedAnchor.id) && ( +
+ + +
+ )} +
+ +
+
+
Connection Status
+
{authStatus === 'loading' ? 'Connecting...' : authMessage}
+
+ +
+
Anchor Domain
+
{selectedAnchor.homeDomain || 'Unknown'}
+
+ +
+
Authenticated Wallet
+
{anchorSession?.accountPublicKey || 'None'}
+
+ +
+
Network
+
{anchorSession?.network || 'TESTNET'}
+
+
+ + {anchorSession?.tokenPayload?.exp && ( +
+ Token expires at {new Date(anchorSession.tokenPayload.exp * 1000).toLocaleString()}. +
+ )} + + {authError && ( +
+ {authError} +
+ )} +
+