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
10 changes: 9 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@
"start": "node dist/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:seed": "prisma db seed",
"prisma:studio": "prisma studio"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/adapter-pg": "^7.4.1",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"pg": "^8.18.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0",
Expand All @@ -31,6 +38,7 @@
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^25.2.3",
"@types/pg": "^8.16.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.1.11",
Expand All @@ -39,4 +47,4 @@
"tsx": "^4.19.2",
"typescript": "^5.9.3"
}
}
}
1 change: 0 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ generator client {

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

// User model - represents Stellar wallet addresses interacting with the protocol
Expand Down
77 changes: 77 additions & 0 deletions backend/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../src/generated/prisma/index.js';

const connectionString = process.env.DATABASE_URL;
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });

async function main() {
console.log('Seeding database...');

// Create example users
const user1 = await prisma.user.upsert({
where: { publicKey: 'GBRPYH6QC6WGLH473XI3CL4B3I754SFSULN5K3X7G3X4I6SGRH3V3U12' },
update: {},
create: {
publicKey: 'GBRPYH6QC6WGLH473XI3CL4B3I754SFSULN5K3X7G3X4I6SGRH3V3U12',
},
});

const user2 = await prisma.user.upsert({
where: { publicKey: 'GDRS6N3K7DQ6GKH47O6E5K5G7B7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V' },
update: {},
create: {
publicKey: 'GDRS6N3K7DQ6GKH47O6E5K5G7B7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V',
},
});

console.log({ user1, user2 });

// Create an example stream
const stream1 = await prisma.stream.upsert({
where: { streamId: 101 },
update: {},
create: {
streamId: 101,
sender: user1.publicKey,
recipient: user2.publicKey,
tokenAddress: 'CBTM5D262F6VQY4A6E4F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X',
ratePerSecond: '100000000', // 10 XLM/sec if decimals=7
depositedAmount: '1000000000000',
withdrawnAmount: '0',
startTime: Math.floor(Date.now() / 1000),
lastUpdateTime: Math.floor(Date.now() / 1000),
isActive: true,
},
});

console.log({ stream1 });

// Create an example event
const event1 = await prisma.streamEvent.create({
data: {
streamId: stream1.streamId,
eventType: 'CREATED',
amount: '1000000000000',
transactionHash: '6f7e8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r',
ledgerSequence: 123456,
timestamp: Math.floor(Date.now() / 1000),
metadata: JSON.stringify({ memo: 'Seed data' }),
},
});

console.log({ event1 });

console.log('Seeding finished.');
}

