From fcadc70cddb667aa531f33c24b16c129dbd49506 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Thu, 11 Jan 2024 17:41:25 -0300 Subject: [PATCH] transaction history: add input addresses, metadata and slots filter update .nvmrc make input addresses and metadata optional (as an arg) reformat slotBoundsPagination.sql Co-authored-by: Sebastien Guillemot add more comments in the code update slotBOundsPagination.queries.ts after reformatting update .nvmrc to lts/hydrogen optimize slot filter tx filter query add support to lower slot limit as -1 to get the full range update openapi.json update tx_count change to cml multiera after rebase fix genesis_block task: missing tx_count assetUtxo and delegationForPool: paginate by tx,block refactor common slotLimits functionality migrated projected nft range endpoint to block,tx pagination update openapi.json add missing endpoint level docs also paginate mint burn history endpoint --- docs/bin/openapi.json | 552 ++++++++++-------- .../app/controllers/AssetUtxosController.ts | 113 +++- .../DelegationForAddressController.ts | 6 + .../DelegationForPoolController.ts | 87 ++- .../controllers/MintBurnHistoryController.ts | 199 +++---- .../ProjectedNftRangeController.ts | 304 ++++++---- .../TransactionHistoryController.ts | 28 +- .../app/models/asset/assetUtxos.queries.ts | 55 +- .../server/app/models/asset/assetUtxos.sql | 32 +- .../models/asset/mintBurnHistory.queries.ts | 88 +-- .../app/models/asset/mintBurnHistory.sql | 54 +- .../delegation/delegationsForPool.queries.ts | 41 +- .../models/delegation/delegationsForPool.sql | 25 +- .../projectedNftRange.queries.ts | 82 ++- .../projected_nft/projectedNftRange.sql | 58 +- .../projectedNftRangeByAddress.queries.ts | 85 ++- .../projectedNftRangeByAddress.sql | 61 +- webserver/server/app/services/AssetUtxos.ts | 12 +- .../server/app/services/DelegationForPool.ts | 26 +- .../app/services/MintBurnHistoryService.ts | 52 +- .../server/app/services/PaginationService.ts | 40 ++ .../server/app/services/ProjectedNftRange.ts | 18 +- webserver/shared/constants.ts | 10 +- webserver/shared/models/AssetUtxos.ts | 45 +- webserver/shared/models/DelegationForPool.ts | 20 +- webserver/shared/models/MintBurn.ts | 101 ++-- webserver/shared/models/ProjectedNftRange.ts | 79 ++- webserver/shared/models/TransactionHistory.ts | 1 + 28 files changed, 1296 insertions(+), 978 deletions(-) diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index bbd79a6f..b01cccc5 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -154,54 +154,69 @@ "AssetUtxosResponse": { "items": { "properties": { - "txId": { + "block": { "type": "string" }, - "slot": { - "type": "number", - "format": "double" - }, - "paymentCred": { - "type": "string" - }, - "assetName": { - "$ref": "#/components/schemas/AssetName" - }, - "policyId": { - "type": "string" - }, - "cip14Fingerprint": { + "txId": { "type": "string" }, - "utxo": { - "properties": { - "index": { - "type": "number", - "format": "double" + "payload": { + "items": { + "properties": { + "utxo": { + "properties": { + "index": { + "type": "number", + "format": "double" + }, + "tx": { + "type": "string" + } + }, + "required": [ + "index", + "tx" + ], + "type": "object" + }, + "slot": { + "type": "number", + "format": "double" + }, + "paymentCred": { + "type": "string" + }, + "assetName": { + "$ref": "#/components/schemas/AssetName" + }, + "policyId": { + "type": "string" + }, + "cip14Fingerprint": { + "type": "string" + }, + "amount": { + "type": "string", + "description": "If the utxo is created, this has the amount. It's undefined if the utxo\nis spent." + } }, - "tx": { - "type": "string" - } + "required": [ + "utxo", + "slot", + "paymentCred", + "assetName", + "policyId", + "cip14Fingerprint" + ], + "type": "object" }, - "required": [ - "index", - "tx" - ], - "type": "object" - }, - "amount": { - "type": "string", - "description": "If the utxo is created, this has the amount. It's undefined if the utxo\nis spent." + "type": "array" } }, "required": [ + "block", "txId", - "slot", - "paymentCred", - "assetName", - "policyId", - "cip14Fingerprint", - "utxo" + "payload" ], "type": "object" }, @@ -216,42 +231,54 @@ "example": "b863bc7369f46136ac1048adb2fa7dae3af944c3bbb2be2f216a8d4f", "pattern": "[0-9a-fA-F]{56}" }, - "AssetUtxosRequest": { + "SlotLimits": { "properties": { - "policyIds": { - "items": { - "$ref": "#/components/schemas/PolicyId" - }, - "type": "array" - }, - "fingerprints": { - "items": { - "$ref": "#/components/schemas/Cip14Fingerprint" - }, - "type": "array" + "to": { + "type": "number", + "format": "double" }, - "range": { + "from": { + "type": "number", + "format": "double" + } + }, + "required": [ + "to", + "from" + ], + "type": "object" + }, + "AssetUtxosRequest": { + "allOf": [ + { "properties": { - "maxSlot": { + "limit": { "type": "number", "format": "double" }, - "minSlot": { - "type": "number", - "format": "double" + "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" + }, + "fingerprints": { + "items": { + "$ref": "#/components/schemas/Cip14Fingerprint" + }, + "type": "array" } }, - "required": [ - "maxSlot", - "minSlot" - ], "type": "object" + }, + { + "$ref": "#/components/schemas/Pagination" } - }, - "required": [ - "range" - ], - "type": "object" + ] }, "BlockSubset": { "properties": { @@ -438,30 +465,45 @@ "DelegationForPoolResponse": { "items": { "properties": { - "slot": { - "type": "number", - "format": "double" + "block": { + "type": "string" }, "txId": { "type": "string" }, - "pool": { - "allOf": [ - { - "$ref": "#/components/schemas/PoolHex" - } - ], - "nullable": true - }, - "credential": { - "$ref": "#/components/schemas/Address" + "payload": { + "items": { + "properties": { + "slot": { + "type": "number", + "format": "double" + }, + "pool": { + "allOf": [ + { + "$ref": "#/components/schemas/PoolHex" + } + ], + "nullable": true + }, + "credential": { + "$ref": "#/components/schemas/Address" + } + }, + "required": [ + "slot", + "pool", + "credential" + ], + "type": "object" + }, + "type": "array" } }, "required": [ - "slot", + "block", "txId", - "pool", - "credential" + "payload" ], "type": "object" }, @@ -471,36 +513,33 @@ "$ref": "#/components/schemas/PoolHex" }, "DelegationForPoolRequest": { - "properties": { - "range": { + "allOf": [ + { "properties": { - "maxSlot": { + "limit": { "type": "number", "format": "double" }, - "minSlot": { - "type": "number", - "format": "double" + "slotLimits": { + "$ref": "#/components/schemas/SlotLimits", + "description": "This limits the transactions in the result to this range of slots.\nEverything else is filtered out" + }, + "pools": { + "items": { + "$ref": "#/components/schemas/Pool" + }, + "type": "array" } }, "required": [ - "maxSlot", - "minSlot" + "pools" ], "type": "object" }, - "pools": { - "items": { - "$ref": "#/components/schemas/Pool" - }, - "type": "array" + { + "$ref": "#/components/schemas/Pagination" } - }, - "required": [ - "range", - "pools" - ], - "type": "object" + ] }, "Asset": { "properties": { @@ -855,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": { @@ -871,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" }, @@ -912,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": [ @@ -959,131 +988,135 @@ "ProjectedNftRangeResponse": { "items": { "properties": { - "forHowLong": { - "type": "string", - "nullable": true, - "description": "UNIX timestamp till which the funds can't be claimed in the Unlocking state.\nIf the status is not Unlocking this is always null.", - "example": "1701266986000" - }, - "plutusDatum": { - "type": "string", - "description": "Projected NFT datum: serialized state of the Projected NFT", - "example": "d8799fd8799f581c9040f057461d9adc09108fe5cb630077cf75c6e981d3ed91f6fb18f6ffd87980ff", - "pattern": "[0-9a-fA-F]+" - }, - "status": { - "$ref": "#/components/schemas/ProjectedNftStatus", - "description": "Projected NFT status: Lock / Unlocking / Claim / Invalid", - "example": "Lock" - }, - "amount": { - "type": "string", - "description": "Number of assets of `asset` type used in this Projected NFT event.", - "example": "1" - }, - "assetName": { - "type": "string", - "description": "Asset name that relates to Projected NFT event", - "example": "415045", - "pattern": "([0-9a-fA-F]{2}){0,32}" - }, - "policyId": { - "type": "string", - "description": "Asset policy id that relates to Projected NFT event", - "example": "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278", - "pattern": "[0-9a-fA-F]{56}" - }, - "previousTxOutputIndex": { - "type": "number", - "format": "double", - "nullable": true, - "description": "Output index of related previous Projected NFT event. Null if event has `status` Lock.", - "example": 1 - }, - "previousTxHash": { - "type": "string", - "nullable": true, - "description": "Transaction id of related previous Projected NFT event.\nE.g. you locked the NFT and get unlocking event: you will see previousTxHash = transaction hash of lock event.\nNull if event has `status` Lock.", - "example": "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda", - "pattern": "[0-9a-fA-F]{64}" - }, - "actionOutputIndex": { - "type": "number", - "format": "double", - "nullable": true, - "description": "Output index of related Projected NFT event. Null if it is claim event (No new UTxO is created).", - "example": 1 - }, - "actionTxId": { + "txId": { "type": "string", "description": "Transaction id of related Projected NFT event", "example": "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda", "pattern": "[0-9a-fA-F]{64}" }, - "ownerAddress": { - "type": "string", - "nullable": true, - "description": "Projected NFT owner address. Not null only if owned by Public Key Hash.", - "example": "9040f057461d9adc09108fe5cb630077cf75c6e981d3ed91f6fb18f6", - "pattern": "[0-9a-fA-F]+" + "block": { + "type": "string" }, - "actionSlot": { - "type": "number", - "format": "double", - "description": "Slot at which the transaction happened", - "example": 512345 + "payload": { + "items": { + "properties": { + "forHowLong": { + "type": "string", + "nullable": true, + "description": "UNIX timestamp till which the funds can't be claimed in the Unlocking state.\nIf the status is not Unlocking this is always null.", + "example": "1701266986000" + }, + "plutusDatum": { + "type": "string", + "description": "Projected NFT datum: serialized state of the Projected NFT", + "example": "d8799fd8799f581c9040f057461d9adc09108fe5cb630077cf75c6e981d3ed91f6fb18f6ffd87980ff", + "pattern": "[0-9a-fA-F]+" + }, + "status": { + "$ref": "#/components/schemas/ProjectedNftStatus", + "description": "Projected NFT status: Lock / Unlocking / Claim / Invalid", + "example": "Lock" + }, + "amount": { + "type": "string", + "description": "Number of assets of `asset` type used in this Projected NFT event.", + "example": "1" + }, + "assetName": { + "type": "string", + "description": "Asset name that relates to Projected NFT event", + "example": "415045", + "pattern": "([0-9a-fA-F]{2}){0,32}" + }, + "policyId": { + "type": "string", + "description": "Asset policy id that relates to Projected NFT event", + "example": "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278", + "pattern": "[0-9a-fA-F]{56}" + }, + "previousTxOutputIndex": { + "type": "number", + "format": "double", + "nullable": true, + "description": "Output index of related previous Projected NFT event. Null if event has `status` Lock.", + "example": 1 + }, + "previousTxHash": { + "type": "string", + "nullable": true, + "description": "Transaction id of related previous Projected NFT event.\nE.g. you locked the NFT and get unlocking event: you will see previousTxHash = transaction hash of lock event.\nNull if event has `status` Lock.", + "example": "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda", + "pattern": "[0-9a-fA-F]{64}" + }, + "actionOutputIndex": { + "type": "number", + "format": "double", + "nullable": true, + "description": "Output index of related Projected NFT event. Null if it is claim event (No new UTxO is created).", + "example": 1 + }, + "ownerAddress": { + "type": "string", + "nullable": true, + "description": "Projected NFT owner address. Not null only if owned by Public Key Hash.", + "example": "9040f057461d9adc09108fe5cb630077cf75c6e981d3ed91f6fb18f6", + "pattern": "[0-9a-fA-F]+" + }, + "actionSlot": { + "type": "number", + "format": "double", + "description": "Slot at which the transaction happened", + "example": 512345 + } + }, + "required": [ + "forHowLong", + "plutusDatum", + "status", + "amount", + "assetName", + "policyId", + "previousTxOutputIndex", + "previousTxHash", + "actionOutputIndex", + "ownerAddress", + "actionSlot" + ], + "type": "object" + }, + "type": "array" } }, "required": [ - "forHowLong", - "plutusDatum", - "status", - "amount", - "assetName", - "policyId", - "previousTxOutputIndex", - "previousTxHash", - "actionOutputIndex", - "actionTxId", - "ownerAddress", - "actionSlot" + "txId", + "block", + "payload" ], "type": "object" }, "type": "array" }, "ProjectedNftRangeRequest": { - "properties": { - "address": { - "type": "string" - }, - "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" + }, + "address": { + "type": "string" } }, - "required": [ - "maxSlot", - "minSlot" - ], - "type": "object", - "description": "Projected NFT events in this slot range will be returned" + "type": "object" + }, + { + "$ref": "#/components/schemas/Pagination" } - }, - "required": [ - "range" - ], - "type": "object" + ] }, "BlockInfo": { "allOf": [ @@ -1430,6 +1463,7 @@ } } }, + "description": "Returns utxo entries filtered either by cip 14 fingerprint or by policy id.\n\nThis is useful to keep track of the utxo set of a particular asset.", "security": [], "parameters": [], "requestBody": { @@ -1589,6 +1623,7 @@ } } }, + "description": "Returns the pool of the last delegation for this address.\n\nNote: the tx can be in the current epoch, so the delegation may not be in\neffect yet.", "security": [], "parameters": [], "requestBody": { @@ -1648,6 +1683,7 @@ } } }, + "description": "Returns the list of delegations for the provided pools. The pool field in\nthe response will be null when the address was previously delegating to a\npool in the input, but now the delegation is moved to a pool outside the\nlist, or when the staking key is unregistered.\n\nThis is useful to keep track of the delegators for a particular pool.", "security": [], "parameters": [], "requestBody": { diff --git a/webserver/server/app/controllers/AssetUtxosController.ts b/webserver/server/app/controllers/AssetUtxosController.ts index 61dca483..81faeab1 100644 --- a/webserver/server/app/controllers/AssetUtxosController.ts +++ b/webserver/server/app/controllers/AssetUtxosController.ts @@ -11,11 +11,23 @@ import type { IAssetUtxosResult } from '../models/asset/assetUtxos.queries'; import { bech32 } from 'bech32'; import { ASSET_UTXOS_LIMIT } from '../../../shared/constants'; import { Address } from '@dcspark/cardano-multiplatform-lib-nodejs'; +import { + adjustToSlotLimits, + resolvePageStart, + resolveUntilTransaction, +} from '../services/PaginationService'; +import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries'; +import { expectType } from 'tsd'; const route = Routes.assetUtxos; @Route('asset/utxos') export class AssetUtxosController extends Controller { + /** + * Returns utxo entries filtered either by cip 14 fingerprint or by policy id. + * + * This is useful to keep track of the utxo set of a particular asset. + */ @SuccessResponse(`${StatusCodes.OK}`) @Post() public async assetUtxos( @@ -40,9 +52,50 @@ export class AssetUtxosController extends Controller { ); } - const response = await tx(pool, async dbTx => { + 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, + }); + } + + const pageStartWithSlot = adjustToSlotLimits( + pageStart, + until, + requestBody.slotLimits, + slotBounds + ); + const data = await getAssetUtxos({ - range: requestBody.range, + after: pageStartWithSlot?.tx_id || 0, + until: until.tx_id, + limit: requestBody.limit || ASSET_UTXOS_LIMIT.DEFAULT_PAGE_SIZE, fingerprints: requestBody.fingerprints?.map(asset => { const decoded = bech32.decode(asset); const payload = bech32.fromWords(decoded.words); @@ -53,31 +106,47 @@ export class AssetUtxosController extends Controller { dbTx, }); - return data.map((data: IAssetUtxosResult): AssetUtxosResponse[0] => { - const address = Address.from_bytes(Uint8Array.from(data.address_raw)); + return data.map((data: IAssetUtxosResult) => { + return { + txId: data.tx as string, + block: data.block, + payload: (data.payload as { [key: string]: string | number }[]).map(x => { + const address = Address.from_bytes( + Uint8Array.from(Buffer.from(x.addressRaw as string, 'hex')) + ); - const paymentCred = address.payment_cred(); - const addressBytes = paymentCred?.to_bytes(); + const paymentCred = address.payment_cred(); + const addressBytes = paymentCred?.to_bytes(); - address.free(); - paymentCred?.free(); + address.free(); + paymentCred?.free(); - return { - txId: data.tx_hash as string, - utxo: { - index: data.output_index, - tx: data.output_tx_hash as string, - }, - paymentCred: Buffer.from(addressBytes as Uint8Array).toString('hex'), - amount: data.amount ? data.amount : undefined, - slot: data.slot, - cip14Fingerprint: bech32.encode('asset', bech32.toWords(data.cip14_fingerprint)), - policyId: Buffer.from(data.policy_id).toString('hex'), - assetName: Buffer.from(data.asset_name).toString('hex'), - }; + return { + utxo: { + index: x.outputIndex, + tx: x.outputTxHash, + }, + paymentCred: Buffer.from(addressBytes as Uint8Array).toString('hex'), + amount: x.amount ? x.amount : undefined, + slot: x.slot, + cip14Fingerprint: bech32.encode( + 'asset', + bech32.toWords(Buffer.from(x.cip14Fingerprint as string, 'hex')) + ), + policyId: x.policyId, + assetName: x.assetName, + }; + }), + } as AssetUtxosResponse[0]; }); }); + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, response); + } + return response; } -} +} \ No newline at end of file diff --git a/webserver/server/app/controllers/DelegationForAddressController.ts b/webserver/server/app/controllers/DelegationForAddressController.ts index b12d3744..4fbd8aed 100644 --- a/webserver/server/app/controllers/DelegationForAddressController.ts +++ b/webserver/server/app/controllers/DelegationForAddressController.ts @@ -15,6 +15,12 @@ const route = Routes.delegationForAddress; @Route('delegation/address') export class DelegationForAddressController extends Controller { + /** + * Returns the pool of the last delegation for this address. + * + * Note: the tx can be in the current epoch, so the delegation may not be in + * effect yet. + */ @SuccessResponse(`${StatusCodes.OK}`) @Post() public async delegationForAddress( diff --git a/webserver/server/app/controllers/DelegationForPoolController.ts b/webserver/server/app/controllers/DelegationForPoolController.ts index 8a1074c2..7379efb3 100644 --- a/webserver/server/app/controllers/DelegationForPoolController.ts +++ b/webserver/server/app/controllers/DelegationForPoolController.ts @@ -8,11 +8,26 @@ 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 { + adjustToSlotLimits, + resolvePageStart, + resolveUntilTransaction, +} from '../services/PaginationService'; +import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries'; +import { expectType } from 'tsd'; const route = Routes.delegationForPool; @Route('delegation/pool') export class DelegationForPoolController extends Controller { + /** + * Returns the list of delegations for the provided pools. The pool field in + * the response will be null when the address was previously delegating to a + * pool in the input, but now the delegation is moved to a pool outside the + * list, or when the staking key is unregistered. + * + * This is useful to keep track of the delegators for a particular pool. + */ @SuccessResponse(`${StatusCodes.OK}`) @Post() public async delegationForPool( @@ -35,33 +50,69 @@ export class DelegationForPoolController extends Controller { ); } - const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; - if (slotRangeSize > POOL_DELEGATION_LIMIT.SLOT_RANGE) { - // 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, - }) + // 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, + }); + } + + const pageStartWithSlot = adjustToSlotLimits( + pageStart, + until, + requestBody.slotLimits, + slotBounds ); - } - const response = await tx(pool, async dbTx => { - const data = await delegationsForPool({ + const response = await delegationsForPool({ pools: requestBody.pools.map(poolId => Buffer.from(poolId, 'hex')), - range: requestBody.range, + after: pageStartWithSlot?.tx_id || 0, + until: until.tx_id, + limit: requestBody.limit || POOL_DELEGATION_LIMIT.DEFAULT_PAGE_SIZE, dbTx, }); - return data.map(data => ({ - credential: data.credential, - pool: data.pool, - txId: data.tx_id, - slot: data.slot, + return response.map(x => ({ + txId: x.tx_id, + block: x.block, + payload: x.payload as DelegationForPoolResponse[0]['payload'], })); }); + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, response); + } + return response; } } 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/controllers/ProjectedNftRangeController.ts b/webserver/server/app/controllers/ProjectedNftRangeController.ts index 3e68adf1..320327f1 100644 --- a/webserver/server/app/controllers/ProjectedNftRangeController.ts +++ b/webserver/server/app/controllers/ProjectedNftRangeController.ts @@ -6,118 +6,220 @@ 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 {PROJECTED_NFT_LIMIT} from "../../../shared/constants"; -import {Errors, genErrorMessage} from "../../../shared/errors"; +import type { + ProjectedNftRangeResponse, + ProjectedNftStatus, +} from '../../../shared/models/ProjectedNftRange'; +import { PROJECTED_NFT_LIMIT } from '../../../shared/constants'; +import { Errors, genErrorMessage } from '../../../shared/errors'; +import { + adjustToSlotLimits, + resolvePageStart, + resolveUntilTransaction, +} from '../services/PaginationService'; +import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries'; +import { expectType } from 'tsd'; const route = Routes.projectedNftEventsRange; @Route('projected-nft/range') export class ProjectedNftRangeController extends Controller { - /** - * Query any [projected NFT](https://github.com/dcSpark/projected-nft-whirlpool). - */ - @SuccessResponse(`${StatusCodes.OK}`) - @Post() - public async projectedNftRange( - @Body() - requestBody: EndpointTypes[typeof route]['input'], - @Res() - errorResponse: TsoaResponse< - StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, - ErrorShape - > - ): Promise { - const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; - - 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, - }) - ); - } - - 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, - }) - ); - } - - return await this.handle_general_query(requestBody); - } + /** + * Query any [projected NFT](https://github.com/dcSpark/projected-nft-whirlpool). + */ + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async projectedNftRange( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + if (requestBody.address !== undefined) { + return await this.handle_by_address_query(requestBody.address, requestBody, errorResponse); + } else { + return await this.handle_general_query(requestBody, errorResponse); } + } - async handle_general_query( - requestBody: EndpointTypes[typeof route]['input'], - ): Promise { - const response = await tx< - ProjectedNftRangeResponse - >(pool, async dbTx => { - const data = await projectedNftRange({ - 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, - policyId: data.policy_id, - assetName: data.asset_name, - amount: data.amount, - status: data.status as ProjectedNftStatus, - plutusDatum: data.plutus_datum, - actionSlot: data.action_slot, - forHowLong: data.for_how_long, - })); + async handle_general_query( + requestBody: EndpointTypes[typeof route]['input'], + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + 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, }); + } + + const pageStartWithSlot = adjustToSlotLimits( + pageStart, + until, + requestBody.slotLimits, + slotBounds + ); - return response; + const data = await projectedNftRange({ + after: pageStartWithSlot?.tx_id || 0, + until: until.tx_id, + limit: requestBody.limit || PROJECTED_NFT_LIMIT.DEFAULT_PAGE_SIZE, + dbTx, + }); + + return data.map(data => ({ + block: data.block, + txId: data.tx_id, + payload: ( + (data.payload as ({ [key: string]: string } & { + actionSlot: number; + actionOutputIndex: number; + })[]) || [] + ).map(data => ({ + ownerAddress: data.ownerAddress, + previousTxHash: data.previousUtxoTxHash, + previousTxOutputIndex: + data.previousTxOutputIndex != null ? parseInt(data.previousTxOutputIndex) : null, + actionOutputIndex: data.actionOutputIndex, + policyId: data.policyId, + assetName: data.assetName, + amount: data.amount, + status: data.status as ProjectedNftStatus, + plutusDatum: data.plutusDatum, + actionSlot: data.actionSlot, + forHowLong: data.forHowLong, + })), + })); + }); + + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, 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, - policyId: data.policy_id, - assetName: data.asset_name, - amount: data.amount, - status: data.status as ProjectedNftStatus, - plutusDatum: data.plutus_datum, - actionSlot: data.action_slot, - forHowLong: data.for_how_long, - })); + return response; + } + + async handle_by_address_query( + address: string, + requestBody: EndpointTypes[typeof route]['input'], + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + 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, }); + } + + const pageStartWithSlot = adjustToSlotLimits( + pageStart, + until, + requestBody.slotLimits, + slotBounds + ); - return response; + const data = await projectedNftRangeByAddress({ + address: address, + after: pageStartWithSlot?.tx_id || 0, + until: until.tx_id, + limit: requestBody.limit || PROJECTED_NFT_LIMIT.DEFAULT_PAGE_SIZE, + dbTx, + }); + + return data.map(data => ({ + block: data.block, + txId: data.tx_id, + payload: ( + (data.payload as ({ [key: string]: string } & { + actionSlot: number; + actionOutputIndex: number; + })[]) || [] + ).map(data => ({ + ownerAddress: data.ownerAddress, + previousTxHash: data.previousUtxoTxHash, + previousTxOutputIndex: + data.previousTxOutputIndex != null ? parseInt(data.previousTxOutputIndex) : null, + actionOutputIndex: data.actionOutputIndex, + policyId: data.policyId, + assetName: data.assetName, + amount: data.amount, + status: data.status as ProjectedNftStatus, + plutusDatum: data.plutusDatum, + actionSlot: data.actionSlot, + forHowLong: data.forHowLong, + })), + })); + }); + + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, response); } -} \ No newline at end of file + + return response; + } +} diff --git a/webserver/server/app/controllers/TransactionHistoryController.ts b/webserver/server/app/controllers/TransactionHistoryController.ts index 33f4297e..90ee1015 100644 --- a/webserver/server/app/controllers/TransactionHistoryController.ts +++ b/webserver/server/app/controllers/TransactionHistoryController.ts @@ -5,7 +5,7 @@ import type { TransactionHistoryResponse } from '../../../shared/models/Transact import { ADDRESS_LIMIT } from '../../../shared/constants'; import tx from 'pg-tx'; import pool from '../services/PgPoolSingleton'; -import { resolvePageStart, resolveUntilTransaction } from '../services/PaginationService'; +import { adjustToSlotLimits, resolvePageStart, resolveUntilTransaction } from '../services/PaginationService'; import type { ErrorShape } from '../../../shared/errors'; import { genErrorMessage } from '../../../shared/errors'; import { Errors } from '../../../shared/errors'; @@ -96,31 +96,7 @@ export class TransactionHistoryController extends Controller { }); } - let pageStartWithSlot = pageStart; - - // if the slotLimits field is set, this shrinks the tx id range - // accordingly if necessary. - if (requestBody.slotLimits) { - const bounds = slotBounds ? slotBounds[0] : { min_tx_id: -1, max_tx_id: -2 }; - - const minTxId = Number(bounds.min_tx_id); - - if (!pageStartWithSlot) { - pageStartWithSlot = { - // block_id is not really used by this query. - block_id: -1, - // if no *after* argument is provided, this starts the pagination - // from the corresponding slot. This allows skipping slots you are - // not interested in. If there is also no slotLimits specified this - // starts from the first tx because of the default of -1. - tx_id: minTxId, - }; - } else { - pageStartWithSlot.tx_id = Math.max(Number(bounds.min_tx_id), pageStartWithSlot.tx_id); - } - - until.tx_id = Math.min(until.tx_id, Number(bounds.max_tx_id)); - } + const pageStartWithSlot = adjustToSlotLimits(pageStart, until, requestBody.slotLimits, slotBounds); const commonRequest = { after: pageStartWithSlot, diff --git a/webserver/server/app/models/asset/assetUtxos.queries.ts b/webserver/server/app/models/asset/assetUtxos.queries.ts index 6ef27732..cb386862 100644 --- a/webserver/server/app/models/asset/assetUtxos.queries.ts +++ b/webserver/server/app/models/asset/assetUtxos.queries.ts @@ -1,25 +1,24 @@ /** Types generated for queries found in "app/models/asset/assetUtxos.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +export type NumberOrString = number | string; + /** 'AssetUtxos' parameters type */ export interface IAssetUtxosParams { + after_tx_id: NumberOrString; fingerprints: readonly (Buffer | null | void)[]; - max_slot: number; - min_slot: number; + limit: NumberOrString; policyIds: readonly (Buffer | null | void)[]; + until_tx_id: NumberOrString; } /** 'AssetUtxos' return type */ export interface IAssetUtxosResult { - address_raw: Buffer; - amount: string | null; - asset_name: Buffer; - cip14_fingerprint: Buffer; - output_index: number; - output_tx_hash: string | null; - policy_id: Buffer; - slot: number; - tx_hash: string | null; + block: string; + payload: Json; + tx: string | null; } /** 'AssetUtxos' query type */ @@ -28,22 +27,24 @@ export interface IAssetUtxosQuery { result: IAssetUtxosResult; } -const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"policyIds":true,"min_slot":true,"max_slot":true},"params":[{"name":"fingerprints","required":false,"transform":{"type":"array_spread"},"locs":[{"a":731,"b":743}]},{"name":"policyIds","required":false,"transform":{"type":"array_spread"},"locs":[{"a":777,"b":786}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":811,"b":820}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":843,"b":852}]}],"statement":"SELECT ENCODE(TXO.HASH,\n 'hex') OUTPUT_TX_HASH,\n \"TransactionOutput\".OUTPUT_INDEX,\n\t\"NativeAsset\".CIP14_FINGERPRINT,\n\t\"NativeAsset\".POLICY_ID,\n\t\"NativeAsset\".ASSET_NAME,\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\n\t\tOR \"NativeAsset\".POLICY_ID IN :policyIds\n\t) AND\n\t\"Block\".SLOT > :min_slot! AND\n\t\"Block\".SLOT <= :max_slot!\nORDER BY \"Transaction\".ID, \"AssetUtxo\".ID ASC"}; +const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"policyIds":true,"after_tx_id":true,"until_tx_id":true,"limit":true},"params":[{"name":"fingerprints","required":false,"transform":{"type":"array_spread"},"locs":[{"a":946,"b":958}]},{"name":"policyIds","required":false,"transform":{"type":"array_spread"},"locs":[{"a":992,"b":1001}]},{"name":"after_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":1030,"b":1042}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":1069,"b":1081}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":1159,"b":1165}]}],"statement":"SELECT\n\tENCODE(\"Transaction\".HASH, 'hex') TX,\n\tENCODE(\"Block\".HASH, 'hex') AS \"block!\",\n\tjson_agg(json_build_object(\n\t\t'outputIndex', \"TransactionOutput\".OUTPUT_INDEX,\n\t\t'outputTxHash', ENCODE(TXO.HASH, 'hex'),\n\t\t'cip14Fingerprint', ENCODE(\"NativeAsset\".CIP14_FINGERPRINT, 'hex'),\n\t\t'policyId', ENCODE(\"NativeAsset\".POLICY_ID, 'hex'),\n\t\t'assetName', ENCODE(\"NativeAsset\".ASSET_NAME, 'hex'),\n\t\t'amount', \"AssetUtxo\".AMOUNT,\n\t\t'slot', \"Block\".SLOT,\n\t\t'addressRaw', ENCODE(\"Address\".PAYLOAD, 'hex')\n\t)) as \"payload!\"\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\n\t\tOR \"NativeAsset\".POLICY_ID IN :policyIds\n\t) AND\n\t\"Transaction\".id > :after_tx_id! AND\n\t\"Transaction\".id <= :until_tx_id!\nGROUP BY (\"Block\".ID, \"Transaction\".ID)\nORDER BY \"Transaction\".ID ASC\nLIMIT :limit!"}; /** * Query generated from SQL: * ``` - * SELECT ENCODE(TXO.HASH, - * 'hex') OUTPUT_TX_HASH, - * "TransactionOutput".OUTPUT_INDEX, - * "NativeAsset".CIP14_FINGERPRINT, - * "NativeAsset".POLICY_ID, - * "NativeAsset".ASSET_NAME, - * "AssetUtxo".AMOUNT, - * "Block".SLOT, - * ENCODE("Transaction".HASH, - * 'hex') TX_HASH, - * "Address".PAYLOAD ADDRESS_RAW + * SELECT + * ENCODE("Transaction".HASH, 'hex') TX, + * ENCODE("Block".HASH, 'hex') AS "block!", + * json_agg(json_build_object( + * 'outputIndex', "TransactionOutput".OUTPUT_INDEX, + * 'outputTxHash', ENCODE(TXO.HASH, 'hex'), + * 'cip14Fingerprint', ENCODE("NativeAsset".CIP14_FINGERPRINT, 'hex'), + * 'policyId', ENCODE("NativeAsset".POLICY_ID, 'hex'), + * 'assetName', ENCODE("NativeAsset".ASSET_NAME, 'hex'), + * 'amount', "AssetUtxo".AMOUNT, + * 'slot', "Block".SLOT, + * 'addressRaw', ENCODE("Address".PAYLOAD, 'hex') + * )) as "payload!" * FROM "AssetUtxo" * JOIN "Transaction" ON "AssetUtxo".TX_ID = "Transaction".ID * JOIN "TransactionOutput" ON "AssetUtxo".UTXO_ID = "TransactionOutput".ID @@ -55,9 +56,11 @@ const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"policyIds":true, * ("NativeAsset".CIP14_FINGERPRINT IN :fingerprints * OR "NativeAsset".POLICY_ID IN :policyIds * ) AND - * "Block".SLOT > :min_slot! AND - * "Block".SLOT <= :max_slot! - * ORDER BY "Transaction".ID, "AssetUtxo".ID ASC + * "Transaction".id > :after_tx_id! AND + * "Transaction".id <= :until_tx_id! + * GROUP BY ("Block".ID, "Transaction".ID) + * ORDER BY "Transaction".ID ASC + * LIMIT :limit! * ``` */ export const assetUtxos = new PreparedQuery(assetUtxosIR); diff --git a/webserver/server/app/models/asset/assetUtxos.sql b/webserver/server/app/models/asset/assetUtxos.sql index 796bce42..1fb28c69 100644 --- a/webserver/server/app/models/asset/assetUtxos.sql +++ b/webserver/server/app/models/asset/assetUtxos.sql @@ -3,17 +3,19 @@ @param fingerprints -> (...) @param policyIds -> (...) */ -SELECT ENCODE(TXO.HASH, - 'hex') OUTPUT_TX_HASH, - "TransactionOutput".OUTPUT_INDEX, - "NativeAsset".CIP14_FINGERPRINT, - "NativeAsset".POLICY_ID, - "NativeAsset".ASSET_NAME, - "AssetUtxo".AMOUNT, - "Block".SLOT, - ENCODE("Transaction".HASH, - 'hex') TX_HASH, - "Address".PAYLOAD ADDRESS_RAW +SELECT + ENCODE("Transaction".HASH, 'hex') TX, + ENCODE("Block".HASH, 'hex') AS "block!", + json_agg(json_build_object( + 'outputIndex', "TransactionOutput".OUTPUT_INDEX, + 'outputTxHash', ENCODE(TXO.HASH, 'hex'), + 'cip14Fingerprint', ENCODE("NativeAsset".CIP14_FINGERPRINT, 'hex'), + 'policyId', ENCODE("NativeAsset".POLICY_ID, 'hex'), + 'assetName', ENCODE("NativeAsset".ASSET_NAME, 'hex'), + 'amount', "AssetUtxo".AMOUNT, + 'slot', "Block".SLOT, + 'addressRaw', ENCODE("Address".PAYLOAD, 'hex') + )) as "payload!" FROM "AssetUtxo" JOIN "Transaction" ON "AssetUtxo".TX_ID = "Transaction".ID JOIN "TransactionOutput" ON "AssetUtxo".UTXO_ID = "TransactionOutput".ID @@ -25,6 +27,8 @@ WHERE ("NativeAsset".CIP14_FINGERPRINT IN :fingerprints OR "NativeAsset".POLICY_ID IN :policyIds ) AND - "Block".SLOT > :min_slot! AND - "Block".SLOT <= :max_slot! -ORDER BY "Transaction".ID, "AssetUtxo".ID ASC; + "Transaction".id > :after_tx_id! AND + "Transaction".id <= :until_tx_id! +GROUP BY ("Block".ID, "Transaction".ID) +ORDER BY "Transaction".ID ASC +LIMIT :limit!; 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/models/delegation/delegationsForPool.queries.ts b/webserver/server/app/models/delegation/delegationsForPool.queries.ts index bbe2d7dc..7aa9c968 100644 --- a/webserver/server/app/models/delegation/delegationsForPool.queries.ts +++ b/webserver/server/app/models/delegation/delegationsForPool.queries.ts @@ -1,18 +1,22 @@ /** Types generated for queries found in "app/models/delegation/delegationsForPool.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +export type NumberOrString = number | string; + /** 'SqlStakeDelegationByPool' parameters type */ export interface ISqlStakeDelegationByPoolParams { - max_slot: number; - min_slot: number; + after_tx_id: NumberOrString; + limit: NumberOrString; pools: readonly (Buffer)[]; + until_tx_id: NumberOrString; } /** 'SqlStakeDelegationByPool' return type */ export interface ISqlStakeDelegationByPoolResult { - credential: string; - pool: string | null; - slot: number; + block: string; + payload: Json; tx_id: string; } @@ -22,19 +26,24 @@ 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":182,"b":188},{"a":604,"b":610},{"a":671,"b":677}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":702,"b":711}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":734,"b":743}]}],"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! \n\t\tTHEN encode(\"StakeDelegationCredentialRelation\".pool_credential, 'hex') \n\t\tELSE NULL\n\t\tEND 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,"after_tx_id":true,"until_tx_id":true,"limit":true},"params":[{"name":"pools","required":true,"transform":{"type":"array_spread"},"locs":[{"a":272,"b":278},{"a":708,"b":714},{"a":775,"b":781}]},{"name":"after_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":810,"b":822}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":849,"b":861}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":941,"b":947}]}],"statement":"SELECT \n\tencode(\"Transaction\".hash, 'hex') as \"tx_id!\",\n\tencode(\"Block\".hash, 'hex') as \"block!\",\n\tjson_agg(json_build_object(\n\t\t'credential', encode(credential, 'hex'),\n\t\t'slot', \"Block\".slot,\n\t\t'pool',\n\t\t\tCASE WHEN \"StakeDelegationCredentialRelation\".pool_credential IN :pools!\n\t\t\tTHEN encode(\"StakeDelegationCredentialRelation\".pool_credential, 'hex')\n\t\t\tELSE NULL\n\t\t\tEND\n\t\t)\n\t) as \"payload!\"\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\"Transaction\".id > :after_tx_id! AND\n\t\"Transaction\".id <= :until_tx_id!\nGROUP BY (\"Block\".hash, \"Transaction\".id)\nORDER BY \"Transaction\".id ASC\nLIMIT :limit!"}; /** * Query generated from SQL: * ``` * SELECT - * encode(credential, 'hex') as "credential!", * encode("Transaction".hash, 'hex') as "tx_id!", - * "Block".slot, - * CASE WHEN "StakeDelegationCredentialRelation".pool_credential IN :pools! - * THEN encode("StakeDelegationCredentialRelation".pool_credential, 'hex') - * ELSE NULL - * END AS pool + * encode("Block".hash, 'hex') as "block!", + * json_agg(json_build_object( + * 'credential', encode(credential, 'hex'), + * 'slot', "Block".slot, + * 'pool', + * CASE WHEN "StakeDelegationCredentialRelation".pool_credential IN :pools! + * THEN encode("StakeDelegationCredentialRelation".pool_credential, 'hex') + * ELSE NULL + * END + * ) + * ) as "payload!" * FROM "StakeDelegationCredentialRelation" * JOIN "StakeCredential" ON stake_credential = "StakeCredential".id * JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id @@ -44,9 +53,11 @@ const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot" * "StakeDelegationCredentialRelation".pool_credential IN :pools! OR * "StakeDelegationCredentialRelation".previous_pool IN :pools! * ) AND - * "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 ("Block".hash, "Transaction".id) + * ORDER BY "Transaction".id ASC + * LIMIT :limit! * ``` */ export const sqlStakeDelegationByPool = new PreparedQuery(sqlStakeDelegationByPoolIR); diff --git a/webserver/server/app/models/delegation/delegationsForPool.sql b/webserver/server/app/models/delegation/delegationsForPool.sql index 0ddebaa8..6a32c3fd 100644 --- a/webserver/server/app/models/delegation/delegationsForPool.sql +++ b/webserver/server/app/models/delegation/delegationsForPool.sql @@ -3,13 +3,18 @@ @param pools -> (...) */ SELECT - encode(credential, 'hex') as "credential!", encode("Transaction".hash, 'hex') as "tx_id!", - "Block".slot, - CASE WHEN "StakeDelegationCredentialRelation".pool_credential IN :pools! - THEN encode("StakeDelegationCredentialRelation".pool_credential, 'hex') - ELSE NULL - END AS pool + encode("Block".hash, 'hex') as "block!", + json_agg(json_build_object( + 'credential', encode(credential, 'hex'), + 'slot', "Block".slot, + 'pool', + CASE WHEN "StakeDelegationCredentialRelation".pool_credential IN :pools! + THEN encode("StakeDelegationCredentialRelation".pool_credential, 'hex') + ELSE NULL + END + ) + ) as "payload!" FROM "StakeDelegationCredentialRelation" JOIN "StakeCredential" ON stake_credential = "StakeCredential".id JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id @@ -19,6 +24,8 @@ WHERE "StakeDelegationCredentialRelation".pool_credential IN :pools! OR "StakeDelegationCredentialRelation".previous_pool IN :pools! ) AND - "Block".slot > :min_slot! AND - "Block".slot <= :max_slot! -ORDER BY ("Block".height, "Transaction".tx_index) ASC; \ No newline at end of file + "Transaction".id > :after_tx_id! AND + "Transaction".id <= :until_tx_id! +GROUP BY ("Block".hash, "Transaction".id) +ORDER BY "Transaction".id ASC +LIMIT :limit!; \ 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 ca38b083..f3e712b1 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts +++ b/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts @@ -1,26 +1,22 @@ /** Types generated for queries found in "app/models/projected_nft/projectedNftRange.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +export type NumberOrString = number | string; + /** 'SqlProjectedNftRange' parameters type */ export interface ISqlProjectedNftRangeParams { - max_slot: number; - min_slot: number; + after_tx_id: NumberOrString; + limit: NumberOrString; + until_tx_id: NumberOrString; } /** 'SqlProjectedNftRange' return type */ export interface ISqlProjectedNftRangeResult { - action_output_index: number | null; - action_slot: number; - action_tx_id: string; - amount: string; - asset_name: string; - for_how_long: string | null; - owner_address: string | null; - plutus_datum: string; - policy_id: string; - previous_tx_hash: string | null; - previous_tx_output_index: string | null; - status: string; + block: string; + payload: Json; + tx_id: string; } /** 'SqlProjectedNftRange' query type */ @@ -29,47 +25,45 @@ 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":1228,"b":1237}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1263,"b":1272}]}],"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":{"after_tx_id":true,"until_tx_id":true,"limit":true},"params":[{"name":"after_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":1369,"b":1381}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":1408,"b":1420}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":1522,"b":1528}]}],"statement":"SELECT\n json_agg(json_build_object(\n 'ownerAddress', encode(\"ProjectedNFT\".owner_address, 'hex'),\n 'previousUtxoTxHash', encode(\"ProjectedNFT\".previous_utxo_tx_hash, 'hex'),\n 'previousTxOutputIndex', \"ProjectedNFT\".previous_utxo_tx_output_index,\n 'actionOutputIndex', CASE\n WHEN \"TransactionOutput\".output_index = NULL THEN NULL\n ELSE \"TransactionOutput\".output_index\n END,\n 'policyId', \"ProjectedNFT\".policy_id,\n 'assetName', \"ProjectedNFT\".asset_name,\n 'amount', \"ProjectedNFT\".amount,\n 'status', 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,\n 'plutusDatum', encode(\"ProjectedNFT\".plutus_datum, 'hex'),\n 'forHowLong', \"ProjectedNFT\".for_how_long,\n 'actionSlot', \"Block\".slot\n )) as \"payload!\",\n encode(\"Block\".hash, 'hex') as \"block!\",\n encode(\"Transaction\".hash, 'hex') as \"tx_id!\"\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\t\"Transaction\".id > :after_tx_id! AND\n\t\"Transaction\".id <= :until_tx_id!\nGROUP BY (\"Block\".id, \"Transaction\".id)\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC\nLIMIT :limit!"}; /** * 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 + * json_agg(json_build_object( + * 'ownerAddress', encode("ProjectedNFT".owner_address, 'hex'), + * 'previousUtxoTxHash', encode("ProjectedNFT".previous_utxo_tx_hash, 'hex'), + * 'previousTxOutputIndex', "ProjectedNFT".previous_utxo_tx_output_index, + * 'actionOutputIndex', CASE + * WHEN "TransactionOutput".output_index = NULL THEN NULL + * ELSE "TransactionOutput".output_index + * END, + * 'policyId', "ProjectedNFT".policy_id, + * 'assetName', "ProjectedNFT".asset_name, + * 'amount', "ProjectedNFT".amount, + * 'status', CASE + * WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + * WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + * WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + * ELSE 'Invalid' + * END, + * 'plutusDatum', encode("ProjectedNFT".plutus_datum, 'hex'), + * 'forHowLong', "ProjectedNFT".for_how_long, + * 'actionSlot', "Block".slot + * )) as "payload!", + * encode("Block".hash, 'hex') as "block!", + * encode("Transaction".hash, 'hex') as "tx_id!" * 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 - * "Block".slot > :min_slot! - * AND "Block".slot <= :max_slot! + * "Transaction".id > :after_tx_id! AND + * "Transaction".id <= :until_tx_id! + * GROUP BY ("Block".id, "Transaction".id) * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * LIMIT :limit! * ``` */ export const sqlProjectedNftRange = new PreparedQuery(sqlProjectedNftRangeIR); diff --git a/webserver/server/app/models/projected_nft/projectedNftRange.sql b/webserver/server/app/models/projected_nft/projectedNftRange.sql index d8e96be4..49ffc3a4 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRange.sql +++ b/webserver/server/app/models/projected_nft/projectedNftRange.sql @@ -2,38 +2,36 @@ @name sqlProjectedNftRange */ 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 + json_agg(json_build_object( + 'ownerAddress', encode("ProjectedNFT".owner_address, 'hex'), + 'previousUtxoTxHash', encode("ProjectedNFT".previous_utxo_tx_hash, 'hex'), + 'previousTxOutputIndex', "ProjectedNFT".previous_utxo_tx_output_index, + 'actionOutputIndex', CASE + WHEN "TransactionOutput".output_index = NULL THEN NULL + ELSE "TransactionOutput".output_index + END, + 'policyId', "ProjectedNFT".policy_id, + 'assetName', "ProjectedNFT".asset_name, + 'amount', "ProjectedNFT".amount, + 'status', CASE + WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + ELSE 'Invalid' + END, + 'plutusDatum', encode("ProjectedNFT".plutus_datum, 'hex'), + 'forHowLong', "ProjectedNFT".for_how_long, + 'actionSlot', "Block".slot + )) as "payload!", + encode("Block".hash, 'hex') as "block!", + encode("Transaction".hash, 'hex') as "tx_id!" 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 - "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 ("Block".id, "Transaction".id) +ORDER BY ("Block".height, "Transaction".tx_index) ASC +LIMIT :limit!; diff --git a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts index 3a438c8e..b65e41b3 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts @@ -1,27 +1,23 @@ /** Types generated for queries found in "app/models/projected_nft/projectedNftRangeByAddress.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +export type NumberOrString = number | string; + /** 'SqlProjectedNftRangeByAddress' parameters type */ export interface ISqlProjectedNftRangeByAddressParams { - max_slot: number; - min_slot: number; + after_tx_id: NumberOrString; + limit: NumberOrString; owner_address: string; + until_tx_id: NumberOrString; } /** 'SqlProjectedNftRangeByAddress' return type */ export interface ISqlProjectedNftRangeByAddressResult { - action_output_index: number | null; - action_slot: number; - action_tx_id: string; - amount: string; - asset_name: string; - for_how_long: string | null; - owner_address: string; - plutus_datum: string; - policy_id: string; - previous_tx_hash: string | null; - previous_tx_output_index: string | null; - status: string; + block: string; + payload: Json; + tx_id: string; } /** 'SqlProjectedNftRangeByAddress' query type */ @@ -30,48 +26,47 @@ 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":1262,"b":1276}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1301,"b":1310}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1336,"b":1345}]}],"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,"after_tx_id":true,"until_tx_id":true,"limit":true},"params":[{"name":"owner_address","required":true,"transform":{"type":"scalar"},"locs":[{"a":1456,"b":1470}]},{"name":"after_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":1496,"b":1508}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":1535,"b":1547}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":1649,"b":1655}]}],"statement":"SELECT\n json_agg(json_build_object(\n 'ownerAddress', encode(\"ProjectedNFT\".owner_address, 'hex'),\n 'previousUtxoTxHash', encode(\"ProjectedNFT\".previous_utxo_tx_hash, 'hex'),\n 'previousTxOutputIndex', \"ProjectedNFT\".previous_utxo_tx_output_index,\n 'actionOutputIndex', CASE\n WHEN \"TransactionOutput\".output_index = NULL THEN NULL\n ELSE \"TransactionOutput\".output_index\n END,\n 'actionTxId', encode(\"Transaction\".hash, 'hex'),\n 'policyId', \"ProjectedNFT\".policy_id,\n 'assetName', \"ProjectedNFT\".asset_name,\n 'amount', \"ProjectedNFT\".amount,\n 'status', 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,\n 'plutusDatum', encode(\"ProjectedNFT\".plutus_datum, 'hex'),\n 'forHowLong', \"ProjectedNFT\".for_how_long,\n 'actionSlot', \"Block\".slot\n )) as \"payload!\",\n encode(\"Block\".hash, 'hex') as \"block!\",\n encode(\"Transaction\".hash, 'hex') as \"tx_id!\"\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! AND\n\t\"Transaction\".id > :after_tx_id! AND\n\t\"Transaction\".id <= :until_tx_id!\nGROUP BY (\"Block\".id, \"Transaction\".id)\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC\nLIMIT :limit!"}; /** * 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 + * json_agg(json_build_object( + * 'ownerAddress', encode("ProjectedNFT".owner_address, 'hex'), + * 'previousUtxoTxHash', encode("ProjectedNFT".previous_utxo_tx_hash, 'hex'), + * 'previousTxOutputIndex', "ProjectedNFT".previous_utxo_tx_output_index, + * 'actionOutputIndex', CASE + * WHEN "TransactionOutput".output_index = NULL THEN NULL + * ELSE "TransactionOutput".output_index + * END, + * 'actionTxId', encode("Transaction".hash, 'hex'), + * 'policyId', "ProjectedNFT".policy_id, + * 'assetName', "ProjectedNFT".asset_name, + * 'amount', "ProjectedNFT".amount, + * 'status', CASE + * WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + * WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + * WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + * ELSE 'Invalid' + * END, + * 'plutusDatum', encode("ProjectedNFT".plutus_datum, 'hex'), + * 'forHowLong', "ProjectedNFT".for_how_long, + * 'actionSlot', "Block".slot + * )) as "payload!", + * encode("Block".hash, 'hex') as "block!", + * encode("Transaction".hash, 'hex') as "tx_id!" * 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! + * encode("ProjectedNFT".owner_address, 'hex') = :owner_address! AND + * "Transaction".id > :after_tx_id! AND + * "Transaction".id <= :until_tx_id! + * GROUP BY ("Block".id, "Transaction".id) * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * LIMIT :limit! * ``` */ 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 index 185533bf..94471c58 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql @@ -2,39 +2,38 @@ @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 + json_agg(json_build_object( + 'ownerAddress', encode("ProjectedNFT".owner_address, 'hex'), + 'previousUtxoTxHash', encode("ProjectedNFT".previous_utxo_tx_hash, 'hex'), + 'previousTxOutputIndex', "ProjectedNFT".previous_utxo_tx_output_index, + 'actionOutputIndex', CASE + WHEN "TransactionOutput".output_index = NULL THEN NULL + ELSE "TransactionOutput".output_index + END, + 'actionTxId', encode("Transaction".hash, 'hex'), + 'policyId', "ProjectedNFT".policy_id, + 'assetName', "ProjectedNFT".asset_name, + 'amount', "ProjectedNFT".amount, + 'status', CASE + WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + ELSE 'Invalid' + END, + 'plutusDatum', encode("ProjectedNFT".plutus_datum, 'hex'), + 'forHowLong', "ProjectedNFT".for_how_long, + 'actionSlot', "Block".slot + )) as "payload!", + encode("Block".hash, 'hex') as "block!", + encode("Transaction".hash, 'hex') as "tx_id!" 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; + encode("ProjectedNFT".owner_address, 'hex') = :owner_address! AND + "Transaction".id > :after_tx_id! AND + "Transaction".id <= :until_tx_id! +GROUP BY ("Block".id, "Transaction".id) +ORDER BY ("Block".height, "Transaction".tx_index) ASC +LIMIT :limit!; diff --git a/webserver/server/app/services/AssetUtxos.ts b/webserver/server/app/services/AssetUtxos.ts index d714603b..ec780962 100644 --- a/webserver/server/app/services/AssetUtxos.ts +++ b/webserver/server/app/services/AssetUtxos.ts @@ -3,18 +3,18 @@ import type { IAssetUtxosResult } from '../models/asset/assetUtxos.queries'; import { assetUtxos } from '../models/asset/assetUtxos.queries'; export async function getAssetUtxos(request: { - range: { - minSlot: number; - maxSlot: number; - }; + after: number; + until: number; fingerprints?: Buffer[]; policyIds?: Buffer[]; + limit: number; dbTx: PoolClient; }): Promise { return await assetUtxos.run( { - max_slot: request.range.maxSlot, - min_slot: request.range.minSlot, + after_tx_id: request.after, + until_tx_id: request.until, + limit: request.limit, // pgtyped doesn't seem to have a way to have an optional array parameter, // and an empty spread expansion fails with postgres. Since none of these // fields is nullable, an array with null should be equivalent to an empty diff --git a/webserver/server/app/services/DelegationForPool.ts b/webserver/server/app/services/DelegationForPool.ts index 2854016b..cb6d12e4 100644 --- a/webserver/server/app/services/DelegationForPool.ts +++ b/webserver/server/app/services/DelegationForPool.ts @@ -1,15 +1,21 @@ import type { PoolClient } from 'pg'; -import type { ISqlStakeDelegationByPoolResult} from '../models/delegation/delegationsForPool.queries'; +import type { ISqlStakeDelegationByPoolResult } from '../models/delegation/delegationsForPool.queries'; import { sqlStakeDelegationByPool } from '../models/delegation/delegationsForPool.queries'; export async function delegationsForPool(request: { - range: { minSlot: number, maxSlot: number }, - pools: Buffer[], - dbTx: PoolClient, + after: number; + until: number; + pools: Buffer[]; + limit: number; + dbTx: PoolClient; }): Promise { - return (await sqlStakeDelegationByPool.run({ - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, - pools: request.pools - }, request.dbTx)); -} \ No newline at end of file + return await sqlStakeDelegationByPool.run( + { + after_tx_id: request.after, + until_tx_id: request.until, + pools: request.pools, + limit: request.limit, + }, + request.dbTx + ); +} 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/server/app/services/PaginationService.ts b/webserver/server/app/services/PaginationService.ts index 3572ea1f..7fea773b 100644 --- a/webserver/server/app/services/PaginationService.ts +++ b/webserver/server/app/services/PaginationService.ts @@ -2,6 +2,7 @@ import type { PoolClient } from 'pg'; import { pageStartByHash } from '../models/pagination/pageStartByHash.queries'; import { sqlBlockByHash } from '../models/pagination/sqlBlockByHash.queries'; import { sqlTransactionBeforeBlock } from '../models/pagination/sqlTransactionBeforeBlock.queries'; +import type { ISlotBoundsPaginationResult } from '../models/pagination/slotBoundsPagination.queries'; export type UntilPaginationType = { until: { @@ -74,3 +75,42 @@ export async function resolvePageStart(request: { tx_id: Number.parseInt(result[0].after_tx_id, 10), }; } + +export type SlotLimits = { + // this is exclusive + from: number; + // this is inclusive + to: number; +}; + +export function adjustToSlotLimits( + pageStartWithSlot: { tx_id: number, block_id: number } | undefined, + until: { tx_id: number }, + slotLimits: SlotLimits | undefined, + slotBounds: ISlotBoundsPaginationResult[] | undefined, +): { tx_id: number, block_id: number } | undefined { + // if the slotLimits field is set, this shrinks the tx id range + // accordingly if necessary. + if (slotLimits) { + const bounds = slotBounds ? slotBounds[0] : { min_tx_id: -1, max_tx_id: -2 }; + + const minTxId = Number(bounds.min_tx_id); + + if (!pageStartWithSlot) { + pageStartWithSlot = { + block_id: -1, + // if no *after* argument is provided, this starts the pagination + // from the corresponding slot. This allows skipping slots you are + // not interested in. If there is also no slotLimits specified this + // starts from the first tx because of the default of -1. + tx_id: minTxId, + }; + } else { + pageStartWithSlot.tx_id = Math.max(Number(bounds.min_tx_id), pageStartWithSlot.tx_id); + } + + until.tx_id = Math.min(until.tx_id, Number(bounds.max_tx_id)); + } + + return pageStartWithSlot; +} \ No newline at end of file diff --git a/webserver/server/app/services/ProjectedNftRange.ts b/webserver/server/app/services/ProjectedNftRange.ts index 49d0dd26..e55b53a6 100644 --- a/webserver/server/app/services/ProjectedNftRange.ts +++ b/webserver/server/app/services/ProjectedNftRange.ts @@ -4,23 +4,29 @@ 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 }, + after: number; + until: number; + limit: number; dbTx: PoolClient, }): Promise { return (await sqlProjectedNftRange.run({ - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, + after_tx_id: request.after, + until_tx_id: request.until, + limit: request.limit, }, request.dbTx)); } export async function projectedNftRangeByAddress(request: { address: string, - range: { minSlot: number, maxSlot: number }, + after: number; + until: number; + limit: number; dbTx: PoolClient, }): Promise { return (await sqlProjectedNftRangeByAddress.run({ owner_address: request.address, - min_slot: request.range.minSlot, - max_slot: request.range.maxSlot, + after_tx_id: request.after, + until_tx_id: request.until, + limit: request.limit, }, request.dbTx)); } diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index 725412aa..da409050 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -28,15 +28,19 @@ export const DEX_PRICE_LIMIT = { }; export const PROJECTED_NFT_LIMIT = { - SLOT_RANGE: 100000, - SINGLE_USER_SLOT_RANGE: 10000000000, + DEFAULT_PAGE_SIZE: 50, }; export const POOL_DELEGATION_LIMIT = { POOLS: 50, - SLOT_RANGE: 10000, + DEFAULT_PAGE_SIZE: 50, }; 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/AssetUtxos.ts b/webserver/shared/models/AssetUtxos.ts index e9633fb7..e76c82a8 100644 --- a/webserver/shared/models/AssetUtxos.ts +++ b/webserver/shared/models/AssetUtxos.ts @@ -1,4 +1,6 @@ +import { SlotLimits } from "../../server/app/services/PaginationService"; import { AssetName, PolicyId } from "./PolicyIdAssetMap"; +import { Pagination } from "./common"; /** * @example "asset1c43p68zwjezc7f6w4w9qkhkwv9ppwz0f7c3amw" @@ -6,27 +8,34 @@ import { AssetName, PolicyId } from "./PolicyIdAssetMap"; export type Cip14Fingerprint = string; export type AssetUtxosRequest = { - range: { minSlot: number; maxSlot: number }; fingerprints?: Cip14Fingerprint[]; policyIds?: PolicyId[]; -}; + /** This limits the transactions in the result to this range of slots. + * Everything else is filtered out */ + slotLimits?: SlotLimits; + + limit?: number; +} & Pagination; export type AssetUtxosResponse = { - /** - * If the utxo is created, this has the amount. It's undefined if the utxo - * is spent. - * - * @example '1031423725351' - */ - amount: string | undefined; - utxo: { - tx: string; - index: number; - }; - cip14Fingerprint: string; - policyId: string; - assetName: AssetName; - paymentCred: string; - slot: number; + payload: { + /** + * If the utxo is created, this has the amount. It's undefined if the utxo + * is spent. + * + * @example '1031423725351' + */ + amount: string | undefined; + cip14Fingerprint: string; + policyId: string; + assetName: AssetName; + paymentCred: string; + slot: number; + utxo: { + tx: string; + index: number; + }; + }[]; txId: string; + block: string; }[]; diff --git a/webserver/shared/models/DelegationForPool.ts b/webserver/shared/models/DelegationForPool.ts index c551a1aa..edbaf955 100644 --- a/webserver/shared/models/DelegationForPool.ts +++ b/webserver/shared/models/DelegationForPool.ts @@ -1,14 +1,24 @@ +import type { Pagination } from "./common"; import { Address } from "./Address"; import { Pool, PoolHex } from "./Pool"; +import { SlotLimits } from "../../server/app/services/PaginationService"; export type DelegationForPoolRequest = { pools: Pool[]; - range: { minSlot: number, maxSlot: number } -}; + + /** This limits the transactions in the result to this range of slots. + * Everything else is filtered out */ + slotLimits?: SlotLimits; + + limit?: number; +} & Pagination; export type DelegationForPoolResponse = { + payload: { credential: Address; - pool: PoolHex | null, - txId: string; + pool: PoolHex | null; slot: number; -}[]; \ No newline at end of file + }[]; + txId: string; + block: string; +}[]; 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[]; diff --git a/webserver/shared/models/ProjectedNftRange.ts b/webserver/shared/models/ProjectedNftRange.ts index 36abb5f5..4b245a8f 100644 --- a/webserver/shared/models/ProjectedNftRange.ts +++ b/webserver/shared/models/ProjectedNftRange.ts @@ -1,38 +1,27 @@ +import { SlotLimits } from "../../server/app/services/PaginationService"; +import { Pagination } 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; + slotLimits?: SlotLimits; + limit?: number; +} & Pagination; export enum ProjectedNftStatus { - Lock = 'Lock', - Unlocking = 'Unlocking', - Claim = 'Claim', - Invalid = 'Invalid' -}; + Lock = "Lock", + Unlocking = "Unlocking", + Claim = "Claim", + Invalid = "Invalid", +} export type ProjectedNftRangeResponse = { + payload: { /** * Slot at which the transaction happened * * @example 512345 */ - actionSlot: number, + actionSlot: number; /** * Projected NFT owner address. Not null only if owned by Public Key Hash. @@ -40,22 +29,14 @@ export type ProjectedNftRangeResponse = { * @pattern [0-9a-fA-F]+ * @example "9040f057461d9adc09108fe5cb630077cf75c6e981d3ed91f6fb18f6" */ - ownerAddress: string | null, - - /** - * Transaction id of related Projected NFT event - * - * @pattern [0-9a-fA-F]{64} - * @example "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda" - */ - actionTxId: string, + ownerAddress: string | null; /** * Output index of related Projected NFT event. Null if it is claim event (No new UTxO is created). * * @example 1 */ - actionOutputIndex: number | null, + actionOutputIndex: number | null; /** * Transaction id of related previous Projected NFT event. @@ -65,51 +46,61 @@ export type ProjectedNftRangeResponse = { * @pattern [0-9a-fA-F]{64} * @example "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda" */ - previousTxHash: string | null, + previousTxHash: string | null; /** * Output index of related previous Projected NFT event. Null if event has `status` Lock. * * @example 1 */ - previousTxOutputIndex: number | null, + previousTxOutputIndex: number | null; /** * Asset policy id that relates to Projected NFT event * * @pattern [0-9a-fA-F]{56} * @example "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278" */ - policyId: string, + policyId: string; /** * Asset name that relates to Projected NFT event * * @pattern ([0-9a-fA-F]{2}){0,32} * @example "415045" */ - assetName: string, + assetName: string; /** * Number of assets of `asset` type used in this Projected NFT event. * * @example "1" */ - amount: string, + amount: string; /** * Projected NFT status: Lock / Unlocking / Claim / Invalid * * @example "Lock" */ - status: ProjectedNftStatus, + status: ProjectedNftStatus; /** * Projected NFT datum: serialized state of the Projected NFT * * @pattern [0-9a-fA-F]+ * @example "d8799fd8799f581c9040f057461d9adc09108fe5cb630077cf75c6e981d3ed91f6fb18f6ffd87980ff" */ - plutusDatum: string, + plutusDatum: string; /** * UNIX timestamp till which the funds can't be claimed in the Unlocking state. * If the status is not Unlocking this is always null. * * @example "1701266986000" */ - forHowLong: string | null, -}[]; \ No newline at end of file + forHowLong: string | null; + }[]; + block: string; + + /** + * Transaction id of related Projected NFT event + * + * @pattern [0-9a-fA-F]{64} + * @example "28eb069e3e8c13831d431e3b2e35f58525493ab2d77fde83184993e4aa7a0eda" + */ + txId: string; +}[]; diff --git a/webserver/shared/models/TransactionHistory.ts b/webserver/shared/models/TransactionHistory.ts index bab38426..8f070c58 100644 --- a/webserver/shared/models/TransactionHistory.ts +++ b/webserver/shared/models/TransactionHistory.ts @@ -1,3 +1,4 @@ +import { SlotLimits } from "../../server/app/services/PaginationService"; import type { Address } from "./Address"; import type { BlockSubset } from "./BlockLatest"; import type { Pagination, RelationFilter } from "./common";