Skip to content
Closed
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
12 changes: 12 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
<<<<<<< HEAD
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=lumenpulse_user
DB_PASSWORD=yourpassword
DB_DATABASE=lumenpulse_db
PYTHON_API_URL=http://localhost:8000
HORIZON_URL=https://horizon.stellar.org

# Backend Configuration
=======
# Copy this file to .env.local for local development.
# Never commit .env.local or any file containing real secret values.
>>>>>>> 24af299369a41e0f5687ed7bec1261d2e41026b7

# ── Required secrets (inject via platform in staging/production) ─────
DB_PASSWORD=postgres-password-here
Expand Down
21 changes: 19 additions & 2 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,30 @@ import { TestController } from './test/test.controller';
import { UploadModule } from './upload/upload.module';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
<<<<<<< HEAD
<<<<<<< HEAD
import { EmailModule } from './email/email.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import databaseConfig from './database/database.config';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { TestController } from './test/test.controller';
import { Module } from '@nestjs/common';
import { StellarService } from './services/StellarService';
import { StellarController } from './routes/stellar';
=======
>>>>>>> 32ecf6ba4de3e51a30acc180ef439b0291d4ebf9
=======
import { GrantsModule } from './grants/grants.module';
import { HealthModule } from './health/health.module';
import { OutboxModule } from './outbox/outbox.module';
import { VerificationModule } from './verification/verification.module';
import { TelegramBotModule } from './telegram-bot/telegram-bot.module';
import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor';
<<<<<<< HEAD
>>>>>>> 8ac87a7a55f7617bca2a78c4d4bf2c6ecf4d7757
=======
import { ExportModule } from './export/export.module';
>>>>>>> 24af299369a41e0f5687ed7bec1261d2e41026b7

