Skip to content

Commit

Permalink
Merge pull request #9 from near-daos/feature/coin_price_history
Browse files Browse the repository at this point in the history
Introducing coin price history
  • Loading branch information
okalenyk authored Jan 19, 2022
2 parents edb102b + 1ebf0e1 commit 8d577c8
Show file tree
Hide file tree
Showing 47 changed files with 612 additions and 126 deletions.
6 changes: 4 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ DATABASE_HOST=localhost
DATABASE_PORT=5437
DATABASE_RUN_MIGRATIONS=true
ORM_MIGRATIONS_DIR=src/migration
DATABASE_MIGRATIONS_LIST=ContractsMigration,ReceiptActionArgsMigration,ContractIdRelationMigration,TransactionProposalVoteMigration,ReceiptActionBlockTimestampMigration
DATABASE_MIGRATIONS_LIST=ReceiptActionArgsMigration,ContractIdRelationMigration,TransactionProposalVoteMigration,ReceiptActionBlockTimestampMigration,NearPriceHistoryMigration

REDIS_CACHE_URL=redis://@localhost:6379/0
REDIS_EVENT_BUS_URL=redis://@localhost:6379/1
Expand All @@ -18,5 +18,7 @@ AGGREGATOR_POLLING_SCHEDULE="0 * * * *"

LOG_LEVELS=log,warn,error

SMART_CONTRACTS=astro
CONTRACT_ENV=mainnet

SODAKI_API_BASE_URL=https://sodaki.com/api
COINGECKO_API_BASE_URL=https://api.coingecko.com/api/v3
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ data:
PORT: "3000"
NEST_APP_TYPE: aggregator
AGGREGATOR_POLLING_SCHEDULE: "{{ .Values.environment.aggregator_polling_schedule }}"
SMART_CONTRACTS: astro
LOG_LEVELS: "log,warn,error"
6 changes: 6 additions & 0 deletions apps/aggregator/src/aggregator.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
import { AggregatorService } from './aggregator.service';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ReceiptModule } from '@dao-stats/receipt';
import { CoinPriceHistoryModule } from '@dao-stats/common/coin-price-history.module';
import { ContractModule } from 'apps/api/src/contract/contract.module';
import { ExchangeModule } from 'libs/exchange/src/exchange.module';

