Skip to content

Commit

Permalink
CIP-0129 drep ids
Browse files Browse the repository at this point in the history
  • Loading branch information
slowbackspace committed Dec 13, 2024
1 parent 6ba1ac3 commit 5009e1c
Show file tree
Hide file tree
Showing 15 changed files with 393 additions and 138 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
'unicorn/prefer-ternary': 0,
'unicorn/no-null': 'off',
'no-nested-ternary': 'off',
'unicorn/number-literal-case': 'off',
'unicorn/prevent-abbreviations': [
'error',
{
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@blockfrost/blockfrost-js": "5.5.0",
"@blockfrost/blockfrost-utils": "2.8.1",
"@blockfrost/openapi": "0.1.69",
"@emurgo/cardano-serialization-lib-nodejs": "11.5.0",
"@fastify/cors": "^9.0.1",
"@fastify/http-proxy": "^9.5.0",
"@fastify/postgres": "^5.2.2",
Expand Down
14 changes: 7 additions & 7 deletions src/routes/governance/dreps/drep-id/delegators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { validateDRepId } from '../../../../utils/validation.js';
import { validateDRepId } from '../../../../utils/governance.js';

async function route(fastify: FastifyInstance) {
fastify.route({
Expand All @@ -32,9 +32,9 @@ async function route(fastify: FastifyInstance) {
SQLQuery.get('governance_dreps_drep_id_delegators_unpaged'),
[
request.query.order,
drepValidation.raw,
drepValidation.id,
drepValidation.hasScript,
drepValidation.dbSync.raw,
drepValidation.dbSync.id,
drepValidation.dbSync.hasScript,
],
)
: await clientDbSync.query<QueryTypes.DRepsDrepIDDelegators>(
Expand All @@ -43,9 +43,9 @@ async function route(fastify: FastifyInstance) {
request.query.order,
request.query.count,
request.query.page,
drepValidation.raw,
drepValidation.id,
drepValidation.hasScript,
drepValidation.dbSync.raw,
drepValidation.dbSync.id,
drepValidation.dbSync.hasScript,
],
);

Expand Down
10 changes: 6 additions & 4 deletions src/routes/governance/dreps/drep-id/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getDbSync, gracefulRelease } from '../../../../utils/database.js';
import { handle400Custom, handle404 } from '../../../../utils/error-handler.js';
import { SQLQuery } from '../../../../sql/index.js';
import { getSchemaForEndpoint } from '@blockfrost/openapi';
import { validateDRepId } from '../../../../utils/validation.js';
import { DRepValidationResult, validateDRepId, enhanceDRep } from '../../../../utils/governance.js';

async function route(fastify: FastifyInstance) {
fastify.route({
Expand All @@ -14,7 +14,7 @@ async function route(fastify: FastifyInstance) {
schema: getSchemaForEndpoint('/governance/dreps/{drep_id}'),

handler: async (request: FastifyRequest<QueryTypes.RequestDRepID>, reply) => {
let drepValidation;
let drepValidation: DRepValidationResult;

try {
drepValidation = validateDRepId(request.params.drep_id);
Expand All @@ -28,7 +28,7 @@ async function route(fastify: FastifyInstance) {
const { rows }: { rows: ResponseTypes.DRepsDrepID[] } =
await clientDbSync.query<QueryTypes.DRepsDrepID>(
SQLQuery.get('governance_dreps_drep_id'),
[drepValidation.raw, drepValidation.id, drepValidation.hasScript],
[drepValidation.dbSync.raw, drepValidation.dbSync.id, drepValidation.dbSync.hasScript],
);

gracefulRelease(clientDbSync);
Expand All @@ -37,7 +37,9 @@ async function route(fastify: FastifyInstance) {
if (!row) {
return handle404(reply);
}
return reply.send(row);
const data = enhanceDRep(row, drepValidation);

return reply.send(data);
} catch (error) {
gracefulRelease(clientDbSync);
throw error;
Expand Down
9 changes: 6 additions & 3 deletions src/routes/governance/dreps/drep-id/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getDbSync, gracefulRelease } from '../../../../utils/database.js';
import { handle400Custom, handle404 } from '../../../../utils/error-handler.js';
import { SQLQuery } from '../../../../sql/index.js';
import { getSchemaForEndpoint } from '@blockfrost/openapi';
import { validateDRepId } from '../../../../utils/validation.js';
import { validateDRepId, enhanceDRep } from '../../../../utils/governance.js';

async function route(fastify: FastifyInstance) {
fastify.route({
Expand All @@ -28,7 +28,7 @@ async function route(fastify: FastifyInstance) {
const { rows }: { rows: ResponseTypes.DRepsDrepIDMetadata[] } =
await clientDbSync.query<QueryTypes.DRepsDrepIDMetadata>(
SQLQuery.get('governance_dreps_drep_id_metadata'),
[drepValidation.raw, drepValidation.id, drepValidation.hasScript],
[drepValidation.dbSync.raw, drepValidation.dbSync.id, drepValidation.dbSync.hasScript],
);

gracefulRelease(clientDbSync);
Expand All @@ -38,7 +38,10 @@ async function route(fastify: FastifyInstance) {
if (!row) {
return handle404(reply);
}
return reply.send(row);

const data = enhanceDRep(row, drepValidation);

return reply.send(data);
} catch (error) {
gracefulRelease(clientDbSync);
throw error;
Expand Down
14 changes: 7 additions & 7 deletions src/routes/governance/dreps/drep-id/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { validateDRepId } from '../../../../utils/validation.js';
import { validateDRepId } from '../../../../utils/governance.js';

async function route(fastify: FastifyInstance) {
fastify.route({
Expand All @@ -32,9 +32,9 @@ async function route(fastify: FastifyInstance) {
SQLQuery.get('governance_dreps_drep_id_updates_unpaged'),
[
request.query.order,
drepValidation.raw,
drepValidation.id,
drepValidation.hasScript,
drepValidation.dbSync.raw,
drepValidation.dbSync.id,
drepValidation.dbSync.hasScript,
],
)
: await clientDbSync.query<QueryTypes.DRepsDrepIDUpdates>(
Expand All @@ -43,9 +43,9 @@ async function route(fastify: FastifyInstance) {
request.query.order,
request.query.count,
request.query.page,
drepValidation.raw,
drepValidation.id,
drepValidation.hasScript,
drepValidation.dbSync.raw,
drepValidation.dbSync.id,
drepValidation.dbSync.hasScript,
],
);

Expand Down
14 changes: 7 additions & 7 deletions src/routes/governance/dreps/drep-id/votes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { validateDRepId } from '../../../../utils/validation.js';
import { validateDRepId } from '../../../../utils/governance.js';

async function route(fastify: FastifyInstance) {
fastify.route({
Expand All @@ -32,9 +32,9 @@ async function route(fastify: FastifyInstance) {
SQLQuery.get('governance_dreps_drep_id_votes_unpaged'),
[
request.query.order,
drepValidation.raw,
drepValidation.id,
drepValidation.hasScript,
drepValidation.dbSync.raw,
drepValidation.dbSync.id,
drepValidation.dbSync.hasScript,
],
)
: await clientDbSync.query<QueryTypes.DRepsDrepIDUpdates>(
Expand All @@ -43,9 +43,9 @@ async function route(fastify: FastifyInstance) {
request.query.order,
request.query.count,
request.query.page,
drepValidation.raw,
drepValidation.id,
drepValidation.hasScript,
drepValidation.dbSync.raw,
drepValidation.dbSync.id,
drepValidation.dbSync.hasScript,
],
);

Expand Down
2 changes: 2 additions & 0 deletions src/routes/governance/dreps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async function route(fastify: FastifyInstance) {

gracefulRelease(clientDbSync);

// TODO: how to handle cip-0129 in list of dreps?

return reply.send(rows);
} catch (error) {
gracefulRelease(clientDbSync);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async function route(fastify: FastifyInstance) {

gracefulRelease(clientDbSync);

// TODO: CIP-0129 compatible voter field
return reply.send(rows);
} catch (error) {
gracefulRelease(clientDbSync);
Expand Down
176 changes: 176 additions & 0 deletions src/utils/governance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { bech32 } from 'bech32';

export interface DRepValidationResult {
// TODO: maybe rename to raw, raw prop to hex,
dbSync: {
id: string;
raw: string | null;
hasScript: boolean;
};
// cip129 is set when the bechDrepId
cip129: {
// CIP129 bech32 representation of dRep ID
id: string;
// CIP129 hex representation of dRep ID (includes 1 byte header)
hex: string | null;
};
isCip129: boolean;
}

/**
* 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): DRepValidationResult => {
const SPECIAL_DREP_IDS = ['drep_always_abstain', 'drep_always_no_confidence'];

if (SPECIAL_DREP_IDS.includes(bechDrepId)) {
return {
dbSync: {
id: bechDrepId,
raw: null,
hasScript: false,
},
cip129: {
id: bechDrepId,
hex: null,
},
isCip129: false,
};
}
const { prefix, words } = bech32.decode(bechDrepId);

if (prefix !== 'drep' && prefix !== 'drep_script') {
throw new Error('Invalid drep id prefix');
}

const hexBuf = Buffer.from(bech32.fromWords(words));

if (hexBuf.length === 28) {
// 28 bytes of keyHash/scriptHash
// Legacy dbSync-compatible format
const drepIdRaw = `\\x${hexBuf.toString('hex')}`;

const hasScript = prefix === 'drep_script';

const keyTypeNibble = 0x2 << 4; // set keyType to dRep
const credentialTypeNibble = hasScript ? 0x3 : 0x2;

const header = keyTypeNibble | credentialTypeNibble;

const headerBuff = Buffer.alloc(1); // Allocate a 1-byte buffer (adjust size as needed)

headerBuff.writeUInt8(header, 0);

const hexWithHeader = Buffer.concat([headerBuff, hexBuf]);
const idWithHeader = bech32.encode('drep', bech32.toWords(hexWithHeader));

return {
dbSync: {
id: bechDrepId,
raw: drepIdRaw,
hasScript: hasScript,
},
cip129: {
id: idWithHeader,
hex: hexWithHeader.toString('hex'),
},
isCip129: false,
};
} else {
// CIP-0129 with 1-byte header
// Extract the first byte
const headerByte = hexBuf[0];

// Decode the first nibble (4 bits) for key type
const keyTypeNibble = (headerByte >> 4) & 0x0f;
let keyType: 'cc_hot' | 'cc_cold' | 'drep';

switch (keyTypeNibble) {
case 0x0: {
keyType = 'cc_hot'; // Hot Credential
break;
}
case 0x1: {
keyType = 'cc_cold'; // Cold Credential
break;
}
case 0x2: {
keyType = 'drep'; // Delegation Representative
break;
}
default: {
throw new Error('Invalid key type in header');
}
}

if (keyType !== 'drep') {
throw new Error('Invalid key type in header for dRep');
}

// Decode the second nibble (4 bits) for credential type
const credentialTypeNibble = headerByte & 0x0f;
let credentialType: 'keyHash' | 'scriptHash';

switch (credentialTypeNibble) {
case 0x2: {
credentialType = 'keyHash'; // Key Hash Credential
break;
}
case 0x3: {
credentialType = 'scriptHash'; // Script Hash Credential
break;
}
default: {
throw new Error('Invalid credential type in header');
}
}

// Extract the rest of the buffer (excluding the header byte)
const drepIdRaw = hexBuf.subarray(1).toString('hex');

const legacyBech32 = bech32.encode('drep', bech32.toWords(hexBuf.subarray(1)));

return {
dbSync: {
id: legacyBech32,
raw: `\\x${drepIdRaw}`,
hasScript: credentialType === 'scriptHash',
},
cip129: {
id: bechDrepId,
hex: hexBuf.toString('hex'),
},
isCip129: true,
};
}
};

/**
* Transform DRep dbsync data to CIP-0129 format if necessary
*
* @param {{drep_id: string; hex: string}} data - DRep dbsync data
* @returns Object - DRep data object with CIP-0129 compatible bech32 ID and hex in case of CIP-0129 drepId. Otherwise the data remain unmodified.
*
* @throws {Error} If the DRep ID prefix is invalid, an error is thrown.
*/
export const enhanceDRep = <T extends { drep_id: string; hex: string }>(
data: T,
dRepValidationResult: DRepValidationResult,
) => {
if (dRepValidationResult.isCip129) {
return {
...data,
drep_id: dRepValidationResult.cip129.id,
hex: dRepValidationResult.cip129.hex,
};
} else {
return data;
}
};
Loading

0 comments on commit 5009e1c

Please sign in to comment.