diff --git a/.github/workflows/frontend-typecheck.yml b/.github/workflows/frontend-typecheck.yml index 1802636b..02b38349 100644 --- a/.github/workflows/frontend-typecheck.yml +++ b/.github/workflows/frontend-typecheck.yml @@ -22,7 +22,7 @@ jobs: node-version: '18' - name: Cache Node.js modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: frontend/node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('frontend/yarn.lock') }} diff --git a/api/backend/app/services/proposal_service.py b/api/backend/app/services/proposal_service.py index c40d4127..87e47915 100644 --- a/api/backend/app/services/proposal_service.py +++ b/api/backend/app/services/proposal_service.py @@ -75,9 +75,9 @@ async def add_metadata_and_agent_detail_in_internal_proposal( where={"id": proposal.agentId}, ) proposal_dict = proposal_data | {"agentId": proposal.agentId, "agentName": agent.name} - if proposal_dict.get("metadataHash") and proposal_dict.get("url"): - url = proposal_dict.get("url") - metadata_hash = proposal_dict.get("metadataHash") + if proposal_dict.get("proposal").get("metadataHash") and proposal_dict.get("proposal").get("metadataUrl"): + url = proposal_dict.get("proposal").get("metadataUrl") + metadata_hash = proposal_dict.get("proposal").get("metadataHash") await self._fetch_metadata(metadata_hash, url, proposal_dict) if proposal_dict: results[index] = proposal_dict @@ -95,12 +95,17 @@ async def get_external_proposals(self, page: int, pageSize: int, sort: str, sear status_code=500, content="Error fetching External Proposals , DB Sync upstream service error" ) response_json = await response.json() - async with asyncio.TaskGroup() as tg: for index, proposal in enumerate(response_json["items"]): tg.create_task(self.add_agent_in_external_proposals(index, proposal, response_json["items"])) - if proposal.get("metadataHash") and proposal.get("url"): - tg.create_task(self._fetch_metadata(proposal.get("metadataHash"), proposal.get("url"), proposal)) + if proposal.get("proposal").get("metadataHash") and proposal.get("proposal").get("metadataUrl"): + tg.create_task( + self._fetch_metadata( + proposal.get("proposal").get("metadataHash"), + proposal.get("proposal").get("metadataUrl"), + proposal, + ) + ) return Page( items=response_json["items"], total=response_json["totalCount"], @@ -110,9 +115,11 @@ async def get_external_proposals(self, page: int, pageSize: int, sort: str, sear ) async def add_agent_in_external_proposals(self, index: int, proposal: Any, proposals: List[Any]): - if proposal["txHash"]: + if proposal["createdAt"]["tx"]: try: - internal_proposal = await self.db.prisma.triggerhistory.find_first(where={"txHash": proposal["txHash"]}) + internal_proposal = await self.db.prisma.triggerhistory.find_first( + where={"txHash": proposal["createdAt"]["tx"]} + ) if internal_proposal: agent = await self.db.prisma.agent.find_first(where={"id": internal_proposal.agentId}) proposal["agentId"] = agent.id diff --git a/dbsync-api/src/controllers/drep.ts b/dbsync-api/src/controllers/drep.ts index 46c0e844..2ff28f9e 100644 --- a/dbsync-api/src/controllers/drep.ts +++ b/dbsync-api/src/controllers/drep.ts @@ -67,8 +67,9 @@ const getDrepActiveDelegators = async (req: Request, res: Response) => { const getDrepLiveDelegators = async (req: Request, res: Response) => { const dRepId = decodeDrep(req.params.id as string) - const activeDelegators = await fetchDrepLiveDelegators(dRepId.credential, dRepId.isScript) - return res.status(200).json(activeDelegators) + const balance = req.query.balance === 'true' + const liveDelegators = await fetchDrepLiveDelegators(dRepId.credential, dRepId.isScript, balance) + return res.status(200).json(liveDelegators) } router.get('/', handlerWrapper(getDrepList)) diff --git a/dbsync-api/src/controllers/proposal.ts b/dbsync-api/src/controllers/proposal.ts index dc30ef7d..b0eb4971 100644 --- a/dbsync-api/src/controllers/proposal.ts +++ b/dbsync-api/src/controllers/proposal.ts @@ -1,30 +1,66 @@ -import { Request, Response, Router } from "express"; -import { handlerWrapper } from "../errors/AppError"; -import { fetchProposals } from "../repository/proposal"; -import { validateHash } from "../helpers/validator"; -import { ProposalTypes, SortTypes } from "../types/proposal"; - -const router = Router(); - - -const getProposals=async(req:Request,res:Response)=>{ - const size = req.query.size ? +req.query.size : 10 - const page = req.query.page ? +req.query.page : 1 - const type = req.query.type ? req.query.type as ProposalTypes : undefined - const sort = req.query.sort ? req.query.sort as SortTypes : undefined - let proposal = req.query.proposal as string - - if (proposal){ - console.log(proposal,validateHash(proposal)) - if (!validateHash(proposal)){ - return res.status(400).json({message:"Provide valid proposal Id"}) +import { Request, Response, Router } from 'express' +import { handlerWrapper } from '../errors/AppError' +import { fetchProposalById, fetchProposalVoteCount, fetchProposalVotes, fetchProposals } from '../repository/proposal' +import { formatProposal, validateHash, validateVoter } from '../helpers/validator' +import { ProposalTypes, SortTypes } from '../types/proposal' + +const router = Router() + +const getProposals = async (req: Request, res: Response) => { + const size = req.query.size ? +req.query.size : 10 + const page = req.query.page ? +req.query.page : 1 + const type = req.query.type ? (req.query.type as ProposalTypes) : undefined + const sort = req.query.sort ? (req.query.sort as SortTypes) : undefined + const includeVoteCount = 'true' == (req.query.vote_count as string) + let proposal = req.query.proposal as string + + if (proposal) { + if (!validateHash(proposal)) { + return res.status(400).json({ message: 'Provide valid proposal Id' }) + } + proposal = proposal.includes('#') ? proposal.slice(0, -2) : proposal + } + const { items, totalCount } = await fetchProposals(page, size, proposal, type, sort, includeVoteCount) + return res.status(200).json({ totalCount: Math.round(totalCount / size), page, size, items }) +} + +const getProposalVoteCount = async (req: Request, res: Response) => { + const proposal = formatProposal(req.params.id as string) + let voter + if (req.params.voter) { + voter = validateVoter(req.params.voter as string) + } + if (!proposal) { + return res.status(400).json({ message: 'Provide valid govAction Id (hash#index) or bech32' }) + } + const totalVoteCount = await fetchProposalVoteCount(proposal.id, proposal.ix, voter) + return res.status(200).json(totalVoteCount) +} + +const getProposalVotes = async (req: Request, res: Response) => { + const proposal = formatProposal(req.params.id as string) + const includeVotingPower = 'true' == (req.query.voting_power as string) + if (!proposal) { + return res.status(400).json({ message: 'Provide valid govAction Id (hash#index) or bech32' }) + } + const votes = await fetchProposalVotes(proposal.id, proposal.ix, includeVotingPower) + return res.status(200).json(votes) +} + +const getProposalById = async (req: Request, res: Response) => { + const proposal = formatProposal(req.params.id as string) + const includeVoteCount = 'true' == (req.query.vote_count as string) + if (!proposal) { + return res.status(400).json({ message: 'Provide valid govAction Id (hash#index) or bech32' }) } - proposal = proposal.includes('#')? proposal.slice(0,-2):proposal - } -const { items,totalCount } = await fetchProposals(page,size,proposal,type,sort) - return res.status(200).json({totalCount:Math.round(totalCount/size),page,size,items}) + const proposalDetails = await fetchProposalById(proposal.id, proposal.ix, includeVoteCount) + return res.status(200).json(proposalDetails) } -router.get('/',handlerWrapper(getProposals)) +router.get('/', handlerWrapper(getProposals)) +router.get('/:id/vote-count', handlerWrapper(getProposalVoteCount)) +router.get('/:id/vote-count/:voter', handlerWrapper(getProposalVoteCount)) +router.get('/:id/votes', handlerWrapper(getProposalVotes)) +router.get('/:id', handlerWrapper(getProposalById)) export default router diff --git a/dbsync-api/src/helpers/validator.ts b/dbsync-api/src/helpers/validator.ts index bab9b067..4086d972 100644 --- a/dbsync-api/src/helpers/validator.ts +++ b/dbsync-api/src/helpers/validator.ts @@ -1,28 +1,28 @@ -import {bech32} from "bech32"; -import AppError from "../errors/AppError"; +import { bech32 } from 'bech32' +import AppError from '../errors/AppError' export function convertToHexIfBech32(address: string): string { if (!address) return '' - if (address.startsWith('stake') || address.startsWith('drep')) { - const decoded = bech32.decode(address); - const data = bech32.fromWords(decoded.words); - return Buffer.from(data).toString('hex'); + if (address.startsWith('stake') || address.startsWith('drep') || address.startsWith('gov_action')) { + const decoded = bech32.decode(address) + const data = bech32.fromWords(decoded.words) + return Buffer.from(data).toString('hex') } else if (address.length === 58 && (address.startsWith('22') || address.startsWith('23'))) { return address.slice(2) } return address } -export function decodeDrep(address: string): { credential: string, isScript?: boolean } { - const drep = decodeAddress(address); +export function decodeDrep(address: string): { credential: string; isScript?: boolean } { + const drep = decodeAddress(address) if (drep.bech32Prefix === 'drep_script') { return { credential: drep.credential.toString('hex'), - isScript: true + isScript: true, } } else { if (drep.dataHeader.length > 1) { - throw new AppError("Drep credential contains invalid header: " + address) + throw new AppError('Drep credential contains invalid header: ' + address) } let isScript const header = drep.dataHeader.at(0) @@ -31,11 +31,11 @@ export function decodeDrep(address: string): { credential: string, isScript?: bo } else if (header == 0x23) { isScript = true } else if (header) { - throw new AppError("Drep credential contains invalid header 0x" + header.toString(16) + ": " + address) + throw new AppError('Drep credential contains invalid header 0x' + header.toString(16) + ': ' + address) } return { credential: drep.credential.toString('hex'), - isScript: isScript + isScript: isScript, } } } @@ -48,7 +48,7 @@ export function encodeDrep(hexValue: string, isScript: boolean) { } else { drepHexVal = '22' + drepHexVal } - const drepBufferVal = Buffer.from(drepHexVal,'hex') + const drepBufferVal = Buffer.from(drepHexVal, 'hex') const bech32Words = bech32.toWords(drepBufferVal) return bech32.encode(isScript ? 'drep_script' : 'drep', bech32Words, 100) } catch (e: any) { @@ -56,54 +56,51 @@ export function encodeDrep(hexValue: string, isScript: boolean) { } } - -export function decodeAddress(address: string): { bech32Prefix: string, dataHeader: Buffer, credential: Buffer } { - if (!address) return {bech32Prefix: "", dataHeader: Buffer.alloc(0), credential: Buffer.alloc(0)}; // Return empty if address is falsy +export function decodeAddress(address: string): { bech32Prefix: string; dataHeader: Buffer; credential: Buffer } { + if (!address) return { bech32Prefix: '', dataHeader: Buffer.alloc(0), credential: Buffer.alloc(0) } // Return empty if address is falsy if (isHexValue(address)) { // Handle the case where the address is hex-encoded (you can tweak this based on your needs) - const buffer = Buffer.from(address, 'hex'); - const dataHeader = buffer.subarray(0, buffer.length - 28); - const credential = buffer.subarray(buffer.length - 28); - return {bech32Prefix: "hex", dataHeader, credential}; + const buffer = Buffer.from(address, 'hex') + const dataHeader = buffer.subarray(0, buffer.length - 28) + const credential = buffer.subarray(buffer.length - 28) + return { bech32Prefix: 'hex', dataHeader, credential } } else { try { // Decode the Bech32 address - const decoded = bech32.decode(address); - const data = bech32.fromWords(decoded.words); - const buffer = Buffer.from(data); + const decoded = bech32.decode(address) + const data = bech32.fromWords(decoded.words) + const buffer = Buffer.from(data) // Split the buffer into header and credential (last 28 bytes for credential) - const dataHeader = buffer.subarray(0, buffer.length - 28); - const credential = buffer.subarray(buffer.length - 28); + const dataHeader = buffer.subarray(0, buffer.length - 28) + const credential = buffer.subarray(buffer.length - 28) return { - bech32Prefix: decoded.prefix, // Extract prefix from the Bech32 decoding result + bech32Prefix: decoded.prefix, // Extract prefix from the Bech32 decoding result dataHeader, - credential - }; - + credential, + } } catch (e: any) { - throw new AppError("Data is not hex or bech32: " + address); + throw new AppError('Data is not hex or bech32: ' + address) } } } - export function isHexValue(value: string): boolean { - const hexRegex = /^(?:[0-9a-fA-F]{56}|[0-9a-fA-F]{58})$/; - return hexRegex.test(value); + const hexRegex = /^(?:[0-9a-fA-F]{56}|[0-9a-fA-F]{58})$/ + return hexRegex.test(value) } export function validateHash(value: string) { const regex = /^[a-f0-9A-F]{64}$/ if (value.includes('#')) { - return regex.test(value.slice(0, -2)) + return regex.test(value.split('#')[0]) } else return regex.test(value) } export function fromHex(prefix: string, hex: string) { - return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, "hex"))); + return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex'))) } export function validateAddress(value: string): boolean { @@ -112,3 +109,37 @@ export function validateAddress(value: string): boolean { } return false } + +export function formatProposal(proposal: string) { + try { + if (proposal.startsWith('gov_action')) { + const decoded = bech32.decode(proposal) + const data = bech32.fromWords(decoded.words) + const hex = Buffer.from(data).toString('hex') + return { id: hex.slice(0, 64), ix: parseInt(hex.slice(64)) } + } else if (validateHash(proposal)) { + const splitFromHash = proposal.split('#') + const indexParsing = parseInt(splitFromHash[1]) + if (isNaN(indexParsing)) { + return undefined + } + return { id: splitFromHash[0], ix: indexParsing } + } + } catch (error: any) { + return undefined + } +} + +export function validateVoter(voterType: string) { + const lowerCaseVoterType = voterType.toLowerCase() + switch (lowerCaseVoterType) { + case 'drep': + return 'drep' + case 'spo': + return 'spo' + case 'cc': + return 'cc' + default: + throw new AppError('Voter is neither drep or cc or spo: ' + voterType) + } +} diff --git a/dbsync-api/src/repository/drep.ts b/dbsync-api/src/repository/drep.ts index bcbcda84..fb3408ad 100644 --- a/dbsync-api/src/repository/drep.ts +++ b/dbsync-api/src/repository/drep.ts @@ -2,7 +2,7 @@ import { prisma } from '../config/db' import { Prisma } from '@prisma/client' import { combineArraysWithSameObjectKey, formatResult } from '../helpers/formatter' import { DrepSortType, DrepStatusType } from '../types/drep' -import { fromHex, isHexValue } from '../helpers/validator' +import { decodeDrep, fromHex, isHexValue } from '../helpers/validator' export const fetchDrepList = async ( page = 1, @@ -623,7 +623,7 @@ export const fetchDrepLiveStats = async (drepId: string, isScript?: boolean) => return response } -export const fetchDrepLiveDelegators = async (dRepId: string, isScript?: boolean) => { +export const fetchDrepLiveDelegators = async (dRepId: string, isScript?: boolean, balance?: boolean) => { let scriptPart = [true, false] if (isScript === true) { scriptPart = [true, true] @@ -696,10 +696,23 @@ export const fetchDrepLiveDelegators = async (dRepId: string, isScript?: boolean GROUP BY latest.stakeAddress, latest.id, latest.delegations::text; `) as Record[] const parseResult = () => { - return result.map((item) => ({ - stakeAddress: item.stakeaddress, - delegatedAt: JSON.parse(item.delegations)[0], - })) + return result.map((item) => { + const amount = + balance && balance == true + ? { + amount: ( + BigInt(item.utxo ? item.utxo : 0) + + BigInt(item.rewardbalance ? item.rewardbalance : 0) + + BigInt(item.rewardrestbalance ? item.rewardrestbalance : 0) + ).toString(), + } + : {} + return { + stakeAddress: item.stakeaddress, + delegatedAt: JSON.parse(item.delegations)[0], + ...amount, + } + }) } const parsedResult = parseResult() parsedResult.sort( @@ -936,4 +949,4 @@ export const fetchDrepDelegationHistory = async (dRepId: string, isScript?: bool }) flattenedDelegations.sort((a: Delegation, b: Delegation) => new Date(b.time).getTime() - new Date(a.time).getTime()) return flattenedDelegations -} +} \ No newline at end of file diff --git a/dbsync-api/src/repository/proposal.ts b/dbsync-api/src/repository/proposal.ts index 44a3011d..527d3f91 100644 --- a/dbsync-api/src/repository/proposal.ts +++ b/dbsync-api/src/repository/proposal.ts @@ -1,10 +1,17 @@ -import { Prisma } from "@prisma/client"; -import { prisma } from "../config/db"; -import { formatResult } from "../helpers/formatter"; -import { ProposalTypes, SortTypes } from "../types/proposal"; +import { Prisma } from '@prisma/client' +import { prisma } from '../config/db' +import { formatResult } from '../helpers/formatter' +import { ProposalTypes, SortTypes } from '../types/proposal' -export const fetchProposals=async(page:number,size:number,proposal?:string,proposalType?:ProposalTypes,sort?:SortTypes)=>{ - const result = await prisma.$queryRaw ` +export const fetchProposals = async ( + page: number, + size: number, + proposal?: string, + proposalType?: ProposalTypes, + sort?: SortTypes, + includeVoteCount?: boolean +) => { + const result = (await prisma.$queryRaw` WITH LatestDrepDistr AS ( SELECT *, @@ -59,10 +66,19 @@ SELECT ELSE null END, + 'status', CASE + when gov_action_proposal.enacted_epoch is not NULL then json_build_object('enactedEpoch', gov_action_proposal.enacted_epoch) + when gov_action_proposal.ratified_epoch is not NULL then json_build_object('ratifiedEpoch', gov_action_proposal.ratified_epoch) + when gov_action_proposal.expired_epoch is not NULL then json_build_object('expiredEpoch', gov_action_proposal.expired_epoch) + else NULL + END, 'expiryDate', epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no), 'expiryEpochNon', gov_action_proposal.expiration, 'createdDate', creator_block.time, + 'createdBlockHash', encode(creator_block.hash, 'hex'), + 'createdBlockNo', creator_block.block_no, 'createdEpochNo', creator_block.epoch_no, + 'createdSlotNo', creator_block.slot_no, 'url', voting_anchor.url, 'metadataHash', encode(voting_anchor.data_hash, 'hex'), 'protocolParams', ROW_TO_JSON(proposal_params), @@ -150,7 +166,11 @@ FROM AND gov_action_proposal.enacted_epoch IS NULL AND gov_action_proposal.expired_epoch IS NULL AND gov_action_proposal.dropped_epoch IS NULL -${proposalType?Prisma.sql`Where gov_action_proposal.type = ${Prisma.raw(`'${proposalType}'::govactiontype`)}`:Prisma.sql``} +${ + proposalType + ? Prisma.sql`Where gov_action_proposal.type = ${Prisma.raw(`'${proposalType}'::govactiontype`)}` + : Prisma.sql`` +} GROUP BY (gov_action_proposal.id, stake_address.view, @@ -169,6 +189,9 @@ GROUP BY gov_action_proposal.index, creator_tx.hash, creator_block.time, + creator_block.hash, + creator_block.block_no, + creator_block.slot_no, epoch_utils.epoch_duration, epoch_utils.last_epoch_no, epoch_utils.last_epoch_end_time, @@ -179,11 +202,904 @@ GROUP BY always_abstain_voting_power.amount, prev_gov_action.index, prev_gov_action_tx.hash) - ${proposal ? Prisma.sql`HAVING creator_tx.hash = decode(${proposal},'hex')`: Prisma.sql``} - ${sort==='ExpiryDate'?Prisma.sql`ORDER BY epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no) DESC`:Prisma.sql`ORDER BY creator_block.time DESC`} - OFFSET ${(page?(page-1): 0) * (size?size:10)} - FETCH NEXT ${size?size:10} ROWS ONLY - ` as Record[] - const totalCount = result.length?Number(result[0].total_count):0 - return {items:formatResult(result),totalCount} -} \ No newline at end of file + ${proposal ? Prisma.sql`HAVING creator_tx.hash = decode(${proposal},'hex')` : Prisma.sql``} + ${ + sort === 'ExpiryDate' + ? Prisma.sql`ORDER BY epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no) DESC` + : Prisma.sql`ORDER BY creator_block.time DESC` + } + OFFSET ${(page ? page - 1 : 0) * (size ? size : 10)} + FETCH NEXT ${size ? size : 10} ROWS ONLY + `) as Record[] + const totalCount = result.length ? Number(result[0].total_count) : 0 + const parsedResults: any[] = [] + for (const res of result) { + const resultData = res.result + const proposalVoteCount = includeVoteCount + ? { vote: await fetchProposalVoteCount(resultData.txHash, resultData.index) } + : undefined + const parsedResult = { + proposal: { + type: resultData.type, + details: resultData.details, + metadataUrl: resultData.url, + metadataHash: resultData.metadataHash, + }, + meta: { + protocolParams: resultData.protocolParams, + title: resultData.title, + abstract: resultData.abstract, + motivation: resultData.motivation, + rationale: resultData.rationale, + }, + createdAt: { + time: resultData.createdDate, + block: parseInt(resultData.createdBlockNo), + blockHash: resultData.createdBlockHash, + epoch: parseInt(resultData.createdEpochNo), + slot: parseInt(resultData.createdSlotNo), + tx: resultData.txHash, + index: parseInt(resultData.index), + }, + expireAt: { + time: resultData.expiryDate, + epoch: parseInt(resultData.expiryEpochNon), + }, + status: resultData.status, + ...proposalVoteCount, + } + + parsedResults.push(parsedResult) + } + + return { items: parsedResults, totalCount } +} + +// /api/propopsals/${id}/vote-count +export const fetchProposalVoteCount = async (proposalId: string, proposalIndex: number, voter?: string) => { + const drepVoteQuery = Prisma.sql` + , drepVoteRecord AS ( + WITH epochDreps AS ( + -- Get the latest DRep distribution for the selected epoch + SELECT * + FROM drep_distr ddr + WHERE ddr.epoch_no = (SELECT epoch FROM latestOrGovActionExpiration) + ), + + abstainAndNoConfidenceDreps AS ( + -- Get voting power for "always abstain" and "always no confidence" Dreps + SELECT DISTINCT + dr.hash_id, + dh.view AS drepType, + COALESCE( + (SELECT dr_inner.amount + FROM drep_distr dr_inner + WHERE dr_inner.hash_id = dr.hash_id + AND dr_inner.epoch_no = (SELECT epoch FROM latestOrGovActionExpiration) + ), 0 + ) AS latestVotingPower + FROM drep_distr dr + JOIN drep_hash dh ON dh.id = dr.hash_id + WHERE dr.epoch_no BETWEEN (SELECT submittedEpoch FROM ga) + AND (SELECT expirationEpoch FROM ga) + AND dh.view IN ('drep_always_abstain', 'drep_always_no_confidence') + ), + + inactiveDreps AS ( + -- Calculate the total voting power of inactive Dreps + SELECT + SUM(amount) AS amnt, + COUNT(DISTINCT epochDreps.hash_id) AS count + FROM epochDreps + LEFT JOIN govActionVotes ON govActionVotes.drep_voter = epochDreps.hash_id + WHERE active_until > (SELECT expirationEpoch FROM ga) + AND govActionVotes.voter_role IS NULL + ), + + votePowers AS ( + -- Calculate the total voting power for each vote option + SELECT + govActionVotes.vote, + SUM(epochDreps.amount) AS power, + COUNT(DISTINCT epochDreps.hash_id) AS count + FROM govActionVotes + JOIN epochDreps ON epochDreps.hash_id = govActionVotes.drep_voter + GROUP BY govActionVotes.vote + ) + + -- Final union query to get all voting statistics + SELECT + text(votePowers.vote) AS vote_type, + votePowers.power, + votePowers.count + FROM votePowers + + UNION + + SELECT + drep_hash.view, + epochDreps.amount, + COUNT(DISTINCT drep_hash.id) AS count + FROM drep_hash + JOIN epochDreps ON drep_hash.id = epochDreps.hash_id + WHERE drep_hash.view IN ('drep_always_abstain', 'drep_always_no_confidence') + GROUP BY drep_hash.view, epochDreps.amount + + UNION + + SELECT + 'total_distribution', + (SELECT SUM(amount) FROM epochDreps), + (SELECT COUNT(DISTINCT hash_id) FROM epochDreps) + + UNION + + SELECT + 'inactive_votes', + (SELECT amnt FROM inactiveDreps), + (SELECT count FROM inactiveDreps) + ) + ` + const drepSelectionQuery = Prisma.sql`SELECT 'drep' AS voterRole, (SELECT jsonb_agg(to_jsonb(drepVoteRecord)) FROM drepVoteRecord) AS voteRecord` + const spoVoteQuery = Prisma.sql` + , poolVoteRecord AS ( + WITH epochSPOs AS ( + -- Get the latest stake pool operator distribution for the selected epoch + SELECT * + FROM pool_stat ps + WHERE ps.epoch_no = (SELECT epoch FROM latestOrGovActionExpiration) + ), + + votePowers AS ( + -- Calculate the total voting power for each vote option + SELECT + govActionVotes.vote, + SUM(epochSPOs.voting_power) AS power, + COUNT(DISTINCT epochSPOs.pool_hash_id) AS count + FROM govActionVotes + JOIN epochSPOs ON epochSPOs.pool_hash_id = govActionVotes.pool_voter + GROUP BY govActionVotes.vote + ) + + SELECT + text(votePowers.vote) AS vote_type, + votePowers.power, + votePowers.count + FROM votePowers + + UNION + + SELECT + 'total_distribution', + (SELECT SUM(voting_power) FROM epochSPOs), + (SELECT COUNT(DISTINCT pool_hash_id) FROM epochSPOs) + ) + ` + const spoSelectionQuery = Prisma.sql`SELECT 'spo' AS voterRole, (SELECT jsonb_agg(to_jsonb(poolVoteRecord)) FROM poolVoteRecord) AS voteRecord` + const ccVoteQuery = Prisma.sql` + , committeeVoteRecord AS ( + WITH CommitteeVotes AS ( + -- Count committee votes for each vote type + SELECT + SUM(CASE WHEN vote = 'Yes' THEN 1 ELSE 0 END) AS cc_Yes_Votes, + SUM(CASE WHEN vote = 'No' THEN 1 ELSE 0 END) AS cc_No_Votes, + SUM(CASE WHEN vote = 'Abstain' THEN 1 ELSE 0 END) AS cc_Abstain_Votes + FROM voting_procedure AS vp + JOIN ga ON ga.id = vp.gov_action_proposal_id + WHERE vp.committee_voter IS NOT NULL + AND (vp.tx_id, vp.committee_voter, vp.gov_action_proposal_id) IN ( + SELECT MAX(tx_id), committee_voter, gov_action_proposal_id + FROM voting_procedure + WHERE committee_voter IS NOT NULL + GROUP BY committee_voter, gov_action_proposal_id + ) + GROUP BY gov_action_proposal_id + ), + CommitteeData AS ( + SELECT DISTINCT ON (ch.raw) + encode(ch.raw, 'hex') AS hash, + cm.expiration_epoch, + ch.has_script + FROM committee_member cm + JOIN committee_hash ch ON cm.committee_hash_id = ch.id + WHERE cm.expiration_epoch > (SELECT epoch FROM latestOrGovActionExpiration) + ORDER BY ch.raw, cm.expiration_epoch DESC + ) + SELECT 'total_distribution' as vote_type, COUNT(DISTINCT hash) AS count + FROM CommitteeData + UNION ALL + SELECT 'yes_votes' as vote_type, COALESCE(cc_Yes_Votes, 0) AS count FROM CommitteeVotes + UNION ALL + SELECT 'no_votes' as vote_type, COALESCE(cc_No_Votes, 0) AS count FROM CommitteeVotes + UNION ALL + SELECT 'abstain_votes' as vote_type, COALESCE(cc_Abstain_Votes, 0) AS count FROM CommitteeVotes + ) + ` + const ccSelectionQuery = Prisma.sql`SELECT 'cc' AS voterRole, (SELECT jsonb_agg(to_jsonb(committeeVoteRecord)) FROM committeeVoteRecord) AS voteRecord` + const temp = (await prisma.$queryRaw` + WITH ga AS ( + -- Get governance action details including ID, submitted epoch, and expiration epoch + SELECT + g.id, + b.epoch_no AS submittedEpoch, + g.expiration AS expirationEpoch + FROM gov_action_proposal g + JOIN tx ON tx.id = g.tx_id + JOIN block b ON b.id = tx.block_id + WHERE tx.hash = DECODE( + ${proposalId}, 'hex' + ) + AND g.index = ${proposalIndex} + ), + + govActionVotes AS ( + -- Get latest votes for each unique voter (DRep, Pool, or Committee) by role + SELECT + rv.voter_role, + rv.vote, + rv.drep_voter, + rv.committee_voter, + rv.pool_voter + FROM ( + SELECT + vp.*, + t.hash AS tx_hash, + b.block_no AS block_no, + ROW_NUMBER() OVER ( + PARTITION BY vp.voter_role, + COALESCE(vp.drep_voter, vp.pool_voter, vp.committee_voter) + ORDER BY t.block_id DESC + ) AS rn + FROM voting_procedure vp + JOIN ga ON vp.gov_action_proposal_id = ga.id + JOIN public.tx t ON t.id = vp.tx_id + JOIN public.block b ON b.id = t.block_id + WHERE vp.invalid IS NULL + ) rv + WHERE rn = 1 + ), + + latestOrGovActionExpiration AS ( + -- Determine the latest epoch to consider for voting calculations + SELECT LEAST( + (SELECT e.no FROM epoch e ORDER BY e.id DESC LIMIT 1), + (SELECT g.expirationEpoch FROM ga g) + ) AS epoch + ) + + ${voter === 'drep' || voter === undefined ? Prisma.sql`${drepVoteQuery}` : Prisma.sql``} + ${voter === 'spo' || voter === undefined ? Prisma.sql`${spoVoteQuery}` : Prisma.sql``} + ${voter === 'cc' || voter === undefined ? Prisma.sql`${ccVoteQuery}` : Prisma.sql``} + + ${voter === 'drep' ? Prisma.sql`${drepSelectionQuery}` : Prisma.sql``} + ${voter === 'spo' ? Prisma.sql`${spoSelectionQuery}` : Prisma.sql``} + ${voter === 'cc' ? Prisma.sql`${ccSelectionQuery}` : Prisma.sql``} + + ${ + voter == undefined + ? Prisma.sql`${drepSelectionQuery} UNION ${spoSelectionQuery} UNION ${ccSelectionQuery}` + : Prisma.sql`` + } + + `) as Record + const tempResult = temp + type PowerAndCount = { + power: string | null + count?: number + } + type CCVoteRecord = { + totalDistribution: number + yes: number + no: number + abstain: number + notVoted: number + } + type Delegators = { + abstain: PowerAndCount + noConfidence: PowerAndCount + } + type DRepVoteRecord = { + totalDistribution: PowerAndCount + yes: PowerAndCount + no: PowerAndCount + abstain: PowerAndCount + inactive: PowerAndCount + notVoted: PowerAndCount + delegators: Delegators + } + type SPOVoteRecord = { + totalDistribution: PowerAndCount + yes: PowerAndCount + no: PowerAndCount + abstain: PowerAndCount + notVoted: PowerAndCount + } + let ccVotes: any[] = [], + spoVotes: any[] = [], + drepVotes: any[] = [] + let ccVoteRecord: CCVoteRecord | undefined, + drepVoteRecord: DRepVoteRecord | undefined, + spoVoteRecord: SPOVoteRecord | undefined + tempResult.forEach((res: any) => { + if (res.voterrole == 'cc') { + ccVotes = res.voterecord ? res.voterecord : [] + } + if (res.voterrole == 'spo') { + spoVotes = res.voterecord ? res.voterecord : [] + } + if (res.voterrole == 'drep') { + drepVotes = res.voterecord ? res.voterecord : [] + } + }) + let emptyPowerAndCount: PowerAndCount = { power: null } + if (ccVotes) { + let total, yes, no, abstain + for (const vote of ccVotes) { + switch (vote.vote_type) { + case 'total_distribution': + total = vote.count + break + case 'yes_votes': + yes = vote.count + break + case 'no_votes': + no = vote.count + break + case 'abstain_votes': + abstain = vote.count + break + default: + break + } + } + ccVoteRecord = { + totalDistribution: total || 0, + yes: yes || 0, + no: no || 0, + abstain: abstain || 0, + notVoted: total || 0 - (yes || 0 + no || 0 + abstain || 0), + } + } + if (spoVotes) { + let total, yes, no, abstain + + for (const vote of spoVotes) { + switch (vote.vote_type) { + case 'total_distribution': + total = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'Yes': + yes = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'No': + no = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'Abstain': + abstain = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + default: + break + } + } + + // Ensure count for notVoted is correctly calculated + const totalCount = total?.count || 0, + totalPower = total?.power || 0 + const yesCount = yes?.count || 0, + yesPower = yes?.power || 0 + const noCount = no?.count || 0, + noPower = no?.power || 0 + const abstainCount = abstain?.count || 0, + abstainPower = abstain?.power || 0 + + // Calculate notVoted counts + const notVotedCount = totalCount - yesCount - noCount - abstainCount + + // Calculate power for notVoted (using BigInt for calculation) + const notVotedPower = ( + BigInt(totalPower) - + BigInt(yesPower) - + BigInt(noPower) - + BigInt(abstainPower) + ).toString() + + spoVoteRecord = { + totalDistribution: total || emptyPowerAndCount, + yes: yes || emptyPowerAndCount, + no: no || emptyPowerAndCount, + abstain: abstain || emptyPowerAndCount, + notVoted: { + count: notVotedCount, + power: notVotedPower, + }, + } + } + if (drepVotes) { + let total, yes, no, abstain, inactive, alwaysAbstain, alwaysNoConfidence + for (const vote of drepVotes) { + switch (vote.vote_type) { + case 'total_distribution': + total = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'Yes': + yes = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'No': + no = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'Abstain': + abstain = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'inactive_votes': + inactive = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'drep_always_abstain': + alwaysAbstain = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + case 'drep_always_no_confidence': + alwaysNoConfidence = { + count: vote.count, + power: vote.power ? vote.power.toString() : null, + } + break + default: + break + } + } + const totalCount = total?.count || 0, + totalPower = total?.power || 0 + const yesCount = yes?.count || 0, + yesPower = yes?.power || 0 + const noCount = no?.count || 0, + noPower = no?.power || 0 + const abstainCount = abstain?.count || 0, + abstainPower = abstain?.power || 0 + const alwaysAbstainCount = alwaysAbstain?.count || 0, + alwaysAbstainPower = alwaysAbstain?.power || 0 + const alwaysNoConfidenceCount = alwaysNoConfidence?.count || 0, + alwaysNoConfidencePower = alwaysNoConfidence?.power || 0 + + const notVotedCount = + totalCount - (yesCount + noCount + abstainCount + alwaysAbstainCount + alwaysNoConfidenceCount) + const notVotedPower = ( + BigInt(totalPower) - + BigInt(yesPower) - + BigInt(noPower) - + BigInt(abstainPower) - + BigInt(alwaysAbstainPower) - + BigInt(alwaysNoConfidencePower) + ).toString() + + drepVoteRecord = { + totalDistribution: total || emptyPowerAndCount, + yes: yes || emptyPowerAndCount, + no: no || emptyPowerAndCount, + abstain: abstain || emptyPowerAndCount, + inactive: inactive || emptyPowerAndCount, + notVoted: { + count: notVotedCount, + power: notVotedPower, + }, + delegators: { + abstain: { power: alwaysAbstain?.power || 0 }, + noConfidence: { power: alwaysNoConfidence?.power || 0 }, + }, + } + } + const result = { + drep: drepVoteRecord, + spo: spoVoteRecord, + cc: ccVoteRecord, + } + if (voter == 'drep') return drepVoteRecord + else if (voter == 'spo') return spoVoteRecord + else if (voter == 'cc') return ccVoteRecord + else return result +} + +// /api/proposals/${id}/votes +export const fetchProposalVotes = async ( + proposalId: string, + proposalIndex: number, + includeVotingPower?: boolean | false +) => { + const results = (await prisma.$queryRaw` + WITH govAction AS ( + SELECT + g.id, + g.expiration + FROM gov_action_proposal g + JOIN tx ON tx.id = g.tx_id + WHERE tx.hash = DECODE( + ${proposalId}, + 'hex' + ) + AND g.index = ${proposalIndex} + ), + latestEpoch AS ( + SELECT + e.no AS latest_no + FROM epoch e + ORDER BY e.no DESC + LIMIT 1 + ) + SELECT + govAction.expiration AS expirationEpoch, + latestEpoch.latest_no AS latestEpoch, + vp.voter_role AS voterRole, + CASE + WHEN vp.drep_voter IS NOT NULL THEN + jsonb_build_object( + 'credential', ENCODE(dh.raw, 'hex'), + 'hasScript', dh.has_script + ${ + includeVotingPower + ? Prisma.sql`, 'votingPower', ( + SELECT dr.amount + FROM drep_distr dr + WHERE dr.hash_id = vp.drep_voter + AND epoch_no = ( + SELECT LEAST(latest_no, govAction.expiration) + FROM latestEpoch + ) + )` + : Prisma.sql`` + } + ) + WHEN vp.pool_voter IS NOT NULL THEN + jsonb_build_object( + 'credential', ENCODE(ph.hash_raw, 'hex') + ${ + includeVotingPower + ? Prisma.sql`, 'votingPower', ( + SELECT ps.voting_power + FROM pool_stat ps + WHERE ps.pool_hash_id = vp.pool_voter + AND ps.epoch_no = ( + SELECT LEAST(latest_no, govAction.expiration) + FROM latestEpoch + ) + )` + : Prisma.sql`` + } + + ) + WHEN vp.committee_voter IS NOT NULL THEN + jsonb_build_object( + 'credential', ENCODE(ch.raw, 'hex'), + 'hasScript', ch.has_script + ) + ELSE NULL + END AS voterInfo, + vp.vote AS vote, + b.time AS time, + ENCODE(b.hash, 'hex') AS block, + b.block_no AS blockNo, + b.epoch_no AS epoch, + b.slot_no AS blockSlot, + ENCODE(tx.hash, 'hex') AS tx, + txo.index AS index + FROM voting_procedure vp + JOIN govAction + ON vp.gov_action_proposal_id = govAction.id + JOIN tx + ON tx.id = vp.tx_id + JOIN block b + ON b.id = tx.block_id + JOIN tx_out txo + ON txo.tx_id = tx.id + LEFT JOIN drep_hash dh + ON dh.id = vp.drep_voter + LEFT JOIN pool_hash ph + ON ph.id = vp.pool_voter + LEFT JOIN committee_hash ch + ON ch.id = vp.committee_voter + JOIN latestEpoch + ON True + ORDER BY b.time DESC; + `) as Record + + type VoteRecord = { + voter: { + role: string + credential: string + hasScript: boolean + votingPower?: string + } + vote: string + createdAt: { + time: string + block: number + blockHash: string + epoch: number + slot: number + tx: string + index: number + } + } + const parsedResults: VoteRecord[] = [] + results.forEach((result: Record) => { + const votingPower = includeVotingPower + ? { votingPower: result.voterinfo.votingPower ? result.voterinfo.votingPower.toString() : '0' } + : undefined + const parsedResult: VoteRecord = { + vote: result.vote, + voter: { + role: result.voterrole, + credential: result.voterinfo.credential, + hasScript: result.voterinfo.hasScript || false, + ...votingPower, + }, + createdAt: { + time: result.time, + block: parseInt(result.blockno), + blockHash: result.block, + epoch: parseInt(result.epoch), + slot: parseInt(result.blockslot), + tx: result.tx, + index: result.index, + }, + } + parsedResults.push(parsedResult) + }) + return parsedResults +} + +// /api/proposals/${id}?include-vote-count=false/true (default false) +export const fetchProposalById = async (proposalId: string, proposaIndex: number, includeVoteCount?: boolean) => { + const result = (await prisma.$queryRaw` + WITH LatestDrepDistr AS ( + SELECT + *, + ROW_NUMBER() OVER (PARTITION BY hash_id ORDER BY epoch_no DESC) AS rn + FROM + drep_distr + ), + EpochUtils AS ( + SELECT + (Max(end_time) - Min(end_time)) /(Max(NO) - Min(NO)) AS epoch_duration, + Max(NO) AS last_epoch_no, + Max(end_time) AS last_epoch_end_time + FROM + epoch + ), + always_no_confidence_voting_power AS ( + SELECT + coalesce(( + SELECT + amount + FROM drep_hash + LEFT JOIN drep_distr ON drep_hash.id = drep_distr.hash_id + WHERE + drep_hash.view = 'drep_always_no_confidence' ORDER BY epoch_no DESC LIMIT 1), 0) AS amount + ), + always_abstain_voting_power AS ( + SELECT + coalesce(( + SELECT + amount + FROM drep_hash + LEFT JOIN drep_distr ON drep_hash.id = drep_distr.hash_id + WHERE + drep_hash.view = 'drep_always_abstain' ORDER BY epoch_no DESC LIMIT 1), 0) AS amount + ) + SELECT + json_build_object( + 'id', gov_action_proposal.id, + 'txHash',encode(creator_tx.hash, 'hex'), + 'index', gov_action_proposal.index, + 'type', gov_action_proposal.type::text, + 'details', CASE + when gov_action_proposal.type = 'TreasuryWithdrawals' then + json_build_object('Reward Address', stake_address.view, 'Amount', treasury_withdrawal.amount) + when gov_action_proposal.type::text = 'InfoAction' then + json_build_object() + when gov_action_proposal.type::text = 'HardForkInitiation' then + json_build_object( + 'major', (gov_action_proposal.description->'contents'->1->>'major')::int, + 'minor', (gov_action_proposal.description->'contents'->1->>'minor')::int + ) + ELSE + null + END, + 'status', CASE + when gov_action_proposal.enacted_epoch is not NULL then json_build_object('enactedEpoch', gov_action_proposal.enacted_epoch) + when gov_action_proposal.ratified_epoch is not NULL then json_build_object('ratifiedEpoch', gov_action_proposal.ratified_epoch) + when gov_action_proposal.expired_epoch is not NULL then json_build_object('expiredEpoch', gov_action_proposal.expired_epoch) + else NULL + END, + 'expiryDate', epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no), + 'expiryEpochNon', gov_action_proposal.expiration, + 'createdDate', creator_block.time, + 'createdBlockHash', encode(creator_block.hash, 'hex'), + 'createdBlockNo', creator_block.block_no, + 'createdEpochNo', creator_block.epoch_no, + 'createdSlotNo', creator_block.slot_no, + 'url', voting_anchor.url, + 'metadataHash', encode(voting_anchor.data_hash, 'hex'), + 'protocolParams', ROW_TO_JSON(proposal_params), + 'title', off_chain_vote_gov_action_data.title, + 'abstract', off_chain_vote_gov_action_data.abstract, + 'motivation', off_chain_vote_gov_action_data.motivation, + 'rationale', off_chain_vote_gov_action_data.rationale, + 'dRepYesVotes', coalesce(Sum(ldd_drep.amount) FILTER (WHERE voting_procedure.vote::text = 'Yes'), 0) +( + CASE WHEN gov_action_proposal.type = 'NoConfidence' THEN + always_no_confidence_voting_power.amount + ELSE + 0 + END), + 'dRepNoVotes', coalesce(Sum(ldd_drep.amount) FILTER (WHERE voting_procedure.vote::text = 'No'), 0) +( + CASE WHEN gov_action_proposal.type = 'NoConfidence' THEN + 0 + ELSE + always_no_confidence_voting_power.amount + END), + 'dRepAbstainVotes', coalesce(Sum(ldd_drep.amount) FILTER (WHERE voting_procedure.vote::text = 'Abstain'), 0) + always_abstain_voting_power.amount, + 'poolYesVotes', coalesce(vp_by_pool.poolYesVotes, 0), + 'poolNoVotes', coalesce(vp_by_pool.poolNoVotes, 0), + 'poolAbstainVotes', coalesce (vp_by_pool.poolAbstainVotes, 0), + 'ccYesVotes', coalesce(vp_by_cc.ccYesVotes, 0), + 'ccNoVotes', coalesce(vp_by_cc.ccNoVotes, 0), + 'ccAbstainVotes', coalesce(vp_by_cc.ccAbstainVotes, 0), + 'prevGovActionIndex', prev_gov_action.index, + 'prevGovActionTxHash', encode(prev_gov_action_tx.hash, 'hex') + ) AS result, + COUNT(*) OVER () AS total_count + FROM + gov_action_proposal + LEFT JOIN treasury_withdrawal + on gov_action_proposal.id = treasury_withdrawal.gov_action_proposal_id + LEFT JOIN stake_address + on stake_address.id = treasury_withdrawal.stake_address_id + CROSS JOIN EpochUtils AS epoch_utils + CROSS JOIN always_no_confidence_voting_power + CROSS JOIN always_abstain_voting_power + JOIN tx AS creator_tx ON creator_tx.id = gov_action_proposal.tx_id + JOIN block AS creator_block ON creator_block.id = creator_tx.block_id + LEFT JOIN voting_anchor ON voting_anchor.id = gov_action_proposal.voting_anchor_id + LEFT JOIN param_proposal as proposal_params ON gov_action_proposal.param_proposal = proposal_params.id + LEFT JOIN off_chain_vote_data ON off_chain_vote_data.voting_anchor_id = voting_anchor.id + LEFT JOIN off_chain_vote_gov_action_data ON off_chain_vote_gov_action_data.off_chain_vote_data_id = off_chain_vote_data.id + LEFT JOIN voting_procedure ON voting_procedure.gov_action_proposal_id = gov_action_proposal.id + LEFT JOIN LatestDrepDistr ldd_drep ON ldd_drep.hash_id = voting_procedure.drep_voter + AND ldd_drep.rn = 1 + LEFT JOIN + ( + SELECT + gov_action_proposal_id, + SUM(CASE WHEN vote = 'Yes' THEN 1 ELSE 0 END) AS poolYesVotes, + SUM(CASE WHEN vote = 'No' THEN 1 ELSE 0 END) AS poolNoVotes, + SUM(CASE WHEN vote = 'Abstain' THEN 1 ELSE 0 END) AS poolAbstainVotes + FROM + voting_procedure + WHERE + pool_voter IS NOT NULL + GROUP BY + gov_action_proposal_id + ) vp_by_pool + ON gov_action_proposal.id = vp_by_pool.gov_action_proposal_id + LEFT JOIN + ( + SELECT + gov_action_proposal_id, + SUM(CASE WHEN vote = 'Yes' THEN 1 ELSE 0 END) AS ccYesVotes, + SUM(CASE WHEN vote = 'No' THEN 1 ELSE 0 END) AS ccNoVotes, + SUM(CASE WHEN vote = 'Abstain' THEN 1 ELSE 0 END) AS ccAbstainVotes + FROM + voting_procedure + WHERE + committee_voter IS NOT NULL + GROUP BY + gov_action_proposal_id + ) vp_by_cc + ON gov_action_proposal.id = vp_by_cc.gov_action_proposal_id + + LEFT JOIN LatestDrepDistr ldd_cc ON ldd_cc.hash_id = voting_procedure.committee_voter + AND ldd_cc.rn = 1 + LEFT JOIN gov_action_proposal AS prev_gov_action ON gov_action_proposal.prev_gov_action_proposal = prev_gov_action.id + LEFT JOIN tx AS prev_gov_action_tx ON prev_gov_action.tx_id = prev_gov_action_tx.id + AND gov_action_proposal.ratified_epoch IS NULL + AND gov_action_proposal.enacted_epoch IS NULL + AND gov_action_proposal.expired_epoch IS NULL + AND gov_action_proposal.dropped_epoch IS NULL + GROUP BY + (gov_action_proposal.id, + stake_address.view, + treasury_withdrawal.amount, + creator_block.epoch_no, + off_chain_vote_gov_action_data.title, + off_chain_vote_gov_action_data.abstract, + off_chain_vote_gov_action_data.motivation, + off_chain_vote_gov_action_data.rationale, + vp_by_pool.poolYesVotes, + vp_by_pool.poolNoVotes, + vp_by_pool.poolAbstainVotes, + vp_by_cc.ccYesVotes, + vp_by_cc.ccNoVotes, + vp_by_cc.ccAbstainVotes, + gov_action_proposal.index, + creator_tx.hash, + creator_block.time, + creator_block.hash, + creator_block.block_no, + creator_block.slot_no, + epoch_utils.epoch_duration, + epoch_utils.last_epoch_no, + epoch_utils.last_epoch_end_time, + proposal_params, + voting_anchor.url, + voting_anchor.data_hash, + always_no_confidence_voting_power.amount, + always_abstain_voting_power.amount, + prev_gov_action.index, + prev_gov_action_tx.hash) + HAVING creator_tx.hash = decode(${proposalId},'hex') AND gov_action_proposal.index = ${proposaIndex}`) as Record< + any, + any + >[] + const proposalVoteCount = includeVoteCount + ? { vote: await fetchProposalVoteCount(proposalId, proposaIndex) } + : undefined + const resultData = result[0].result + const parsedResult = { + proposal: { + type: resultData.type, + details: resultData.details, + metadataUrl: resultData.url, + metadataHash: resultData.metadataHash, + }, + meta: { + protocolParams: resultData.protocolParams, + title: resultData.title, + abstract: resultData.abstract, + motivation: resultData.motivation, + rationale: resultData.rationale, + }, + createdAt: { + time: resultData.createdDate, + block: parseInt(resultData.createdBlockNo), + blockHash: resultData.createdBlockHash, + epoch: parseInt(resultData.createdEpochNo), + slot: parseInt(resultData.createdSlotNo), + tx: resultData.txHash, + index: parseInt(resultData.index), + }, + status: resultData.status, + expireAt: { + time: resultData.expiryDate, + epoch: parseInt(resultData.expiryEpochNon), + }, + ...proposalVoteCount, + } + return parsedResult +} diff --git a/dbsync-api/swagger.yaml b/dbsync-api/swagger.yaml index bbf26ce6..fe7c04e0 100644 --- a/dbsync-api/swagger.yaml +++ b/dbsync-api/swagger.yaml @@ -161,6 +161,13 @@ paths: schema: type: string example: 'drep15fy2nszna039pdregpryhzahnwcr2rred4nx2qaec7k07nvcq26' + - name: balance + in: query + required: false + description: The live balance of the delegator. + schema: + type: boolean + example: false responses: '200': description: A list of active delegators. @@ -228,7 +235,7 @@ paths: - name: balance in: query required: false - description: The minimum balance of the delegator. + description: The active balance of the delegator. schema: type: boolean example: false @@ -786,6 +793,12 @@ paths: tags: - Gov Action parameters: + - in: query + name: vote_count + schema: + type: boolean + default: false + required: false - in: query name: page schema: @@ -937,6 +950,113 @@ paths: description: Invalid address provided '500': description: Internal server error + /api/proposal/{id}: + get: + summary: Get the specific proposal details + description: Returns Proposal details for a valid proposal id + tags: + - Gov Action + parameters: + - name: id + in: path + required: true + description: Proposal Id in hash#index format or bech32. + schema: + type: string + example: 'f72fb9e4438d7075d3ccd15d6c2ef743016125fef25a46ce63e48ac8bc29172c#0' + - in: query + name: vote_count + schema: + type: boolean + default: false + required: false + responses: + '200': + description: Proposal details + '400': + description: Invalid proposal id format + '404': + description: Proposal not found + /api/proposal/{id}/votes: + get: + summary: Get vote history for a specific proposal + description: Returns history of votes with voter info and vote time. Optional parameter to include voting power. + tags: + - Gov Action + parameters: + - name: id + in: path + required: true + descripton: Proposal Id in hash#index format or bech32. + schema: + type: string + example: 'f72fb9e4438d7075d3ccd15d6c2ef743016125fef25a46ce63e48ac8bc29172c#0' + - in: query + name: voting_power + schema: + type: boolean + default: false + required: false + responses: + '200': + description: Proposal voting history + '400': + description: Invalid proposal id format + '404': + description: Proposal not found + /api/proposal/{id}/vote-count: + get: + summary: Get detailed vote count of a proposal + description: Returns detials of votes (yes, no, abstain, noconfidence) from DReps, SPOs and CCs for a specific proposal Id + tags: + - Gov Action + parameters: + - name: id + in: path + required: true + descripton: Proposal Id in hash#index format or bech32. + schema: + type: string + example: 'f72fb9e4438d7075d3ccd15d6c2ef743016125fef25a46ce63e48ac8bc29172c#0' + responses: + '200': + description: Proposal vote count and voting power + '400': + description: Invalid proposal id format + '404': + description: Proposal not found + /api/proposal/{id}/vote-count/{voter}: + get: + summary: Get detailed vote count of a sprcific type of voter on a proposal + description: Returns detials of votes (yes, no, abstain, noconfidence) from DReps or SPOs or CCs + tags: + - Gov Action + parameters: + - name: id + in: path + required: true + descripton: Proposal Id in hash#index format or bech32. + schema: + type: string + example: 'f72fb9e4438d7075d3ccd15d6c2ef743016125fef25a46ce63e48ac8bc29172c#0' + - name: voter + in: path + requires: false + description: Get count of specific type of voter. drep | spo | cc + schema: + type: string + enum: + - 'drep' + - 'spo' + - 'cc' + example: 'drep' + responses: + '200': + description: Proposal vote count and voting power + '400': + description: Invalid proposal id format + '404': + description: Proposal not found /api/gov-action/count: get: summary: Get the total number of government action proposals diff --git a/frontend/src/app/(pages)/governance-actions/components/proposalCard.tsx b/frontend/src/app/(pages)/governance-actions/components/proposalCard.tsx index 2c0f9e17..e6976a84 100644 --- a/frontend/src/app/(pages)/governance-actions/components/proposalCard.tsx +++ b/frontend/src/app/(pages)/governance-actions/components/proposalCard.tsx @@ -28,8 +28,12 @@ const formatProposalType = (type: string) => { }; const ProposalCard: React.FC = ({ proposal }) => { const { isOpen, toggleDialog } = useAppDialog(); - const isDataMissing = proposal.title === null; - const proposalId = `${proposal.txHash}#${proposal.index}`; + const proposalInfo = proposal.proposal; + const proposalMeta = proposal.meta; + const proposalCreation = proposal.createdAt; + const proposalExpiry = proposal.expireAt; + const isDataMissing = proposalMeta.title === null; + const proposalId = `${proposalCreation.tx}#${proposalCreation.index}`; const handleCopyGovernanceActionId = () => { navigator.clipboard.writeText(proposalId); @@ -48,7 +52,7 @@ const ProposalCard: React.FC = ({ proposal }) => { }; const handleProposalExternalRedirect = () => { - window.open(`https://govtool.cardanoapi.io/connected/governance_actions/${proposal.txHash}`, '_blank'); + window.open(`https://govtool.cardanoapi.io/connected/governance_actions/${proposalCreation.tx}`, '_blank'); }; const [currentConnectedWallet] = useAtom(currentConnectedWalletAtom); @@ -63,7 +67,7 @@ const ProposalCard: React.FC = ({ proposal }) => {

- {isDataMissing ? 'Title Missing' : proposal.title} + {isDataMissing ? 'Title Missing' : proposalMeta.title}

= ({ proposal }) => { />
- {proposal.abstract !== null && ( + {proposalMeta.abstract !== null && (

Abstract

-

{proposal.abstract}

+

{proposalMeta.abstract}

)}
@@ -104,7 +108,7 @@ const ProposalCard: React.FC = ({ proposal }) => {
- {formatProposalType(proposal.type)} + {formatProposalType(proposalInfo.type)} {proposal.agentId && proposal.agentName && ( @@ -120,16 +124,16 @@ const ProposalCard: React.FC = ({ proposal }) => {
Submitted: - {proposal.createdDate && formatDisplayDate(proposal.createdDate)} + {proposalCreation.time && formatDisplayDate(proposalCreation.time)} - (Epoch {proposal.createdEpochNo}) + (Epoch {proposalCreation.epoch})

Expires: - {proposal.createdDate && formatDisplayDate(proposal?.expiryDate)} + {proposalCreation.time && formatDisplayDate(proposalExpiry.time)} - (Epoch {proposal.expiryEpochNo}) + (Epoch {proposalExpiry.epoch})

diff --git a/frontend/src/models/types/proposal.ts b/frontend/src/models/types/proposal.ts index 41d28c2a..d15342f4 100644 --- a/frontend/src/models/types/proposal.ts +++ b/frontend/src/models/types/proposal.ts @@ -1,26 +1,31 @@ export interface IProposal { - id: string; - txHash: string; - index: number; - type: string; - details: any; - expiryDate: string; - expiryEpochNo: number; - createdDate: string; - createdEpochNo: number; - url: string; - metadataHash: string; - title: string | null; - abstract: string | null; - motivation: string | null; - rationale: string | null; - metadata: any; - references: string[]; - yesVotes: number; - noVotes: number; - abstainVotes: number; - metadataStatus: null | string; - metadataValid: boolean; + proposal: { + type: string; + details: any; + metadataUrl: string; + metadataHash: string; + }; + meta: { + protocolParams: any | null; + title: string | null; + abstract: string | null; + motivation: string | null; + rationale: string | null; + }; + createdAt: { + time: string; + block: number; + blockHash: string; + epoch: number; + slot: number; + tx: string; + index: number; + }; + expireAt: { + time: string; + epoch: number; + }; + status: string | null; } export enum ProposalListSort {