diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fddd1ce..bc848955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Retrieving data for special dreps `drep_always_abstain` and `drep_always_no_confidence` + ## [2.2.1] - 2024-09-24 ### Fixed diff --git a/src/routes/governance/dreps/drep-id/delegators.ts b/src/routes/governance/dreps/drep-id/delegators.ts index 88ab78bf..d3dd7442 100644 --- a/src/routes/governance/dreps/drep-id/delegators.ts +++ b/src/routes/governance/dreps/drep-id/delegators.ts @@ -6,7 +6,7 @@ import { SQLQuery } from '../../../../sql/index.js'; import { getSchemaForEndpoint } from '@blockfrost/openapi'; import { isUnpaged } from '../../../../utils/routes.js'; import { handle400Custom } from '@blockfrost/blockfrost-utils/lib/fastify.js'; -import { drepIdToRaw } from '../../../../utils/bech32.js'; +import { validateDRepId } from '../../../../utils/validation.js'; async function route(fastify: FastifyInstance) { fastify.route({ @@ -15,10 +15,10 @@ async function route(fastify: FastifyInstance) { schema: getSchemaForEndpoint('/governance/dreps/{drep_id}/delegators'), handler: async (request: FastifyRequest, reply) => { - let drepHex; + let drepValidation; try { - drepHex = drepIdToRaw(request.params.drep_id); + drepValidation = validateDRepId(request.params.drep_id); } catch { return handle400Custom(reply, 'Invalid or malformed drep id.'); } @@ -30,11 +30,17 @@ async function route(fastify: FastifyInstance) { const { rows }: { rows: ResponseTypes.DRepsDrepIDDelegators } = unpaged ? await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_delegators_unpaged'), - [request.query.order], + [request.query.order, drepValidation.raw, drepValidation.id], ) : await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_delegators'), - [request.query.order, request.query.count, request.query.page, drepHex], + [ + request.query.order, + request.query.count, + request.query.page, + drepValidation.raw, + drepValidation.id, + ], ); clientDbSync.release(); diff --git a/src/routes/governance/dreps/drep-id/index.ts b/src/routes/governance/dreps/drep-id/index.ts index 8beb8c46..42996183 100644 --- a/src/routes/governance/dreps/drep-id/index.ts +++ b/src/routes/governance/dreps/drep-id/index.ts @@ -5,7 +5,7 @@ import { getDbSync } from '../../../../utils/database.js'; import { handle400Custom, handle404 } from '../../../../utils/error-handler.js'; import { SQLQuery } from '../../../../sql/index.js'; import { getSchemaForEndpoint } from '@blockfrost/openapi'; -import { drepIdToRaw } from '../../../../utils/bech32.js'; +import { validateDRepId } from '../../../../utils/validation.js'; async function route(fastify: FastifyInstance) { fastify.route({ @@ -14,10 +14,10 @@ async function route(fastify: FastifyInstance) { schema: getSchemaForEndpoint('/governance/dreps/{drep_id}'), handler: async (request: FastifyRequest, reply) => { - let drepHex; + let drepValidation; try { - drepHex = drepIdToRaw(request.params.drep_id); + drepValidation = validateDRepId(request.params.drep_id); } catch { return handle400Custom(reply, 'Invalid or malformed drep id.'); } @@ -28,7 +28,7 @@ async function route(fastify: FastifyInstance) { const { rows }: { rows: ResponseTypes.DRepsDrepID[] } = await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id'), - [drepHex], + [drepValidation.raw, drepValidation.id], ); clientDbSync.release(); diff --git a/src/routes/governance/dreps/drep-id/metadata.ts b/src/routes/governance/dreps/drep-id/metadata.ts index e37ebed4..f2ff504d 100644 --- a/src/routes/governance/dreps/drep-id/metadata.ts +++ b/src/routes/governance/dreps/drep-id/metadata.ts @@ -5,7 +5,7 @@ import { getDbSync } from '../../../../utils/database.js'; import { handle400Custom, handle404 } from '../../../../utils/error-handler.js'; import { SQLQuery } from '../../../../sql/index.js'; import { getSchemaForEndpoint } from '@blockfrost/openapi'; -import { drepIdToRaw } from '../../../../utils/bech32.js'; +import { validateDRepId } from '../../../../utils/validation.js'; async function route(fastify: FastifyInstance) { fastify.route({ @@ -14,10 +14,10 @@ async function route(fastify: FastifyInstance) { schema: getSchemaForEndpoint('/governance/dreps/{drep_id}/metadata'), handler: async (request: FastifyRequest, reply) => { - let drepHex; + let drepValidation; try { - drepHex = drepIdToRaw(request.params.drep_id); + drepValidation = validateDRepId(request.params.drep_id); } catch { return handle400Custom(reply, 'Invalid or malformed drep id.'); } @@ -28,7 +28,7 @@ async function route(fastify: FastifyInstance) { const { rows }: { rows: ResponseTypes.DRepsDrepIDMetadata[] } = await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_metadata'), - [drepHex], + [drepValidation.raw, drepValidation.id], ); clientDbSync.release(); diff --git a/src/routes/governance/dreps/drep-id/updates.ts b/src/routes/governance/dreps/drep-id/updates.ts index 715583fd..47b62b4b 100644 --- a/src/routes/governance/dreps/drep-id/updates.ts +++ b/src/routes/governance/dreps/drep-id/updates.ts @@ -6,7 +6,7 @@ import { SQLQuery } from '../../../../sql/index.js'; import { getSchemaForEndpoint } from '@blockfrost/openapi'; import { isUnpaged } from '../../../../utils/routes.js'; import { handle400Custom } from '@blockfrost/blockfrost-utils/lib/fastify.js'; -import { drepIdToRaw } from '../../../../utils/bech32.js'; +import { validateDRepId } from '../../../../utils/validation.js'; async function route(fastify: FastifyInstance) { fastify.route({ @@ -15,10 +15,10 @@ async function route(fastify: FastifyInstance) { schema: getSchemaForEndpoint('/governance/dreps/{drep_id}/updates'), handler: async (request: FastifyRequest, reply) => { - let drepHex; + let drepValidation; try { - drepHex = drepIdToRaw(request.params.drep_id); + drepValidation = validateDRepId(request.params.drep_id); } catch { return handle400Custom(reply, 'Invalid or malformed drep id.'); } @@ -30,11 +30,17 @@ async function route(fastify: FastifyInstance) { const { rows }: { rows: ResponseTypes.DRepsDrepIDUpdates } = unpaged ? await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_updates_unpaged'), - [request.query.order, request.params.drep_id], + [request.query.order, drepValidation.raw, drepValidation.id], ) : await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_updates'), - [request.query.order, request.query.count, request.query.page, drepHex], + [ + request.query.order, + request.query.count, + request.query.page, + drepValidation.raw, + drepValidation.id, + ], ); clientDbSync.release(); diff --git a/src/routes/governance/dreps/drep-id/votes.ts b/src/routes/governance/dreps/drep-id/votes.ts index 3807cc16..9d80c9bb 100644 --- a/src/routes/governance/dreps/drep-id/votes.ts +++ b/src/routes/governance/dreps/drep-id/votes.ts @@ -6,7 +6,7 @@ import { SQLQuery } from '../../../../sql/index.js'; import { getSchemaForEndpoint } from '@blockfrost/openapi'; import { isUnpaged } from '../../../../utils/routes.js'; import { handle400Custom } from '@blockfrost/blockfrost-utils/lib/fastify.js'; -import { drepIdToRaw } from '../../../../utils/bech32.js'; +import { validateDRepId } from '../../../../utils/validation.js'; async function route(fastify: FastifyInstance) { fastify.route({ @@ -15,10 +15,10 @@ async function route(fastify: FastifyInstance) { schema: getSchemaForEndpoint('/governance/dreps/{drep_id}/votes'), handler: async (request: FastifyRequest, reply) => { - let drepHex; + let drepValidation; try { - drepHex = drepIdToRaw(request.params.drep_id); + drepValidation = validateDRepId(request.params.drep_id); } catch { return handle400Custom(reply, 'Invalid or malformed drep id.'); } @@ -30,11 +30,17 @@ async function route(fastify: FastifyInstance) { const { rows }: { rows: ResponseTypes.DRepsDrepIDUpdates } = unpaged ? await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_votes_unpaged'), - [request.query.order, request.params.drep_id], + [request.query.order, drepValidation.raw, drepValidation.id], ) : await clientDbSync.query( SQLQuery.get('governance_dreps_drep_id_votes'), - [request.query.order, request.query.count, request.query.page, drepHex], + [ + request.query.order, + request.query.count, + request.query.page, + drepValidation.raw, + drepValidation.id, + ], ); clientDbSync.release(); diff --git a/src/sql/governance/dreps_drep_id.sql b/src/sql/governance/dreps_drep_id.sql index 41d01214..7c4d2ba9 100644 --- a/src/sql/governance/dreps_drep_id.sql +++ b/src/sql/governance/dreps_drep_id.sql @@ -38,6 +38,11 @@ FROM drep_hash dh FROM queried_epoch ) ) -WHERE dh.raw = $1 +WHERE ( + CASE + WHEN $1::bytea IS NOT NULL THEN dh.raw = $1 + ELSE dh.view = $2 + END + ) ORDER BY (tx_id, cert_index) DESC LIMIT 1 \ No newline at end of file diff --git a/src/sql/governance/dreps_drep_id_delegators.sql b/src/sql/governance/dreps_drep_id_delegators.sql index 2d84ac9e..78a530ed 100644 --- a/src/sql/governance/dreps_drep_id_delegators.sql +++ b/src/sql/governance/dreps_drep_id_delegators.sql @@ -34,7 +34,10 @@ FROM ( FROM delegation_vote dv JOIN drep_hash dh ON (dh.id = dv.drep_hash_id) JOIN stake_address sa ON (sa.id = dv.addr_id) - WHERE dh.raw = $4 + WHERE ( + ($4 IS NOT NULL AND dh.raw = $4::bytea) OR + ($4 IS NULL AND dh.view = $5) + ) AND dv.id = ( SELECT MAX(id) FROM delegation_vote diff --git a/src/sql/governance/dreps_drep_id_metadata.sql b/src/sql/governance/dreps_drep_id_metadata.sql index fe3d88fc..301abea7 100644 --- a/src/sql/governance/dreps_drep_id_metadata.sql +++ b/src/sql/governance/dreps_drep_id_metadata.sql @@ -8,6 +8,9 @@ FROM drep_hash dh JOIN drep_registration dr ON (dh.id = dr.drep_hash_id) JOIN voting_anchor va ON (dr.voting_anchor_id = va.id) JOIN off_chain_vote_data ocvd ON (ocvd.voting_anchor_id = va.id) -WHERE dh.raw = $1 +WHERE ( + ($1 IS NOT NULL AND dh.raw = $1::bytea) OR + ($1 IS NULL AND dh.view = $2) + ) ORDER BY (dr.tx_id, dr.cert_index) DESC LIMIT 1 \ No newline at end of file diff --git a/src/sql/governance/dreps_drep_id_updates.sql b/src/sql/governance/dreps_drep_id_updates.sql index d3357a7a..11ea4acf 100644 --- a/src/sql/governance/dreps_drep_id_updates.sql +++ b/src/sql/governance/dreps_drep_id_updates.sql @@ -9,7 +9,10 @@ SELECT encode(tx.hash, 'hex') AS "tx_hash", FROM drep_hash dh JOIN drep_registration dr ON (dh.id = dr.drep_hash_id) JOIN tx ON (dr.tx_id = tx.id) -WHERE dh.raw = $4 +WHERE ( + ($4 IS NOT NULL AND dh.raw = $4::bytea) OR + ($4 IS NULL AND dh.view = $5) + ) ORDER BY CASE WHEN LOWER($1) = 'desc' THEN dr.id END DESC, diff --git a/src/sql/governance/dreps_drep_id_votes.sql b/src/sql/governance/dreps_drep_id_votes.sql index 350d4b50..3df605fb 100644 --- a/src/sql/governance/dreps_drep_id_votes.sql +++ b/src/sql/governance/dreps_drep_id_votes.sql @@ -4,7 +4,10 @@ SELECT encode(tx.hash, 'hex') AS "tx_hash", FROM voting_procedure vp JOIN drep_hash dh ON (vp.drep_voter = dh.id) JOIN tx ON (vp.tx_id = tx.id) -WHERE dh.raw = $4 +WHERE ( + ($4 IS NOT NULL AND dh.raw = $4::bytea) OR + ($4 IS NULL AND dh.view = $5) + ) ORDER BY CASE WHEN LOWER($1) = 'desc' THEN vp.id END DESC, diff --git a/src/sql/governance/unpaged/dreps_drep_id_delegators.sql b/src/sql/governance/unpaged/dreps_drep_id_delegators.sql index 27b0e597..813b5d4c 100644 --- a/src/sql/governance/unpaged/dreps_drep_id_delegators.sql +++ b/src/sql/governance/unpaged/dreps_drep_id_delegators.sql @@ -34,7 +34,10 @@ FROM ( FROM delegation_vote dv JOIN drep_hash dh ON (dh.id = dv.drep_hash_id) JOIN stake_address sa ON (sa.id = dv.addr_id) - WHERE dh.view = $2 + WHERE ( + ($2 IS NOT NULL AND dh.raw = $2::bytea) OR + ($2 IS NULL AND dh.view = $3) + ) AND dv.id = ( SELECT MAX(id) FROM delegation_vote diff --git a/src/sql/governance/unpaged/dreps_drep_id_updates.sql b/src/sql/governance/unpaged/dreps_drep_id_updates.sql index ff4425b2..7fae339e 100644 --- a/src/sql/governance/unpaged/dreps_drep_id_updates.sql +++ b/src/sql/governance/unpaged/dreps_drep_id_updates.sql @@ -9,7 +9,10 @@ SELECT encode(tx.hash, 'hex') AS "tx_hash", FROM drep_hash dh JOIN drep_registration dr ON (dh.id = dr.drep_hash_id) JOIN tx ON (dr.tx_id = tx.id) -WHERE dh.view = $2 +WHERE ( + ($2 IS NOT NULL AND dh.raw = $2::bytea) OR + ($2 IS NULL AND dh.view = $3) + ) ORDER BY CASE WHEN LOWER($1) = 'desc' THEN dr.id END DESC, diff --git a/src/sql/governance/unpaged/dreps_drep_id_votes.sql b/src/sql/governance/unpaged/dreps_drep_id_votes.sql index 403acc95..5877fd6b 100644 --- a/src/sql/governance/unpaged/dreps_drep_id_votes.sql +++ b/src/sql/governance/unpaged/dreps_drep_id_votes.sql @@ -4,7 +4,10 @@ SELECT encode(tx.hash, 'hex') AS "tx_hash", FROM voting_procedure vp JOIN drep_hash dh ON (vp.drep_voter = dh.id) JOIN tx ON (vp.tx_id = tx.id) -WHERE dh.view = $2 +WHERE ( + ($2 IS NOT NULL AND dh.raw = $3::bytea) OR + ($2 IS NULL AND dh.view = $3) + ) ORDER BY CASE WHEN LOWER($1) = 'desc' THEN vp.id END DESC, diff --git a/src/utils/bech32.ts b/src/utils/bech32.ts deleted file mode 100644 index 4e0d0210..00000000 --- a/src/utils/bech32.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { bech32 } from 'bech32'; - -export const drepIdToRaw = (bechDrepId: string): string => { - const { prefix, words } = bech32.decode(bechDrepId); - - console.log('prefix', prefix); - if (prefix !== 'drep' && prefix !== 'drep_script') { - throw new Error('Invalid drep id prefix'); - } - - const hashBuf = Buffer.from(bech32.fromWords(words)); - - return `\\x${Buffer.from(hashBuf).toString('hex')}`; -}; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 359549d9..bb9f6cf1 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,5 +1,6 @@ import { validation } from '@blockfrost/blockfrost-utils'; import { getConfig } from '../config.js'; +import { bech32 } from 'bech32'; export const isNumber = validation.isNumber; export const validateHex = validation.validateHex; @@ -31,3 +32,42 @@ export const isTestnet = (): boolean => { return network === 'mainnet' ? false : true; }; + +/** + * Validates a DRep ID and returns both the ID and its raw format if applicable. + * + * @param {string} bechDrepId - The DRep ID in Bech32 format that needs to be validated. + * @returns {{ id: string, raw: string | null }} - An object containing the validated ID and its raw form. + * - `id`: The original DRep ID. + * - `raw`: The raw format of the DRep ID in hexadecimal if applicable, or `null` for special cases ()drep_always_abstain, drep_always_no_confidence). + * + * @throws {Error} If the DRep ID prefix is invalid, an error is thrown. + */ +export const validateDRepId = ( + bechDrepId: string, +): { + id: string; + raw: string | null; +} => { + const SPECIAL_DREP_IDS = ['drep_always_abstain', 'drep_always_no_confidence']; + + if (SPECIAL_DREP_IDS.includes(bechDrepId)) { + return { + id: bechDrepId, + raw: null, + }; + } + const { prefix, words } = bech32.decode(bechDrepId); + + if (prefix !== 'drep' && prefix !== 'drep_script') { + throw new Error('Invalid drep id prefix'); + } + + const hashBuf = Buffer.from(bech32.fromWords(words)); + const drepIdRaw = `\\x${Buffer.from(hashBuf).toString('hex')}`; + + return { + id: bechDrepId, + raw: drepIdRaw, + }; +}; diff --git a/test/unit/tests/utils/bech32.ts b/test/unit/tests/utils/bech32.ts deleted file mode 100644 index 019cbfbc..00000000 --- a/test/unit/tests/utils/bech32.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { drepIdToRaw } from '../../../../src/utils/bech32.js'; -import { describe, expect, test } from 'vitest'; - -describe('bech32', () => { - test('drepIdToRaw', () => { - expect(drepIdToRaw('drep1y3wylkrkyt3q6u078ajh8f2henflpsq5hrcqhfa3yfmlqx7z66n')).toEqual( - '\\x245c4fd87622e20d71fe3f6573a557ccd3f0c014b8f00ba7b12277f0', - ); - - expect(drepIdToRaw('drep1edu7a90eszdus0hguck2w3lxr5r0juvc9frrxv3d2e6fcnqte0e')).toEqual( - '\\xcb79ee95f9809bc83ee8e62ca747e61d06f971982a4633322d56749c', - ); - - expect(drepIdToRaw('drep_script1hmgwyt6zv89j5htlnwcttk95lr0x7r87sxzr9dumxnc3vadhlap')).toEqual( - '\\xbed0e22f4261cb2a5d7f9bb0b5d8b4f8de6f0cfe818432b79b34f116', - ); - - expect(drepIdToRaw('drep1hmgwyt6zv89j5htlnwcttk95lr0x7r87sxzr9dumxnc3vj02hpq')).toEqual( - '\\xbed0e22f4261cb2a5d7f9bb0b5d8b4f8de6f0cfe818432b79b34f116', - ); - expect(drepIdToRaw('drep_script16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5sh6f6d')).toEqual( - '\\xd04d39c4e4b42e9edddbb741bb6636683b6822ae2c68df704b65ce4a', - ); - }); -}); diff --git a/test/unit/tests/utils/validation.ts b/test/unit/tests/utils/validation.ts index 75436c00..4af3ed62 100644 --- a/test/unit/tests/utils/validation.ts +++ b/test/unit/tests/utils/validation.ts @@ -104,4 +104,54 @@ describe('validation-format-utils', () => { expect(result).toStrictEqual(fixture.response); }); }); + + test('validationUtils.validateDRepId', () => { + expect(validationUtils.validateDRepId('drep_always_abstain')).toStrictEqual({ + id: 'drep_always_abstain', + raw: null, + }); + + expect(validationUtils.validateDRepId('drep_always_no_confidence')).toStrictEqual({ + id: 'drep_always_no_confidence', + raw: null, + }); + + expect( + validationUtils.validateDRepId('drep1y3wylkrkyt3q6u078ajh8f2henflpsq5hrcqhfa3yfmlqx7z66n'), + ).toStrictEqual({ + id: 'drep1y3wylkrkyt3q6u078ajh8f2henflpsq5hrcqhfa3yfmlqx7z66n', + raw: '\\x245c4fd87622e20d71fe3f6573a557ccd3f0c014b8f00ba7b12277f0', + }); + + expect( + validationUtils.validateDRepId('drep1edu7a90eszdus0hguck2w3lxr5r0juvc9frrxv3d2e6fcnqte0e'), + ).toStrictEqual({ + id: 'drep1edu7a90eszdus0hguck2w3lxr5r0juvc9frrxv3d2e6fcnqte0e', + raw: '\\xcb79ee95f9809bc83ee8e62ca747e61d06f971982a4633322d56749c', + }); + + expect( + validationUtils.validateDRepId( + 'drep_script1hmgwyt6zv89j5htlnwcttk95lr0x7r87sxzr9dumxnc3vadhlap', + ), + ).toStrictEqual({ + id: 'drep_script1hmgwyt6zv89j5htlnwcttk95lr0x7r87sxzr9dumxnc3vadhlap', + raw: '\\xbed0e22f4261cb2a5d7f9bb0b5d8b4f8de6f0cfe818432b79b34f116', + }); + + expect( + validationUtils.validateDRepId('drep1hmgwyt6zv89j5htlnwcttk95lr0x7r87sxzr9dumxnc3vj02hpq'), + ).toStrictEqual({ + id: 'drep1hmgwyt6zv89j5htlnwcttk95lr0x7r87sxzr9dumxnc3vj02hpq', + raw: '\\xbed0e22f4261cb2a5d7f9bb0b5d8b4f8de6f0cfe818432b79b34f116', + }); + expect( + validationUtils.validateDRepId( + 'drep_script16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5sh6f6d', + ), + ).toStrictEqual({ + id: 'drep_script16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5sh6f6d', + raw: '\\xd04d39c4e4b42e9edddbb741bb6636683b6822ae2c68df704b65ce4a', + }); + }); });