Skip to content

Commit 404be01

Browse files
authored
Merge pull request #11 from cardanoapi/feat/block-details-api
Feat/block details api
2 parents 89bd831 + 77614bc commit 404be01

File tree

7 files changed

+224
-57
lines changed

7 files changed

+224
-57
lines changed

src/controllers/blockchain.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response, Router } from 'express'
22
import { handlerWrapper } from '../errors/AppError'
3-
import { fetchCommitteeGovState, fetchEpochDuration, fetchEpochParams } from '../repository/blockchain'
3+
import { fetchBlockInfo, fetchCommitteeGovState, fetchEpochDuration, fetchEpochParams } from '../repository/blockchain'
44

55
const router = Router()
66

@@ -16,13 +16,22 @@ const getEpochParams = async (req: Request, res: Response): Promise<any> => {
1616
return res.status(200).json(result)
1717
}
1818

19-
const getCommitteeGovState = async(req:Request, res:Response):Promise<any> => {
19+
const getCommitteeGovState = async (req: Request, res: Response): Promise<any> => {
2020
const result = await fetchCommitteeGovState()
2121
return res.status(200).json(result)
2222
}
2323

24+
const getBlockInfo = async (req: Request, res: Response): Promise<any> => {
25+
const blockNo = !isNaN(parseInt(req.query.block_no as string)) ? parseInt(req.query.block_no as string) : undefined
26+
const limit = !isNaN(parseInt(req.query.limit as string)) ? parseInt(req.query.limit as string) : undefined
27+
const result = await fetchBlockInfo(limit, blockNo)
28+
if (!result) return res.status(404).json(null)
29+
return res.status(200).json(result)
30+
}
31+
2432
router.get('/epoch', handlerWrapper(getEpochDuration))
2533
router.get('/epoch/params', handlerWrapper(getEpochParams))
2634
router.get('/gov-state/committee', handlerWrapper(getCommitteeGovState))
35+
router.get('/block', handlerWrapper(getBlockInfo))
2736

2837
export default router

src/controllers/proposal.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Request, Response, Router } from 'express'
22
import { handlerWrapper } from '../errors/AppError'
33
import { fetchProposalById, fetchProposalVoteCount, fetchProposalVotes, fetchProposals } from '../repository/proposal'
44
import { formatProposal, validateHash, validateVoter } from '../helpers/validator'
5-
import { ProposalTypes, SortTypes } from '../types/proposal'
5+
import { GovActionStateTypes, ProposalTypes, SortTypes } from '../types/proposal'
66

77
const router = Router()
88

@@ -13,10 +13,11 @@ const getProposals = async (req: Request, res: Response) => {
1313
? Array.isArray(req.query.type)
1414
? (req.query.type as ProposalTypes[])
1515
: typeof req.query.type === 'string'
16-
? req.query.type.split(',').map((type) => type as ProposalTypes)
17-
: undefined
16+
? req.query.type.split(',').map((type) => type as ProposalTypes)
17+
: undefined
1818
: undefined
1919
const sort = req.query.sort ? (req.query.sort as SortTypes) : undefined
20+
const state = req.query.state ? (req.query.state as GovActionStateTypes) : undefined
2021
const includeVoteCount = 'true' == (req.query.vote_count as string)
2122
let proposal = req.query.proposal as string
2223

@@ -26,7 +27,7 @@ const getProposals = async (req: Request, res: Response) => {
2627
}
2728
proposal = proposal.includes('#') ? proposal.split('#')[0] : proposal
2829
}
29-
const { items, totalCount } = await fetchProposals(page, size, proposal, type, sort, includeVoteCount)
30+
const { items, totalCount } = await fetchProposals(page, size, proposal, type, sort, state, includeVoteCount)
3031
return res.status(200).json({ totalCount: totalCount, page, size, items })
3132
}
3233

@@ -60,6 +61,7 @@ const getProposalById = async (req: Request, res: Response) => {
6061
return res.status(400).json({ message: 'Provide valid govAction Id (hash#index) or bech32' })
6162
}
6263
const proposalDetails = await fetchProposalById(proposal.id, proposal.ix, includeVoteCount)
64+
if (!proposalDetails) return res.status(404).json(proposalDetails)
6365
return res.status(200).json(proposalDetails)
6466
}
6567