@Module({
imports: [
Expand Down Expand Up @@ -116,9 +133,9 @@ import { ExportModule } from './export/export.module';
ModerationModule,
FeatureFlagsModule,
],
controllers: [AppController, TestController, TestExceptionController],
controllers: [AppController, TestController, TestExceptionController, StellarController],
providers: [
AppService,
AppService, StellarService,
{
provide: APP_GUARD,
useClass: RateLimitGuard,
Expand Down
14 changes: 14 additions & 0 deletions apps/backend/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// add import
import { TransactionFeed } from "../components/stellar/TransactionFeed";

function Dashboard() {
const publicKey = "YOUR_STELLAR_PUBLIC_KEY";

return (
<div>
{/* existing dashboard content */}

<TransactionFeed publicKey={publicKey} />
</div>
);
}
61 changes: 61 additions & 0 deletions apps/backend/src/routes/stellar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// src/routes/stellar.ts
import {
Controller,
Get,
Query,
BadRequestException,
ServiceUnavailableException,
InternalServerErrorException

Check failure on line 8 in apps/backend/src/routes/stellar.ts

View workflow job for this annotation

GitHub Actions / backend-checks

'InternalServerErrorException' is defined but never used
} from '@nestjs/common';
import { StellarService } from '../services/StellarService';

@Controller('stellar')
export class StellarController {
constructor(private readonly stellarService: StellarService) {}

@Get('assets')
async getAssets(
@Query('asset_code') assetCode?: string,
@Query('issuer') issuer?: string,
@Query('q') q?: string,
@Query('limit') limit: number = 20, // NestJS will handle the default
) {
try {
let assets = [];

// Logic flow based on provided query parameters
if (assetCode) {
assets = await this.stellarService.findAssetsByCode(assetCode);
} else if (issuer) {
assets = await this.stellarService.findAssetsByIssuer(issuer);
} else if (q) {
assets = await this.stellarService.searchAssets(q);
} else {
throw new BadRequestException('Please provide asset_code, issuer, or a search query (q)');
}

// Return standardized response
return {
success: true,
assets: assets.slice(0, Number(limit)),
pagination: {
total_returned: Math.min(assets.length, Number(limit)),
limit: Number(limit),
// Horizon uses cursors for real pagination;
// this is a simple slice for now.
},
};

} catch (err) {
// If it's already a NestJS error (like the BadRequest above), just rethrow it
if (err instanceof BadRequestException) throw err;

// Log the error internally for debugging
console.error('Stellar Horizon Error:', err);

// Handle cases where Horizon responds but with an error
const message = err.response?.data?.title || err.message;
throw new ServiceUnavailableException(`Horizon error: ${message}`);

Check failure on line 58 in apps/backend/src/routes/stellar.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe member access .message on an `any` value

Check failure on line 58 in apps/backend/src/routes/stellar.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe member access .response on an `any` value

Check failure on line 58 in apps/backend/src/routes/stellar.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe assignment of an `any` value
}
}
}
52 changes: 52 additions & 0 deletions apps/backend/src/services/StellarService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
// Import Horizon specifically from the modern SDK
import { Horizon } from 'stellar-sdk';

@Injectable()
export class StellarService {
// Update the type to reflect the Horizon namespace
private horizon: Horizon.Server;

constructor() {
// Initialize using Horizon.Server
const horizonUrl = process.env.HORIZON_URL || 'https://horizon.stellar.org';
this.horizon = new Horizon.Server(horizonUrl);
}

async findAssetsByCode(assetCode: string) {
// Access assets through the horizon instance
const res = await this.horizon.assets().forCode(assetCode).call();
return res.records.map(record => this.normalizeAsset(record));
}

async findAssetsByIssuer(issuer: string) {
const res = await this.horizon.assets().forIssuer(issuer).call();
return res.records.map(record => this.normalizeAsset(record));
}

async searchAssets(query: string) {
const res = await this.horizon.assets().call();
return res.records
.filter(a =>
(a.asset_code && a.asset_code.includes(query)) ||
(a.asset_issuer && a.asset_issuer.includes(query))
)
.map(record => this.normalizeAsset(record));
}

/**
* Helper to format the Horizon record into a clean object.
* Using 'any' here for simplicity, but in v13.3.0,
* this is technically Horizon.ServerApi.AssetRecord
*/
private normalizeAsset(record: any) {
return {
code: record.asset_code,
issuer: record.asset_issuer,

Check failure on line 45 in apps/backend/src/services/StellarService.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe member access .asset_code on an `any` value

Check failure on line 45 in apps/backend/src/services/StellarService.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe assignment of an `any` value
num_accounts: record.num_accounts,

Check failure on line 46 in apps/backend/src/services/StellarService.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe member access .asset_issuer on an `any` value

Check failure on line 46 in apps/backend/src/services/StellarService.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe assignment of an `any` value
metadata: {

Check failure on line 47 in apps/backend/src/services/StellarService.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe member access .num_accounts on an `any` value

Check failure on line 47 in apps/backend/src/services/StellarService.ts

View workflow job for this annotation

GitHub Actions / backend-checks

Unsafe assignment of an `any` value
domain: record.home_domain || null,
},
};
}
}
28 changes: 28 additions & 0 deletions apps/backend/src/test/stellar.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../app.module';

describe('StellarController (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

it('/stellar/assets?asset_code=USDC (GET)', async () => {
const res = await request(app.getHttpServer()).get('/stellar/assets?asset_code=USDC');
expect(res.status).toBe(200);
expect(res.body.assets).toBeInstanceOf(Array);
});

it('/stellar/assets (GET) without params should fail', async () => {
const res = await request(app.getHttpServer()).get('/stellar/assets');
expect(res.status).toBe(400);
});
});
72 changes: 72 additions & 0 deletions apps/webapp/app/alerts/PriceAlertsPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// app/frontend/src/pages/alerts/PriceAlertsPage.js
import React, { useState, useEffect } from 'react';
import { getAlerts, createAlert, updateAlert, deleteAlert } from '../../services/priceAlertService';

function PriceAlertsPage() {
const [alerts, setAlerts] = useState([]);
const [newAlert, setNewAlert] = useState({ asset: '', targetPrice: '', direction: 'above' });

useEffect(() => {
getAlerts().then(setAlerts);
}, []);

const handleCreate = async () => {
await createAlert(newAlert);
setAlerts(await getAlerts());
setNewAlert({ asset: '', targetPrice: '', direction: 'above' });
};

const handleUpdate = async (id, updated) => {
await updateAlert(id, updated);
setAlerts(await getAlerts());
};

const handleDelete = async (id) => {
if (window.confirm('Delete this alert?')) {
await deleteAlert(id);
setAlerts(await getAlerts());
}
};

return (
<div>
<h2>Price Alerts</h2>
<div>
<input placeholder="Asset (e.g. XLM)" value={newAlert.asset} onChange={e => setNewAlert({ ...newAlert, asset: e.target.value })} />
<input placeholder="Target Price" value={newAlert.targetPrice} onChange={e => setNewAlert({ ...newAlert, targetPrice: e.target.value })} />
<select value={newAlert.direction} onChange={e => setNewAlert({ ...newAlert, direction: e.target.value })}>
<option value="above">Above</option>
<option value="below">Below</option>
</select>
<button onClick={handleCreate}>Create Alert</button>
</div>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Target Price</th>
<th>Direction</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{alerts.map(a => (
<tr key={a.id}>
<td>{a.asset}</td>
<td>{a.targetPrice}</td>
<td>{a.direction}</td>
<td>{a.createdAt}</td>
<td>
<button onClick={() => handleUpdate(a.id, { ...a, targetPrice: prompt('New price:', a.targetPrice) })}>Edit</button>
<button onClick={() => handleDelete(a.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default PriceAlertsPage;
18 changes: 18 additions & 0 deletions apps/webapp/app/alerts/priceAlertService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios';

export async function getAlerts() {
const res = await axios.get('/api/alerts');
return res.data;
}

export async function createAlert(alert) {
await axios.post('/api/alerts', alert);
}

export async function updateAlert(id, alert) {
await axios.put(`/api/alerts/${id}`, alert);
}

export async function deleteAlert(id) {
await axios.delete(`/api/alerts/${id}`);
}
24 changes: 24 additions & 0 deletions apps/webapp/app/alerts/price_alerts_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const service = require('../services/price_alerts_service');

router.get('/api/alerts', async (req, res) => {
res.json(await service.listAlerts(req.user.id));
});

router.post('/api/alerts', async (req, res) => {
await service.createAlert(req.user.id, req.body);
res.json({ success: true });
});

router.put('/api/alerts/:id', async (req, res) => {
await service.updateAlert(req.user.id, req.params.id, req.body);
res.json({ success: true });
});

router.delete('/api/alerts/:id', async (req, res) => {
await service.deleteAlert(req.user.id, req.params.id);
res.json({ success: true });
});

module.exports = router;
26 changes: 26 additions & 0 deletions apps/webapp/app/alerts/price_alerts_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const db = require('../db');

async function listAlerts(userId) {
const res = await db.query('SELECT * FROM price_alerts WHERE user_id=$1', [userId]);
return res.rows;
}

async function createAlert(userId, { asset, targetPrice, direction }) {
await db.query(
'INSERT INTO price_alerts (id, user_id, asset, target_price, direction, created_at) VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW())',
[userId, asset, targetPrice, direction]
);
}

async function updateAlert(userId, id, { targetPrice, direction }) {
await db.query(
'UPDATE price_alerts SET target_price=$1, direction=$2 WHERE id=$3 AND user_id=$4',
[targetPrice, direction, id, userId]
);
}

async function deleteAlert(userId, id) {
await db.query('DELETE FROM price_alerts WHERE id=$1 AND user_id=$2', [id, userId]);
}

module.exports = { listAlerts, createAlert, updateAlert, deleteAlert };
25 changes: 25 additions & 0 deletions apps/webapp/app/alerts/price_alerts_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const service = require('../src/services/price_alerts_service');

describe('Price Alerts Service', () => {
it('creates a new alert', async () => {
await service.createAlert('user1', { asset: 'XLM', targetPrice: 0.5, direction: 'above' });
const alerts = await service.listAlerts('user1');
expect(alerts.some(a => a.asset === 'XLM')).toBe(true);
});

it('updates an alert', async () => {
const alerts = await service.listAlerts('user1');
const id = alerts[0].id;
await service.updateAlert('user1', id, { targetPrice: 0.6, direction: 'below' });
const updated = await service.listAlerts('user1');
expect(updated.find(a => a.id === id).target_price).toBe(0.6);
});

it('deletes an alert', async () => {
const alerts = await service.listAlerts('user1');
const id = alerts[0].id;
await service.deleteAlert('user1', id);
const updated = await service.listAlerts('user1');
expect(updated.find(a => a.id === id)).toBeUndefined();
});
});
Loading
Loading