From 9cc2979a46958a721c13a8fe3a17be339ae23414 Mon Sep 17 00:00:00 2001 From: Eugene Gostkin Date: Mon, 18 Dec 2023 18:32:57 +0100 Subject: [PATCH] Add query by address --- .../ProjectedNftRangeController.ts | 59 +++++++++++++- .../projectedNftRangeByAddress.queries.ts | 79 +++++++++++++++++++ .../projectedNftRangeByAddress.sql | 40 ++++++++++ .../server/app/services/ProjectedNftRange.ts | 13 +++ webserver/shared/constants.ts | 6 +- webserver/shared/models/ProjectedNftRange.ts | 3 +- 6 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts create mode 100644 webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql diff --git a/webserver/server/app/controllers/ProjectedNftRangeController.ts b/webserver/server/app/controllers/ProjectedNftRangeController.ts index 344f43ae..e45c5084 100644 --- a/webserver/server/app/controllers/ProjectedNftRangeController.ts +++ b/webserver/server/app/controllers/ProjectedNftRangeController.ts @@ -5,8 +5,10 @@ import pool from '../services/PgPoolSingleton'; import type { ErrorShape } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; -import { projectedNftRange } from '../services/ProjectedNftRange'; +import { projectedNftRange, projectedNftRangeByAddress } from '../services/ProjectedNftRange'; import type {ProjectedNftRangeResponse} from '../../../shared/models/ProjectedNftRange'; +import {PROJECTED_NFT_LIMIT} from "../../../shared/constants"; +import {Errors, genErrorMessage} from "../../../shared/errors"; const route = Routes.projectedNftEventsRange; @@ -22,6 +24,28 @@ export class ProjectedNftRangeController extends Controller { StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, ErrorShape > + ): Promise { + const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; + if (slotRangeSize > PROJECTED_NFT_LIMIT.SLOT_RANGE) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.SlotRangeLimitExceeded, { + limit: PROJECTED_NFT_LIMIT.SLOT_RANGE, + found: slotRangeSize, + }) + ); + } + + if (requestBody.address !== null) { + return await this.handle_by_address_query(requestBody.address, requestBody); + } else { + return await this.handle_general_query(requestBody); + } + } + + async handle_general_query( + requestBody: EndpointTypes[typeof route]['input'], ): Promise { const response = await tx< ProjectedNftRangeResponse @@ -50,4 +74,37 @@ export class ProjectedNftRangeController extends Controller { return response; } + + async handle_by_address_query( + address: string, + requestBody: EndpointTypes[typeof route]['input'], + ): Promise { + const response = await tx< + ProjectedNftRangeResponse + >(pool, async dbTx => { + const data = await projectedNftRangeByAddress({ + address: address, + range: requestBody.range, + dbTx + }); + + return data.map(data => ({ + ownerAddress: data.owner_address, + previousTxHash: data.previous_tx_hash, + previousTxOutputIndex: data.previous_tx_output_index != null ? parseInt(data.previous_tx_output_index) : null, + actionTxId: data.action_tx_id, + actionOutputIndex: data.action_output_index, + asset: `${data.policy_id}.${data.asset_name}`, + policyId: data.policy_id, + assetName: data.asset_name, + amount: data.amount, + status: data.status, + plutusDatum: data.plutus_datum, + actionSlot: data.action_slot, + forHowLong: data.for_how_long, + })); + }); + + return response; + } } \ No newline at end of file diff --git a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts new file mode 100644 index 00000000..15ed1558 --- /dev/null +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts @@ -0,0 +1,79 @@ +/** Types generated for queries found in "app/models/projected_nft/projectedNftRangeByAddress.sql" */ +import { PreparedQuery } from '@pgtyped/query'; + +/** 'SqlProjectedNftRangeByAddress' parameters type */ +export interface ISqlProjectedNftRangeByAddressParams { + max_slot: number; + min_slot: number; + owner_address: string; +} + +/** 'SqlProjectedNftRangeByAddress' return type */ +export interface ISqlProjectedNftRangeByAddressResult { + action_output_index: number | null; + action_slot: number; + action_tx_id: string | null; + amount: string; + asset_name: string; + for_how_long: string | null; + owner_address: string | null; + plutus_datum: string | null; + policy_id: string; + previous_tx_hash: string | null; + previous_tx_output_index: string | null; + status: string | null; +} + +/** 'SqlProjectedNftRangeByAddress' query type */ +export interface ISqlProjectedNftRangeByAddressQuery { + params: ISqlProjectedNftRangeByAddressParams; + result: ISqlProjectedNftRangeByAddressResult; +} + +const sqlProjectedNftRangeByAddressIR: any = {"usedParamSet":{"owner_address":true,"min_slot":true,"max_slot":true},"params":[{"name":"owner_address","required":true,"transform":{"type":"scalar"},"locs":[{"a":1250,"b":1264}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1289,"b":1298}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1324,"b":1333}]}],"statement":"SELECT\n encode(\"ProjectedNFT\".owner_address, 'hex') as owner_address,\n\n encode(\"ProjectedNFT\".previous_utxo_tx_hash, 'hex') as previous_tx_hash,\n \"ProjectedNFT\".previous_utxo_tx_output_index as previous_tx_output_index,\n\n CASE\n WHEN \"TransactionOutput\".output_index = NULL THEN NULL\n ELSE \"TransactionOutput\".output_index\n END AS action_output_index,\n\n encode(\"Transaction\".hash, 'hex') as action_tx_id,\n\n \"ProjectedNFT\".policy_id as policy_id,\n \"ProjectedNFT\".asset_name as asset_name,\n \"ProjectedNFT\".amount as amount,\n\n CASE\n WHEN \"ProjectedNFT\".operation = 0 THEN 'Lock'\n WHEN \"ProjectedNFT\".operation = 1 THEN 'Unlocking'\n WHEN \"ProjectedNFT\".operation = 2 THEN 'Claim'\n ELSE 'Invalid'\n END AS status,\n\n encode(\"ProjectedNFT\".plutus_datum, 'hex') as plutus_datum,\n \"ProjectedNFT\".for_how_long as for_how_long,\n\n \"Block\".slot as action_slot\nFROM \"ProjectedNFT\"\n LEFT JOIN \"TransactionOutput\" ON \"TransactionOutput\".id = \"ProjectedNFT\".hololocker_utxo_id\n JOIN \"Transaction\" ON \"Transaction\".id = \"ProjectedNFT\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE\n encode(\"ProjectedNFT\".owner_address, 'hex') = :owner_address!\n AND \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; + +/** + * Query generated from SQL: + * ``` + * SELECT + * encode("ProjectedNFT".owner_address, 'hex') as owner_address, + * + * encode("ProjectedNFT".previous_utxo_tx_hash, 'hex') as previous_tx_hash, + * "ProjectedNFT".previous_utxo_tx_output_index as previous_tx_output_index, + * + * CASE + * WHEN "TransactionOutput".output_index = NULL THEN NULL + * ELSE "TransactionOutput".output_index + * END AS action_output_index, + * + * encode("Transaction".hash, 'hex') as action_tx_id, + * + * "ProjectedNFT".policy_id as policy_id, + * "ProjectedNFT".asset_name as asset_name, + * "ProjectedNFT".amount as amount, + * + * CASE + * WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + * WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + * WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + * ELSE 'Invalid' + * END AS status, + * + * encode("ProjectedNFT".plutus_datum, 'hex') as plutus_datum, + * "ProjectedNFT".for_how_long as for_how_long, + * + * "Block".slot as action_slot + * FROM "ProjectedNFT" + * LEFT JOIN "TransactionOutput" ON "TransactionOutput".id = "ProjectedNFT".hololocker_utxo_id + * JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * encode("ProjectedNFT".owner_address, 'hex') = :owner_address! + * AND "Block".slot > :min_slot! + * AND "Block".slot <= :max_slot! + * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * ``` + */ +export const sqlProjectedNftRangeByAddress = new PreparedQuery(sqlProjectedNftRangeByAddressIR); + + diff --git a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql new file mode 100644 index 00000000..8aae388c --- /dev/null +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql @@ -0,0 +1,40 @@ +/* +@name sqlProjectedNftRangeByAddress +*/ +SELECT + encode("ProjectedNFT".owner_address, 'hex') as owner_address, + + encode("ProjectedNFT".previous_utxo_tx_hash, 'hex') as previous_tx_hash, + "ProjectedNFT".previous_utxo_tx_output_index as previous_tx_output_index, + + CASE + WHEN "TransactionOutput".output_index = NULL THEN NULL + ELSE "TransactionOutput".output_index + END AS action_output_index, + + encode("Transaction".hash, 'hex') as action_tx_id, + + "ProjectedNFT".policy_id as policy_id, + "ProjectedNFT".asset_name as asset_name, + "ProjectedNFT".amount as amount, + + CASE + WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + ELSE 'Invalid' + END AS status, + + encode("ProjectedNFT".plutus_datum, 'hex') as plutus_datum, + "ProjectedNFT".for_how_long as for_how_long, + + "Block".slot as action_slot +FROM "ProjectedNFT" + LEFT JOIN "TransactionOutput" ON "TransactionOutput".id = "ProjectedNFT".hololocker_utxo_id + JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + JOIN "Block" ON "Transaction".block_id = "Block".id +WHERE + encode("ProjectedNFT".owner_address, 'hex') = :owner_address! + AND "Block".slot > :min_slot! + AND "Block".slot <= :max_slot! +ORDER BY ("Block".height, "Transaction".tx_index) ASC; diff --git a/webserver/server/app/services/ProjectedNftRange.ts b/webserver/server/app/services/ProjectedNftRange.ts index 83d0eb7f..49d0dd26 100644 --- a/webserver/server/app/services/ProjectedNftRange.ts +++ b/webserver/server/app/services/ProjectedNftRange.ts @@ -1,6 +1,7 @@ import type { PoolClient } from 'pg'; import type { ISqlProjectedNftRangeResult} from '../models/projected_nft/projectedNftRange.queries'; import { sqlProjectedNftRange } from '../models/projected_nft/projectedNftRange.queries'; +import { sqlProjectedNftRangeByAddress } from '../models/projected_nft/projectedNftRangeByAddress.queries'; export async function projectedNftRange(request: { range: { minSlot: number, maxSlot: number }, @@ -11,3 +12,15 @@ export async function projectedNftRange(request: { max_slot: request.range.maxSlot, }, request.dbTx)); } + +export async function projectedNftRangeByAddress(request: { + address: string, + range: { minSlot: number, maxSlot: number }, + dbTx: PoolClient, +}): Promise { + return (await sqlProjectedNftRangeByAddress.run({ + owner_address: request.address, + min_slot: request.range.minSlot, + max_slot: request.range.maxSlot, + }, request.dbTx)); +} diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index f5c7c900..0b364a3c 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -29,5 +29,9 @@ export const DEX_PRICE_LIMIT = { export const POOL_DELEGATION_LIMIT = { POOLS: 50, - SLOT_RANGE: 200, + SLOT_RANGE: 20000, +}; + +export const PROJECTED_NFT_LIMIT = { + SLOT_RANGE: 100000, }; diff --git a/webserver/shared/models/ProjectedNftRange.ts b/webserver/shared/models/ProjectedNftRange.ts index a7a35d47..302d8a6f 100644 --- a/webserver/shared/models/ProjectedNftRange.ts +++ b/webserver/shared/models/ProjectedNftRange.ts @@ -15,7 +15,8 @@ export type ProjectedNftRangeRequest = { * @example 46154860 */ maxSlot: number - } + }, + address: string | null }; export type ProjectedNftRangeResponse = {