Skip to content

Commit 88dd505

Browse files
Merge pull request #309 from cardanoapi/feat/active-delegators
Feat/active delegators
2 parents 3dcb827 + d9f1e77 commit 88dd505

File tree

4 files changed

+348
-147
lines changed

4 files changed

+348
-147
lines changed

dbsync-api/src/controllers/drep.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
fetchDrepActiveDelegation,
99
fetchDrepRegistrationDetails,
1010
fetchDrepVoteDetails,
11+
fetchDrepActiveDelegators,
12+
fetchDrepDelegationHistory,
1113
} from '../repository/drep'
1214
import { DrepSortType, DrepStatusType } from '../types/drep'
1315

@@ -46,7 +48,7 @@ const getDrepDelegationDetails = async (req: Request, res: Response) => {
4648
if (dRepId && !isHexValue(dRepId)) {
4749
return res.status(400).json({ message: 'Provide a valid Drep ID' })
4850
}
49-
const result = await fetchDrepDelegationDetails(dRepId)
51+
const result = await fetchDrepDelegationHistory(dRepId)
5052
return res.status(200).json(result)
5153
}
5254

@@ -68,11 +70,21 @@ const getDrepActiveDelegation = async (req: Request, res: Response) => {
6870
return res.status(200).json(result)
6971
}
7072

73+
const getDrepActiveDelegators = async (req: Request, res: Response) => {
74+
const dRepId = convertToHexIfBech32(req.params.id as string)
75+
if (dRepId && !isHexValue(dRepId)) {
76+
return res.status(400).json({ message: 'Provide a valid Drep ID' })
77+
}
78+
const activeDelegators = await fetchDrepActiveDelegators(dRepId)
79+
return res.status(200).json(activeDelegators)
80+
}
81+
7182
router.get('/', handlerWrapper(getDrepList))
7283
router.get('/:id', handlerWrapper(getDrepDetails))
7384
router.get('/:id/vote', handlerWrapper(getDrepVoteDetails))
7485
router.get('/:id/delegation', handlerWrapper(getDrepDelegationDetails))
7586
router.get('/:id/registration', handlerWrapper(getDrepRegistrationDetails))
76-
router.get('/:id/active-delegation', handlerWrapper(getDrepActiveDelegation))
87+
router.get('/:id/live-delegation', handlerWrapper(getDrepActiveDelegation))
88+
router.get('/:id/live-delegators', handlerWrapper(getDrepActiveDelegators))
7789

7890
export default router

dbsync-api/src/helpers/validator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export function validateHash(value:string) {
2525
}else return regex.test(value)
2626
}
2727

