Skip to content

Commit

Permalink
feat/nft-analytics (#965)
Browse files Browse the repository at this point in the history
* Add tymescaledb config

* Add analytics cron job

* Update analytics job

* Add timescale migrations

* Save data into postgress

* Update migrations

* Add analytics resolver

* Update migrations

* Add hour to data api get price

* Upgrade dependencies

* add analytics indexer

* Revert admin resolvers

* Merge trending analytics with analytics

* Remove commented code

* increase batch logs

* Update analytics Indexer

* Clean up logs

* remove duplicate case

* Fix mapping for unknown prices

* Undo commented configs

* Analytics

* Add logging

* Add handle for buy now

* Fix buy now events

* Fix single event indexing

* Add bid event to index

* fix compile error

* Fix indexer duplicates

* Add date logs

* Change timestamp saving

* more logs

* Fix timestamp saving

* Analytics ectract methods

* Add queries analytics

* Clean up tools service

* revert explore stats changes

* Update stats resolver

* Add caching layer

* Delete unused service

* Update naming

* Remove empty lines

* Update formating

* Update general analyticsQuery

* Remove nullable for resolve fields params

* Update graphql schema

* Add collections analytics resolver

* Update collections resolver

* Add config for public api

* Add volume data for collection query

* Update volume data fields

* Clean up queries

* Change views names

* Update typeorm configs

* Update typeorm to use config service

* remove unused fields

* Update Aggregate value

* Save floor price also for analytics

* Add null check

* Remove purchase event

* Log error and return empty

* Add logging

* Add max value check for floor price

* Rename analytics parser

* Code review follow up

* Add floor price data volume

* When search by identifier return 0 for sum

* Change name from sum to value

* Update floor price analytics

* Fix floor price view

* Add locf and rename function

* Add change listing and price indexing

* Clean up unused functions for analytics

* Update AggregateValue to map object from timescale

* remove unused dependency

* Add analytics data getter tests

* Add data setter tests

* Rename tests

* Update testing module scope

* Trigger build
  • Loading branch information
danielailie authored Jun 27, 2023
1 parent e394ed4 commit 0b0f0f1
Show file tree
Hide file tree
Showing 94 changed files with 4,423 additions and 1,266 deletions.
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ services:
ports:
- 27017:27017

timescaledb:
image: timescale/timescaledb:latest-pg12
restart: always
shm_size: 1gb
ports:
- 5431:5432
environment:
POSTGRES_USER: timescaledb
POSTGRES_PASSWORD: password
volumes:
- timescaledb:/var/lib/postgresql/data

db:
image: mysql:latest
container_name: nft-db
Expand All @@ -69,6 +81,9 @@ services:
# Where our data will be persisted
volumes:
- my-db:/var/lib/mysql

# Names our volume
volumes:
my-db:
timescaledb:
name: timescaledb
2,121 changes: 1,122 additions & 999 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
"typeorm": "ts-node --require ts-node/register ./node_modules/typeorm/cli.js -d src/datasource.ts",
"generate-migration": "npm run typeorm -- migration:generate src/db/migrations/$npm_config_name",
"run-migrations": "npm run typeorm migration:run",
"revert-migrations": "npm run typeorm migration:revert"
"revert-migrations": "npm run typeorm migration:revert",
"typeormTimescale": "ts-node --require ts-node/register ./node_modules/typeorm/cli.js -d src/common/persistence/timescaledb/typeorm.config.ts",
"generate-migration-timescale": "npm run typeormTimescale -- migration:generate src/common/persistence/timescaledb/migrations/$npm_config_name",
"run-migrations-timescale": "npm run typeormTimescale migration:run --transaction=each",
"revert-migrations-timescale": "npm run typeormTimescale migration:revert"
},
"dependencies": {
"@elastic/elasticsearch": "7.12.0",
Expand All @@ -52,7 +56,7 @@
"amqp-connection-manager": "3.7.0",
"amqplib": "0.8.0",
"apollo-server": "3.11.1",
"aws-sdk": "2.974.0",
"aws-sdk": "2.1366.0",
"axios": "0.21.4",
"axios-retry": "3.1.8",
"bignumber.js": "9.0.1",
Expand All @@ -71,13 +75,15 @@
"graphql-upload": "13.0.0",
"helmet": "4.4.1",
"jwks-rsa": "2.0.3",
"moment": "^2.29.4",
"mongoose": "6.9.0",
"mysql2": "2.2.5",
"nest-winston": "1.8.0",
"object-hash": "2.2.0",
"passport": "0.6.0",
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",
"pg": "8.10.0",
"prom-client": "13.1.0",
"redis": "3.1.2",
"reflect-metadata": "0.1.13",
Expand All @@ -86,11 +92,11 @@
"save": "2.5.0",
"swagger-ui-express": "4.3.0",
"tiny-async-pool": "1.2.0",
"typeorm": "0.3.11",
"typeorm": "0.3.16",
"winston": "3.7.2"
},
"devDependencies": {
"@nestjs/cli": "9.2.0",
"@nestjs/cli": "9.4.2",
"@nestjs/schematics": "9.0.4",
"@nestjs/testing": "9.3.12",
"@types/express": "^4.17.11",
Expand Down
90 changes: 90 additions & 0 deletions schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ input AddLikeArgs {
identifier: String!
}

type AnalyticsAggregateValue {
avg: Float
count: Float
max: Float
min: Float
series: String
time: String
value: Float
}

input AnalyticsArgs {
metric: String!
series: String
time: String
}

input AnalyticsInput {
range: TimeRange
resolution: TimeResolutionsEnum
}

input ArtistFilters {
sorting: ArtistSortingEnum!
}
Expand Down Expand Up @@ -459,6 +480,10 @@ type Collection {
website: String
}

input CollectionAnalyticsArgs {
series: String
}

type CollectionAsset {
assets(input: CollectionAssetsRetriveCount): [CollectionAssetModel]
totalCount: String
Expand Down Expand Up @@ -546,6 +571,41 @@ input CollectionStatsFilter {
paymentToken: String
}

type CollectionsAnalyticsModel {
collectionIdentifier: String!
details: CollectionsDetailsModel!
floorPrice: Float!
floorPriceData(input: AnalyticsArgs): [AnalyticsAggregateValue!]!
holders: Int!
volume24h: Float
volumeData(input: AnalyticsArgs): [AnalyticsAggregateValue!]!
}

type CollectionsAnalyticsModelEdge {
cursor: String
node: CollectionsAnalyticsModel
}

type CollectionsAnalyticsModelPageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}

type CollectionsAnalyticsResponse {
edges: [CollectionsAnalyticsModelEdge!]
pageData: PageData
pageInfo: CollectionsAnalyticsModelPageInfo
}

type CollectionsDetailsModel {
collectionIdentifier: String!
collectionName: String!
items: Int!
owner: String!
}

input CollectionsFilter {
"""Flag for active last 30 days"""
activeLast30Days: Boolean
Expand Down Expand Up @@ -688,6 +748,15 @@ input FlagNftInput {
nsfwFlag: Float!
}

type GeneralAnalyticsModel {
collections: Int!
holders: Int!
listing(input: AnalyticsInput!): [AnalyticsAggregateValue!]!
marketplaces: Int!
nfts(input: AnalyticsInput!): [AnalyticsAggregateValue!]!
volume(input: AnalyticsInput!): [AnalyticsAggregateValue!]!
}

enum GroupBy {
IDENTIFIER
}
Expand Down Expand Up @@ -1165,12 +1234,14 @@ type Query {
campaigns(filters: CampaignsFilter, pagination: ConnectionArgs): CampaignsResponse!
collectionStats(filters: CollectionStatsFilter!): CollectionStats!
collections(filters: CollectionsFilter, pagination: ConnectionArgs, sorting: CollectionsSortingEnum): CollectionResponse!
collectionsAnalytics(input: CollectionAnalyticsArgs, pagination: ConnectionArgs): CollectionsAnalyticsResponse!
currentPaymentTokens(filters: CurrentPaymentTokensFilters): [Token!]!
exploreCollectionsStats: ExploreCollectionsStats!
exploreNftsStats: ExploreNftsStats!
exploreStats: ExploreStats!
featuredCollections(filters: FeaturedCollectionsFilter, pagination: ConnectionArgs): CollectionResponse!
featuredNfts(pagination: ConnectionArgs): AssetsResponse!
generalAnalytics(input: AnalyticsInput): GeneralAnalyticsModel!
hasClaimedTickets(collectionIdentifier: String!): Boolean!
isWhitelisted: WhitelistedInfo!
marketplaces(filters: MarketplaceFilters, pagination: ConnectionArgs): MarketplacesResponse!
Expand Down Expand Up @@ -1333,6 +1404,25 @@ enum TierStatusEnum {
Sold
}

enum TimeRange {
ALL
DAY
HOUR
MONTH
WEEK
YEAR
}

enum TimeResolutionsEnum {
INTERVAL_1_MINUTE
INTERVAL_10_MINUTES
INTERVAL_30_MINUTES
INTERVAL_DAY
INTERVAL_HOUR
INTERVAL_MONTH
INTERVAL_WEEK
}

type Token {
activeAuctions: Int
decimals: Float!
Expand Down
17 changes: 17 additions & 0 deletions src/analytics.indexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NestFactory } from '@nestjs/core';
import BigNumber from 'bignumber.js';
import { AnalyticsService } from './modules/analytics/analytics.service';
import { AnalyticsModule } from './modules/analytics/analytics.module';

export async function run(startDateUtc: string, endDateUtc: string) {
BigNumber.config({ EXPONENTIAL_AT: [-100, 100] });
const app = await NestFactory.create(AnalyticsModule);
const analyticsService = app.get<AnalyticsService>(AnalyticsService);
await analyticsService.indexAnalyticsLogs(
parseInt(startDateUtc),
parseInt(endDateUtc),
);
return process.exit(0);
}

run(process.argv[2], process.argv[3]);
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { BlacklistedCollectionsModule } from './modules/blacklist/blacklisted-co
import '@multiversx/sdk-nestjs/lib/src/utils/extensions/date.extensions';
import '@multiversx/sdk-nestjs/lib/src/utils/extensions/array.extensions';
import '@multiversx/sdk-nestjs/lib/src/utils/extensions/number.extensions';
import { TimescaleDbModule } from './common/persistence/timescaledb/timescaledb.module';

@Module({
imports: [
Expand Down Expand Up @@ -91,6 +92,7 @@ import '@multiversx/sdk-nestjs/lib/src/utils/extensions/number.extensions';
ArtistsModuleGraph,
ExploreStatsModuleGraph,
PrimarySaleModuleGraph,
TimescaleDbModule,
],
})
export class AppModule {}
15 changes: 15 additions & 0 deletions src/common/persistence/persistence.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,21 @@ export class PersistenceService {
);
}

async getCollectionFloorPrice(
identifier: string,
marketplaceKey: string = undefined,
paymentToken: string = mxConfig.egld,
): Promise<number> {
return await this.execute(
this.getCollectionStats.name,
this.collectionStatsRepository.getFloorPriceForCollection(
identifier,
marketplaceKey,
paymentToken,
),
);
}

async getCampaign(
campaignId: string,
minterAddress: string,
Expand Down
104 changes: 104 additions & 0 deletions src/common/persistence/timescaledb/analytics-data.getter.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { computeTimeInterval } from 'src/utils/analytics.utils';
import { AnalyticsArgs } from './entities/analytics.query';
import { FloorPriceDaily, SumDaily } from './entities/sum-daily.entity';
import { AnalyticsAggregateValue } from 'src/modules/analytics/models/analytics-aggregate-value';

@Injectable()
export class AnalyticsDataGetterService {
constructor(
@InjectRepository(SumDaily, 'timescaledb')
private readonly sumDaily: Repository<SumDaily>,
@InjectRepository(FloorPriceDaily, 'timescaledb')
private readonly floorPriceDaily: Repository<FloorPriceDaily>,
) {}

async getTopCollectionsDaily(
{ metric, series }: AnalyticsArgs,
limit: number = 10,
offset: number = 0,
): Promise<[AnalyticsAggregateValue[], number]> {
const query = this.sumDaily
.createQueryBuilder()
.select('sum(sum) as value')
.addSelect('series')
.addSelect('time')
.andWhere('key = :metric', { metric })
.andWhere(`time between now() - INTERVAL '1 day' and now()`)
.orderBy('sum', 'DESC')
.groupBy('series, sum, time');
if (series) {
query.andWhere('series = :series', { series });
}
const [response, count] = await Promise.all([
query.offset(offset).limit(limit).getRawMany(),
query.getCount(),
]);
if (series && count === 0) {
return [[new AnalyticsAggregateValue({ value: 0, series: series })], 1];
}

return [
response?.map((row) =>
AnalyticsAggregateValue.fromTimescaleObjext(row),
) ?? [],
count ?? 0,
];
}

async getVolumeData({
time,
series,
metric,
start,
}: AnalyticsArgs): Promise<AnalyticsAggregateValue[]> {
const [startDate, endDate] = computeTimeInterval(time, start);
const query = await this.sumDaily
.createQueryBuilder()
.select("time_bucket_gapfill('1 day', time) as timestamp")
.addSelect('sum(sum) as value')
.where('key = :metric', { metric })
.andWhere('series = :series', { series })
.andWhere(
endDate ? 'time BETWEEN :startDate AND :endDate' : 'time >= :startDate',
{ startDate, endDate },
)
.orderBy('timestamp', 'ASC')
.groupBy('timestamp')
.getRawMany();

return (
query?.map((row) => AnalyticsAggregateValue.fromTimescaleObjext(row)) ??
[]
);
}

async getFloorPriceData({
time,
series,
metric,
start,
}: AnalyticsArgs): Promise<AnalyticsAggregateValue[]> {
const [startDate, endDate] = computeTimeInterval(time, start);
const query = await this.floorPriceDaily
.createQueryBuilder()
.select("time_bucket_gapfill('1 day', time) as timestamp")
.addSelect('locf(min(min)) as value')
.where('key = :metric', { metric })
.andWhere('series = :series', { series })
.andWhere(
endDate ? 'time BETWEEN :startDate AND :endDate' : 'time >= :startDate',
{ startDate, endDate },
)
.orderBy('timestamp', 'ASC')
.groupBy('timestamp')
.getRawMany();

return (
query?.map((row) => AnalyticsAggregateValue.fromTimescaleObjext(row)) ??
[]
);
}
}
Loading

0 comments on commit 0b0f0f1

Please sign in to comment.