src/repository/blockchain.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,34 @@ export async function fetchCommitteeGovState() {
103103

104104
return convertKeysToCamelCase(rawResult)
105105
}
106+
107+
export async function fetchBlockInfo(limit?: number, blockNo?: number) {
108+
let result: any
109+
if (blockNo) result = await prisma.$queryRaw`select * from block b where b.block_no=${blockNo}`
110+
else
111+
result =
112+
await prisma.$queryRaw`select * from block b where b.block_no is not null order by b.block_no desc limit ${
113+
limit || 5
114+
} `
115+
116+
const parseResult = (result: any) => {
117+
return {
118+
blockNo: Number(result.block_no),
119+
hash: result.hash.toString('hex'),
120+
epochNo: result.epoch_no,
121+
slotNo: result.slot_no.toString(),
122+
epochSlotNo: result.epoch_slot_no,
123+
size: result.size,
124+
time: result.time,
125+
txCount: Number(result.tx_count),
126+
verificationKey: result.vrf_key,
127+
}
128+
}
129+
130+
if (result.length == 0) return null
131+
if (result.length == 1) return parseResult(result[0])
132+
else
133+
return result.map((res: any) => {
134+
return parseResult(res)
135+
})
136+
}

src/repository/drep.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -363,22 +363,25 @@ export const fetchDrepVoteDetails = async (dRepId: string, isScript?: boolean) =
363363
voteTxHash,
364364
govActionType
365365
FROM TimeOrderedDrepVoteDetails
366-
ORDER BY govActionId, voteTxHash DESC
366+
ORDER BY govActionId, time DESC
367+
),
368+
OrderedVoteDetails AS (
369+
SELECT * from GroupedVoteDetails ORDER BY time DESC
367370
)
368371
SELECT json_agg(
369372
json_build_object(
370-
'govActionId', GroupedVoteDetails.govActionId,
371-
'title', GroupedVoteDetails.title,
372-
'voteType', GroupedVoteDetails.voteType,
373-
'voteAnchorUrl', GroupedVoteDetails.voteAnchorUrl,
374-
'voteAnchorHash', GroupedVoteDetails.voteAnchorHash,
375-
'epochNo', GroupedVoteDetails.epochNo,
376-
'time', GroupedVoteDetails.time,
377-
'voteTxHash', GroupedVoteDetails.voteTxHash,
378-
'govActionType', GroupedVoteDetails.govActionType
373+
'govActionId', OrderedVoteDetails.govActionId,
374+
'title', OrderedVoteDetails.title,
375+
'voteType', OrderedVoteDetails.voteType,
376+
'voteAnchorUrl', OrderedVoteDetails.voteAnchorUrl,
377+
'voteAnchorHash', OrderedVoteDetails.voteAnchorHash,
378+
'epochNo', OrderedVoteDetails.epochNo,
379+
'time', OrderedVoteDetails.time,
380+
'voteTxHash', OrderedVoteDetails.voteTxHash,
381+
'govActionType', OrderedVoteDetails.govActionType
379382
)
380383
) AS votes
381-
from GroupedVoteDetails
384+
from OrderedVoteDetails
382385
`) as Record<any, any>[]
383386
return result[0].votes
384387
}

src/repository/proposal.ts

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { Prisma } from '@prisma/client'
22
import { prisma } from '../config/db'
3+
import { formatResult } from '../helpers/formatter'
4+
import { GovActionStateTypes, ProposalTypes, SortTypes } from '../types/proposal'
35
import { ProposalTypes, SortTypes } from '../types/proposal'
46

7+
58
export const fetchProposals = async (
69
page: number,
710
size: number,
811
proposal?: string,
912
proposalType?: ProposalTypes[],
1013
sort?: SortTypes,
14+
state?: GovActionStateTypes,
1115
includeVoteCount?: boolean
1216
) => {
1317
const result = (await prisma.$queryRaw`
@@ -180,12 +184,28 @@ SELECT
180184
ELSE
181185
null
182186
END,
183-
'status', CASE
184-
when gov_action_proposal.enacted_epoch is not NULL then json_build_object('enactedEpoch', gov_action_proposal.enacted_epoch)
185-
when gov_action_proposal.ratified_epoch is not NULL then json_build_object('ratifiedEpoch', gov_action_proposal.ratified_epoch)
186-
when gov_action_proposal.expired_epoch is not NULL then json_build_object('expiredEpoch', gov_action_proposal.expired_epoch)
187-
else NULL
188-
END,
187+
'status', json_build_object(
188+
'ratified',
189+
json_build_object(
190+
'epoch', gov_action_proposal.ratified_epoch,
191+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.ratified_epoch)
192+
),
193+
'enacted',
194+
json_build_object(
195+
'epoch', gov_action_proposal.enacted_epoch,
196+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.enacted_epoch)
197+
),
198+
'dropped',
199+
json_build_object(
200+
'epoch', gov_action_proposal.dropped_epoch,
201+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.dropped_epoch)
202+
),
203+
'expired',
204+
json_build_object(
205+
'epoch', gov_action_proposal.expired_epoch,
206+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.expired_epoch)
207+
)
208+
),
189209
'expiryDate', epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no),
190210
'expiryEpochNon', gov_action_proposal.expiration,
191211
'createdDate', creator_block.time,
@@ -243,6 +263,10 @@ FROM
243263
CROSS JOIN always_abstain_voting_power
244264
JOIN tx AS creator_tx ON creator_tx.id = gov_action_proposal.tx_id
245265
JOIN block AS creator_block ON creator_block.id = creator_tx.block_id
266+
LEFT JOIN epoch AS ratified_epoch ON ratified_epoch.no = gov_action_proposal.ratified_epoch
267+
LEFT JOIN epoch AS enacted_epoch ON enacted_epoch.no = gov_action_proposal.enacted_epoch
268+
LEFT JOIN epoch AS dropped_epoch ON dropped_epoch.no = gov_action_proposal.dropped_epoch
269+
LEFT JOIN epoch AS expired_epoch ON expired_epoch.no = gov_action_proposal.expired_epoch
246270
LEFT JOIN voting_anchor ON voting_anchor.id = gov_action_proposal.voting_anchor_id
247271
LEFT JOIN param_proposal as proposal_params ON gov_action_proposal.param_proposal = proposal_params.id
248272
LEFT JOIN cost_model AS cost_model ON proposal_params.cost_model_id = cost_model.id
@@ -290,14 +314,33 @@ FROM
290314
AND gov_action_proposal.enacted_epoch IS NULL
291315
AND gov_action_proposal.expired_epoch IS NULL
292316
AND gov_action_proposal.dropped_epoch IS NULL
293-
${
294-
proposalType
295-
? Prisma.sql`WHERE gov_action_proposal.type = ANY(${Prisma.raw(
296-
`ARRAY[${proposalType.map((type) => `'${type}'`).join(', ')}]::govactiontype[]`
297-
)})`
298-
: Prisma.sql``
299-
}
300-
317+
${
318+
state === 'Live'
319+
? Prisma.sql`WHERE gov_action_proposal.ratified_epoch is NULL
320+
AND gov_action_proposal.enacted_epoch is NULL
321+
AND gov_action_proposal.dropped_epoch is NULL
322+
AND gov_action_proposal.expired_epoch is NULL`
323+
: state === 'Expired'
324+
? Prisma.sql`WHERE gov_action_proposal.expired_epoch is not NULL`
325+
: state === 'Enacted'
326+
? Prisma.sql`WHERE gov_action_proposal.enacted_epoch is not NULL`
327+
: state === 'Ratified'
328+
? Prisma.sql`WHERE gov_action_proposal.ratified_epoch is not NULL`
329+
: state === 'Dropped'
330+
? Prisma.sql`WHERE gov_action_proposal.dropped_epoch is not NULL`
331+
: Prisma.sql``
332+
}
333+
${
334+
proposalType
335+
? state
336+
? Prisma.sql`AND gov_action_proposal.type = ANY(${Prisma.raw(
337+
`ARRAY[${proposalType.map((type) => `'${type}'`).join(', ')}]::govactiontype[]`
338+
)})`
339+
: Prisma.sql`WHERE gov_action_proposal.type = ANY(${Prisma.raw(
340+
`ARRAY[${proposalType.map((type) => `'${type}'`).join(', ')}]::govactiontype[]`
341+
)})`
342+
: Prisma.sql``
343+
}
301344
GROUP BY
302345
(gov_action_proposal.id,
303346
stake_address.view,
@@ -329,11 +372,23 @@ ${
329372
always_no_confidence_voting_power.amount,
330373
always_abstain_voting_power.amount,
331374
prev_gov_action.index,
332-
prev_gov_action_tx.hash)
375+
prev_gov_action_tx.hash,
376+
ratified_epoch.no,
377+
ratified_epoch.end_time,
378+
enacted_epoch.no,
379+
enacted_epoch.end_time,
380+
dropped_epoch.no,
381+
dropped_epoch.end_time,
382+
expired_epoch.no,
383+
expired_epoch.end_time
384+
)
333385
${proposal ? Prisma.sql`HAVING creator_tx.hash = decode(${proposal},'hex')` : Prisma.sql``}
334386
${
335-
sort === 'ExpiryDate'
336-
? Prisma.sql`ORDER BY epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no) DESC`
387+
sort === 'Soon to Expire'
388+
? proposal
389+
? Prisma.sql``
390+
: Prisma.sql`HAVING epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no) >= CURRENT_DATE
391+
ORDER BY epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no) ASC`
337392
: Prisma.sql`ORDER BY creator_block.time DESC`
338393
}
339394
OFFSET ${(page ? page - 1 : 0) * (size ? size : 10)}
@@ -380,6 +435,17 @@ ${
380435
parsedResults.push(parsedResult)
381436
}
382437

