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+ } ;
0 commit comments