main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
16 changes: 14 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,29 @@ app.get('/', (req: Request, res: Response) => {
*/
app.get('/health', async (req: Request, res: Response) => {
const { getSandboxConfig } = await import('./config/sandbox.js');
const { prisma } = await import('./lib/prisma.js');
const sandboxConfig = getSandboxConfig();

res.json({
status: 'healthy',
let dbStatus = 'healthy';
try {
await prisma.$queryRaw`SELECT 1`;
} catch (error) {
dbStatus = 'unhealthy';
}

const status = dbStatus === 'healthy' ? 'healthy' : 'unhealthy';
res.status(status === 'healthy' ? 200 : 503).json({
status,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: '1.0.0',
apiVersions: {
supported: ['v1'],
default: 'v1',
},
services: {
database: dbStatus,
},
sandbox: {
enabled: sandboxConfig.enabled,
available: sandboxConfig.enabled,
Expand Down
24 changes: 19 additions & 5 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@ import logger from './logger.js';

dotenv.config();

const port = process.env.PORT || 3001;
const startServer = async () => {
try {
// Validate database connectivity
const { prisma } = await import('./lib/prisma.js');
await prisma.$connect();
await prisma.$queryRaw`SELECT 1`;
logger.info('Database connection established successfully');

app.listen(port, () => {
logger.info(`Server started on port ${port}`);
logger.info(`API Documentation available at http://localhost:${port}/api-docs`);
});
const port = process.env.PORT || 3001;
app.listen(port, () => {
logger.info(`Server started on port ${port}`);
logger.info(`API Documentation available at http://localhost:${port}/api-docs`);
});
} catch (error) {
logger.error('Failed to start server due to database connection error:', error);
process.exit(1);
}
};

startServer();
18 changes: 16 additions & 2 deletions backend/src/lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import { PrismaClient } from '@prisma/client';
import pg from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/index.js';

const globalForPrisma = global as unknown as { prisma: PrismaClient };
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
pool: pg.Pool;
};

const connectionString = process.env.DATABASE_URL;

if (!globalForPrisma.pool) {
globalForPrisma.pool = new pg.Pool({ connectionString });
}

const adapter = new PrismaPg(globalForPrisma.pool);

export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});

Expand Down
34 changes: 34 additions & 0 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -969,3 +969,37 @@ body {
top: 1rem;
}
}

.dashboard-loading-state,
.dashboard-error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
text-align: center;
border: 1px dashed rgba(19, 38, 61, 0.2);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.55);
padding: 2rem;
}

.dashboard-loading-state p,
.dashboard-error-state p {
margin-top: 1rem;
color: #4d6985;
}

.spinner {
width: 2.5rem;
height: 2.5rem;
border-radius: 999px;
border: 3px solid rgba(13, 83, 120, 0.1);
border-top-color: var(--accent-strong);
animation: spin 1s linear infinite;
}

.dashboard-error-state h3 {
color: #b12f3f;
margin: 0;
}
39 changes: 38 additions & 1 deletion frontend/app/incoming/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
"use client";

import IncomingStreams from "../../components/IncomingStreams";
import { Navbar } from "@/components/Navbar";
import { useWallet } from "@/context/wallet-context";
import React, { useEffect, useState } from "react";
import { fetchDashboardData, type Stream } from "@/lib/dashboard";

export default function IncomingPage() {
const { session, status } = useWallet();
const [streams, setStreams] = useState<Stream[]>([]);
const [loading, setLoading] = useState(true);
const [prevKey, setPrevKey] = useState(session?.publicKey);

// Reset loading state if public key changes (preferred over useEffect for this)
if (session?.publicKey !== prevKey) {
setPrevKey(session?.publicKey);
setLoading(true);
}

useEffect(() => {
if (session?.publicKey) {
fetchDashboardData(session.publicKey)
.then(data => setStreams(data.incomingStreams))
.catch(err => console.error("Failed to fetch incoming streams:", err))
.finally(() => setLoading(false));
}
}, [session?.publicKey]);

return (
<div className="flex min-h-screen flex-col bg-background font-sans text-foreground">
<Navbar />
<main className="flex-1 py-12 relative z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<IncomingStreams />
{status !== "connected" ? (
<div className="text-center py-20 bg-white/5 rounded-3xl backdrop-blur-xl border border-white/10">
<h2 className="text-2xl font-bold mb-4">Wallet Not Connected</h2>
<p className="text-slate-400">Please connect your wallet in the app to view your incoming streams.</p>
</div>
) : loading ? (
<div className="text-center py-20">
<div className="spinner mx-auto mb-4"></div>
<p>Loading incoming streams...</p>
</div>
) : (
<IncomingStreams streams={streams} />
)}
</div>
</main>
</div>
Expand Down
52 changes: 20 additions & 32 deletions frontend/components/IncomingStreams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,31 @@

import React, { useState } from 'react';
import toast from "react-hot-toast";
import type { Stream } from '@/lib/dashboard';

interface IncomingStreamData {
id: string;
sender: string;
token: string;
rate: string;
accrued: number;
status: 'Active' | 'Completed' | 'Paused';
interface IncomingStreamsProps {
streams: Stream[];
}

const mockIncomingStreams: IncomingStreamData[] = [
{ id: '101', sender: 'G...56yA', token: 'USDC', rate: '500/mo', accrued: 125.50, status: 'Active' },
{ id: '102', sender: 'G...Klm9', token: 'XLM', rate: '1000/mo', accrued: 450.00, status: 'Active' },
{ id: '103', sender: 'G...22Pq', token: 'EURC', rate: '200/mo', accrued: 200.00, status: 'Completed' },
{ id: '104', sender: 'G...99Zx', token: 'USDC', rate: '1200/mo', accrued: 0.00, status: 'Paused' },
{ id: '105', sender: 'G...44Tb', token: 'XLM', rate: '300/mo', accrued: 300.00, status: 'Completed' },
];

const IncomingStreams: React.FC = () => {
const IncomingStreams: React.FC<IncomingStreamsProps> = ({ streams }) => {
const [filter, setFilter] = useState<'All' | 'Active' | 'Completed' | 'Paused'>('All');

const filteredStreams = filter === 'All'
? mockIncomingStreams
: mockIncomingStreams.filter(s => s.status === filter);
? streams
: streams.filter(s => s.status === filter);

const handleWithdraw = async () => {
const toastId = toast.loading("Transaction pending...");
const toastId = toast.loading("Transaction pending...");

try {
// Simulate async transaction (replace with real blockchain call later)
await new Promise((resolve) => setTimeout(resolve, 2000));
try {
// Simulate async transaction (replace with real blockchain call later)
await new Promise((resolve) => setTimeout(resolve, 2000));

toast.success("Withdrawal successful!", { id: toastId });
} catch {
toast.error("Transaction failed.", { id: toastId });
}
};
toast.success("Withdrawal successful!", { id: toastId });
} catch {
toast.error("Transaction failed.", { id: toastId });
}
};

const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilter(e.target.value as 'All' | 'Active' | 'Completed' | 'Paused');
Expand Down Expand Up @@ -70,19 +58,19 @@ const IncomingStreams: React.FC = () => {
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Sender</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Token</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Rate</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Accrued Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Deposited</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Withdrawn</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredStreams.map((stream) => (
<tr key={stream.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">{stream.sender}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">{stream.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{stream.rate}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 font-bold">{stream.accrued.toFixed(2)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{stream.deposited} {stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 font-bold">{stream.withdrawn} {stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
Expand Down
Loading
Loading