From c24477a976db6da1bf25972710c725df517e964d 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 --- .../TransactionHistoryController.ts | 35 ++++++- .../slotBoundsPagination.queries.ts | 57 +++++++++++ .../pagination/slotBoundsPagination.sql | 25 +++++ .../sqlHistoryForAddresses.queries.ts | 14 ++- .../transaction/sqlHistoryForAddresses.sql | 8 +- .../sqlHistoryForCredentials.queries.ts | 14 ++- .../transaction/sqlHistoryForCredentials.sql | 10 +- .../app/services/TransactionHistoryService.ts | 98 +++++++++++++++++++ webserver/shared/models/TransactionHistory.ts | 18 ++++ 9 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 webserver/server/app/models/pagination/slotBoundsPagination.queries.ts create mode 100644 webserver/server/app/models/pagination/slotBoundsPagination.sql diff --git a/webserver/server/app/controllers/TransactionHistoryController.ts b/webserver/server/app/controllers/TransactionHistoryController.ts index 36609e73..7278487e 100644 --- a/webserver/server/app/controllers/TransactionHistoryController.ts +++ b/webserver/server/app/controllers/TransactionHistoryController.ts @@ -15,6 +15,7 @@ import { Routes } from '../../../shared/routes'; import sortBy from 'lodash/sortBy'; import { getAddressTypes } from '../models/utils'; import { RelationFilterType } from '../../../shared/models/common'; +import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries'; const route = Routes.transactionHistory; @@ -62,7 +63,7 @@ export class TransactionHistoryController extends Controller { const cardanoTxs = await tx< ErrorShape | [TransactionHistoryResponse, TransactionHistoryResponse] >(pool, async dbTx => { - const [until, pageStart] = await Promise.all([ + const [until, pageStart, slotBounds] = await Promise.all([ resolveUntilTransaction({ block_hash: Buffer.from(requestBody.untilBlock, 'hex'), dbTx, @@ -74,6 +75,12 @@ export class TransactionHistoryController extends Controller { 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, { @@ -87,8 +94,32 @@ export class TransactionHistoryController extends Controller { }); } + let pageStartWithSlot = pageStart; + + 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: -1, + tx_id: minTxId, + }; + } else { + if (minTxId > pageStartWithSlot.tx_id) { + pageStartWithSlot.tx_id = minTxId; + } + } + + const maxTxId = Number(bounds.max_tx_id); + if (maxTxId < until.tx_id) { + until.tx_id = maxTxId; + } + } + const commonRequest = { - after: pageStart, + after: pageStartWithSlot, limit: requestBody.limit ?? ADDRESS_LIMIT.RESPONSE, until, dbTx, diff --git a/webserver/server/app/models/pagination/slotBoundsPagination.queries.ts b/webserver/server/app/models/pagination/slotBoundsPagination.queries.ts new file mode 100644 index 00000000..ba3c7103 --- /dev/null +++ b/webserver/server/app/models/pagination/slotBoundsPagination.queries.ts @@ -0,0 +1,57 @@ +/** Types generated for queries found in "app/models/pagination/slotBoundsPagination.sql" */ +import { PreparedQuery } from '@pgtyped/runtime'; + +/** 'SlotBoundsPagination' parameters type */ +export interface ISlotBoundsPaginationParams { + high: number; + low: number; +} + +/** 'SlotBoundsPagination' return type */ +export interface ISlotBoundsPaginationResult { + max_slot: number; + max_tx_id: string | null; + min_slot: number; + min_tx_id: string | null; +} + +/** 'SlotBoundsPagination' query type */ +export interface ISlotBoundsPaginationQuery { + params: ISlotBoundsPaginationParams; + result: ISlotBoundsPaginationResult; +} + +const slotBoundsPaginationIR: any = {"usedParamSet":{"low":true,"high":true},"params":[{"name":"low","required":true,"transform":{"type":"scalar"},"locs":[{"a":193,"b":197}]},{"name":"high","required":true,"transform":{"type":"scalar"},"locs":[{"a":449,"b":454}]}],"statement":"WITH MIN_HASH AS\n\t(SELECT COALESCE(\"Transaction\".ID,\n\n\t\t\t\t\t\t\t\t\t\t-1) AS MIN_TX_ID,\n\t\t\tSLOT AS MIN_SLOT\n\t\tFROM \"Transaction\"\n\t\tJOIN \"Block\" ON \"Block\".ID = \"Transaction\".BLOCK_ID\n\t\tWHERE SLOT <= :low!\n\t\tORDER BY \"Block\".ID DESC, \"Transaction\".ID DESC\n\t\tLIMIT 1),\n\tMAX_HASH AS\n\t(SELECT SLOT AS MAX_SLOT,\n\t\t\tCOALESCE(MAX(\"Transaction\".ID),\n\n\t\t\t\t-2) AS MAX_TX_ID\n\t\tFROM \"Transaction\"\n\t\tJOIN \"Block\" ON \"Transaction\".BLOCK_ID = \"Block\".ID\n\t\tWHERE SLOT <= :high!\n\t\tGROUP BY \"Block\".ID\n\t\tORDER BY \"Block\".ID DESC\n\t\tLIMIT 1)\nSELECT *\nFROM MIN_HASH\nLEFT JOIN MAX_HASH ON 1 = 1"}; + +/** + * Query generated from SQL: + * ``` + * WITH MIN_HASH AS + * (SELECT COALESCE("Transaction".ID, + * + * -1) AS MIN_TX_ID, + * SLOT AS MIN_SLOT + * FROM "Transaction" + * JOIN "Block" ON "Block".ID = "Transaction".BLOCK_ID + * WHERE SLOT <= :low! + * ORDER BY "Block".ID DESC, "Transaction".ID DESC + * LIMIT 1), + * MAX_HASH AS + * (SELECT SLOT AS MAX_SLOT, + * COALESCE(MAX("Transaction".ID), + * + * -2) AS MAX_TX_ID + * FROM "Transaction" + * JOIN "Block" ON "Transaction".BLOCK_ID = "Block".ID + * WHERE SLOT <= :high! + * GROUP BY "Block".ID + * ORDER BY "Block".ID DESC + * LIMIT 1) + * SELECT * + * FROM MIN_HASH + * LEFT JOIN MAX_HASH ON 1 = 1 + * ``` + */ +export const slotBoundsPagination = new PreparedQuery(slotBoundsPaginationIR); + + diff --git a/webserver/server/app/models/pagination/slotBoundsPagination.sql b/webserver/server/app/models/pagination/slotBoundsPagination.sql new file mode 100644 index 00000000..c0b19c05 --- /dev/null +++ b/webserver/server/app/models/pagination/slotBoundsPagination.sql @@ -0,0 +1,25 @@ +/* @name slotBoundsPagination */ +WITH MIN_HASH AS + (SELECT COALESCE("Transaction".ID, + + -1) AS MIN_TX_ID, + SLOT AS MIN_SLOT + FROM "Transaction" + JOIN "Block" ON "Block".ID = "Transaction".BLOCK_ID + WHERE SLOT <= :low! + ORDER BY "Block".ID DESC, "Transaction".ID DESC + LIMIT 1), + MAX_HASH AS + (SELECT SLOT AS MAX_SLOT, + COALESCE(MAX("Transaction".ID), + + -2) AS MAX_TX_ID + FROM "Transaction" + JOIN "Block" ON "Transaction".BLOCK_ID = "Block".ID + WHERE SLOT <= :high! + GROUP BY "Block".ID + ORDER BY "Block".ID DESC + LIMIT 1) +SELECT * +FROM MIN_HASH +LEFT JOIN MAX_HASH ON 1 = 1; \ No newline at end of file diff --git a/webserver/server/app/models/transaction/sqlHistoryForAddresses.queries.ts b/webserver/server/app/models/transaction/sqlHistoryForAddresses.queries.ts index 10c81309..c9b8f040 100644 --- a/webserver/server/app/models/transaction/sqlHistoryForAddresses.queries.ts +++ b/webserver/server/app/models/transaction/sqlHistoryForAddresses.queries.ts @@ -3,6 +3,8 @@ import { PreparedQuery } from '@pgtyped/runtime'; export type BufferArray = (Buffer)[]; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + export type NumberOrString = number | string; /** 'SqlHistoryForAddresses' parameters type */ @@ -21,7 +23,9 @@ export interface ISqlHistoryForAddressesResult { hash: Buffer; height: number; id: string; + input_addresses: Json | null; is_valid: boolean; + metadata: Buffer; payload: Buffer; slot: number; tx_index: number; @@ -33,7 +37,7 @@ export interface ISqlHistoryForAddressesQuery { result: ISqlHistoryForAddressesResult; } -const sqlHistoryForAddressesIR: any = {"usedParamSet":{"addresses":true,"until_tx_id":true,"after_tx_id":true,"limit":true},"params":[{"name":"addresses","required":false,"transform":{"type":"scalar"},"locs":[{"a":91,"b":100}]},{"name":"until_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":373,"b":384},{"a":788,"b":799},{"a":1250,"b":1261}]},{"name":"after_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":440,"b":451},{"a":854,"b":865},{"a":1325,"b":1336}]},{"name":"limit","required":false,"transform":{"type":"scalar"},"locs":[{"a":516,"b":521},{"a":929,"b":934},{"a":1409,"b":1414},{"a":1924,"b":1929}]}],"statement":"WITH\n address_row AS (\n SELECT *\n FROM \"Address\"\n WHERE \"Address\".payload = ANY (:addresses)\n ),\n outputs AS (\n SELECT DISTINCT ON (\"TransactionOutput\".tx_id) \"TransactionOutput\".tx_id\n FROM \"TransactionOutput\"\n INNER JOIN address_row ON \"TransactionOutput\".address_id = address_row.id\n WHERE\n \"TransactionOutput\".tx_id <= (:until_tx_id)\n AND\n \"TransactionOutput\".tx_id > (:after_tx_id)\n ORDER BY \"TransactionOutput\".tx_id ASC\n LIMIT (:limit)\n ),\n inputs AS (\n SELECT DISTINCT ON (\"TransactionInput\".tx_id) \"TransactionInput\".tx_id\n FROM \"TransactionInput\"\n INNER JOIN address_row ON \"TransactionInput\".address_id = address_row.id\n WHERE\n \"TransactionInput\".tx_id <= (:until_tx_id)\n AND\n \"TransactionInput\".tx_id > (:after_tx_id)\n ORDER BY \"TransactionInput\".tx_id ASC\n LIMIT (:limit)\n ),\n ref_inputs AS (\n SELECT DISTINCT ON (\"TransactionReferenceInput\".tx_id) \"TransactionReferenceInput\".tx_id\n FROM \"TransactionReferenceInput\"\n INNER JOIN address_row ON \"TransactionReferenceInput\".address_id = address_row.id\n WHERE\n \"TransactionReferenceInput\".tx_id <= (:until_tx_id)\n AND\n \"TransactionReferenceInput\".tx_id > (:after_tx_id)\n ORDER BY \"TransactionReferenceInput\".tx_id ASC\n LIMIT (:limit)\n )\nSELECT \"Transaction\".id,\n \"Transaction\".payload,\n \"Transaction\".hash,\n \"Transaction\".tx_index,\n \"Transaction\".is_valid,\n \"Block\".hash AS block_hash,\n \"Block\".epoch,\n \"Block\".slot,\n \"Block\".era,\n \"Block\".height\nFROM \"Transaction\"\nINNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \"Transaction\".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs)\nORDER BY \"Transaction\".id ASC\nLIMIT (:limit)"}; +const sqlHistoryForAddressesIR: any = {"usedParamSet":{"addresses":true,"until_tx_id":true,"after_tx_id":true,"limit":true},"params":[{"name":"addresses","required":false,"transform":{"type":"scalar"},"locs":[{"a":91,"b":100}]},{"name":"until_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":373,"b":384},{"a":788,"b":799},{"a":1250,"b":1261}]},{"name":"after_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":440,"b":451},{"a":854,"b":865},{"a":1325,"b":1336}]},{"name":"limit","required":false,"transform":{"type":"scalar"},"locs":[{"a":516,"b":521},{"a":929,"b":934},{"a":1409,"b":1414},{"a":2329,"b":2334}]}],"statement":"WITH\n address_row AS (\n SELECT *\n FROM \"Address\"\n WHERE \"Address\".payload = ANY (:addresses)\n ),\n outputs AS (\n SELECT DISTINCT ON (\"TransactionOutput\".tx_id) \"TransactionOutput\".tx_id\n FROM \"TransactionOutput\"\n INNER JOIN address_row ON \"TransactionOutput\".address_id = address_row.id\n WHERE\n \"TransactionOutput\".tx_id <= (:until_tx_id)\n AND\n \"TransactionOutput\".tx_id > (:after_tx_id)\n ORDER BY \"TransactionOutput\".tx_id ASC\n LIMIT (:limit)\n ),\n inputs AS (\n SELECT DISTINCT ON (\"TransactionInput\".tx_id) \"TransactionInput\".tx_id\n FROM \"TransactionInput\"\n INNER JOIN address_row ON \"TransactionInput\".address_id = address_row.id\n WHERE\n \"TransactionInput\".tx_id <= (:until_tx_id)\n AND\n \"TransactionInput\".tx_id > (:after_tx_id)\n ORDER BY \"TransactionInput\".tx_id ASC\n LIMIT (:limit)\n ),\n ref_inputs AS (\n SELECT DISTINCT ON (\"TransactionReferenceInput\".tx_id) \"TransactionReferenceInput\".tx_id\n FROM \"TransactionReferenceInput\"\n INNER JOIN address_row ON \"TransactionReferenceInput\".address_id = address_row.id\n WHERE\n \"TransactionReferenceInput\".tx_id <= (:until_tx_id)\n AND\n \"TransactionReferenceInput\".tx_id > (:after_tx_id)\n ORDER BY \"TransactionReferenceInput\".tx_id ASC\n LIMIT (:limit)\n )\nSELECT \"Transaction\".id,\n \"Transaction\".payload,\n \"Transaction\".hash,\n \"Transaction\".tx_index,\n \"Transaction\".is_valid,\n \"Block\".hash AS block_hash,\n \"Block\".epoch,\n \"Block\".slot,\n \"Block\".era,\n \"Block\".height,\n \"TransactionMetadata\".payload AS metadata,\n json_agg(DISTINCT \"Address\".PAYLOAD) input_addresses\nFROM \"Transaction\"\nINNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nINNER JOIN \"TransactionInput\" ON \"TransactionInput\".tx_id = \"Transaction\".id\nINNER JOIN \"Address\" ON \"Address\".id = \"TransactionInput\".address_id\nLEFT JOIN \"TransactionMetadata\" ON \"Transaction\".id = \"TransactionMetadata\".tx_id\nWHERE \"Transaction\".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs)\nGROUP BY \"Transaction\".id, \"Block\".id, \"TransactionMetadata\".id\nORDER BY \"Transaction\".id ASC\nLIMIT (:limit)"}; /** * Query generated from SQL: @@ -86,10 +90,16 @@ const sqlHistoryForAddressesIR: any = {"usedParamSet":{"addresses":true,"until_t * "Block".epoch, * "Block".slot, * "Block".era, - * "Block".height + * "Block".height, + * "TransactionMetadata".payload AS metadata, + * json_agg(DISTINCT "Address".PAYLOAD) input_addresses * FROM "Transaction" * INNER JOIN "Block" ON "Transaction".block_id = "Block".id + * INNER JOIN "TransactionInput" ON "TransactionInput".tx_id = "Transaction".id + * INNER JOIN "Address" ON "Address".id = "TransactionInput".address_id + * LEFT JOIN "TransactionMetadata" ON "Transaction".id = "TransactionMetadata".tx_id * WHERE "Transaction".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs) + * GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id * ORDER BY "Transaction".id ASC * LIMIT (:limit) * ``` diff --git a/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql b/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql index b57b7fcd..ac3d7d51 100644 --- a/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql +++ b/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql @@ -47,9 +47,15 @@ SELECT "Transaction".id, "Block".epoch, "Block".slot, "Block".era, - "Block".height + "Block".height, + "TransactionMetadata".payload AS metadata, + json_agg(DISTINCT "Address".PAYLOAD) input_addresses FROM "Transaction" INNER JOIN "Block" ON "Transaction".block_id = "Block".id +INNER JOIN "TransactionInput" ON "TransactionInput".tx_id = "Transaction".id +INNER JOIN "Address" ON "Address".id = "TransactionInput".address_id +LEFT JOIN "TransactionMetadata" ON "Transaction".id = "TransactionMetadata".tx_id WHERE "Transaction".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs) +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/transaction/sqlHistoryForCredentials.queries.ts b/webserver/server/app/models/transaction/sqlHistoryForCredentials.queries.ts index e66b4f15..50c042d1 100644 --- a/webserver/server/app/models/transaction/sqlHistoryForCredentials.queries.ts +++ b/webserver/server/app/models/transaction/sqlHistoryForCredentials.queries.ts @@ -3,6 +3,8 @@ import { PreparedQuery } from '@pgtyped/runtime'; export type BufferArray = (Buffer)[]; +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + export type NumberOrString = number | string; /** 'SqlHistoryForCredentials' parameters type */ @@ -22,7 +24,9 @@ export interface ISqlHistoryForCredentialsResult { hash: Buffer; height: number; id: string; + input_addresses: Json | null; is_valid: boolean; + metadata: Buffer; payload: Buffer; slot: number; tx_index: number; @@ -34,7 +38,7 @@ export interface ISqlHistoryForCredentialsQuery { result: ISqlHistoryForCredentialsResult; } -const sqlHistoryForCredentialsIR: any = {"usedParamSet":{"credentials":true,"relation":true,"until_tx_id":true,"after_tx_id":true,"limit":true},"params":[{"name":"credentials","required":false,"transform":{"type":"scalar"},"locs":[{"a":288,"b":299}]},{"name":"relation","required":false,"transform":{"type":"scalar"},"locs":[{"a":354,"b":362}]},{"name":"until_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":464,"b":475}]},{"name":"after_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":527,"b":538}]},{"name":"limit","required":false,"transform":{"type":"scalar"},"locs":[{"a":598,"b":603}]}],"statement":"WITH\n tx_relations AS (\n SELECT DISTINCT ON (\"TxCredentialRelation\".tx_id) \"TxCredentialRelation\".tx_id\n FROM \"StakeCredential\"\n INNER JOIN \"TxCredentialRelation\" ON \"TxCredentialRelation\".credential_id = \"StakeCredential\".id\n WHERE\n \"StakeCredential\".credential = ANY (:credentials)\n AND\n (\"TxCredentialRelation\".relation & (:relation)) > 0\n AND\n \n \"TxCredentialRelation\".tx_id <= (:until_tx_id)\n AND \n \"TxCredentialRelation\".tx_id > (:after_tx_id)\n ORDER BY \"TxCredentialRelation\".tx_id ASC\n LIMIT (:limit)\n )\nSELECT \"Transaction\".id,\n \"Transaction\".payload,\n \"Transaction\".hash,\n \"Transaction\".tx_index,\n \"Transaction\".is_valid,\n \"Block\".hash AS block_hash,\n \"Block\".epoch,\n \"Block\".slot,\n \"Block\".era,\n \"Block\".height\nFROM tx_relations\nINNER JOIN \"Transaction\" ON tx_relations.tx_id = \"Transaction\".id\nINNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id"}; +const sqlHistoryForCredentialsIR: any = {"usedParamSet":{"credentials":true,"relation":true,"until_tx_id":true,"after_tx_id":true,"limit":true},"params":[{"name":"credentials","required":false,"transform":{"type":"scalar"},"locs":[{"a":288,"b":299}]},{"name":"relation","required":false,"transform":{"type":"scalar"},"locs":[{"a":354,"b":362}]},{"name":"until_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":464,"b":475}]},{"name":"after_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":527,"b":538}]},{"name":"limit","required":false,"transform":{"type":"scalar"},"locs":[{"a":598,"b":603}]}],"statement":"WITH\n tx_relations AS (\n SELECT DISTINCT ON (\"TxCredentialRelation\".tx_id) \"TxCredentialRelation\".tx_id\n FROM \"StakeCredential\"\n INNER JOIN \"TxCredentialRelation\" ON \"TxCredentialRelation\".credential_id = \"StakeCredential\".id\n WHERE\n \"StakeCredential\".credential = ANY (:credentials)\n AND\n (\"TxCredentialRelation\".relation & (:relation)) > 0\n AND\n \n \"TxCredentialRelation\".tx_id <= (:until_tx_id)\n AND \n \"TxCredentialRelation\".tx_id > (:after_tx_id)\n ORDER BY \"TxCredentialRelation\".tx_id ASC\n LIMIT (:limit)\n )\nSELECT \"Transaction\".id,\n \"Transaction\".payload,\n \"Transaction\".hash,\n \"Transaction\".tx_index,\n \"Transaction\".is_valid,\n \"Block\".hash AS block_hash,\n \"Block\".epoch,\n \"Block\".slot,\n \"Block\".era,\n \"Block\".height,\n \"TransactionMetadata\".payload AS metadata,\n json_agg(DISTINCT \"Address\".PAYLOAD) input_addresses\nFROM tx_relations\nINNER JOIN \"Transaction\" ON tx_relations.tx_id = \"Transaction\".id\nINNER JOIN \"TransactionInput\" ON \"TransactionInput\".tx_id = \"Transaction\".id\nINNER JOIN \"Address\" ON \"Address\".id = \"TransactionInput\".address_id\nLEFT JOIN \"TransactionMetadata\" ON \"Transaction\".id = \"TransactionMetadata\".tx_id\nINNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nGROUP BY \"Transaction\".id, \"Block\".id, \"TransactionMetadata\".id"}; /** * Query generated from SQL: @@ -65,10 +69,16 @@ const sqlHistoryForCredentialsIR: any = {"usedParamSet":{"credentials":true,"rel * "Block".epoch, * "Block".slot, * "Block".era, - * "Block".height + * "Block".height, + * "TransactionMetadata".payload AS metadata, + * json_agg(DISTINCT "Address".PAYLOAD) input_addresses * FROM tx_relations * INNER JOIN "Transaction" ON tx_relations.tx_id = "Transaction".id + * INNER JOIN "TransactionInput" ON "TransactionInput".tx_id = "Transaction".id + * INNER JOIN "Address" ON "Address".id = "TransactionInput".address_id + * LEFT JOIN "TransactionMetadata" ON "Transaction".id = "TransactionMetadata".tx_id * INNER JOIN "Block" ON "Transaction".block_id = "Block".id + * GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id * ``` */ export const sqlHistoryForCredentials = new PreparedQuery(sqlHistoryForCredentialsIR); diff --git a/webserver/server/app/models/transaction/sqlHistoryForCredentials.sql b/webserver/server/app/models/transaction/sqlHistoryForCredentials.sql index 822388a3..78558ada 100644 --- a/webserver/server/app/models/transaction/sqlHistoryForCredentials.sql +++ b/webserver/server/app/models/transaction/sqlHistoryForCredentials.sql @@ -25,8 +25,14 @@ SELECT "Transaction".id, "Block".epoch, "Block".slot, "Block".era, - "Block".height + "Block".height, + "TransactionMetadata".payload AS metadata, + json_agg(DISTINCT "Address".PAYLOAD) input_addresses FROM tx_relations INNER JOIN "Transaction" ON tx_relations.tx_id = "Transaction".id -INNER JOIN "Block" ON "Transaction".block_id = "Block".id; +INNER JOIN "TransactionInput" ON "TransactionInput".tx_id = "Transaction".id +INNER JOIN "Address" ON "Address".id = "TransactionInput".address_id +LEFT JOIN "TransactionMetadata" ON "Transaction".id = "TransactionMetadata".tx_id +INNER JOIN "Block" ON "Transaction".block_id = "Block".id +GROUP BY "Transaction".id, "Block".id, "TransactionMetadata".id; diff --git a/webserver/server/app/services/TransactionHistoryService.ts b/webserver/server/app/services/TransactionHistoryService.ts index c8de3597..fa0d1643 100644 --- a/webserver/server/app/services/TransactionHistoryService.ts +++ b/webserver/server/app/services/TransactionHistoryService.ts @@ -4,6 +4,7 @@ import { sqlHistoryForAddresses } from '../models/transaction/sqlHistoryForAddre import type { PoolClient } from 'pg'; import type { TransactionPaginationType } from './PaginationService'; import type { RelationFilter } from '../../../shared/models/common'; +import { Address, Transaction } from '@dcspark/cardano-multiplatform-lib-nodejs'; export async function historyForCredentials( request: TransactionPaginationType & { @@ -39,6 +40,11 @@ export async function historyForCredentials( transaction: { hash: entry.hash.toString('hex'), payload: entry.payload.toString('hex'), + outputs: computeOutputs(entry.payload), + metadata: entry.metadata && entry.metadata.toString('hex'), + inputCredentials: entry.input_addresses + ? (entry.input_addresses as string[]).map(getPaymentCred) + : [], }, })), }; @@ -76,7 +82,99 @@ export async function historyForAddresses( transaction: { hash: entry.hash.toString('hex'), payload: entry.payload.toString('hex'), + outputs: computeOutputs(entry.payload), + metadata: entry.metadata && entry.metadata.toString('hex'), + inputCredentials: entry.input_addresses + ? (entry.input_addresses as string[]).map(getPaymentCred) + : [], }, })), }; } + +function computeOutputs( + tx: Buffer +): { asset: { policyId: string; assetName: string } | null; amount: string; address: string }[] { + const transaction = Transaction.from_bytes(tx); + + const rawOutputs = transaction.body().outputs(); + + const outputs = []; + + for (let i = 0; i < rawOutputs.len(); i++) { + const output = rawOutputs.get(i); + + const rawAddress = output.address(); + const address = rawAddress.to_bech32(); + rawAddress.free(); + + const amount = output.amount(); + const ma = amount.multiasset(); + + if (ma) { + const policyIds = ma.keys(); + + for (let j = 0; j < policyIds.len(); j++) { + const policyId = policyIds.get(j); + + const assets = ma.get(policyId); + + if (!assets) { + continue; + } + + const assetNames = assets.keys(); + + for (let k = 0; k < assetNames.len(); k++) { + const assetName = assetNames.get(k); + + const amount = assets.get(assetName); + + if (amount === undefined) { + continue; + } + + outputs.push({ + amount: amount.to_str(), + asset: { + policyId: policyId.to_hex(), + assetName: Buffer.from(assetName.to_bytes()).toString('hex'), + }, + address + }); + + assetName.free(); + } + + assetNames.free(); + assets.free(); + policyId.free(); + } + + policyIds.free(); + ma.free(); + } + + outputs.push({ amount: amount.coin().to_str(), asset: null, address }); + + amount.free(); + output.free(); + } + + rawOutputs.free(); + transaction.free(); + + return outputs; +} + +function getPaymentCred(addressRaw: string): string { + const address = Address.from_bytes(Buffer.from(addressRaw.slice(2), 'hex')); + + const paymentCred = address.payment_cred(); + const addressBytes = paymentCred?.to_bytes(); + + address.free(); + paymentCred?.free(); + + return Buffer.from(addressBytes as Uint8Array).toString('hex'); +} diff --git a/webserver/shared/models/TransactionHistory.ts b/webserver/shared/models/TransactionHistory.ts index 73935e5d..70f262ea 100644 --- a/webserver/shared/models/TransactionHistory.ts +++ b/webserver/shared/models/TransactionHistory.ts @@ -1,5 +1,6 @@ import type { Address } from "./Address"; import type { BlockSubset } from "./BlockLatest"; +import { AssetName, PolicyId } from "./PolicyIdAssetMap"; import type { Pagination, RelationFilter } from "./common"; export type TransactionHistoryRequest = { @@ -8,6 +9,8 @@ export type TransactionHistoryRequest = { relationFilter?: RelationFilter; /** Defaults to `ADDRESS_LIMIT.RESPONSE` */ limit?: number; + + slotLimits?: SlotLimits; } & Pagination; export type BlockInfo = BlockSubset & { @@ -33,6 +36,16 @@ export type TransactionInfo = { * @example "84a500818258209cb4f8c2eecccc9f1e13768046f37ef56dcb5a4dc44f58907fe4ae21d7cf621d020181825839019cb581f4337a6142e477af6e00fe41b1fc4a5944a575681b8499a3c0bd07ce733b5911eb657e7aff5d35f8b0682fe0380f7621af2bbcb2f71b0000000586321393021a0002a389031a004b418c048183028200581cbd07ce733b5911eb657e7aff5d35f8b0682fe0380f7621af2bbcb2f7581c53215c471b7ac752e3ddf8f2c4c1e6ed111857bfaa675d5e31ce8bcea1008282582073e584cda9fe483fbefb81c251e616018a2b493ef56820f0095b63adede54ff758404f13df42ef1684a3fd55255d8368c9ecbd15b55e2761a2991cc4f401a753c16d6da1da158e84b87b4de9715af7d9adc0d79a7c1f2c3097228e02b20be4616a0c82582066c606974819f457ceface78ee3c4d181a84ca9927a3cfc92ef8c0b6dd4576e8584014ae9ee9ed5eb5700b6c5ac270543671f5d4f943d4726f4614dc061174ee29db44b9e7fc58e6c98c13fad8594f2633c5ec70a9a87f5cbf130308a42edb553001f5f6" */ payload: string; + + outputs: { + asset: { policyId: PolicyId; assetName: AssetName } | null; + amount: string; + address: string; + }[]; + + metadata: string | null; + + inputCredentials: string[]; }; export type TxAndBlockInfo = { @@ -42,3 +55,8 @@ export type TxAndBlockInfo = { export type TransactionHistoryResponse = { transactions: TxAndBlockInfo[]; }; + +export type SlotLimits = { + from: number; + to: number; +};