Optimize price API response size reduce the number of query to that API
This commit is contained in:
		
							parent
							
								
									f6c7839524
								
							
						
					
					
						commit
						5749820999
					
				@ -41,7 +41,13 @@ class MiningRoutes {
 | 
				
			|||||||
      res.header('Pragma', 'public');
 | 
					      res.header('Pragma', 'public');
 | 
				
			||||||
      res.header('Cache-control', 'public');
 | 
					      res.header('Cache-control', 'public');
 | 
				
			||||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
					      res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
				
			||||||
      res.status(200).send(await PricesRepository.$getHistoricalPrice());
 | 
					      if (req.query.timestamp) {
 | 
				
			||||||
 | 
					        res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
 | 
				
			||||||
 | 
					          parseInt(<string>req.query.timestamp ?? 0, 10)
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        res.status(200).send(await PricesRepository.$getHistoricalPrices());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
					      res.status(500).send(e instanceof Error ? e.message : e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -104,9 +104,48 @@ class PricesRepository {
 | 
				
			|||||||
    return rates[0];
 | 
					    return rates[0];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async $getHistoricalPrice(): Promise<Conversion | null> {
 | 
					  public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [rates]: any[] = await DB.query(`SELECT *, UNIX_TIMESTAMP(time) as time FROM prices ORDER BY time DESC`);
 | 
					      const [rates]: any[] = await DB.query(`
 | 
				
			||||||
 | 
					        SELECT *, UNIX_TIMESTAMP(time) AS time
 | 
				
			||||||
 | 
					        FROM prices
 | 
				
			||||||
 | 
					        WHERE UNIX_TIMESTAMP(time) < ?
 | 
				
			||||||
 | 
					        ORDER BY time DESC
 | 
				
			||||||
 | 
					        LIMIT 1`,
 | 
				
			||||||
 | 
					        [timestamp]
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (!rates) {
 | 
				
			||||||
 | 
					        throw Error(`Cannot get single historical price from the database`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Compute fiat exchange rates
 | 
				
			||||||
 | 
					      const latestPrice = await this.$getLatestConversionRates();
 | 
				
			||||||
 | 
					      const exchangeRates: ExchangeRates = {
 | 
				
			||||||
 | 
					        USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100,
 | 
				
			||||||
 | 
					        USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100,
 | 
				
			||||||
 | 
					        USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100,
 | 
				
			||||||
 | 
					        USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100,
 | 
				
			||||||
 | 
					        USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100,
 | 
				
			||||||
 | 
					        USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        prices: rates,
 | 
				
			||||||
 | 
					        exchangeRates: exchangeRates
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      logger.err(`Cannot fetch single historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async $getHistoricalPrices(): Promise<Conversion | null> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const [rates]: any[] = await DB.query(`
 | 
				
			||||||
 | 
					        SELECT *, UNIX_TIMESTAMP(time) AS time
 | 
				
			||||||
 | 
					        FROM prices
 | 
				
			||||||
 | 
					        ORDER BY time DESC
 | 
				
			||||||
 | 
					      `);
 | 
				
			||||||
      if (!rates) {
 | 
					      if (!rates) {
 | 
				
			||||||
        throw Error(`Cannot get average historical price from the database`);
 | 
					        throw Error(`Cannot get average historical price from the database`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -127,7 +166,7 @@ class PricesRepository {
 | 
				
			|||||||
        exchangeRates: exchangeRates
 | 
					        exchangeRates: exchangeRates
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.err(`Cannot fetch averaged historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
 | 
					      logger.err(`Cannot fetch historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -327,7 +327,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
            this.fetchRbfHistory$.next(this.tx.txid);
 | 
					            this.fetchRbfHistory$.next(this.tx.txid);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          this.priceService.getBlockPrice$(tx.status.block_time).pipe(
 | 
					          this.priceService.getBlockPrice$(tx.status.block_time, true).pipe(
 | 
				
			||||||
            tap((price) => {
 | 
					            tap((price) => {
 | 
				
			||||||
              this.blockConversion = price;
 | 
					              this.blockConversion = price;
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,7 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ngOnChanges(changes): void {
 | 
					  ngOnChanges(changes): void {
 | 
				
			||||||
    if (changes.line?.currentValue) {
 | 
					    if (changes.line?.currentValue) {
 | 
				
			||||||
      this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp).pipe(
 | 
					      this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true).pipe(
 | 
				
			||||||
        tap((price) => {
 | 
					        tap((price) => {
 | 
				
			||||||
          this.blockConversion = price;
 | 
					          this.blockConversion = price;
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
				
			|||||||
@ -305,7 +305,10 @@ export class ApiService {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getHistoricalPrice$(): Observable<Conversion> {
 | 
					  getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> {
 | 
				
			||||||
    return this.httpClient.get<Conversion>( this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price');
 | 
					    return this.httpClient.get<Conversion>(
 | 
				
			||||||
 | 
					      this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' +
 | 
				
			||||||
 | 
					        (timestamp ? `?timestamp=${timestamp}` : '')
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -41,6 +41,10 @@ export interface ConversionDict {
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
export class PriceService {
 | 
					export class PriceService {
 | 
				
			||||||
  priceObservable$: Observable<Conversion>;
 | 
					  priceObservable$: Observable<Conversion>;
 | 
				
			||||||
 | 
					  singlePriceObservable$: Observable<Conversion>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  lastQueriedTimestamp: number;
 | 
				
			||||||
 | 
					  lastPriceHistoryUpdate: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  historicalPrice: ConversionDict = {
 | 
					  historicalPrice: ConversionDict = {
 | 
				
			||||||
    prices: null,
 | 
					    prices: null,
 | 
				
			||||||
@ -63,53 +67,83 @@ export class PriceService {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getBlockPrice$(blockTimestamp: number): Observable<Price | undefined> {
 | 
					  getBlockPrice$(blockTimestamp: number, singlePrice = false): Observable<Price | undefined> {
 | 
				
			||||||
    if (!this.priceObservable$) {
 | 
					    const now = new Date().getTime() / 1000;
 | 
				
			||||||
      this.priceObservable$ = this.apiService.getHistoricalPrice$().pipe(shareReplay());
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Query nearest price for a specific blockTimestamp. The observable is invalidated if we
 | 
				
			||||||
 | 
					     * query a different timestamp than the last one
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    if (singlePrice) {
 | 
				
			||||||
 | 
					      if (!this.singlePriceObservable$ || (this.singlePriceObservable$ && blockTimestamp !== this.lastQueriedTimestamp)) {
 | 
				
			||||||
 | 
					        this.singlePriceObservable$ = this.apiService.getHistoricalPrice$(blockTimestamp).pipe(shareReplay());
 | 
				
			||||||
 | 
					        this.lastQueriedTimestamp = blockTimestamp;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return this.singlePriceObservable$.pipe(
 | 
				
			||||||
 | 
					        map((conversion) => {
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            price: {
 | 
				
			||||||
 | 
					              USD: conversion.prices[0].USD, EUR: conversion.prices[0].EUR, GBP: conversion.prices[0].GBP, CAD: conversion.prices[0].CAD,
 | 
				
			||||||
 | 
					              CHF: conversion.prices[0].CHF, AUD: conversion.prices[0].AUD, JPY: conversion.prices[0].JPY
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            exchangeRates: conversion.exchangeRates,
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this.priceObservable$.pipe(
 | 
					    /**
 | 
				
			||||||
      map((conversion) => {
 | 
					     * Query all price history only once. The observable is invalidated after 1 hour
 | 
				
			||||||
        if (!blockTimestamp) {
 | 
					     */
 | 
				
			||||||
          return undefined;
 | 
					    else {
 | 
				
			||||||
        }
 | 
					      if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600))) {
 | 
				
			||||||
 | 
					        this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined).pipe(shareReplay());
 | 
				
			||||||
 | 
					        this.lastPriceHistoryUpdate = new Date().getTime() / 1000;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const historicalPrice = {
 | 
					      return this.priceObservable$.pipe(
 | 
				
			||||||
          prices: {},
 | 
					        map((conversion) => {
 | 
				
			||||||
          exchangeRates: conversion.exchangeRates,
 | 
					          if (!blockTimestamp) {
 | 
				
			||||||
        };
 | 
					            return undefined;
 | 
				
			||||||
        for (const price of conversion.prices) {
 | 
					          }
 | 
				
			||||||
          historicalPrice.prices[price.time] = {
 | 
					
 | 
				
			||||||
            USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
 | 
					          const historicalPrice = {
 | 
				
			||||||
            CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
 | 
					            prices: {},
 | 
				
			||||||
 | 
					            exchangeRates: conversion.exchangeRates,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
        }
 | 
					          for (const price of conversion.prices) {
 | 
				
			||||||
 | 
					            historicalPrice.prices[price.time] = {
 | 
				
			||||||
        const priceTimestamps = Object.keys(historicalPrice.prices);
 | 
					              USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
 | 
				
			||||||
        priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
 | 
					              CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
 | 
				
			||||||
        priceTimestamps.sort().reverse();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Small trick here. Because latest blocks have higher timestamps than our
 | 
					 | 
				
			||||||
        // latest price timestamp (we only insert once every hour), we have no price for them.
 | 
					 | 
				
			||||||
        // Therefore we want to fallback to the websocket price by returning an undefined `price` field.
 | 
					 | 
				
			||||||
        // Since historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists
 | 
					 | 
				
			||||||
        // it will return `undefined` and automatically use the websocket price.
 | 
					 | 
				
			||||||
        // This way we can differenciate blocks without prices like the genesis block
 | 
					 | 
				
			||||||
        // vs ones without a price (yet) like the latest blocks
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
        for (const t of priceTimestamps) {
 | 
					 | 
				
			||||||
          const priceTimestamp = parseInt(t, 10);
 | 
					 | 
				
			||||||
          if (blockTimestamp > priceTimestamp) {
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
              price: historicalPrice.prices[priceTimestamp],
 | 
					 | 
				
			||||||
              exchangeRates: historicalPrice.exchangeRates,
 | 
					 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					
 | 
				
			||||||
    
 | 
					          const priceTimestamps = Object.keys(historicalPrice.prices);
 | 
				
			||||||
        return this.getEmptyPrice();    
 | 
					          priceTimestamps.push(Number.MAX_SAFE_INTEGER.toString());
 | 
				
			||||||
      })
 | 
					          priceTimestamps.sort().reverse();
 | 
				
			||||||
    );
 | 
					
 | 
				
			||||||
 | 
					          // Small trick here. Because latest blocks have higher timestamps than our
 | 
				
			||||||
 | 
					          // latest price timestamp (we only insert once every hour), we have no price for them.
 | 
				
			||||||
 | 
					          // Therefore we want to fallback to the websocket price by returning an undefined `price` field.
 | 
				
			||||||
 | 
					          // Since historicalPrice.prices[Number.MAX_SAFE_INTEGER] does not exists
 | 
				
			||||||
 | 
					          // it will return `undefined` and automatically use the websocket price.
 | 
				
			||||||
 | 
					          // This way we can differenciate blocks without prices like the genesis block
 | 
				
			||||||
 | 
					          // vs ones without a price (yet) like the latest blocks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          for (const t of priceTimestamps) {
 | 
				
			||||||
 | 
					            const priceTimestamp = parseInt(t, 10);
 | 
				
			||||||
 | 
					            if (blockTimestamp > priceTimestamp) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                price: historicalPrice.prices[priceTimestamp],
 | 
				
			||||||
 | 
					                exchangeRates: historicalPrice.exchangeRates,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return this.getEmptyPrice();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user