From d9b9e31abe623866d609e581985070be742c967b Mon Sep 17 00:00:00 2001 From: Eugene Gostkin Date: Wed, 27 Dec 2023 01:53:55 +0100 Subject: [PATCH] Introduce efficient slot pagination --- .../tasks/src/multiera/multiera_asset_utxo.rs | 2 +- .../DelegationForPoolController.ts | 46 +++++++--- .../ProjectedNftRangeController.ts | 91 +++++++++++-------- .../app/models/asset/assetUtxos.queries.ts | 4 +- .../delegation/delegationsForPool.queries.ts | 13 ++- .../models/delegation/delegationsForPool.sql | 10 ++ .../projectedNftRange.queries.ts | 13 ++- .../projected_nft/projectedNftRange.sql | 10 ++ .../projectedNftRangeByAddress.queries.ts | 13 ++- .../projectedNftRangeByAddress.sql | 10 ++ .../server/app/services/DelegationForPool.ts | 7 +- .../server/app/services/ProjectedNftRange.ts | 14 +-- webserver/shared/constants.ts | 5 +- webserver/shared/models/DelegationForPool.ts | 14 ++- webserver/shared/models/ProjectedNftRange.ts | 33 +++---- webserver/shared/models/common.ts | 18 ++++ 16 files changed, 211 insertions(+), 92 deletions(-) diff --git a/indexer/tasks/src/multiera/multiera_asset_utxo.rs b/indexer/tasks/src/multiera/multiera_asset_utxo.rs index 5a5f28d2..b3d0491a 100644 --- a/indexer/tasks/src/multiera/multiera_asset_utxo.rs +++ b/indexer/tasks/src/multiera/multiera_asset_utxo.rs @@ -1,5 +1,5 @@ use super::multiera_stake_credentials::MultieraStakeCredentialTask; -use crate::config::AddressConfig::PayloadAndReadonlyConfig; +use crate::config::AddressConfig::AddressConfig; use crate::config::EmptyConfig::EmptyConfig; use crate::config::ReadonlyConfig::ReadonlyConfig; use crate::dsl::task_macro::*; diff --git a/webserver/server/app/controllers/DelegationForPoolController.ts b/webserver/server/app/controllers/DelegationForPoolController.ts index 3419dbcf..05c32613 100644 --- a/webserver/server/app/controllers/DelegationForPoolController.ts +++ b/webserver/server/app/controllers/DelegationForPoolController.ts @@ -6,8 +6,11 @@ import { genErrorMessage, type ErrorShape, Errors } from '../../../shared/errors import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { delegationsForPool } from '../services/DelegationForPool'; -import type { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool'; -import { POOL_DELEGATION_LIMIT } from '../../../shared/constants'; +import type { + DelegationForPoolResponse, + DelegationForPoolSingleResponse +} from '../../../shared/models/DelegationForPool'; +import {POOL_DELEGATION_LIMIT, PROJECTED_NFT_LIMIT} from '../../../shared/constants'; const route = Routes.delegationForPool; @@ -35,22 +38,31 @@ export class DelegationForPoolController extends Controller { ); } - const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; - if (slotRangeSize > POOL_DELEGATION_LIMIT.SLOT_RANGE) { + const after = requestBody.after != undefined ? requestBody.after : 0; + const until = requestBody.untilSlot != undefined ? requestBody.untilSlot : Number.MAX_VALUE; + const limit = requestBody.limit != undefined ? requestBody.limit : PROJECTED_NFT_LIMIT.MAX_LIMIT; + + if (limit > POOL_DELEGATION_LIMIT.MAX_LIMIT) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return errorResponse( - StatusCodes.BAD_REQUEST, - genErrorMessage(Errors.SlotRangeLimitExceeded, { - limit: POOL_DELEGATION_LIMIT.SLOT_RANGE, - found: slotRangeSize, - }) + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.SlotRangeLimitExceeded, { + limit: POOL_DELEGATION_LIMIT.MAX_LIMIT, + found: limit, + }) ); } - const response = await tx(pool, async dbTx => { + let params = { + afterSlot: after, + untilSlot: until, + limit: limit + }; + + const result = await tx(pool, async dbTx => { const data = await delegationsForPool({ pools: requestBody.pools.map(poolId => Buffer.from(poolId, 'hex')), - range: requestBody.range, + params: params, dbTx, }); @@ -62,6 +74,14 @@ export class DelegationForPoolController extends Controller { })); }); - return response; - } + let newAfter = undefined; + + if (result.length >= params.limit) { + newAfter = result[result.length - 1].slot; + } + + return { + result: result, + after: newAfter, + }; } } diff --git a/webserver/server/app/controllers/ProjectedNftRangeController.ts b/webserver/server/app/controllers/ProjectedNftRangeController.ts index 11f77367..419f4fd6 100644 --- a/webserver/server/app/controllers/ProjectedNftRangeController.ts +++ b/webserver/server/app/controllers/ProjectedNftRangeController.ts @@ -6,7 +6,11 @@ import type { ErrorShape } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { projectedNftRange, projectedNftRangeByAddress } from '../services/ProjectedNftRange'; -import type {ProjectedNftRangeResponse, ProjectedNftStatus} from '../../../shared/models/ProjectedNftRange'; +import type { + ProjectedNftRangeResponse, + ProjectedNftRangeSingleResponse, + ProjectedNftStatus +} from '../../../shared/models/ProjectedNftRange'; import {PROJECTED_NFT_LIMIT} from "../../../shared/constants"; import {Errors, genErrorMessage} from "../../../shared/errors"; @@ -25,45 +29,42 @@ export class ProjectedNftRangeController extends Controller { ErrorShape > ): Promise { - const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; + const after = requestBody.after != undefined ? requestBody.after : 0; + const until = requestBody.untilSlot != undefined ? requestBody.untilSlot : Number.MAX_VALUE; + const limit = requestBody.limit != undefined ? requestBody.limit : PROJECTED_NFT_LIMIT.MAX_LIMIT; - if (requestBody.address !== undefined) { - if (slotRangeSize > PROJECTED_NFT_LIMIT.SINGLE_USER_SLOT_RANGE) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return errorResponse( - StatusCodes.BAD_REQUEST, - genErrorMessage(Errors.SlotRangeLimitExceeded, { - limit: PROJECTED_NFT_LIMIT.SINGLE_USER_SLOT_RANGE, - found: slotRangeSize, - }) - ); - } + if (limit > PROJECTED_NFT_LIMIT.MAX_LIMIT) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.SlotRangeLimitExceeded, { + limit: PROJECTED_NFT_LIMIT.MAX_LIMIT, + found: limit, + }) + ); + } - return await this.handle_by_address_query(requestBody.address, requestBody); - } else { - 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, - }) - ); - } + let params = { + afterSlot: after, + untilSlot: until, + limit: limit + }; - return await this.handle_general_query(requestBody); + if (requestBody.address !== undefined) { + return await this.handle_by_address_query(requestBody.address, params); + } else { + return await this.handle_general_query(params); } } async handle_general_query( - requestBody: EndpointTypes[typeof route]['input'], + params: { afterSlot: number, untilSlot: number, limit: number }, ): Promise { - const response = await tx< - ProjectedNftRangeResponse + const result = await tx< + ProjectedNftRangeSingleResponse[] >(pool, async dbTx => { const data = await projectedNftRange({ - range: requestBody.range, + params: params, dbTx }); @@ -83,19 +84,28 @@ export class ProjectedNftRangeController extends Controller { })); }); - return response; + let after = undefined; + + if (result.length >= params.limit) { + after = result[result.length - 1].actionSlot; + } + + return { + result: result, + after: after, + }; } async handle_by_address_query( address: string, - requestBody: EndpointTypes[typeof route]['input'], + params: { afterSlot: number, untilSlot: number, limit: number }, ): Promise { - const response = await tx< - ProjectedNftRangeResponse + const result = await tx< + ProjectedNftRangeSingleResponse[] >(pool, async dbTx => { const data = await projectedNftRangeByAddress({ address: address, - range: requestBody.range, + params: params, dbTx }); @@ -115,6 +125,15 @@ export class ProjectedNftRangeController extends Controller { })); }); - return response; + let after = undefined; + + if (result.length >= params.limit) { + after = result[result.length - 1].actionSlot; + } + + return { + result: result, + after: after, + }; } } \ No newline at end of file diff --git a/webserver/server/app/models/asset/assetUtxos.queries.ts b/webserver/server/app/models/asset/assetUtxos.queries.ts index e3d41ec0..717077d9 100644 --- a/webserver/server/app/models/asset/assetUtxos.queries.ts +++ b/webserver/server/app/models/asset/assetUtxos.queries.ts @@ -25,7 +25,7 @@ export interface IAssetUtxosQuery { result: IAssetUtxosResult; } -const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"min_slot":true,"max_slot":true},"params":[{"name":"fingerprints","required":true,"transform":{"type":"array_spread"},"locs":[{"a":677,"b":690}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":712,"b":721}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":744,"b":753}]}],"statement":"SELECT ENCODE(TXO.HASH,\n 'hex') OUTPUT_TX_HASH,\n \"TransactionOutput\".OUTPUT_INDEX,\n\t\"NativeAsset\".CIP14_FINGERPRINT,\n\t\"AssetUtxo\".AMOUNT,\n\t\"Block\".SLOT,\n\tENCODE(\"Transaction\".HASH,\n 'hex') TX_HASH,\n\t\"Address\".PAYLOAD ADDRESS_RAW\nFROM \"AssetUtxo\"\nJOIN \"Transaction\" ON \"AssetUtxo\".TX_ID = \"Transaction\".ID\nJOIN \"TransactionOutput\" ON \"AssetUtxo\".UTXO_ID = \"TransactionOutput\".ID\nJOIN \"Transaction\" TXO ON \"TransactionOutput\".TX_ID = TXO.ID\nJOIN \"Address\" ON \"Address\".id = \"TransactionOutput\".address_id\nJOIN \"NativeAsset\" ON \"AssetUtxo\".ASSET_ID = \"NativeAsset\".ID\nJOIN \"Block\" ON \"Transaction\".BLOCK_ID = \"Block\".ID\nWHERE \n\t\"NativeAsset\".CIP14_FINGERPRINT IN :fingerprints! AND\n\t\"Block\".SLOT > :min_slot! AND\n\t\"Block\".SLOT <= :max_slot!\nORDER BY \"Transaction\".ID ASC"}; +const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"min_slot":true,"max_slot":true},"params":[{"name":"fingerprints","required":true,"transform":{"type":"array_spread"},"locs":[{"a":677,"b":690}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":712,"b":721}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":744,"b":753}]}],"statement":"SELECT ENCODE(TXO.HASH,\n 'hex') OUTPUT_TX_HASH,\n \"TransactionOutput\".OUTPUT_INDEX,\n\t\"NativeAsset\".CIP14_FINGERPRINT,\n\t\"AssetUtxo\".AMOUNT,\n\t\"Block\".SLOT,\n\tENCODE(\"Transaction\".HASH,\n 'hex') TX_HASH,\n\t\"Address\".PAYLOAD ADDRESS_RAW\nFROM \"AssetUtxo\"\nJOIN \"Transaction\" ON \"AssetUtxo\".TX_ID = \"Transaction\".ID\nJOIN \"TransactionOutput\" ON \"AssetUtxo\".UTXO_ID = \"TransactionOutput\".ID\nJOIN \"Transaction\" TXO ON \"TransactionOutput\".TX_ID = TXO.ID\nJOIN \"Address\" ON \"Address\".id = \"TransactionOutput\".address_id\nJOIN \"NativeAsset\" ON \"AssetUtxo\".ASSET_ID = \"NativeAsset\".ID\nJOIN \"Block\" ON \"Transaction\".BLOCK_ID = \"Block\".ID\nWHERE \n\t\"NativeAsset\".CIP14_FINGERPRINT IN :fingerprints! AND\n\t\"Block\".SLOT > :min_slot! AND\n\t\"Block\".SLOT <= :max_slot!\nORDER BY \"Transaction\".ID, \"AssetUtxo\".ID ASC"}; /** * Query generated from SQL: @@ -50,7 +50,7 @@ const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"min_slot":true," * "NativeAsset".CIP14_FINGERPRINT IN :fingerprints! AND * "Block".SLOT > :min_slot! AND * "Block".SLOT <= :max_slot! - * ORDER BY "Transaction".ID ASC + * ORDER BY "Transaction".ID, "AssetUtxo".ID ASC * ``` */ export const assetUtxos = new PreparedQuery(assetUtxosIR); diff --git a/webserver/server/app/models/delegation/delegationsForPool.queries.ts b/webserver/server/app/models/delegation/delegationsForPool.queries.ts index 45e1d7c4..17c40f20 100644 --- a/webserver/server/app/models/delegation/delegationsForPool.queries.ts +++ b/webserver/server/app/models/delegation/delegationsForPool.queries.ts @@ -3,6 +3,7 @@ import { PreparedQuery } from '@pgtyped/query'; /** 'SqlStakeDelegationByPool' parameters type */ export interface ISqlStakeDelegationByPoolParams { + limit: string; max_slot: number; min_slot: number; pools: readonly (Buffer)[]; @@ -22,7 +23,7 @@ export interface ISqlStakeDelegationByPoolQuery { result: ISqlStakeDelegationByPoolResult; } -const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot":true,"max_slot":true},"params":[{"name":"pools","required":true,"transform":{"type":"array_spread"},"locs":[{"a":176,"b":182},{"a":590,"b":596},{"a":657,"b":663}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":688,"b":697}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":720,"b":729}]}],"statement":"SELECT \n\tencode(credential, 'hex') as credential,\n\tencode(\"Transaction\".hash, 'hex') as tx_id,\n\t\"Block\".slot,\n\tCASE WHEN \"StakeDelegationCredentialRelation\".pool_credential IN :pools! THEN encode(\"StakeDelegationCredentialRelation\".pool_credential, 'hex') ELSE NULL END AS pool\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n (\n\t\t\"StakeDelegationCredentialRelation\".pool_credential IN :pools! OR\n\t \t\"StakeDelegationCredentialRelation\".previous_pool IN :pools!\n\t) AND\n\t\"Block\".slot > :min_slot! AND\n\t\"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; +const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot":true,"max_slot":true,"limit":true},"params":[{"name":"pools","required":true,"transform":{"type":"array_spread"},"locs":[{"a":176,"b":182},{"a":590,"b":596},{"a":657,"b":663}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":688,"b":697},{"a":1083,"b":1092}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":720,"b":729},{"a":1126,"b":1135}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":1151,"b":1157}]}],"statement":"SELECT \n\tencode(credential, 'hex') as credential,\n\tencode(\"Transaction\".hash, 'hex') as tx_id,\n\t\"Block\".slot,\n\tCASE WHEN \"StakeDelegationCredentialRelation\".pool_credential IN :pools! THEN encode(\"StakeDelegationCredentialRelation\".pool_credential, 'hex') ELSE NULL END AS pool\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n (\n\t\t\"StakeDelegationCredentialRelation\".pool_credential IN :pools! OR\n\t \t\"StakeDelegationCredentialRelation\".previous_pool IN :pools!\n\t) AND\n\t\"Block\".slot > :min_slot! AND\n\t\"Block\".slot <= :max_slot!\n AND \"Block\".height <= (\n SELECT MAX(\"Heights\".height) FROM\n (SELECT \"Block\".height as height FROM \"StakeDelegationCredentialRelation\"\n JOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n WHERE\n \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\n LIMIT :limit!) AS \"Heights\"\n )\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; /** * Query generated from SQL: @@ -43,6 +44,16 @@ const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot" * ) AND * "Block".slot > :min_slot! AND * "Block".slot <= :max_slot! + * AND "Block".height <= ( + * SELECT MAX("Heights".height) FROM + * (SELECT "Block".height as height FROM "StakeDelegationCredentialRelation" + * JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * "Block".slot > :min_slot! + * AND "Block".slot <= :max_slot! + * LIMIT :limit!) AS "Heights" + * ) * ORDER BY ("Block".height, "Transaction".tx_index) ASC * ``` */ diff --git a/webserver/server/app/models/delegation/delegationsForPool.sql b/webserver/server/app/models/delegation/delegationsForPool.sql index 2e9379c1..bb1410fc 100644 --- a/webserver/server/app/models/delegation/delegationsForPool.sql +++ b/webserver/server/app/models/delegation/delegationsForPool.sql @@ -18,4 +18,14 @@ WHERE ) AND "Block".slot > :min_slot! AND "Block".slot <= :max_slot! + AND "Block".height <= ( + SELECT MAX("Heights".height) FROM + (SELECT "Block".height as height FROM "StakeDelegationCredentialRelation" + JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id + JOIN "Block" ON "Transaction".block_id = "Block".id + WHERE + "Block".slot > :min_slot! + AND "Block".slot <= :max_slot! + LIMIT :limit!) AS "Heights" + ) ORDER BY ("Block".height, "Transaction".tx_index) ASC; \ No newline at end of file diff --git a/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts b/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts index 07cc2489..8157142a 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts +++ b/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts @@ -3,6 +3,7 @@ import { PreparedQuery } from '@pgtyped/query'; /** 'SqlProjectedNftRange' parameters type */ export interface ISqlProjectedNftRangeParams { + limit: string; max_slot: number; min_slot: number; } @@ -29,7 +30,7 @@ export interface ISqlProjectedNftRangeQuery { result: ISqlProjectedNftRangeResult; } -const sqlProjectedNftRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot":true},"params":[{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1219,"b":1228}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1254,"b":1263}]}],"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 \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; +const sqlProjectedNftRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot":true,"limit":true},"params":[{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1219,"b":1228},{"a":1575,"b":1584}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1254,"b":1263},{"a":1618,"b":1627}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":1643,"b":1649}]}],"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 \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\n AND \"Block\".height <= (\n SELECT MAX(\"Heights\".height) FROM\n (SELECT \"Block\".height as height FROM \"ProjectedNFT\"\n JOIN \"Transaction\" ON \"Transaction\".id = \"ProjectedNFT\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n WHERE\n \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\n LIMIT :limit!) AS \"Heights\"\n )\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; /** * Query generated from SQL: @@ -69,6 +70,16 @@ const sqlProjectedNftRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot": * WHERE * "Block".slot > :min_slot! * AND "Block".slot <= :max_slot! + * AND "Block".height <= ( + * SELECT MAX("Heights".height) FROM + * (SELECT "Block".height as height FROM "ProjectedNFT" + * JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * "Block".slot > :min_slot! + * AND "Block".slot <= :max_slot! + * LIMIT :limit!) AS "Heights" + * ) * ORDER BY ("Block".height, "Transaction".tx_index) ASC * ``` */ diff --git a/webserver/server/app/models/projected_nft/projectedNftRange.sql b/webserver/server/app/models/projected_nft/projectedNftRange.sql index df00dc11..19001d67 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRange.sql +++ b/webserver/server/app/models/projected_nft/projectedNftRange.sql @@ -36,4 +36,14 @@ FROM "ProjectedNFT" WHERE "Block".slot > :min_slot! AND "Block".slot <= :max_slot! + AND "Block".height <= ( + SELECT MAX("Heights".height) FROM + (SELECT "Block".height as height FROM "ProjectedNFT" + JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + JOIN "Block" ON "Transaction".block_id = "Block".id + WHERE + "Block".slot > :min_slot! + AND "Block".slot <= :max_slot! + LIMIT :limit!) AS "Heights" + ) ORDER BY ("Block".height, "Transaction".tx_index) ASC; diff --git a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts index 15ed1558..8f673004 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts @@ -3,6 +3,7 @@ import { PreparedQuery } from '@pgtyped/query'; /** 'SqlProjectedNftRangeByAddress' parameters type */ export interface ISqlProjectedNftRangeByAddressParams { + limit: string; max_slot: number; min_slot: number; owner_address: string; @@ -30,7 +31,7 @@ export interface ISqlProjectedNftRangeByAddressQuery { 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"}; +const sqlProjectedNftRangeByAddressIR: any = {"usedParamSet":{"owner_address":true,"min_slot":true,"max_slot":true,"limit":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},{"a":1669,"b":1678}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1324,"b":1333},{"a":1716,"b":1725}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":1745,"b":1751}]}],"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!\n AND \"Block\".height <= (\n SELECT MAX(\"Heights\".height) FROM\n (SELECT \"Block\".height as height FROM \"ProjectedNFT\"\n JOIN \"Transaction\" ON \"Transaction\".id = \"ProjectedNFT\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n WHERE\n \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\n LIMIT :limit!) AS \"Heights\"\n )\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; /** * Query generated from SQL: @@ -71,6 +72,16 @@ const sqlProjectedNftRangeByAddressIR: any = {"usedParamSet":{"owner_address":tr * encode("ProjectedNFT".owner_address, 'hex') = :owner_address! * AND "Block".slot > :min_slot! * AND "Block".slot <= :max_slot! + * AND "Block".height <= ( + * SELECT MAX("Heights".height) FROM + * (SELECT "Block".height as height FROM "ProjectedNFT" + * JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * "Block".slot > :min_slot! + * AND "Block".slot <= :max_slot! + * LIMIT :limit!) AS "Heights" + * ) * ORDER BY ("Block".height, "Transaction".tx_index) ASC * ``` */ diff --git a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql index 8aae388c..ccba0eb7 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql @@ -37,4 +37,14 @@ WHERE encode("ProjectedNFT".owner_address, 'hex') = :owner_address! AND "Block".slot > :min_slot! AND "Block".slot <= :max_slot! + AND "Block".height <= ( + SELECT MAX("Heights".height) FROM + (SELECT "Block".height as height FROM "ProjectedNFT" + JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + JOIN "Block" ON "Transaction".block_id = "Block".id + WHERE + "Block".slot > :min_slot! + AND "Block".slot <= :max_slot! + LIMIT :limit!) AS "Heights" + ) ORDER BY ("Block".height, "Transaction".tx_index) ASC; diff --git a/webserver/server/app/services/DelegationForPool.ts b/webserver/server/app/services/DelegationForPool.ts index 2854016b..b248d7f5 100644 --- a/webserver/server/app/services/DelegationForPool.ts +++ b/webserver/server/app/services/DelegationForPool.ts @@ -3,13 +3,14 @@ import type { ISqlStakeDelegationByPoolResult} from '../models/delegation/delega import { sqlStakeDelegationByPool } from '../models/delegation/delegationsForPool.queries'; export async function delegationsForPool(request: { - range: { minSlot: number, maxSlot: number }, + params: { afterSlot: number, untilSlot: number, limit: number }, pools: Buffer[], dbTx: PoolClient, }): Promise { return (await sqlStakeDelegationByPool.run({ - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, + min_slot: request.params.afterSlot, + max_slot: request.params.untilSlot, + limit: request.params.limit.toString(), pools: request.pools }, request.dbTx)); } \ No newline at end of file diff --git a/webserver/server/app/services/ProjectedNftRange.ts b/webserver/server/app/services/ProjectedNftRange.ts index 49d0dd26..d22a4f05 100644 --- a/webserver/server/app/services/ProjectedNftRange.ts +++ b/webserver/server/app/services/ProjectedNftRange.ts @@ -4,23 +4,25 @@ import { sqlProjectedNftRange } from '../models/projected_nft/projectedNftRange. import { sqlProjectedNftRangeByAddress } from '../models/projected_nft/projectedNftRangeByAddress.queries'; export async function projectedNftRange(request: { - range: { minSlot: number, maxSlot: number }, + params: { afterSlot: number, untilSlot: number, limit: number }, dbTx: PoolClient, }): Promise { return (await sqlProjectedNftRange.run({ - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, + min_slot: request.params.afterSlot, + max_slot: request.params.untilSlot, + limit: request.params.limit.toString(), }, request.dbTx)); } export async function projectedNftRangeByAddress(request: { address: string, - range: { minSlot: number, maxSlot: number }, + params: { afterSlot: number, untilSlot: number, limit: number }, dbTx: PoolClient, }): Promise { return (await sqlProjectedNftRangeByAddress.run({ owner_address: request.address, - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, + min_slot: request.params.afterSlot, + max_slot: request.params.untilSlot, + limit: request.params.limit.toString(), }, request.dbTx)); } diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index 725412aa..72863ad7 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -28,13 +28,12 @@ export const DEX_PRICE_LIMIT = { }; export const PROJECTED_NFT_LIMIT = { - SLOT_RANGE: 100000, - SINGLE_USER_SLOT_RANGE: 10000000000, + MAX_LIMIT: 100, }; export const POOL_DELEGATION_LIMIT = { POOLS: 50, - SLOT_RANGE: 10000, + MAX_LIMIT: 500, }; export const ASSET_UTXOS_LIMIT = { diff --git a/webserver/shared/models/DelegationForPool.ts b/webserver/shared/models/DelegationForPool.ts index 37209e58..3e227727 100644 --- a/webserver/shared/models/DelegationForPool.ts +++ b/webserver/shared/models/DelegationForPool.ts @@ -1,14 +1,20 @@ import { Address } from "./Address"; import { Pool, PoolHex } from "./Pool"; +import type { SlotPagination } from "./common"; export type DelegationForPoolRequest = { pools: Pool[]; - range: { minSlot: number, maxSlot: number } -}; + limit: number | undefined +} & SlotPagination; -export type DelegationForPoolResponse = { +export type DelegationForPoolSingleResponse = { credential: Address; pool: PoolHex | null, txId: string | null; slot: number; -}[]; \ No newline at end of file +}; + +export type DelegationForPoolResponse = { + result: DelegationForPoolSingleResponse[], + after: number | undefined, +}; \ No newline at end of file diff --git a/webserver/shared/models/ProjectedNftRange.ts b/webserver/shared/models/ProjectedNftRange.ts index e14f365b..9d034966 100644 --- a/webserver/shared/models/ProjectedNftRange.ts +++ b/webserver/shared/models/ProjectedNftRange.ts @@ -1,23 +1,9 @@ +import type {SlotPagination } from "./common"; + export type ProjectedNftRangeRequest = { - /** - * Projected NFT events in this slot range will be returned - */ - range: { - /** - * Minimal slot from which the events should be returned (not inclusive) - * - * @example 46154769 - */ - minSlot: number, - /** - * Maximal slot from which the events should be returned (inclusive) - * - * @example 46154860 - */ - maxSlot: number - }, - address: string | undefined -}; + address: string | undefined, + limit: number | undefined +} & SlotPagination; export enum ProjectedNftStatus { Lock = 'Lock', @@ -26,7 +12,7 @@ export enum ProjectedNftStatus { Invalid = 'Invalid' }; -export type ProjectedNftRangeResponse = { +export type ProjectedNftRangeSingleResponse = { /** * Slot at which the transaction happened * @@ -112,4 +98,9 @@ export type ProjectedNftRangeResponse = { * @example "1701266986000" */ forHowLong: string | null, -}[]; \ No newline at end of file +}; + +export type ProjectedNftRangeResponse = { + result: ProjectedNftRangeSingleResponse[], + after: number | undefined, +} \ No newline at end of file diff --git a/webserver/shared/models/common.ts b/webserver/shared/models/common.ts index 4f6d8729..31fd88fd 100644 --- a/webserver/shared/models/common.ts +++ b/webserver/shared/models/common.ts @@ -74,6 +74,24 @@ export type UntilBlockPagination = { }; export type Pagination = AfterBlockPagination & UntilBlockPagination; +export type AfterSlotPagination = { + /** + * Minimal slot from which the events should be returned (not inclusive) + * + * @example 46154769 + */ + after: number | undefined, +} +export type UntilSlotPagination = { + /** + * Maximal slot from which the events should be returned (inclusive) + * + * @example 46154860 + */ + untilSlot: number | undefined, +} +export type SlotPagination = AfterSlotPagination & UntilSlotPagination; + export type UtxoPointer = { /** * @pattern [0-9a-fA-F]{64}