diff --git a/api/middleware/auth.js b/api/middleware/auth.js new file mode 100644 index 00000000..e400b0c3 --- /dev/null +++ b/api/middleware/auth.js @@ -0,0 +1,18 @@ +// Simple mock OAuth authentication middleware +export const oauthAuth = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized: Missing or invalid token' }); + } + + const token = authHeader.split(' ')[1]; + // In a real scenario, we would validate the token with an OAuth provider + // Here we just check if it's not empty and meets a minimum length + if (token.length < 10) { + return res.status(401).json({ error: 'Unauthorized: Invalid token' }); + } + + // Attach mock user object + req.user = { id: 'user-1', roles: ['api_user'] }; + next(); +}; diff --git a/api/middleware/rateLimiter.js b/api/middleware/rateLimiter.js new file mode 100644 index 00000000..d127fb8d --- /dev/null +++ b/api/middleware/rateLimiter.js @@ -0,0 +1,23 @@ +const rateLimitWindow = 60 * 1000; // 1 minute +const maxRequests = 100; +const requests = new Map(); + +export const rateLimiter = (req, res, next) => { + const ip = req.ip || req.connection.remoteAddress; + const now = Date.now(); + + if (!requests.has(ip)) { + requests.set(ip, []); + } + + const userRequests = requests.get(ip); + const windowRequests = userRequests.filter(time => now - time < rateLimitWindow); + + if (windowRequests.length >= maxRequests) { + return res.status(429).json({ error: 'Too many requests, please try again later.' }); + } + + windowRequests.push(now); + requests.set(ip, windowRequests); + next(); +}; diff --git a/api/routes/accounts.js b/api/routes/accounts.js new file mode 100644 index 00000000..09b8677e --- /dev/null +++ b/api/routes/accounts.js @@ -0,0 +1,16 @@ +import express from 'express'; +export const router = express.Router(); + +router.get('/:accountId', (req, res) => { + const { accountId } = req.params; + + // Mock data for the dashboard account endpoint + res.json({ + id: accountId, + balance: '1000 XLM', + status: 'active', + sequence_number: '123456789', + subentry_count: 2, + last_modified_ledger: 1000000 + }); +}); diff --git a/api/routes/transactions.js b/api/routes/transactions.js new file mode 100644 index 00000000..79205fb8 --- /dev/null +++ b/api/routes/transactions.js @@ -0,0 +1,30 @@ +import express from 'express'; +export const router = express.Router(); + +router.get('/', (req, res) => { + const { accountId, limit = 10 } = req.query; + + if (!accountId) { + return res.status(400).json({ error: 'accountId query parameter is required' }); + } + + // Mock transactions for the dashboard + const transactions = []; + const parsedLimit = parseInt(limit, 10); + + for (let i = 0; i < parsedLimit; i++) { + transactions.push({ + id: `tx_${Date.now()}_${i}`, + source_account: accountId, + created_at: new Date(Date.now() - i * 3600000).toISOString(), + fee_charged: '100', + successful: true, + operation_count: 1 + }); + } + + res.json({ + data: transactions, + limit: parsedLimit + }); +}); diff --git a/api/server.js b/api/server.js new file mode 100644 index 00000000..be2f5c55 --- /dev/null +++ b/api/server.js @@ -0,0 +1,63 @@ +import express from 'express'; +import { createServer } from 'http'; +import { WebSocketServer } from 'ws'; +import { rateLimiter } from './middleware/rateLimiter.js'; +import { oauthAuth } from './middleware/auth.js'; +import { router as accountsRouter } from './routes/accounts.js'; +import { router as transactionsRouter } from './routes/transactions.js'; + +const app = express(); +const server = createServer(app); +const wss = new WebSocketServer({ server }); + +app.use(express.json()); +app.use(rateLimiter); + +// Public API routes +app.use('/api/v1/accounts', oauthAuth, accountsRouter); +app.use('/api/v1/transactions', oauthAuth, transactionsRouter); + +// Documentation endpoint +app.get('/api/docs', (req, res) => { + res.json({ + version: '1.0', + description: 'Stellar Dev Dashboard Public API', + endpoints: { + '/api/v1/accounts/:accountId': 'GET - Retrieve account data', + '/api/v1/transactions': 'GET - Query transactions (query params: accountId, limit)', + '/ws': 'WebSocket - Subscribe to real-time updates' + } + }); +}); + +// WebSocket support for real-time updates +wss.on('connection', (ws, req) => { + console.log('WebSocket client connected'); + ws.send(JSON.stringify({ type: 'connected', message: 'Successfully connected to real-time updates.' })); + + ws.on('message', (message) => { + try { + const data = JSON.parse(message.toString()); + if (data.type === 'subscribe') { + ws.send(JSON.stringify({ type: 'subscribed', channel: data.channel })); + } + } catch (e) { + ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' })); + } + }); + + // Simulate real-time updates + const interval = setInterval(() => { + ws.send(JSON.stringify({ type: 'update', data: { timestamp: new Date().toISOString(), status: 'active' } })); + }, 10000); + + ws.on('close', () => { + clearInterval(interval); + console.log('WebSocket client disconnected'); + }); +}); + +const PORT = process.env.API_PORT || 4000; +server.listen(PORT, () => { + console.log(`Public API server running on port ${PORT}`); +}); diff --git a/package.json b/package.json index 7289b9ab..2a8430c5 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "ci:testing": "npm run test:coverage && npm run test:coverage:check && npm run ci:workflows", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "docs:api:generate": "node scripts/generate-api-docs.mjs" + "docs:api:generate": "node scripts/generate-api-docs.mjs", + "api:start": "node api/server.js" }, "dependencies": { "@tensorflow/tfjs-node": "^5.0.0", @@ -67,7 +68,8 @@ "recharts": "^2.12.7", "uuid": "^9.0.1", "zustand": "^4.5.4", - "swr": "^2.2.0" + "swr": "^2.2.0", + "ws": "^8.16.0" }, "optionalDependencies": { "@ledgerhq/hw-transport-webhid": "^6.29.4", diff --git a/src/App.tsx b/src/App.tsx index dc8ac026..fa663ab8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import './styles/mobile-performance.css'; import { AccessibilityProvider } from './context/AccessibilityContext'; import ErrorBoundary from './components/ErrorBoundary'; import { DeveloperTools } from './components/DeveloperTools'; +import OnboardingFlow from './components/onboarding/OnboardingFlow'; const DashboardLayout = lazy(() => import('./routes/DashboardLayout')); @@ -36,10 +37,20 @@ function AppLoadingFallback() { } export default function App() { + const [showOnboarding, setShowOnboarding] = React.useState(false); + + React.useEffect(() => { + const hasCompleted = localStorage.getItem('hasCompletedOnboarding'); + if (!hasCompleted) { + setShowOnboarding(true); + } + }, []); + return ( + {showOnboarding && setShowOnboarding(false)} />} }> } /> diff --git a/src/components/anchors/AnchorIntegration.jsx b/src/components/anchors/AnchorIntegration.jsx index 80f00c03..c747f783 100644 --- a/src/components/anchors/AnchorIntegration.jsx +++ b/src/components/anchors/AnchorIntegration.jsx @@ -14,6 +14,8 @@ import { Coins, ChevronDown, ChevronUp, + Star, + Shield, } from 'lucide-react'; import anchorService from '../../lib/anchors.js'; import auditTrail from '../../lib/auditTrail.js'; @@ -623,8 +625,13 @@ function AnchorCard({ anchor, feeData, isExpanded, onToggle, onSelect, isSelecte
{anchor.name}
-
- {anchor.supportedAssets.length} assets • {anchor.depositMethods.length + anchor.withdrawalMethods.length} methods +
+ {anchor.supportedAssets.length} assets • {anchor.depositMethods.length + anchor.withdrawalMethods.length} methods + {anchor.rating && ( + + {anchor.rating.toFixed(1)} + + )}
@@ -749,6 +756,61 @@ function AnchorCard({ anchor, feeData, isExpanded, onToggle, onSelect, isSelecte +
+
+
Anchor Capabilities & SEP Support
+ +
+ + {anchor.reviews && anchor.reviews.length > 0 && ( +
+
User Reviews
+
+ {anchor.reviews.map((review, idx) => ( +
+
+ {review.user} + + {review.rating} + +
+
"{review.comment}"
+
+ ))} +
+
+ )} +
+
Fee Structure: Deposit {anchor.fees.deposit}, Withdrawal {anchor.fees.withdrawal} (min: {anchor.fees.minimum}) diff --git a/src/components/onboarding/OnboardingFlow.jsx b/src/components/onboarding/OnboardingFlow.jsx new file mode 100644 index 00000000..f2ab753f --- /dev/null +++ b/src/components/onboarding/OnboardingFlow.jsx @@ -0,0 +1,224 @@ +import React, { useState, useEffect } from 'react'; +import { ChevronRight, ChevronLeft, Check, SkipForward, Wallet, User, Layout, Star } from 'lucide-react'; +import { connectFreighter } from '../../lib/wallet/freighter.js'; + +export default function OnboardingFlow({ onComplete }) { + const [step, setStep] = useState(0); + const [walletConnected, setWalletConnected] = useState(false); + const [preferences, setPreferences] = useState({ theme: 'dark', layout: 'advanced' }); + + const steps = [ + { + id: 'welcome', + title: 'Welcome to Stellar Dev Dashboard', + description: 'Let\'s get you set up to explore the Stellar network, test integrations, and build powerful applications.', + icon: + }, + { + id: 'account', + title: 'Create Your Account', + description: 'You can generate a new testnet account or bring your own.', + icon: + }, + { + id: 'wallet', + title: 'Connect Your Wallet', + description: 'Connect Freighter to securely sign transactions without sharing your secret keys.', + icon: + }, + { + id: 'preferences', + title: 'Customize Your Setup', + description: 'Choose how you want your dashboard to look and behave.', + icon: + } + ]; + + const handleNext = () => { + if (step < steps.length - 1) { + setStep(prev => prev + 1); + } else { + finishOnboarding(); + } + }; + + const handlePrev = () => { + if (step > 0) { + setStep(prev => prev - 1); + } + }; + + const finishOnboarding = () => { + localStorage.setItem('hasCompletedOnboarding', 'true'); + localStorage.setItem('dashboardPreferences', JSON.stringify(preferences)); + if (onComplete) onComplete(); + }; + + const handleConnectWallet = async () => { + try { + const account = await connectFreighter(); + if (account) { + setWalletConnected(true); + setTimeout(handleNext, 1000); // Auto advance after success + } + } catch (err) { + alert('Failed to connect wallet. Ensure Freighter is installed and unlocked.'); + } + }; + + return ( +
+
+ + + {/* Progress Bar */} +
+ {steps.map((s, idx) => ( +
+ ))} +
+ + {/* Content */} +
+
+ {steps[step].icon} +
+

+ {steps[step].title} +

+

+ {steps[step].description} +

+ +
+ {step === 1 && ( +
+ +
+ )} + + {step === 2 && ( +
+ {walletConnected ? ( +
+ Wallet Connected Successfully +
+ ) : ( + + )} +
+ )} + + {step === 3 && ( +
+
setPreferences(p => ({ ...p, theme: 'dark' }))}> +
Dark Theme
+
Easier on the eyes
+
+
setPreferences(p => ({ ...p, theme: 'light' }))}> +
Light Theme
+
Crisp and clear
+
+
+ )} +
+
+ + {/* Navigation */} +
+ + + +
+
+
+ ); +} diff --git a/src/lib/anchors.js b/src/lib/anchors.js index df859e9c..52d2d852 100644 --- a/src/lib/anchors.js +++ b/src/lib/anchors.js @@ -25,7 +25,12 @@ class AnchorService { withdrawalMethods: ['bank_transfer', 'crypto'], fees: { deposit: '1.49%', withdrawal: '1.49%', minimum: '$0.99' }, processingTime: { deposit: '1-3 business days', withdrawal: '1-3 business days' }, - status: 'active' + status: 'active', + rating: 4.5, + reviews: [ + { user: 'user1', rating: 5, comment: 'Very reliable and fast.' }, + { user: 'user2', rating: 4, comment: 'Good but fees are slightly high.' } + ] }, { id: 'kraken', @@ -39,7 +44,12 @@ class AnchorService { withdrawalMethods: ['bank_transfer', 'wire', 'crypto'], fees: { deposit: 'Free', withdrawal: '0.0005 BTC', minimum: '$1' }, processingTime: { deposit: '1-5 business days', withdrawal: '1-5 business days' }, - status: 'active' + status: 'active', + rating: 4.8, + reviews: [ + { user: 'trader89', rating: 5, comment: 'Excellent security and low fees.' }, + { user: 'crypto_fan', rating: 5, comment: 'Best exchange for advanced trading.' } + ] }, { id: 'binance', @@ -53,7 +63,12 @@ class AnchorService { withdrawalMethods: ['crypto', 'p2p'], fees: { deposit: 'Free', withdrawal: '0.0005 XLM', minimum: '$1' }, processingTime: { deposit: 'Instant', withdrawal: 'Instant' }, - status: 'active' + status: 'active', + rating: 4.6, + reviews: [ + { user: 'whale22', rating: 5, comment: 'Huge liquidity.' }, + { user: 'newbie', rating: 4, comment: 'UI is a bit complex.' } + ] }, { id: 'bitstamp', @@ -67,7 +82,12 @@ class AnchorService { withdrawalMethods: ['bank_transfer', 'wire', 'crypto'], fees: { deposit: 'Free', withdrawal: '0.5%', minimum: '$10' }, processingTime: { deposit: '1-4 business days', withdrawal: '1-4 business days' }, - status: 'active' + status: 'active', + rating: 4.3, + reviews: [ + { user: 'investor_eu', rating: 4, comment: 'Solid fiat on-ramp for EUR.' }, + { user: 'anon', rating: 4, comment: 'Oldest exchange, very trustworthy.' } + ] }, { id: 'gatehub', @@ -81,7 +101,12 @@ class AnchorService { withdrawalMethods: ['bank_transfer', 'wire', 'crypto'], fees: { deposit: '1%', withdrawal: '1%', minimum: '$2.5' }, processingTime: { deposit: '1-3 business days', withdrawal: '1-3 business days' }, - status: 'active' + status: 'active', + rating: 4.1, + reviews: [ + { user: 'stellar_lover', rating: 5, comment: 'Great Stellar integration.' }, + { user: 'tester', rating: 3, comment: 'Support can be slow.' } + ] } ]; @@ -421,19 +446,41 @@ 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 = {}; + const result = { + CURRENCIES: [] + }; + let currentSection = null; + let currentCurrency = null; + 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 (trimmed.startsWith('[[CURRENCIES]]')) { + currentSection = 'CURRENCIES'; + currentCurrency = {}; + result.CURRENCIES.push(currentCurrency); + return; + } + + if (trimmed.startsWith('[')) { + currentSection = trimmed.replace(/[[\]]/g, ''); + if (!result[currentSection]) { + result[currentSection] = {}; + } + return; + } + + const match = trimmed.match(/^([A-Za-z0-9_]+)\s*=\s*['"](.*?)['"]$/); if (match) { - result[match[1]] = match[2]; + if (currentSection === 'CURRENCIES' && currentCurrency) { + currentCurrency[match[1]] = match[2]; + } else if (currentSection && typeof result[currentSection] === 'object' && !Array.isArray(result[currentSection])) { + result[currentSection][match[1]] = match[2]; + } else { + result[match[1]] = match[2]; + } } }); return result; @@ -462,6 +509,32 @@ class AnchorService { return this.parseToml(text); } + /** + * Check anchor capabilities (SEP support) + * @param {string} anchorId - Anchor identifier + * @returns {object} Capabilities + */ + async checkCapabilities(anchorId) { + const anchor = this.getAnchor(anchorId); + if (!anchor || !anchor.homeDomain) { + return null; + } + + try { + const toml = await this.fetchStellarToml(anchor.homeDomain); + return { + sep10Auth: !!toml.WEB_AUTH_ENDPOINT, + sep24Interactive: !!toml.TRANSFER_SERVER_SEP0024, + sep31CrossBorder: !!toml.DIRECT_PAYMENT_SERVER, + sep6Transfer: !!toml.TRANSFER_SERVER, + sep12KYC: !!toml.KYC_SERVER, + currencies: toml.CURRENCIES || [] + }; + } catch (e) { + return null; + } + } + async getWebAuthEndpoint(anchor) { if (!anchor) { throw new Error('Anchor not found');