From 83c7b4dc6b998a363417a254139043f7d989f426 Mon Sep 17 00:00:00 2001 From: ikprk Date: Tue, 11 Jun 2024 22:14:53 +0200 Subject: [PATCH] New query --- .../resolvers/CreatorToken/index.ts | 146 +++++++++++++++++- .../resolvers/CreatorToken/types.ts | 30 +++- 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/server-extension/resolvers/CreatorToken/index.ts b/src/server-extension/resolvers/CreatorToken/index.ts index b2ee3b72e..203113dd9 100644 --- a/src/server-extension/resolvers/CreatorToken/index.ts +++ b/src/server-extension/resolvers/CreatorToken/index.ts @@ -1,7 +1,10 @@ -import { Args, Query, Resolver } from 'type-graphql' +import { Args, Ctx, Info, Query, Resolver } from 'type-graphql' import { EntityManager } from 'typeorm' import { CreatorToken, RevenueShare, TokenAccount } from '../../../model' +import { model } from '../model' +import { GraphQLResolveInfo } from 'graphql' import { computeTransferrableAmount } from './services' +import { ListQuery } from '@subsquid/openreader/lib/sql/query' import { GetAccountTransferrableBalanceArgs, GetAccountTransferrableBalanceResult, @@ -9,7 +12,17 @@ import { GetCumulativeHistoricalShareAllocationResult, GetShareDividendsResult, GetShareDividensArgs, + MarketplaceTokensArgs, + CreatorToken as TokenReturnType, + MarketplaceTokensReturnType, } from './types' +import { getResolveTree } from '@subsquid/openreader/lib/util/resolve-tree' +import { parseAnyTree, parseSqlArguments } from '@subsquid/openreader/lib/opencrud/tree' +import { extendClause } from '../../../utils/sql' +import { Context } from '../../check' +import { getCurrentBlockHeight } from '../../../utils/blockHeight' + +export const BLOCKS_PER_DAY = 10 * 60 * 24 // 10 blocs per minute, 60 mins * 24 hours @Resolver() export class TokenResolver { @@ -41,6 +54,137 @@ export class TokenResolver { } } + @Query(() => [MarketplaceTokensReturnType]) + async getMarketplaceTokens( + @Args() args: MarketplaceTokensArgs, + @Info() info: GraphQLResolveInfo, + @Ctx() ctx: Context + ) { + const em = await this.em() + const { lastProcessedBlock } = await getCurrentBlockHeight(em) + + if (lastProcessedBlock < 0) { + throw new Error('Failed to retrieve processor block height') + } + + const tree = getResolveTree(info) + const sqlArgs = parseSqlArguments(model, 'CreatorToken', { + where: args.where, + }) + + const tokenSubTree = tree.fieldsByTypeName.MarketplaceTokensReturnType.creatorToken + const tokenFields = parseAnyTree(model, 'CreatorToken', info.schema, tokenSubTree) + + const topTokensCtes = ` +WITH recent_transactions AS ( + SELECT + tr.amm_id, + ac.token_id, + ROUND(tr.price_paid / tr.quantity) AS price_paid, + tr.created_in + FROM amm_transaction tr + JOIN amm_curve ac ON tr.amm_id = ac.id + WHERE tr.created_in >= ${lastProcessedBlock - args.periodDays * BLOCKS_PER_DAY} +), +oldest_transactions AS ( + SELECT + tr.token_id, + tr.price_paid AS oldest_price_paid + FROM recent_transactions tr + JOIN ( + SELECT token_id, MIN(created_in) AS oldest_created_in + FROM recent_transactions + GROUP BY token_id + ) oldest ON tr.token_id = oldest.token_id AND tr.created_in = oldest.oldest_created_in +), +newest_transactions AS ( + SELECT + tr.token_id, + tr.price_paid AS newest_price_paid + FROM recent_transactions tr + JOIN ( + SELECT token_id, MAX(created_in) AS newest_created_in + FROM recent_transactions + GROUP BY token_id + ) newest ON tr.token_id = newest.token_id AND tr.created_in = newest.newest_created_in +), +price_changes AS ( + SELECT + ot.token_id, + ot.oldest_price_paid, + nt.newest_price_paid, + CASE + WHEN ot.oldest_price_paid = 0 THEN 0 + ELSE ((nt.newest_price_paid - ot.oldest_price_paid) * 100.0 / ot.oldest_price_paid) + END AS percentage_change + FROM oldest_transactions ot + JOIN newest_transactions nt ON ot.token_id = nt.token_id +), +ranked_tokens AS ( + SELECT token_id, percentage_change, newest_price_paid, oldest_price_paid, + ROW_NUMBER() OVER (ORDER BY percentage_change DESC) AS growth_rank, + ROW_NUMBER() OVER (ORDER BY percentage_change ASC) AS shrink_rank + FROM price_changes +), +top_tokens AS ( + SELECT * + FROM ranked_tokens + WHERE growth_rank <= 10 OR shrink_rank <= 10 +) +` + + const listQuery = new ListQuery( + model, + ctx.openreader.dialect, + 'CreatorToken', + tokenFields, + sqlArgs + ) + + let listQuerySql = listQuery.sql + + listQuerySql = extendClause( + listQuerySql, + 'SELECT', + 'tT.percentage_change as pricePercentageChange' + ) + + listQuerySql = extendClause( + listQuerySql, + 'FROM', + 'LEFT JOIN top_tokens tT ON creator_token.id = tT.token_id', + '' + ) + + listQuerySql = extendClause( + listQuerySql, + 'WHERE', + 'tT.growth_rank <= 10 OR tT.shrink_rank <= 10', + 'AND' + ) + + listQuerySql = `${topTokensCtes} ${listQuerySql}` + ;(listQuery as { sql: string }).sql = listQuerySql + + const oldListQMap = listQuery.map.bind(listQuery) + listQuery.map = (rows: unknown[][]) => { + const pricePercentageChanges: unknown[] = [] + + for (const row of rows) { + pricePercentageChanges.push(row.pop()) + } + const channelsMapped = oldListQMap(rows) + return channelsMapped.map((creatorToken, i) => ({ + creatorToken, + pricePercentageChange: pricePercentageChanges[i] ?? 0, + })) + } + + const result = await ctx.openreader.executeQuery(listQuery) + + return result as TokenReturnType[] + } + @Query(() => GetCumulativeHistoricalShareAllocationResult) async getCumulativeHistoricalShareAllocation( @Args() { tokenId }: GetCumulativeHistoricalShareAllocationArgs diff --git a/src/server-extension/resolvers/CreatorToken/types.ts b/src/server-extension/resolvers/CreatorToken/types.ts index 249689d30..4a87e1b0b 100644 --- a/src/server-extension/resolvers/CreatorToken/types.ts +++ b/src/server-extension/resolvers/CreatorToken/types.ts @@ -1,4 +1,5 @@ -import { ArgsType, Field, Int, ObjectType } from 'type-graphql' +import { ArgsType, Field, Float, Int, ObjectType } from 'type-graphql' +import { GraphQLScalarType } from 'graphql' @ArgsType() export class GetShareDividensArgs { @@ -44,3 +45,30 @@ export class GetAccountTransferrableBalanceResult { @Field(() => Int, { nullable: false }) transferrableCrtAmount!: number } + +@ObjectType() +export class CreatorToken { + @Field(() => String, { nullable: false }) id!: string +} + +@ObjectType() +export class MarketplaceTokensReturnType { + @Field(() => CreatorToken, { nullable: false }) creatorToken!: CreatorToken + @Field(() => Float, { nullable: false }) pricePercentageChange!: number +} + +export const TokenWhereInput = new GraphQLScalarType({ + name: 'CreatorTokenWhereInput', +}) + +@ArgsType() +export class MarketplaceTokensArgs { + @Field(() => TokenWhereInput, { nullable: true }) + where?: Record + + @Field(() => Int, { + nullable: false, + description: 'The number of days in period', + }) + periodDays: number +}