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('Cache-control', 'public');
 | 
			
		||||
      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) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -104,9 +104,48 @@ class PricesRepository {
 | 
			
		||||
    return rates[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getHistoricalPrice(): Promise<Conversion | null> {
 | 
			
		||||
  public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
 | 
			
		||||
    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) {
 | 
			
		||||
        throw Error(`Cannot get average historical price from the database`);
 | 
			
		||||
      }
 | 
			
		||||
@ -127,7 +166,7 @@ class PricesRepository {
 | 
			
		||||
        exchangeRates: exchangeRates
 | 
			
		||||
      };
 | 
			
		||||
    } 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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -327,7 +327,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
            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) => {
 | 
			
		||||
              this.blockConversion = price;
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes): void {
 | 
			
		||||
    if (changes.line?.currentValue) {
 | 
			
		||||
      this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp).pipe(
 | 
			
		||||
      this.priceService.getBlockPrice$(changes.line?.currentValue.timestamp, true).pipe(
 | 
			
		||||
        tap((price) => {
 | 
			
		||||
          this.blockConversion = price;
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
@ -305,7 +305,10 @@ export class ApiService {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHistoricalPrice$(): Observable<Conversion> {
 | 
			
		||||
    return this.httpClient.get<Conversion>( this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price');
 | 
			
		||||
  getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> {
 | 
			
		||||
    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 {
 | 
			
		||||
  priceObservable$: Observable<Conversion>;
 | 
			
		||||
  singlePriceObservable$: Observable<Conversion>;
 | 
			
		||||
 | 
			
		||||
  lastQueriedTimestamp: number;
 | 
			
		||||
  lastPriceHistoryUpdate: number;
 | 
			
		||||
 | 
			
		||||
  historicalPrice: ConversionDict = {
 | 
			
		||||
    prices: null,
 | 
			
		||||
@ -63,53 +67,83 @@ export class PriceService {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockPrice$(blockTimestamp: number): Observable<Price | undefined> {
 | 
			
		||||
    if (!this.priceObservable$) {
 | 
			
		||||
      this.priceObservable$ = this.apiService.getHistoricalPrice$().pipe(shareReplay());
 | 
			
		||||
  getBlockPrice$(blockTimestamp: number, singlePrice = false): Observable<Price | undefined> {
 | 
			
		||||
    const now = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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) => {
 | 
			
		||||
        if (!blockTimestamp) {
 | 
			
		||||
          return undefined;
 | 
			
		||||
        }
 | 
			
		||||
    /**
 | 
			
		||||
     * Query all price history only once. The observable is invalidated after 1 hour
 | 
			
		||||
     */
 | 
			
		||||
    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 = {
 | 
			
		||||
          prices: {},
 | 
			
		||||
          exchangeRates: conversion.exchangeRates,
 | 
			
		||||
        };
 | 
			
		||||
        for (const price of conversion.prices) {
 | 
			
		||||
          historicalPrice.prices[price.time] = {
 | 
			
		||||
            USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
 | 
			
		||||
            CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
 | 
			
		||||
      return this.priceObservable$.pipe(
 | 
			
		||||
        map((conversion) => {
 | 
			
		||||
          if (!blockTimestamp) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const historicalPrice = {
 | 
			
		||||
            prices: {},
 | 
			
		||||
            exchangeRates: conversion.exchangeRates,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const priceTimestamps = Object.keys(historicalPrice.prices);
 | 
			
		||||
        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,
 | 
			
		||||
          for (const price of conversion.prices) {
 | 
			
		||||
            historicalPrice.prices[price.time] = {
 | 
			
		||||
              USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD,
 | 
			
		||||
              CHF: price.CHF, AUD: price.AUD, JPY: price.JPY
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        return this.getEmptyPrice();    
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
          const priceTimestamps = Object.keys(historicalPrice.prices);
 | 
			
		||||
          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