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) && (
+
+
+ {anchorSession ? 'Reconnect to Anchor' : 'Connect to Anchor'}
+
+
+ Disconnect
+
+
+ )}
+
+
+
+
+
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}
+
+ )}
+
+
window.open(instructions.supportUrl, '_blank')}
diff --git a/src/lib/anchors.js b/src/lib/anchors.js
index 6769409..df859e9 100644
--- a/src/lib/anchors.js
+++ b/src/lib/anchors.js
@@ -1,3 +1,7 @@
+import { Transaction, Networks } from '@stellar/stellar-sdk';
+import { encryptWithKey, decryptWithKey, generateKey } from './encryption.js';
+import auditTrail from './auditTrail.js';
+
/**
* Stellar Anchor Integration System
* Integrates with major Stellar anchors for deposit/withdrawal operations
@@ -6,12 +10,16 @@
class AnchorService {
constructor() {
this.anchors = new Map();
+ this.authSessionKey = null;
+ this.authSessionStorePrefix = 'sep10-anchor-session:';
this.supportedAnchors = [
{
id: 'coinbase',
name: 'Coinbase',
icon: '🪙',
website: 'https://coinbase.com',
+ homeDomain: 'coinbase.com',
+ authEndpoint: 'https://coinbase.com/auth',
supportedAssets: ['XLM', 'USDC', 'BTC', 'ETH', 'USDT'],
depositMethods: ['bank_transfer', 'card', 'crypto'],
withdrawalMethods: ['bank_transfer', 'crypto'],
@@ -24,6 +32,8 @@ class AnchorService {
name: 'Kraken',
icon: '🐙',
website: 'https://kraken.com',
+ homeDomain: 'kraken.com',
+ authEndpoint: 'https://kraken.com/auth',
supportedAssets: ['XLM', 'USDC', 'BTC', 'ETH', 'USDT', 'EUR', 'USD'],
depositMethods: ['bank_transfer', 'wire', 'crypto'],
withdrawalMethods: ['bank_transfer', 'wire', 'crypto'],
@@ -36,6 +46,8 @@ class AnchorService {
name: 'Binance',
icon: '🔶',
website: 'https://binance.com',
+ homeDomain: 'binance.com',
+ authEndpoint: 'https://binance.com/auth',
supportedAssets: ['XLM', 'USDC', 'BTC', 'ETH', 'USDT', 'BUSD'],
depositMethods: ['crypto', 'card', 'p2p'],
withdrawalMethods: ['crypto', 'p2p'],
@@ -48,6 +60,8 @@ class AnchorService {
name: 'Bitstamp',
icon: '📊',
website: 'https://bitstamp.net',
+ homeDomain: 'bitstamp.net',
+ authEndpoint: 'https://bitstamp.net/auth',
supportedAssets: ['XLM', 'USDC', 'BTC', 'ETH', 'EUR', 'USD'],
depositMethods: ['bank_transfer', 'wire', 'crypto', 'card'],
withdrawalMethods: ['bank_transfer', 'wire', 'crypto'],
@@ -60,6 +74,8 @@ class AnchorService {
name: 'GateHub',
icon: '🚪',
website: 'https://gatehub.net',
+ homeDomain: 'gatehub.net',
+ authEndpoint: 'https://gatehub.net/auth',
supportedAssets: ['XLM', 'USDC', 'BTC', 'ETH', 'EUR', 'USD'],
depositMethods: ['bank_transfer', 'wire', 'crypto'],
withdrawalMethods: ['bank_transfer', 'wire', 'crypto'],
@@ -199,7 +215,7 @@ class AnchorService {
feeCalculation,
score: this.calculateAnchorScore(anchor, feeCalculation)
});
- } catch (error) {
+ } catch {
// Skip anchors that can't process this request
}
});
@@ -405,6 +421,224 @@ class AnchorService {
return `STELLAR-${Date.now()}-${Math.random().toString(36).substr(2, 8).toUpperCase()}`;
}
+ /**
+ * Parse a minimal subset of stellar.toml to extract values used for SEP-10.
+ * @param {string} tomlText
+ * @returns {object}
+ */
+ parseToml(tomlText) {
+ const result = {};
+ tomlText.split(/\r?\n/).forEach(line => {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) return;
+ const match = trimmed.match(/^([A-Za-z0-9_]+)\s*=\s*['"](.+?)['"]$/);
+ if (match) {
+ result[match[1]] = match[2];
+ }
+ });
+ return result;
+ }
+
+ async fetchStellarToml(homeDomain) {
+ if (!homeDomain) {
+ throw new Error('Home domain is required to resolve stellar.toml');
+ }
+
+ const url = `https://${homeDomain}/.well-known/stellar.toml`;
+ const start = performance.now();
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: { 'Accept': 'text/plain' }
+ });
+ const duration = Math.round(performance.now() - start);
+
+ auditTrail.logAPICall(url, 'GET', {}, { status: response.status, responseTime: duration });
+
+ if (!response.ok) {
+ throw new Error(`Unable to fetch stellar.toml for ${homeDomain}`);
+ }
+
+ const text = await response.text();
+ return this.parseToml(text);
+ }
+
+ async getWebAuthEndpoint(anchor) {
+ if (!anchor) {
+ throw new Error('Anchor not found');
+ }
+
+ if (anchor.authEndpoint) {
+ return anchor.authEndpoint;
+ }
+
+ if (anchor.homeDomain) {
+ const toml = await this.fetchStellarToml(anchor.homeDomain);
+ if (!toml.WEB_AUTH_ENDPOINT) {
+ throw new Error(`WEB_AUTH_ENDPOINT not defined in stellar.toml for ${anchor.homeDomain}`);
+ }
+ return toml.WEB_AUTH_ENDPOINT;
+ }
+
+ throw new Error(`Anchor ${anchor.id} does not support SEP-10 authentication`);
+ }
+
+ hasWebAuth(anchorId) {
+ const anchor = this.getAnchor(anchorId);
+ return Boolean(anchor && (anchor.authEndpoint || anchor.homeDomain));
+ }
+
+ async requestChallengeTransaction(anchorId, accountPublicKey, network = 'TESTNET') {
+ const anchor = this.getAnchor(anchorId);
+ const endpoint = await this.getWebAuthEndpoint(anchor);
+ const networkPassphrase = network === 'PUBLIC' ? Networks.PUBLIC : Networks.TESTNET;
+
+ const url = new URL(endpoint);
+ url.searchParams.set('account', accountPublicKey);
+ url.searchParams.set('network_passphrase', networkPassphrase);
+
+ const start = performance.now();
+ const response = await fetch(url.toString(), { method: 'GET', headers: { 'Accept': 'application/json' } });
+ const duration = Math.round(performance.now() - start);
+
+ const responseData = await response.clone().json().catch(() => ({}));
+ auditTrail.logAPICall(url.toString(), 'GET', { account: accountPublicKey }, { status: response.status, responseTime: duration, responseData });
+
+ if (!response.ok) {
+ throw new Error(`Failed to request SEP-10 challenge: ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (!data.transaction) {
+ throw new Error('Invalid SEP-10 challenge response from anchor');
+ }
+
+ try {
+ new Transaction(data.transaction, networkPassphrase);
+ } catch (error) {
+ throw new Error(`Invalid SEP-10 challenge transaction: ${error.message}`);
+ }
+
+ return data;
+ }
+
+ async submitChallengeTransaction(anchorId, signedTransactionXdr) {
+ const anchor = this.getAnchor(anchorId);
+ const endpoint = await this.getWebAuthEndpoint(anchor);
+ const start = performance.now();
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ transaction: signedTransactionXdr })
+ });
+
+ const duration = Math.round(performance.now() - start);
+ const responseBody = await response.clone().json().catch(() => ({}));
+
+ auditTrail.logAPICall(endpoint, 'POST', { transaction: '[REDACTED]' }, { status: response.status, responseTime: duration, response: responseBody });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`SEP-10 authentication failed (${response.status}): ${errorText}`);
+ }
+
+ const data = await response.json();
+ if (!data.token) {
+ throw new Error('Anchor auth response did not include a JWT token');
+ }
+
+ return data.token;
+ }
+
+ async getOrCreateSessionKey() {
+ if (this.authSessionKey) {
+ return this.authSessionKey;
+ }
+
+ this.authSessionKey = await generateKey();
+ return this.authSessionKey;
+ }
+
+ async saveAnchorAuthSession(anchorId, token, accountPublicKey, network, homeDomain) {
+ try {
+ const rawKey = await this.getOrCreateSessionKey();
+ const encrypted = await encryptWithKey(token, rawKey);
+ const sessionPayload = {
+ ciphertext: encrypted.ciphertext,
+ iv: encrypted.iv,
+ accountPublicKey,
+ network,
+ homeDomain,
+ createdAt: new Date().toISOString()
+ };
+ sessionStorage.setItem(this.authSessionStorePrefix + anchorId, JSON.stringify(sessionPayload));
+ auditTrail.logSecurityEvent('Stored SEP-10 auth session', { anchorId, homeDomain });
+ return true;
+ } catch (error) {
+ auditTrail.logError(error, { operation: 'saveAnchorAuthSession', anchorId });
+ throw error;
+ }
+ }
+
+ async getAnchorAuthSession(anchorId) {
+ if (typeof window === 'undefined' || !window.sessionStorage) {
+ return null;
+ }
+
+ const sessionString = sessionStorage.getItem(this.authSessionStorePrefix + anchorId);
+ if (!sessionString) {
+ return null;
+ }
+
+ try {
+ const sessionPayload = JSON.parse(sessionString);
+ const rawKey = await this.getOrCreateSessionKey();
+ const token = await decryptWithKey(sessionPayload.ciphertext, rawKey, sessionPayload.iv);
+ return {
+ token,
+ accountPublicKey: sessionPayload.accountPublicKey,
+ network: sessionPayload.network,
+ homeDomain: sessionPayload.homeDomain,
+ createdAt: sessionPayload.createdAt
+ };
+ } catch (error) {
+ sessionStorage.removeItem(this.authSessionStorePrefix + anchorId);
+ auditTrail.logSecurityEvent('Failed to decrypt SEP-10 auth session', { anchorId, error: error.message });
+ return null;
+ }
+ }
+
+ async clearAnchorAuthSession(anchorId) {
+ if (typeof window === 'undefined' || !window.sessionStorage) {
+ return false;
+ }
+
+ sessionStorage.removeItem(this.authSessionStorePrefix + anchorId);
+ auditTrail.logUserAction('Cleared SEP-10 auth session', { anchorId });
+ return true;
+ }
+
+ parseJwt(token) {
+ if (!token || typeof token !== 'string') {
+ return null;
+ }
+
+ const parts = token.split('.');
+ if (parts.length !== 3) {
+ return null;
+ }
+
+ try {
+ const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
+ const padded = payload.padEnd(Math.ceil(payload.length / 4) * 4, '=');
+ const decoded = atob(padded);
+ return JSON.parse(decoded);
+ } catch (error) {
+ auditTrail.logError(error, { operation: 'parseJwt' });
+ return null;
+ }
+ }
+
/**
* Get real-time rates (mock implementation)
* @param {string} anchorId - Anchor identifier