@Module({
imports: [
Expand All @@ -40,6 +43,9 @@ import { ReceiptModule } from '@dao-stats/receipt';
DaoStatsModule,
DaoStatsHistoryModule,
HttpCacheModule,
CoinPriceHistoryModule,
ContractModule,
ExchangeModule,
],
providers: [
AggregatorService,
Expand Down
48 changes: 46 additions & 2 deletions apps/aggregator/src/aggregator.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
import { TransactionService } from '@dao-stats/transaction';
import { CacheService } from '@dao-stats/cache';
import { ReceiptActionService } from '@dao-stats/receipt';
import { ContractService } from 'apps/api/src/contract/contract.service';
import { CurrencyType } from '@dao-stats/common/types/currency-type';
import { CoinGeckoService, SodakiService } from '@dao-stats/exchange';
import { CoinPriceHistoryService } from '@dao-stats/common/coin-price-history.service';

@Injectable()
export class AggregatorService {
Expand All @@ -31,6 +35,10 @@ export class AggregatorService {
private readonly daoService: DaoService,
private readonly daoStatsService: DaoStatsService,
private readonly daoStatsHistoryService: DaoStatsHistoryService,
private readonly contractService: ContractService,
private readonly sodakiService: SodakiService,
private readonly coinGeckoService: CoinGeckoService,
private readonly coinPriceHistoryService: CoinPriceHistoryService,
) {
const { pollingSchedule } = this.configService.get('aggregator');

Expand All @@ -42,9 +50,11 @@ export class AggregatorService {
}

public async scheduleAggregation(from?: bigint, to?: bigint): Promise<void> {
const { smartContracts } = this.configService.get('aggregator');
const contracts = await this.contractService.find();

for (const contract of contracts) {
const { contractId, coin } = contract;

for (const contractId of smartContracts) {
this.logger.log(`Processing contract: ${contractId}...`);

const { AggregationModule, AggregationService } = await import(
Expand Down Expand Up @@ -130,6 +140,40 @@ export class AggregatorService {
await this.daoStatsHistoryService.createOrUpdate(metric);
}

this.logger.log(
`Retrieving current market price for contract: ${contractId}`,
);

for (const currency in CurrencyType) {
let price: number;

try {
price = await this.sodakiService.getCoinSpotPrice(
coin,
CurrencyType[currency],
);
} catch (e) {
this.logger.warn(e);

this.logger.log(
'Unable to get market price from Sodaki. Switching to CoinGecko for another try.',
);

price = await this.coinGeckoService.getCoinPrice(
coin,
CurrencyType[currency],
);
}

this.coinPriceHistoryService.createOrUpdate({
coin,
currency: CurrencyType[currency],
price,
});

this.logger.log(`Stored market price for ${coin}: ${price}${currency}`);
}

this.logger.log(`Finished processing contract: ${contractId}`);
}

Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { FlowModule } from './flow/flow.module';
import { TvlModule } from './tvl/tvl.module';
import { ApiDaoModule } from './dao/dao.module';
import { TokensModule } from './tokens/tokens.module';
import { MarketModule } from './market/market.module';

@Module({
imports: [
Expand Down Expand Up @@ -69,6 +70,7 @@ import { TokensModule } from './tokens/tokens.module';
FlowModule,
TvlModule,
TokensModule,
MarketModule,
],
providers: [
{
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class Api {
});
app.enableCors();
app.setGlobalPrefix('/api/v1/:contractId', {
exclude: ['/api/v1/contracts'],
exclude: ['/api/v1/contracts', '/api/v1/market/([^\\s]+)'],
});

if (process.env.NODE_ENV === 'development') {
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/contract/dto/contract.dto.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CoinType } from '@dao-stats/common/types/coin-type';
import { ApiProperty } from '@nestjs/swagger';

export class ContractResponse {
Expand All @@ -12,4 +13,7 @@ export class ContractResponse {

@ApiProperty()
conversionFactor: number;

@ApiProperty()
coin: CoinType;
}
17 changes: 17 additions & 0 deletions apps/api/src/market/dto/coin-price-history.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';

import { CoinType, CurrencyType } from '@dao-stats/common/types';

export class CoinPriceHistoryResponse {
@ApiProperty()
date: number;

@ApiProperty()
coin: CoinType;

@ApiProperty()
currency: CurrencyType;

@ApiProperty()
price: number;
}
1 change: 1 addition & 0 deletions apps/api/src/market/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './coin-price-history.dto';
42 changes: 42 additions & 0 deletions apps/api/src/market/market.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';

import { CoinType } from '@dao-stats/common/types';
import { CoinPriceQuery } from '@dao-stats/common/dto';

import { MarketService } from './market.service';
import { NoContractContext } from '../decorators';
import { MetricQueryPipe } from '../pipes';
import { CoinPriceHistoryResponse } from './dto';

@ApiTags('Market')
@Controller('/api/v1/market')
export class MarketController {
constructor(private readonly marketService: MarketService) {}

@ApiResponse({
status: 200,
type: [CoinPriceHistoryResponse],
})
@ApiBadRequestResponse({
description: 'Bad Request Response based on the query params set',
})
@ApiParam({
name: 'coin',
description: `Coin Type: e.g ${CoinType.Near}`,
enum: Object.values(CoinType),
})
@NoContractContext()
@Get('/:coin/price')
async coinPrice(
@Param('coin') coin: CoinType = CoinType.Near,
@Query(MetricQueryPipe) query: CoinPriceQuery,
): Promise<CoinPriceHistoryResponse[]> {
return this.marketService.getCoinPriceHistory(coin, query);
}
}
24 changes: 24 additions & 0 deletions apps/api/src/market/market.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheModule, Module } from '@nestjs/common';

import { CacheConfigService } from '@dao-stats/config/cache';
import { Contract, CoinPriceHistoryModule } from '@dao-stats/common';
import { TransactionModule } from '@dao-stats/transaction';

import { MarketService } from './market.service';
import { MarketController } from './market.controller';

@Module({
imports: [
CacheModule.registerAsync({
useClass: CacheConfigService,
}),
TypeOrmModule.forFeature([Contract]),
CoinPriceHistoryModule,
TransactionModule,
],
providers: [MarketService],
controllers: [MarketController],
exports: [MarketService],
})
export class MarketModule {}
34 changes: 34 additions & 0 deletions apps/api/src/market/market.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import moment from 'moment';

import { CoinPriceHistoryService } from '@dao-stats/common';
import { CoinPriceQuery } from '@dao-stats/common/dto';
import { CoinType } from '@dao-stats/common/types';

import { CoinPriceHistoryResponse } from './dto';

@Injectable()
export class MarketService {
constructor(
private readonly coinPriceHistoryService: CoinPriceHistoryService,
) {}

async getCoinPriceHistory(
coin: CoinType,
query: CoinPriceQuery,
): Promise<CoinPriceHistoryResponse[]> {
const { currency, from, to } = query;

const history = await this.coinPriceHistoryService.findByDateRange(
coin,
currency,
from,
to,
);

return history.map((price) => ({
...price,
date: moment(price.date).valueOf(),
}));
}
}
1 change: 1 addition & 0 deletions apps/api/src/pipes/metric-query.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class MetricQueryPipe implements PipeTransform {
}

return {
...query,
from: from.valueOf(),
to: to.valueOf(),
};
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/tvl/tvl.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { CacheModule, Module } from '@nestjs/common';

import { CacheConfigService } from '@dao-stats/config/cache';
import { DaoStatsHistoryModule, DaoStatsModule } from '@dao-stats/common';
import {
DaoStatsHistoryModule,
DaoStatsModule,
CoinPriceHistoryModule,
} from '@dao-stats/common';
import { TransactionModule } from '@dao-stats/transaction';

import { TvlController } from './tvl.controller';
Expand All @@ -18,6 +22,7 @@ import { MetricService } from '../common/metric.service';
DaoStatsHistoryModule,
TransactionModule,
ContractModule,
CoinPriceHistoryModule,
],
providers: [TvlService, MetricService],
controllers: [TvlController],
Expand Down
4 changes: 2 additions & 2 deletions libs/astro/src/aggregation.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { CacheModule, Module } from '@nestjs/common';
import { NearModule } from '@dao-stats/near';
import { NearHelperModule } from '@dao-stats/near-helper';
import { NearIndexerModule } from '@dao-stats/near-indexer';
import { SodakiModule } from '@dao-stats/sodaki';

import {
AggregationService,
Expand All @@ -14,6 +13,7 @@ import {
} from '.';
import { AstroModule } from './astro.module';
import configuration from './config/configuration';
import { ExchangeModule } from 'libs/exchange/src/exchange.module';

@Module({
imports: [
Expand All @@ -25,7 +25,7 @@ import configuration from './config/configuration';
NearModule,
NearIndexerModule,
NearHelperModule,
SodakiModule,
ExchangeModule,
],
providers: [
AggregationService,
Expand Down
3 changes: 0 additions & 3 deletions libs/astro/src/config/astro-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,3 @@ dao:
contractName: 'sputnik-dao.near'
tokenFactoryContractName: 'tkn.near'
bridgeTokenFactoryContractName: 'factory.bridge.near'

sodaki:
baseUri: 'https://www.sodaki.com/api'
10 changes: 8 additions & 2 deletions libs/astro/src/metrics/tokens/fts-value-locked.metric.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';

import { DaoStatsMetric, convertFunds } from '@dao-stats/common';
import { CurrencyType, CoinType } from '@dao-stats/common/types';
import { NearHelperService } from '@dao-stats/near-helper';
import { SodakiService } from '@dao-stats/sodaki';
import { SodakiService } from '@dao-stats/exchange';

import { AstroService } from '../../astro.service';
import {
DaoContractMetricCurrentParams,
Expand Down Expand Up @@ -30,7 +33,10 @@ export class FtsValueLockedMetric implements DaoContractMetricInterface {
const tokens = await this.nearHelperService.getLikelyTokens(
contract.contractId,
);
const nearPrice = await this.sodakiService.getNearPrice();
const nearPrice = await this.sodakiService.getCoinSpotPrice(
CoinType.Near,
CurrencyType.USD,
);
const tokenBalances = await Promise.all(
tokens.map(async (token) => {
const fTokenContract = await this.astroService.getFTokenContract(token);
Expand Down
12 changes: 12 additions & 0 deletions libs/common/src/coin-price-history.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';

import { CoinPriceHistory } from './entities/coin-price-history.entity';
import { CoinPriceHistoryService } from './coin-price-history.service';

@Module({
imports: [TypeOrmModule.forFeature([CoinPriceHistory])],
providers: [CoinPriceHistoryService],
exports: [CoinPriceHistoryService],
})
export class CoinPriceHistoryModule {}
Loading

0 comments on commit 8d577c8

Please sign in to comment.