28+
export function fromHex(prefix: string, hex: string) {
29+
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, "hex")));
30+
}
31+
2832
export function validateAddress(value: string): boolean {
2933
if (isHexValue(value)){
3034
return value.length === 56 || value.length === 58

dbsync-api/src/repository/drep.ts

Lines changed: 218 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { prisma } from '../config/db'
22
import { Prisma } from '@prisma/client'
33
import { combineArraysWithSameObjectKey, formatResult } from '../helpers/formatter'
44
import { DrepSortType, DrepStatusType } from '../types/drep'
5-
import { isHexValue } from '../helpers/validator'
5+
import { fromHex, isHexValue } from '../helpers/validator'
66

77
export const fetchDrepList = async (page = 1, size = 10, search = '', status?: DrepStatusType, sort?: DrepSortType) => {
88
const result = (await prisma.$queryRaw`
@@ -352,6 +352,7 @@ export const fetchDrepVoteDetails = async (dRepId: string) => {
352352
return result[0].votes
353353
}
354354

355+
// drep delegation historty
355356
export const fetchDrepDelegationDetails = async (dRepId: string) => {
356357
const delegateDetails = prisma.$queryRaw`
357358
with delegator as (select *, ROW_NUMBER() OVER (PARTITION BY addr_id order by tx_id desc) AS rn
@@ -459,77 +460,71 @@ export const fetchDrepRegistrationDetails = async (dRepId: string) => {
459460

460461
export const fetchDrepActiveDelegation = async (drepId: string) => {
461462
const result = (await prisma.$queryRaw`
462-
WITH latest AS (
463+
WITH liveRecord AS (WITH latest AS (
464+
WITH stakes AS (
465+
SELECT DISTINCT sa.id AS id, sa.view AS stakeAddress
466+
FROM delegation_vote dv
467+
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
468+
JOIN stake_address sa ON sa.id = dv.addr_id
469+
WHERE dh.raw = DECODE(${drepId}, 'hex')
470+
)
463471
SELECT
464-
sa.view AS stake_view,
465-
dh.view AS dh_view,
466-
sa.id AS stake_addr_id,
467-
dh.raw,
468-
dh.id,
469-
ROW_NUMBER() OVER (PARTITION BY sa.id ORDER BY dv.tx_id DESC) AS rn
470-
FROM stake_address sa
471-
JOIN delegation_vote dv ON sa.id = dv.addr_id
472-
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
473-
ORDER BY dv.tx_id DESC
472+
stakes.stakeAddress,
473+
stakes.id
474+
FROM stakes
475+
JOIN LATERAL (
476+
SELECT
477+
ENCODE(tx.hash, 'hex') AS tx_id,
478+
b.epoch_no,
479+
b.time,
480+
dh.raw AS raw_check
481+
FROM delegation_vote dv
482+
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
483+
JOIN tx ON tx.id = dv.tx_id
484+
JOIN block b ON b.id = tx.block_id
485+
WHERE dv.addr_id = stakes.id
486+
ORDER BY dv.tx_id DESC
487+
LIMIT 1
488+
) AS subquery ON subquery.raw_check = DECODE(${drepId}, 'hex')
489+
GROUP BY stakes.stakeAddress, stakes.id
490+
ORDER BY stakes.id
474491
)
475492
SELECT
476-
dh.view,
477-
latest.stake_view,
478-
SUM(uv.value) AS total_value,
479-
(SELECT SUM(amount)
493+
COUNT(DISTINCT(latest.stakeAddress)) AS activeDelegators,
494+
COALESCE(SUM(uv.value), 0) +
495+
COALESCE((
496+
SELECT SUM(amount)
480497
FROM reward r
481-
WHERE r.addr_id = latest.stake_addr_id
498+
WHERE r.addr_id = latest.id
482499
AND r.earned_epoch >
483500
(SELECT blka.epoch_no
484501
FROM withdrawal w
485502
JOIN tx txa ON txa.id = w.tx_id
486503
JOIN block blka ON blka.id = txa.block_id
487-
WHERE w.addr_id = latest.stake_addr_id
504+
WHERE w.addr_id = latest.id
488505
ORDER BY w.tx_id DESC
489-
LIMIT 1)) AS rewardBalance,
490-
(SELECT SUM(amount)
506+
LIMIT 1)
507+
), 0) +
508+
COALESCE((
509+
SELECT SUM(amount)
491510
FROM reward_rest r
492-
WHERE r.addr_id = latest.stake_addr_id
493-
AND r.earned_epoch >
494-
(SELECT blka.epoch_no
495-
FROM withdrawal w
496-
JOIN tx txa ON txa.id = w.tx_id
497-
JOIN block blka ON blka.id = txa.block_id
498-
WHERE w.addr_id = latest.stake_addr_id
499-
ORDER BY w.tx_id DESC
500-
LIMIT 1)) AS rewardRestBalance
501-
FROM drep_hash dh
502-
JOIN latest ON dh.id = latest.id
503-
JOIN utxo_view uv ON uv.stake_address_id = latest.stake_addr_id
504-
WHERE latest.rn = 1
505-
AND (dh.view != 'drep_always_no_confidence'
506-
OR dh.view != 'drep_always_abstain')
507-
AND dh.raw = decode(${drepId}, 'hex')
508-
GROUP BY latest.stake_addr_id, dh.view, latest.stake_view;
511+
WHERE r.addr_id = latest.id
512+
AND r.earned_epoch >
513+
(SELECT blka.epoch_no
514+
FROM withdrawal w
515+
JOIN tx txa ON txa.id = w.tx_id
516+
JOIN block blka ON blka.id = txa.block_id
517+
WHERE w.addr_id = latest.id
518+
ORDER BY w.tx_id DESC
519+
LIMIT 1)
520+
), 0) AS liveVotingPower
521+
FROM latest
522+
LEFT JOIN utxo_view uv ON uv.stake_address_id = latest.id
523+
GROUP BY latest.stakeAddress, latest.id)
524+
SELECT SUM(activedelegators) as activeDelegators, SUM(livevotingpower) as liveVotingPower
525+
FROM liveRecord
509526
`) as Record<string, any>[]
510527

511-
const response: Record<string, any> = {}
512-
513-
for (const row of result) {
514-
const { view: drepId, stake_view: stakeView, total_value, rewardbalance, rewardrestbalance } = row
515-
516-
const totalRewardBalance: BigInt =
517-
(rewardbalance != null ? BigInt(rewardbalance) : BigInt(0)) +
518-
(rewardrestbalance != null ? BigInt(rewardrestbalance) : BigInt(0))
519-
520-
if (!response[drepId]) {
521-
response[drepId] = {
522-
delegators: [],
523-
}
524-
}
525-
526-
response[drepId].delegators.push({
527-
[stakeView]: {
528-
utxoBalance: BigInt(total_value).toString(),
529-
rewardBalance: totalRewardBalance.toString(),
530-
},
531-
})
532-
}
533528
const latestEpoch = await prisma.epoch.findFirst({
534529
orderBy: {
535530
start_time: 'desc',
@@ -546,25 +541,173 @@ export const fetchDrepActiveDelegation = async (drepId: string) => {
546541
epoch_no: latestEpoch ? (latestEpoch.no as number) : 0,
547542
},
548543
})
544+
const totalVotingPower = drepDistr._sum.amount as bigint
545+
const decimalInfluence = Number(result[0].livevotingpower) / Number(totalVotingPower)
546+
const influence = (decimalInfluence * 100).toFixed(4) + '%'
547+
const response = {
548+
liveDelegators: result[0].activedelegators ? parseInt(result[0].activedelegators) : 0,
549+
liveVotingPower: result[0].livevotingpower ? result[0].livevotingpower.toString() : '0',
550+
influence: influence,
551+
}
552+
return response
553+
}
554+
555+
export const fetchDrepActiveDelegators = async (dRepId: string) => {
556+
const result = (await prisma.$queryRaw`
557+
WITH latest AS (
558+
WITH stakes AS (
559+
SELECT DISTINCT sa.id AS id, sa.view AS stakeAddress
560+
FROM delegation_vote dv
561+
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
562+
JOIN stake_address sa ON sa.id = dv.addr_id
563+
WHERE dh.raw = DECODE(${dRepId}, 'hex')
564+
)
565+
SELECT
566+
stakes.stakeAddress,
567+
stakes.id,
568+
JSON_AGG(
569+
JSON_BUILD_OBJECT(
570+
'txId', subquery.tx_id,
571+
'epoch', subquery.epoch_no,
572+
'time', subquery.time
573+
)
574+
) AS delegations
575+
FROM stakes
576+
JOIN LATERAL (
577+
SELECT
578+
ENCODE(tx.hash, 'hex') AS tx_id,
579+
b.epoch_no,
580+
b.time,
581+
dh.raw AS raw_check
582+
FROM delegation_vote dv
583+
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
584+
JOIN tx ON tx.id = dv.tx_id
585+
JOIN block b ON b.id = tx.block_id
586+
WHERE dv.addr_id = stakes.id
587+
ORDER BY dv.tx_id DESC
588+
LIMIT 1
589+
) AS subquery ON subquery.raw_check = DECODE(${dRepId}, 'hex')
590+
GROUP BY stakes.stakeAddress, stakes.id
591+
ORDER BY stakes.id
592+
)
593+
SELECT
594+
latest.stakeAddress,
595+
latest.delegations::text,
596+
COALESCE(SUM(uv.value), 0) AS utxo,
597+
(SELECT SUM(amount)
598+
FROM reward r
599+
WHERE r.addr_id = latest.id
600+
AND r.earned_epoch >
601+
(SELECT blka.epoch_no
602+
FROM withdrawal w
603+
JOIN tx txa ON txa.id = w.tx_id
604+
JOIN block blka ON blka.id = txa.block_id
605+
WHERE w.addr_id = latest.id
606+
ORDER BY w.tx_id DESC
607+
LIMIT 1)) AS rewardBalance,
608+
(SELECT SUM(amount)
609+
FROM reward_rest r
610+
WHERE r.addr_id = latest.id
611+
AND r.earned_epoch >
612+
(SELECT blka.epoch_no
613+
FROM withdrawal w
614+
JOIN tx txa ON txa.id = w.tx_id
615+
JOIN block blka ON blka.id = txa.block_id
616+
WHERE w.addr_id = latest.id
617+
ORDER BY w.tx_id DESC
618+
LIMIT 1)) AS rewardRestBalance
619+
FROM latest
620+
LEFT JOIN utxo_view uv ON uv.stake_address_id = latest.id
621+
GROUP BY latest.stakeAddress, latest.id, latest.delegations::text;
622+
`) as Record<string, any>[]
623+
const parsedResult = () => {
624+
return result.map((item) => ({
625+
stakeAddress: item.stakeaddress,
626+
delegatedAt: JSON.parse(item.delegations)[0],
627+
balance: {
628+
utxo: item.utxo.toString(),
629+
reward: item.rewardbalance ? item.rewardbalance.toString() : '0',
630+
rewardRest: item.rewardrestbalance ? item.rewardrestbalance.toString() : '0',
631+
},
632+
}))
633+
}
634+
return parsedResult()
635+
}
549636

550-
const calculateSum = (data: Record<string, any>): string => {
551-
let totalSum = BigInt(0)
552-
for (const drepId in data) {
553-
const delegators = data[drepId].delegators
554-
for (const delegator of delegators) {
555-
for (const stakeView in delegator) {
556-
const { utxoBalance, rewardBalance } = delegator[stakeView]
557-
totalSum += BigInt(utxoBalance) + BigInt(rewardBalance)
637+
export const fetchDrepDelegationHistory = async (dRepId: string) => {
638+
const result = (await prisma.$queryRaw`
639+
WITH stakes AS (
640+
SELECT DISTINCT sa.id AS id, sa.view AS stake
641+
FROM delegation_vote dv
642+
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
643+
JOIN stake_address sa ON sa.id = dv.addr_id
644+
WHERE dh.raw = DECODE(${dRepId}, 'hex')
645+
)
646+
SELECT
647+
stakes.stake,
648+
JSON_AGG(
649+
JSON_BUILD_OBJECT(
650+
'drep', dh.view,
651+
'tx_id', ENCODE(tx.hash, 'hex'),
652+
'epoch_no', b.epoch_no,
653+
'time', b.time
654+
) ORDER BY dv.tx_id DESC
655+
) AS delegations
656+
FROM delegation_vote dv
657+
JOIN stakes ON dv.addr_id = stakes.id
658+
JOIN drep_hash dh ON dh.id = dv.drep_hash_id
659+
JOIN tx ON tx.id = dv.tx_id
660+
JOIN block b ON b.id = tx.block_id
661+
GROUP BY stakes.stake
662+
ORDER BY stakes.stake;
663+
`) as Record<string, any>[]
664+
665+
const processDelegations = (data: any[], bech32Drep: string) => {
666+
type DelegationInfo = { tx_id: string; epoch_no: number; time: string }
667+
type DelegationHistory = { joined?: DelegationInfo; left?: DelegationInfo }
668+
type Result = {
669+
stakeAddress: string
670+
delegation: DelegationHistory[]
671+
}
672+
673+
const result = []
674+
675+
for (const stakeData of data) {
676+
const stakeAddress = stakeData.stake
677+
const delegations = stakeData.delegations
678+
679+
let partialResult: Result = {
680+
stakeAddress: stakeAddress,
681+
delegation: [],
682+
}
683+
684+
let joinedFound = false
685+
let delegationHistory: DelegationHistory = { joined: undefined, left: undefined }
686+
let stakeDelegationHistory: DelegationHistory[] = []
687+
688+
for (let i = delegations.length - 1; i >= 0; i--) {
689+
const delegation = delegations[i]
690+
const { drep, tx_id, epoch_no, time } = delegation
691+
692+
if (drep === bech32Drep) {
693+
delegationHistory.joined = { tx_id, epoch_no, time }
694+
joinedFound = true
695+
} else if (joinedFound) {
696+
delegationHistory.left = { tx_id, epoch_no, time }
697+
stakeDelegationHistory.push(delegationHistory)
698+
delegationHistory = { joined: undefined, left: undefined }
699+
joinedFound = false
558700
}
559701
}
702+
if (delegationHistory.joined || delegationHistory.left) {
703+
stakeDelegationHistory.push(delegationHistory)
704+
}
705+
partialResult.delegation = stakeDelegationHistory
706+
result.push(partialResult)
560707
}
561-
return totalSum.toString()
562-
}
563708

564-
const votingPower = calculateSum(response)
565-
const totalVotingPower = drepDistr._sum.amount as bigint
566-
const decimalInfluence = Number(votingPower) / Number(totalVotingPower)
567-
const influence = (decimalInfluence * 100).toFixed(4) + '%'
568-
response.influence = influence
569-
return response
709+
return result
710+
}
711+
const drepbech32 = fromHex('drep', dRepId)
712+
return processDelegations(result, drepbech32)
570713
}

0 commit comments

Comments
 (0)