Skip to content

Commit f29f808

Browse files
authored
Merge pull request #1 from rodrigoioyz/feature/frontend-base
[Cardano][Pyth] Fullstack dashboard + backend price API integration
2 parents e8ccd74 + 6c8061d commit f29f808

37 files changed

Lines changed: 18026 additions & 0 deletions

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"snyk.advanced.autoSelectOrganization": true
3+
}

backend/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.env
3+
dist
4+
npm-debug.log
5+
.DS_Store

backend/app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import dotenv from 'dotenv';
2+
import Server from './models/server';
3+
dotenv.config();
4+
const server = new Server();
5+
server.listen();

backend/controllers/controller.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { Request, Response } from "express";
2+
import { MeshWallet, BlockfrostProvider } from "@meshsdk/core";
3+
import dotenv from "dotenv";
4+
dotenv.config();
5+
6+
7+
8+
const PYTH_URLS = [
9+
process.env.BASE_URL1,
10+
process.env.BASE_URL2,
11+
process.env.BASE_URL3,
12+
].filter(Boolean) as string[];
13+
14+
const pythRequestBody = {
15+
symbols: ["Crypto.ADA/USD"],
16+
properties: ["price", "confidence", "exponent", "publisherCount"],
17+
formats: ["leEcdsa"],
18+
channel: "fixed_rate@200ms",
19+
parsed: true,
20+
jsonBinaryEncoding: "hex",
21+
};
22+
23+
async function fetchWithFallback(path: string, body: object): Promise<any> {
24+
let lastError: Error | null = null;
25+
26+
for (const baseUrl of PYTH_URLS) {
27+
try {
28+
const response = await fetch(`${baseUrl}${path}`, {
29+
method: "POST",
30+
headers: {
31+
"Authorization": `Bearer ${process.env.PYTH_API_KEY}`,
32+
"Content-Type": "application/json",
33+
},
34+
body: JSON.stringify(body),
35+
});
36+
37+
if (!response.ok) {
38+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
39+
}
40+
41+
return await response.json();
42+
43+
} catch (error: any) {
44+
console.warn(`⚠️ Falló ${baseUrl}: ${error.message}`);
45+
lastError = error;
46+
}
47+
}
48+
49+
throw new Error(`Todos los endpoints fallaron. Último error: ${lastError?.message}`);
50+
}
51+
52+
export const getADAPriceFromPyth = async (req: Request, res: Response) => {
53+
try {
54+
const data = await fetchWithFallback("/latest_price", pythRequestBody);
55+
56+
const feed = data.parsed.priceFeeds[0];
57+
const price = Number(feed.price) * Math.pow(10, feed.exponent);
58+
const confidence = Number(feed.confidence) * Math.pow(10, feed.exponent);
59+
const timestamp = new Date(Number(data.parsed.timestampUs) / 1000).toISOString();
60+
61+
res.json({
62+
symbol: "ADA/USD",
63+
price,
64+
confidence,
65+
publishers: feed.publisherCount,
66+
timestamp,
67+
leEcdsaPayload: data.leEcdsa?.data ?? null,
68+
});
69+
70+
} catch (error: any) {
71+
res.status(500).json({ error: error.message });
72+
}
73+
};
74+
75+
76+
export const getADAPriceRangeFromPyth = async (req: Request, res: Response) => {
77+
try {
78+
const { from, to, interval } = req.query;
79+
80+
if (!from || !to) {
81+
res.status(400).json({ error: "Se requieren 'from' y 'to' (Unix en segundos)" });
82+
return;
83+
}
84+
85+
const fromSec = Number(from);
86+
const toSec = Number(to);
87+
88+
// Intervalo en segundos, por defecto 1 minuto
89+
const intervalSec = Number(interval ?? 60);
90+
91+
// Generar array de timestamps entre from y to
92+
const timestamps: number[] = [];
93+
for (let t = fromSec; t <= toSec; t += intervalSec) {
94+
timestamps.push(t);
95+
}
96+
97+
// Límite de seguridad para no saturar la API
98+
if (timestamps.length > 500) {
99+
res.status(400).json({
100+
error: `Demasiados puntos (${timestamps.length}). Reduce el rango o aumenta el intervalo.`,
101+
suggestion: `Máximo recomendado: 500 puntos. Con interval=${Math.ceil((toSec - fromSec) / 500)}s entraría justo.`
102+
});
103+
return;
104+
}
105+
106+
// Llamadas en paralelo con límite de concurrencia (evitar rate limit 429)
107+
const CONCURRENCY = 10;
108+
const results: any[] = [];
109+
110+
for (let i = 0; i < timestamps.length; i += CONCURRENCY) {
111+
const batch = timestamps.slice(i, i + CONCURRENCY);
112+
113+
const batchResults = await Promise.allSettled(
114+
batch.map((ts) =>
115+
fetchWithFallback("/price", {
116+
symbols: ["Crypto.ADA/USD"],
117+
properties: ["price", "confidence", "exponent"],
118+
formats: ["leEcdsa"],
119+
channel: "fixed_rate@200ms",
120+
parsed: true,
121+
jsonBinaryEncoding: "hex",
122+
timestamp: ts * 1_000_000, // → microsegundos
123+
})
124+
)
125+
);
126+
127+
for (const result of batchResults) {
128+
if (result.status === "fulfilled") {
129+
const feed = result.value.parsed.priceFeeds[0];
130+
const price = Number(feed.price) * Math.pow(10, feed.exponent);
131+
const confidence = Number(feed.confidence) * Math.pow(10, feed.exponent);
132+
const time = Math.floor(Number(result.value.parsed.timestampUs) / 1_000_000);
133+
134+
results.push({
135+
time, // Unix segundos — formato que espera TradingView
136+
value: price,
137+
confidence,
138+
});
139+
}
140+
// Si falla un punto simplemente se omite, no rompe todo
141+
}
142+
}
143+
144+
// Ordenar por tiempo ascendente
145+
results.sort((a, b) => a.time - b.time);
146+
147+
res.json({
148+
symbol: "ADA/USD",
149+
from: new Date(fromSec * 1000).toISOString(),
150+
to: new Date(toSec * 1000).toISOString(),
151+
intervalSec,
152+
points: results.length,
153+
data: results,
154+
});
155+
156+
} catch (error: any) {
157+
res.status(500).json({ error: error.message });
158+
}
159+
};
160+
161+
162+
export const getADAPriceHistoryFromPyth = async (req: Request, res: Response) => {
163+
try {
164+
const { timestamp } = req.query;
165+
166+
if (!timestamp) {
167+
res.status(400).json({ error: "Se requiere el parámetro 'timestamp' (Unix en segundos)" });
168+
return;
169+
}
170+
171+
// Convertir segundos → microsegundos que requiere Pyth
172+
const timestampUs = Number(timestamp) * 1_000_000;
173+
174+
const data = await fetchWithFallback("/price", {
175+
symbols: ["Crypto.ADA/USD"],
176+
properties: ["price", "confidence", "exponent", "publisherCount"],
177+
formats: ["leEcdsa"],
178+
channel: "fixed_rate@200ms",
179+
parsed: true,
180+
jsonBinaryEncoding: "hex",
181+
timestamp: timestampUs,
182+
});
183+
184+
const feed = data.parsed.priceFeeds[0];
185+
const price = Number(feed.price) * Math.pow(10, feed.exponent);
186+
const confidence = Number(feed.confidence) * Math.pow(10, feed.exponent);
187+
const timestamp_iso = new Date(Number(data.parsed.timestampUs) / 1000).toISOString();
188+
189+
res.json({
190+
symbol: "ADA/USD",
191+
price,
192+
confidence,
193+
publishers: feed.publisherCount,
194+
timestamp_requested: new Date(Number(timestamp) * 1000).toISOString(),
195+
timestamp_actual: timestamp_iso,
196+
leEcdsaPayload: data.leEcdsa?.data ?? null,
197+
});
198+
199+
} catch (error: any) {
200+
// Pyth retorna 404 si no hay datos para ese timestamp
201+
if (error.message.includes("404")) {
202+
res.status(404).json({ error: "No se encontraron datos para ese timestamp" });
203+
return;
204+
}
205+
res.status(500).json({ error: error.message });
206+
}
207+
};

