diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index 856fa32f..b42a80e6 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -894,6 +894,29 @@ }, "MintBurnSingleResponse": { "properties": { + "block": { + "type": "string", + "description": "Block id of related mint / burn event", + "example": "4e90f1d14ad742a1c0e094a89ad180b896068f93fc3969614b1c53bac547b374", + "pattern": "[0-9a-fA-F]{64}" + }, + "txId": { + "type": "string", + "description": "Transaction id of related mint / burn event", + "example": "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda", + "pattern": "[0-9a-fA-F]{64}" + }, + "metadata": { + "type": "string", + "nullable": true, + "description": "Transaction metadata of related mint / burn event" + }, + "actionSlot": { + "type": "number", + "format": "double", + "description": "Slot at which the transaction happened", + "example": 512345 + }, "assets": { "properties": {}, "additionalProperties": { @@ -910,37 +933,14 @@ "42657272794e617679": "1" } } - }, - "metadata": { - "type": "string", - "nullable": true, - "description": "Transaction metadata of related mint / burn event" - }, - "actionBlockId": { - "type": "string", - "description": "Block id of related mint / burn event", - "example": "4e90f1d14ad742a1c0e094a89ad180b896068f93fc3969614b1c53bac547b374", - "pattern": "[0-9a-fA-F]{64}" - }, - "actionTxId": { - "type": "string", - "description": "Transaction id of related mint / burn event", - "example": "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda", - "pattern": "[0-9a-fA-F]{64}" - }, - "actionSlot": { - "type": "number", - "format": "double", - "description": "Slot at which the transaction happened", - "example": 512345 } }, "required": [ - "assets", + "block", + "txId", "metadata", - "actionBlockId", - "actionTxId", - "actionSlot" + "actionSlot", + "assets" ], "type": "object" }, @@ -951,40 +951,30 @@ "type": "array" }, "MintBurnHistoryRequest": { - "properties": { - "policyIds": { - "items": { - "$ref": "#/components/schemas/PolicyId" - }, - "type": "array" - }, - "range": { + "allOf": [ + { "properties": { - "maxSlot": { + "limit": { "type": "number", - "format": "double", - "description": "Maximal slot from which the events should be returned (inclusive)", - "example": 46154860 + "format": "double" }, - "minSlot": { - "type": "number", - "format": "double", - "description": "Minimal slot from which the events should be returned (not inclusive)", - "example": 46154769 + "slotLimits": { + "$ref": "#/components/schemas/SlotLimits", + "description": "This limits the transactions in the result to this range of slots.\nEverything else is filtered out" + }, + "policyIds": { + "items": { + "$ref": "#/components/schemas/PolicyId" + }, + "type": "array" } }, - "required": [ - "maxSlot", - "minSlot" - ], - "type": "object", - "description": "Mint Burn events in this slot range will be returned" + "type": "object" + }, + { + "$ref": "#/components/schemas/Pagination" } - }, - "required": [ - "range" - ], - "type": "object" + ] }, "ProjectedNftStatus": { "enum": [ @@ -1219,23 +1209,6 @@ "description": "Filter which uses of the address are considered relevant for the query.\n\nThis is a bitmask, so you can combine multiple options\nex: `RelationFilterType.Input & RelationFilterType.Output`\n\nNote: relations only apply to credentials and not to full bech32 addresses", "pattern": "([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" }, - "SlotLimits": { - "properties": { - "to": { - "type": "number", - "format": "double" - }, - "from": { - "type": "number", - "format": "double" - } - }, - "required": [ - "to", - "from" - ], - "type": "object" - }, "TransactionHistoryRequest": { "allOf": [ { diff --git a/webserver/server/app/controllers/MintBurnHistoryController.ts b/webserver/server/app/controllers/MintBurnHistoryController.ts index 8ada0212..c3334800 100644 --- a/webserver/server/app/controllers/MintBurnHistoryController.ts +++ b/webserver/server/app/controllers/MintBurnHistoryController.ts @@ -3,7 +3,7 @@ import { StatusCodes } from 'http-status-codes'; import tx from 'pg-tx'; import pool from '../services/PgPoolSingleton'; -import type { ErrorShape } from '../../../shared/errors'; +import { Errors, genErrorMessage, type ErrorShape } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { mintBurnRange, mintBurnRangeByPolicyIds } from '../services/MintBurnHistoryService'; @@ -11,8 +11,15 @@ import type { MintBurnSingleResponse } from '../../../shared/models/MintBurn'; import type { PolicyId } from '../../../shared/models/PolicyIdAssetMap'; import type { ISqlMintBurnRangeResult, - ISqlMintBurnRangeByPolicyIdsResult, } from '../models/asset/mintBurnHistory.queries'; +import { + adjustToSlotLimits, + resolvePageStart, + resolveUntilTransaction, +} from '../services/PaginationService'; +import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries'; +import { MINT_BURN_HISTORY_LIMIT } from '../../../shared/constants'; +import { expectType } from 'tsd'; const route = Routes.mintBurnHistory; @@ -34,122 +41,102 @@ export class MintRangeController extends Controller { ErrorShape > ): Promise { - if (requestBody.policyIds !== undefined && requestBody.policyIds.length > 0) { - return await this.handle_by_policy_ids_query(requestBody.policyIds, requestBody); - } else { - return await this.handle_general_query(requestBody); - } - } + // note: we use a SQL transaction to make sure the pagination check works properly + // otherwise, a rollback could happen between getting the pagination info and the history query + const response = await tx(pool, async dbTx => { + const [until, pageStart, slotBounds] = await Promise.all([ + resolveUntilTransaction({ + block_hash: Buffer.from(requestBody.untilBlock, 'hex'), + dbTx, + }), + requestBody.after == null + ? Promise.resolve(undefined) + : resolvePageStart({ + after_block: Buffer.from(requestBody.after.block, 'hex'), + after_tx: Buffer.from(requestBody.after.tx, 'hex'), + dbTx, + }), + !requestBody.slotLimits + ? Promise.resolve(undefined) + : slotBoundsPagination.run( + { low: requestBody.slotLimits.from, high: requestBody.slotLimits.to }, + dbTx + ), + ]); + + if (until == null) { + return genErrorMessage(Errors.BlockHashNotFound, { + untilBlock: requestBody.untilBlock, + }); + } + if (requestBody.after != null && pageStart == null) { + return genErrorMessage(Errors.PageStartNotFound, { + blockHash: requestBody.after.block, + txHash: requestBody.after.tx, + }); + } - async handle_general_query( - requestBody: EndpointTypes[typeof route]['input'] - ): Promise { - const assets = await tx(pool, async dbTx => { - const data = await mintBurnRange({ - range: requestBody.range, - dbTx, + const pageStartWithSlot = adjustToSlotLimits( + pageStart, + until, + requestBody.slotLimits, + slotBounds + ); + + const assets = await tx(pool, async dbTx => { + if (requestBody.policyIds !== undefined && requestBody.policyIds.length > 0) { + const data = await mintBurnRangeByPolicyIds({ + after: pageStartWithSlot?.tx_id || 0, + until: until.tx_id, + limit: requestBody.limit || MINT_BURN_HISTORY_LIMIT.DEFAULT_PAGE_SIZE, + policyIds: requestBody.policyIds, + dbTx, + }); + + return data; + } else { + const data = await mintBurnRange({ + after: pageStartWithSlot?.tx_id || 0, + until: until.tx_id, + limit: requestBody.limit || MINT_BURN_HISTORY_LIMIT.DEFAULT_PAGE_SIZE, + dbTx, + }); + + return data; + } }); - return data; - }); - - let mintRangeResponse: MintBurnSingleResponse = { - actionTxId: '', - actionBlockId: '', - metadata: null, - actionSlot: 0, - assets: {}, - }; - - const result: MintBurnSingleResponse[] = []; - - for (const entry of assets) { - const policyId = entry.policy_id !== null ? entry.policy_id.toString() : ''; - const assetName = entry.asset_name !== null ? entry.asset_name.toString() : ''; - const actionTxId = entry.action_tx_id !== null ? entry.action_tx_id.toString() : ''; - const actionBlockId = entry.action_block_id !== null ? entry.action_block_id.toString() : ''; - - if (mintRangeResponse.actionTxId != actionTxId) { - if (mintRangeResponse.actionTxId.length > 0) { - result.push(mintRangeResponse); + return assets.map(entry => { + const assets: { [policyId: PolicyId]: { [assetName: string]: string } } = {}; + + for (const pair of entry.payload as { + policyId: string; + assetName: string; + amount: string; + }[]) { + if (!assets[pair.policyId]) { + assets[pair.policyId] = { [pair.assetName]: pair.amount }; + } else { + assets[pair.policyId][pair.assetName] = pair.amount; + } } - mintRangeResponse = { + return { + assets: assets, actionSlot: entry.action_slot, - actionTxId: actionTxId, - actionBlockId: actionBlockId, metadata: entry.action_tx_metadata, - assets: {}, + txId: entry.tx, + block: entry.block, }; - } - - const for_policy = mintRangeResponse.assets[policyId] ?? {}; - - for_policy[assetName] = entry.amount; - mintRangeResponse.assets[policyId] = for_policy; - } - - if (mintRangeResponse.actionTxId.length > 0) { - result.push(mintRangeResponse); - } - - return result; - } - - async handle_by_policy_ids_query( - policyIds: PolicyId[], - requestBody: EndpointTypes[typeof route]['input'] - ): Promise { - const assets = await tx(pool, async dbTx => { - const data = await mintBurnRangeByPolicyIds({ - range: requestBody.range, - policyIds: policyIds, - dbTx, }); - - return data; }); - let mintRangeResponse: MintBurnSingleResponse = { - actionTxId: '', - actionBlockId: '', - metadata: null, - actionSlot: 0, - assets: {}, - }; - - const result: MintBurnSingleResponse[] = []; - - for (const entry of assets) { - const policyId = entry.policy_id !== null ? entry.policy_id.toString() : ''; - const assetName = entry.asset_name !== null ? entry.asset_name.toString() : ''; - const actionTxId = entry.action_tx_id !== null ? entry.action_tx_id.toString() : ''; - const actionBlockId = entry.action_block_id !== null ? entry.action_block_id.toString() : ''; - - if (mintRangeResponse.actionTxId != actionTxId) { - if (mintRangeResponse.actionTxId.length > 0) { - result.push(mintRangeResponse); - } - - mintRangeResponse = { - actionSlot: entry.action_slot, - actionTxId: actionTxId, - actionBlockId: actionBlockId, - metadata: entry.action_tx_metadata, - assets: {}, - }; - } - - const for_policy = mintRangeResponse.assets[policyId] ?? {}; - - for_policy[assetName] = entry.amount; - mintRangeResponse.assets[policyId] = for_policy; - } - - if (mintRangeResponse.actionTxId.length > 0) { - result.push(mintRangeResponse); + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, response); } - return result; + return response; } } diff --git a/webserver/server/app/models/asset/mintBurnHistory.queries.ts b/webserver/server/app/models/asset/mintBurnHistory.queries.ts index 365cc555..ca24f495 100644 --- a/webserver/server/app/models/asset/mintBurnHistory.queries.ts +++ b/webserver/server/app/models/asset/mintBurnHistory.queries.ts @@ -1,21 +1,24 @@ /** Types generated for queries found in "app/models/asset/mintBurnHistory.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +export type NumberOrString = number | string; + /** 'SqlMintBurnRange' parameters type */ export interface ISqlMintBurnRangeParams { - max_slot: number; - min_slot: number; + after_tx_id: NumberOrString; + limit: NumberOrString; + until_tx_id: NumberOrString; } /** 'SqlMintBurnRange' return type */ export interface ISqlMintBurnRangeResult { - action_block_id: string | null; action_slot: number; - action_tx_id: string | null; action_tx_metadata: string | null; - amount: string; - asset_name: string | null; - policy_id: string | null; + block: string; + payload: Json; + tx: string; } /** 'SqlMintBurnRange' query type */ @@ -24,31 +27,32 @@ export interface ISqlMintBurnRangeQuery { result: ISqlMintBurnRangeResult; } -const sqlMintBurnRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot":true},"params":[{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":793,"b":802}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":828,"b":837}]}],"statement":"SELECT\n \"AssetMint\".amount as amount,\n encode(\"NativeAsset\".policy_id, 'hex') as policy_id,\n encode(\"NativeAsset\".asset_name, 'hex') as asset_name,\n encode(\"Transaction\".hash, 'hex') as action_tx_id,\n encode(\"Block\".hash, 'hex') as action_block_id,\n CASE\n WHEN \"TransactionMetadata\".payload = NULL THEN NULL\n ELSE encode(\"TransactionMetadata\".payload, 'hex')\n END AS action_tx_metadata,\n \"Block\".slot as action_slot\nFROM \"AssetMint\"\n LEFT JOIN \"TransactionMetadata\" ON \"TransactionMetadata\".id = \"AssetMint\".tx_id\n JOIN \"NativeAsset\" ON \"NativeAsset\".id = \"AssetMint\".asset_id\n JOIN \"Transaction\" ON \"Transaction\".id = \"AssetMint\".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 sqlMintBurnRangeIR: any = {"usedParamSet":{"after_tx_id":true,"until_tx_id":true,"limit":true},"params":[{"name":"after_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":734,"b":746}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":773,"b":785}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":887,"b":893}]}],"statement":"SELECT\n\tENCODE(\"Transaction\".HASH, 'hex') \"tx!\",\n\tENCODE(\"Block\".HASH, 'hex') AS \"block!\",\n\t\"Block\".slot AS action_slot,\n\tENCODE(\"TransactionMetadata\".payload, 'hex') as action_tx_metadata,\n\tjson_agg(json_build_object(\n 'amount', \"AssetMint\".amount::text,\n 'policyId', encode(\"NativeAsset\".policy_id, 'hex'),\n 'assetName', encode(\"NativeAsset\".asset_name, 'hex')\n\t)) as \"payload!\"\nFROM \"AssetMint\"\n LEFT JOIN \"TransactionMetadata\" ON \"TransactionMetadata\".id = \"AssetMint\".tx_id\n JOIN \"NativeAsset\" ON \"NativeAsset\".id = \"AssetMint\".asset_id\n JOIN \"Transaction\" ON \"Transaction\".id = \"AssetMint\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE\n\t\"Transaction\".id > :after_tx_id! AND\n\t\"Transaction\".id <= :until_tx_id!\nGROUP BY \"Transaction\".id, \"Block\".id, \"TransactionMetadata\".id\nORDER BY \"Transaction\".id ASC\nLIMIT :limit!"}; /** * Query generated from SQL: * ``` * SELECT - * "AssetMint".amount as amount, - * encode("NativeAsset".policy_id, 'hex') as policy_id, - * encode("NativeAsset".asset_name, 'hex') as asset_name, - * encode("Transaction".hash, 'hex') as action_tx_id, - * encode("Block".hash, 'hex') as action_block_id, - * CASE - * WHEN "TransactionMetadata".payload = NULL THEN NULL - * ELSE encode("TransactionMetadata".payload, 'hex') - * END AS action_tx_metadata, - * "Block".slot as action_slot + * ENCODE("Transaction".HASH, 'hex') "tx!", + * ENCODE("Block".HASH, 'hex') AS "block!", + * "Block".slot AS action_slot, + * ENCODE("TransactionMetadata".payload, 'hex') as action_tx_metadata, + * json_agg(json_build_object( + * 'amount', "AssetMint".amount::text, + * 'policyId', encode("NativeAsset".policy_id, 'hex'), + * 'assetName', encode("NativeAsset".asset_name, 'hex') + * )) as "payload!" * FROM "AssetMint" * LEFT JOIN "TransactionMetadata" ON "TransactionMetadata".id = "AssetMint".tx_id * JOIN "NativeAsset" ON "NativeAsset".id = "AssetMint".asset_id * JOIN "Transaction" ON "Transaction".id = "AssetMint".tx_id * JOIN "Block" ON "Transaction".block_id = "Block".id * WHERE - * "Block".slot > :min_slot! - * AND "Block".slot <= :max_slot! - * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * "Transaction".id > :after_tx_id! AND + * "Transaction".id <= :until_tx_id! + * GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id + * ORDER BY "Transaction".id ASC + * LIMIT :limit! * ``` */ export const sqlMintBurnRange = new PreparedQuery(sqlMintBurnRangeIR); @@ -56,20 +60,19 @@ export const sqlMintBurnRange = new PreparedQuery :min_slot!\n AND \"Block\".slot <= :max_slot!\n AND \"NativeAsset\".policy_id IN :policy_ids!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; +const sqlMintBurnRangeByPolicyIdsIR: any = {"usedParamSet":{"after_tx_id":true,"until_tx_id":true,"policy_ids":true,"limit":true},"params":[{"name":"policy_ids","required":true,"transform":{"type":"array_spread"},"locs":[{"a":822,"b":833}]},{"name":"after_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":734,"b":746}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":773,"b":785}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":935,"b":941}]}],"statement":"SELECT\n\tENCODE(\"Transaction\".HASH, 'hex') \"tx!\",\n\tENCODE(\"Block\".HASH, 'hex') AS \"block!\",\n\t\"Block\".slot AS action_slot,\n\tENCODE(\"TransactionMetadata\".payload, 'hex') as action_tx_metadata,\n\tjson_agg(json_build_object(\n 'amount', \"AssetMint\".amount::text,\n 'policyId', encode(\"NativeAsset\".policy_id, 'hex'),\n 'assetName', encode(\"NativeAsset\".asset_name, 'hex')\n\t)) as \"payload!\"\nFROM \"AssetMint\"\n LEFT JOIN \"TransactionMetadata\" ON \"TransactionMetadata\".id = \"AssetMint\".tx_id\n JOIN \"NativeAsset\" ON \"NativeAsset\".id = \"AssetMint\".asset_id\n JOIN \"Transaction\" ON \"Transaction\".id = \"AssetMint\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE\n\t\"Transaction\".id > :after_tx_id! AND\n\t\"Transaction\".id <= :until_tx_id!\n AND \"NativeAsset\".policy_id IN :policy_ids!\nGROUP BY \"Transaction\".id, \"Block\".id, \"TransactionMetadata\".id\nORDER BY \"Transaction\".id ASC\nLIMIT :limit!"}; /** * Query generated from SQL: * ``` * SELECT - * "AssetMint".amount as amount, - * encode("NativeAsset".policy_id, 'hex') as policy_id, - * encode("NativeAsset".asset_name, 'hex') as asset_name, - * encode("Transaction".hash, 'hex') as action_tx_id, - * encode("Block".hash, 'hex') as action_block_id, - * CASE - * WHEN "TransactionMetadata".payload = NULL THEN NULL - * ELSE encode("TransactionMetadata".payload, 'hex') - * END AS action_tx_metadata, - * "Block".slot as action_slot + * ENCODE("Transaction".HASH, 'hex') "tx!", + * ENCODE("Block".HASH, 'hex') AS "block!", + * "Block".slot AS action_slot, + * ENCODE("TransactionMetadata".payload, 'hex') as action_tx_metadata, + * json_agg(json_build_object( + * 'amount', "AssetMint".amount::text, + * 'policyId', encode("NativeAsset".policy_id, 'hex'), + * 'assetName', encode("NativeAsset".asset_name, 'hex') + * )) as "payload!" * FROM "AssetMint" * LEFT JOIN "TransactionMetadata" ON "TransactionMetadata".id = "AssetMint".tx_id * JOIN "NativeAsset" ON "NativeAsset".id = "AssetMint".asset_id * JOIN "Transaction" ON "Transaction".id = "AssetMint".tx_id * JOIN "Block" ON "Transaction".block_id = "Block".id * WHERE - * "Block".slot > :min_slot! - * AND "Block".slot <= :max_slot! + * "Transaction".id > :after_tx_id! AND + * "Transaction".id <= :until_tx_id! * AND "NativeAsset".policy_id IN :policy_ids! - * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id + * ORDER BY "Transaction".id ASC + * LIMIT :limit! * ``` */ export const sqlMintBurnRangeByPolicyIds = new PreparedQuery(sqlMintBurnRangeByPolicyIdsIR); diff --git a/webserver/server/app/models/asset/mintBurnHistory.sql b/webserver/server/app/models/asset/mintBurnHistory.sql index 56a724bc..e913eabe 100644 --- a/webserver/server/app/models/asset/mintBurnHistory.sql +++ b/webserver/server/app/models/asset/mintBurnHistory.sql @@ -2,48 +2,50 @@ @name sqlMintBurnRange */ SELECT - "AssetMint".amount as amount, - encode("NativeAsset".policy_id, 'hex') as policy_id, - encode("NativeAsset".asset_name, 'hex') as asset_name, - encode("Transaction".hash, 'hex') as action_tx_id, - encode("Block".hash, 'hex') as action_block_id, - CASE - WHEN "TransactionMetadata".payload = NULL THEN NULL - ELSE encode("TransactionMetadata".payload, 'hex') - END AS action_tx_metadata, - "Block".slot as action_slot + ENCODE("Transaction".HASH, 'hex') "tx!", + ENCODE("Block".HASH, 'hex') AS "block!", + "Block".slot AS action_slot, + ENCODE("TransactionMetadata".payload, 'hex') as action_tx_metadata, + json_agg(json_build_object( + 'amount', "AssetMint".amount::text, + 'policyId', encode("NativeAsset".policy_id, 'hex'), + 'assetName', encode("NativeAsset".asset_name, 'hex') + )) as "payload!" FROM "AssetMint" LEFT JOIN "TransactionMetadata" ON "TransactionMetadata".id = "AssetMint".tx_id JOIN "NativeAsset" ON "NativeAsset".id = "AssetMint".asset_id JOIN "Transaction" ON "Transaction".id = "AssetMint".tx_id JOIN "Block" ON "Transaction".block_id = "Block".id WHERE - "Block".slot > :min_slot! - AND "Block".slot <= :max_slot! -ORDER BY ("Block".height, "Transaction".tx_index) ASC; + "Transaction".id > :after_tx_id! AND + "Transaction".id <= :until_tx_id! +GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id +ORDER BY "Transaction".id ASC +LIMIT :limit!; /* @name sqlMintBurnRangeByPolicyIds @param policy_ids -> (...) */ SELECT - "AssetMint".amount as amount, - encode("NativeAsset".policy_id, 'hex') as policy_id, - encode("NativeAsset".asset_name, 'hex') as asset_name, - encode("Transaction".hash, 'hex') as action_tx_id, - encode("Block".hash, 'hex') as action_block_id, - CASE - WHEN "TransactionMetadata".payload = NULL THEN NULL - ELSE encode("TransactionMetadata".payload, 'hex') - END AS action_tx_metadata, - "Block".slot as action_slot + ENCODE("Transaction".HASH, 'hex') "tx!", + ENCODE("Block".HASH, 'hex') AS "block!", + "Block".slot AS action_slot, + ENCODE("TransactionMetadata".payload, 'hex') as action_tx_metadata, + json_agg(json_build_object( + 'amount', "AssetMint".amount::text, + 'policyId', encode("NativeAsset".policy_id, 'hex'), + 'assetName', encode("NativeAsset".asset_name, 'hex') + )) as "payload!" FROM "AssetMint" LEFT JOIN "TransactionMetadata" ON "TransactionMetadata".id = "AssetMint".tx_id JOIN "NativeAsset" ON "NativeAsset".id = "AssetMint".asset_id JOIN "Transaction" ON "Transaction".id = "AssetMint".tx_id JOIN "Block" ON "Transaction".block_id = "Block".id WHERE - "Block".slot > :min_slot! - AND "Block".slot <= :max_slot! + "Transaction".id > :after_tx_id! AND + "Transaction".id <= :until_tx_id! AND "NativeAsset".policy_id IN :policy_ids! -ORDER BY ("Block".height, "Transaction".tx_index) ASC; +GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id +ORDER BY "Transaction".id ASC +LIMIT :limit!; \ No newline at end of file diff --git a/webserver/server/app/services/MintBurnHistoryService.ts b/webserver/server/app/services/MintBurnHistoryService.ts index 8c108e36..4a6c2d75 100644 --- a/webserver/server/app/services/MintBurnHistoryService.ts +++ b/webserver/server/app/services/MintBurnHistoryService.ts @@ -1,26 +1,44 @@ import type { PoolClient } from 'pg'; -import type { ISqlMintBurnRangeResult, ISqlMintBurnRangeByPolicyIdsResult } from '../models/asset/mintBurnHistory.queries'; -import { sqlMintBurnRange, sqlMintBurnRangeByPolicyIds } from '../models/asset/mintBurnHistory.queries'; -import type { PolicyId } from "../../../shared/models/PolicyIdAssetMap"; +import type { + ISqlMintBurnRangeResult, + ISqlMintBurnRangeByPolicyIdsResult, +} from '../models/asset/mintBurnHistory.queries'; +import { + sqlMintBurnRange, + sqlMintBurnRangeByPolicyIds, +} from '../models/asset/mintBurnHistory.queries'; +import type { PolicyId } from '../../../shared/models/PolicyIdAssetMap'; export async function mintBurnRange(request: { - range: { minSlot: number, maxSlot: number }, - dbTx: PoolClient, + after: number; + until: number; + limit: number; + dbTx: PoolClient; }): Promise { - return (await sqlMintBurnRange.run({ - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, - }, request.dbTx)); + return await sqlMintBurnRange.run( + { + after_tx_id: request.after, + until_tx_id: request.until, + limit: request.limit, + }, + request.dbTx + ); } export async function mintBurnRangeByPolicyIds(request: { - range: { minSlot: number, maxSlot: number }, - policyIds: PolicyId[], - dbTx: PoolClient, + after: number; + until: number; + limit: number; + policyIds: PolicyId[]; + dbTx: PoolClient; }): Promise { - return (await sqlMintBurnRangeByPolicyIds.run({ - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, - policy_ids: request.policyIds.map(id => Buffer.from(id, 'hex')), - }, request.dbTx)); + return await sqlMintBurnRangeByPolicyIds.run( + { + after_tx_id: request.after, + until_tx_id: request.until, + limit: request.limit, + policy_ids: request.policyIds.map(id => Buffer.from(id, 'hex')), + }, + request.dbTx + ); } diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index d92559b4..da409050 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -39,4 +39,8 @@ export const POOL_DELEGATION_LIMIT = { export const ASSET_UTXOS_LIMIT = { ASSETS: 50, DEFAULT_PAGE_SIZE: 50, +}; + +export const MINT_BURN_HISTORY_LIMIT = { + DEFAULT_PAGE_SIZE: 50, }; \ No newline at end of file diff --git a/webserver/shared/models/MintBurn.ts b/webserver/shared/models/MintBurn.ts index 8a442201..acf82c9c 100644 --- a/webserver/shared/models/MintBurn.ts +++ b/webserver/shared/models/MintBurn.ts @@ -1,62 +1,51 @@ -import {PolicyId} from "./PolicyIdAssetMap" -import {Amount} from "./common"; +import { SlotLimits } from "../../server/app/services/PaginationService"; +import { PolicyId } from "./PolicyIdAssetMap"; +import { Amount, Pagination } from "./common"; export type MintBurnHistoryRequest = { - /** - * Mint Burn 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 - }, - policyIds: PolicyId[] | undefined -}; + policyIds: PolicyId[] | undefined; + + /** This limits the transactions in the result to this range of slots. + * Everything else is filtered out */ + slotLimits?: SlotLimits; + + limit?: number; +} & Pagination; export type MintBurnSingleResponse = { - /** - * Slot at which the transaction happened - * - * @example 512345 - */ - actionSlot: number, - - /** - * Transaction id of related mint / burn event - * - * @pattern [0-9a-fA-F]{64} - * @example "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda" - */ - actionTxId: string, - - /** - * Block id of related mint / burn event - * - * @pattern [0-9a-fA-F]{64} - * @example "4e90f1d14ad742a1c0e094a89ad180b896068f93fc3969614b1c53bac547b374" - */ - actionBlockId: string, - - /** - * Transaction metadata of related mint / burn event - */ - metadata: string | null, - - /** - * Assets changed in a particular transaction - * - * @example { "b863bc7369f46136ac1048adb2fa7dae3af944c3bbb2be2f216a8d4f": { "42657272794e617679": "1" }} - */ - assets: { [policyId: string]: { [assetName: string]: Amount } }; + /** + * Assets changed in a particular transaction + * + * @example { "b863bc7369f46136ac1048adb2fa7dae3af944c3bbb2be2f216a8d4f": { "42657272794e617679": "1" }} + */ + assets: { [policyId: string]: { [assetName: string]: Amount } }; + + /** + * Slot at which the transaction happened + * + * @example 512345 + */ + actionSlot: number; + + /** + * Transaction metadata of related mint / burn event + */ + metadata: string | null; + + /** + * Transaction id of related mint / burn event + * + * @pattern [0-9a-fA-F]{64} + * @example "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda" + */ + txId: string; + /** + * Block id of related mint / burn event + * + * @pattern [0-9a-fA-F]{64} + * @example "4e90f1d14ad742a1c0e094a89ad180b896068f93fc3969614b1c53bac547b374" + */ + block: string; }; -export type MintBurnHistoryResponse = MintBurnSingleResponse[] \ No newline at end of file +export type MintBurnHistoryResponse = MintBurnSingleResponse[];