diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index 42950f27..bbd79a6f 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -1111,6 +1111,16 @@ }, "TransactionInfo": { "properties": { + "inputCredentials": { + "items": { + "type": "string" + }, + "type": "array" + }, + "metadata": { + "type": "string", + "nullable": true + }, "payload": { "type": "string", "description": "cbor-encoded transaction", @@ -1166,10 +1176,35 @@ "description": "Filter which uses of the address are considered relevant for the query.\n\nThis is a bitmask, so you can combine multiple options\nex: `RelationFilterType.Input & RelationFilterType.Output`\n\nNote: relations only apply to credentials and not to full bech32 addresses", "pattern": "([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" }, + "SlotLimits": { + "properties": { + "to": { + "type": "number", + "format": "double" + }, + "from": { + "type": "number", + "format": "double" + } + }, + "required": [ + "to", + "from" + ], + "type": "object" + }, "TransactionHistoryRequest": { "allOf": [ { "properties": { + "withInputContext": { + "type": "boolean", + "description": "If this is set to true, the result includes the input addresses (which are\nnot part of the tx), and the metadata (if any)" + }, + "slotLimits": { + "$ref": "#/components/schemas/SlotLimits", + "description": "This limits the transactions in the result to this range of slots.\nEverything else is filtered out" + }, "limit": { "type": "number", "format": "double", diff --git a/indexer/entity/src/block.rs b/indexer/entity/src/block.rs index 000219a8..3516c0c8 100644 --- a/indexer/entity/src/block.rs +++ b/indexer/entity/src/block.rs @@ -12,6 +12,7 @@ pub struct Model { pub epoch: i32, pub slot: i32, pub payload: Option>, + pub tx_count: i32, } #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] diff --git a/indexer/migration/src/lib.rs b/indexer/migration/src/lib.rs index 11f27ea8..fa57a67c 100644 --- a/indexer/migration/src/lib.rs +++ b/indexer/migration/src/lib.rs @@ -20,6 +20,7 @@ mod m20230223_000015_modify_block_table; mod m20230927_000016_create_stake_delegation_table; mod m20231025_000017_projected_nft; mod m20231220_000018_asset_utxo_table; +mod m20240229_000019_add_block_tx_count_column; pub struct Migrator; @@ -47,6 +48,7 @@ impl MigratorTrait for Migrator { Box::new(m20230927_000016_create_stake_delegation_table::Migration), Box::new(m20231025_000017_projected_nft::Migration), Box::new(m20231220_000018_asset_utxo_table::Migration), + Box::new(m20240229_000019_add_block_tx_count_column::Migration), ] } } diff --git a/indexer/migration/src/m20240229_000019_add_block_tx_count_column.rs b/indexer/migration/src/m20240229_000019_add_block_tx_count_column.rs new file mode 100644 index 00000000..eeae529f --- /dev/null +++ b/indexer/migration/src/m20240229_000019_add_block_tx_count_column.rs @@ -0,0 +1,36 @@ +use sea_schema::migration::prelude::*; + +use entity::block::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20240229_000019_add_block_tx_count_column" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Entity) + .add_column(ColumnDef::new(Column::TxCount).integer()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Entity) + .drop_column(Column::TxCount) + .to_owned(), + ) + .await + } +} diff --git a/indexer/tasks/src/genesis/genesis_block.rs b/indexer/tasks/src/genesis/genesis_block.rs index c2dabbce..a19f7c23 100644 --- a/indexer/tasks/src/genesis/genesis_block.rs +++ b/indexer/tasks/src/genesis/genesis_block.rs @@ -47,6 +47,7 @@ async fn handle_block( epoch: Set(0), slot: Set(0), payload: Set(Some(block_payload)), + tx_count: Set(0), ..Default::default() }; diff --git a/indexer/tasks/src/multiera/multiera_block.rs b/indexer/tasks/src/multiera/multiera_block.rs index 5f74d611..d919f85e 100644 --- a/indexer/tasks/src/multiera/multiera_block.rs +++ b/indexer/tasks/src/multiera/multiera_block.rs @@ -49,7 +49,25 @@ async fn handle_block( epoch: Set(block.2.epoch.unwrap() as i32), slot: Set(block.1.header().slot() as i32), payload: Set(Some(block_payload)), + tx_count: Set(block_tx_count(block.1) as i32), ..Default::default() }; block.insert(db_tx).await } + +fn block_tx_count(block: &cml_multi_era::MultiEraBlock) -> usize { + match block { + cml_multi_era::MultiEraBlock::Byron( + cml_multi_era::byron::block::ByronBlock::EpochBoundary(_), + ) => 0, + cml_multi_era::MultiEraBlock::Byron(cml_multi_era::byron::block::ByronBlock::Main( + block, + )) => block.body.tx_payload.len(), + cml_multi_era::MultiEraBlock::Shelley(block) => block.transaction_bodies.len(), + cml_multi_era::MultiEraBlock::Allegra(block) => block.transaction_bodies.len(), + cml_multi_era::MultiEraBlock::Mary(block) => block.transaction_bodies.len(), + cml_multi_era::MultiEraBlock::Alonzo(block) => block.transaction_bodies.len(), + cml_multi_era::MultiEraBlock::Babbage(block) => block.transaction_bodies.len(), + cml_multi_era::MultiEraBlock::Conway(block) => block.transaction_bodies.len(), + } +} diff --git a/webserver/server/.nvmrc b/webserver/server/.nvmrc index cab13a79..a77793ec 100644 --- a/webserver/server/.nvmrc +++ b/webserver/server/.nvmrc @@ -1 +1 @@ -v14.17.0 +lts/hydrogen diff --git a/webserver/server/app/controllers/TransactionHistoryController.ts b/webserver/server/app/controllers/TransactionHistoryController.ts index 36609e73..33f4297e 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; @@ -32,7 +33,9 @@ export class TransactionHistoryController extends Controller { requestBody: EndpointTypes[typeof route]['input'], @Res() errorResponse: TsoaResponse< - StatusCodes.CONFLICT | StatusCodes.BAD_REQUEST | StatusCodes.UNPROCESSABLE_ENTITY, + | StatusCodes.CONFLICT + | StatusCodes.BAD_REQUEST + | StatusCodes.UNPROCESSABLE_ENTITY, ErrorShape > ): Promise { @@ -62,7 +65,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 +77,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,11 +96,38 @@ 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 commonRequest = { - after: pageStart, + after: pageStartWithSlot, limit: requestBody.limit ?? ADDRESS_LIMIT.RESPONSE, until, dbTx, + withInputContext: !!requestBody.withInputContext }; const result = await Promise.all([ historyForCredentials({ diff --git a/webserver/server/app/models/block/sqlBlockLatest.queries.ts b/webserver/server/app/models/block/sqlBlockLatest.queries.ts index b072f9b9..163271c6 100644 --- a/webserver/server/app/models/block/sqlBlockLatest.queries.ts +++ b/webserver/server/app/models/block/sqlBlockLatest.queries.ts @@ -17,6 +17,7 @@ export interface ISqlBlockLatestResult { id: number; payload: Buffer | null; slot: number; + tx_count: number | null; } /** 'SqlBlockLatest' query type */ 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..4587b218 --- /dev/null +++ b/webserver/server/app/models/pagination/slotBoundsPagination.queries.ts @@ -0,0 +1,87 @@ +/** 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_tx_id: string | null; + 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":782,"b":786}]},{"name":"high","required":true,"transform":{"type":"scalar"},"locs":[{"a":1036,"b":1041}]}],"statement":"WITH\n low_block AS (\n SELECT\n \"Block\".id,\n \"Block\".slot\n FROM\n \"Block\"\n WHERE\n \n slot <= :low! AND tx_count > 0\n ORDER BY\n \"Block\".id DESC\n LIMIT\n 1\n ),\n high_block AS (\n SELECT\n \"Block\".id,\n \"Block\".slot\n FROM\n \"Block\"\n WHERE\n slot <= :high! AND tx_count > 0\n ORDER BY\n \"Block\".id DESC\n LIMIT 1\n ),\n min_hash AS (\n (SELECT\n COALESCE(MAX(\"Transaction\".id), -1) AS min_tx_id\n FROM\n \"Transaction\"\n JOIN low_block ON \"Transaction\".block_id = low_block.id\n GROUP BY\n low_block.slot\n LIMIT\n 1\n )\n UNION (SELECT min_tx_id FROM (values(-1)) s(min_tx_id))\n ORDER BY min_tx_id DESC\n LIMIT\n 1\n ),\n max_hash AS (\n SELECT\n COALESCE(MAX(\"Transaction\".id), -2) AS max_tx_id\n FROM\n \"Transaction\"\n JOIN high_block ON \"Transaction\".block_id = high_block.id\n GROUP BY\n high_block.slot\n )\nSELECT\n *\nFROM min_hash\nLEFT JOIN max_hash ON 1 = 1"}; + +/** + * Query generated from SQL: + * ``` + * WITH + * low_block AS ( + * SELECT + * "Block".id, + * "Block".slot + * FROM + * "Block" + * WHERE + * + * slot <= :low! AND tx_count > 0 + * ORDER BY + * "Block".id DESC + * LIMIT + * 1 + * ), + * high_block AS ( + * SELECT + * "Block".id, + * "Block".slot + * FROM + * "Block" + * WHERE + * slot <= :high! AND tx_count > 0 + * ORDER BY + * "Block".id DESC + * LIMIT 1 + * ), + * min_hash AS ( + * (SELECT + * COALESCE(MAX("Transaction".id), -1) AS min_tx_id + * FROM + * "Transaction" + * JOIN low_block ON "Transaction".block_id = low_block.id + * GROUP BY + * low_block.slot + * LIMIT + * 1 + * ) + * UNION (SELECT min_tx_id FROM (values(-1)) s(min_tx_id)) + * ORDER BY min_tx_id DESC + * LIMIT + * 1 + * ), + * max_hash AS ( + * SELECT + * COALESCE(MAX("Transaction".id), -2) AS max_tx_id + * FROM + * "Transaction" + * JOIN high_block ON "Transaction".block_id = high_block.id + * GROUP BY + * high_block.slot + * ) + * 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..4527bccb --- /dev/null +++ b/webserver/server/app/models/pagination/slotBoundsPagination.sql @@ -0,0 +1,71 @@ +/* @name slotBoundsPagination */ +WITH + low_block AS ( + SELECT + "Block".id, + "Block".slot + FROM + "Block" + WHERE + /* + We use <= here even though slot filter parameter is exclusive on the + lower bound. This is because the tx that we find here (after joining + with min_hash) is used later in a condition of the form: + + "Transaction".id > :after_tx_id! + + For example. + + Lets say :low is 1, and there is a block with txs at this slot. This + means we want to find _at least_ the first tx in slot 2. + + So what we want in this query is to find the last tx in slot 1. + Then, when we use the > comparator we would get the right tx. + */ + slot <= :low! AND tx_count > 0 + ORDER BY + "Block".id DESC + LIMIT + 1 + ), + high_block AS ( + SELECT + "Block".id, + "Block".slot + FROM + "Block" + WHERE + slot <= :high! AND tx_count > 0 + ORDER BY + "Block".id DESC + LIMIT 1 + ), + min_hash AS ( + (SELECT + COALESCE(MAX("Transaction".id), -1) AS min_tx_id + FROM + "Transaction" + JOIN low_block ON "Transaction".block_id = low_block.id + GROUP BY + low_block.slot + LIMIT + 1 + ) + UNION (SELECT min_tx_id FROM (values(-1)) s(min_tx_id)) + ORDER BY min_tx_id DESC + LIMIT + 1 + ), + max_hash AS ( + SELECT + COALESCE(MAX("Transaction".id), -2) AS max_tx_id + FROM + "Transaction" + JOIN high_block ON "Transaction".block_id = high_block.id + GROUP BY + high_block.slot + ) +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..53899d00 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 */ @@ -11,6 +13,7 @@ export interface ISqlHistoryForAddressesParams { after_tx_id?: NumberOrString | null | void; limit?: NumberOrString | null | void; until_tx_id?: NumberOrString | null | void; + with_input_context: boolean; } /** 'SqlHistoryForAddresses' return type */ @@ -20,8 +23,10 @@ export interface ISqlHistoryForAddressesResult { era: number; hash: Buffer; height: number; - id: string; + id: string | null; + input_addresses: Json | null; is_valid: boolean; + metadata: Buffer | null; payload: Buffer; slot: number; tx_index: number; @@ -33,7 +38,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,"with_input_context":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":2215,"b":2220},{"a":3446,"b":3451}]},{"name":"with_input_context","required":true,"transform":{"type":"scalar"},"locs":[{"a":3493,"b":3512},{"a":3576,"b":3595}]}],"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 ),\n base_query AS (\n SELECT \"Transaction\".id,\n \"Transaction\".payload as \"payload!\",\n \"Transaction\".hash as \"hash!\",\n \"Transaction\".tx_index as \"tx_index!\",\n \"Transaction\".is_valid as \"is_valid!\",\n \"Block\".hash AS \"block_hash!\",\n \"Block\".epoch as \"epoch!\",\n \"Block\".slot as \"slot!\",\n \"Block\".era as \"era!\",\n \"Block\".height as \"height!\",\n NULL :: bytea as metadata,\n NULL :: json as input_addresses\n FROM \"Transaction\"\n INNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n WHERE \"Transaction\".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs)\n ORDER BY \"Transaction\".id ASC\n LIMIT (:limit)\n ),\n query_with_inputs_and_metadata AS (\n SELECT \"Transaction\".id,\n \"Transaction\".payload as \"payload!\",\n \"Transaction\".hash as \"hash!\",\n \"Transaction\".tx_index as \"tx_index!\",\n \"Transaction\".is_valid as \"is_valid!\",\n \"Block\".hash AS \"block_hash!\",\n \"Block\".epoch as \"epoch!\",\n \"Block\".slot as \"slot!\",\n \"Block\".era as \"era!\",\n \"Block\".height as \"height!\",\n \"TransactionMetadata\".payload AS metadata,\n json_agg(DISTINCT \"Address\".PAYLOAD) input_addresses\n FROM \"Transaction\"\n INNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n INNER JOIN \"TransactionInput\" ON \"TransactionInput\".tx_id = \"Transaction\".id\n INNER JOIN \"Address\" ON \"Address\".id = \"TransactionInput\".address_id\n LEFT JOIN \"TransactionMetadata\" ON \"Transaction\".id = \"TransactionMetadata\".tx_id\n WHERE \"Transaction\".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs)\n GROUP BY \"Transaction\".id, \"Block\".id, \"TransactionMetadata\".id\n ORDER BY \"Transaction\".id ASC\n LIMIT (:limit)\n )\nSELECT * FROM base_query WHERE NOT :with_input_context!\nUNION ALL\n(SELECT * from query_with_inputs_and_metadata WHERE :with_input_context!)"}; /** * Query generated from SQL: @@ -76,22 +81,52 @@ const sqlHistoryForAddressesIR: any = {"usedParamSet":{"addresses":true,"until_t * "TransactionReferenceInput".tx_id > (:after_tx_id) * ORDER BY "TransactionReferenceInput".tx_id ASC * LIMIT (:limit) + * ), + * base_query AS ( + * SELECT "Transaction".id, + * "Transaction".payload as "payload!", + * "Transaction".hash as "hash!", + * "Transaction".tx_index as "tx_index!", + * "Transaction".is_valid as "is_valid!", + * "Block".hash AS "block_hash!", + * "Block".epoch as "epoch!", + * "Block".slot as "slot!", + * "Block".era as "era!", + * "Block".height as "height!", + * NULL :: bytea as metadata, + * NULL :: json as input_addresses + * FROM "Transaction" + * INNER JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE "Transaction".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs) + * ORDER BY "Transaction".id ASC + * LIMIT (:limit) + * ), + * query_with_inputs_and_metadata AS ( + * SELECT "Transaction".id, + * "Transaction".payload as "payload!", + * "Transaction".hash as "hash!", + * "Transaction".tx_index as "tx_index!", + * "Transaction".is_valid as "is_valid!", + * "Block".hash AS "block_hash!", + * "Block".epoch as "epoch!", + * "Block".slot as "slot!", + * "Block".era as "era!", + * "Block".height as "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) * ) - * SELECT "Transaction".id, - * "Transaction".payload, - * "Transaction".hash, - * "Transaction".tx_index, - * "Transaction".is_valid, - * "Block".hash AS block_hash, - * "Block".epoch, - * "Block".slot, - * "Block".era, - * "Block".height - * FROM "Transaction" - * INNER JOIN "Block" ON "Transaction".block_id = "Block".id - * WHERE "Transaction".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs) - * ORDER BY "Transaction".id ASC - * LIMIT (:limit) + * SELECT * FROM base_query WHERE NOT :with_input_context! + * UNION ALL + * (SELECT * from query_with_inputs_and_metadata WHERE :with_input_context!) * ``` */ export const sqlHistoryForAddresses = new PreparedQuery(sqlHistoryForAddressesIR); diff --git a/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql b/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql index b57b7fcd..34658c77 100644 --- a/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql +++ b/webserver/server/app/models/transaction/sqlHistoryForAddresses.sql @@ -37,19 +37,49 @@ WITH "TransactionReferenceInput".tx_id > (:after_tx_id) ORDER BY "TransactionReferenceInput".tx_id ASC LIMIT (:limit) + ), + base_query AS ( + SELECT "Transaction".id, + "Transaction".payload as "payload!", + "Transaction".hash as "hash!", + "Transaction".tx_index as "tx_index!", + "Transaction".is_valid as "is_valid!", + "Block".hash AS "block_hash!", + "Block".epoch as "epoch!", + "Block".slot as "slot!", + "Block".era as "era!", + "Block".height as "height!", + NULL :: bytea as metadata, + NULL :: json as input_addresses + FROM "Transaction" + INNER JOIN "Block" ON "Transaction".block_id = "Block".id + WHERE "Transaction".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs) + ORDER BY "Transaction".id ASC + LIMIT (:limit) + ), + query_with_inputs_and_metadata AS ( + SELECT "Transaction".id, + "Transaction".payload as "payload!", + "Transaction".hash as "hash!", + "Transaction".tx_index as "tx_index!", + "Transaction".is_valid as "is_valid!", + "Block".hash AS "block_hash!", + "Block".epoch as "epoch!", + "Block".slot as "slot!", + "Block".era as "era!", + "Block".height as "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) ) -SELECT "Transaction".id, - "Transaction".payload, - "Transaction".hash, - "Transaction".tx_index, - "Transaction".is_valid, - "Block".hash AS block_hash, - "Block".epoch, - "Block".slot, - "Block".era, - "Block".height -FROM "Transaction" -INNER JOIN "Block" ON "Transaction".block_id = "Block".id -WHERE "Transaction".id IN (SELECT * FROM inputs UNION ALL SELECT * from ref_inputs UNION ALL SELECT * from outputs) -ORDER BY "Transaction".id ASC -LIMIT (:limit); \ No newline at end of file +SELECT * FROM base_query WHERE NOT :with_input_context! +UNION ALL +(SELECT * from query_with_inputs_and_metadata WHERE :with_input_context!); \ 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..e02270de 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 */ @@ -12,6 +14,7 @@ export interface ISqlHistoryForCredentialsParams { limit?: NumberOrString | null | void; relation?: number | null | void; until_tx_id?: NumberOrString | null | void; + with_input_context: boolean; } /** 'SqlHistoryForCredentials' return type */ @@ -21,8 +24,10 @@ export interface ISqlHistoryForCredentialsResult { era: number; hash: Buffer; height: number; - id: string; + id: string | null; + input_addresses: Json | null; is_valid: boolean; + metadata: Buffer | null; payload: Buffer; slot: number; tx_index: number; @@ -34,7 +39,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,"with_input_context":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}]},{"name":"with_input_context","required":true,"transform":{"type":"scalar"},"locs":[{"a":2446,"b":2465},{"a":2529,"b":2548}]}],"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 ),\n base_query AS (\n SELECT \"Transaction\".id,\n \"Transaction\".payload as \"payload!\",\n \"Transaction\".hash as \"hash!\",\n \"Transaction\".tx_index as \"tx_index!\",\n \"Transaction\".is_valid as \"is_valid!\",\n \"Block\".hash AS \"block_hash!\",\n \"Block\".epoch as \"epoch!\",\n \"Block\".slot as \"slot!\",\n \"Block\".era as \"era!\",\n \"Block\".height as \"height!\",\n NULL :: bytea as metadata,\n NULL :: json as input_addresses\n FROM tx_relations\n INNER JOIN \"Transaction\" ON tx_relations.tx_id = \"Transaction\".id\n INNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n ),\n query_with_inputs_and_metadata AS (\n SELECT \"Transaction\".id,\n \"Transaction\".payload as \"payload!\",\n \"Transaction\".hash as \"hash!\",\n \"Transaction\".tx_index as \"tx_index!\",\n \"Transaction\".is_valid as \"is_valid!\",\n \"Block\".hash as \"block_hash!\",\n \"Block\".epoch as \"epoch!\",\n \"Block\".slot as \"slot!\",\n \"Block\".era as \"era!\",\n \"Block\".height as \"height!\",\n \"TransactionMetadata\".payload AS metadata,\n json_agg(DISTINCT \"Address\".PAYLOAD) input_addresses\n FROM tx_relations\n INNER JOIN \"Transaction\" ON tx_relations.tx_id = \"Transaction\".id\n INNER JOIN \"TransactionInput\" ON \"TransactionInput\".tx_id = \"Transaction\".id\n INNER JOIN \"Address\" ON \"Address\".id = \"TransactionInput\".address_id\n LEFT JOIN \"TransactionMetadata\" ON \"Transaction\".id = \"TransactionMetadata\".tx_id\n INNER JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\n GROUP BY \"Transaction\".id, \"Block\".id, \"TransactionMetadata\".id\n )\nSELECT * FROM base_query WHERE NOT :with_input_context!\nUNION ALL (SELECT * FROM query_with_inputs_and_metadata WHERE :with_input_context!)"}; /** * Query generated from SQL: @@ -55,20 +60,47 @@ const sqlHistoryForCredentialsIR: any = {"usedParamSet":{"credentials":true,"rel * "TxCredentialRelation".tx_id > (:after_tx_id) * ORDER BY "TxCredentialRelation".tx_id ASC * LIMIT (:limit) + * ), + * base_query AS ( + * SELECT "Transaction".id, + * "Transaction".payload as "payload!", + * "Transaction".hash as "hash!", + * "Transaction".tx_index as "tx_index!", + * "Transaction".is_valid as "is_valid!", + * "Block".hash AS "block_hash!", + * "Block".epoch as "epoch!", + * "Block".slot as "slot!", + * "Block".era as "era!", + * "Block".height as "height!", + * NULL :: bytea as metadata, + * NULL :: json as input_addresses + * FROM tx_relations + * INNER JOIN "Transaction" ON tx_relations.tx_id = "Transaction".id + * INNER JOIN "Block" ON "Transaction".block_id = "Block".id + * ), + * query_with_inputs_and_metadata AS ( + * SELECT "Transaction".id, + * "Transaction".payload as "payload!", + * "Transaction".hash as "hash!", + * "Transaction".tx_index as "tx_index!", + * "Transaction".is_valid as "is_valid!", + * "Block".hash as "block_hash!", + * "Block".epoch as "epoch!", + * "Block".slot as "slot!", + * "Block".era as "era!", + * "Block".height as "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 * ) - * SELECT "Transaction".id, - * "Transaction".payload, - * "Transaction".hash, - * "Transaction".tx_index, - * "Transaction".is_valid, - * "Block".hash AS block_hash, - * "Block".epoch, - * "Block".slot, - * "Block".era, - * "Block".height - * FROM tx_relations - * INNER JOIN "Transaction" ON tx_relations.tx_id = "Transaction".id - * INNER JOIN "Block" ON "Transaction".block_id = "Block".id + * SELECT * FROM base_query WHERE NOT :with_input_context! + * UNION ALL (SELECT * FROM query_with_inputs_and_metadata WHERE :with_input_context!) * ``` */ 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..fc2b1c3f 100644 --- a/webserver/server/app/models/transaction/sqlHistoryForCredentials.sql +++ b/webserver/server/app/models/transaction/sqlHistoryForCredentials.sql @@ -15,18 +15,45 @@ WITH "TxCredentialRelation".tx_id > (:after_tx_id) ORDER BY "TxCredentialRelation".tx_id ASC LIMIT (:limit) + ), + base_query AS ( + SELECT "Transaction".id, + "Transaction".payload as "payload!", + "Transaction".hash as "hash!", + "Transaction".tx_index as "tx_index!", + "Transaction".is_valid as "is_valid!", + "Block".hash AS "block_hash!", + "Block".epoch as "epoch!", + "Block".slot as "slot!", + "Block".era as "era!", + "Block".height as "height!", + NULL :: bytea as metadata, + NULL :: json as input_addresses + FROM tx_relations + INNER JOIN "Transaction" ON tx_relations.tx_id = "Transaction".id + INNER JOIN "Block" ON "Transaction".block_id = "Block".id + ), + query_with_inputs_and_metadata AS ( + SELECT "Transaction".id, + "Transaction".payload as "payload!", + "Transaction".hash as "hash!", + "Transaction".tx_index as "tx_index!", + "Transaction".is_valid as "is_valid!", + "Block".hash as "block_hash!", + "Block".epoch as "epoch!", + "Block".slot as "slot!", + "Block".era as "era!", + "Block".height as "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 ) -SELECT "Transaction".id, - "Transaction".payload, - "Transaction".hash, - "Transaction".tx_index, - "Transaction".is_valid, - "Block".hash AS block_hash, - "Block".epoch, - "Block".slot, - "Block".era, - "Block".height -FROM tx_relations -INNER JOIN "Transaction" ON tx_relations.tx_id = "Transaction".id -INNER JOIN "Block" ON "Transaction".block_id = "Block".id; +SELECT * FROM base_query WHERE NOT :with_input_context! +UNION ALL (SELECT * FROM query_with_inputs_and_metadata WHERE :with_input_context!); diff --git a/webserver/server/app/services/TransactionHistoryService.ts b/webserver/server/app/services/TransactionHistoryService.ts index c8de3597..417bf6b1 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 } from '@dcspark/cardano-multiplatform-lib-nodejs'; export async function historyForCredentials( request: TransactionPaginationType & { @@ -11,6 +12,7 @@ export async function historyForCredentials( stakeCredentials: Buffer[]; relationFilter: RelationFilter; limit: number; + withInputContext?: boolean; } ): Promise { if (request.stakeCredentials.length === 0) return { transactions: [] }; @@ -21,6 +23,7 @@ export async function historyForCredentials( limit: request.limit.toString(), until_tx_id: request.until.tx_id.toString(), relation: request.relationFilter, + with_input_context: !!request.withInputContext, }, request.dbTx ); @@ -37,8 +40,16 @@ export async function historyForCredentials( }, transaction: { - hash: entry.hash.toString('hex'), - payload: entry.payload.toString('hex'), + ...{ + hash: entry.hash.toString('hex'), + payload: entry.payload.toString('hex'), + }, + ...(request.withInputContext && { + metadata: entry.metadata && entry.metadata.toString('hex'), + inputCredentials: entry.input_addresses + ? (entry.input_addresses as string[]).map(getPaymentCred) + : [], + }), }, })), }; @@ -49,6 +60,7 @@ export async function historyForAddresses( addresses: Buffer[]; dbTx: PoolClient; limit: number; + withInputContext?: boolean; } ): Promise { if (request.addresses?.length === 0) return { transactions: [] }; @@ -58,6 +70,7 @@ export async function historyForAddresses( after_tx_id: (request.after?.tx_id ?? -1)?.toString(), limit: request.limit.toString(), until_tx_id: request.until.tx_id.toString(), + with_input_context: !!request.withInputContext, }, request.dbTx ); @@ -74,9 +87,29 @@ export async function historyForAddresses( }, transaction: { - hash: entry.hash.toString('hex'), - payload: entry.payload.toString('hex'), + ...{ + hash: entry.hash.toString('hex'), + payload: entry.payload.toString('hex'), + }, + ...(request.withInputContext && { + metadata: entry.metadata && entry.metadata.toString('hex'), + inputCredentials: entry.input_addresses + ? (entry.input_addresses as string[]).map(getPaymentCred) + : [], + }), }, })), }; } + +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..bab38426 100644 --- a/webserver/shared/models/TransactionHistory.ts +++ b/webserver/shared/models/TransactionHistory.ts @@ -8,6 +8,13 @@ export type TransactionHistoryRequest = { relationFilter?: RelationFilter; /** Defaults to `ADDRESS_LIMIT.RESPONSE` */ limit?: number; + + /** This limits the transactions in the result to this range of slots. + * Everything else is filtered out */ + slotLimits?: SlotLimits; + /** If this is set to true, the result includes the input addresses (which are + * not part of the tx), and the metadata (if any) */ + withInputContext?: boolean; } & Pagination; export type BlockInfo = BlockSubset & { @@ -33,6 +40,10 @@ export type TransactionInfo = { * @example "84a500818258209cb4f8c2eecccc9f1e13768046f37ef56dcb5a4dc44f58907fe4ae21d7cf621d020181825839019cb581f4337a6142e477af6e00fe41b1fc4a5944a575681b8499a3c0bd07ce733b5911eb657e7aff5d35f8b0682fe0380f7621af2bbcb2f71b0000000586321393021a0002a389031a004b418c048183028200581cbd07ce733b5911eb657e7aff5d35f8b0682fe0380f7621af2bbcb2f7581c53215c471b7ac752e3ddf8f2c4c1e6ed111857bfaa675d5e31ce8bcea1008282582073e584cda9fe483fbefb81c251e616018a2b493ef56820f0095b63adede54ff758404f13df42ef1684a3fd55255d8368c9ecbd15b55e2761a2991cc4f401a753c16d6da1da158e84b87b4de9715af7d9adc0d79a7c1f2c3097228e02b20be4616a0c82582066c606974819f457ceface78ee3c4d181a84ca9927a3cfc92ef8c0b6dd4576e8584014ae9ee9ed5eb5700b6c5ac270543671f5d4f943d4726f4614dc061174ee29db44b9e7fc58e6c98c13fad8594f2633c5ec70a9a87f5cbf130308a42edb553001f5f6" */ payload: string; + + metadata?: string | null; + + inputCredentials?: string[]; }; export type TxAndBlockInfo = { @@ -42,3 +53,10 @@ export type TxAndBlockInfo = { export type TransactionHistoryResponse = { transactions: TxAndBlockInfo[]; }; + +export type SlotLimits = { + // this is exclusive + from: number; + // this is inclusive + to: number; +};