backend/models/server.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import express,{Application} from "express";
2+
import rateLimit from 'express-rate-limit';
3+
import cors from "cors";
4+
import dotenv from "dotenv";
5+
dotenv.config();
6+
7+
import getADAPriceRoute from "../routes/getADAPriceFromPyth";
8+
import getADAPriceRangeRoute from "../routes/getADAPriceRangeFromPyth";
9+
import getADAPriceHistoryRoute from "../routes/getADAPriceHistoryFromPyth";
10+
11+
class Server{
12+
private app: Application;
13+
private port: number;
14+
15+
private apiPath = {
16+
17+
getADAPricePath: '/api/get-adaprice',
18+
getADAPriceRangePath: '/api/get-adaprice-range',
19+
getADAPriceHistoryPath: '/api/get-adaprice-history',
20+
21+
}
22+
23+
constructor(){
24+
this.app = express();
25+
this.port = Number(process.env.PORT) || 3001;
26+
this.middlewares();
27+
this.routes();
28+
}
29+
30+
middlewares(){
31+
32+
this.app.use(cors());
33+
this.app.use(express.json({ limit: '25mb' }));
34+
this.app.use(express.urlencoded({ extended: true, limit: '25mb' }));
35+
36+
}
37+
38+
routes(){
39+
40+
this.app.use(this.apiPath.getADAPricePath, getADAPriceRoute);
41+
this.app.use(this.apiPath.getADAPriceRangePath, getADAPriceRangeRoute);
42+
this.app.use(this.apiPath.getADAPriceHistoryPath, getADAPriceHistoryRoute);
43+
44+
45+
}
46+
47+
listen(){
48+
this.app.listen(this.port, '127.0.0.1', () => {
49+
console.log('🚀 Server Pyth Hackaton runing http://127.0.0.1:' + this.port + ' version 1.0.0');
50+
51+
});
52+
}
53+
}
54+
55+
export default Server;

0 commit comments

Comments
 (0)