Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -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();
};
23 changes: 23 additions & 0 deletions api/middleware/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -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();
};
16 changes: 16 additions & 0 deletions api/routes/accounts.js
Original file line number Diff line number Diff line change
@@ -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
});
});
30 changes: 30 additions & 0 deletions api/routes/transactions.js
Original file line number Diff line number Diff line change
@@ -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
});
});
63 changes: 63 additions & 0 deletions api/server.js
Original file line number Diff line number Diff line change
@@ -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}`);
});
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down Expand Up @@ -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 (
<I18nProvider>
<AccessibilityProvider>
<ErrorBoundary maxRetries={2}>
{showOnboarding && <OnboardingFlow onComplete={() => setShowOnboarding(false)} />}
<Suspense fallback={<AppLoadingFallback />}>
<Routes>
<Route path="/connect" element={<DashboardLayout />} />
Expand Down
66 changes: 64 additions & 2 deletions src/components/anchors/AnchorIntegration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -623,8 +625,13 @@ function AnchorCard({ anchor, feeData, isExpanded, onToggle, onSelect, isSelecte
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-primary)' }}>
{anchor.name}
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
{anchor.supportedAssets.length} assets • {anchor.depositMethods.length + anchor.withdrawalMethods.length} methods
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
<span>{anchor.supportedAssets.length} assets • {anchor.depositMethods.length + anchor.withdrawalMethods.length} methods</span>
{anchor.rating && (
<span style={{ display: 'flex', alignItems: 'center', gap: '2px', color: '#f59e0b' }}>
<Star size={12} fill="currentColor" /> {anchor.rating.toFixed(1)}
</span>
)}
</div>
</div>
</div>
Expand Down Expand Up @@ -749,6 +756,61 @@ function AnchorCard({ anchor, feeData, isExpanded, onToggle, onSelect, isSelecte
</div>
</div>

<div style={{ marginTop: '16px', borderTop: '1px solid var(--border)', paddingTop: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ fontWeight: 600, fontSize: '12px', color: 'var(--text-primary)' }}>Anchor Capabilities & SEP Support</div>
<button
onClick={async (e) => {
e.stopPropagation();
try {
const caps = await anchorService.checkCapabilities(anchor.id);
if (caps) {
alert(`SEP-10 Auth: ${caps.sep10Auth ? 'Yes' : 'No'}\nSEP-24 Interactive: ${caps.sep24Interactive ? 'Yes' : 'No'}\nSEP-31 Cross-border: ${caps.sep31CrossBorder ? 'Yes' : 'No'}\nSEP-6 Transfer: ${caps.sep6Transfer ? 'Yes' : 'No'}\nSEP-12 KYC: ${caps.sep12KYC ? 'Yes' : 'No'}\nSupported Currencies: ${caps.currencies?.length || 0}`);
} else {
alert('Could not fetch capabilities. Anchor may not support SEP standards.');
}
} catch (err) {
alert('Error fetching capabilities.');
}
}}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
padding: '4px 10px',
background: 'var(--cyan)',
color: '#fff',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
<Shield size={12} />
Check Capabilities
</button>
</div>

{anchor.reviews && anchor.reviews.length > 0 && (
<div style={{ marginTop: '16px' }}>
<div style={{ fontWeight: 600, fontSize: '12px', color: 'var(--text-primary)', marginBottom: '8px' }}>User Reviews</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '8px' }}>
{anchor.reviews.map((review, idx) => (
<div key={idx} style={{ padding: '8px', background: 'var(--bg-card)', borderRadius: '6px', fontSize: '11px', border: '1px solid var(--border)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{review.user}</span>
<span style={{ display: 'flex', alignItems: 'center', color: '#f59e0b' }}>
<Star size={10} fill="currentColor" /> {review.rating}
</span>
</div>
<div style={{ color: 'var(--text-secondary)' }}>"{review.comment}"</div>
</div>
))}
</div>
</div>
)}
</div>

<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid var(--border)' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
<strong>Fee Structure:</strong> Deposit {anchor.fees.deposit}, Withdrawal {anchor.fees.withdrawal} (min: {anchor.fees.minimum})
Expand Down
Loading