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
17,686 changes: 9,077 additions & 8,609 deletions package-lock.json

Large diffs are not rendered by default.

161 changes: 83 additions & 78 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,80 +1,85 @@
{
"name": "chainremit_backend",
"version": "1.0.0",
"description": "",
"main": "dist/app.js",
"scripts": {
"dev": "nodemon src/app.ts",
"build": "tsc",
"start": "node dist/app.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\"",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "https://github.com/MetroLogic/chainremit_backend.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/MetroLogic/chainremit_backend.git/issues"
},
"homepage": "https://github.com/MetroLogic/chainremit_backend.git#readme",
"dependencies": {
"bcrypt": "^6.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^8.0.1",
"google-auth-library": "^10.1.0",
"helmet": "^6.1.5",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"typescript": "^5.0.0",
"winston": "^3.17.0"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/bcrypt": "^6.0.0",
"@types/compression": "^1.8.1",
"@types/cors": "^2.8.19",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.10",
"@types/node": "^20.0.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/winston": "^2.4.4",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"globals": "^16.3.0",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jiti": "^2.5.1",
"lint-staged": "^16.1.2",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.1",
"typescript-eslint": "^8.38.0"
},
"lint-staged": {
"src/**/*.ts": [
"eslint --fix",
"prettier --write"
]
}
"name": "chainremit_backend",
"version": "1.0.0",
"description": "",
"main": "dist/app.js",
"scripts": {
"dev": "nodemon src/app.ts",
"build": "tsc",
"start": "node dist/app.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\"",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "https://github.com/MetroLogic/chainremit_backend.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/MetroLogic/chainremit_backend.git/issues"
},
"homepage": "https://github.com/MetroLogic/chainremit_backend.git#readme",
"dependencies": {
"axios": "^1.7.0",
"bcrypt": "^6.0.0",
"bull": "^4.10.4",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^8.0.1",
"google-auth-library": "^10.1.0",
"helmet": "^6.1.5",
"ioredis": "^5.3.2",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"typescript": "^5.0.0",
"winston": "^3.17.0",
"ws": "^8.15.0"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/bcrypt": "^6.0.0",
"@types/compression": "^1.8.1",
"@types/cors": "^2.8.19",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.10",
"@types/node": "^20.0.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/winston": "^2.4.4",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"globals": "^16.3.0",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jiti": "^2.5.1",
"lint-staged": "^16.1.2",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.1",
"typescript-eslint": "^8.38.0"
},
"lint-staged": {
"src/**/*.ts": [
"eslint --fix",
"prettier --write"
]
}
}
12 changes: 11 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import logger from './utils/logger';
import { setupSwagger } from './swagger';
import authRouter from '../src/router/auth.router';

import ratesRouter from '../src/router/rates.router';

import { scheduleRatesJob } from '../src/jobs/rates.job';

// Load environment variables
const env = process.env.NODE_ENV || 'development';
dotenv.config({ path: `.env.${env}` });
Expand All @@ -31,6 +35,7 @@ app.use(express.json());
setupSwagger(app);

app.use('/auth', authRouter);
app.use('/api/rates', ratesRouter);

// Health check endpoint
app.get('/health', (_req, res) => {
Expand All @@ -45,8 +50,13 @@ app.use((err: Error, _req: express.Request, res: express.Response) => {

const PORT = process.env.PORT || 3000;
if (process.env.NODE_ENV !== 'test') {
app.listen(PORT, () => {
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
scheduleRatesJob();
});
// WebSocket setup
import('../src/ws/rates.ws').then(({ setupRatesWebSocket }) => {
setupRatesWebSocket(server);
});
}

Expand Down
9 changes: 9 additions & 0 deletions src/config/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Redis from 'ioredis';

const redis = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
});

export default redis;
52 changes: 52 additions & 0 deletions src/controller/rates.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Request, Response } from 'express';
import { RatesService } from '../services/rates.service';
import Joi from 'joi';

export class RatesController {
static async getCurrentRates(req: Request, res: Response) {
try {
const cached = await RatesService.getCachedRates();
if (cached) return res.json(cached);
const rates = await RatesService.fetchCurrentRates();
await RatesService.cacheRates(rates);
return res.json(rates);
} catch (err) {
return res.status(500).json({ error: 'Failed to fetch current rates' });
}
}

static async getHistoricalRates(req: Request, res: Response) {
try {
const historical = await RatesService.getHistoricalRates();
return res.json(historical);
} catch (err) {
return res.status(500).json({ error: 'Failed to fetch historical rates' });
}
}

static async postPriceAlert(req: Request, res: Response) {
const schema = Joi.object({
symbol: Joi.string().required(),
targetPrice: Joi.number().required(),
direction: Joi.string().valid('above', 'below').required(),
userId: Joi.string().required(),
});
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).json({ error: error.details[0].message });
try {
await RatesService.setPriceAlert({ ...value, createdAt: Date.now() });
return res.status(201).json({ success: true });
} catch (err) {
return res.status(500).json({ error: 'Failed to set price alert' });
}
}

static async getSupportedCurrencies(req: Request, res: Response) {
try {
const currencies = await RatesService.getSupportedCurrencies();
return res.json(currencies);
} catch (err) {
return res.status(500).json({ error: 'Failed to fetch supported currencies' });
}
}
}
23 changes: 23 additions & 0 deletions src/jobs/rates.job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Bull from 'bull';
import { RatesService } from '../services/rates.service';

const ratesQueue = new Bull('rates-update', {
redis: {
host: process.env.REDIS_HOST || '127.0.0.1',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
},
});

ratesQueue.process(async () => {
const rates = await RatesService.fetchCurrentRates();
await RatesService.cacheRates(rates);
await RatesService.storeHistoricalRates(rates);
});

export function scheduleRatesJob() {
// Every minute
ratesQueue.add({}, { repeat: { cron: '* * * * *' } });
}

export default ratesQueue;
19 changes: 19 additions & 0 deletions src/model/rates.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Rate {
symbol: string;
price: number;
currency: string;
timestamp: number;
}

export interface HistoricalRate {
timestamp: number;
rates: Rate[];
}

export interface PriceAlert {
symbol: string;
targetPrice: number;
direction: 'above' | 'below';
userId: string;
createdAt: number;
}
11 changes: 11 additions & 0 deletions src/router/rates.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Router } from 'express';
import { RatesController } from '../controller/rates.controller';

const router = Router();

router.get('/current', RatesController.getCurrentRates);
router.get('/historical', RatesController.getHistoricalRates);
router.post('/alerts', RatesController.postPriceAlert);
router.get('/supported-currencies', RatesController.getSupportedCurrencies);

export default router;
Loading