438+
if (includeVoteCount && sort == 'Highest Yes Count') {
439+
parsedResults.sort((a, b) => {
440+
const aDrepYes = a.vote.drep.yes.count ?? 0
441+
const aSpoYes = a.vote.spo.yes.count ?? 0
442+
const bDrepYes = b.vote.drep.yes.count ?? 0
443+
const bSpoYes = b.vote.spo.yes.count ?? 0
444+
const aCCYes = a.vote.cc.yes
445+
const bCCYes = b.vote.cc.yes
446+
return bDrepYes + bSpoYes + bCCYes - (aDrepYes + aSpoYes + aCCYes)
447+
})
448+
}
383449
return { items: parsedResults, totalCount }
384450
}
385451

@@ -1181,12 +1247,28 @@ export const fetchProposalById = async (proposalId: string, proposaIndex: number
11811247
ELSE
11821248
null
11831249
END,
1184-
'status', CASE
1185-
when gov_action_proposal.enacted_epoch is not NULL then json_build_object('enactedEpoch', gov_action_proposal.enacted_epoch)
1186-
when gov_action_proposal.ratified_epoch is not NULL then json_build_object('ratifiedEpoch', gov_action_proposal.ratified_epoch)
1187-
when gov_action_proposal.expired_epoch is not NULL then json_build_object('expiredEpoch', gov_action_proposal.expired_epoch)
1188-
else NULL
1189-
END,
1250+
'status', json_build_object(
1251+
'ratified',
1252+
json_build_object(
1253+
'epoch', gov_action_proposal.ratified_epoch,
1254+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.ratified_epoch)
1255+
),
1256+
'enacted',
1257+
json_build_object(
1258+
'epoch', gov_action_proposal.enacted_epoch,
1259+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.enacted_epoch)
1260+
),
1261+
'dropped',
1262+
json_build_object(
1263+
'epoch', gov_action_proposal.dropped_epoch,
1264+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.dropped_epoch)
1265+
),
1266+
'expired',
1267+
json_build_object(
1268+
'epoch', gov_action_proposal.expired_epoch,
1269+
'time', (SELECT end_time FROM epoch WHERE epoch.no = gov_action_proposal.expired_epoch)
1270+
)
1271+
),
11901272
'expiryDate', epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no),
11911273
'expiryEpochNon', gov_action_proposal.expiration,
11921274
'createdDate', creator_block.time,
@@ -1331,6 +1413,9 @@ export const fetchProposalById = async (proposalId: string, proposaIndex: number
13311413
const proposalVoteCount = includeVoteCount
13321414
? { vote: await fetchProposalVoteCount(proposalId, proposaIndex) }
13331415
: undefined
1416+
if (result.length == 0) {
1417+
return null
1418+
}
13341419
const resultData = result[0].result
13351420
const parsedResult = {
13361421
proposal: {

src/types/proposal.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1-
export type ProposalTypes = 'ParameterChange'| 'HardForkInitiation'| 'TreasuryWithdrawals'| 'NoConfidence'| 'NewCommittee'| 'NewConstitution'| 'InfoAction'
2-
export type SortTypes = 'CreatedDate'|'ExpiryDate'
1+
export type ProposalTypes =
2+
| 'ParameterChange'
3+
| 'HardForkInitiation'
4+
| 'TreasuryWithdrawals'
5+
| 'NoConfidence'
6+
| 'NewCommittee'
7+
| 'NewConstitution'
8+
| 'InfoAction'
9+
export type SortTypes = 'Newly Created' | 'Soon to Expire' | 'Highest Yes Count'
10+
export type GovActionStateTypes = 'Live' | 'Expired' | 'Ratified' | 'Enacted' | 'Dropped'

0 commit comments

Comments
 (0)