From b18796002a455923c290e68a4829a82e4b1c42e3 Mon Sep 17 00:00:00 2001 From: Emmanuel Mutisya <emmanuelmutisya254@gmail.com> Date: Mon, 16 Sep 2024 11:40:03 +0300 Subject: [PATCH 1/4] add pagination to DRep delegators list --- backend/src/drep/drep.controller.ts | 15 +++ backend/src/drep/drep.service.ts | 95 ++++++++++++------- .../queries/drepDelegatorsWithVotingPower.ts | 29 +++++- .../dreps/[drepid]/delegators/page.tsx | 4 +- .../components/atoms/DrepDelegatorsList.tsx | 80 ++++++++++++---- .../src/components/molecules/DRepsTable.tsx | 47 ++------- .../molecules/DrepProfileMetrics.tsx | 4 +- .../src/components/molecules/Pagination.tsx | 43 +++++++-- frontend/src/constants/queryKeys.ts | 3 +- .../src/hooks/useGetDrepDelegatorsQuery.ts | 19 ++++ .../services/requests/getDrepDelegators.ts | 12 +++ frontend/types/api.ts | 12 ++- 12 files changed, 250 insertions(+), 113 deletions(-) create mode 100644 frontend/src/hooks/useGetDrepDelegatorsQuery.ts create mode 100644 frontend/src/services/requests/getDrepDelegators.ts diff --git a/backend/src/drep/drep.controller.ts b/backend/src/drep/drep.controller.ts index 1ebb6fbe..763ad55b 100644 --- a/backend/src/drep/drep.controller.ts +++ b/backend/src/drep/drep.controller.ts @@ -148,4 +148,19 @@ export class DrepController { isRegistered(@Param('voterId') voterId: string) { return this.drepService.isDrepRegistered(voterId); } + + @Get(':voterId/delegators') + getDrepDelegators( + @Param('voterId') voterId: string, + @Query('page', new DefaultValuePipe(1), ParseIntPipe) + page: number, + @Query('perPage', new DefaultValuePipe(24), ParseIntPipe) + perPage: number, + ) { + return this.drepService.getDrepDelegatorsWithVotingPower( + voterId, + page, + perPage + ); + } } diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts index 51b6fe7a..60efa772 100644 --- a/backend/src/drep/drep.service.ts +++ b/backend/src/drep/drep.service.ts @@ -1,15 +1,21 @@ -import {HttpException, HttpStatus, Injectable, Logger, NotFoundException,} from '@nestjs/common'; -import {createDrepDto, ValidateMetadataDTO} from 'src/dto'; -import {faker} from '@faker-js/faker'; +import { + HttpException, + HttpStatus, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { createDrepDto, ValidateMetadataDTO } from 'src/dto'; +import { faker } from '@faker-js/faker'; import * as blake from 'blakejs'; -import {HttpService} from '@nestjs/axios'; -import {AttachmentService} from 'src/attachment/attachment.service'; -import {catchError, firstValueFrom, Observable} from 'rxjs'; -import {AxiosResponse} from 'axios'; -import {InjectDataSource} from '@nestjs/typeorm'; -import {DataSource} from 'typeorm'; -import {ReactionsService} from 'src/reactions/reactions.service'; -import {CommentsService} from 'src/comments/comments.service'; +import { HttpService } from '@nestjs/axios'; +import { AttachmentService } from 'src/attachment/attachment.service'; +import { catchError, firstValueFrom, Observable } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { ReactionsService } from 'src/reactions/reactions.service'; +import { CommentsService } from 'src/comments/comments.service'; import { Delegation, IPFSResponse, @@ -18,16 +24,23 @@ import { MetadataValidationStatus, ValidateMetadataResult, } from 'src/common/types'; -import {AuthService} from 'src/auth/auth.service'; -import {getAllDRepsQuery, getTotalResultsQuery} from 'src/queries/getDReps'; -import {getDRepDelegatorsCountQuery, getDRepVotesCountQuery, getDRepVotingPowerQuery,} from 'src/queries/drepStats'; -import {Metadata} from 'src/entities/metadata.entity'; -import {getEpochParams} from 'src/queries/getEpochParams'; -import {getDRepDelegatorsHistory} from 'src/queries/drepDelegatorsHistory'; -import {JsonLd} from 'jsonld/jsonld-spec'; -import {Response} from 'express'; -import {getDrepCexplorerDetailsQuery} from 'src/queries/drepCexplorerDetails'; -import {getDrepDelegatorsWithVotingPowerQuery} from 'src/queries/drepDelegatorsWithVotingPower'; +import { AuthService } from 'src/auth/auth.service'; +import { getAllDRepsQuery, getTotalResultsQuery } from 'src/queries/getDReps'; +import { + getDRepDelegatorsCountQuery, + getDRepVotesCountQuery, + getDRepVotingPowerQuery, +} from 'src/queries/drepStats'; +import { Metadata } from 'src/entities/metadata.entity'; +import { getEpochParams } from 'src/queries/getEpochParams'; +import { getDRepDelegatorsHistory } from 'src/queries/drepDelegatorsHistory'; +import { JsonLd } from 'jsonld/jsonld-spec'; +import { Response } from 'express'; +import { getDrepCexplorerDetailsQuery } from 'src/queries/drepCexplorerDetails'; +import { + getDrepDelegatorsCountQuery, + getDrepDelegatorsWithVotingPowerQuery, +} from 'src/queries/drepDelegatorsWithVotingPower'; import { BlockfrostService } from 'src/blockfrost/blockfrost.service'; @Injectable() @@ -275,12 +288,9 @@ export class DrepService { if (drep.length > 0) drepVoterId = drep[0].signature_drepVoterId; const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId); - const drepDelegators = - await this.getDrepDelegatorsWithVotingPower(drepVoterId); const combinedResult = { ...drep[0], cexplorerDetails: drepCexplorer, - delegators: drepDelegators, }; if ( (!drep || drep.length === 0) && @@ -308,12 +318,9 @@ export class DrepService { .where('signature.drepVoterId = :drepVoterId', { drepVoterId }) .getRawMany(); const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId); - const drepDelegators = - await this.getDrepDelegatorsWithVotingPower(drepVoterId); const combinedResult = { ...drep[0], cexplorerDetails: drepCexplorer, - delegators: drepDelegators, }; if ( (!drep || drep.length === 0) && @@ -695,17 +702,37 @@ export class DrepService { } } - async getDrepDelegatorsWithVotingPower(drepVoterId: string) { + async getDrepDelegatorsWithVotingPower( + drepVoterId: string, + currentPage: number, + itemsPerPage: number, + ) { + const offset = (currentPage - 1) * itemsPerPage; + const delegatorsWithVotingPower = await this.cexplorerService.manager.query( - getDrepDelegatorsWithVotingPowerQuery, + getDrepDelegatorsWithVotingPowerQuery(itemsPerPage, offset), [drepVoterId], ); - return delegatorsWithVotingPower.map((delegator) => ({ - stakeAddress: delegator?.stake_address, - delegationEpoch: delegator?.delegation_epoch, - votingPower: delegator?.voting_power, - })); + const totalResults = await this.cexplorerService.manager.query( + getDrepDelegatorsCountQuery(), + [drepVoterId], + ); + + const totalItems = parseInt(totalResults[0].total, 10); + const totalPages = Math.ceil(totalItems / itemsPerPage); + + return { + data: delegatorsWithVotingPower.map((delegator) => ({ + stakeAddress: delegator?.stake_address, + delegationEpoch: delegator?.delegation_epoch, + votingPower: delegator?.voting_power, + })), + totalItems, + currentPage, + itemsPerPage, + totalPages, + }; } async updateDrepInfo(drepId: number, drep: createDrepDto) { diff --git a/backend/src/queries/drepDelegatorsWithVotingPower.ts b/backend/src/queries/drepDelegatorsWithVotingPower.ts index a00f942a..61e8341c 100644 --- a/backend/src/queries/drepDelegatorsWithVotingPower.ts +++ b/backend/src/queries/drepDelegatorsWithVotingPower.ts @@ -38,7 +38,10 @@ // voting_power DESC // `; -export const getDrepDelegatorsWithVotingPowerQuery: string = ` +export const getDrepDelegatorsWithVotingPowerQuery = ( + itemsPerPage: number, + offset?: number, +) => ` WITH latest_delegations AS ( SELECT delegation_vote.addr_id, MAX(block.time) AS latest_time FROM drep_hash @@ -63,5 +66,27 @@ export const getDrepDelegatorsWithVotingPowerQuery: string = ` GROUP BY sa.view, b.epoch_no ORDER BY voting_power DESC - LIMIT 24 + LIMIT ${itemsPerPage} + OFFSET ${offset} +`; + +export const getDrepDelegatorsCountQuery = () => ` + WITH latest_delegations AS ( + SELECT delegation_vote.addr_id, MAX(block.time) AS latest_time + FROM drep_hash + JOIN delegation_vote ON delegation_vote.drep_hash_id = drep_hash.id + JOIN stake_address ON delegation_vote.addr_id = stake_address.id + JOIN tx ON delegation_vote.tx_id = tx.id + JOIN block ON tx.block_id = block.id + WHERE drep_hash.view = $1 + GROUP BY delegation_vote.addr_id + ) + SELECT COUNT(DISTINCT sa.id) AS total + FROM drep_hash AS dh + JOIN delegation_vote AS dv ON dh.id = dv.drep_hash_id + JOIN stake_address sa ON dv.addr_id = sa.id + JOIN tx ON dv.tx_id = tx.id + JOIN block b ON tx.block_id = b.id + JOIN latest_delegations ld ON dv.addr_id = ld.addr_id + AND b.time = ld.latest_time `; diff --git a/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx b/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx index 82f27290..c84590b9 100644 --- a/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx +++ b/frontend/src/app/[locale]/dreps/[drepid]/delegators/page.tsx @@ -1,12 +1,10 @@ 'use client'; import DrepProfileMetrics from '@/components/molecules/DrepProfileMetrics'; -import { useGetSingleDRepQuery } from '@/hooks/useGetSingleDRepQuery'; import { useParams } from 'next/navigation'; const DelegatorsPage = () => { const { drepid } = useParams(); - const { dRep } = useGetSingleDRepQuery(drepid); - return <DrepProfileMetrics drepMetrics={dRep} />; + return <DrepProfileMetrics voterId={String(drepid)} />; }; export default DelegatorsPage; diff --git a/frontend/src/components/atoms/DrepDelegatorsList.tsx b/frontend/src/components/atoms/DrepDelegatorsList.tsx index eba93c13..c690fae7 100644 --- a/frontend/src/components/atoms/DrepDelegatorsList.tsx +++ b/frontend/src/components/atoms/DrepDelegatorsList.tsx @@ -6,8 +6,13 @@ import { formattedAda, lovelaceToAda, } from '@/lib'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import HoverText from './HoverText'; +import { useGetDrepDelegators } from '@/hooks/useGetDrepDelegatorsQuery'; +import { Box, Skeleton } from '@mui/material'; +import Pagination from '../molecules/Pagination'; +import { useSearchParams } from 'next/navigation'; + const ViewProfileAction = () => { return ( <div className="flex w-fit flex-row items-center gap-2 rounded-full bg-gray-200 px-3 py-1 text-sm"> @@ -16,19 +21,42 @@ const ViewProfileAction = () => { </div> ); }; -const DrepDelegatorslist = ({ delegators }: { delegators: any[] }) => { + +const DrepDelegatorslist = ({ voterId }: { voterId: string }) => { + const [currentPage, setCurrentPage] = useState(1); const { latestEpoch } = useCardano(); const { isMobile, screenWidth } = useScreenDimension(); + const searchParams = useSearchParams(); + + useEffect(() => { + setCurrentPage(Number(searchParams.get('page') || 1)); + }, [searchParams]); + + const { Delegators, isDelegatorsLoading } = useGetDrepDelegators( + voterId, + currentPage, + ); + return ( - <div className="w-full overflow-x-scroll"> + <div className="w-full"> <p className="text-3xl font-bold">Delegators</p> - {delegators && delegators.length > 0 ? ( - delegators.map((delegator, index) => { - return ( - <div key={index}> - <div className="flex flex-col"> - <div className="flex w-full flex-row items-center justify-between text-nowrap py-4"> - <div className="flex min-w-40 flex-col"> + {isDelegatorsLoading && ( + <div className="px-1"> + {Array.from({ length: 24 }).map((_, index) => ( + <Skeleton height={80} key={index} /> + ))} + </div> + )} + {!isDelegatorsLoading && Delegators.data.length > 0 && ( + <div className="flex flex-col overflow-x-auto"> + {Delegators.data.map((delegator, index) => { + return ( + <div className="flex w-full flex-col" key={index}> + <div + className="flex w-full flex-row items-center justify-between text-nowrap py-4" + + > + <div className="flex w-40 shrink-0 flex-col lg:w-60"> <p className="font-bold"> Epoch {delegator?.delegationEpoch}{' '} {delegator?.delegationEpoch == latestEpoch && '(actual)'} @@ -41,7 +69,7 @@ const DrepDelegatorslist = ({ delegators }: { delegators: any[] }) => { </p> </div> - <div className="flex min-w-40 flex-col items-center justify-start"> + <div className="flex w-40 shrink-0 flex-col items-center justify-start"> <p className="font-bold">Active Stake</p> <div> <HoverText @@ -53,26 +81,40 @@ const DrepDelegatorslist = ({ delegators }: { delegators: any[] }) => { </div> </div> - <div className="flex min-w-40 flex-col items-center justify-start"> + <div className="flex w-40 shrink-0 flex-col items-center justify-start"> <p className="font-bold">Epoch</p> <p> {delegator.delegationEpoch}</p> </div> - <div className="flex min-w-40 flex-col items-start justify-start"> + <div className="flex max-w-40 shrink-0 flex-col items-start justify-start"> <p className="font-bold">Actions</p> <div className="flex items-center gap-2"> <ViewProfileAction /> </div> </div> </div> - <hr className="w-dvw border" /> + <hr className="w-full border" /> </div> - </div> - ); - }) - ) : ( - <p>No delegators to show</p> + ); + })} + </div> )} + {!isDelegatorsLoading && Delegators.data.length < 1 && ( + <p className="text-center">No delegators to show</p> + )} + + {!isDelegatorsLoading && + Delegators?.data && + Delegators?.data.length > 0 && ( + <Box className="mt-6 flex justify-end"> + <Pagination + currentPage={Delegators.currentPage} + totalPages={Delegators.totalPages} + totalItems={Delegators.totalItems} + dataType="Delegators" + /> + </Box> + )} </div> ); }; diff --git a/frontend/src/components/molecules/DRepsTable.tsx b/frontend/src/components/molecules/DRepsTable.tsx index 0057a873..27a7b091 100644 --- a/frontend/src/components/molecules/DRepsTable.tsx +++ b/frontend/src/components/molecules/DRepsTable.tsx @@ -6,7 +6,8 @@ import { useGetDRepsQuery } from '@/hooks/useGetDRepsQuery'; import { convertString, formatAsCurrency, - handleCopyText, percentageDifference, + handleCopyText, + percentageDifference, shortNumber, } from '@/lib'; import { useScreenDimension } from '@/hooks'; @@ -15,13 +16,12 @@ import Button from '../atoms/Button'; import Link from 'next/link'; import HoverText from '../atoms/HoverText'; import Pagination from './Pagination'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import CopyToClipBoard from '../atoms/svgs/CopyToClipBoardIcon'; import ArrowDownIcon from '../atoms/svgs/ArrowDownIcon'; import ArrowUpIcon from '../atoms/svgs/ArrowUpIcon'; import DatabaseNullIcon from '../atoms/svgs/DatabaseNullIcon'; import CrossIcon from '../atoms/svgs/CrossIcon'; -import Typography from "@mui/material/Typography"; +import Typography from '@mui/material/Typography'; type DRepsTableProps = { query?: string; @@ -49,9 +49,6 @@ const DRepsTable = ({ campaignStatus, type, }: DRepsTableProps) => { - const searchParams = useSearchParams(); - const pathName = usePathname(); - const { replace } = useRouter(); const { isMobile } = useScreenDimension(); const { DReps, isDRepsLoading, isError } = useGetDRepsQuery( @@ -64,36 +61,9 @@ const DRepsTable = ({ type, ); - // Handle table pagination - function moveToPage(targetPage: number) { - const params = new URLSearchParams(searchParams); - - if (page !== targetPage) { - params.set('page', targetPage.toString()); - } - replace(`${pathName}?${params.toString()}`); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - - function moveToFirstPage(firstPage: number) { - moveToPage(firstPage); - } - - function moveToLastPage(lastPage: number) { - moveToPage(lastPage); - } - - function moveToPreviousPage(previousPage: number) { - moveToPage(previousPage); - } - - function moveToNextPage(nextPage: number) { - moveToPage(nextPage); - } - return ( - <div className="flex flex-col overflow-x-auto dreps-table-wrapper"> - <table className="min-w-full dreps-table"> + <div className="dreps-table-wrapper flex flex-col overflow-x-auto"> + <table className="dreps-table min-w-full"> <thead className="mb-2"> <tr className="overflow-x-auto text-nowrap bg-white text-left text-xl font-black"> <th className="py-2">DRep</th> @@ -125,7 +95,7 @@ const DRepsTable = ({ </div> </th> <th className="py-2"> - <div className="flex items-center justifiy-end text-left"> + <div className="justify-end flex items-center text-left"> <span>Delegators</span> {sort === 'delegators' && (order === 'desc' ? ( @@ -331,10 +301,7 @@ const DRepsTable = ({ currentPage={DReps.currentPage} totalPages={DReps.totalPages} totalItems={DReps.totalItems} - moveToFirstPage={moveToFirstPage} - moveToLastPage={moveToLastPage} - moveToPreviousPage={moveToPreviousPage} - moveToNextPage={moveToNextPage} + dataType="DReps" /> </Box> )} diff --git a/frontend/src/components/molecules/DrepProfileMetrics.tsx b/frontend/src/components/molecules/DrepProfileMetrics.tsx index 8201200f..23ab8097 100644 --- a/frontend/src/components/molecules/DrepProfileMetrics.tsx +++ b/frontend/src/components/molecules/DrepProfileMetrics.tsx @@ -1,10 +1,10 @@ import React from 'react'; import DrepDelegatorsList from '../atoms/DrepDelegatorsList'; -const DrepProfileMetrics = ({drepMetrics}:{drepMetrics: any}) => { +const DrepProfileMetrics = ({voterId}:{voterId: string}) => { return ( <div className='bg-white p-5 min-h-screen'> - <DrepDelegatorsList delegators={drepMetrics?.delegators} /> + <DrepDelegatorsList voterId={voterId} /> </div> ); }; diff --git a/frontend/src/components/molecules/Pagination.tsx b/frontend/src/components/molecules/Pagination.tsx index d71872a0..09c2f19a 100644 --- a/frontend/src/components/molecules/Pagination.tsx +++ b/frontend/src/components/molecules/Pagination.tsx @@ -4,29 +4,54 @@ import ChevronsRightIcon from '../atoms/svgs/ChevronsRightIcon'; import ChevronRightIcon from '../atoms/svgs/ChevronRightIcon'; import ChevronLeftIcon from '../atoms/svgs/ChevronLeftIcon'; import ChevronsLeftIcon from '../atoms/svgs/ChevronsLeftIcon'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; type PaginationProps = { currentPage: number; totalPages: number; totalItems: number; - moveToFirstPage?: Function; - moveToPreviousPage?: Function; - moveToNextPage?: Function; - moveToLastPage?: Function; + dataType: string; }; const Pagination = ({ currentPage, totalPages, totalItems, - moveToFirstPage, - moveToPreviousPage, - moveToNextPage, - moveToLastPage, + dataType }: PaginationProps) => { + const searchParams = useSearchParams(); + const pathName = usePathname(); + const { replace } = useRouter(); + const isLastPage = currentPage === totalPages; const isFirstPage = currentPage === 1; + function moveToPage(targetPage: number) { + const params = new URLSearchParams(searchParams); + + if (currentPage !== targetPage) { + params.set('page', targetPage.toString()); + } + replace(`${pathName}?${params.toString()}`); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + function moveToFirstPage(firstPage: number) { + moveToPage(firstPage); + } + + function moveToLastPage(lastPage: number) { + moveToPage(lastPage); + } + + function moveToPreviousPage(previousPage: number) { + moveToPage(previousPage); + } + + function moveToNextPage(nextPage: number) { + moveToPage(nextPage); + } + return ( <> {!totalItems && totalPages && currentPage ? ( @@ -92,7 +117,7 @@ const Pagination = ({ </Box> </Box> <span className="textColor2 mr-2 text-sm"> - Total DReps: {totalItems} + Total {dataType}: {totalItems} </span> </Box> )} diff --git a/frontend/src/constants/queryKeys.ts b/frontend/src/constants/queryKeys.ts index c4c7cea4..bbb43fbc 100644 --- a/frontend/src/constants/queryKeys.ts +++ b/frontend/src/constants/queryKeys.ts @@ -5,5 +5,6 @@ export const QUERY_KEYS = { getSingleDRepKey: 'getSingleDRepKey', getAllDRepsKey: 'getAllDRepsKey', getDRepStatsKey: 'getDRepStatsKey', - getDRepTimelineKey: 'getDRepTimelineKey' + getDRepTimelineKey: 'getDRepTimelineKey', + getDrepDelegators: 'getDrepDelegators' }; diff --git a/frontend/src/hooks/useGetDrepDelegatorsQuery.ts b/frontend/src/hooks/useGetDrepDelegatorsQuery.ts new file mode 100644 index 00000000..43077766 --- /dev/null +++ b/frontend/src/hooks/useGetDrepDelegatorsQuery.ts @@ -0,0 +1,19 @@ +import { QUERY_KEYS } from '@/constants/queryKeys'; +import { useQuery } from 'react-query'; +import { Delegators } from '../../types/api'; +import { getDrepDelegators } from '@/services/requests/getDrepDelegators'; + +export const useGetDrepDelegators = ( + voterId: string, + page?: number, + perPage?: number, +) => { + const { data, isLoading } = useQuery<Delegators>({ + queryKey: [QUERY_KEYS.getDrepDelegators, voterId, page], + queryFn: async () => await getDrepDelegators(voterId, page, perPage), + enabled: !!voterId, + refetchOnWindowFocus: false, + }); + + return { Delegators: data, isDelegatorsLoading: isLoading }; +}; diff --git a/frontend/src/services/requests/getDrepDelegators.ts b/frontend/src/services/requests/getDrepDelegators.ts new file mode 100644 index 00000000..af303865 --- /dev/null +++ b/frontend/src/services/requests/getDrepDelegators.ts @@ -0,0 +1,12 @@ +import axiosInstance from '../axiosInstance'; + +export const getDrepDelegators = async ( + voterId: string, + page?: number, + perPage?: number, +) => { + const response = await axiosInstance.get(`dreps/${voterId}/delegators`, { + params: { page, perPage }, + }); + return response.data; +}; diff --git a/frontend/types/api.ts b/frontend/types/api.ts index fc594256..c41f9f84 100644 --- a/frontend/types/api.ts +++ b/frontend/types/api.ts @@ -26,6 +26,14 @@ export type Delegator = { votingPower: number | null; }; +export type Delegators = { + data: Delegator[]; + totalItems: number; + currentPage: number; + itemsPerPage: number; + totalPages: number; +}; + export type SingleDRep = { drep_deletedAt?: string | null; drep_id?: number | null; @@ -57,8 +65,6 @@ export type SingleDRep = { signature_drepSignatureKey?: string | null; signature_drepId?: number | null; cexplorerDetails: DRepCExplorerDetails; - activity?: any[]; - delegators?: Delegator[]; }; export type DRepStats = { @@ -78,4 +84,4 @@ export type DelegationData = { type: string; total_stake: string; added_power: boolean; -}; \ No newline at end of file +}; -- GitLab From f9208f79346ffb6973ed3c92fb45652bf479fbee Mon Sep 17 00:00:00 2001 From: theeurbanlegend <mbuto152@gmail.com> Date: Mon, 16 Sep 2024 16:55:13 +0300 Subject: [PATCH 2/4] Updated model, updated usage --- backend/src/drep/drep.controller.ts | 2 +- backend/src/drep/drep.service.ts | 16 +++++------ backend/src/entities/drep.entity.ts | 8 ------ backend/src/entities/note.entity.ts | 6 ++++- backend/src/entities/signatures.entity.ts | 27 +++++++++++++------ backend/src/note/note.service.ts | 4 +-- .../components/atoms/DrepClaimProfileCard.tsx | 2 +- .../src/components/atoms/DrepProfileCard.tsx | 4 +-- .../organisms/UpdateProfileStep2.tsx | 6 ++--- frontend/src/context/drepContext.tsx | 2 +- frontend/types/api.ts | 8 +++--- 11 files changed, 46 insertions(+), 39 deletions(-) diff --git a/backend/src/drep/drep.controller.ts b/backend/src/drep/drep.controller.ts index 1ebb6fbe..6a21d4d3 100644 --- a/backend/src/drep/drep.controller.ts +++ b/backend/src/drep/drep.controller.ts @@ -82,7 +82,7 @@ export class DrepController { if (drepId) { drep = await this.drepService.getSingleDrepViaID(drepId); - drepVoterId = drep.signature_drepVoterId; + drepVoterId = drep.signature_voterId; } else if (drepVoterId) { drep = await this.drepService.getSingleDrepViaVoterID(drepVoterId); } diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts index 51b6fe7a..e9bc07c1 100644 --- a/backend/src/drep/drep.service.ts +++ b/backend/src/drep/drep.service.ts @@ -60,7 +60,7 @@ export class DrepService { // if (query) { // const nameFilteredDReps = query ? await this.getDRepsByName(query) : []; // nameFilteredDRepViews = nameFilteredDReps.map( - // (drep) => drep.signature_drepVoterId, + // (drep) => drep.signature_voterId, // ); // } @@ -77,7 +77,7 @@ export class DrepService { if (campaignStatus) { const voltaireDReps = (await this.getAllDRepsVoltaire()) ?? []; - dRepViews = voltaireDReps.map((drep) => drep.signature_drepVoterId); + dRepViews = voltaireDReps.map((drep) => drep.signature_voterId); } const drepList = await this.getAllDRepsCexplorer( @@ -101,7 +101,7 @@ export class DrepService { const mergedDRepsData = drepList.data.map((drep) => { const voltaireDrep = voltaireDReps.find( - (voltaireDrep) => voltaireDrep.signature_drepVoterId === drep.view, + (voltaireDrep) => voltaireDrep.signature_voterId === drep.view, ); //account for voting options if ( @@ -252,7 +252,7 @@ export class DrepService { .getRepository('Drep') .createQueryBuilder('drep') .leftJoinAndSelect('drep.signatures', 'signature') - .where('signature.drepVoterId IN (:...views)', { views }) + .where('signature.voterId IN (:...views)', { views }) .getRawMany(); } @@ -272,7 +272,7 @@ export class DrepService { .where('drep.id = :drepId', { drepId }) .getRawMany(); let drepVoterId; - if (drep.length > 0) drepVoterId = drep[0].signature_drepVoterId; + if (drep.length > 0) drepVoterId = drep[0].signature_voterId; const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId); const drepDelegators = @@ -305,7 +305,7 @@ export class DrepService { .getRepository('Drep') .createQueryBuilder('drep') .leftJoinAndSelect('signature', 'signature', 'signature.drepId = drep.id') - .where('signature.drepVoterId = :drepVoterId', { drepVoterId }) + .where('signature.voterId = :drepVoterId', { drepVoterId }) .getRawMany(); const drepCexplorer = await this.getDrepCexplorerDetails(drepVoterId); const drepDelegators = @@ -590,7 +590,7 @@ export class DrepService { // 'delegators' visibility if (delegation) { visibilityConditions.push( - 'note.note_visibility = :delegators AND signature.drepVoterId = :drepVoterId', + 'note.note_visibility = :delegators AND signature.voterId = :drepVoterId', ); visibilityParams.delegators = 'delegators'; visibilityParams.drepVoterId = delegation.drep_view; @@ -599,7 +599,7 @@ export class DrepService { // 'myself' visibility if (stakeKeyBech32) { visibilityConditions.push( - 'note.note_visibility = :myself AND signature.drepStakeKey = :stakeKeyBech32', + 'note.note_visibility = :myself AND signature.stakeKey = :stakeKeyBech32', ); visibilityParams.myself = 'myself'; visibilityParams.stakeKeyBech32 = stakeKeyBech32; diff --git a/backend/src/entities/drep.entity.ts b/backend/src/entities/drep.entity.ts index c1d21efe..5f7c4a39 100644 --- a/backend/src/entities/drep.entity.ts +++ b/backend/src/entities/drep.entity.ts @@ -4,14 +4,6 @@ import { BaseEntity } from 'src/global'; @Entity() export class Drep extends BaseEntity { - @Column({ type: 'json', nullable: true }) - social: Record<string, any>; - @Column({ nullable: true }) - platform_statement: string; - @Column({ nullable: true }) - expertise: string; - @Column({ nullable: true }) - perspective: string; @OneToMany(() => Signature, (signature) => signature.drep) signatures: Signature[]; } diff --git a/backend/src/entities/note.entity.ts b/backend/src/entities/note.entity.ts index e0dc3a34..7d8b59e6 100644 --- a/backend/src/entities/note.entity.ts +++ b/backend/src/entities/note.entity.ts @@ -2,6 +2,7 @@ import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; import { Drep } from './drep.entity'; import { BaseEntity } from 'src/global'; import { Reaction } from './reaction.entity'; +import { Signature } from './signatures.entity'; @Entity() export class Note extends BaseEntity { @@ -15,7 +16,10 @@ export class Note extends BaseEntity { note_content: string; @ManyToOne(() => Drep, (drep) => drep.id) - voter: Drep; + drep: Drep; // This is the Drep/ drep page that the note belongs to/will be hosted by + + @ManyToOne(() => Signature, (signature) => signature.id) + author: Signature; // This is the Signature/ user that wrote the note @Column() note_visibility: string; diff --git a/backend/src/entities/signatures.entity.ts b/backend/src/entities/signatures.entity.ts index 54dd90d0..3ec4f8a7 100644 --- a/backend/src/entities/signatures.entity.ts +++ b/backend/src/entities/signatures.entity.ts @@ -3,16 +3,27 @@ import { Drep } from './drep.entity'; @Entity() export class Signature { + //can belong to a Drep or voter @PrimaryGeneratedColumn() id: number; - @ManyToOne(() => Drep, (drep) => drep.id, { onDelete: 'CASCADE' }) - drep: number; - @Column() - drepVoterId: string; - @Column() - drepStakeKey: string; + + @ManyToOne(() => Drep, (drep) => drep.id, { + nullable: true, + onDelete: 'CASCADE', + }) + drep: Drep; + + @Column({nullable: true}) + voterId: string; + + @Column({nullable: true}) + stakeKey: string; + @Column({ nullable: true, unique: false, default: null }) - drepSignature: string; + signature: string; + @Column({ nullable: true, unique: false, default: null }) - drepSignatureKey: string; + signatureKey: string; + + } diff --git a/backend/src/note/note.service.ts b/backend/src/note/note.service.ts index d228c012..372d6651 100644 --- a/backend/src/note/note.service.ts +++ b/backend/src/note/note.service.ts @@ -135,7 +135,7 @@ export class NoteService { // 'delegators' visibility if (delegation) { queryBuilder.orWhere( - 'note.note_visibility = :delegators AND signature.drepVoterId = :drepVoterId', + 'note.note_visibility = :delegators AND signature.voterId = :drepVoterId', { delegators: 'delegators', drepVoterId: delegation.drep_view, @@ -145,7 +145,7 @@ export class NoteService { // 'myself' visibility if (stakeKeyBech32) { queryBuilder.orWhere( - 'note.note_visibility = :myself AND signature.drepStakeKey = :stakeKeyBech32', + 'note.note_visibility = :myself AND signature.stakeKey = :stakeKeyBech32', { myself: 'myself', stakeKeyBech32: stakeKeyBech32, diff --git a/frontend/src/components/atoms/DrepClaimProfileCard.tsx b/frontend/src/components/atoms/DrepClaimProfileCard.tsx index adcd263e..b08970c6 100644 --- a/frontend/src/components/atoms/DrepClaimProfileCard.tsx +++ b/frontend/src/components/atoms/DrepClaimProfileCard.tsx @@ -165,7 +165,7 @@ const DrepClaimProfileCard = ({ )} </div> {(drep?.cexplorerDetails?.view == dRepIDBech32 || - drep?.signature_drepVoterId == dRepIDBech32) && + drep?.signature_voterId == dRepIDBech32) && isLoggedIn && ( <div className="flex max-w-prose flex-col gap-2"> <Link href={`/dreps/workflow/profile/new`}> diff --git a/frontend/src/components/atoms/DrepProfileCard.tsx b/frontend/src/components/atoms/DrepProfileCard.tsx index 4bdaf8df..3af38ef2 100644 --- a/frontend/src/components/atoms/DrepProfileCard.tsx +++ b/frontend/src/components/atoms/DrepProfileCard.tsx @@ -340,11 +340,11 @@ const DrepProfileCard = ({ drep, state }: { drep: any; state: boolean }) => { /> )} {(drep?.cexplorerDetails?.view == dRepIDBech32 || - drep?.signature_drepVoterId == dRepIDBech32) && + drep?.signature_voterId == dRepIDBech32) && renderUnsavedChanges()} </div> {(drep?.cexplorerDetails?.view == dRepIDBech32 || - drep?.signature_drepVoterId == dRepIDBech32) && ( + drep?.signature_voterId == dRepIDBech32) && ( <div className="flex max-w-prose flex-col gap-2"> <Button handleClick={ diff --git a/frontend/src/components/organisms/UpdateProfileStep2.tsx b/frontend/src/components/organisms/UpdateProfileStep2.tsx index 86b333fa..8514e0a9 100644 --- a/frontend/src/components/organisms/UpdateProfileStep2.tsx +++ b/frontend/src/components/organisms/UpdateProfileStep2.tsx @@ -52,10 +52,10 @@ const UpdateProfileStep2 = () => { drep = await getSingleDRepViaVoterId(dRepIDBech32); } setNewDrepId(drep.drep_id); - setValue('signature', drep.signature_drepSignature); + setValue('signature', drep.signature_signature); setSignature({ - signature: drep.signature_drepSignature, - key: drep.signature_drepSignatureKey, + signature: drep.signature_signature, + key: drep.signature_signatureKey, }); if (drep.drep_platform_statement) { setStep2Status('update'); diff --git a/frontend/src/context/drepContext.tsx b/frontend/src/context/drepContext.tsx index 14572485..9b640930 100644 --- a/frontend/src/context/drepContext.tsx +++ b/frontend/src/context/drepContext.tsx @@ -129,7 +129,7 @@ function DRepProvider(props: Props) { if (drep?.drep_id) { setNewDrepId(drep?.drep_id); } - if (drep?.signature_drepSignature) { + if (drep?.signature_signature) { setStep2Status('success'); } //check for metadata locally first diff --git a/frontend/types/api.ts b/frontend/types/api.ts index fc594256..bbf874a0 100644 --- a/frontend/types/api.ts +++ b/frontend/types/api.ts @@ -51,10 +51,10 @@ export type SingleDRep = { attachment_drepId?: number | null; attachment_commentId?: number | null; signature_id?: number | null; - signature_drepVoterId?: string | null; - signature_drepStakeKey?: string | null; - signature_drepSignature?: string | null; - signature_drepSignatureKey?: string | null; + signature_voterId?: string | null; + signature_stakeKey?: string | null; + signature_signature?: string | null; + signature_signatureKey?: string | null; signature_drepId?: number | null; cexplorerDetails: DRepCExplorerDetails; activity?: any[]; -- GitLab From 2bf0754251d2feb9e56921ecd2ed63ee4b490529 Mon Sep 17 00:00:00 2001 From: theeurbanlegend <mbuto152@gmail.com> Date: Mon, 16 Sep 2024 17:54:59 +0300 Subject: [PATCH 3/4] bug fix, added transition --- frontend/src/components/atoms/PageBanner.tsx | 89 +++++++++++--------- frontend/src/hooks/useGetNodeStatusQuery.ts | 3 +- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/atoms/PageBanner.tsx b/frontend/src/components/atoms/PageBanner.tsx index d93d4757..0ce0f7ba 100644 --- a/frontend/src/components/atoms/PageBanner.tsx +++ b/frontend/src/components/atoms/PageBanner.tsx @@ -1,64 +1,75 @@ 'use client'; +import { useDRepContext } from '@/context/drepContext'; import { useGetNodeStatusQuery } from '@/hooks/useGetNodeStatusQuery'; -import { Box, Typography } from '@mui/material'; +import { Box, Slide, Typography } from '@mui/material'; import { usePathname } from 'next/navigation'; import React, { useEffect, useState } from 'react'; const PageBanner = () => { - const { NodeStatus, isLoading, isFetching, isError } = useGetNodeStatusQuery(); + const { NodeStatus, isLoading, isFetching, isError, isFetchedAfterMount } = + useGetNodeStatusQuery(); const pathname = usePathname(); + const [showBanner, setShowBanner] = useState(false); const [nodeStats, setNodeStats] = useState(null); + const {currentLocale}=useDRepContext(); const dbNonDependentPages = [ - '/', - '/dreps', - '/dreps/workflow/profile/new', - '/dreps/workflow/profile/update', - ]; + `/${currentLocale}`, + `/${currentLocale}/dreps`, + `/${currentLocale}/dreps/workflow/profile/new`, + `/${currentLocale}/dreps/workflow/profile/update`, + ]; useEffect(() => { if (NodeStatus) { setNodeStats(NodeStatus); } }, [NodeStatus, isLoading, isFetching]); + useEffect(() => { + setShowBanner(renderCondition()); + }, [pathname, isFetchedAfterMount, isError, nodeStats]); + const renderCondition = () => { + return ( + isFetchedAfterMount && + !dbNonDependentPages.some((page) => pathname == page) && + (isError || (nodeStats && nodeStats?.behindBy >= 30)) + ); + }; const renderStatus = () => { if (!nodeStats && !isError) return '-'; if (nodeStats && !isError) { - return nodeStats?.behindBy > 30 ? 'Lagging' : 'Following'; + return nodeStats?.behindBy >= 30 ? 'Lagging' : 'Following'; } if (isError) return 'Offline'; }; - - if ( - dbNonDependentPages.some(page => pathname == page) || - (!isError && (!nodeStats || (nodeStats && nodeStats?.behindBy <= 30))) - ) - return null; + if (!showBanner) return null; return ( - <Box component={'div'} className="flex items-center justify-center gap-2"> - <div className="inline-flex items-center gap-1"> - <Typography>Epoch:</Typography> - <Typography>{nodeStats?.epoch_no || '-'}</Typography> - </div> - <div className="inline-flex items-center gap-1"> - <Typography>Slot:</Typography> - <Typography>{nodeStats?.epoch_slot_no || '-'}</Typography> - </div> - <div className="inline-flex items-center gap-1"> - <Typography>Status:</Typography> - <Typography - className={`${renderStatus() === 'Offline' && 'text-extra_red'}`} - > - {renderStatus()} - </Typography> - </div> - <div> - <Typography variant="caption"> - {nodeStats && - `Last updated ${new Date(nodeStats?.time).toLocaleString('en-US')}`} - </Typography> - </div> - </Box> + <Slide in={showBanner} appear exit direction="down"> + <Box component={'div'} className="flex items-center justify-center gap-2"> + <div className="inline-flex items-center gap-1"> + <Typography>Epoch:</Typography> + <Typography>{nodeStats?.epoch_no || '-'}</Typography> + </div> + <div className="inline-flex items-center gap-1"> + <Typography>Slot:</Typography> + <Typography>{nodeStats?.epoch_slot_no || '-'}</Typography> + </div> + <div className="inline-flex items-center gap-1"> + <Typography>Status:</Typography> + <Typography + className={`${renderStatus() === 'Offline' && 'text-extra_red'}`} + > + {renderStatus()} + </Typography> + </div> + <div> + <Typography variant="caption"> + {nodeStats && + `Last updated ${new Date(nodeStats?.time).toLocaleString('en-US')}`} + </Typography> + </div> + </Box> + </Slide> ); }; -export default PageBanner; \ No newline at end of file +export default PageBanner; diff --git a/frontend/src/hooks/useGetNodeStatusQuery.ts b/frontend/src/hooks/useGetNodeStatusQuery.ts index 05b8152b..60ceb7b4 100644 --- a/frontend/src/hooks/useGetNodeStatusQuery.ts +++ b/frontend/src/hooks/useGetNodeStatusQuery.ts @@ -2,7 +2,7 @@ import { getCurrentNodeStatus } from '@/services/requests/getCurrentNodeStatus'; import { useQuery } from 'react-query'; export const useGetNodeStatusQuery = () => { - const { data, isLoading, isFetching, isError, error , isSuccess} = useQuery({ + const { data, isLoading, isFetching, isError, error , isSuccess, isFetchedAfterMount} = useQuery({ queryKey: 'nodeStatus', queryFn: async () => getCurrentNodeStatus(), refetchInterval: 10000, @@ -12,6 +12,7 @@ export const useGetNodeStatusQuery = () => { NodeStatus: data, isLoading, isFetching, + isFetchedAfterMount, isError: !isSuccess || isError, error, }; -- GitLab From 75202a6d34295f28093848f48f20c8e3746173b3 Mon Sep 17 00:00:00 2001 From: Emmanuel Mutisya <emmanuelmutisya254@gmail.com> Date: Mon, 16 Sep 2024 23:26:26 +0300 Subject: [PATCH 4/4] account for deregistration when checking DRep register status --- backend/src/drep/drep.service.ts | 10 ++++++++-- backend/src/queries/drepRegistration.ts | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 backend/src/queries/drepRegistration.ts diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts index 60efa772..b40783ac 100644 --- a/backend/src/drep/drep.service.ts +++ b/backend/src/drep/drep.service.ts @@ -42,6 +42,7 @@ import { getDrepDelegatorsWithVotingPowerQuery, } from 'src/queries/drepDelegatorsWithVotingPower'; import { BlockfrostService } from 'src/blockfrost/blockfrost.service'; +import { drepRegistrationQuery } from 'src/queries/drepRegistration'; @Injectable() export class DrepService { @@ -967,8 +968,13 @@ export class DrepService { } async isDrepRegistered(voterId: string) { - const registration = await this.getDrepDateofRegistration(voterId); + const latestRegistration = await this.cexplorerService.manager.query( + drepRegistrationQuery, + [voterId], + ); + + const regDeposit = latestRegistration[0]?.deposit; - return !!registration ? true : false; + return regDeposit === null || regDeposit > 0; } } diff --git a/backend/src/queries/drepRegistration.ts b/backend/src/queries/drepRegistration.ts new file mode 100644 index 00000000..2e972879 --- /dev/null +++ b/backend/src/queries/drepRegistration.ts @@ -0,0 +1,7 @@ +export const drepRegistrationQuery = ` + SELECT dh.view,dr.deposit,dr.tx_id, + ROW_NUMBER() OVER (PARTITION BY drep_hash_id ORDER BY tx_id DESC) AS rn + FROM drep_hash dh + JOIN drep_registration dr ON dh.id = dr.drep_hash_id + where dh.view = $1 + limit 1`; -- GitLab