diff --git a/src/app.ts b/src/app.ts index 2ef8b73a..5de37138 100644 --- a/src/app.ts +++ b/src/app.ts @@ -171,6 +171,7 @@ const start = (options = {}): FastifyInstance => { registerRoute(app, import('./routes/txs/hash/withdrawals.js')); registerRoute(app, import('./routes/txs/hash/metadata/index.js')); registerRoute(app, import('./routes/txs/hash/metadata/cbor.js')); + registerRoute(app, import('./routes/txs/hash/required-signers.js')); // utils registerRoute(app, import('./routes/utils/addresses/xpub/xpub/role/index.js')); diff --git a/src/routes/txs/hash/required-signers.ts b/src/routes/txs/hash/required-signers.ts new file mode 100644 index 00000000..2f8a947d --- /dev/null +++ b/src/routes/txs/hash/required-signers.ts @@ -0,0 +1,56 @@ +import { getSchemaForEndpoint } from '@blockfrost/openapi'; +import { isUnpaged } from '../../../utils/routes.js'; +import { toJSONStream } from '../../../utils/string-utils.js'; +import { FastifyInstance, FastifyRequest } from 'fastify'; +import { SQLQuery } from '../../../sql/index.js'; +import * as QueryTypes from '../../../types/queries/tx.js'; +import { getDbSync } from '../../../utils/database.js'; + +async function route(fastify: FastifyInstance) { + fastify.route({ + url: '/txs/:hash/required_signers', + method: 'GET', + schema: getSchemaForEndpoint('/txs/:hash:/required_signers'), + handler: async (request: FastifyRequest, reply) => { + const clientDbSync = await getDbSync(fastify); + + try { + const { rows } = await clientDbSync.query( + SQLQuery.get('txs_hash_wits'), + [request.params.hash], + ); + + clientDbSync.release(); + + if (rows.length === 0) { + return reply.send([]); + } + + const list: string[] = []; + + for (const row of rows) { + list.push(row.hash); + } + + const unpaged = isUnpaged(request); + + if (unpaged) { + // Use of Reply.raw functions is at your own risk as you are skipping all the Fastify logic of handling the HTTP response + // https://www.fastify.io/docs/latest/Reference/Reply/#raw + reply.raw.writeHead(200, { 'Content-Type': 'application/json' }); + await toJSONStream(list, reply.raw); + return reply; + } else { + return reply.send(list); + } + } catch (error) { + if (clientDbSync) { + clientDbSync.release(); + } + throw error; + } + }, + }); +} + +export default route; diff --git a/src/sql/index.ts b/src/sql/index.ts index 89fda6ae..87f93658 100644 --- a/src/sql/index.ts +++ b/src/sql/index.ts @@ -149,6 +149,7 @@ const QUERY_FILES = { txs_hash_mirs: 'txs/txs_hash_mirs.sql', txs_hash_pool_updates: 'txs/txs_hash_pool_updates.sql', txs_hash_metadata: 'txs/txs_hash_metadata.sql', + txs_hash_wits: 'txs/txs_hash_wits.sql', } as const; type QueryKey = keyof typeof QUERY_FILES; diff --git a/src/sql/txs/txs_hash_wits.sql b/src/sql/txs/txs_hash_wits.sql new file mode 100644 index 00000000..ff43821a --- /dev/null +++ b/src/sql/txs/txs_hash_wits.sql @@ -0,0 +1,5 @@ +SELECT encode(wit.hash, 'hex') AS "hash" +FROM tx + JOIN extra_key_witness wit ON (wit.tx_id = tx.id) +WHERE encode(tx.hash, 'hex') = $1 +ORDER BY wit.id diff --git a/src/types/queries/tx.ts b/src/types/queries/tx.ts index c6b5d50c..5194c7f6 100644 --- a/src/types/queries/tx.ts +++ b/src/types/queries/tx.ts @@ -154,3 +154,7 @@ export interface TxRedeemers { fee: string; redeemer_data_hash: string; } + +export interface TxWits { + hash: string; +} diff --git a/test/unit/fixtures/txs.fixtures.ts b/test/unit/fixtures/txs.fixtures.ts index b647f5ea..a9574498 100644 --- a/test/unit/fixtures/txs.fixtures.ts +++ b/test/unit/fixtures/txs.fixtures.ts @@ -1137,6 +1137,20 @@ const response_txs_redeemers = [ }, ]; +const query_txs_required_signers = [ + { hash: 'd52e11f3e48436dd42dbec6d88c239732e503b8b7a32af58e5f87625' }, + { hash: '41b32682c413535dbca5178f92f3cee5dede31b995400b8c371e2469' }, + { hash: 'd52e11f3e48436dd42dbec6d88c239732e503b8b7a32af58e5f87625' }, + { hash: '666414964a05b01cef36427b8a0fb0f621806c43e66e7a4d3cca3bfb' }, +]; + +const response_txs_required_signers = [ + 'd52e11f3e48436dd42dbec6d88c239732e503b8b7a32af58e5f87625', + '41b32682c413535dbca5178f92f3cee5dede31b995400b8c371e2469', + 'd52e11f3e48436dd42dbec6d88c239732e503b8b7a32af58e5f87625', + '666414964a05b01cef36427b8a0fb0f621806c43e66e7a4d3cca3bfb', +]; + const response_404 = { error: 'Not Found', message: 'The requested component has not been found.', @@ -1542,6 +1556,28 @@ export default [ }, response: [], }, + { + name: 'respond with success and data on /txs/:hash/required_signers', + endpoint: '/txs/6e6644e0f8aeec3437bec536408fc007a6147d94098f2dbaeb6ad80d0508631b/required_signers', + sqlQueryMock: { + rows: query_found, + }, + sqlQueryMock2: { + rows: query_txs_required_signers, + }, + response: response_txs_required_signers, + }, + { + name: 'respond with success and data on /txs/:hash/required_signers', + endpoint: '/txs/6e6644e0f8aeec3437bec536408fc007a6147d94098f2dbaeb6ad80d0508631b/required_signers', + sqlQueryMock: { + rows: query_found, + }, + sqlQueryMock2: { + rows: [], + }, + response: [], + }, /* 404s */ @@ -1637,6 +1673,14 @@ export default [ }, response: response_404, }, + { + name: 'respond with 404 and empty data on /txs/:hash/required_signers', + endpoint: '/txs/stonks_tx/required_signers', + sqlQueryMock: { + rows: [], + }, + response: response_404, + }, /* 500s @@ -1778,4 +1822,12 @@ export default [ }, response: response_500, }, + { + name: 'respond with 500 and null on /txs/:hash/required_signers', + endpoint: '/txs/stonks_tx/required_signers', + sqlQueryMock: { + rows: null, + }, + response: response_500, + }, ]; //as const;