Skip to content

Commit

Permalink
Created new price endpoint to return price and timestamp (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
bobo-k2 authored Mar 25, 2024
1 parent 54e21fe commit e4ae3e7
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
} from './services/GiantSquid';
import { BluezNftService, INftService } from './services/NftService';
import { NftController } from './controllers/NftController';
import { TokenStatsControllerV2 } from './controllers/TokenStatsControllerV2';

const container = new Container();

Expand Down Expand Up @@ -137,6 +138,7 @@ container.bind<ICallParser>(CallNameMapping.batch).to(BatchCallParser).inSinglet

// controllers registration
container.bind<IControllerBase>(ContainerTypes.Controller).to(TokenStatsController);
container.bind<IControllerBase>(ContainerTypes.Controller).to(TokenStatsControllerV2);
container.bind<IControllerBase>(ContainerTypes.Controller).to(DappsStakingController);
container.bind<IControllerBase>(ContainerTypes.Controller).to(DappsStakingV3Controller);
container.bind<IControllerBase>(ContainerTypes.Controller).to(NodeController);
Expand Down
43 changes: 43 additions & 0 deletions src/controllers/TokenStatsControllerV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import express, { Request, Response } from 'express';
import { injectable, inject } from 'inversify';
import { ContainerTypes } from '../containertypes';
import { IPriceProvider } from '../services/IPriceProvider';
import { IStatsIndexerService } from '../services/StatsIndexerService';
import { IStatsService } from '../services/StatsService';
import { ControllerBase } from './ControllerBase';
import { IControllerBase } from './IControllerBase';

@injectable()
export class TokenStatsControllerV2 extends ControllerBase implements IControllerBase {
constructor(
@inject(ContainerTypes.StatsService) private _statsService: IStatsService,
@inject(ContainerTypes.StatsIndexerService) private _indexerService: IStatsIndexerService,
@inject(ContainerTypes.PriceProviderWithFailover) private _priceProvider: IPriceProvider,
) {
super();
}

public register(app: express.Application): void {
/**
* @description Token current price route v2.
*/
app.route('/api/v2/token/price/:symbol').get(async (req: Request, res: Response) => {
/*
#swagger.description = 'Retrieves current token price with timestamp'
#swagger.tags = ['Token']
#swagger.parameters['symbol'] = {
in: 'path',
description: 'Token symbol (eg. ASTR or SDN)',
required: true,
enum: ['ASTR', 'SDN']
}
*/
try {
const currency = req.query.currency as string | undefined;
res.json(await this._priceProvider.getPriceWithTimestamp(req.params.symbol, currency));
} catch (err) {
this.handleError(res, err as Error);
}
});
}
}
11 changes: 3 additions & 8 deletions src/services/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class CacheService<T> {

constructor(private cachedItemValidityTimeMs: number = 60000) {}

public getItem(key: string): T | undefined {
public getItem(key: string): CacheItem<T> | undefined{
Guard.ThrowIfUndefined('key', key);

const cacheItem = this.cache.get(key);
Expand All @@ -19,12 +19,7 @@ export class CacheService<T> {
return undefined;
}

if (this.isExpired(cacheItem)) {
this.cache.delete(key);
return undefined;
}

return cacheItem.data;
return cacheItem;
}

public setItem(key: string, item: T): void {
Expand All @@ -37,7 +32,7 @@ export class CacheService<T> {
});
}

private isExpired(cacheItem: CacheItem<T>): boolean {
public isExpired(cacheItem: CacheItem<T>): boolean {
return cacheItem.updatedAt + this.cachedItemValidityTimeMs < Date.now();
}
}
11 changes: 10 additions & 1 deletion src/services/CoinGeckoPriceProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios, { AxiosRequestConfig } from 'axios';
import { inject, injectable } from 'inversify';
import { IPriceProvider } from './IPriceProvider';
import { IPriceProvider, TokenInfo } from './IPriceProvider';
import { ContainerTypes } from '../containertypes';
import { IFirebaseService } from './FirebaseService';

Expand Down Expand Up @@ -30,6 +30,15 @@ export class CoinGeckoPriceProvider implements IPriceProvider {
return 0;
}

public async getPriceWithTimestamp(symbol: string, currency: string | undefined): Promise<TokenInfo>{
const price = await this.getPrice(symbol, currency);

return {
price,
lastUpdated: Date.now(),
};
}

private async getTokenId(symbol: string): Promise<string | undefined> {
if (!CoinGeckoPriceProvider.tokens) {
// Cache received data since token list is a quite big.
Expand Down
9 changes: 9 additions & 0 deletions src/services/DiaDataPriceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,13 @@ export class DiaDataPriceProvider implements IPriceProvider {

return 0;
}

public async getPriceWithTimestamp(symbol: string): Promise<{ price: number; lastUpdated: number }> {
const price = await this.getPrice(symbol);

return {
price,
lastUpdated: Date.now(),
};
}
}
12 changes: 12 additions & 0 deletions src/services/IPriceProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export type TokenInfo = {
price: number;
lastUpdated: number;
}


/**
* Definition of provider for access token price.
*/
Expand All @@ -7,4 +13,10 @@ export interface IPriceProvider {
* @param tokenInfo Token information.
*/
getPrice(symbol: string, currency: string | undefined): Promise<number>;

/**
* Gets current token price in USD with timestamp.
* @param tokenInfo Token price and timestamp.
*/
getPriceWithTimestamp(symbol: string, currency: string | undefined): Promise<TokenInfo>;
}
37 changes: 26 additions & 11 deletions src/services/PriceProviderWithFailover.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { injectable } from 'inversify';
import container from '../container';
import { ContainerTypes } from '../containertypes';
import { IPriceProvider } from './IPriceProvider';
import { IPriceProvider, TokenInfo } from './IPriceProvider';
import { CacheService } from './CacheService';
import { Guard } from '../guard';

Expand All @@ -20,26 +20,41 @@ export class PriceProviderWithFailover implements IPriceProvider {
*/
public async getPrice(symbol: string, currency = 'usd'): Promise<number> {
Guard.ThrowIfUndefined('symbol', symbol);
const priceInfo = await this.getPriceWithTimestamp(symbol, currency);

return priceInfo.price;
}

public async getPriceWithTimestamp(symbol: string, currency = 'usd'): Promise<TokenInfo> {
Guard.ThrowIfUndefined('symbol', symbol);

const providers = container.getAll<IPriceProvider>(ContainerTypes.PriceProvider);
const cacheKey = `${symbol}-${currency}`;
const cacheItem = this.priceCache.getItem(cacheKey);

if (cacheItem && !this.priceCache.isExpired(cacheItem)) {
// Price is cached and still valid.
return { price: cacheItem.data, lastUpdated: cacheItem.updatedAt };
}

// Fetch a new price.
for (const provider of providers) {
try {
const cacheItem = this.priceCache.getItem(cacheKey);
if (cacheItem) {
return cacheItem;
} else {
const price = await provider.getPrice(symbol, currency);
this.priceCache.setItem(cacheKey, price);

return price;
}
const price = await provider.getPrice(symbol, currency);
this.priceCache.setItem(cacheKey, price);

return { price, lastUpdated: Date.now() };
} catch (error) {
// Execution moves to next price provider, so nothing special to do here.
console.log(error);
}
}

return 0;
// Last resort, all providers failed. Return from cache.
if (cacheItem) {
return { price: cacheItem.data, lastUpdated: cacheItem.updatedAt };
} else {
throw new Error('Unable to fetch price from any provider.');
}
}
}

0 comments on commit e4ae3e7

Please sign in to comment.