Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/frontend-typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand Down
23 changes: 15 additions & 8 deletions api/backend/app/services/proposal_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"],
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions dbsync-api/src/controllers/drep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
86 changes: 61 additions & 25 deletions dbsync-api/src/controllers/proposal.ts
Original file line number Diff line number Diff line change
@@ -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
101 changes: 66 additions & 35 deletions dbsync-api/src/helpers/validator.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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,
}
}
}
Expand All @@ -48,62 +48,59 @@ 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) {
throw new AppError(e?.message || 'Error During DrepViewConversion: ', e)
}
}


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 {
Expand All @@ -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)
}
}
27 changes: 20 additions & 7 deletions dbsync-api/src/repository/drep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -696,10 +696,23 @@ export const fetchDrepLiveDelegators = async (dRepId: string, isScript?: boolean
GROUP BY latest.stakeAddress, latest.id, latest.delegations::text;
`) as Record<string, any>[]
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(
Expand Down Expand Up @@ -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
}
}
Loading